diff --git a/src/components/DualSpreadsheet/DualSpreadsheet.tsx b/src/components/DualSpreadsheet/DualSpreadsheet.tsx index c8677f4..bee583a 100644 --- a/src/components/DualSpreadsheet/DualSpreadsheet.tsx +++ b/src/components/DualSpreadsheet/DualSpreadsheet.tsx @@ -1,13 +1,13 @@ import { produce } from 'immer' import { - CSSProperties, - FC, - memo, - useCallback, - useEffect, - useMemo, - useRef, - useState, + CSSProperties, + FC, + memo, + useCallback, + useEffect, + useMemo, + useRef, + useState, } from 'react' import { VariableSizeGrid } from 'react-window' import { useMultiSheetEngine } from '../../hooks/useMultiSheetEngine' @@ -181,7 +181,7 @@ const FormulaBar = memo( formulaBarRef, }: Omit) => { // Теперь FormulaBar всегда отображается, так как он один - + return (
@@ -331,10 +331,14 @@ interface SpreadsheetProps { stopEditing: (save: boolean) => void setActiveSheet: (type: SheetType) => void activeCellInputRef: React.RefObject - containerRefs: React.MutableRefObject> + containerRefs: React.MutableRefObject< + Record + > handleKeyDown: (e: React.KeyboardEvent) => void headerRefs: React.MutableRefObject> - rowHeaderRefs: React.MutableRefObject> + rowHeaderRefs: React.MutableRefObject< + Record + > gridRefs: React.MutableRefObject> onGridScroll: ( sheetType: SheetType, @@ -412,7 +416,9 @@ const Spreadsheet = ({
setActiveSheet(sheetType)} >
(headerRefs.current[sheetType] = el)} - className="overflow-x-hidden column-headers" + className="column-headers overflow-x-hidden" >
{Array.from({ length: COL_COUNT }).map((_, i) => { @@ -492,7 +498,7 @@ const Spreadsheet = ({ > {getColumnLabel(i)}
@@ -502,7 +508,7 @@ const Spreadsheet = ({
{/* Grid */} -
+
(gridRefs.current[sheetType] = el)} columnCount={COL_COUNT} @@ -559,7 +565,7 @@ const DualSpreadsheet: FC = ({ templateData = {} }) => { calculations: null, }) const activeCellInputRef = useRef(null) - const formulaBarRef = useRef(null); + const formulaBarRef = useRef(null) const headerRefs = useRef>({ report: null, calculations: null, @@ -910,63 +916,64 @@ const DualSpreadsheet: FC = ({ templateData = {} }) => { }) // --- Spreadsheet Component --- - useEffect(() => { - if (!separatorRef) return; - + if (!separatorRef) return + const handleMouseDown = (e: MouseEvent) => { - e.preventDefault(); - document.body.style.cursor = 'col-resize'; - + e.preventDefault() + document.body.style.cursor = 'col-resize' + const handleMouseMove = (me: MouseEvent) => { - const container = separatorRef.parentElement; - if (!container) return; - const bounds = container.getBoundingClientRect(); - const newLeftWidth = me.clientX - bounds.left; - const totalWidth = bounds.width; - - let newLeftPercentage = newLeftWidth / totalWidth; + const container = separatorRef.parentElement + if (!container) return + const bounds = container.getBoundingClientRect() + const newLeftWidth = me.clientX - bounds.left + const totalWidth = bounds.width + + let newLeftPercentage = newLeftWidth / totalWidth // Ограничиваем минимальную ширину (например 20%) - newLeftPercentage = Math.max(0.2, Math.min(0.8, newLeftPercentage)); - + newLeftPercentage = Math.max(0.2, Math.min(0.8, newLeftPercentage)) + setSpreadsheetWidths({ L: newLeftPercentage, R: 1 - newLeftPercentage, - }); - }; - + }) + } + const handleMouseUp = () => { - document.body.style.cursor = 'auto'; - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); - }; - - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleMouseUp); - }; - - separatorRef.addEventListener('mousedown', handleMouseDown); - + document.body.style.cursor = 'auto' + document.removeEventListener('mousemove', handleMouseMove) + document.removeEventListener('mouseup', handleMouseUp) + } + + document.addEventListener('mousemove', handleMouseMove) + document.addEventListener('mouseup', handleMouseUp) + } + + separatorRef.addEventListener('mousedown', handleMouseDown) + return () => { - separatorRef.removeEventListener('mousedown', handleMouseDown); - }; - }, [separatorRef]); - + separatorRef.removeEventListener('mousedown', handleMouseDown) + } + }, [separatorRef]) + return (
- { formulaBarRef.current = el; }} - /> -
+ { + formulaBarRef.current = el + }} + /> +
= ({ templateData = {} }) => { onGridScroll={onGridScroll} handleColumnResize={handleColumnResize} /> -
-
+
= ({ templateData = {} }) => { ) } -export default DualSpreadsheet \ No newline at end of file +export default DualSpreadsheet diff --git a/src/components/TemplateManager/TemplateEditor.tsx b/src/components/TemplateManager/TemplateEditor.tsx index 78115a2..f5a90e5 100644 --- a/src/components/TemplateManager/TemplateEditor.tsx +++ b/src/components/TemplateManager/TemplateEditor.tsx @@ -23,6 +23,8 @@ export const TemplateEditor: React.FC = ({ const [excelData, setExcelData] = useState>( template.excelData || {}, ) + const [isLoading, setIsLoading] = useState(false) + const [dataVersion, setDataVersion] = useState(0) // Для принудительного обновления const fileInputRef = useRef(null) const navigate = useNavigate() @@ -30,7 +32,9 @@ export const TemplateEditor: React.FC = ({ const file = event.target.files?.[0] if (!file) return + setIsLoading(true) const reader = new FileReader() + reader.onload = (e) => { try { const data = new Uint8Array(e.target?.result as ArrayBuffer) @@ -39,38 +43,94 @@ export const TemplateEditor: React.FC = ({ // Читаем первый лист const firstSheetName = workbook.SheetNames[0] const worksheet = workbook.Sheets[firstSheetName] - const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) + + // Получаем диапазон ячеек + const range = XLSX.utils.decode_range(worksheet['!ref'] || 'A1:A1') // Преобразуем в формат для нашей системы const formattedData: Record = {} + const mergedCells: Array<{ from: string; to: string; value: any }> = [] - ;(jsonData as any[][]).forEach((row, rowIndex) => { - row.forEach((cell, colIndex) => { - if (cell !== undefined && cell !== null && cell !== '') { - const cellName = - String.fromCharCode(65 + colIndex) + (rowIndex + 1) - formattedData[cellName] = cell + // Обрабатываем объединенные ячейки + const merges = worksheet['!merges'] || [] + + // Сначала читаем обычные ячейки + for (let R = range.s.r; R <= range.e.r; ++R) { + for (let C = range.s.c; C <= range.e.c; ++C) { + const cellAddress = XLSX.utils.encode_cell({ r: R, c: C }) + const cell = worksheet[cellAddress] + + if ( + cell && + cell.v !== undefined && + cell.v !== null && + cell.v !== '' + ) { + formattedData[cellAddress] = cell.v } + } + } + + // Обрабатываем объединенные ячейки + merges.forEach((merge) => { + const startCell = XLSX.utils.encode_cell({ + r: merge.s.r, + c: merge.s.c, }) + const endCell = XLSX.utils.encode_cell({ r: merge.e.r, c: merge.e.c }) + const startCellData = worksheet[startCell] + + if (startCellData && startCellData.v !== undefined) { + // Записываем значение в первую ячейку объединенного диапазона + formattedData[startCell] = startCellData.v + + // Сохраняем информацию об объединении для UI + mergedCells.push({ + from: startCell, + to: endCell, + value: startCellData.v, + }) + + // Заполняем остальные ячейки в объединенном диапазоне пустыми значениями + // но помечаем их как часть объединения + for (let R = merge.s.r; R <= merge.e.r; ++R) { + for (let C = merge.s.c; C <= merge.e.c; ++C) { + const cellAddr = XLSX.utils.encode_cell({ r: R, c: C }) + if (cellAddr !== startCell) { + formattedData[cellAddr] = '' // Пустое значение для объединенных ячеек + } + } + } + } }) - setExcelData(formattedData) + // Обновляем состояние + const newData = { ...formattedData } + setExcelData(newData) setEditedTemplate((prev) => ({ ...prev, excelFile: file, - excelData: formattedData, + excelData: newData, + mergedCells: mergedCells, // Сохраняем информацию об объединенных ячейках updatedAt: new Date(), })) - // Принудительно обновляем данные в таблице - setTimeout(() => { - setExcelData({ ...formattedData }) - }, 100) + // Принудительно обновляем версию данных для перерендера + setDataVersion((prev) => prev + 1) + + setIsLoading(false) } catch (error) { console.error('Ошибка при чтении Excel файла:', error) alert('Ошибка при чтении Excel файла') + setIsLoading(false) } } + + reader.onerror = () => { + alert('Ошибка при загрузке файла') + setIsLoading(false) + } + reader.readAsArrayBuffer(file) } @@ -156,11 +216,23 @@ export const TemplateEditor: React.FC = ({ size="sm" onClick={() => fileInputRef.current?.click()} className="flex h-7 items-center gap-2" + disabled={isLoading} > - - - {editedTemplate.excelFile ? 'Заменить файл' : 'Загрузить Excel'} - + {isLoading ? ( + <> +
+ Загрузка... + + ) : ( + <> + + + {editedTemplate.excelFile + ? 'Заменить файл' + : 'Загрузить Excel'} + + + )}
@@ -168,7 +240,7 @@ export const TemplateEditor: React.FC = ({ {/* Таблицы - теперь занимают всё оставшееся пространство */}
- +
) diff --git a/src/types/template.ts b/src/types/template.ts index 266c48f..bada3a1 100644 --- a/src/types/template.ts +++ b/src/types/template.ts @@ -52,6 +52,13 @@ export interface TemplateElement { layout?: ElementLayout // позиция и размеры в визуальном редакторе } +// Информация об объединенных ячейках +export interface MergedCell { + from: string // начальная ячейка (например, "A1") + to: string // конечная ячейка (например, "C3") + value: any // значение объединенной ячейки +} + // Типы шаблонов export interface Template { id: string @@ -59,6 +66,7 @@ export interface Template { description?: string excelFile?: File excelData?: Record // данные из Excel + mergedCells?: MergedCell[] // информация об объединенных ячейках elements: TemplateElement[] layoutSettings?: FormLayoutSettings // настройки отображения формы createdAt: Date