уменьшение кода
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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<CellData[][]>(() => {
|
|
||||||
// Создаем сетку 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<string>(''); // Для отмены изменений
|
|
||||||
const [formulaBarValue, setFormulaBarValue] = useState<string>(''); // Значение в formula bar
|
|
||||||
const [columnWidths, setColumnWidths] = useState<number[]>(Array(10).fill(96)); // 96px = w-24
|
|
||||||
const [rowHeights, setRowHeights] = useState<number[]>(Array(20).fill(32)); // 32px = h-8
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const inputRefs = useRef<(HTMLInputElement | null)[][]>([]);
|
|
||||||
const formulaBarRef = useRef<HTMLInputElement>(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<string, [number, number]> = {
|
|
||||||
'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 (
|
|
||||||
<div
|
|
||||||
ref={containerRef}
|
|
||||||
className="w-full h-full bg-white outline-none"
|
|
||||||
tabIndex={0}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
>
|
|
||||||
{/* Formula bar */}
|
|
||||||
<div className="border-b border-gray-200 p-2 bg-white">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<div className="text-sm text-gray-600 min-w-[60px]">
|
|
||||||
{selectedCell ? `${getColumnLabel(selectedCell.col)}${selectedCell.row + 1}` : ''}
|
|
||||||
</div>
|
|
||||||
{isCalculating && (
|
|
||||||
<div className="text-xs text-blue-600 animate-pulse">
|
|
||||||
Вычисляется...
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex-1">
|
|
||||||
<input
|
|
||||||
ref={formulaBarRef}
|
|
||||||
type="text"
|
|
||||||
className={`w-full px-2 py-1 text-sm border rounded focus:outline-none ${
|
|
||||||
isEditing
|
|
||||||
? 'border-blue-400 focus:ring-2 focus:ring-blue-300'
|
|
||||||
: 'border-gray-300 focus:ring-2 focus:ring-blue-500'
|
|
||||||
}`}
|
|
||||||
placeholder="Введите формулу..."
|
|
||||||
value={formulaBarValue}
|
|
||||||
onChange={(e) => {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Spreadsheet */}
|
|
||||||
<div className="overflow-auto h-[calc(100vh-200px)]">
|
|
||||||
<table className="border-collapse">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
{/* Empty corner cell */}
|
|
||||||
<th className="w-12 h-8 bg-gray-100 border border-gray-300 text-xs text-gray-600 font-medium sticky top-0 left-0 z-20"></th>
|
|
||||||
{/* Column headers */}
|
|
||||||
{Array.from({ length: 10 }, (_, i) => (
|
|
||||||
<th
|
|
||||||
key={i}
|
|
||||||
className="h-8 bg-gray-100 border border-gray-300 text-xs text-gray-600 font-medium text-center sticky top-0 z-10 relative"
|
|
||||||
style={{ width: columnWidths[i] }}
|
|
||||||
>
|
|
||||||
{getColumnLabel(i)}
|
|
||||||
{/* Resize handle */}
|
|
||||||
<div
|
|
||||||
className="absolute top-0 right-0 w-1 h-full cursor-col-resize hover:bg-blue-500 hover:bg-opacity-50"
|
|
||||||
onMouseDown={(e) => {
|
|
||||||
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);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{cells.map((row, rowIndex) => (
|
|
||||||
<tr key={rowIndex}>
|
|
||||||
{/* Row header */}
|
|
||||||
<td
|
|
||||||
className="w-12 bg-gray-100 border border-gray-300 text-xs text-gray-600 font-medium text-center sticky left-0 z-10 relative"
|
|
||||||
style={{ height: rowHeights[rowIndex] }}
|
|
||||||
>
|
|
||||||
{rowIndex + 1}
|
|
||||||
{/* Resize handle */}
|
|
||||||
<div
|
|
||||||
className="absolute bottom-0 left-0 w-full h-1 cursor-row-resize hover:bg-blue-500 hover:bg-opacity-50"
|
|
||||||
onMouseDown={(e) => {
|
|
||||||
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);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
{/* Data cells */}
|
|
||||||
{row.map((cell, colIndex) => (
|
|
||||||
<td
|
|
||||||
key={colIndex}
|
|
||||||
className={`border border-gray-300 relative ${
|
|
||||||
selectedCell?.row === rowIndex && selectedCell?.col === colIndex
|
|
||||||
? ''
|
|
||||||
: 'hover:bg-gray-50'
|
|
||||||
}`}
|
|
||||||
style={{
|
|
||||||
width: columnWidths[colIndex],
|
|
||||||
height: rowHeights[rowIndex],
|
|
||||||
...(selectedCell?.row === rowIndex && selectedCell?.col === colIndex
|
|
||||||
? { boxShadow: isEditing
|
|
||||||
? 'inset 0 0 0 2px #3b82f6, inset 0 0 0 4px rgba(59, 130, 246, 0.3)' // Синяя рамка + полупрозрачная голубая вокруг
|
|
||||||
: 'inset 0 0 0 2px #3b82f6' // Только синяя рамка в режиме выделения
|
|
||||||
}
|
|
||||||
: {})
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
// Если в режиме редактирования и это активная ячейка, не обрабатываем клик
|
|
||||||
if (isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCellClick(rowIndex, colIndex);
|
|
||||||
// Фокусируемся на контейнере, чтобы работали клавиши
|
|
||||||
containerRef.current?.focus();
|
|
||||||
}}
|
|
||||||
onDoubleClick={() => handleCellDoubleClick(rowIndex, colIndex)}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className={`w-full h-full px-1 text-sm bg-transparent border-none outline-none resize-none ${
|
|
||||||
isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex
|
|
||||||
? 'cursor-text'
|
|
||||||
: 'cursor-cell'
|
|
||||||
}`}
|
|
||||||
value={isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex
|
|
||||||
? cell.value
|
|
||||||
: String(getCellValue(rowIndex, colIndex) || '')}
|
|
||||||
onChange={(e) => {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Spreadsheet;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default as Spreadsheet } from './Spreadsheet';
|
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
export { DualSpreadsheet } from "./DualSpreadsheet";
|
export { DualSpreadsheet } from "./DualSpreadsheet";
|
||||||
export { Spreadsheet } from "./Spreadsheet";
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ export function useMultiSheetEngine({
|
|||||||
const workbookRef = useRef<Workbook | null>(null);
|
const workbookRef = useRef<Workbook | null>(null);
|
||||||
const [isCalculating, setIsCalculating] = useState(false);
|
const [isCalculating, setIsCalculating] = useState(false);
|
||||||
const [sheetCells, setSheetCells] = useState<Record<string, CellData[][]>>({});
|
const [sheetCells, setSheetCells] = useState<Record<string, CellData[][]>>({});
|
||||||
|
const debouncedRecalcTimeoutRef = useRef<number | null>(null);
|
||||||
|
const isRecalculatingRef = useRef(false);
|
||||||
|
|
||||||
// Инициализация workbook и листов
|
// Инициализация workbook и листов
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -54,7 +56,7 @@ export function useMultiSheetEngine({
|
|||||||
|
|
||||||
// Синхронизация всех листов из движка
|
// Синхронизация всех листов из движка
|
||||||
const syncAllSheetsFromEngine = useCallback(() => {
|
const syncAllSheetsFromEngine = useCallback(() => {
|
||||||
if (!workbookRef.current) return;
|
if (!workbookRef.current || isRecalculatingRef.current) return;
|
||||||
|
|
||||||
const newSheetCells: Record<string, CellData[][]> = {};
|
const newSheetCells: Record<string, CellData[][]> = {};
|
||||||
|
|
||||||
@@ -84,26 +86,78 @@ export function useMultiSheetEngine({
|
|||||||
setSheetCells(newSheetCells);
|
setSheetCells(newSheetCells);
|
||||||
}, [sheets]);
|
}, [sheets]);
|
||||||
|
|
||||||
// Установка значения ячейки
|
// Безопасная функция для вычислений
|
||||||
const setCellValue = useCallback((sheetName: string, row: number, col: number, value: string) => {
|
const safeRecalc = useCallback(() => {
|
||||||
if (!workbookRef.current) return;
|
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 cellName = columnToLabel(col) + (row + 1);
|
||||||
const qualifiedName = `${sheetName}!${cellName}`;
|
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.setCell(qualifiedName, value);
|
||||||
|
|
||||||
// Запускаем пересчет
|
// Запускаем безопасный пересчет
|
||||||
workbookRef.current.recalc();
|
safeRecalc();
|
||||||
|
}, [safeRecalc]);
|
||||||
setIsCalculating(false);
|
|
||||||
|
|
||||||
// Синхронизируем состояние
|
|
||||||
syncAllSheetsFromEngine();
|
|
||||||
}, [syncAllSheetsFromEngine]);
|
|
||||||
|
|
||||||
// Получение значения ячейки (вычисленное)
|
// Получение значения ячейки (вычисленное)
|
||||||
const getCellValue = useCallback((sheetName: string, row: number, col: number): CellValue => {
|
const getCellValue = useCallback((sheetName: string, row: number, col: number): CellValue => {
|
||||||
@@ -165,15 +219,24 @@ export function useMultiSheetEngine({
|
|||||||
const recalculate = useCallback(() => {
|
const recalculate = useCallback(() => {
|
||||||
if (!workbookRef.current) return;
|
if (!workbookRef.current) return;
|
||||||
|
|
||||||
setIsCalculating(true);
|
safeRecalc();
|
||||||
workbookRef.current.recalc();
|
}, [safeRecalc]);
|
||||||
setIsCalculating(false);
|
|
||||||
syncAllSheetsFromEngine();
|
// Очистка таймаутов при размонтировании
|
||||||
}, [syncAllSheetsFromEngine]);
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (debouncedRecalcTimeoutRef.current) {
|
||||||
|
clearTimeout(debouncedRecalcTimeoutRef.current);
|
||||||
|
}
|
||||||
|
isRecalculatingRef.current = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sheetCells,
|
sheetCells,
|
||||||
setCellValue,
|
setCellValue,
|
||||||
|
setCellValueWithoutRecalc,
|
||||||
|
debouncedRecalc,
|
||||||
getCellValue,
|
getCellValue,
|
||||||
getCellRawValue,
|
getCellRawValue,
|
||||||
getModifiedCells,
|
getModifiedCells,
|
||||||
|
|||||||
@@ -210,8 +210,17 @@ export class Cell {
|
|||||||
|
|
||||||
// Заменяем квалифицированные ссылки на ячейки
|
// Заменяем квалифицированные ссылки на ячейки
|
||||||
code = code.replace(QUALIFIED_REF_RE, (match) => {
|
code = code.replace(QUALIFIED_REF_RE, (match) => {
|
||||||
const cellValue = this.sheet.workbook.getCell(match).evaluate();
|
try {
|
||||||
return typeof cellValue === 'string' ? `"${cellValue}"` : String(cellValue ?? 0);
|
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)) {
|
if (this.isFunction(match)) {
|
||||||
return match;
|
return match;
|
||||||
}
|
}
|
||||||
const qualifiedRef = `${this.sheet.name}!${match}`;
|
try {
|
||||||
const cellValue = this.sheet.workbook.getCell(qualifiedRef).evaluate();
|
const qualifiedRef = `${this.sheet.name}!${match}`;
|
||||||
return typeof cellValue === 'string' ? `"${cellValue}"` : String(cellValue ?? 0);
|
const cellValue = this.sheet.workbook.getCell(qualifiedRef).evaluate();
|
||||||
|
return typeof cellValue === 'string' ? `"${cellValue}"` : String(cellValue ?? 0);
|
||||||
|
} catch (error) {
|
||||||
|
return '"#REF!"';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Заменяем функции Excel
|
// Заменяем функции Excel
|
||||||
const functions = this.sheet.workbook.getFunctions();
|
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');
|
const funcRegex = new RegExp(`\\b${funcName}\\s*\\(`, 'gi');
|
||||||
code = code.replace(funcRegex, `__functions__.${funcName}(`);
|
code = code.replace(funcRegex, `__functions__.${funcName}(`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export function createExampleWorkbook(): Workbook {
|
|||||||
const wb = new Workbook();
|
const wb = new Workbook();
|
||||||
|
|
||||||
// Создаём листы
|
// Создаём листы
|
||||||
const sheetMain = wb.addSheet('Sheet1');
|
wb.addSheet('Sheet1');
|
||||||
|
|
||||||
// Заполняем данные с локальными ссылками
|
// Заполняем данные с локальными ссылками
|
||||||
wb.setCell('Sheet1!A1', 10);
|
wb.setCell('Sheet1!A1', 10);
|
||||||
@@ -16,7 +16,7 @@ export function createExampleWorkbook(): Workbook {
|
|||||||
wb.setCell('Sheet1!C1', '="Результат: " + A3'); // Строковая операция с локальной ссылкой
|
wb.setCell('Sheet1!C1', '="Результат: " + A3'); // Строковая операция с локальной ссылкой
|
||||||
|
|
||||||
// Пример с квалифицированными ссылками (если есть несколько листов)
|
// Пример с квалифицированными ссылками (если есть несколько листов)
|
||||||
const countsSheet = wb.addSheet('Counts');
|
wb.addSheet('Counts');
|
||||||
wb.setCell('Counts!A1', 'N455');
|
wb.setCell('Counts!A1', 'N455');
|
||||||
wb.setCell('Sheet1!D1', '="Отчет по протоколу " + Counts!A1');
|
wb.setCell('Sheet1!D1', '="Отчет по протоколу " + Counts!A1');
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
// Пример шаблонных данных для отчета (только простые значения из Excel)
|
|
||||||
export const exampleTemplateData = {
|
export const exampleTemplateData = {
|
||||||
// Заголовки
|
|
||||||
A1: 'Отчет о продажах',
|
A1: 'Отчет о продажах',
|
||||||
A2: 'Период: Январь 2024',
|
A2: 'Период: Январь 2024',
|
||||||
A3: '',
|
A3: '',
|
||||||
|
|
||||||
// Таблица данных
|
|
||||||
A4: 'Товар',
|
A4: 'Товар',
|
||||||
B4: 'Количество',
|
B4: 'Количество',
|
||||||
C4: 'Цена',
|
C4: 'Цена',
|
||||||
@@ -14,42 +11,39 @@ export const exampleTemplateData = {
|
|||||||
A5: 'Товар A',
|
A5: 'Товар A',
|
||||||
B5: 100,
|
B5: 100,
|
||||||
C5: 1500,
|
C5: 1500,
|
||||||
D5: 150000, // Простое значение вместо формулы
|
D5: 150000,
|
||||||
|
|
||||||
A6: 'Товар B',
|
A6: 'Товар B',
|
||||||
B6: 50,
|
B6: 50,
|
||||||
C6: 2000,
|
C6: 2000,
|
||||||
D6: 100000, // Простое значение вместо формулы
|
D6: 100000,
|
||||||
|
|
||||||
A7: 'Товар C',
|
A7: 'Товар C',
|
||||||
B7: 75,
|
B7: 75,
|
||||||
C7: 1200,
|
C7: 1200,
|
||||||
D7: 90000, // Простое значение вместо формулы
|
D7: 90000,
|
||||||
|
|
||||||
A8: '',
|
A8: '',
|
||||||
A9: 'Итого:',
|
A9: 'Итого:',
|
||||||
D9: 340000, // Простое значение вместо формулы
|
D9: 340000,
|
||||||
|
|
||||||
// Дополнительные поля для расчетов
|
|
||||||
A11: 'НДС (20%):',
|
A11: 'НДС (20%):',
|
||||||
D11: 68000, // Простое значение вместо формулы
|
D11: 68000,
|
||||||
|
|
||||||
A12: 'Всего к оплате:',
|
A12: 'Всего к оплате:',
|
||||||
D12: 408000, // Простое значение вместо формулы
|
D12: 408000,
|
||||||
|
|
||||||
// Поля для ссылок на правый лист (пока пустые, формулы можно добавить поверх)
|
|
||||||
A14: 'Скидка из расчетов:',
|
A14: 'Скидка из расчетов:',
|
||||||
D14: '', // Пустое значение, формулу можно добавить поверх
|
D14: '',
|
||||||
|
|
||||||
A15: 'Итого со скидкой:',
|
A15: 'Итого со скидкой:',
|
||||||
D15: '' // Пустое значение, формулу можно добавить поверх
|
D15: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
// Пример данных для листа вычислений
|
|
||||||
export const exampleCalculationsData = {
|
export const exampleCalculationsData = {
|
||||||
A1: 'Расчеты',
|
A1: 'Расчеты',
|
||||||
A2: 'Скидка:',
|
A2: 'Скидка:',
|
||||||
B2: '=Report!D9*0.05', // 5% скидка от общей суммы
|
B2: '=Report!D9*0.05',
|
||||||
|
|
||||||
A4: 'Дополнительные расчеты:',
|
A4: 'Дополнительные расчеты:',
|
||||||
A5: 'Средняя цена:',
|
A5: 'Средняя цена:',
|
||||||
|
|||||||
Reference in New Issue
Block a user