This commit is contained in:
Artyom Tsyrulnikov
2025-07-13 16:51:51 +03:00
parent 36bdb4b2be
commit e1fe966bcd
8 changed files with 1227 additions and 10 deletions

View 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\роверьте консоль для подробностей.`);
} 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;

View File

@@ -0,0 +1 @@
export { default as DualSpreadsheet } from './DualSpreadsheet';