"use client"; import { closestCenter, DndContext, KeyboardSensor, MouseSensor, TouchSensor, useSensor, useSensors, type DragEndEvent, } from "@dnd-kit/core"; import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { zodResolver } from "@hookform/resolvers/zod"; import { GripVerticalIcon, PencilIcon, PlusIcon, RefreshCwIcon, SearchIcon, Trash2Icon, } from "lucide-react"; import { useCallback, useEffect, useState, type CSSProperties } from "react"; import { Controller, useForm, type Resolver } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod/v4"; import { useConfirm } from "@/components/confirm-provider"; import { OptionCombobox } from "@/components/option-combobox"; import { ProjectDialog } from "@/components/project-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { ButtonGroup } from "@/components/ui/button-group"; import { Field, FieldContent, FieldError, FieldLabel, } from "@/components/ui/field"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { createTicketPriorityConfig, deleteTicketPriorityConfig, fetchTicketPriorityConfigs, updateTicketPriorityConfig, updateTicketPriorityConfigSort, type CreateTicketPriorityConfigPayload, type TicketPriorityConfig, } from "@/lib/api/ticket-config"; import { getEnumOptions } from "@/lib/enums"; import { Status, StatusLabels } from "@/lib/generated/enums"; import { cn } from "@/lib/utils"; const listStatusOptions = [ { value: "all", label: "全部状态" }, ...getEnumOptions(StatusLabels) .filter((item) => Number(item.value) !== Status.Deleted) .map((item) => ({ value: String(item.value), label: item.label })), ] as const; const formSchema = z.object({ name: z.string().trim().min(1, "优先级名称不能为空"), firstResponseMinutes: z .string() .trim() .min(1, "首响时长不能为空") .regex(/^\d+$/, "请输入正整数"), resolutionMinutes: z .string() .trim() .min(1, "解决时长不能为空") .regex(/^\d+$/, "请输入正整数"), status: z.enum([String(Status.Ok), String(Status.Disabled)], { message: "请选择状态", }), remark: z.string().trim(), }); type EditForm = z.infer; const resolver = zodResolver(formSchema as never) as Resolver< z.input, undefined, z.output >; const emptyForm: EditForm = { name: "", firstResponseMinutes: "30", resolutionMinutes: "1440", status: String(Status.Ok), remark: "", }; function buildForm(item: TicketPriorityConfig | null): EditForm { if (!item) { return emptyForm; } return { name: item.name, firstResponseMinutes: String(item.firstResponseMinutes), resolutionMinutes: String(item.resolutionMinutes), status: String(item.status) as EditForm["status"], remark: item.remark || "", }; } function buildPayload(form: EditForm): CreateTicketPriorityConfigPayload { return { name: form.name.trim(), firstResponseMinutes: Number(form.firstResponseMinutes), resolutionMinutes: Number(form.resolutionMinutes), status: Number(form.status), remark: form.remark.trim(), }; } type SortablePriorityRowProps = { item: TicketPriorityConfig; disabled: boolean; onEdit: (item: TicketPriorityConfig) => void; onDelete: (item: TicketPriorityConfig) => void; }; function SortablePriorityRow({ item, disabled, onEdit, onDelete, }: SortablePriorityRowProps) { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: item.id, disabled, }); const style: CSSProperties = { transform: CSS.Transform.toString(transform), transition, }; return ( {item.name} {item.firstResponseMinutes} 分钟 {item.resolutionMinutes} 分钟 {item.status === Status.Ok ? "启用" : "停用"} ); } export default function TicketPrioritiesPage() { const [keywordInput, setKeywordInput] = useState(""); const [statusFilterInput, setStatusFilterInput] = useState("all"); const [keyword, setKeyword] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [sorting, setSorting] = useState(false); const [dialogOpen, setDialogOpen] = useState(false); const [editingItem, setEditingItem] = useState( null, ); const [deleting, setDeleting] = useState(false); const [items, setItems] = useState([]); const confirm = useConfirm(); const sensors = useSensors( useSensor(MouseSensor, { activationConstraint: { distance: 6 } }), useSensor(TouchSensor, { activationConstraint: { delay: 120, tolerance: 8 }, }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }), ); const loadData = useCallback(async () => { setLoading(true); try { const data = await fetchTicketPriorityConfigs({ name: keyword.trim() || undefined, status: statusFilter === "all" ? undefined : statusFilter, }); setItems(Array.isArray(data) ? data : []); } catch (error) { toast.error( error instanceof Error ? error.message : "加载工单优先级失败", ); } finally { setLoading(false); } }, [keyword, statusFilter]); useEffect(() => { void loadData(); }, [loadData]); function applyFilters() { setKeyword(keywordInput); setStatusFilter(statusFilterInput); } async function handleSubmit(payload: CreateTicketPriorityConfigPayload) { if (saving) { return; } setSaving(true); try { if (editingItem) { await updateTicketPriorityConfig({ id: editingItem.id, ...payload }); toast.success(`已更新工单优先级:${payload.name}`); } else { await createTicketPriorityConfig(payload); toast.success(`已创建工单优先级:${payload.name}`); } setDialogOpen(false); setEditingItem(null); await loadData(); } catch (error) { toast.error( error instanceof Error ? error.message : "保存工单优先级失败", ); } finally { setSaving(false); } } async function handleDelete(item: TicketPriorityConfig) { if (deleting) { return; } const confirmed = await confirm({ title: "确认删除优先级", description: `删除后将无法恢复。确定要删除工单优先级“${item.name}”吗?`, confirmText: "确认删除", cancelText: "取消", variant: "destructive", }); if (!confirmed) { return; } setDeleting(true); try { await deleteTicketPriorityConfig(item.id); toast.success(`已删除工单优先级:${item.name}`); await loadData(); } catch (error) { toast.error( error instanceof Error ? error.message : "删除工单优先级失败", ); } finally { setDeleting(false); } } async function handleDragEnd(event: DragEndEvent) { const { active, over } = event; if (!over || active.id === over.id || sorting || loading) { return; } const previousResults = items; const oldIndex = previousResults.findIndex((item) => item.id === active.id); const newIndex = previousResults.findIndex((item) => item.id === over.id); if (oldIndex < 0 || newIndex < 0) { return; } const nextResults = arrayMove(previousResults, oldIndex, newIndex); setItems(nextResults); setSorting(true); try { await updateTicketPriorityConfigSort(nextResults.map((item) => item.id)); toast.success("工单优先级排序已更新"); await loadData(); } catch (error) { setItems(previousResults); toast.error(error instanceof Error ? error.message : "更新排序失败"); } finally { setSorting(false); } } return ( <>
setKeywordInput(event.target.value)} onKeyDown={(event) => { if (event.key === "Enter") { event.preventDefault(); applyFilters(); } }} placeholder="按优先级名称筛选" className="pl-9" />
({ value: item.value, label: item.label, }))} />
{loading ? ( ) : items.length > 0 ? ( item.id)} strategy={verticalListSortingStrategy} > {items.map((item) => ( { setEditingItem(current); setDialogOpen(true); }} onDelete={handleDelete} /> ))} ) : ( )}
名称 首响时长 解决时长 状态 操作
加载中...
暂无工单优先级
{ setDialogOpen(nextOpen); if (!nextOpen) { setEditingItem(null); } }} onSubmit={handleSubmit} /> ); } type TicketPriorityEditDialogProps = { open: boolean; saving: boolean; item: TicketPriorityConfig | null; onOpenChange: (open: boolean) => void; onSubmit: (payload: CreateTicketPriorityConfigPayload) => Promise; }; function TicketPriorityEditDialog({ open, saving, item, onOpenChange, onSubmit, }: TicketPriorityEditDialogProps) { const formId = "ticket-priority-edit-form"; const form = useForm< z.input, undefined, z.output >({ resolver, defaultValues: buildForm(item), }); const { register, control, handleSubmit, reset, formState: { errors }, } = form; useEffect(() => { reset(buildForm(item)); }, [item, reset]); return ( } >
onSubmit(buildPayload(values)), )} > 名称 {errors.name ? : null}
首响时长 {errors.firstResponseMinutes ? ( ) : null} 解决时长 {errors.resolutionMinutes ? ( ) : null}
状态 ( )} /> {errors.status ? : null} 备注