From 43c245e37e2d72dc46a1b6153a97415ef5b81b21 Mon Sep 17 00:00:00 2001 From: mlogclub Date: Sat, 2 May 2026 20:01:31 +0800 Subject: [PATCH] feat(ticket): rebuild lightweight ticket workspace --- .../_components/ticket-assign-dialog.tsx | 2 +- .../_components/ticket-detail-dialog.tsx | 309 +++++++++++ web/app/dashboard/tickets/page.tsx | 512 ++++++++++++------ web/lib/api/ticket.ts | 1 + 4 files changed, 655 insertions(+), 169 deletions(-) create mode 100644 web/app/dashboard/tickets/_components/ticket-detail-dialog.tsx diff --git a/web/app/dashboard/tickets/_components/ticket-assign-dialog.tsx b/web/app/dashboard/tickets/_components/ticket-assign-dialog.tsx index bbaa2ab..ad1e19d 100644 --- a/web/app/dashboard/tickets/_components/ticket-assign-dialog.tsx +++ b/web/app/dashboard/tickets/_components/ticket-assign-dialog.tsx @@ -62,7 +62,7 @@ export function TicketAssignDialog({ {open ? ( void + onChanged: () => void +} + +const statusOptions: Array<{ value: TicketStatus; label: string }> = [ + { value: "pending", label: "待处理" }, + { value: "in_progress", label: "处理中" }, + { value: "done", label: "已处理" }, +] + +function sourceLabel(source: string) { + switch (source) { + case "manual": + return "手动创建" + case "conversation": + return "会话生成" + default: + return source || "-" + } +} + +function metadataValue(value?: string | number | null) { + if (value === undefined || value === null || value === "") { + return "-" + } + return String(value) +} + +export function TicketDetailDialog({ + ticketId, + open, + onOpenChange, + onChanged, +}: TicketDetailDialogProps) { + const [detail, setDetail] = useState(null) + const [loading, setLoading] = useState(false) + const [statusSaving, setStatusSaving] = useState(null) + const [progressSaving, setProgressSaving] = useState(false) + const [progressContent, setProgressContent] = useState("") + const [assignOpen, setAssignOpen] = useState(false) + + const loadDetail = useCallback(async () => { + if (!open || !ticketId) { + setDetail(null) + return + } + setLoading(true) + try { + const data = await fetchTicketDetail(ticketId) + setDetail(data) + } catch (error) { + toast.error(error instanceof Error ? error.message : "加载工单详情失败") + } finally { + setLoading(false) + } + }, [open, ticketId]) + + useEffect(() => { + void loadDetail() + }, [loadDetail]) + + async function handleStatusChange(status: TicketStatus) { + if (!detail || detail.ticket.status === status) { + return + } + setStatusSaving(status) + try { + await changeTicketStatus({ ticketId: detail.ticket.id, status }) + toast.success("工单状态已更新") + await loadDetail() + onChanged() + } catch (error) { + toast.error(error instanceof Error ? error.message : "更新工单状态失败") + } finally { + setStatusSaving(null) + } + } + + async function handleCreateProgress() { + if (!detail) { + return + } + const content = progressContent.trim() + if (!content) { + toast.error("请填写处理进展") + return + } + setProgressSaving(true) + try { + await createTicketProgress({ + ticketId: detail.ticket.id, + content, + }) + toast.success("处理进展已记录") + setProgressContent("") + await loadDetail() + onChanged() + } catch (error) { + toast.error(error instanceof Error ? error.message : "记录处理进展失败") + } finally { + setProgressSaving(false) + } + } + + async function handleAssigned() { + await loadDetail() + onChanged() + } + + const ticket = detail?.ticket + + return ( + <> + + + + + {ticket?.title ?? "工单详情"} + {ticket ? : null} + + {ticket ? ( +
+ {ticket.ticketNo} + {sourceLabel(ticket.source)} + 创建人:{metadataValue(ticket.createdByName || ticket.createdBy)} +
+ ) : null} +
+ + + {loading && !ticket ? ( +
+ + 加载中... +
+ ) : ticket ? ( +
+
+
描述
+
+ {ticket.description || "暂无描述"} +
+
+ +
+
+
状态
+
+ {statusOptions.map((option) => ( + + ))} +
+
+
+
负责人
+
+
+ + {ticket.currentAssigneeName || "未分配"} +
+ +
+
+
+ +
+
标签
+ {ticket.tags && ticket.tags.length > 0 ? ( +
+ {ticket.tags.map((tag) => ( + + {tag.name} + + ))} +
+ ) : ( +
暂无标签
+ )} +
+ +
+ + + + + + +
+ + + +
+
+ + 处理进展 +
+
+