refactor: enhance ticket detail workflow view

This commit is contained in:
hailin
2026-07-02 16:15:45 +08:00
parent ad270d3809
commit 942b712c20
4 changed files with 165 additions and 64 deletions

View File

@@ -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<Record<TicketStatus, { label: string; icon: React.
closed: { label: '关闭工单', icon: XCircle }
}
const STATUS_ACTION_HINTS: Partial<Record<TicketStatus, string>> = {
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<Ticket | null>(null)
const [events, setEvents] = React.useState<TicketEvent[]>([])
@@ -104,34 +124,65 @@ export function TicketDetailPage({ ticketId }: { ticketId: number }) {
return (
<AppPage maxWidth="6xl">
<AppPageHeader
title={ticket.title}
description={ticket.ticket_no}
meta={
<>
<AppStatusBadge status={ticket.status} />
<AppPriorityBadge priority={ticket.priority} />
</>
}
actions={
<div className="flex flex-wrap gap-2">
{NEXT_STATUSES[ticket.status].map((status) => {
const action = STATUS_ACTIONS[status]
const Icon = action?.icon
return (
<Button
key={status}
variant={status === 'closed' ? 'outline' : 'secondary'}
onClick={() => openTransition(status)}
>
{Icon ? <Icon data-icon="inline-start" /> : null}
{getStatusActionLabel(ticket.status, status)}
</Button>
)
})}
</div>
}
/>
<AppPageHeader
title={ticket.title}
description={ticket.ticket_no}
meta={
<>
<AppStatusBadge status={ticket.status} />
<AppPriorityBadge priority={ticket.priority} />
</>
}
actions={
<div className="flex flex-wrap gap-2">
{NEXT_STATUSES[ticket.status].map((status) => {
const action = STATUS_ACTIONS[status]
const Icon = action?.icon
return (
<Button
key={status}
variant={getActionVariant(status)}
onClick={() => openTransition(status)}
>
{Icon ? <Icon data-icon="inline-start" /> : null}
{getStatusActionLabel(ticket.status, status)}
</Button>
)
})}
</div>
}
/>
<TicketOverviewCard ticket={ticket} />
{NEXT_STATUSES[ticket.status].length ? (
<Card>
<CardContent className="grid gap-3 p-4 md:grid-cols-2">
{NEXT_STATUSES[ticket.status].map((status) => {
const action = STATUS_ACTIONS[status]
const Icon = action?.icon ?? CircleDot
return (
<button
key={status}
type="button"
className="flex min-h-20 items-start gap-3 rounded-md border border-border bg-background p-3 text-left transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40"
onClick={() => openTransition(status)}
>
<span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary">
<Icon className="size-4" />
</span>
<span className="min-w-0">
<span className="block text-sm font-medium text-foreground">
{getStatusActionLabel(ticket.status, status)}
</span>
<span className="mt-1 block text-xs leading-5 text-muted-foreground">
{STATUS_ACTION_HINTS[status] ?? '记录本次处理动作并推进流程。'}
</span>
</span>
</button>
)
})}
</CardContent>
</Card>
) : null}
<AppDetailLayout
side={
<>
@@ -167,3 +218,31 @@ export function TicketDetailPage({ ticketId }: { ticketId: number }) {
</AppPage>
)
}
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 (
<Card>
<CardContent className="grid gap-4 p-4 md:grid-cols-[minmax(0,1fr)_auto] md:items-center">
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
{items.map((item) => (
<div key={item.label} className="min-w-0">
<p className="text-xs font-medium text-muted-foreground">{item.label}</p>
<p className="mt-1 truncate text-sm font-medium text-foreground">{item.value}</p>
</div>
))}
</div>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Clock3 className="size-4" />
创建于 {formatDateTime(ticket.created_at)}
</div>
</CardContent>
</Card>
)
}