index.tsx 6.86 KB
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<LabelTemplate>(() => {
    if (initialTemplate) return { ...initialTemplate };
    return createDefaultTemplate(templateId ?? undefined);
  });
  const [selectedId, setSelectedId] = useState<string | null>(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<typeof createDefaultElement>[0],
    configOverride?: Partial<Record<string, unknown>>
  ) => {
    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<LabelElement>) => {
    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<LabelTemplate>) => {
    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 (
    <div className="label-template-editor flex flex-col h-full min-h-0 bg-gray-50">
      {/* Toolbar */}
      <div className="flex items-center gap-3 px-4 py-2.5 border-b border-gray-200 bg-white shrink-0">
        <Button variant="outline" size="sm" onClick={onClose} className="border-gray-200 bg-white hover:bg-gray-50">
          <ArrowLeft className="w-4 h-4 mr-1" />
          Back
        </Button>
        <span className="text-sm font-medium text-gray-800 truncate flex-1">
          {template.name}
        </span>
        <Select value={presetSelectValue} onValueChange={(v) => { setPresetSelectValue(v); handleApplyPreset(v); }}>
          <SelectTrigger className="w-[200px] h-8 text-xs bg-white border-gray-200">
            <SelectValue placeholder="Load preset template" />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="none">Load preset template</SelectItem>
            {PRESET_TEMPLATES.map((p, i) => (
              <SelectItem key={i} value={String(i)}>
                {p.name}
              </SelectItem>
            ))}
          </SelectContent>
        </Select>
        <Button size="sm" onClick={handleExport} variant="outline" className="border-gray-200 bg-white hover:bg-gray-50">
          <Download className="w-4 h-4 mr-1" />
          Export JSON
        </Button>
        <Button
          size="sm"
          className="bg-[#1e3a8a] text-white hover:bg-[#1e3a8a]/90 shrink-0"
          onClick={handleSave}
        >
          <Save className="w-4 h-4 mr-1" />
          Save
        </Button>
      </div>

      {/* Three columns - items-stretch ensures canvas bottom aligns with Elements bottom */}
      <div className="flex flex-1 min-h-0 gap-0 items-stretch">
        <div className="flex-shrink-0 flex flex-col">
          <ElementsPanel onAddElement={addElement} />
        </div>
        <div className="flex-1 min-w-0 min-h-0 border-x border-gray-200 bg-gray-100/50 flex flex-col">
          <LabelCanvas
          template={template}
          selectedId={selectedId}
          onSelect={setSelectedId}
          onUpdateElement={updateElement}
          onDeleteElement={deleteElement}
          onTemplateChange={handleTemplateChange}
          scale={scale}
          onZoomIn={() => setScale((s) => Math.min(MAX_SCALE, s + SCALE_STEP))}
          onZoomOut={() => setScale((s) => Math.max(MIN_SCALE, s - SCALE_STEP))}
          onPreview={() => setPreviewOpen(true)}
        />
        </div>
        <Dialog open={previewOpen} onOpenChange={setPreviewOpen}>
          <DialogContent className="max-w-[90vw] max-h-[90vh] overflow-auto">
            <DialogHeader>
              <DialogTitle>Label Preview</DialogTitle>
            </DialogHeader>
            <LabelPreviewOnly template={template} maxWidth={500} />
          </DialogContent>
        </Dialog>
        <div className="flex-shrink-0 flex flex-col" style={{ width: 288 }}>
          <PropertiesPanel
            template={template}
            selectedElement={selectedElement}
            onTemplateChange={handleTemplateChange}
            onElementChange={updateElement}
            onDeleteElement={deleteElement}
          />
        </div>
      </div>
    </div>
  );
}