"use client"; import * as React from "react"; import { LibraryIcon, PlusIcon, SaveIcon, Settings2Icon, Trash2Icon } from "lucide-react"; import { toast } from "sonner"; import { AssetInterceptRulesEditor } from "@/components/asset-intercept-rules-editor"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@/components/ui/combobox"; import { Field, FieldDescription, FieldGroup, FieldLabel } from "@/components/ui/field"; import { Input } from "@/components/ui/input"; import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle } from "@/components/ui/sheet"; import { Spinner } from "@/components/ui/spinner"; import { Textarea } from "@/components/ui/textarea"; import { api } from "@/lib/api"; import type { AssetInterceptRuleInput, TaskCategory, TaskTemplate } from "@/lib/types"; import { cn } from "@/lib/utils"; interface TemplateDraft { name: string; description: string; goal: string; categoryID: number | null; interceptRules: AssetInterceptRuleInput[]; } // TemplateSeed 是「另存为模板」时从创建表单带入的初值。 type TemplateSeed = Pick; interface TaskTemplateManagerProps { open: boolean; onOpenChange: (open: boolean) => void; templates: TaskTemplate[]; seed: TemplateSeed | null; onCreated: (template: TaskTemplate) => void; onUpdated: (template: TaskTemplate) => void; onDeleted: (id: number) => void; } const emptyDraft = (): TemplateDraft => ({ name: "", description: "", goal: "", categoryID: null, interceptRules: [], }); function templateDraft(template: TaskTemplate): TemplateDraft { return { name: template.name, description: template.description, goal: template.goal, categoryID: template.category_id ?? null, interceptRules: template.intercept_rules ?? [], }; } function TaskTemplateManager({ open, onOpenChange, templates, seed, onCreated, onUpdated, onDeleted, }: TaskTemplateManagerProps) { const [selectedID, setSelectedID] = React.useState(null); const [draft, setDraft] = React.useState(emptyDraft); const [saving, setSaving] = React.useState(false); const [deleteOpen, setDeleteOpen] = React.useState(false); const [deleting, setDeleting] = React.useState(false); const [categories, setCategories] = React.useState([]); const wasOpen = React.useRef(false); React.useEffect(() => { if (!open) return; api .taskCategories() .then(setCategories) .catch(() => setCategories([])); }, [open]); React.useEffect(() => { if (open && !wasOpen.current) { if (seed) { setSelectedID(null); setDraft({ name: "", description: seed.description, goal: seed.goal, categoryID: seed.categoryID, interceptRules: seed.interceptRules, }); } else if (templates[0]) { setSelectedID(templates[0].id); setDraft(templateDraft(templates[0])); } else { setSelectedID(null); setDraft(emptyDraft()); } } wasOpen.current = open; }, [open, seed, templates]); const selectTemplate = (template: TaskTemplate) => { setSelectedID(template.id); setDraft(templateDraft(template)); }; const startNew = () => { setSelectedID(null); setDraft(emptyDraft()); }; const updateDraft = (field: "name" | "description" | "goal", value: string) => { setDraft((current) => ({ ...current, [field]: value })); }; const patchDraft = (patch: Partial) => { setDraft((current) => ({ ...current, ...patch })); }; async function save() { const input = { name: draft.name.trim(), description: draft.description.trim(), goal: draft.goal.trim(), category_id: draft.categoryID, intercept_rules: draft.interceptRules .map((r) => ({ ...r, pattern: r.pattern.trim() })) .filter((r) => r.pattern !== ""), }; if (!input.name || !input.description || !input.goal) { toast.error("请填写模板名称、描述和目标"); return; } setSaving(true); try { if (selectedID == null) { const created = await api.createTaskTemplate(input); onCreated(created); setSelectedID(created.id); setDraft(templateDraft(created)); toast.success("模板已创建"); } else { const updated = await api.updateTaskTemplate(selectedID, input); onUpdated(updated); setDraft(templateDraft(updated)); toast.success("模板已更新"); } } catch (error) { toast.error(`保存失败:${(error as Error).message}`); } finally { setSaving(false); } } async function remove() { if (selectedID == null) return; const deletedID = selectedID; setDeleting(true); try { await api.deleteTaskTemplate(deletedID); onDeleted(deletedID); const next = templates.find((template) => template.id !== deletedID); if (next) { setSelectedID(next.id); setDraft(templateDraft(next)); } else { startNew(); } setDeleteOpen(false); toast.success("模板已删除"); } catch (error) { toast.error(`删除失败:${(error as Error).message}`); } finally { setDeleting(false); } } let saveLabel = saving ? "保存中" : "保存修改"; if (!saving && selectedID == null) saveLabel = "创建模板"; return ( <> 任务模板管理 模板保存描述、目标、分类与任务级拦截/允许规则;修改不会影响已经创建的任务。
{templates.length === 0 && (

暂无模板

)} {templates.map((template) => ( ))}
模板名称 updateDraft("name", event.target.value)} /> 描述