diff --git a/src/components/DualSpreadsheet/DualSpreadsheet.tsx b/src/components/DualSpreadsheet/DualSpreadsheet.tsx index 140f819..39ea9e3 100644 --- a/src/components/DualSpreadsheet/DualSpreadsheet.tsx +++ b/src/components/DualSpreadsheet/DualSpreadsheet.tsx @@ -4,16 +4,17 @@ import { useMultiSheetEngine } from '../../hooks/useMultiSheetEngine'; interface CellData { value: string; isSelected: boolean; - isModified: boolean; // Флаг для отслеживания изменений - templateValue?: string; // Исходное значение из шаблона + isModified: boolean; + templateValue?: string; } interface DualSpreadsheetProps { - templateData?: Record>; // Данные для обоих листов + templateData?: Record>; } +type SheetType = 'report' | 'calculations'; + const DualSpreadsheet: FC = ({ templateData = {} }) => { - // Используем мультилистовый движок const multiSheetEngine = useMultiSheetEngine({ sheets: [ { name: 'Report', rows: 20, cols: 10 }, @@ -22,107 +23,115 @@ const DualSpreadsheet: FC = ({ templateData = {} }) => { initialData: templateData || {} }); - const [activeSheet, setActiveSheet] = useState<'report' | 'calculations'>('report'); - const [reportCells, setReportCells] = useState(() => { - return Array(20).fill(null).map(() => + const [activeSheet, setActiveSheet] = useState('report'); + + // Объединенные состояния для обеих таблиц + const [sheetData, setSheetData] = useState>({ + report: Array(20).fill(null).map(() => Array(10).fill(null).map(() => ({ value: '', isSelected: false, isModified: false, templateValue: '' })) - ); - }); - - const [calculationsCells, setCalculationsCells] = useState(() => { - return Array(20).fill(null).map(() => + ), + calculations: Array(20).fill(null).map(() => Array(10).fill(null).map(() => ({ value: '', isSelected: false, isModified: false })) - ); + ) }); const [selectedCell, setSelectedCell] = useState<{row: number, col: number} | null>(null); const [isEditing, setIsEditing] = useState(false); - const [editingValue, setEditingValue] = useState(''); const [formulaBarValue, setFormulaBarValue] = useState(''); - const [reportColumnWidths, setReportColumnWidths] = useState(Array(10).fill(96)); - const [calculationsColumnWidths, setCalculationsColumnWidths] = useState(Array(10).fill(96)); - const [rowHeights, setRowHeights] = useState(Array(20).fill(32)); + const [hasEditingChanges, setHasEditingChanges] = useState(false); + const [initialEditingValue, setInitialEditingValue] = useState(''); - const reportContainerRef = useRef(null); - const calculationsContainerRef = useRef(null); - const reportInputRefs = useRef<(HTMLInputElement | null)[][]>([]); - const calculationsInputRefs = useRef<(HTMLInputElement | null)[][]>([]); - const reportFormulaBarRef = useRef(null); - const calculationsFormulaBarRef = useRef(null); + // Объединенные размеры колонок + const [columnWidths, setColumnWidths] = useState>({ + report: Array(10).fill(96), + calculations: Array(10).fill(96) + }); + + const [rowHeights] = useState(Array(20).fill(32)); + + // Объединенные refs + const containerRefs = useRef>({ + report: null, + calculations: null + }); + + const inputRefs = useRef>({ + report: [], + calculations: [] + }); + + const formulaBarRefs = useRef>({ + report: null, + calculations: null + }); - // Инициализируем refs для всех input + // Инициализация refs useEffect(() => { - reportInputRefs.current = Array(20).fill(null).map(() => Array(10).fill(null)); - calculationsInputRefs.current = Array(20).fill(null).map(() => Array(10).fill(null)); + inputRefs.current.report = Array(20).fill(null).map(() => Array(10).fill(null)); + inputRefs.current.calculations = Array(20).fill(null).map(() => Array(10).fill(null)); }, []); - // Загружаем шаблонные данные в отчет + // Загрузка шаблонных данных useEffect(() => { - if (templateData && templateData.Report && Object.keys(templateData.Report).length > 0) { - setReportCells(prev => { - const newCells = [...prev]; - for (let row = 0; row < 20; row++) { - for (let col = 0; col < 10; col++) { - const cellName = getColumnLabel(col) + (row + 1); + if (templateData?.Report && Object.keys(templateData.Report).length > 0) { + setSheetData(prev => ({ + ...prev, + report: prev.report.map((row, rowIndex) => + row.map((cell, colIndex) => { + const cellName = getColumnLabel(colIndex) + (rowIndex + 1); const templateValue = templateData.Report[cellName]; - if (templateValue !== undefined) { - newCells[row][col] = { - ...newCells[row][col], - value: String(templateValue), - templateValue: String(templateValue) - }; - } - } - } - return newCells; - }); + return templateValue !== undefined ? { + ...cell, + value: String(templateValue), + templateValue: String(templateValue) + } : cell; + }) + ) + })); } }, [templateData]); // Синхронизация с движком useEffect(() => { - const reportSheetCells = multiSheetEngine.sheetCells['Report']; - const calculationsSheetCells = multiSheetEngine.sheetCells['Calculations']; - - if (reportSheetCells) { - setReportCells(prev => { - const newCells = [...prev]; - for (let row = 0; row < 20; row++) { - for (let col = 0; col < 10; col++) { - const rawValue = multiSheetEngine.getCellRawValue('Report', row, col); - if (rawValue !== newCells[row][col].value) { - newCells[row][col] = { ...newCells[row][col], value: rawValue }; - } + const updateSheetData = (sheetType: SheetType, sheetName: string) => { + const sheetCells = multiSheetEngine.sheetCells[sheetName]; + if (sheetCells) { + setSheetData(prev => { + const newData = prev[sheetType].map((row, rowIndex) => + row.map((cell, colIndex) => { + const rawValue = multiSheetEngine.getCellRawValue(sheetName, rowIndex, colIndex); + return rawValue !== cell.value ? { ...cell, value: rawValue } : cell; + }) + ); + + // Проверяем, действительно ли есть изменения + const hasChanges = newData.some((row, rowIndex) => + row.some((cell, colIndex) => cell.value !== prev[sheetType][rowIndex][colIndex].value) + ); + + if (hasChanges) { + return { ...prev, [sheetType]: newData }; } - } - return newCells; - }); + return prev; + }); + } + }; + + // Обновляем только если не в процессе пересчета + if (!multiSheetEngine.isCalculating) { + updateSheetData('report', 'Report'); + updateSheetData('calculations', 'Calculations'); } - - if (calculationsSheetCells) { - setCalculationsCells(prev => { - const newCells = [...prev]; - for (let row = 0; row < 20; row++) { - for (let col = 0; col < 10; col++) { - const rawValue = multiSheetEngine.getCellRawValue('Calculations', row, col); - if (rawValue !== newCells[row][col].value) { - newCells[row][col] = { ...newCells[row][col], value: rawValue }; - } - } - } - return newCells; - }); - } - }, [multiSheetEngine.sheetCells, multiSheetEngine.getCellRawValue]); + }, [multiSheetEngine.sheetCells, multiSheetEngine.isCalculating]); // Синхронизация formula bar useEffect(() => { @@ -135,48 +144,38 @@ const DualSpreadsheet: FC = ({ templateData = {} }) => { } }, [selectedCell, activeSheet, multiSheetEngine.sheetCells, multiSheetEngine.getCellRawValue]); - const getColumnLabel = (index: number) => { - return String.fromCharCode(65 + index); - }; + const getColumnLabel = (index: number) => String.fromCharCode(65 + index); + + const getCurrentSheetName = () => activeSheet === 'report' ? 'Report' : 'Calculations'; - // Получение измененных ячеек для бэкенда const getChangedCells = () => { - // Возвращаем только измененные ячейки листа "Отчет" const reportChanges: Record = {}; - // Проверяем каждую ячейку в отчете на изменения - for (let row = 0; row < 20; row++) { - for (let col = 0; col < 10; col++) { - const cell = reportCells[row][col]; + sheetData.report.forEach((row, rowIndex) => { + row.forEach((cell, colIndex) => { if (cell.isModified) { - const cellName = getColumnLabel(col) + (row + 1); - const rawValue = multiSheetEngine.getCellRawValue('Report', row, col); - const calculatedValue = multiSheetEngine.getCellValue('Report', row, col); + const cellName = getColumnLabel(colIndex) + (rowIndex + 1); + const rawValue = multiSheetEngine.getCellRawValue('Report', rowIndex, colIndex); + const calculatedValue = multiSheetEngine.getCellValue('Report', rowIndex, colIndex); - reportChanges[cellName] = { - rawValue, - calculatedValue - }; + reportChanges[cellName] = { rawValue, calculatedValue }; } - } - } + }); + }); return reportChanges; }; - // Экспорт функции для родительского компонента useEffect(() => { if (typeof window !== 'undefined') { (window as any).getChangedCells = getChangedCells; } }, [multiSheetEngine]); - // Обработчик экспорта изменений const handleExportChanges = () => { const changes = getChangedCells(); console.log('Измененные ячейки отчета для отправки на бэкенд:', changes); - // Здесь можно добавить реальную отправку на бэкенд const changedCount = Object.keys(changes).length; if (changedCount > 0) { alert(`Изменения экспортированы!\nИзмененных ячеек в отчете: ${changedCount}\n\nПроверьте консоль для подробностей.`); @@ -185,37 +184,20 @@ const DualSpreadsheet: FC = ({ templateData = {} }) => { } }; - const getCurrentSheetName = () => { - return activeSheet === 'report' ? 'Report' : 'Calculations'; - }; - - const getCurrentCells = () => { - return activeSheet === 'report' ? reportCells : calculationsCells; - }; - - const setCurrentCells = (updater: (prev: CellData[][]) => CellData[][]) => { - if (activeSheet === 'report') { - setReportCells(updater); - } else { - setCalculationsCells(updater); - } - }; - const handleCellClick = (row: number, col: number) => { setSelectedCell({ row, col }); setIsEditing(false); - const containerRef = activeSheet === 'report' ? reportContainerRef : calculationsContainerRef; - containerRef.current?.focus(); + containerRefs.current[activeSheet]?.focus(); }; const handleCellDoubleClick = (row: number, col: number) => { setSelectedCell({ row, col }); - const cells = getCurrentCells(); - setEditingValue(cells[row][col].value); + const currentValue = sheetData[activeSheet][row][col].value; + setInitialEditingValue(currentValue); + setHasEditingChanges(false); setIsEditing(true); setTimeout(() => { - const inputRefs = activeSheet === 'report' ? reportInputRefs : calculationsInputRefs; - const input = inputRefs.current[row]?.[col]; + const input = inputRefs.current[activeSheet][row]?.[col]; if (input) { input.focus(); input.setSelectionRange(input.value.length, input.value.length); @@ -223,70 +205,48 @@ const DualSpreadsheet: FC = ({ templateData = {} }) => { }, 10); }; - const handleCellChange = (row: number, col: number, value: string) => { + const handleCellChange = (row: number, col: number, value: string, shouldRecalc: boolean = false) => { const sheetName = getCurrentSheetName(); - // Для отчета отслеживаем изменения относительно шаблона - if (activeSheet === 'report') { - setReportCells(prev => { - const newCells = [...prev]; - const cell = newCells[row][col]; - - // Определяем, изменилось ли значение относительно шаблона - // Пользователь может добавить формулу поверх простого значения или очистить ячейку - const isModified = value !== (cell.templateValue || ''); - - newCells[row][col] = { - ...cell, - value, - isModified - }; - return newCells; - }); - } else { - setCalculationsCells(prev => { - const newCells = [...prev]; - newCells[row][col] = { ...newCells[row][col], value }; - return newCells; - }); + // Отслеживаем изменения во время редактирования + if (isEditing && selectedCell?.row === row && selectedCell?.col === col) { + setHasEditingChanges(value !== initialEditingValue); } - // Обновляем formula bar + setSheetData(prev => ({ + ...prev, + [activeSheet]: prev[activeSheet].map((rowData, rowIndex) => + rowIndex === row ? rowData.map((cell, colIndex) => { + if (colIndex === col) { + const isModified = activeSheet === 'report' + ? value !== (cell.templateValue || '') + : false; + return { ...cell, value, isModified }; + } + return cell; + }) : rowData + ) + })); + if (selectedCell && selectedCell.row === row && selectedCell.col === col) { setFormulaBarValue(value); } - // Обновляем движок - multiSheetEngine.setCellValue(sheetName, row, col, value); + if (shouldRecalc) { + multiSheetEngine.setCellValue(sheetName, row, col, value); + } else { + multiSheetEngine.setCellValueWithoutRecalc(sheetName, row, col, value); + } }; - // Функция для восстановления шаблонного значения const restoreTemplateValue = (row: number, col: number) => { if (activeSheet !== 'report') return; - setReportCells(prev => { - const newCells = [...prev]; - const cell = newCells[row][col]; - - if (cell.templateValue) { - const restoredValue = cell.templateValue; - newCells[row][col] = { - ...cell, - value: restoredValue, - isModified: false - }; - - // Обновляем движок с восстановленным значением - multiSheetEngine.setCellValue('Report', row, col, restoredValue); - - // Обновляем formula bar - if (selectedCell && selectedCell.row === row && selectedCell.col === col) { - setFormulaBarValue(restoredValue); - } - } - - return newCells; - }); + const cell = sheetData.report[row][col]; + if (cell.templateValue) { + const restoredValue = cell.templateValue; + handleCellChange(row, col, restoredValue, true); + } }; const moveSelection = (deltaRow: number, deltaCol: number) => { @@ -301,14 +261,14 @@ const DualSpreadsheet: FC = ({ templateData = {} }) => { const startEditing = (focusTarget?: 'cell' | 'formula') => { if (!selectedCell) return; - const cells = getCurrentCells(); - setEditingValue(cells[selectedCell.row][selectedCell.col].value); + const currentValue = sheetData[activeSheet][selectedCell.row][selectedCell.col].value; + setInitialEditingValue(currentValue); + setHasEditingChanges(false); setIsEditing(true); if (focusTarget === 'cell') { setTimeout(() => { - const inputRefs = activeSheet === 'report' ? reportInputRefs : calculationsInputRefs; - const input = inputRefs.current[selectedCell.row]?.[selectedCell.col]; + const input = inputRefs.current[activeSheet][selectedCell.row]?.[selectedCell.col]; if (input) { input.focus(); input.select(); @@ -316,35 +276,20 @@ const DualSpreadsheet: FC = ({ templateData = {} }) => { }, 0); } else if (focusTarget === 'formula') { setTimeout(() => { - const formulaBarRef = activeSheet === 'report' ? reportFormulaBarRef : calculationsFormulaBarRef; - if (formulaBarRef.current) { - formulaBarRef.current.focus(); - formulaBarRef.current.select(); - } + formulaBarRefs.current[activeSheet]?.focus(); + formulaBarRefs.current[activeSheet]?.select(); }, 0); } }; const stopEditing = (save: boolean = true) => { - if (!save && selectedCell) { - const cells = getCurrentCells(); - const oldValue = editingValue; - - setCurrentCells(prev => { - const newCells = [...prev]; - newCells[selectedCell.row][selectedCell.col] = { - ...newCells[selectedCell.row][selectedCell.col], - value: oldValue - }; - return newCells; - }); - - setFormulaBarValue(oldValue); - multiSheetEngine.setCellValue(getCurrentSheetName(), selectedCell.row, selectedCell.col, oldValue); + if (save && selectedCell && (hasEditingChanges || formulaBarValue.startsWith('='))) { + multiSheetEngine.debouncedRecalc(); } setIsEditing(false); - const containerRef = activeSheet === 'report' ? reportContainerRef : calculationsContainerRef; - containerRef.current?.focus(); + setHasEditingChanges(false); + setInitialEditingValue(''); + containerRefs.current[activeSheet]?.focus(); }; const handleKeyDown = (e: React.KeyboardEvent) => { @@ -362,72 +307,107 @@ const DualSpreadsheet: FC = ({ templateData = {} }) => { return; } - switch (e.key) { - case 'ArrowUp': - e.preventDefault(); - moveSelection(-1, 0); - break; - case 'ArrowDown': - e.preventDefault(); - moveSelection(1, 0); - break; - case 'ArrowLeft': - e.preventDefault(); - moveSelection(0, -1); - break; - case 'ArrowRight': - e.preventDefault(); - moveSelection(0, 1); - break; - case 'Enter': - e.preventDefault(); - startEditing('cell'); - break; - case 'F2': - e.preventDefault(); - startEditing('cell'); - break; - case 'Delete': - case 'Backspace': - e.preventDefault(); - handleCellChange(selectedCell.row, selectedCell.col, ''); - break; - default: - if (e.key === 'z' && (e.ctrlKey || e.metaKey) && activeSheet === 'report') { - e.preventDefault(); - restoreTemplateValue(selectedCell.row, selectedCell.col); - } else if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) { - e.preventDefault(); - handleCellChange(selectedCell.row, selectedCell.col, e.key); - startEditing('cell'); - } - break; + const keyHandlers: Record void> = { + 'ArrowUp': () => moveSelection(-1, 0), + 'ArrowDown': () => moveSelection(1, 0), + 'ArrowLeft': () => moveSelection(0, -1), + 'ArrowRight': () => moveSelection(0, 1), + 'Enter': () => startEditing('cell'), + 'F2': () => startEditing('cell'), + 'Delete': () => handleCellChange(selectedCell.row, selectedCell.col, '', true), + 'Backspace': () => handleCellChange(selectedCell.row, selectedCell.col, '', true), + }; + + if (keyHandlers[e.key]) { + e.preventDefault(); + keyHandlers[e.key](); + } else if (e.key === 'z' && (e.ctrlKey || e.metaKey) && activeSheet === 'report') { + e.preventDefault(); + restoreTemplateValue(selectedCell.row, selectedCell.col); + } else if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) { + e.preventDefault(); + handleCellChange(selectedCell.row, selectedCell.col, e.key, false); + startEditing('cell'); } }; - const handleReportResize = (colIndex: number, newWidth: number) => { - setReportColumnWidths(prev => { - const newWidths = [...prev]; - newWidths[colIndex] = Math.max(60, newWidth); - return newWidths; - }); + const handleColumnResize = (colIndex: number, newWidth: number) => { + setColumnWidths(prev => ({ + ...prev, + [activeSheet]: prev[activeSheet].map((width, index) => + index === colIndex ? Math.max(60, newWidth) : width + ) + })); }; - const handleCalculationsResize = (colIndex: number, newWidth: number) => { - setCalculationsColumnWidths(prev => { - const newWidths = [...prev]; - newWidths[colIndex] = Math.max(60, newWidth); - return newWidths; - }); - }; + // Универсальный компонент для Formula Bar + const FormulaBar = ({ sheetType }: { sheetType: SheetType }) => ( +
+
+
+ {selectedCell && activeSheet === sheetType ? + `${getColumnLabel(selectedCell.col)}${selectedCell.row + 1}` : ''} +
+ {multiSheetEngine.isCalculating && ( +
Вычисляется...
+ )} +
+ formulaBarRefs.current[sheetType] = el} + type="text" + className={`w-full px-2 py-1 text-sm border rounded focus:outline-none ${ + isEditing && activeSheet === sheetType + ? 'border-blue-400 focus:ring-2 focus:ring-blue-300' + : 'border-gray-300 focus:ring-2 focus:ring-blue-500' + }`} + placeholder="Введите формулу..." + value={activeSheet === sheetType ? formulaBarValue : ''} + onChange={(e) => { + if (activeSheet === sheetType) { + const newValue = e.target.value; + setFormulaBarValue(newValue); + + // Отслеживаем изменения для formula bar + if (isEditing) { + setHasEditingChanges(newValue !== initialEditingValue); + } + + if (selectedCell) { + handleCellChange(selectedCell.row, selectedCell.col, newValue, false); + } + } + }} + onFocus={() => { + if (selectedCell && activeSheet === sheetType) { + startEditing('formula'); + } + }} + onBlur={() => stopEditing(true)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + stopEditing(true); + containerRefs.current[sheetType]?.focus(); + } else if (e.key === 'Escape') { + e.preventDefault(); + stopEditing(false); + } + }} + /> +
+
+
+ ); - const renderReportSpreadsheet = () => { - const cells = reportCells; - const sheetName = 'Report'; + // Универсальный рендер таблицы + const renderSpreadsheet = (sheetType: SheetType) => { + const cells = sheetData[sheetType]; + const sheetName = sheetType === 'report' ? 'Report' : 'Calculations'; + const widths = columnWidths[sheetType]; return (
- a + b, 0)}px` }}> +
a + b, 0)}px` }}> @@ -435,7 +415,7 @@ const DualSpreadsheet: FC = ({ templateData = {} }) => { - ))} - - ))} - -
{getColumnLabel(i)}
= ({ templateData = {} }) => { onMouseDown={(e) => { e.preventDefault(); const startX = e.clientX; - const startWidth = reportColumnWidths[i]; + const startWidth = widths[i]; const handleMouseMove = (e: MouseEvent) => { const newWidth = startWidth + (e.clientX - startX); - handleReportResize(i, newWidth); + handleColumnResize(i, newWidth); }; const handleMouseUp = () => { @@ -473,18 +453,16 @@ const DualSpreadsheet: FC = ({ templateData = {} }) => {
= ({ templateData = {} }) => { : {}) }} onClick={() => { - if (isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex && activeSheet === 'report') { + if (isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex && activeSheet === sheetType) { return; } - if (activeSheet !== 'report') { - setActiveSheet('report'); + if (activeSheet !== sheetType) { + setActiveSheet(sheetType); } handleCellClick(rowIndex, colIndex); }} onDoubleClick={() => { - if (activeSheet !== 'report') { - setActiveSheet('report'); + if (activeSheet !== sheetType) { + setActiveSheet(sheetType); } handleCellDoubleClick(rowIndex, colIndex); }} @@ -510,27 +488,31 @@ const DualSpreadsheet: FC = ({ templateData = {} }) => { { - handleCellChange(rowIndex, colIndex, e.target.value); - setFormulaBarValue(e.target.value); + const newValue = e.target.value; + handleCellChange(rowIndex, colIndex, newValue, false); + setFormulaBarValue(newValue); + + // Отслеживаем изменения в ячейке + if (isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex && activeSheet === sheetType) { + setHasEditingChanges(newValue !== initialEditingValue); + } }} onFocus={(e) => { - if (activeSheet !== 'report') { - setActiveSheet('report'); + if (activeSheet !== sheetType) { + setActiveSheet(sheetType); } setSelectedCell({ row: rowIndex, col: colIndex }); - if (!isEditing || activeSheet !== 'report') { + if (!isEditing || activeSheet !== sheetType) { (e.target as HTMLInputElement).blur(); } }} @@ -546,152 +528,14 @@ const DualSpreadsheet: FC = ({ templateData = {} }) => { } }} onMouseUp={(e) => { - if (isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex && activeSheet === 'report') { + if (isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex && activeSheet === sheetType) { e.stopPropagation(); } }} - readOnly={!isEditing || selectedCell?.row !== rowIndex || selectedCell?.col !== colIndex || activeSheet !== 'report'} + readOnly={!isEditing || selectedCell?.row !== rowIndex || selectedCell?.col !== colIndex || activeSheet !== sheetType} ref={(el) => { - if (reportInputRefs.current[rowIndex]) { - reportInputRefs.current[rowIndex][colIndex] = el; - } - }} - /> -
-
- ); - }; - - const renderCalculationsSpreadsheet = () => { - const cells = calculationsCells; - const sheetName = 'Calculations'; - - return ( -
- a + b, 0)}px` }}> - - - - {Array.from({ length: 10 }, (_, i) => ( - - ))} - - - - {cells.map((row, rowIndex) => ( - - - {row.map((cell, colIndex) => ( -
- {getColumnLabel(i)} -
{ - e.preventDefault(); - const startX = e.clientX; - const startWidth = calculationsColumnWidths[i]; - - const handleMouseMove = (e: MouseEvent) => { - const newWidth = startWidth + (e.clientX - startX); - handleCalculationsResize(i, newWidth); - }; - - const handleMouseUp = () => { - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); - }; - - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleMouseUp); - }} - /> -
- {rowIndex + 1} - { - if (isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex && activeSheet === 'calculations') { - return; - } - if (activeSheet !== 'calculations') { - setActiveSheet('calculations'); - } - handleCellClick(rowIndex, colIndex); - }} - onDoubleClick={() => { - if (activeSheet !== 'calculations') { - setActiveSheet('calculations'); - } - handleCellDoubleClick(rowIndex, colIndex); - }} - > - { - handleCellChange(rowIndex, colIndex, e.target.value); - setFormulaBarValue(e.target.value); - }} - onFocus={(e) => { - if (activeSheet !== 'calculations') { - setActiveSheet('calculations'); - } - setSelectedCell({ row: rowIndex, col: colIndex }); - if (!isEditing || activeSheet !== 'calculations') { - (e.target as HTMLInputElement).blur(); - } - }} - onBlur={() => stopEditing(true)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault(); - stopEditing(true); - moveSelection(1, 0); - } else if (e.key === 'Escape') { - e.preventDefault(); - stopEditing(false); - } - }} - onMouseUp={(e) => { - if (isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex && activeSheet === 'calculations') { - e.stopPropagation(); - } - }} - readOnly={!isEditing || selectedCell?.row !== rowIndex || selectedCell?.col !== colIndex || activeSheet !== 'calculations'} - ref={(el) => { - if (calculationsInputRefs.current[rowIndex]) { - calculationsInputRefs.current[rowIndex][colIndex] = el; + if (inputRefs.current[sheetType][rowIndex]) { + inputRefs.current[sheetType][rowIndex][colIndex] = el; } }} /> @@ -726,14 +570,12 @@ const DualSpreadsheet: FC = ({ templateData = {} }) => { -
- -
+ @@ -744,66 +586,15 @@ const DualSpreadsheet: FC = ({ templateData = {} }) => {

Отчет

- - {/* Formula bar для отчета */} -
-
-
- {selectedCell && activeSheet === 'report' ? `${getColumnLabel(selectedCell.col)}${selectedCell.row + 1}` : ''} -
- {multiSheetEngine.isCalculating && ( -
- Вычисляется... -
- )} -
- { - if (activeSheet === 'report') { - setFormulaBarValue(e.target.value); - if (selectedCell) { - handleCellChange(selectedCell.row, selectedCell.col, e.target.value); - } - } - }} - onFocus={() => { - if (selectedCell && activeSheet === 'report') { - startEditing('formula'); - } - }} - onBlur={() => stopEditing(true)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault(); - stopEditing(true); - reportContainerRef.current?.focus(); - } else if (e.key === 'Escape') { - e.preventDefault(); - stopEditing(false); - } - }} - /> -
-
-
- +
containerRefs.current.report = el} className="flex-1 bg-white outline-none overflow-hidden" tabIndex={activeSheet === 'report' ? 0 : -1} onKeyDown={activeSheet === 'report' ? handleKeyDown : undefined} onClick={() => setActiveSheet('report')} > - {renderReportSpreadsheet()} + {renderSpreadsheet('report')}
@@ -812,66 +603,15 @@ const DualSpreadsheet: FC = ({ templateData = {} }) => {

Вычисления

- - {/* Formula bar для вычислений */} -
-
-
- {selectedCell && activeSheet === 'calculations' ? `${getColumnLabel(selectedCell.col)}${selectedCell.row + 1}` : ''} -
- {multiSheetEngine.isCalculating && ( -
- Вычисляется... -
- )} -
- { - if (activeSheet === 'calculations') { - setFormulaBarValue(e.target.value); - if (selectedCell) { - handleCellChange(selectedCell.row, selectedCell.col, e.target.value); - } - } - }} - onFocus={() => { - if (selectedCell && activeSheet === 'calculations') { - startEditing('formula'); - } - }} - onBlur={() => stopEditing(true)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault(); - stopEditing(true); - calculationsContainerRef.current?.focus(); - } else if (e.key === 'Escape') { - e.preventDefault(); - stopEditing(false); - } - }} - /> -
-
-
- +
containerRefs.current.calculations = el} className="flex-1 bg-white outline-none overflow-hidden" tabIndex={activeSheet === 'calculations' ? 0 : -1} onKeyDown={activeSheet === 'calculations' ? handleKeyDown : undefined} onClick={() => setActiveSheet('calculations')} > - {renderCalculationsSpreadsheet()} + {renderSpreadsheet('calculations')}
diff --git a/src/components/Spreadsheet/Spreadsheet.tsx b/src/components/Spreadsheet/Spreadsheet.tsx deleted file mode 100644 index 4da0a9b..0000000 --- a/src/components/Spreadsheet/Spreadsheet.tsx +++ /dev/null @@ -1,552 +0,0 @@ -import { FC, useEffect, useRef, useState } from 'react'; -import { useSpreadsheetEngine } from '../../hooks/useSpreadsheetEngine'; - -interface CellData { - value: string; - isSelected: boolean; -} - -const Spreadsheet: FC = () => { - const { - cells: engineCells, - setCellValue: setEngineCellValue, - getCellValue, - getCellRawValue, - isCalculating, - recalculate - } = useSpreadsheetEngine({ - rows: 20, - cols: 10, - sheetName: 'Sheet1' - }); - - const [cells, setCells] = useState(() => { - // Создаем сетку 20x10 с пустыми ячейками - return Array(20).fill(null).map(() => - Array(10).fill(null).map(() => ({ value: '', isSelected: false })) - ); - }); - - const [selectedCell, setSelectedCell] = useState<{row: number, col: number} | null>(null); - const [isEditing, setIsEditing] = useState(false); - const [editingValue, setEditingValue] = useState(''); // Для отмены изменений - const [formulaBarValue, setFormulaBarValue] = useState(''); // Значение в formula bar - const [columnWidths, setColumnWidths] = useState(Array(10).fill(96)); // 96px = w-24 - const [rowHeights, setRowHeights] = useState(Array(20).fill(32)); // 32px = h-8 - const containerRef = useRef(null); - const inputRefs = useRef<(HTMLInputElement | null)[][]>([]); - const formulaBarRef = useRef(null); - - // Инициализируем refs для всех input - useEffect(() => { - inputRefs.current = Array(20).fill(null).map(() => Array(10).fill(null)); - }, []); - - // Синхронизируем локальное состояние с движком - useEffect(() => { - setCells(prev => { - const newCells = [...prev]; - for (let row = 0; row < 20; row++) { - for (let col = 0; col < 10; col++) { - const rawValue = getCellRawValue(row, col); - if (rawValue !== newCells[row][col].value) { - newCells[row][col] = { ...newCells[row][col], value: rawValue }; - } - } - } - return newCells; - }); - }, [engineCells, getCellRawValue]); - - // Тестовые данные для демонстрации - useEffect(() => { - // Добавляем тестовые данные - setEngineCellValue(0, 0, '10'); // A1 = 10 - setEngineCellValue(1, 0, '20'); // A2 = 20 - setEngineCellValue(2, 0, '=A1+A2'); // A3 = A1+A2 - setEngineCellValue(0, 1, '=A1*2'); // B1 = A1*2 - setEngineCellValue(1, 1, '=SUM(A1,A2)'); // B2 = SUM(A1,A2) - setEngineCellValue(0, 2, '="Результат: " + A3'); // C1 = "Результат: " + A3 - - // Демонстрация волатильных функций - setEngineCellValue(0, 3, '=RAND()'); // D1 = случайное число - setEngineCellValue(1, 3, '=RANDBETWEEN(1,100)'); // D2 = случайное число от 1 до 100 - setEngineCellValue(2, 3, '=D1*100'); // D3 = D1*100 (зависит от волатильной функции) - setEngineCellValue(0, 4, '=TODAY()'); // E1 = сегодняшняя дата - setEngineCellValue(1, 4, '=NOW()'); // E2 = текущая дата и время - }, [setEngineCellValue]); - - // Синхронизируем значение formula bar с выбранной ячейкой - useEffect(() => { - if (selectedCell) { - // Показываем сырое значение (формулу) в formula bar - const rawValue = getCellRawValue(selectedCell.row, selectedCell.col); - setFormulaBarValue(rawValue); - } else { - setFormulaBarValue(''); - } - }, [selectedCell, getCellRawValue, engineCells]); - - const handleCellClick = (row: number, col: number) => { - setSelectedCell({ row, col }); - setIsEditing(false); - containerRef.current?.focus(); - }; - - const handleCellDoubleClick = (row: number, col: number) => { - setSelectedCell({ row, col }); - // Сохраняем текущее значение из UI для возможности отмены - setEditingValue(cells[row][col].value); - setIsEditing(true); - // Фокус на input ячейки и курсор в конец - setTimeout(() => { - const input = inputRefs.current[row]?.[col]; - if (input) { - input.focus(); - // Устанавливаем курсор в конец текста - input.setSelectionRange(input.value.length, input.value.length); - } - }, 10); // Увеличиваем задержку для надежности - }; - - const moveSelection = (deltaRow: number, deltaCol: number) => { - if (!selectedCell) return; - - const newRow = Math.max(0, Math.min(19, selectedCell.row + deltaRow)); - const newCol = Math.max(0, Math.min(9, selectedCell.col + deltaCol)); - - setSelectedCell({ row: newRow, col: newCol }); - setIsEditing(false); - }; - - const startEditing = (focusTarget?: 'cell' | 'formula') => { - if (!selectedCell) return; - // Сохраняем текущее значение из UI для возможности отмены - setEditingValue(cells[selectedCell.row][selectedCell.col].value); - setIsEditing(true); - - // Фокус в зависимости от того, откуда вызвано редактирование - if (focusTarget === 'cell') { - setTimeout(() => { - const input = inputRefs.current[selectedCell.row]?.[selectedCell.col]; - if (input) { - input.focus(); - input.select(); - } - }, 0); - } else if (focusTarget === 'formula') { - setTimeout(() => { - if (formulaBarRef.current) { - formulaBarRef.current.focus(); - formulaBarRef.current.select(); - } - }, 0); - } - // Если focusTarget не указан, не меняем фокус - }; - - const stopEditing = (save: boolean = true) => { - if (!save && selectedCell) { - // Отменяем изменения - возвращаем старое значение - setCells(prev => { - const newCells = [...prev]; - newCells[selectedCell.row][selectedCell.col] = { - ...newCells[selectedCell.row][selectedCell.col], - value: editingValue - }; - return newCells; - }); - setFormulaBarValue(editingValue); - // Восстанавливаем значение в движке - setEngineCellValue(selectedCell.row, selectedCell.col, editingValue); - } - setIsEditing(false); - containerRef.current?.focus(); - }; - - const handleCellChange = (row: number, col: number, value: string) => { - // Обновляем локальное состояние для UI (сырое значение) - setCells(prev => { - const newCells = [...prev]; - newCells[row][col] = { ...newCells[row][col], value }; - return newCells; - }); - - // Обновляем formula bar если изменяется текущая выбранная ячейка - if (selectedCell && selectedCell.row === row && selectedCell.col === col) { - setFormulaBarValue(value); - } - - // Обновляем значение в движке (это может быть медленно, поэтому делаем в конце) - setEngineCellValue(row, col, value); - }; - - const getColumnLabel = (index: number) => { - return String.fromCharCode(65 + index); // A, B, C, ... - }; - - const handleResize = (colIndex: number, newWidth: number) => { - setColumnWidths(prev => { - const newWidths = [...prev]; - newWidths[colIndex] = Math.max(60, newWidth); // Минимальная ширина 60px - return newWidths; - }); - }; - - const handleRowResize = (rowIndex: number, newHeight: number) => { - setRowHeights(prev => { - const newHeights = [...prev]; - newHeights[rowIndex] = Math.max(24, newHeight); // Минимальная высота 24px - return newHeights; - }); - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (!selectedCell) return; - - // Если в режиме редактирования, обрабатываем Esc и стрелочки - if (isEditing) { - if (e.key === 'Escape') { - e.preventDefault(); - stopEditing(false); // Отменяем изменения - } else if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) { - e.preventDefault(); - stopEditing(true); // Сохраняем изменения - // Затем перемещаемся - const deltaMap: Record = { - 'ArrowUp': [-1, 0], - 'ArrowDown': [1, 0], - 'ArrowLeft': [0, -1], - 'ArrowRight': [0, 1] - }; - const [deltaRow, deltaCol] = deltaMap[e.key]; - moveSelection(deltaRow, deltaCol); - } - return; - } - - switch (e.key) { - case 'ArrowUp': - e.preventDefault(); - moveSelection(-1, 0); - break; - case 'ArrowDown': - e.preventDefault(); - moveSelection(1, 0); - break; - case 'ArrowLeft': - e.preventDefault(); - moveSelection(0, -1); - break; - case 'ArrowRight': - e.preventDefault(); - moveSelection(0, 1); - break; - case 'Enter': - e.preventDefault(); - if (selectedCell) { - setEditingValue(cells[selectedCell.row][selectedCell.col].value); - setIsEditing(true); - setTimeout(() => { - const input = inputRefs.current[selectedCell.row]?.[selectedCell.col]; - if (input) { - input.focus(); - // Устанавливаем курсор в конец текста - input.setSelectionRange(input.value.length, input.value.length); - } - }, 0); - } - break; - case 'F2': - e.preventDefault(); - if (selectedCell) { - setEditingValue(cells[selectedCell.row][selectedCell.col].value); - setIsEditing(true); - setTimeout(() => { - const input = inputRefs.current[selectedCell.row]?.[selectedCell.col]; - if (input) { - input.focus(); - // Устанавливаем курсор в конец текста - input.setSelectionRange(input.value.length, input.value.length); - } - }, 0); - } - break; - case 'Backspace': - case 'Delete': - e.preventDefault(); - if (selectedCell) { - handleCellChange(selectedCell.row, selectedCell.col, ''); - } - break; - default: - // Если нажата обычная клавиша (буква/цифра), начинаем редактирование - if (e.key.length === 1 && !e.ctrlKey && !e.altKey && !e.metaKey) { - e.preventDefault(); - if (selectedCell) { - handleCellChange(selectedCell.row, selectedCell.col, e.key); - setFormulaBarValue(e.key); - setEditingValue(cells[selectedCell.row][selectedCell.col].value); - setIsEditing(true); - setTimeout(() => { - const input = inputRefs.current[selectedCell.row]?.[selectedCell.col]; - if (input) { - input.focus(); - // Устанавливаем курсор в конец - input.setSelectionRange(input.value.length, input.value.length); - } - }, 0); - } - } - break; - } - }; - - return ( -
- {/* Formula bar */} -
-
-
- {selectedCell ? `${getColumnLabel(selectedCell.col)}${selectedCell.row + 1}` : ''} -
- {isCalculating && ( -
- Вычисляется... -
- )} -
- { - setFormulaBarValue(e.target.value); - if (selectedCell) { - handleCellChange(selectedCell.row, selectedCell.col, e.target.value); - } - }} - onFocus={() => { - if (selectedCell) { - startEditing('formula'); - } - }} - onBlur={() => stopEditing(true)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault(); - stopEditing(true); - containerRef.current?.focus(); - } else if (e.key === 'Escape') { - e.preventDefault(); - stopEditing(false); - } - }} - /> -
-
-
- - {/* Spreadsheet */} -
- - - - {/* Empty corner cell */} - - {/* Column headers */} - {Array.from({ length: 10 }, (_, i) => ( - - ))} - - - - {cells.map((row, rowIndex) => ( - - {/* Row header */} - - {/* Data cells */} - {row.map((cell, colIndex) => ( - - ))} - - ))} - -
- {getColumnLabel(i)} - {/* Resize handle */} -
{ - e.preventDefault(); - const startX = e.clientX; - const startWidth = columnWidths[i]; - - const handleMouseMove = (e: MouseEvent) => { - const newWidth = startWidth + (e.clientX - startX); - handleResize(i, newWidth); - }; - - const handleMouseUp = () => { - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); - }; - - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleMouseUp); - }} - /> -
- {rowIndex + 1} - {/* Resize handle */} -
{ - e.preventDefault(); - const startY = e.clientY; - const startHeight = rowHeights[rowIndex]; - - const handleMouseMove = (e: MouseEvent) => { - const newHeight = startHeight + (e.clientY - startY); - handleRowResize(rowIndex, newHeight); - }; - - const handleMouseUp = () => { - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); - }; - - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleMouseUp); - }} - /> -
{ - // Если в режиме редактирования и это активная ячейка, не обрабатываем клик - if (isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex) { - return; - } - - handleCellClick(rowIndex, colIndex); - // Фокусируемся на контейнере, чтобы работали клавиши - containerRef.current?.focus(); - }} - onDoubleClick={() => handleCellDoubleClick(rowIndex, colIndex)} - > - { - handleCellChange(rowIndex, colIndex, e.target.value); - setFormulaBarValue(e.target.value); - }} - onFocus={(e) => { - // Только устанавливаем выбранную ячейку, не включаем режим редактирования автоматически - setSelectedCell({ row: rowIndex, col: colIndex }); - // Если не в режиме редактирования, убираем фокус - if (!isEditing) { - (e.target as HTMLInputElement).blur(); - } - }} - onBlur={() => stopEditing(true)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault(); - stopEditing(true); - moveSelection(1, 0); - } else if (e.key === 'Escape') { - e.preventDefault(); - stopEditing(false); - } - }} - onClick={(e) => { - // Если в режиме редактирования и это активная ячейка, не обрабатываем клик - if (isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex) { - return; - } - - e.stopPropagation(); - setSelectedCell({ row: rowIndex, col: colIndex }); - setIsEditing(false); // Явно отключаем режим редактирования при клике - // Убираем фокус с input чтобы не мигал курсор - (e.target as HTMLInputElement).blur(); - }} - onDoubleClick={(e) => { - e.stopPropagation(); - setSelectedCell({ row: rowIndex, col: colIndex }); - setEditingValue(cells[rowIndex][colIndex].value); - setIsEditing(true); - // Фокус уже на input, устанавливаем курсор в конец - setTimeout(() => { - const input = e.target as HTMLInputElement; - input.focus(); - input.setSelectionRange(input.value.length, input.value.length); - }, 0); - }} - onMouseDown={(e) => { - // В режиме редактирования разрешаем нормальное поведение мыши - if (isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex) { - e.stopPropagation(); - } - }} - onMouseUp={(e) => { - // В режиме редактирования разрешаем нормальное поведение мыши - if (isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex) { - e.stopPropagation(); - } - }} - readOnly={!isEditing || selectedCell?.row !== rowIndex || selectedCell?.col !== colIndex} - ref={(el) => { - if (inputRefs.current[rowIndex]) { - inputRefs.current[rowIndex][colIndex] = el; - } - }} - /> -
-
-
- ); -}; - -export default Spreadsheet; \ No newline at end of file diff --git a/src/components/Spreadsheet/index.ts b/src/components/Spreadsheet/index.ts deleted file mode 100644 index bd9fee5..0000000 --- a/src/components/Spreadsheet/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as Spreadsheet } from './Spreadsheet'; diff --git a/src/components/index.ts b/src/components/index.ts index 7bd9907..a4dbbd9 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,3 +1,2 @@ export { DualSpreadsheet } from "./DualSpreadsheet"; -export { Spreadsheet } from "./Spreadsheet"; diff --git a/src/hooks/useMultiSheetEngine.ts b/src/hooks/useMultiSheetEngine.ts index 0ce6e40..bc58a59 100644 --- a/src/hooks/useMultiSheetEngine.ts +++ b/src/hooks/useMultiSheetEngine.ts @@ -24,6 +24,8 @@ export function useMultiSheetEngine({ const workbookRef = useRef(null); const [isCalculating, setIsCalculating] = useState(false); const [sheetCells, setSheetCells] = useState>({}); + const debouncedRecalcTimeoutRef = useRef(null); + const isRecalculatingRef = useRef(false); // Инициализация workbook и листов useEffect(() => { @@ -54,7 +56,7 @@ export function useMultiSheetEngine({ // Синхронизация всех листов из движка const syncAllSheetsFromEngine = useCallback(() => { - if (!workbookRef.current) return; + if (!workbookRef.current || isRecalculatingRef.current) return; const newSheetCells: Record = {}; @@ -84,26 +86,78 @@ export function useMultiSheetEngine({ setSheetCells(newSheetCells); }, [sheets]); - // Установка значения ячейки - const setCellValue = useCallback((sheetName: string, row: number, col: number, value: string) => { - if (!workbookRef.current) return; + // Безопасная функция для вычислений + const safeRecalc = useCallback(() => { + if (!workbookRef.current || isRecalculatingRef.current) return; + + isRecalculatingRef.current = true; + setIsCalculating(true); + + try { + workbookRef.current.recalc(); + syncAllSheetsFromEngine(); + } catch (error) { + console.warn('Ошибка при вычислении:', error); + } finally { + // Небольшая задержка для визуального индикатора + setTimeout(() => { + setIsCalculating(false); + isRecalculatingRef.current = false; + }, 150); + } + }, [syncAllSheetsFromEngine]); + + // Отложенный пересчет с дебаунсом + const debouncedRecalc = useCallback(() => { + if (!workbookRef.current || isRecalculatingRef.current) return; + + // Очищаем предыдущий отложенный пересчет + if (debouncedRecalcTimeoutRef.current) { + clearTimeout(debouncedRecalcTimeoutRef.current); + } + + // Запускаем пересчет через 300ms + debouncedRecalcTimeoutRef.current = setTimeout(() => { + safeRecalc(); + }, 300); + }, [safeRecalc]); + + // Установка значения ячейки без немедленного пересчета + const setCellValueWithoutRecalc = useCallback((sheetName: string, row: number, col: number, value: string) => { + if (!workbookRef.current || isRecalculatingRef.current) return; const cellName = columnToLabel(col) + (row + 1); const qualifiedName = `${sheetName}!${cellName}`; - setIsCalculating(true); + // Устанавливаем значение в движке без пересчета + workbookRef.current.setCell(qualifiedName, value); + + // Обновляем только локальное состояние ячейки + setSheetCells(prev => { + const newSheetCells = { ...prev }; + if (newSheetCells[sheetName]) { + const newCells = [...newSheetCells[sheetName]]; + newCells[row] = [...newCells[row]]; + newCells[row][col] = { ...newCells[row][col], value }; + newSheetCells[sheetName] = newCells; + } + return newSheetCells; + }); + }, []); + + // Установка значения ячейки с немедленным пересчетом + const setCellValue = useCallback((sheetName: string, row: number, col: number, value: string) => { + if (!workbookRef.current || isRecalculatingRef.current) return; + + const cellName = columnToLabel(col) + (row + 1); + const qualifiedName = `${sheetName}!${cellName}`; // Устанавливаем значение в движке workbookRef.current.setCell(qualifiedName, value); - // Запускаем пересчет - workbookRef.current.recalc(); - - setIsCalculating(false); - - // Синхронизируем состояние - syncAllSheetsFromEngine(); - }, [syncAllSheetsFromEngine]); + // Запускаем безопасный пересчет + safeRecalc(); + }, [safeRecalc]); // Получение значения ячейки (вычисленное) const getCellValue = useCallback((sheetName: string, row: number, col: number): CellValue => { @@ -165,15 +219,24 @@ export function useMultiSheetEngine({ const recalculate = useCallback(() => { if (!workbookRef.current) return; - setIsCalculating(true); - workbookRef.current.recalc(); - setIsCalculating(false); - syncAllSheetsFromEngine(); - }, [syncAllSheetsFromEngine]); + safeRecalc(); + }, [safeRecalc]); + + // Очистка таймаутов при размонтировании + useEffect(() => { + return () => { + if (debouncedRecalcTimeoutRef.current) { + clearTimeout(debouncedRecalcTimeoutRef.current); + } + isRecalculatingRef.current = false; + }; + }, []); return { sheetCells, setCellValue, + setCellValueWithoutRecalc, + debouncedRecalc, getCellValue, getCellRawValue, getModifiedCells, diff --git a/src/lib/spreadsheet-engine.ts b/src/lib/spreadsheet-engine.ts index f302fe4..7ab2fbd 100644 --- a/src/lib/spreadsheet-engine.ts +++ b/src/lib/spreadsheet-engine.ts @@ -210,8 +210,17 @@ export class Cell { // Заменяем квалифицированные ссылки на ячейки code = code.replace(QUALIFIED_REF_RE, (match) => { - const cellValue = this.sheet.workbook.getCell(match).evaluate(); - return typeof cellValue === 'string' ? `"${cellValue}"` : String(cellValue ?? 0); + try { + const [sheetName] = match.split('!'); + const sheet = this.sheet.workbook.getSheet(sheetName); + if (!sheet) { + return '"#REF!"'; + } + const cellValue = this.sheet.workbook.getCell(match).evaluate(); + return typeof cellValue === 'string' ? `"${cellValue}"` : String(cellValue ?? 0); + } catch (error) { + return '"#REF!"'; + } }); // Заменяем локальные ссылки на ячейки @@ -220,14 +229,18 @@ export class Cell { if (this.isFunction(match)) { return match; } - const qualifiedRef = `${this.sheet.name}!${match}`; - const cellValue = this.sheet.workbook.getCell(qualifiedRef).evaluate(); - return typeof cellValue === 'string' ? `"${cellValue}"` : String(cellValue ?? 0); + try { + const qualifiedRef = `${this.sheet.name}!${match}`; + const cellValue = this.sheet.workbook.getCell(qualifiedRef).evaluate(); + return typeof cellValue === 'string' ? `"${cellValue}"` : String(cellValue ?? 0); + } catch (error) { + return '"#REF!"'; + } }); // Заменяем функции Excel const functions = this.sheet.workbook.getFunctions(); - for (const [funcName, func] of Object.entries(functions)) { + for (const [funcName] of Object.entries(functions)) { const funcRegex = new RegExp(`\\b${funcName}\\s*\\(`, 'gi'); code = code.replace(funcRegex, `__functions__.${funcName}(`); } diff --git a/src/lib/spreadsheet-example.ts b/src/lib/spreadsheet-example.ts index 5f50d99..4c33180 100644 --- a/src/lib/spreadsheet-example.ts +++ b/src/lib/spreadsheet-example.ts @@ -5,7 +5,7 @@ export function createExampleWorkbook(): Workbook { const wb = new Workbook(); // Создаём листы - const sheetMain = wb.addSheet('Sheet1'); + wb.addSheet('Sheet1'); // Заполняем данные с локальными ссылками wb.setCell('Sheet1!A1', 10); @@ -16,7 +16,7 @@ export function createExampleWorkbook(): Workbook { wb.setCell('Sheet1!C1', '="Результат: " + A3'); // Строковая операция с локальной ссылкой // Пример с квалифицированными ссылками (если есть несколько листов) - const countsSheet = wb.addSheet('Counts'); + wb.addSheet('Counts'); wb.setCell('Counts!A1', 'N455'); wb.setCell('Sheet1!D1', '="Отчет по протоколу " + Counts!A1'); diff --git a/src/lib/template-data.ts b/src/lib/template-data.ts index 4fb96a9..5982ed1 100644 --- a/src/lib/template-data.ts +++ b/src/lib/template-data.ts @@ -1,11 +1,8 @@ -// Пример шаблонных данных для отчета (только простые значения из Excel) export const exampleTemplateData = { - // Заголовки A1: 'Отчет о продажах', A2: 'Период: Январь 2024', A3: '', - // Таблица данных A4: 'Товар', B4: 'Количество', C4: 'Цена', @@ -14,42 +11,39 @@ export const exampleTemplateData = { A5: 'Товар A', B5: 100, C5: 1500, - D5: 150000, // Простое значение вместо формулы + D5: 150000, A6: 'Товар B', B6: 50, C6: 2000, - D6: 100000, // Простое значение вместо формулы + D6: 100000, A7: 'Товар C', B7: 75, C7: 1200, - D7: 90000, // Простое значение вместо формулы + D7: 90000, A8: '', A9: 'Итого:', - D9: 340000, // Простое значение вместо формулы + D9: 340000, - // Дополнительные поля для расчетов A11: 'НДС (20%):', - D11: 68000, // Простое значение вместо формулы + D11: 68000, A12: 'Всего к оплате:', - D12: 408000, // Простое значение вместо формулы - - // Поля для ссылок на правый лист (пока пустые, формулы можно добавить поверх) + D12: 408000, + A14: 'Скидка из расчетов:', - D14: '', // Пустое значение, формулу можно добавить поверх + D14: '', A15: 'Итого со скидкой:', - D15: '' // Пустое значение, формулу можно добавить поверх + D15: '' }; -// Пример данных для листа вычислений export const exampleCalculationsData = { A1: 'Расчеты', A2: 'Скидка:', - B2: '=Report!D9*0.05', // 5% скидка от общей суммы + B2: '=Report!D9*0.05', A4: 'Дополнительные расчеты:', A5: 'Средняя цена:',