"use client" import { useCallback, useEffect, useRef, useState } from "react" import { MessageSquareTextIcon, RefreshCcwIcon, SendIcon, UserRoundIcon } from "lucide-react" import { toast } from "sonner" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { ScrollArea } from "@/components/ui/scroll-area" import { Separator } from "@/components/ui/separator" import { Textarea } from "@/components/ui/textarea" import { changeTicketStatus, createTicketProgress, fetchTicketDetail, type CreateTicketPayload, type TicketDetail, type TicketStatus, type UpdateTicketPayload, updateTicket, } from "@/lib/api/ticket" import { cn, formatDateTime } from "@/lib/utils" import { EditDialog } from "./edit" import { TicketAssignDialog } from "./ticket-assign-dialog" import { TicketStatusBadge } from "./ticket-status-badge" type TicketDetailDialogProps = { ticketId: number | null open: boolean onOpenChange: (open: boolean) => 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 [editOpen, setEditOpen] = useState(false) const [editSaving, setEditSaving] = useState(false) const loadSeqRef = useRef(0) const dialogSeqRef = useRef(0) const currentTicketIdRef = useRef(null) currentTicketIdRef.current = open ? ticketId : null function isCurrentOperation(targetTicketId: number, dialogSeq: number) { return currentTicketIdRef.current === targetTicketId && dialogSeqRef.current === dialogSeq } const loadDetail = useCallback(async (targetTicketId = ticketId, dialogSeq = dialogSeqRef.current) => { if (!targetTicketId || !isCurrentOperation(targetTicketId, dialogSeq)) { return } const seq = loadSeqRef.current + 1 loadSeqRef.current = seq if (!open || !targetTicketId) { setDetail(null) setLoading(false) return } setLoading(true) try { const data = await fetchTicketDetail(targetTicketId) if (loadSeqRef.current !== seq || !isCurrentOperation(targetTicketId, dialogSeq)) { return } setDetail(data) } catch (error) { if (loadSeqRef.current !== seq || !isCurrentOperation(targetTicketId, dialogSeq)) { return } toast.error(error instanceof Error ? error.message : "加载工单详情失败") } finally { if (loadSeqRef.current === seq) { setLoading(false) } } }, [open, ticketId]) useEffect(() => { dialogSeqRef.current += 1 setDetail(null) setLoading(false) setStatusSaving(null) setProgressSaving(false) setEditSaving(false) setAssignOpen(false) setEditOpen(false) setProgressContent("") }, [open, ticketId]) useEffect(() => { void loadDetail(ticketId, dialogSeqRef.current) }, [loadDetail, ticketId]) async function handleStatusChange(status: TicketStatus) { if (!detail || detail.ticket.status === status) { return } const activeTicketId = detail.ticket.id const activeDialogSeq = dialogSeqRef.current setStatusSaving(status) try { await changeTicketStatus({ ticketId: activeTicketId, status }) if (!isCurrentOperation(activeTicketId, activeDialogSeq)) { return } toast.success("工单状态已更新") await loadDetail(activeTicketId, activeDialogSeq) if (!isCurrentOperation(activeTicketId, activeDialogSeq)) { return } onChanged() } catch (error) { if (!isCurrentOperation(activeTicketId, activeDialogSeq)) { return } toast.error(error instanceof Error ? error.message : "更新工单状态失败") } finally { if (isCurrentOperation(activeTicketId, activeDialogSeq)) { setStatusSaving(null) } } } async function handleCreateProgress() { if (!detail) { return } const activeTicketId = detail.ticket.id const activeDialogSeq = dialogSeqRef.current const content = progressContent.trim() if (!content) { toast.error("请填写处理进展") return } setProgressSaving(true) try { await createTicketProgress({ ticketId: activeTicketId, content, }) if (!isCurrentOperation(activeTicketId, activeDialogSeq)) { return } toast.success("处理进展已记录") setProgressContent("") await loadDetail(activeTicketId, activeDialogSeq) if (!isCurrentOperation(activeTicketId, activeDialogSeq)) { return } onChanged() } catch (error) { if (!isCurrentOperation(activeTicketId, activeDialogSeq)) { return } toast.error(error instanceof Error ? error.message : "记录处理进展失败") } finally { if (isCurrentOperation(activeTicketId, activeDialogSeq)) { setProgressSaving(false) } } } async function handleAssigned() { const activeTicketId = ticket?.id const activeDialogSeq = dialogSeqRef.current if (!activeTicketId || !isCurrentOperation(activeTicketId, activeDialogSeq)) { return } await loadDetail(activeTicketId, activeDialogSeq) if (!isCurrentOperation(activeTicketId, activeDialogSeq)) { return } onChanged() } async function handleUpdateTicket(payload: CreateTicketPayload | UpdateTicketPayload) { if (!("ticketId" in payload) || payload.ticketId <= 0) { toast.error("请选择工单") return } const activeDialogSeq = dialogSeqRef.current setEditSaving(true) try { await updateTicket(payload) if (!isCurrentOperation(payload.ticketId, activeDialogSeq)) { return } toast.success("工单已更新") setEditOpen(false) await loadDetail(payload.ticketId, activeDialogSeq) if (!isCurrentOperation(payload.ticketId, activeDialogSeq)) { return } onChanged() } catch (error) { if (!isCurrentOperation(payload.ticketId, activeDialogSeq)) { return } toast.error(error instanceof Error ? error.message : "更新工单失败") } finally { if (isCurrentOperation(payload.ticketId, activeDialogSeq)) { setEditSaving(false) } } } 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} ))}
) : (
暂无标签
)}
处理进展