index.tsx 5.07 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 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';

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]);

  return (
    <div className="flex flex-col h-full min-h-0">
      {/* Toolbar */}
      <div className="flex items-center gap-2 px-4 py-2 border-b border-gray-200 bg-white shrink-0">
        <Button variant="outline" size="sm" onClick={onClose}>
          <ArrowLeft className="w-4 h-4 mr-1" />
          Back
        </Button>
        <span className="text-sm font-medium text-gray-700 truncate flex-1">
          {template.name}
        </span>
        <Button size="sm" onClick={handleExport} variant="outline">
          <Download className="w-4 h-4 mr-1" />
          Export JSON
        </Button>
        <Button
          size="sm"
          className="bg-blue-600 hover:bg-blue-700 text-white"
          onClick={handleSave}
        >
          <Save className="w-4 h-4 mr-1" />
          Save
        </Button>
      </div>

      {/* Three columns */}
      <div className="flex flex-1 min-h-0">
        <ElementsPanel onAddElement={addElement} />
        <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)}
        />
        <Dialog open={previewOpen} onOpenChange={setPreviewOpen}>
          <DialogContent className="max-w-[90vw] max-h-[90vh] overflow-auto">
            <DialogHeader>
              <DialogTitle>标签预览</DialogTitle>
            </DialogHeader>
            <LabelPreviewOnly template={template} maxWidth={500} />
          </DialogContent>
        </Dialog>
        <PropertiesPanel
          template={template}
          selectedElement={selectedElement}
          onTemplateChange={handleTemplateChange}
          onElementChange={updateElement}
          onDeleteElement={deleteElement}
        />
      </div>
    </div>
  );
}