2 окна и настройка элементов
This commit is contained in:
@@ -17,8 +17,8 @@ type SheetType = 'report' | 'calculations';
|
||||
const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
|
||||
const multiSheetEngine = useMultiSheetEngine({
|
||||
sheets: [
|
||||
{ name: 'Report', rows: 20, cols: 10 },
|
||||
{ name: 'Calculations', rows: 20, cols: 10 }
|
||||
{ name: 'Report', rows: 50, cols: 20 },
|
||||
{ name: 'Calculations', rows: 50, cols: 20 }
|
||||
],
|
||||
initialData: templateData || {}
|
||||
});
|
||||
@@ -27,16 +27,16 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
|
||||
|
||||
// Объединенные состояния для обеих таблиц
|
||||
const [sheetData, setSheetData] = useState<Record<SheetType, CellData[][]>>({
|
||||
report: Array(20).fill(null).map(() =>
|
||||
Array(10).fill(null).map(() => ({
|
||||
report: Array(50).fill(null).map(() =>
|
||||
Array(20).fill(null).map(() => ({
|
||||
value: '',
|
||||
isSelected: false,
|
||||
isModified: false,
|
||||
templateValue: ''
|
||||
}))
|
||||
),
|
||||
calculations: Array(20).fill(null).map(() =>
|
||||
Array(10).fill(null).map(() => ({
|
||||
calculations: Array(50).fill(null).map(() =>
|
||||
Array(20).fill(null).map(() => ({
|
||||
value: '',
|
||||
isSelected: false,
|
||||
isModified: false
|
||||
@@ -52,11 +52,11 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
|
||||
|
||||
// Объединенные размеры колонок
|
||||
const [columnWidths, setColumnWidths] = useState<Record<SheetType, number[]>>({
|
||||
report: Array(10).fill(96),
|
||||
calculations: Array(10).fill(96)
|
||||
report: Array(20).fill(96),
|
||||
calculations: Array(20).fill(96)
|
||||
});
|
||||
|
||||
const [rowHeights] = useState<number[]>(Array(20).fill(32));
|
||||
const [rowHeights] = useState<number[]>(Array(50).fill(32));
|
||||
|
||||
// Объединенные refs
|
||||
const containerRefs = useRef<Record<SheetType, HTMLDivElement | null>>({
|
||||
@@ -76,8 +76,8 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
|
||||
|
||||
// Инициализация refs
|
||||
useEffect(() => {
|
||||
inputRefs.current.report = Array(20).fill(null).map(() => Array(10).fill(null));
|
||||
inputRefs.current.calculations = Array(20).fill(null).map(() => Array(10).fill(null));
|
||||
inputRefs.current.report = Array(50).fill(null).map(() => Array(20).fill(null));
|
||||
inputRefs.current.calculations = Array(50).fill(null).map(() => Array(20).fill(null));
|
||||
}, []);
|
||||
|
||||
// Загрузка шаблонных данных
|
||||
@@ -99,16 +99,16 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
|
||||
}));
|
||||
|
||||
// Загружаем данные в движок
|
||||
Object.entries(templateData.Report).forEach(([cellName, value]) => {
|
||||
const match = cellName.match(/^([A-Z]+)(\d+)$/);
|
||||
if (match) {
|
||||
const col = match[1].charCodeAt(0) - 65;
|
||||
const row = parseInt(match[2]) - 1;
|
||||
if (row >= 0 && row < 20 && col >= 0 && col < 10) {
|
||||
multiSheetEngine.setCellValue('Report', row, col, String(value));
|
||||
Object.entries(templateData.Report).forEach(([cellName, value]) => {
|
||||
const match = cellName.match(/^([A-Z]+)(\d+)$/);
|
||||
if (match) {
|
||||
const col = match[1].charCodeAt(0) - 65;
|
||||
const row = parseInt(match[2]) - 1;
|
||||
if (row >= 0 && row < 50 && col >= 0 && col < 20) {
|
||||
multiSheetEngine.setCellValue('Report', row, col, String(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [templateData]);
|
||||
|
||||
@@ -264,8 +264,8 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
|
||||
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));
|
||||
const newRow = Math.max(0, Math.min(49, selectedCell.row + deltaRow));
|
||||
const newCol = Math.max(0, Math.min(19, selectedCell.col + deltaCol));
|
||||
|
||||
setSelectedCell({ row: newRow, col: newCol });
|
||||
setIsEditing(false);
|
||||
@@ -413,17 +413,26 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
|
||||
|
||||
// Универсальный рендер таблицы
|
||||
const renderSpreadsheet = (sheetType: SheetType) => {
|
||||
const cells = sheetData[sheetType];
|
||||
const sheetName = sheetType === 'report' ? 'Report' : 'Calculations';
|
||||
const data = sheetData[sheetType];
|
||||
const widths = columnWidths[sheetType];
|
||||
|
||||
return (
|
||||
<div className="overflow-auto h-full">
|
||||
<table className="border-collapse" style={{ minWidth: `${48 + widths.reduce((a, b) => a + b, 0)}px` }}>
|
||||
<div
|
||||
ref={el => containerRefs.current[sheetType] = el}
|
||||
className="bg-white rounded-lg shadow-md overflow-auto border border-gray-300"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
tabIndex={0}
|
||||
>
|
||||
<table className="w-full border-collapse" style={{ tableLayout: 'fixed' }}>
|
||||
<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) => (
|
||||
{Array.from({ length: 20 }, (_, i) => (
|
||||
<th
|
||||
key={i}
|
||||
className="h-8 bg-gray-100 border border-gray-300 text-xs text-gray-600 font-medium text-center relative"
|
||||
@@ -456,101 +465,65 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{cells.map((row, rowIndex) => (
|
||||
{data.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
|
||||
<td
|
||||
key={colIndex}
|
||||
className={`border border-gray-300 relative ${
|
||||
selectedCell?.row === rowIndex && selectedCell?.col === colIndex && activeSheet === sheetType
|
||||
? ''
|
||||
: 'hover:bg-gray-50'
|
||||
className={`border border-gray-200 ${
|
||||
(selectedCell?.row === rowIndex && selectedCell?.col === colIndex && activeSheet === sheetType) ? 'bg-blue-100' : ''
|
||||
} ${
|
||||
cell.isModified ? 'bg-yellow-50' : ''
|
||||
}`}
|
||||
style={{
|
||||
width: widths[colIndex],
|
||||
height: rowHeights[rowIndex],
|
||||
...(selectedCell?.row === rowIndex && selectedCell?.col === colIndex && activeSheet === sheetType
|
||||
? { 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'
|
||||
}
|
||||
: {})
|
||||
style={{
|
||||
minWidth: `${widths[colIndex]}px`,
|
||||
width: `${widths[colIndex]}px`,
|
||||
height: `${rowHeights[rowIndex]}px`,
|
||||
overflow: 'hidden',
|
||||
padding: 0,
|
||||
position: 'relative'
|
||||
}}
|
||||
onClick={() => {
|
||||
if (isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex && activeSheet === sheetType) {
|
||||
return;
|
||||
}
|
||||
if (activeSheet !== sheetType) {
|
||||
setActiveSheet(sheetType);
|
||||
}
|
||||
if (activeSheet !== sheetType) setActiveSheet(sheetType);
|
||||
handleCellClick(rowIndex, colIndex);
|
||||
}}
|
||||
onDoubleClick={() => {
|
||||
if (activeSheet !== sheetType) {
|
||||
setActiveSheet(sheetType);
|
||||
}
|
||||
handleCellDoubleClick(rowIndex, colIndex);
|
||||
}}
|
||||
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 && activeSheet === sheetType
|
||||
? 'cursor-text'
|
||||
: 'cursor-cell'
|
||||
} ${
|
||||
cell.isModified ? 'text-orange-700 font-medium' : ''
|
||||
}`}
|
||||
value={isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex && activeSheet === sheetType
|
||||
? cell.value
|
||||
: String(multiSheetEngine.getCellValue(sheetName, rowIndex, colIndex) || '')}
|
||||
onChange={(e) => {
|
||||
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 !== sheetType) {
|
||||
setActiveSheet(sheetType);
|
||||
}
|
||||
setSelectedCell({ row: rowIndex, col: colIndex });
|
||||
if (!isEditing || activeSheet !== sheetType) {
|
||||
(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 === sheetType) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
readOnly={!isEditing || selectedCell?.row !== rowIndex || selectedCell?.col !== colIndex || activeSheet !== sheetType}
|
||||
ref={(el) => {
|
||||
if (inputRefs.current[sheetType][rowIndex]) {
|
||||
inputRefs.current[sheetType][rowIndex][colIndex] = el;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex ? (
|
||||
<input
|
||||
ref={el => {
|
||||
if (inputRefs.current[sheetType][rowIndex]) {
|
||||
inputRefs.current[sheetType][rowIndex][colIndex] = el;
|
||||
}
|
||||
}}
|
||||
type="text"
|
||||
value={sheetData[sheetType][rowIndex][colIndex].value}
|
||||
onChange={(e) => handleCellChange(rowIndex, colIndex, e.target.value)}
|
||||
onBlur={() => stopEditing(true)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
stopEditing(true);
|
||||
} else if (e.key === 'Escape') {
|
||||
stopEditing(false);
|
||||
}
|
||||
}}
|
||||
className="absolute top-0 left-0 w-full h-full p-2 border-2 border-blue-500 z-20 box-border"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="relative w-full h-full p-2 truncate"
|
||||
style={{
|
||||
...(selectedCell?.row === rowIndex && selectedCell?.col === colIndex && activeSheet === sheetType
|
||||
? { boxShadow: 'inset 0 0 0 2px #3b82f6' }
|
||||
: {})
|
||||
}}
|
||||
>
|
||||
{String(multiSheetEngine.getCellValue(sheetType === 'report' ? 'Report' : 'Calculations', rowIndex, colIndex) || '')}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
@@ -562,73 +535,36 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
|
||||
};
|
||||
|
||||
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>
|
||||
<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 className="flex flex-col h-full space-y-4 p-4 bg-gray-100">
|
||||
{/* Двойная панель */}
|
||||
<div className="flex-1 flex min-h-0">
|
||||
<div className="flex-grow flex space-x-4 overflow-hidden">
|
||||
{/* Левая панель - Отчет */}
|
||||
<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>
|
||||
<FormulaBar sheetType="report" />
|
||||
<div
|
||||
ref={(el) => 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')}
|
||||
>
|
||||
{renderSpreadsheet('report')}
|
||||
</div>
|
||||
<div className="w-1/2 flex flex-col">
|
||||
<h2 className="text-lg font-semibold mb-2">Отчет</h2>
|
||||
{renderSpreadsheet('report')}
|
||||
</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>
|
||||
<FormulaBar sheetType="calculations" />
|
||||
<div
|
||||
ref={(el) => 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')}
|
||||
>
|
||||
{renderSpreadsheet('calculations')}
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold mb-2">Расчеты</h2>
|
||||
{renderSpreadsheet('calculations')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0">
|
||||
<FormulaBar sheetType={activeSheet} />
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0 flex justify-end items-center">
|
||||
<button
|
||||
onClick={handleExportChanges}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
Экспорт изменений
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DualSpreadsheet;
|
||||
export default DualSpreadsheet;
|
||||
Reference in New Issue
Block a user