загрузка excel

This commit is contained in:
2025-07-18 10:52:48 +03:00
parent 021368f173
commit 8b67db832c
3 changed files with 171 additions and 84 deletions

View File

@@ -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<FormulaBarProps, 'sheetType' | 'activeSheet'>) => {
// Теперь FormulaBar всегда отображается, так как он один
return (
<div className="flex-shrink-0 border-b bg-background px-3 py-1">
<div className="flex items-center space-x-2">
@@ -331,10 +331,14 @@ interface SpreadsheetProps {
stopEditing: (save: boolean) => void
setActiveSheet: (type: SheetType) => void
activeCellInputRef: React.RefObject<HTMLInputElement>
containerRefs: React.MutableRefObject<Record<SheetType, HTMLDivElement | null>>
containerRefs: React.MutableRefObject<
Record<SheetType, HTMLDivElement | null>
>
handleKeyDown: (e: React.KeyboardEvent) => void
headerRefs: React.MutableRefObject<Record<SheetType, HTMLDivElement | null>>
rowHeaderRefs: React.MutableRefObject<Record<SheetType, HTMLDivElement | null>>
rowHeaderRefs: React.MutableRefObject<
Record<SheetType, HTMLDivElement | null>
>
gridRefs: React.MutableRefObject<Record<SheetType, VariableSizeGrid | null>>
onGridScroll: (
sheetType: SheetType,
@@ -412,7 +416,9 @@ const Spreadsheet = ({
<div
ref={parentRef}
className="flex min-w-0 flex-col overflow-hidden"
style={{ flexBasis: `${spreadsheetWidths[SHEET_NAMES[sheetType]] * 100}%` }}
style={{
flexBasis: `${spreadsheetWidths[SHEET_NAMES[sheetType]] * 100}%`,
}}
onClick={() => setActiveSheet(sheetType)}
>
<div
@@ -462,7 +468,7 @@ const Spreadsheet = ({
{/* Column Headers */}
<div
ref={(el) => (headerRefs.current[sheetType] = el)}
className="overflow-x-hidden column-headers"
className="column-headers overflow-x-hidden"
>
<div className="flex">
{Array.from({ length: COL_COUNT }).map((_, i) => {
@@ -492,7 +498,7 @@ const Spreadsheet = ({
>
{getColumnLabel(i)}
<div
className="absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-primary hover:bg-opacity-50 resizer"
className="resizer absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-primary hover:bg-opacity-50"
onMouseDown={handleMouseDown}
/>
</div>
@@ -502,7 +508,7 @@ const Spreadsheet = ({
</div>
{/* Grid */}
<div className="flex-1 min-h-0">
<div className="min-h-0 flex-1">
<VariableSizeGrid
ref={(el) => (gridRefs.current[sheetType] = el)}
columnCount={COL_COUNT}
@@ -559,7 +565,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
calculations: null,
})
const activeCellInputRef = useRef<HTMLInputElement>(null)
const formulaBarRef = useRef<HTMLInputElement | null>(null);
const formulaBarRef = useRef<HTMLInputElement | null>(null)
const headerRefs = useRef<Record<SheetType, HTMLDivElement | null>>({
report: null,
calculations: null,
@@ -910,63 +916,64 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ 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 (
<div className="flex h-full flex-col bg-background">
<FormulaBar
selectedCell={selectedCell}
formulaBarValue={formulaBarValue}
isCalculating={multiSheetEngine.isCalculating}
isEditing={isEditing}
onValueChange={handleFormulaBarChange}
onFocus={handleFormulaBarFocus}
onBlur={handleFormulaBarBlur}
onKeyDown={handleFormulaBarKeyDown}
formulaBarRef={(el) => { formulaBarRef.current = el; }}
/>
<div className="flex flex-1 min-w-0 gap-1 overflow-hidden p-1">
<FormulaBar
selectedCell={selectedCell}
formulaBarValue={formulaBarValue}
isCalculating={multiSheetEngine.isCalculating}
isEditing={isEditing}
onValueChange={handleFormulaBarChange}
onFocus={handleFormulaBarFocus}
onBlur={handleFormulaBarBlur}
onKeyDown={handleFormulaBarKeyDown}
formulaBarRef={(el) => {
formulaBarRef.current = el
}}
/>
<div className="flex min-w-0 flex-1 gap-1 overflow-hidden p-1">
<Spreadsheet
sheetType="report"
columnWidths={columnWidths.report}
@@ -990,11 +997,11 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
onGridScroll={onGridScroll}
handleColumnResize={handleColumnResize}
/>
<div
ref={setSeparatorRef}
className="flex items-center justify-center w-2 cursor-col-resize bg-border hover:bg-primary/20 transition-colors"
<div
ref={setSeparatorRef}
className="flex w-2 cursor-col-resize items-center justify-center bg-border transition-colors hover:bg-primary/20"
>
<div className="w-0.5 h-4 bg-muted-foreground/50 rounded-full" />
<div className="h-4 w-0.5 rounded-full bg-muted-foreground/50" />
</div>
<Spreadsheet
sheetType="calculations"
@@ -1024,4 +1031,4 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
)
}
export default DualSpreadsheet
export default DualSpreadsheet

View File

@@ -23,6 +23,8 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
const [excelData, setExcelData] = useState<Record<string, any>>(
template.excelData || {},
)
const [isLoading, setIsLoading] = useState(false)
const [dataVersion, setDataVersion] = useState(0) // Для принудительного обновления
const fileInputRef = useRef<HTMLInputElement>(null)
const navigate = useNavigate()
@@ -30,7 +32,9 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
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<TemplateEditorProps> = ({
// Читаем первый лист
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<string, any> = {}
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<TemplateEditorProps> = ({
size="sm"
onClick={() => fileInputRef.current?.click()}
className="flex h-7 items-center gap-2"
disabled={isLoading}
>
<Upload className="h-3 w-3" />
<span className="text-xs">
{editedTemplate.excelFile ? 'Заменить файл' : 'Загрузить Excel'}
</span>
{isLoading ? (
<>
<div className="h-3 w-3 animate-spin rounded-full border-2 border-current border-t-transparent" />
<span className="text-xs">Загрузка...</span>
</>
) : (
<>
<Upload className="h-3 w-3" />
<span className="text-xs">
{editedTemplate.excelFile
? 'Заменить файл'
: 'Загрузить Excel'}
</span>
</>
)}
</Button>
</div>
</div>
@@ -168,7 +240,7 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
{/* Таблицы - теперь занимают всё оставшееся пространство */}
<div className="min-h-0 flex-1">
<DualSpreadsheet templateData={{ Report: excelData }} />
<DualSpreadsheet key={dataVersion} templateData={{ L: excelData }} />
</div>
</div>
)

View File

@@ -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<string, any> // данные из Excel
mergedCells?: MergedCell[] // информация об объединенных ячейках
elements: TemplateElement[]
layoutSettings?: FormLayoutSettings // настройки отображения формы
createdAt: Date