From 942b712c201f054e6f2dfe2b8973c40e49133b91 Mon Sep 17 00:00:00 2001 From: hailin Date: Thu, 2 Jul 2026 16:15:45 +0800 Subject: [PATCH] refactor: enhance ticket detail workflow view --- .../src/components/app/AppDetailLayout.tsx | 4 +- .../src/features/tickets/TicketDetailPage.tsx | 137 ++++++++++++++---- .../features/tickets/TicketStatusTimeline.tsx | 80 ++++++---- .../tickets/TicketTransitionDialog.tsx | 8 +- 4 files changed, 165 insertions(+), 64 deletions(-) diff --git a/frontend/src/components/app/AppDetailLayout.tsx b/frontend/src/components/app/AppDetailLayout.tsx index e3ce59b..4818b82 100644 --- a/frontend/src/components/app/AppDetailLayout.tsx +++ b/frontend/src/components/app/AppDetailLayout.tsx @@ -7,8 +7,8 @@ type AppDetailLayoutProps = { export function AppDetailLayout({ children, side }: AppDetailLayoutProps) { return ( -
-
{children}
+
+
{children}
) diff --git a/frontend/src/features/tickets/TicketDetailPage.tsx b/frontend/src/features/tickets/TicketDetailPage.tsx index 124fe3a..54ed969 100644 --- a/frontend/src/features/tickets/TicketDetailPage.tsx +++ b/frontend/src/features/tickets/TicketDetailPage.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Bot, CheckCircle2, CircleDot, Send, XCircle } from 'lucide-react' +import { Bot, CheckCircle2, CircleDot, Clock3, Send, XCircle } from 'lucide-react' import { AppEmptyState } from '@/components/app/AppEmptyState' import { AppDetailLayout } from '@/components/app/AppDetailLayout' @@ -10,8 +10,10 @@ import { AppSection } from '@/components/app/AppSection' import { AppStatusBadge } from '@/components/app/AppStatusBadge' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' +import { Card, CardContent } from '@/components/ui/card' import { Separator } from '@/components/ui/separator' import { SolutionLinkPanel } from '@/features/solutions/SolutionLinkPanel' +import { formatDateTime } from '@/lib/format' import { getTicket, listTicketEvents, transitionTicket } from './api' import { TicketComments } from './TicketComments' @@ -37,6 +39,14 @@ const STATUS_ACTIONS: Partial> = { + pending: '补充信息完成后进入支持队列。', + in_progress: '确认接手并开始技术排查。', + pending_confirm: '已给出处理结果,等待业务侧确认。', + resolved: '业务确认问题已解决,保留后续关闭入口。', + closed: '关闭代表流程结束,不再继续处理。' +} + function getStatusActionLabel(currentStatus: TicketStatus, targetStatus: TicketStatus) { if (targetStatus === 'closed' && currentStatus !== 'resolved') { return '取消工单' @@ -44,6 +54,16 @@ function getStatusActionLabel(currentStatus: TicketStatus, targetStatus: TicketS return STATUS_ACTIONS[targetStatus]?.label ?? '调整状态' } +function displayUser(user: Ticket['requester']) { + return user?.display_name || user?.username || '未设置' +} + +function getActionVariant(status: TicketStatus) { + if (status === 'closed') return 'outline' + if (status === 'resolved') return 'default' + return 'secondary' +} + export function TicketDetailPage({ ticketId }: { ticketId: number }) { const [ticket, setTicket] = React.useState(null) const [events, setEvents] = React.useState([]) @@ -104,34 +124,65 @@ export function TicketDetailPage({ ticketId }: { ticketId: number }) { return ( - - - - - } - actions={ -
- {NEXT_STATUSES[ticket.status].map((status) => { - const action = STATUS_ACTIONS[status] - const Icon = action?.icon - return ( - - ) - })} -
- } - /> + + + + + } + actions={ +
+ {NEXT_STATUSES[ticket.status].map((status) => { + const action = STATUS_ACTIONS[status] + const Icon = action?.icon + return ( + + ) + })} +
+ } + /> + + {NEXT_STATUSES[ticket.status].length ? ( + + + {NEXT_STATUSES[ticket.status].map((status) => { + const action = STATUS_ACTIONS[status] + const Icon = action?.icon ?? CircleDot + return ( + + ) + })} + + + ) : null} @@ -167,3 +218,31 @@ export function TicketDetailPage({ ticketId }: { ticketId: number }) {
) } + +function TicketOverviewCard({ ticket }: { ticket: Ticket }) { + const items = [ + { label: '请求人', value: displayUser(ticket.requester) }, + { label: '处理人', value: displayUser(ticket.assignee) }, + { label: '关联系统', value: ticket.related_system || '未填写' }, + { label: '更新时间', value: formatDateTime(ticket.updated_at) } + ] + + return ( + + +
+ {items.map((item) => ( +
+

{item.label}

+

{item.value}

+
+ ))} +
+
+ + 创建于 {formatDateTime(ticket.created_at)} +
+
+
+ ) +} diff --git a/frontend/src/features/tickets/TicketStatusTimeline.tsx b/frontend/src/features/tickets/TicketStatusTimeline.tsx index 199261d..d962be6 100644 --- a/frontend/src/features/tickets/TicketStatusTimeline.tsx +++ b/frontend/src/features/tickets/TicketStatusTimeline.tsx @@ -1,6 +1,10 @@ +import { CheckCircle2, CircleDot, MessageSquareText, Paperclip, PencilLine } from 'lucide-react' + import { AppSection } from '@/components/app/AppSection' import { AppStatusBadge } from '@/components/app/AppStatusBadge' import { Badge } from '@/components/ui/badge' +import { Separator } from '@/components/ui/separator' +import { formatDateTime } from '@/lib/format' import type { Ticket, TicketEvent } from './types' @@ -27,13 +31,12 @@ const COMMENT_TYPE_LABELS: Record = { user: '用户评论' } -function formatEventTime(value: string) { - return new Intl.DateTimeFormat('zh-CN', { - month: 'numeric', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - }).format(new Date(value)) +const EVENT_ICONS: Record> = { + created: CheckCircle2, + updated: PencilLine, + status_change: CircleDot, + commented: MessageSquareText, + attachment_added: Paperclip } function displayStatus(value: unknown) { @@ -65,6 +68,13 @@ function describeEvent(event: TicketEvent) { return event.remark || '记录已更新' } +function eventVariant(eventType: string) { + if (eventType === 'status_change') return 'info' + if (eventType === 'commented') return 'secondary' + if (eventType === 'created') return 'success' + return 'outline' +} + type TicketStatusTimelineProps = { ticket: Ticket events: TicketEvent[] @@ -73,15 +83,16 @@ type TicketStatusTimelineProps = { export function TicketStatusTimeline({ ticket, events }: TicketStatusTimelineProps) { return ( -
-
+
+
-

当前状态

-

- 最后更新时间:{new Date(ticket.updated_at).toLocaleString('zh-CN')} -

+
+

当前流程状态

+ +
+

最后更新时间:{formatDateTime(ticket.updated_at)}

- + {ticket.ticket_no}
{events.length === 0 ? ( @@ -89,25 +100,28 @@ export function TicketStatusTimeline({ ticket, events }: TicketStatusTimelinePro

暂无处理记录

) : ( -
+
{events.map((event, index) => (
-
- {index < events.length - 1 ?
: null} + + {index < events.length - 1 ?
: null}
-
-
-

- {EVENT_LABELS[event.event_type] ?? event.event_type} -

- {event.event_type === 'created' ? {ticket.ticket_no} : null} +
+
+
+
+

+ {EVENT_LABELS[event.event_type] ?? event.event_type} +

+ {displayOperator(event)} +
+

{formatDateTime(event.created_at)}

+
+ +

{describeEvent(event)}

+ {event.remark ?

说明:{event.remark}

: null}
-

{describeEvent(event)}

- {event.remark ?

说明:{event.remark}

: null} -

- {displayOperator(event)} · {formatEventTime(event.created_at)} -

))} @@ -117,3 +131,13 @@ export function TicketStatusTimeline({ ticket, events }: TicketStatusTimelinePro ) } + +function TimelineIcon({ eventType }: { eventType: string }) { + const Icon = EVENT_ICONS[eventType] ?? CircleDot + + return ( +
+ +
+ ) +} diff --git a/frontend/src/features/tickets/TicketTransitionDialog.tsx b/frontend/src/features/tickets/TicketTransitionDialog.tsx index 95c2b22..8f08cec 100644 --- a/frontend/src/features/tickets/TicketTransitionDialog.tsx +++ b/frontend/src/features/tickets/TicketTransitionDialog.tsx @@ -1,5 +1,6 @@ import * as React from 'react' +import { AppField } from '@/components/app/AppField' import { Button } from '@/components/ui/button' import { Dialog, @@ -66,10 +67,7 @@ export function TicketTransitionDialog({ {actionLabel} 请说明本次流转的原因、处理动作或待确认事项。 -
- +