feat: add category-driven ticket workflows

This commit is contained in:
hailin
2026-07-03 16:17:32 +08:00
parent 6586889733
commit 7692374067
16 changed files with 777 additions and 87 deletions

View File

@@ -17,20 +17,12 @@ import { formatDateTime } from '@/lib/format'
import { getTicket, listTicketEvents, transitionTicket } from './api'
import { TicketComments } from './TicketComments'
import { getTicketCategoryLabel, getTicketNextStatuses } from './ticketCategories'
import { TicketMetaPanel } from './TicketMetaPanel'
import { TicketStatusTimeline } from './TicketStatusTimeline'
import { TicketTransitionDialog } from './TicketTransitionDialog'
import type { Ticket, TicketEvent, TicketStatus } from './types'
const NEXT_STATUSES: Record<TicketStatus, TicketStatus[]> = {
pending_info: ['pending', 'closed'],
pending: ['in_progress', 'closed'],
in_progress: ['pending_confirm'],
pending_confirm: ['resolved', 'in_progress', 'closed'],
resolved: ['closed'],
closed: []
}
const STATUS_ACTIONS: Partial<Record<TicketStatus, { label: string; icon: React.ComponentType<{ className?: string }> }>> = {
pending: { label: '转为待处理', icon: CircleDot },
in_progress: { label: '开始处理', icon: CircleDot },
@@ -93,10 +85,18 @@ export function TicketDetailPage({ ticketId }: { ticketId: number }) {
setTransitionDialogOpen(true)
}
async function handleTransition(status: TicketStatus, remark: string) {
async function handleTransition(
status: TicketStatus,
values: {
remark: string
resolution: string
is_resolved: boolean
replied_to_customer: boolean
}
) {
if (!ticket) return
try {
const updated = await transitionTicket(ticket.id, status, remark)
const updated = await transitionTicket(ticket.id, status, values)
setTicket(updated)
setTransitionError('')
refreshEvents()
@@ -122,6 +122,8 @@ export function TicketDetailPage({ ticketId }: { ticketId: number }) {
)
}
const nextStatuses = getTicketNextStatuses(ticket.category_code, ticket.status)
return (
<AppPage maxWidth="6xl">
<AppPageHeader
@@ -135,7 +137,7 @@ export function TicketDetailPage({ ticketId }: { ticketId: number }) {
}
actions={
<div className="flex flex-wrap gap-2">
{NEXT_STATUSES[ticket.status].map((status) => {
{nextStatuses.map((status) => {
const action = STATUS_ACTIONS[status]
const Icon = action?.icon
return (
@@ -153,10 +155,10 @@ export function TicketDetailPage({ ticketId }: { ticketId: number }) {
}
/>
<TicketOverviewCard ticket={ticket} />
{NEXT_STATUSES[ticket.status].length ? (
{nextStatuses.length ? (
<Card>
<CardContent className="grid gap-3 p-4 md:grid-cols-2">
{NEXT_STATUSES[ticket.status].map((status) => {
{nextStatuses.map((status) => {
const action = STATUS_ACTIONS[status]
const Icon = action?.icon ?? CircleDot
return (
@@ -202,7 +204,37 @@ export function TicketDetailPage({ ticketId }: { ticketId: number }) {
}
>
<AppSection title="问题描述">
<p className="whitespace-pre-wrap text-sm leading-6">{ticket.description}</p>
<div className="grid gap-4">
<div>
<p className="text-xs font-medium text-muted-foreground">问题分类</p>
<p className="mt-1 text-sm font-medium text-foreground">{getTicketCategoryLabel(ticket.category_code)}</p>
</div>
<div>
<p className="text-xs font-medium text-muted-foreground">问题表现</p>
<p className="mt-1 whitespace-pre-wrap text-sm leading-6">{ticket.problem_performance || '未填写'}</p>
</div>
<div>
<p className="text-xs font-medium text-muted-foreground">问题描述</p>
<p className="mt-1 whitespace-pre-wrap text-sm leading-6">{ticket.description}</p>
</div>
</div>
</AppSection>
<AppSection title="解决方案">
{ticket.resolution ? (
<div className="grid gap-3">
<p className="whitespace-pre-wrap text-sm leading-6">{ticket.resolution}</p>
<div className="flex flex-wrap gap-2">
<Badge variant={ticket.is_resolved ? 'success' : 'outline'}>
{ticket.is_resolved ? '已解决' : '未解决'}
</Badge>
<Badge variant={ticket.replied_to_customer ? 'info' : 'outline'}>
{ticket.replied_to_customer ? '已回复客户' : '未回复客户'}
</Badge>
</div>
</div>
) : (
<AppEmptyState title="暂无解决方案" description="解决工单后将在这里展示处理结论。" />
)}
</AppSection>
<TicketStatusTimeline ticket={ticket} events={events} />
<TicketComments ticketId={ticket.id} onCommentAdded={refreshEvents} />
@@ -221,6 +253,10 @@ export function TicketDetailPage({ ticketId }: { ticketId: number }) {
function TicketOverviewCard({ ticket }: { ticket: Ticket }) {
const items = [
{ label: '问题分类', value: getTicketCategoryLabel(ticket.category_code) },
{ label: '问题对象', value: ticket.problem_object || '未填写' },
{ label: '提出人', value: ticket.submitter_name || displayUser(ticket.requester) },
{ label: '提出机构', value: ticket.submitter_org || '未填写' },
{ label: '请求人', value: displayUser(ticket.requester) },
{ label: '处理人', value: displayUser(ticket.assignee) },
{ label: '关联系统', value: ticket.related_system || '未填写' },