загрузка excel
This commit is contained in:
@@ -1,13 +1,13 @@
|
|||||||
import { produce } from 'immer'
|
import { produce } from 'immer'
|
||||||
import {
|
import {
|
||||||
CSSProperties,
|
CSSProperties,
|
||||||
FC,
|
FC,
|
||||||
memo,
|
memo,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { VariableSizeGrid } from 'react-window'
|
import { VariableSizeGrid } from 'react-window'
|
||||||
import { useMultiSheetEngine } from '../../hooks/useMultiSheetEngine'
|
import { useMultiSheetEngine } from '../../hooks/useMultiSheetEngine'
|
||||||
@@ -181,7 +181,7 @@ const FormulaBar = memo(
|
|||||||
formulaBarRef,
|
formulaBarRef,
|
||||||
}: Omit<FormulaBarProps, 'sheetType' | 'activeSheet'>) => {
|
}: Omit<FormulaBarProps, 'sheetType' | 'activeSheet'>) => {
|
||||||
// Теперь FormulaBar всегда отображается, так как он один
|
// Теперь FormulaBar всегда отображается, так как он один
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-shrink-0 border-b bg-background px-3 py-1">
|
<div className="flex-shrink-0 border-b bg-background px-3 py-1">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
@@ -331,10 +331,14 @@ interface SpreadsheetProps {
|
|||||||
stopEditing: (save: boolean) => void
|
stopEditing: (save: boolean) => void
|
||||||
setActiveSheet: (type: SheetType) => void
|
setActiveSheet: (type: SheetType) => void
|
||||||
activeCellInputRef: React.RefObject<HTMLInputElement>
|
activeCellInputRef: React.RefObject<HTMLInputElement>
|
||||||
containerRefs: React.MutableRefObject<Record<SheetType, HTMLDivElement | null>>
|
containerRefs: React.MutableRefObject<
|
||||||
|
Record<SheetType, HTMLDivElement | null>
|
||||||
|
>
|
||||||
handleKeyDown: (e: React.KeyboardEvent) => void
|
handleKeyDown: (e: React.KeyboardEvent) => void
|
||||||
headerRefs: React.MutableRefObject<Record<SheetType, HTMLDivElement | null>>
|
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>>
|
gridRefs: React.MutableRefObject<Record<SheetType, VariableSizeGrid | null>>
|
||||||
onGridScroll: (
|
onGridScroll: (
|
||||||
sheetType: SheetType,
|
sheetType: SheetType,
|
||||||
@@ -412,7 +416,9 @@ const Spreadsheet = ({
|
|||||||
<div
|
<div
|
||||||
ref={parentRef}
|
ref={parentRef}
|
||||||
className="flex min-w-0 flex-col overflow-hidden"
|
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)}
|
onClick={() => setActiveSheet(sheetType)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -462,7 +468,7 @@ const Spreadsheet = ({
|
|||||||
{/* Column Headers */}
|
{/* Column Headers */}
|
||||||
<div
|
<div
|
||||||
ref={(el) => (headerRefs.current[sheetType] = el)}
|
ref={(el) => (headerRefs.current[sheetType] = el)}
|
||||||
className="overflow-x-hidden column-headers"
|
className="column-headers overflow-x-hidden"
|
||||||
>
|
>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
{Array.from({ length: COL_COUNT }).map((_, i) => {
|
{Array.from({ length: COL_COUNT }).map((_, i) => {
|
||||||
@@ -492,7 +498,7 @@ const Spreadsheet = ({
|
|||||||
>
|
>
|
||||||
{getColumnLabel(i)}
|
{getColumnLabel(i)}
|
||||||
<div
|
<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}
|
onMouseDown={handleMouseDown}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -502,7 +508,7 @@ const Spreadsheet = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Grid */}
|
{/* Grid */}
|
||||||
<div className="flex-1 min-h-0">
|
<div className="min-h-0 flex-1">
|
||||||
<VariableSizeGrid
|
<VariableSizeGrid
|
||||||
ref={(el) => (gridRefs.current[sheetType] = el)}
|
ref={(el) => (gridRefs.current[sheetType] = el)}
|
||||||
columnCount={COL_COUNT}
|
columnCount={COL_COUNT}
|
||||||
@@ -559,7 +565,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
|
|||||||
calculations: null,
|
calculations: null,
|
||||||
})
|
})
|
||||||
const activeCellInputRef = useRef<HTMLInputElement>(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>>({
|
const headerRefs = useRef<Record<SheetType, HTMLDivElement | null>>({
|
||||||
report: null,
|
report: null,
|
||||||
calculations: null,
|
calculations: null,
|
||||||
@@ -910,63 +916,64 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// --- Spreadsheet Component ---
|
// --- Spreadsheet Component ---
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!separatorRef) return;
|
if (!separatorRef) return
|
||||||
|
|
||||||
const handleMouseDown = (e: MouseEvent) => {
|
const handleMouseDown = (e: MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
document.body.style.cursor = 'col-resize';
|
document.body.style.cursor = 'col-resize'
|
||||||
|
|
||||||
const handleMouseMove = (me: MouseEvent) => {
|
const handleMouseMove = (me: MouseEvent) => {
|
||||||
const container = separatorRef.parentElement;
|
const container = separatorRef.parentElement
|
||||||
if (!container) return;
|
if (!container) return
|
||||||
const bounds = container.getBoundingClientRect();
|
const bounds = container.getBoundingClientRect()
|
||||||
const newLeftWidth = me.clientX - bounds.left;
|
const newLeftWidth = me.clientX - bounds.left
|
||||||
const totalWidth = bounds.width;
|
const totalWidth = bounds.width
|
||||||
|
|
||||||
let newLeftPercentage = newLeftWidth / totalWidth;
|
let newLeftPercentage = newLeftWidth / totalWidth
|
||||||
// Ограничиваем минимальную ширину (например 20%)
|
// Ограничиваем минимальную ширину (например 20%)
|
||||||
newLeftPercentage = Math.max(0.2, Math.min(0.8, newLeftPercentage));
|
newLeftPercentage = Math.max(0.2, Math.min(0.8, newLeftPercentage))
|
||||||
|
|
||||||
setSpreadsheetWidths({
|
setSpreadsheetWidths({
|
||||||
L: newLeftPercentage,
|
L: newLeftPercentage,
|
||||||
R: 1 - newLeftPercentage,
|
R: 1 - newLeftPercentage,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
const handleMouseUp = () => {
|
||||||
document.body.style.cursor = 'auto';
|
document.body.style.cursor = 'auto'
|
||||||
document.removeEventListener('mousemove', handleMouseMove);
|
document.removeEventListener('mousemove', handleMouseMove)
|
||||||
document.removeEventListener('mouseup', handleMouseUp);
|
document.removeEventListener('mouseup', handleMouseUp)
|
||||||
};
|
}
|
||||||
|
|
||||||
document.addEventListener('mousemove', handleMouseMove);
|
document.addEventListener('mousemove', handleMouseMove)
|
||||||
document.addEventListener('mouseup', handleMouseUp);
|
document.addEventListener('mouseup', handleMouseUp)
|
||||||
};
|
}
|
||||||
|
|
||||||
separatorRef.addEventListener('mousedown', handleMouseDown);
|
separatorRef.addEventListener('mousedown', handleMouseDown)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
separatorRef.removeEventListener('mousedown', handleMouseDown);
|
separatorRef.removeEventListener('mousedown', handleMouseDown)
|
||||||
};
|
}
|
||||||
}, [separatorRef]);
|
}, [separatorRef])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col bg-background">
|
<div className="flex h-full flex-col bg-background">
|
||||||
<FormulaBar
|
<FormulaBar
|
||||||
selectedCell={selectedCell}
|
selectedCell={selectedCell}
|
||||||
formulaBarValue={formulaBarValue}
|
formulaBarValue={formulaBarValue}
|
||||||
isCalculating={multiSheetEngine.isCalculating}
|
isCalculating={multiSheetEngine.isCalculating}
|
||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
onValueChange={handleFormulaBarChange}
|
onValueChange={handleFormulaBarChange}
|
||||||
onFocus={handleFormulaBarFocus}
|
onFocus={handleFormulaBarFocus}
|
||||||
onBlur={handleFormulaBarBlur}
|
onBlur={handleFormulaBarBlur}
|
||||||
onKeyDown={handleFormulaBarKeyDown}
|
onKeyDown={handleFormulaBarKeyDown}
|
||||||
formulaBarRef={(el) => { formulaBarRef.current = el; }}
|
formulaBarRef={(el) => {
|
||||||
/>
|
formulaBarRef.current = el
|
||||||
<div className="flex flex-1 min-w-0 gap-1 overflow-hidden p-1">
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex min-w-0 flex-1 gap-1 overflow-hidden p-1">
|
||||||
<Spreadsheet
|
<Spreadsheet
|
||||||
sheetType="report"
|
sheetType="report"
|
||||||
columnWidths={columnWidths.report}
|
columnWidths={columnWidths.report}
|
||||||
@@ -990,11 +997,11 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
|
|||||||
onGridScroll={onGridScroll}
|
onGridScroll={onGridScroll}
|
||||||
handleColumnResize={handleColumnResize}
|
handleColumnResize={handleColumnResize}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
ref={setSeparatorRef}
|
ref={setSeparatorRef}
|
||||||
className="flex items-center justify-center w-2 cursor-col-resize bg-border hover:bg-primary/20 transition-colors"
|
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>
|
</div>
|
||||||
<Spreadsheet
|
<Spreadsheet
|
||||||
sheetType="calculations"
|
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>>(
|
const [excelData, setExcelData] = useState<Record<string, any>>(
|
||||||
template.excelData || {},
|
template.excelData || {},
|
||||||
)
|
)
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [dataVersion, setDataVersion] = useState(0) // Для принудительного обновления
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
@@ -30,7 +32,9 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
|
|||||||
const file = event.target.files?.[0]
|
const file = event.target.files?.[0]
|
||||||
if (!file) return
|
if (!file) return
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
|
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
try {
|
try {
|
||||||
const data = new Uint8Array(e.target?.result as ArrayBuffer)
|
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 firstSheetName = workbook.SheetNames[0]
|
||||||
const worksheet = workbook.Sheets[firstSheetName]
|
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 formattedData: Record<string, any> = {}
|
||||||
|
const mergedCells: Array<{ from: string; to: string; value: any }> = []
|
||||||
|
|
||||||
;(jsonData as any[][]).forEach((row, rowIndex) => {
|
// Обрабатываем объединенные ячейки
|
||||||
row.forEach((cell, colIndex) => {
|
const merges = worksheet['!merges'] || []
|
||||||
if (cell !== undefined && cell !== null && cell !== '') {
|
|
||||||
const cellName =
|
// Сначала читаем обычные ячейки
|
||||||
String.fromCharCode(65 + colIndex) + (rowIndex + 1)
|
for (let R = range.s.r; R <= range.e.r; ++R) {
|
||||||
formattedData[cellName] = cell
|
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) => ({
|
setEditedTemplate((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
excelFile: file,
|
excelFile: file,
|
||||||
excelData: formattedData,
|
excelData: newData,
|
||||||
|
mergedCells: mergedCells, // Сохраняем информацию об объединенных ячейках
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Принудительно обновляем данные в таблице
|
// Принудительно обновляем версию данных для перерендера
|
||||||
setTimeout(() => {
|
setDataVersion((prev) => prev + 1)
|
||||||
setExcelData({ ...formattedData })
|
|
||||||
}, 100)
|
setIsLoading(false)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при чтении Excel файла:', error)
|
console.error('Ошибка при чтении Excel файла:', error)
|
||||||
alert('Ошибка при чтении Excel файла')
|
alert('Ошибка при чтении Excel файла')
|
||||||
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reader.onerror = () => {
|
||||||
|
alert('Ошибка при загрузке файла')
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
reader.readAsArrayBuffer(file)
|
reader.readAsArrayBuffer(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,11 +216,23 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
className="flex h-7 items-center gap-2"
|
className="flex h-7 items-center gap-2"
|
||||||
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
<Upload className="h-3 w-3" />
|
{isLoading ? (
|
||||||
<span className="text-xs">
|
<>
|
||||||
{editedTemplate.excelFile ? 'Заменить файл' : 'Загрузить Excel'}
|
<div className="h-3 w-3 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||||
</span>
|
<span className="text-xs">Загрузка...</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Upload className="h-3 w-3" />
|
||||||
|
<span className="text-xs">
|
||||||
|
{editedTemplate.excelFile
|
||||||
|
? 'Заменить файл'
|
||||||
|
: 'Загрузить Excel'}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -168,7 +240,7 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
|
|||||||
|
|
||||||
{/* Таблицы - теперь занимают всё оставшееся пространство */}
|
{/* Таблицы - теперь занимают всё оставшееся пространство */}
|
||||||
<div className="min-h-0 flex-1">
|
<div className="min-h-0 flex-1">
|
||||||
<DualSpreadsheet templateData={{ Report: excelData }} />
|
<DualSpreadsheet key={dataVersion} templateData={{ L: excelData }} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -52,6 +52,13 @@ export interface TemplateElement {
|
|||||||
layout?: ElementLayout // позиция и размеры в визуальном редакторе
|
layout?: ElementLayout // позиция и размеры в визуальном редакторе
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Информация об объединенных ячейках
|
||||||
|
export interface MergedCell {
|
||||||
|
from: string // начальная ячейка (например, "A1")
|
||||||
|
to: string // конечная ячейка (например, "C3")
|
||||||
|
value: any // значение объединенной ячейки
|
||||||
|
}
|
||||||
|
|
||||||
// Типы шаблонов
|
// Типы шаблонов
|
||||||
export interface Template {
|
export interface Template {
|
||||||
id: string
|
id: string
|
||||||
@@ -59,6 +66,7 @@ export interface Template {
|
|||||||
description?: string
|
description?: string
|
||||||
excelFile?: File
|
excelFile?: File
|
||||||
excelData?: Record<string, any> // данные из Excel
|
excelData?: Record<string, any> // данные из Excel
|
||||||
|
mergedCells?: MergedCell[] // информация об объединенных ячейках
|
||||||
elements: TemplateElement[]
|
elements: TemplateElement[]
|
||||||
layoutSettings?: FormLayoutSettings // настройки отображения формы
|
layoutSettings?: FormLayoutSettings // настройки отображения формы
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
|
|||||||
Reference in New Issue
Block a user