загрузка 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

@@ -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,
@@ -911,47 +917,46 @@ 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">
@@ -964,9 +969,11 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
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}
@@ -992,9 +999,9 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
/> />
<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"

View File

@@ -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}
> >
{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" /> <Upload className="h-3 w-3" />
<span className="text-xs"> <span className="text-xs">
{editedTemplate.excelFile ? 'Заменить файл' : 'Загрузить Excel'} {editedTemplate.excelFile
? 'Заменить файл'
: 'Загрузить Excel'}
</span> </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>
) )

View File

@@ -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