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}
+
)
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}
请说明本次流转的原因、处理动作或待确认事项。
-
+