копирование
This commit is contained in:
@@ -101,6 +101,13 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
[]
|
||||
)
|
||||
|
||||
// Состояние для копирования/вставки
|
||||
const [copiedCells, setCopiedCells] = useState<{
|
||||
data: { row: number; col: number; value: string; rawValue: string }[]
|
||||
sourceSheet: SheetType
|
||||
isCut: boolean
|
||||
} | null>(null)
|
||||
|
||||
// React Query хуки для оптимизации API запросов
|
||||
const { data: latestFile, isLoading: isLoadingLatestFile } =
|
||||
useLatestFileForTemplate(templateId)
|
||||
@@ -354,6 +361,24 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
}
|
||||
}, [isEditing, editingValue, updateCellHighlights])
|
||||
|
||||
// Сбрасываем скопированные ячейки при смене активного листа
|
||||
useEffect(() => {
|
||||
if (copiedCells) {
|
||||
setCopiedCells(null)
|
||||
}
|
||||
}, [activeSheet])
|
||||
|
||||
// Автоматический сброс скопированных ячеек через 30 секунд
|
||||
useEffect(() => {
|
||||
if (!copiedCells) return
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
setCopiedCells(null)
|
||||
}, 30000) // 30 секунд
|
||||
|
||||
return () => clearTimeout(timeout)
|
||||
}, [copiedCells])
|
||||
|
||||
const startEditing = useCallback(() => {
|
||||
if (!selectedCell) return
|
||||
// Сбрасываем множественное выделение при начале редактирования
|
||||
@@ -427,6 +452,11 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
stopEditing(true)
|
||||
}
|
||||
|
||||
// Сбрасываем скопированные ячейки при навигации
|
||||
if (copiedCells) {
|
||||
setCopiedCells(null)
|
||||
}
|
||||
|
||||
const newRow = Math.max(
|
||||
0,
|
||||
Math.min(ROW_COUNT - 1, selectedCell.row + deltaRow)
|
||||
@@ -440,7 +470,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
// Сбрасываем множественное выделение при навигации
|
||||
setMultiSelection([])
|
||||
},
|
||||
[selectedCell, isEditing, stopEditing]
|
||||
[selectedCell, isEditing, stopEditing, copiedCells]
|
||||
)
|
||||
|
||||
// Функция для очистки множества выделенных ячеек
|
||||
@@ -460,6 +490,152 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
setMultiSelection([])
|
||||
}, [multiSelection, activeSheet, multiSheetEngine, updateRevision])
|
||||
|
||||
// Функция копирования выделенных ячеек
|
||||
const handleCopy = useCallback(
|
||||
async (isCut = false) => {
|
||||
if (!selectedCell && multiSelection.length === 0) return
|
||||
|
||||
const sheetName = SHEET_NAMES[activeSheet]
|
||||
let cellsToCopy: { row: number; col: number }[] = []
|
||||
|
||||
// Определяем какие ячейки копировать
|
||||
if (multiSelection.length > 0) {
|
||||
cellsToCopy = getAllCellsInSelections(multiSelection)
|
||||
} else if (selectedCell) {
|
||||
cellsToCopy = [selectedCell]
|
||||
}
|
||||
|
||||
if (cellsToCopy.length === 0) return
|
||||
|
||||
// Собираем данные ячеек
|
||||
const copiedData = cellsToCopy.map(({ row, col }) => ({
|
||||
row,
|
||||
col,
|
||||
value: String(multiSheetEngine.getCellValue(sheetName, row, col) || ''),
|
||||
rawValue: String(
|
||||
multiSheetEngine.getCellRawValue(sheetName, row, col) || ''
|
||||
),
|
||||
}))
|
||||
|
||||
// Сохраняем в state
|
||||
setCopiedCells({
|
||||
data: copiedData,
|
||||
sourceSheet: activeSheet,
|
||||
isCut,
|
||||
})
|
||||
|
||||
// Подготавливаем данные для буфера обмена в формате TSV (Tab-Separated Values)
|
||||
// Находим границы выделения
|
||||
const minRow = Math.min(...cellsToCopy.map(c => c.row))
|
||||
const maxRow = Math.max(...cellsToCopy.map(c => c.row))
|
||||
const minCol = Math.min(...cellsToCopy.map(c => c.col))
|
||||
const maxCol = Math.max(...cellsToCopy.map(c => c.col))
|
||||
|
||||
// Создаем двумерный массив для TSV
|
||||
const rows: string[][] = []
|
||||
for (let row = minRow; row <= maxRow; row++) {
|
||||
const rowData: string[] = []
|
||||
for (let col = minCol; col <= maxCol; col++) {
|
||||
const cellData = copiedData.find(c => c.row === row && c.col === col)
|
||||
rowData.push(cellData ? String(cellData.value) : '')
|
||||
}
|
||||
rows.push(rowData)
|
||||
}
|
||||
|
||||
// Конвертируем в TSV строку
|
||||
const tsvData = rows.map(row => row.join('\t')).join('\n')
|
||||
|
||||
// Копируем в буфер обмена
|
||||
try {
|
||||
await navigator.clipboard.writeText(tsvData)
|
||||
|
||||
// Если это вырезание, очищаем ячейки
|
||||
if (isCut) {
|
||||
cellsToCopy.forEach(({ row, col }) => {
|
||||
multiSheetEngine.setCellValueWithoutRecalc(sheetName, row, col, '')
|
||||
})
|
||||
setUserEdited(true)
|
||||
updateRevision()
|
||||
multiSheetEngine.debouncedRecalc()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка при копировании в буфер обмена:', error)
|
||||
}
|
||||
},
|
||||
[
|
||||
selectedCell,
|
||||
multiSelection,
|
||||
activeSheet,
|
||||
multiSheetEngine,
|
||||
updateRevision,
|
||||
]
|
||||
)
|
||||
|
||||
// Функция вставки из буфера обмена
|
||||
const handlePaste = useCallback(async () => {
|
||||
if (!selectedCell) return
|
||||
|
||||
try {
|
||||
const clipboardText = await navigator.clipboard.readText()
|
||||
|
||||
if (!clipboardText.trim()) return
|
||||
|
||||
const sheetName = SHEET_NAMES[activeSheet]
|
||||
const startRow = selectedCell.row
|
||||
const startCol = selectedCell.col
|
||||
|
||||
// Парсим TSV/CSV данные
|
||||
const rows = clipboardText.split('\n').filter(row => row !== '')
|
||||
const data: string[][] = rows.map(row => {
|
||||
// Поддерживаем как табуляции (TSV), так и запятые (CSV)
|
||||
if (row.includes('\t')) {
|
||||
return row.split('\t')
|
||||
} else {
|
||||
// Простой парсинг CSV (без учета кавычек с запятыми внутри)
|
||||
return row.split(',')
|
||||
}
|
||||
})
|
||||
|
||||
if (data.length === 0) return
|
||||
|
||||
// Вставляем данные
|
||||
let hasChanges = false
|
||||
for (let rowIndex = 0; rowIndex < data.length; rowIndex++) {
|
||||
for (let colIndex = 0; colIndex < data[rowIndex].length; colIndex++) {
|
||||
const targetRow = startRow + rowIndex
|
||||
const targetCol = startCol + colIndex
|
||||
|
||||
// Проверяем границы
|
||||
if (targetRow >= ROW_COUNT || targetCol >= COL_COUNT) continue
|
||||
|
||||
const cellValue = data[rowIndex][colIndex]
|
||||
|
||||
// Устанавливаем значение ячейки
|
||||
multiSheetEngine.setCellValueWithoutRecalc(
|
||||
sheetName,
|
||||
targetRow,
|
||||
targetCol,
|
||||
cellValue
|
||||
)
|
||||
hasChanges = true
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
setUserEdited(true)
|
||||
updateRevision()
|
||||
multiSheetEngine.debouncedRecalc()
|
||||
}
|
||||
|
||||
// Очищаем состояние копирования если это была операция вырезания
|
||||
if (copiedCells?.isCut) {
|
||||
setCopiedCells(null)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка при вставке из буфера обмена:', error)
|
||||
}
|
||||
}, [selectedCell, activeSheet, multiSheetEngine, updateRevision, copiedCells])
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (isEditing) return
|
||||
@@ -519,6 +695,27 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
return
|
||||
}
|
||||
|
||||
// Ctrl+C - копировать
|
||||
if (e.key === 'c' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault()
|
||||
handleCopy(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Ctrl+X - вырезать
|
||||
if (e.key === 'x' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault()
|
||||
handleCopy(true)
|
||||
return
|
||||
}
|
||||
|
||||
// Ctrl+V - вставить
|
||||
if (e.key === 'v' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault()
|
||||
handlePaste()
|
||||
return
|
||||
}
|
||||
|
||||
if (keyHandlers[e.key]) {
|
||||
e.preventDefault()
|
||||
keyHandlers[e.key]()
|
||||
@@ -538,6 +735,8 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
handleClearCell,
|
||||
multiSelection,
|
||||
handleClearMultipleCells,
|
||||
handleCopy,
|
||||
handlePaste,
|
||||
]
|
||||
)
|
||||
|
||||
@@ -547,6 +746,11 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
stopEditing(true)
|
||||
}
|
||||
|
||||
// Сбрасываем скопированные ячейки при клике на новую ячейку
|
||||
if (copiedCells) {
|
||||
setCopiedCells(null)
|
||||
}
|
||||
|
||||
// Если удерживается Ctrl (или Cmd на Mac), добавляем к множественному выделению
|
||||
if (event && (event.ctrlKey || event.metaKey)) {
|
||||
const newSelection = createSelectionFromRange(row, col, row, col)
|
||||
@@ -572,7 +776,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
setSelectedCell({ row, col })
|
||||
containerRefs.current[activeSheet]?.focus({ preventScroll: true })
|
||||
},
|
||||
[isEditing, stopEditing, activeSheet, selectedCell]
|
||||
[isEditing, stopEditing, activeSheet, selectedCell, copiedCells]
|
||||
)
|
||||
|
||||
// Новый обработчик для начала drag selection
|
||||
@@ -584,12 +788,17 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
// Если удерживаются модификаторы, не начинаем drag
|
||||
if (event.ctrlKey || event.metaKey || event.shiftKey) return
|
||||
|
||||
// Сбрасываем скопированные ячейки при drag selection
|
||||
if (copiedCells) {
|
||||
setCopiedCells(null)
|
||||
}
|
||||
|
||||
setIsDragging(true)
|
||||
setDragStart({ row, col })
|
||||
setMultiSelection([])
|
||||
setSelectedCell({ row, col })
|
||||
},
|
||||
[]
|
||||
[copiedCells]
|
||||
)
|
||||
|
||||
// Обработчик для drag selection
|
||||
@@ -983,6 +1192,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
onGridScroll={onGridScroll}
|
||||
handleColumnResize={handleColumnResize}
|
||||
mergedCells={mergedCells}
|
||||
copiedCells={copiedCells}
|
||||
/>
|
||||
{index === 0 && (
|
||||
<div
|
||||
|
||||
@@ -35,6 +35,8 @@ export const Cell = memo(
|
||||
style, // Добавляем style для react-window
|
||||
mergedCellInfo,
|
||||
highlightColor, // Цвет подсветки для формул
|
||||
isCopied, // Является ли ячейка скопированной
|
||||
isCut, // Является ли ячейка вырезанной
|
||||
}: CellProps) => {
|
||||
// Если ячейка объединена, но не является основной, не рендерим её
|
||||
if (mergedCellInfo?.isMerged && !mergedCellInfo.isMainCell) {
|
||||
@@ -67,6 +69,17 @@ export const Cell = memo(
|
||||
}
|
||||
}
|
||||
|
||||
// Стили для скопированных/вырезанных ячеек
|
||||
if (isCopied || isCut) {
|
||||
cellStyle = {
|
||||
...cellStyle,
|
||||
border: '2px dashed hsl(var(--primary))',
|
||||
backgroundColor: isCut
|
||||
? 'rgba(239, 68, 68, 0.1)'
|
||||
: 'rgba(34, 197, 94, 0.1)', // красноватый для вырезанных, зеленоватый для скопированных
|
||||
}
|
||||
}
|
||||
|
||||
// Отладка стилей для измененных ячеек
|
||||
if (isModified) {
|
||||
// console.log(`🎨 Стили для измененной ячейки ${rowIndex},${colIndex}:`, //{
|
||||
|
||||
@@ -20,6 +20,7 @@ export const CellRenderer = ({
|
||||
cachedData,
|
||||
mergedCells,
|
||||
highlightedCells,
|
||||
copiedCells,
|
||||
handleCellClick,
|
||||
handleCellDoubleClick,
|
||||
handleCellChange,
|
||||
@@ -121,6 +122,15 @@ export const CellRenderer = ({
|
||||
}
|
||||
: undefined
|
||||
|
||||
// Проверяем, является ли ячейка скопированной
|
||||
const isCopied =
|
||||
copiedCells &&
|
||||
copiedCells.sourceSheet === sheetType &&
|
||||
copiedCells.data.some(
|
||||
cell => cell.row === rowIndex && cell.col === columnIndex
|
||||
)
|
||||
const isCut = isCopied && copiedCells?.isCut
|
||||
|
||||
return (
|
||||
<Cell
|
||||
style={style}
|
||||
@@ -155,6 +165,8 @@ export const CellRenderer = ({
|
||||
activeCellRef={activeCellInputRef}
|
||||
mergedCellInfo={mergedCellProps}
|
||||
highlightColor={highlightColor}
|
||||
isCopied={isCopied}
|
||||
isCut={isCut}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ export const Spreadsheet = ({
|
||||
onGridScroll,
|
||||
handleColumnResize,
|
||||
mergedCells,
|
||||
copiedCells,
|
||||
}: SpreadsheetProps) => {
|
||||
const widths = columnWidths
|
||||
const cachedData = getCachedCellData(sheetType)
|
||||
@@ -53,6 +54,7 @@ export const Spreadsheet = ({
|
||||
cachedData,
|
||||
mergedCells,
|
||||
highlightedCells,
|
||||
copiedCells,
|
||||
multiSelectionActive:
|
||||
activeSheet === sheetType && multiSelection.length > 0,
|
||||
handleCellClick,
|
||||
|
||||
@@ -98,6 +98,8 @@ export interface CellProps {
|
||||
rowSpan?: number
|
||||
colSpan?: number
|
||||
}
|
||||
isCopied?: boolean // Является ли ячейка скопированной
|
||||
isCut?: boolean // Является ли ячейка вырезанной
|
||||
highlightColor?: string // Цвет подсветки для формул
|
||||
}
|
||||
|
||||
@@ -153,6 +155,11 @@ export interface CellRendererData {
|
||||
stopEditing: (save: boolean) => void
|
||||
setActiveSheet: (type: SheetType) => void
|
||||
activeCellInputRef: React.RefObject<HTMLInputElement | null>
|
||||
copiedCells?: {
|
||||
data: { row: number; col: number; value: string; rawValue: string }[]
|
||||
sourceSheet: SheetType
|
||||
isCut: boolean
|
||||
} | null
|
||||
}
|
||||
|
||||
export interface CellRendererProps {
|
||||
@@ -211,6 +218,11 @@ export interface SpreadsheetProps {
|
||||
newWidth: number
|
||||
) => void
|
||||
mergedCells?: MergedCell[] // Добавляем поддержку объединенных ячеек
|
||||
copiedCells?: {
|
||||
data: { row: number; col: number; value: string; rawValue: string }[]
|
||||
sourceSheet: SheetType
|
||||
isCut: boolean
|
||||
} | null
|
||||
}
|
||||
|
||||
export interface MergedCellInfo {
|
||||
|
||||
@@ -83,16 +83,6 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
// const handleTemplateUpdate = async (updatedTemplate: Template) => {
|
||||
// try {
|
||||
// await updateTemplate(updatedTemplate)
|
||||
// setSelectedTemplate(updatedTemplate)
|
||||
// } catch (err) {
|
||||
// console.error('Error updating template:', err)
|
||||
// alert('Ошибка при обновлении шаблона. Попробуйте еще раз.')
|
||||
// }
|
||||
// }
|
||||
|
||||
const handleDeleteTemplates = () => {
|
||||
templatesToDelete.forEach(t => deleteTemplate(t.id))
|
||||
setTemplatesToDelete([])
|
||||
|
||||
Reference in New Issue
Block a user