ручной рефакторинг

This commit is contained in:
2025-07-20 09:11:46 +03:00
parent 1d92316db7
commit da7024cd08
8 changed files with 489 additions and 462 deletions

View File

@@ -1,25 +1,10 @@
import {
AlignCenter,
AlignLeft,
AlignRight,
ArrowLeft,
Bold,
FileText,
Grid,
Italic,
Palette,
Type,
Underline,
Upload,
Wrench,
} from 'lucide-react'
import React, { useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import * as XLSX from 'xlsx'
import { Template } from '../../types/template'
import DualSpreadsheet from '../DualSpreadsheet/DualSpreadsheet'
import { Button } from '../ui/button'
import { Separator } from '../ui/separator'
import { parseExcelFile } from '@/services/excelSevice'
import { Template } from '@/types/template'
import React, { useState } from 'react'
import { ExcelUploadPanel } from './ExcelUploadPanel'
import { FormattingToolbar } from './FormattingToolbar'
import { HeaderBar } from './HeaderBar'
import { SpreadsheetViewer } from './SpreadsheetViewer'
interface TemplateEditorProps {
template: Template
@@ -30,284 +15,56 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
template,
onBack,
}) => {
const [editedTemplate, setEditedTemplate] = useState<Template>(template)
const [editedTemplate, setEditedTemplate] = useState(template)
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 navigate = useNavigate()
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (!file) {
console.log('No file selected')
return
}
const [dataVersion, setDataVersion] = useState(0)
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.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)
const workbook = XLSX.read(data, { type: 'array' })
// Читаем первый лист
const firstSheetName = workbook.SheetNames[0]
const worksheet = workbook.Sheets[firstSheetName]
// Получаем диапазон ячеек
const range = XLSX.utils.decode_range(worksheet['!ref'] || 'A1:A1')
// Преобразуем в формат для нашей системы
const formattedData: Record<string, any> = {}
const mergedCells: Array<{ from: string; to: string; value: any }> = []
// Обрабатываем объединенные ячейки
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] = '' // Пустое значение для объединенных ячеек
}
}
}
}
})
// Обновляем состояние
const newData = { ...formattedData }
setExcelData(newData)
setEditedTemplate((prev) => ({
...prev,
excelFile: file,
excelData: newData,
mergedCells: mergedCells, // Сохраняем информацию об объединенных ячейках
updatedAt: new Date(),
}))
// Принудительно обновляем версию данных для перерендера
setDataVersion((prev) => prev + 1)
setIsLoading(false)
} catch (error) {
console.error('Ошибка при чтении Excel файла:', error)
alert('Ошибка при чтении Excel файла')
setIsLoading(false)
}
}
reader.onerror = () => {
alert('Ошибка при загрузке файла')
try {
const { data, mergedCells } = await parseExcelFile(file)
setExcelData(data)
setEditedTemplate(prev => ({
...prev,
excelFile: file,
excelData: data,
mergedCells,
updatedAt: new Date(),
}))
setDataVersion(v => v + 1)
} catch (err) {
console.error(err)
alert('Ошибка при чтении Excel файла')
} finally {
setIsLoading(false)
}
reader.readAsArrayBuffer(file)
}
const handleCreateElements = () => {
navigate(`/templates/${editedTemplate.id}/elements`)
}
return (
<div className="flex h-screen flex-col bg-background">
{/* Заголовок */}
<div className="flex-shrink-0 border-b bg-card px-4 py-1">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Button variant="ghost" size="icon" onClick={onBack}>
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<h1 className="text-lg font-semibold">{editedTemplate.name}</h1>
<p className="mt-0.5 text-xs text-muted-foreground">
Настройте Excel шаблон и значения отчета
</p>
</div>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={handleCreateElements}
className="flex items-center gap-2"
>
<Wrench className="h-3.5 w-3.5" />
Элементы интерфейса
</Button>
<Button
variant="outline"
size="sm"
onClick={() =>
navigate(`/templates/${editedTemplate.id}/protocols`)
}
className="flex items-center gap-2"
>
<FileText className="h-3.5 w-3.5" />
Создать протокол
</Button>
</div>
</div>
</div>
{/* Компактная панель загрузки Excel и инструментов */}
<HeaderBar template={editedTemplate} onBack={onBack} />
<div className="flex-shrink-0 border-b bg-muted/30 px-4 py-2">
<div className="flex items-center justify-between">
{/* Левая часть - панель инструментов стилизации */}
<div className="flex items-center gap-0.5 rounded-lg border bg-background p-0.5">
{/* Группа текстового форматирования */}
<div className="flex items-center">
<Button variant="ghost" size="sm" className="h-6 w-6 rounded p-0">
<Bold className="h-3 w-3" />
</Button>
<Button variant="ghost" size="sm" className="h-6 w-6 rounded p-0">
<Italic className="h-3 w-3" />
</Button>
<Button variant="ghost" size="sm" className="h-6 w-6 rounded p-0">
<Underline className="h-3 w-3" />
</Button>
</div>
<Separator orientation="vertical" className="mx-0.5 h-4" />
{/* Группа выравнивания */}
<div className="flex items-center">
<Button variant="ghost" size="sm" className="h-6 w-6 rounded p-0">
<AlignLeft className="h-3 w-3" />
</Button>
<Button variant="ghost" size="sm" className="h-6 w-6 rounded p-0">
<AlignCenter className="h-3 w-3" />
</Button>
<Button variant="ghost" size="sm" className="h-6 w-6 rounded p-0">
<AlignRight className="h-3 w-3" />
</Button>
</div>
<Separator orientation="vertical" className="mx-0.5 h-4" />
{/* Группа цвета и стилей */}
<div className="flex items-center">
<Button variant="ghost" size="sm" className="h-6 w-6 rounded p-0">
<Palette className="h-3 w-3" />
</Button>
<Button variant="ghost" size="sm" className="h-6 w-6 rounded p-0">
<Type className="h-3 w-3" />
</Button>
<Button variant="ghost" size="sm" className="h-6 w-6 rounded p-0">
<Grid className="h-3 w-3" />
</Button>
</div>
</div>
{/* Правая часть - загрузка Excel */}
<div className="flex items-center gap-2">
<input
ref={fileInputRef}
type="file"
accept=".xlsx,.xls"
onChange={handleFileUpload}
className="hidden"
/>
{/* Статус файла - всегда отображается */}
<div
className={`flex items-center gap-1.5 rounded-md border px-2 py-1 ${
editedTemplate.excelFile
? 'border-emerald-200 bg-emerald-50'
: 'border-gray-200 bg-gray-50'
}`}
>
<FileText
className={`h-3 w-3 ${
editedTemplate.excelFile
? 'text-emerald-600'
: 'text-gray-400'
}`}
/>
<span
className={`max-w-32 truncate text-xs font-medium ${
editedTemplate.excelFile
? 'text-emerald-700'
: 'text-gray-500'
}`}
>
{editedTemplate.excelFile
? editedTemplate.excelFile.name
: 'Файл не выбран'}
</span>
</div>
{/* Кнопка загрузки - всегда отображается */}
<Button
variant="outline"
size="sm"
onClick={() => fileInputRef.current?.click()}
className="h-7 rounded-md px-2"
disabled={isLoading}
>
{isLoading ? (
<div className="h-3 w-3 animate-spin rounded-full border-2 border-current border-t-transparent" />
) : (
<Upload className="h-3 w-3" />
)}
</Button>
</div>
<FormattingToolbar />
<ExcelUploadPanel
fileName={editedTemplate.excelFile?.name}
isLoading={isLoading}
onUploadClick={() => {}}
onFileChange={handleFileChange}
/>
</div>
</div>
{/* Таблицы - теперь занимают всё оставшееся пространство */}
<div className="min-h-0 flex-1">
<DualSpreadsheet
key={dataVersion}
templateData={{ L: excelData }}
mergedCells={editedTemplate.mergedCells}
templateId={`template-${editedTemplate.id}`}
/>
</div>
<SpreadsheetViewer
data={excelData}
mergedCells={editedTemplate.mergedCells || []}
dataVersion={dataVersion}
templateId={editedTemplate.id}
/>
</div>
)
}