копирование

This commit is contained in:
2025-07-24 05:10:56 +03:00
parent ab96a271f0
commit 32e5cda438
6 changed files with 252 additions and 13 deletions

View File

@@ -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 запросов // React Query хуки для оптимизации API запросов
const { data: latestFile, isLoading: isLoadingLatestFile } = const { data: latestFile, isLoading: isLoadingLatestFile } =
useLatestFileForTemplate(templateId) useLatestFileForTemplate(templateId)
@@ -354,6 +361,24 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
} }
}, [isEditing, editingValue, updateCellHighlights]) }, [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(() => { const startEditing = useCallback(() => {
if (!selectedCell) return if (!selectedCell) return
// Сбрасываем множественное выделение при начале редактирования // Сбрасываем множественное выделение при начале редактирования
@@ -427,6 +452,11 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
stopEditing(true) stopEditing(true)
} }
// Сбрасываем скопированные ячейки при навигации
if (copiedCells) {
setCopiedCells(null)
}
const newRow = Math.max( const newRow = Math.max(
0, 0,
Math.min(ROW_COUNT - 1, selectedCell.row + deltaRow) Math.min(ROW_COUNT - 1, selectedCell.row + deltaRow)
@@ -440,7 +470,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
// Сбрасываем множественное выделение при навигации // Сбрасываем множественное выделение при навигации
setMultiSelection([]) setMultiSelection([])
}, },
[selectedCell, isEditing, stopEditing] [selectedCell, isEditing, stopEditing, copiedCells]
) )
// Функция для очистки множества выделенных ячеек // Функция для очистки множества выделенных ячеек
@@ -460,6 +490,152 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
setMultiSelection([]) setMultiSelection([])
}, [multiSelection, activeSheet, multiSheetEngine, updateRevision]) }, [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( const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => { (e: React.KeyboardEvent) => {
if (isEditing) return if (isEditing) return
@@ -519,6 +695,27 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
return 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]) { if (keyHandlers[e.key]) {
e.preventDefault() e.preventDefault()
keyHandlers[e.key]() keyHandlers[e.key]()
@@ -538,6 +735,8 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
handleClearCell, handleClearCell,
multiSelection, multiSelection,
handleClearMultipleCells, handleClearMultipleCells,
handleCopy,
handlePaste,
] ]
) )
@@ -547,6 +746,11 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
stopEditing(true) stopEditing(true)
} }
// Сбрасываем скопированные ячейки при клике на новую ячейку
if (copiedCells) {
setCopiedCells(null)
}
// Если удерживается Ctrl (или Cmd на Mac), добавляем к множественному выделению // Если удерживается Ctrl (или Cmd на Mac), добавляем к множественному выделению
if (event && (event.ctrlKey || event.metaKey)) { if (event && (event.ctrlKey || event.metaKey)) {
const newSelection = createSelectionFromRange(row, col, row, col) const newSelection = createSelectionFromRange(row, col, row, col)
@@ -572,7 +776,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
setSelectedCell({ row, col }) setSelectedCell({ row, col })
containerRefs.current[activeSheet]?.focus({ preventScroll: true }) containerRefs.current[activeSheet]?.focus({ preventScroll: true })
}, },
[isEditing, stopEditing, activeSheet, selectedCell] [isEditing, stopEditing, activeSheet, selectedCell, copiedCells]
) )
// Новый обработчик для начала drag selection // Новый обработчик для начала drag selection
@@ -584,12 +788,17 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
// Если удерживаются модификаторы, не начинаем drag // Если удерживаются модификаторы, не начинаем drag
if (event.ctrlKey || event.metaKey || event.shiftKey) return if (event.ctrlKey || event.metaKey || event.shiftKey) return
// Сбрасываем скопированные ячейки при drag selection
if (copiedCells) {
setCopiedCells(null)
}
setIsDragging(true) setIsDragging(true)
setDragStart({ row, col }) setDragStart({ row, col })
setMultiSelection([]) setMultiSelection([])
setSelectedCell({ row, col }) setSelectedCell({ row, col })
}, },
[] [copiedCells]
) )
// Обработчик для drag selection // Обработчик для drag selection
@@ -983,6 +1192,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
onGridScroll={onGridScroll} onGridScroll={onGridScroll}
handleColumnResize={handleColumnResize} handleColumnResize={handleColumnResize}
mergedCells={mergedCells} mergedCells={mergedCells}
copiedCells={copiedCells}
/> />
{index === 0 && ( {index === 0 && (
<div <div

View File

@@ -35,6 +35,8 @@ export const Cell = memo(
style, // Добавляем style для react-window style, // Добавляем style для react-window
mergedCellInfo, mergedCellInfo,
highlightColor, // Цвет подсветки для формул highlightColor, // Цвет подсветки для формул
isCopied, // Является ли ячейка скопированной
isCut, // Является ли ячейка вырезанной
}: CellProps) => { }: CellProps) => {
// Если ячейка объединена, но не является основной, не рендерим её // Если ячейка объединена, но не является основной, не рендерим её
if (mergedCellInfo?.isMerged && !mergedCellInfo.isMainCell) { 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) { if (isModified) {
// console.log(`🎨 Стили для измененной ячейки ${rowIndex},${colIndex}:`, //{ // console.log(`🎨 Стили для измененной ячейки ${rowIndex},${colIndex}:`, //{

View File

@@ -20,6 +20,7 @@ export const CellRenderer = ({
cachedData, cachedData,
mergedCells, mergedCells,
highlightedCells, highlightedCells,
copiedCells,
handleCellClick, handleCellClick,
handleCellDoubleClick, handleCellDoubleClick,
handleCellChange, handleCellChange,
@@ -121,6 +122,15 @@ export const CellRenderer = ({
} }
: undefined : undefined
// Проверяем, является ли ячейка скопированной
const isCopied =
copiedCells &&
copiedCells.sourceSheet === sheetType &&
copiedCells.data.some(
cell => cell.row === rowIndex && cell.col === columnIndex
)
const isCut = isCopied && copiedCells?.isCut
return ( return (
<Cell <Cell
style={style} style={style}
@@ -155,6 +165,8 @@ export const CellRenderer = ({
activeCellRef={activeCellInputRef} activeCellRef={activeCellInputRef}
mergedCellInfo={mergedCellProps} mergedCellInfo={mergedCellProps}
highlightColor={highlightColor} highlightColor={highlightColor}
isCopied={isCopied}
isCut={isCut}
/> />
) )
} }

View File

@@ -38,6 +38,7 @@ export const Spreadsheet = ({
onGridScroll, onGridScroll,
handleColumnResize, handleColumnResize,
mergedCells, mergedCells,
copiedCells,
}: SpreadsheetProps) => { }: SpreadsheetProps) => {
const widths = columnWidths const widths = columnWidths
const cachedData = getCachedCellData(sheetType) const cachedData = getCachedCellData(sheetType)
@@ -53,6 +54,7 @@ export const Spreadsheet = ({
cachedData, cachedData,
mergedCells, mergedCells,
highlightedCells, highlightedCells,
copiedCells,
multiSelectionActive: multiSelectionActive:
activeSheet === sheetType && multiSelection.length > 0, activeSheet === sheetType && multiSelection.length > 0,
handleCellClick, handleCellClick,

View File

@@ -98,6 +98,8 @@ export interface CellProps {
rowSpan?: number rowSpan?: number
colSpan?: number colSpan?: number
} }
isCopied?: boolean // Является ли ячейка скопированной
isCut?: boolean // Является ли ячейка вырезанной
highlightColor?: string // Цвет подсветки для формул highlightColor?: string // Цвет подсветки для формул
} }
@@ -153,6 +155,11 @@ export interface CellRendererData {
stopEditing: (save: boolean) => void stopEditing: (save: boolean) => void
setActiveSheet: (type: SheetType) => void setActiveSheet: (type: SheetType) => void
activeCellInputRef: React.RefObject<HTMLInputElement | null> activeCellInputRef: React.RefObject<HTMLInputElement | null>
copiedCells?: {
data: { row: number; col: number; value: string; rawValue: string }[]
sourceSheet: SheetType
isCut: boolean
} | null
} }
export interface CellRendererProps { export interface CellRendererProps {
@@ -211,6 +218,11 @@ export interface SpreadsheetProps {
newWidth: number newWidth: number
) => void ) => void
mergedCells?: MergedCell[] // Добавляем поддержку объединенных ячеек mergedCells?: MergedCell[] // Добавляем поддержку объединенных ячеек
copiedCells?: {
data: { row: number; col: number; value: string; rawValue: string }[]
sourceSheet: SheetType
isCut: boolean
} | null
} }
export interface MergedCellInfo { export interface MergedCellInfo {

View File

@@ -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 = () => { const handleDeleteTemplates = () => {
templatesToDelete.forEach(t => deleteTemplate(t.id)) templatesToDelete.forEach(t => deleteTemplate(t.id))
setTemplatesToDelete([]) setTemplatesToDelete([])