ручной рефакторинг
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user