загрузка excel
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user