2 menus
This commit is contained in:
882
src/components/DualSpreadsheet/DualSpreadsheet.tsx
Normal file
882
src/components/DualSpreadsheet/DualSpreadsheet.tsx
Normal file
@@ -0,0 +1,882 @@
|
||||
import { FC, useEffect, useRef, useState } from 'react';
|
||||
import { useMultiSheetEngine } from '../../hooks/useMultiSheetEngine';
|
||||
|
||||
interface CellData {
|
||||
value: string;
|
||||
isSelected: boolean;
|
||||
isModified: boolean; // Флаг для отслеживания изменений
|
||||
templateValue?: string; // Исходное значение из шаблона
|
||||
}
|
||||
|
||||
interface DualSpreadsheetProps {
|
||||
templateData?: Record<string, Record<string, any>>; // Данные для обоих листов
|
||||
}
|
||||
|
||||
const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
|
||||
// Используем мультилистовый движок
|
||||
const multiSheetEngine = useMultiSheetEngine({
|
||||
sheets: [
|
||||
{ name: 'Report', rows: 20, cols: 10 },
|
||||
{ name: 'Calculations', rows: 20, cols: 10 }
|
||||
],
|
||||
initialData: templateData || {}
|
||||
});
|
||||
|
||||
const [activeSheet, setActiveSheet] = useState<'report' | 'calculations'>('report');
|
||||
const [reportCells, setReportCells] = useState<CellData[][]>(() => {
|
||||
return Array(20).fill(null).map(() =>
|
||||
Array(10).fill(null).map(() => ({
|
||||
value: '',
|
||||
isSelected: false,
|
||||
isModified: false,
|
||||
templateValue: ''
|
||||
}))
|
||||
);
|
||||
});
|
||||
|
||||
const [calculationsCells, setCalculationsCells] = useState<CellData[][]>(() => {
|
||||
return 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<string>('');
|
||||
const [formulaBarValue, setFormulaBarValue] = useState<string>('');
|
||||
const [reportColumnWidths, setReportColumnWidths] = useState<number[]>(Array(10).fill(96));
|
||||
const [calculationsColumnWidths, setCalculationsColumnWidths] = useState<number[]>(Array(10).fill(96));
|
||||
const [rowHeights, setRowHeights] = useState<number[]>(Array(20).fill(32));
|
||||
|
||||
const reportContainerRef = useRef<HTMLDivElement>(null);
|
||||
const calculationsContainerRef = useRef<HTMLDivElement>(null);
|
||||
const reportInputRefs = useRef<(HTMLInputElement | null)[][]>([]);
|
||||
const calculationsInputRefs = useRef<(HTMLInputElement | null)[][]>([]);
|
||||
const reportFormulaBarRef = useRef<HTMLInputElement>(null);
|
||||
const calculationsFormulaBarRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Инициализируем refs для всех input
|
||||
useEffect(() => {
|
||||
reportInputRefs.current = Array(20).fill(null).map(() => Array(10).fill(null));
|
||||
calculationsInputRefs.current = 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);
|
||||
const templateValue = templateData.Report[cellName];
|
||||
if (templateValue !== undefined) {
|
||||
newCells[row][col] = {
|
||||
...newCells[row][col],
|
||||
value: String(templateValue),
|
||||
templateValue: String(templateValue)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return newCells;
|
||||
});
|
||||
}
|
||||
}, [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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
return newCells;
|
||||
});
|
||||
}
|
||||
|
||||
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]);
|
||||
|
||||
// Синхронизация formula bar
|
||||
useEffect(() => {
|
||||
if (selectedCell) {
|
||||
const sheetName = activeSheet === 'report' ? 'Report' : 'Calculations';
|
||||
const rawValue = multiSheetEngine.getCellRawValue(sheetName, selectedCell.row, selectedCell.col);
|
||||
setFormulaBarValue(rawValue);
|
||||
} else {
|
||||
setFormulaBarValue('');
|
||||
}
|
||||
}, [selectedCell, activeSheet, multiSheetEngine.sheetCells, multiSheetEngine.getCellRawValue]);
|
||||
|
||||
const getColumnLabel = (index: number) => {
|
||||
return String.fromCharCode(65 + index);
|
||||
};
|
||||
|
||||
// Получение измененных ячеек для бэкенда
|
||||
const getChangedCells = () => {
|
||||
// Возвращаем только измененные ячейки листа "Отчет"
|
||||
const reportChanges: Record<string, { rawValue: string; calculatedValue: any }> = {};
|
||||
|
||||
// Проверяем каждую ячейку в отчете на изменения
|
||||
for (let row = 0; row < 20; row++) {
|
||||
for (let col = 0; col < 10; col++) {
|
||||
const cell = reportCells[row][col];
|
||||
if (cell.isModified) {
|
||||
const cellName = getColumnLabel(col) + (row + 1);
|
||||
const rawValue = multiSheetEngine.getCellRawValue('Report', row, col);
|
||||
const calculatedValue = multiSheetEngine.getCellValue('Report', row, col);
|
||||
|
||||
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Проверьте консоль для подробностей.`);
|
||||
} else {
|
||||
alert('Нет изменений для экспорта.');
|
||||
}
|
||||
};
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
const handleCellDoubleClick = (row: number, col: number) => {
|
||||
setSelectedCell({ row, col });
|
||||
const cells = getCurrentCells();
|
||||
setEditingValue(cells[row][col].value);
|
||||
setIsEditing(true);
|
||||
setTimeout(() => {
|
||||
const inputRefs = activeSheet === 'report' ? reportInputRefs : calculationsInputRefs;
|
||||
const input = inputRefs.current[row]?.[col];
|
||||
if (input) {
|
||||
input.focus();
|
||||
input.setSelectionRange(input.value.length, input.value.length);
|
||||
}
|
||||
}, 10);
|
||||
};
|
||||
|
||||
const handleCellChange = (row: number, col: number, value: string) => {
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
// Обновляем formula bar
|
||||
if (selectedCell && selectedCell.row === row && selectedCell.col === col) {
|
||||
setFormulaBarValue(value);
|
||||
}
|
||||
|
||||
// Обновляем движок
|
||||
multiSheetEngine.setCellValue(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 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;
|
||||
const cells = getCurrentCells();
|
||||
setEditingValue(cells[selectedCell.row][selectedCell.col].value);
|
||||
setIsEditing(true);
|
||||
|
||||
if (focusTarget === 'cell') {
|
||||
setTimeout(() => {
|
||||
const inputRefs = activeSheet === 'report' ? reportInputRefs : calculationsInputRefs;
|
||||
const input = inputRefs.current[selectedCell.row]?.[selectedCell.col];
|
||||
if (input) {
|
||||
input.focus();
|
||||
input.select();
|
||||
}
|
||||
}, 0);
|
||||
} else if (focusTarget === 'formula') {
|
||||
setTimeout(() => {
|
||||
const formulaBarRef = activeSheet === 'report' ? reportFormulaBarRef : calculationsFormulaBarRef;
|
||||
if (formulaBarRef.current) {
|
||||
formulaBarRef.current.focus();
|
||||
formulaBarRef.current.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);
|
||||
}
|
||||
setIsEditing(false);
|
||||
const containerRef = activeSheet === 'report' ? reportContainerRef : calculationsContainerRef;
|
||||
containerRef.current?.focus();
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (!selectedCell) return;
|
||||
|
||||
if (isEditing) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
stopEditing(true);
|
||||
moveSelection(1, 0);
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
stopEditing(false);
|
||||
}
|
||||
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 handleReportResize = (colIndex: number, newWidth: number) => {
|
||||
setReportColumnWidths(prev => {
|
||||
const newWidths = [...prev];
|
||||
newWidths[colIndex] = Math.max(60, newWidth);
|
||||
return newWidths;
|
||||
});
|
||||
};
|
||||
|
||||
const handleCalculationsResize = (colIndex: number, newWidth: number) => {
|
||||
setCalculationsColumnWidths(prev => {
|
||||
const newWidths = [...prev];
|
||||
newWidths[colIndex] = Math.max(60, newWidth);
|
||||
return newWidths;
|
||||
});
|
||||
};
|
||||
|
||||
const renderReportSpreadsheet = () => {
|
||||
const cells = reportCells;
|
||||
const sheetName = 'Report';
|
||||
|
||||
return (
|
||||
<div className="overflow-auto h-full">
|
||||
<table className="border-collapse" style={{ minWidth: `${48 + reportColumnWidths.reduce((a, b) => a + b, 0)}px` }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="w-12 h-8 bg-gray-100 border border-gray-300 text-xs text-gray-600 font-medium"></th>
|
||||
{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 relative"
|
||||
style={{ width: reportColumnWidths[i] }}
|
||||
>
|
||||
{getColumnLabel(i)}
|
||||
<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 = reportColumnWidths[i];
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const newWidth = startWidth + (e.clientX - startX);
|
||||
handleReportResize(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}>
|
||||
<td className="w-12 h-8 bg-gray-100 border border-gray-300 text-xs text-gray-600 font-medium text-center">
|
||||
{rowIndex + 1}
|
||||
</td>
|
||||
{row.map((cell, colIndex) => (
|
||||
<td
|
||||
key={colIndex}
|
||||
className={`border border-gray-300 relative ${
|
||||
selectedCell?.row === rowIndex && selectedCell?.col === colIndex && activeSheet === 'report'
|
||||
? ''
|
||||
: 'hover:bg-gray-50'
|
||||
} ${
|
||||
cell.isModified
|
||||
? 'bg-yellow-50'
|
||||
: ''
|
||||
}`}
|
||||
style={{
|
||||
width: reportColumnWidths[colIndex],
|
||||
height: rowHeights[rowIndex],
|
||||
...(selectedCell?.row === rowIndex && selectedCell?.col === colIndex && activeSheet === 'report'
|
||||
? { 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 && activeSheet === 'report') {
|
||||
return;
|
||||
}
|
||||
if (activeSheet !== 'report') {
|
||||
setActiveSheet('report');
|
||||
}
|
||||
handleCellClick(rowIndex, colIndex);
|
||||
}}
|
||||
onDoubleClick={() => {
|
||||
if (activeSheet !== 'report') {
|
||||
setActiveSheet('report');
|
||||
}
|
||||
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 && activeSheet === 'report'
|
||||
? 'cursor-text'
|
||||
: 'cursor-cell'
|
||||
} ${
|
||||
cell.isModified
|
||||
? 'text-orange-700 font-medium'
|
||||
: ''
|
||||
}`}
|
||||
value={isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex && activeSheet === 'report'
|
||||
? cell.value
|
||||
: String(multiSheetEngine.getCellValue(sheetName, rowIndex, colIndex) || '')}
|
||||
onChange={(e) => {
|
||||
handleCellChange(rowIndex, colIndex, e.target.value);
|
||||
setFormulaBarValue(e.target.value);
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
if (activeSheet !== 'report') {
|
||||
setActiveSheet('report');
|
||||
}
|
||||
setSelectedCell({ row: rowIndex, col: colIndex });
|
||||
if (!isEditing || activeSheet !== 'report') {
|
||||
(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 === 'report') {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
readOnly={!isEditing || selectedCell?.row !== rowIndex || selectedCell?.col !== colIndex || activeSheet !== 'report'}
|
||||
ref={(el) => {
|
||||
if (reportInputRefs.current[rowIndex]) {
|
||||
reportInputRefs.current[rowIndex][colIndex] = el;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderCalculationsSpreadsheet = () => {
|
||||
const cells = calculationsCells;
|
||||
const sheetName = 'Calculations';
|
||||
|
||||
return (
|
||||
<div className="overflow-auto h-full">
|
||||
<table className="border-collapse" style={{ minWidth: `${48 + calculationsColumnWidths.reduce((a, b) => a + b, 0)}px` }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="w-12 h-8 bg-gray-100 border border-gray-300 text-xs text-gray-600 font-medium"></th>
|
||||
{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 relative"
|
||||
style={{ width: calculationsColumnWidths[i] }}
|
||||
>
|
||||
{getColumnLabel(i)}
|
||||
<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 = 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);
|
||||
}}
|
||||
/>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{cells.map((row, rowIndex) => (
|
||||
<tr key={rowIndex}>
|
||||
<td className="w-12 h-8 bg-gray-100 border border-gray-300 text-xs text-gray-600 font-medium text-center">
|
||||
{rowIndex + 1}
|
||||
</td>
|
||||
{row.map((cell, colIndex) => (
|
||||
<td
|
||||
key={colIndex}
|
||||
className={`border border-gray-300 relative ${
|
||||
selectedCell?.row === rowIndex && selectedCell?.col === colIndex && activeSheet === 'calculations'
|
||||
? ''
|
||||
: 'hover:bg-gray-50'
|
||||
}`}
|
||||
style={{
|
||||
width: calculationsColumnWidths[colIndex],
|
||||
height: rowHeights[rowIndex],
|
||||
...(selectedCell?.row === rowIndex && selectedCell?.col === colIndex && activeSheet === 'calculations'
|
||||
? { 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 && activeSheet === 'calculations') {
|
||||
return;
|
||||
}
|
||||
if (activeSheet !== 'calculations') {
|
||||
setActiveSheet('calculations');
|
||||
}
|
||||
handleCellClick(rowIndex, colIndex);
|
||||
}}
|
||||
onDoubleClick={() => {
|
||||
if (activeSheet !== 'calculations') {
|
||||
setActiveSheet('calculations');
|
||||
}
|
||||
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 && activeSheet === 'calculations'
|
||||
? 'cursor-text'
|
||||
: 'cursor-cell'
|
||||
}`}
|
||||
value={isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex && activeSheet === 'calculations'
|
||||
? cell.value
|
||||
: String(multiSheetEngine.getCellValue(sheetName, rowIndex, colIndex) || '')}
|
||||
onChange={(e) => {
|
||||
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;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col bg-gray-50">
|
||||
{/* Заголовок */}
|
||||
<div className="bg-white border-b border-gray-200 p-4 flex-shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-gray-800">Система отчетов</h1>
|
||||
<div className="flex items-center space-x-4 mt-2 text-xs text-gray-600">
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="w-3 h-3 bg-yellow-50 border border-yellow-200 rounded"></div>
|
||||
<span>Измененные ячейки</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="w-3 h-3 bg-white border border-gray-200 rounded"></div>
|
||||
<span>Шаблонные данные</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="text-xs text-gray-500">Ctrl+Z - восстановить шаблон</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={handleExportChanges}
|
||||
className="px-4 py-2 rounded-md text-sm font-medium bg-green-500 text-white hover:bg-green-600 transition-colors"
|
||||
>
|
||||
Экспорт изменений
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Двойная панель */}
|
||||
<div className="flex-1 flex min-h-0">
|
||||
{/* Левая панель - Отчет */}
|
||||
<div className="w-1/2 border-r border-gray-200 flex flex-col">
|
||||
<div className="bg-gray-100 px-4 py-2 border-b border-gray-200 flex-shrink-0">
|
||||
<h2 className="text-sm font-medium text-gray-700">Отчет</h2>
|
||||
</div>
|
||||
|
||||
{/* Formula bar для отчета */}
|
||||
<div className="border-b border-gray-200 p-2 bg-white flex-shrink-0">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="text-sm text-gray-600 min-w-[60px]">
|
||||
{selectedCell && activeSheet === 'report' ? `${getColumnLabel(selectedCell.col)}${selectedCell.row + 1}` : ''}
|
||||
</div>
|
||||
{multiSheetEngine.isCalculating && (
|
||||
<div className="text-xs text-blue-600 animate-pulse">
|
||||
Вычисляется...
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<input
|
||||
ref={reportFormulaBarRef}
|
||||
type="text"
|
||||
className={`w-full px-2 py-1 text-sm border rounded focus:outline-none ${
|
||||
isEditing && activeSheet === 'report'
|
||||
? 'border-blue-400 focus:ring-2 focus:ring-blue-300'
|
||||
: 'border-gray-300 focus:ring-2 focus:ring-blue-500'
|
||||
}`}
|
||||
placeholder="Введите формулу..."
|
||||
value={activeSheet === 'report' ? formulaBarValue : ''}
|
||||
onChange={(e) => {
|
||||
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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={reportContainerRef}
|
||||
className="flex-1 bg-white outline-none overflow-hidden"
|
||||
tabIndex={activeSheet === 'report' ? 0 : -1}
|
||||
onKeyDown={activeSheet === 'report' ? handleKeyDown : undefined}
|
||||
onClick={() => setActiveSheet('report')}
|
||||
>
|
||||
{renderReportSpreadsheet()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Правая панель - Вычисления */}
|
||||
<div className="w-1/2 flex flex-col">
|
||||
<div className="bg-gray-100 px-4 py-2 border-b border-gray-200 flex-shrink-0">
|
||||
<h2 className="text-sm font-medium text-gray-700">Вычисления</h2>
|
||||
</div>
|
||||
|
||||
{/* Formula bar для вычислений */}
|
||||
<div className="border-b border-gray-200 p-2 bg-white flex-shrink-0">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="text-sm text-gray-600 min-w-[60px]">
|
||||
{selectedCell && activeSheet === 'calculations' ? `${getColumnLabel(selectedCell.col)}${selectedCell.row + 1}` : ''}
|
||||
</div>
|
||||
{multiSheetEngine.isCalculating && (
|
||||
<div className="text-xs text-blue-600 animate-pulse">
|
||||
Вычисляется...
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<input
|
||||
ref={calculationsFormulaBarRef}
|
||||
type="text"
|
||||
className={`w-full px-2 py-1 text-sm border rounded focus:outline-none ${
|
||||
isEditing && activeSheet === 'calculations'
|
||||
? 'border-blue-400 focus:ring-2 focus:ring-blue-300'
|
||||
: 'border-gray-300 focus:ring-2 focus:ring-blue-500'
|
||||
}`}
|
||||
placeholder="Введите формулу..."
|
||||
value={activeSheet === 'calculations' ? formulaBarValue : ''}
|
||||
onChange={(e) => {
|
||||
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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={calculationsContainerRef}
|
||||
className="flex-1 bg-white outline-none overflow-hidden"
|
||||
tabIndex={activeSheet === 'calculations' ? 0 : -1}
|
||||
onKeyDown={activeSheet === 'calculations' ? handleKeyDown : undefined}
|
||||
onClick={() => setActiveSheet('calculations')}
|
||||
>
|
||||
{renderCalculationsSpreadsheet()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DualSpreadsheet;
|
||||
Reference in New Issue
Block a user