import React, { useCallback, useState } from 'react'; import { Button } from '../../ui/button'; import { ArrowLeft, Save, Download } from 'lucide-react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from '../../ui/dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '../../ui/select'; import type { LabelTemplate, LabelElement } from '../../../types/labelTemplate'; import { createDefaultTemplate, createDefaultElement } from '../../../types/labelTemplate'; import { ElementsPanel } from './ElementsPanel'; import { LabelCanvas, LabelPreviewOnly } from './LabelCanvas'; import { PropertiesPanel } from './PropertiesPanel'; import { saveTemplate } from '../../../lib/labelTemplateStorage'; import { PRESET_TEMPLATES, presetToTemplate } from '../../../lib/presetLabelTemplates'; const MIN_SCALE = 0.5; const MAX_SCALE = 2; const SCALE_STEP = 0.25; const DEFAULT_SCALE = 1.0; interface LabelTemplateEditorProps { /** null = 新建,string = 编辑该 id */ templateId: string | null; initialTemplate: LabelTemplate | null; onClose: () => void; onSaved: () => void; } export function LabelTemplateEditor({ templateId, initialTemplate, onClose, onSaved, }: LabelTemplateEditorProps) { const [template, setTemplate] = useState(() => { if (initialTemplate) return { ...initialTemplate }; return createDefaultTemplate(templateId ?? undefined); }); const [selectedId, setSelectedId] = useState(null); const [scale, setScale] = useState(DEFAULT_SCALE); const [previewOpen, setPreviewOpen] = useState(false); const selectedElement = template.elements.find((el) => el.id === selectedId) ?? null; const addElement = useCallback(( type: Parameters[0], configOverride?: Partial> ) => { const el = createDefaultElement(type, 30, 30); if (configOverride && Object.keys(configOverride).length > 0) { el.config = { ...el.config, ...configOverride }; } setTemplate((prev) => ({ ...prev, elements: [...prev.elements, el], })); setSelectedId(el.id); }, []); const updateElement = useCallback((id: string, patch: Partial) => { setTemplate((prev) => ({ ...prev, elements: prev.elements.map((el) => el.id === id ? { ...el, ...patch } : el ), })); }, []); const deleteElement = useCallback((id: string) => { setTemplate((prev) => ({ ...prev, elements: prev.elements.filter((el) => el.id !== id), })); setSelectedId(null); }, []); const handleTemplateChange = useCallback((patch: Partial) => { setTemplate((prev) => ({ ...prev, ...patch })); }, []); const handleSave = useCallback(() => { saveTemplate(template); onSaved(); onClose(); }, [template, onSaved, onClose]); const handleExport = useCallback(() => { const blob = new Blob([JSON.stringify(template, null, 2)], { type: 'application/json', }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `label-template-${template.id}.json`; a.click(); URL.revokeObjectURL(url); }, [template]); const [presetSelectValue, setPresetSelectValue] = useState('none'); const handleApplyPreset = useCallback((index: string) => { if (index === 'none') return; const idx = parseInt(index, 10); if (isNaN(idx) || idx < 0 || idx >= PRESET_TEMPLATES.length) return; const preset = PRESET_TEMPLATES[idx]; const newTemplate = presetToTemplate(preset, template.id); setTemplate(newTemplate); setSelectedId(null); setPresetSelectValue('none'); }, [template.id]); return (
{/* Toolbar */}
{template.name}
{/* Three columns - items-stretch ensures canvas bottom aligns with Elements bottom */}
setScale((s) => Math.min(MAX_SCALE, s + SCALE_STEP))} onZoomOut={() => setScale((s) => Math.max(MIN_SCALE, s - SCALE_STEP))} onPreview={() => setPreviewOpen(true)} />
Label Preview
); }