import React, { useState, useCallback, useEffect, useRef, useMemo } from 'react'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '../ui/table'; import { Input } from '../ui/input'; import { Button } from '../ui/button'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '../ui/select'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '../ui/dialog'; import { Plus, Pencil, MoreHorizontal } from 'lucide-react'; import { toast } from 'sonner'; import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, } from '../ui/pagination'; import { getLabelTemplates, getLabelTemplate, deleteLabelTemplate } from '../../services/labelTemplateService'; import { getLocations } from '../../services/locationService'; import { appliedLocationToEditor, type LabelTemplateDto } from '../../types/labelTemplate'; import { LabelTemplateEditor } from './LabelTemplateEditor'; import type { LabelTemplate } from '../../types/labelTemplate'; import type { LocationDto } from '../../types/location'; function toDisplay(v: string | null | undefined): string { const s = (v ?? "").trim(); return s ? s : "None"; } function locationColumnText(t: LabelTemplateDto, locations: LocationDto[]): string { const mode = appliedLocationToEditor(t); if (mode === "ALL") return "All"; const ids = t.appliedLocationIds ?? []; if (ids.length === 0) return "Specified (0)"; const names = ids.map( (id) => locations.find((l) => l.id === id)?.locationName?.trim() || id, ); if (names.length <= 2) return names.join(", "); return `${names.slice(0, 2).join(", ")} +${names.length - 2}`; } /** 列表行:名称列 ← templateName / name */ function templateListDisplayName(t: LabelTemplateDto): string { const n = (t.templateName ?? t.name ?? "").trim(); return n ? n : "None"; } /** 列表行:模板编码列 ← templateCode / id */ function templateListDisplayCode(t: LabelTemplateDto): string { const c = (t.templateCode ?? t.id ?? "").trim(); return c ? c : "None"; } /** 列表行:门店展示 ← locationText,缺省时再推导 */ function templateListDisplayLocation(t: LabelTemplateDto, locations: LocationDto[]): string { const lt = (t.locationText ?? "").trim(); if (lt) return lt; return locationColumnText(t, locations); } /** 列表行:元素数量 ← contentsCount / elements.length */ function templateListContentsCount(t: LabelTemplateDto): number { if (typeof t.contentsCount === "number") return t.contentsCount; return t.elements?.length ?? 0; } /** 列表行:尺寸 ← sizeText,缺省时用 width×height unit */ function templateListDisplaySize(t: LabelTemplateDto): string { const st = (t.sizeText ?? "").trim(); if (st) return st; const w = t.width; const h = t.height; const u = t.unit; if (w != null && h != null && u) return `${w}×${h} ${u}`; return "None"; } export function LabelTemplatesView() { const [templates, setTemplates] = useState([]); const [viewMode, setViewMode] = useState<'list' | 'editor'>('list'); const [editingTemplateId, setEditingTemplateId] = useState(null); const [initialTemplate, setInitialTemplate] = useState(null); const [loading, setLoading] = useState(false); const [total, setTotal] = useState(0); const [refreshSeq, setRefreshSeq] = useState(0); const [actionsOpenForId, setActionsOpenForId] = useState(null); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [deletingTemplate, setDeletingTemplate] = useState(null); const [keyword, setKeyword] = useState(''); const [locationFilter, setLocationFilter] = useState('all'); const [labelTypeFilter, setLabelTypeFilter] = useState('all'); const [stateFilter, setStateFilter] = useState('all'); const [pageIndex, setPageIndex] = useState(1); const [pageSize, setPageSize] = useState(10); const [locations, setLocations] = useState([]); const abortRef = useRef(null); const keywordTimerRef = useRef(null); const [debouncedKeyword, setDebouncedKeyword] = useState(''); useEffect(() => { if (keywordTimerRef.current) window.clearTimeout(keywordTimerRef.current); keywordTimerRef.current = window.setTimeout(() => setDebouncedKeyword(keyword.trim()), 300); return () => { if (keywordTimerRef.current) window.clearTimeout(keywordTimerRef.current); }; }, [keyword]); const totalPages = Math.max(1, Math.ceil(total / pageSize)); useEffect(() => { let cancelled = false; (async () => { try { const res = await getLocations({ skipCount: 0, maxResultCount: 500 }); if (!cancelled) setLocations(res.items ?? []); } catch { if (!cancelled) setLocations([]); } })(); return () => { cancelled = true; }; }, []); useEffect(() => { setPageIndex(1); }, [debouncedKeyword, locationFilter, labelTypeFilter, stateFilter, pageSize]); useEffect(() => { if (viewMode !== 'list') return; const run = async () => { abortRef.current?.abort(); const ac = new AbortController(); abortRef.current = ac; setLoading(true); try { const skipCount = (pageIndex - 1) * pageSize; const res = await getLabelTemplates( { skipCount, maxResultCount: pageSize, keyword: debouncedKeyword || undefined, locationId: locationFilter !== 'all' ? locationFilter : undefined, labelType: labelTypeFilter !== 'all' ? (labelTypeFilter as any) : undefined, state: stateFilter === 'all' ? undefined : stateFilter === 'true', }, ac.signal, ); setTemplates(res.items ?? []); setTotal(res.totalCount ?? 0); } catch (e: any) { if (e?.name === 'AbortError') return; toast.error('Failed to load label templates.', { description: e?.message ? String(e.message) : 'Please try again.', }); setTemplates([]); setTotal(0); } finally { setLoading(false); } }; run(); return () => abortRef.current?.abort(); }, [debouncedKeyword, locationFilter, labelTypeFilter, stateFilter, pageIndex, pageSize, refreshSeq, viewMode]); const refreshList = () => setRefreshSeq((x) => x + 1); const handleNewTemplate = () => { setEditingTemplateId(null); setInitialTemplate(null); setViewMode('editor'); }; const handleEditTemplate = async (templateCode: string) => { setEditingTemplateId(templateCode); setLoading(true); try { const apiTemplate = await getLabelTemplate(templateCode); // 转换 API 返回的 DTO 到编辑器需要的格式 const editorTemplate: LabelTemplate = { id: apiTemplate.id, name: (apiTemplate.name ?? apiTemplate.templateName ?? '').trim() || '未命名模板', labelType: (apiTemplate.labelType as any) ?? 'PRICE', unit: (apiTemplate.unit as any) ?? 'cm', width: apiTemplate.width ?? 6, height: apiTemplate.height ?? 4, appliedLocation: appliedLocationToEditor(apiTemplate), appliedLocationIds: [...(apiTemplate.appliedLocationIds ?? [])], showRuler: apiTemplate.showRuler ?? true, showGrid: apiTemplate.showGrid ?? true, elements: (apiTemplate.elements ?? []) as LabelTemplate['elements'], }; setInitialTemplate(editorTemplate); setViewMode('editor'); } catch (e: any) { toast.error('Failed to load template.', { description: e?.message ? String(e.message) : 'Please try again.', }); } finally { setLoading(false); } }; const handleCloseEditor = () => { setViewMode('list'); setEditingTemplateId(null); setInitialTemplate(null); }; const openDelete = (template: LabelTemplateDto) => { setActionsOpenForId(null); setDeletingTemplate(template); setIsDeleteDialogOpen(true); }; if (viewMode === 'editor') { return (
); } return (
setKeyword(e.target.value)} style={{ height: 40, boxSizing: 'border-box' }} className="bg-white border border-gray-300 rounded-md w-40 shrink-0 placeholder:text-gray-500" />
Label Template Template Code Location Label Type Contents Size Actions {loading ? ( Loading... ) : templates.length === 0 ? ( No templates yet. Click "New Label Template" to create one. ) : ( templates.map((t) => ( {templateListDisplayName(t)} {templateListDisplayCode(t)} {toDisplay(templateListDisplayLocation(t, locations))} {toDisplay(t.labelType)} {templateListContentsCount(t)} element(s) {templateListDisplaySize(t)} setActionsOpenForId(open ? t.id : null)} > )) )}
Showing {total === 0 ? 0 : (pageIndex - 1) * pageSize + 1}- {Math.min(pageIndex * pageSize, total)} of {total}
{ e.preventDefault(); setPageIndex((p) => Math.max(1, p - 1)); }} aria-disabled={pageIndex <= 1} className={pageIndex <= 1 ? "pointer-events-none opacity-50" : ""} /> e.preventDefault()} > Page {pageIndex} / {totalPages} { e.preventDefault(); setPageIndex((p) => Math.min(totalPages, p + 1)); }} aria-disabled={pageIndex >= totalPages} className={pageIndex >= totalPages ? "pointer-events-none opacity-50" : ""} />
{ setIsDeleteDialogOpen(open); if (!open) setDeletingTemplate(null); }} onDeleted={refreshList} />
); } function DeleteLabelTemplateDialog({ open, template, onOpenChange, onDeleted, }: { open: boolean; template: LabelTemplateDto | null; onOpenChange: (open: boolean) => void; onDeleted: () => void; }) { const [submitting, setSubmitting] = useState(false); const name = useMemo(() => { const n = (template?.templateName ?? template?.name ?? "").trim(); return n || (template?.templateCode ?? template?.id ?? "").trim() || "this template"; }, [template]); const submit = async () => { if (!template?.id) return; setSubmitting(true); try { await deleteLabelTemplate(template.id); toast.success("Label template deleted.", { description: "The label template has been removed successfully.", }); onOpenChange(false); onDeleted(); } catch (e: any) { toast.error("Failed to delete label template.", { description: e?.message ? String(e.message) : "Please try again.", }); } finally { setSubmitting(false); } }; return ( Delete Label Template This action cannot be undone.
Are you sure you want to delete {name}?
); }