feat: rebuild support workspace frontend

This commit is contained in:
hailin
2026-07-16 17:20:45 +08:00
parent fcb8121f0a
commit e90648e129
95 changed files with 4353 additions and 2165 deletions

View File

@@ -1,5 +1,5 @@
import * as React from 'react'
import { Bot, CheckCircle2, CircleDot, Clock3, Send, XCircle } from 'lucide-react'
import { CheckCircle2, CircleDot, Clock3, Send, XCircle } from 'lucide-react'
import { AppEmptyState } from '@/components/app/AppEmptyState'
import { AppDetailLayout } from '@/components/app/AppDetailLayout'
@@ -10,8 +10,8 @@ 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 { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
import { useAgentPageContext } from '@/features/agent-workspace/AgentWorkspaceProvider'
import { SolutionLinkPanel } from '@/features/solutions/SolutionLinkPanel'
import { selectDefaultTicketSolution } from '@/features/solutions/solutionSelection'
import type { TicketSolution } from '@/features/solutions/types'
@@ -33,14 +33,6 @@ 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 '取消工单'
@@ -67,6 +59,11 @@ export function TicketDetailPage({ ticketId }: { ticketId: number }) {
const [transitionDialogOpen, setTransitionDialogOpen] = React.useState(false)
const [transitionError, setTransitionError] = React.useState('')
const [linkedSolutions, setLinkedSolutions] = React.useState<TicketSolution[]>([])
const agentContext = React.useMemo(() => ticket ? ({
routeKey: 'ticket-detail', entityType: 'ticket', entityId: ticket.ticket_no, title: ticket.title, status: ticket.status,
metadata: { 优先级: ticket.priority, 关联系统: ticket.related_system || '未填写', 问题对象: ticket.problem_object || '未填写' }
}) : null, [ticket])
useAgentPageContext(agentContext)
React.useEffect(() => {
refreshTicket()
@@ -115,7 +112,7 @@ export function TicketDetailPage({ ticketId }: { ticketId: number }) {
if (error) {
return (
<AppPage maxWidth="6xl">
<AppPage maxWidth="6xl" header={<AppPageHeader title="工单详情" />}>
<AppEmptyState title="无法加载工单" description={error} />
</AppPage>
)
@@ -123,7 +120,7 @@ export function TicketDetailPage({ ticketId }: { ticketId: number }) {
if (!ticket) {
return (
<AppPage maxWidth="6xl">
<AppPage maxWidth="6xl" header={<AppPageHeader title="工单详情" />}>
<AppEmptyState title="正在加载工单" />
</AppPage>
)
@@ -141,10 +138,12 @@ export function TicketDetailPage({ ticketId }: { ticketId: number }) {
: ''
return (
<AppPage maxWidth="6xl">
<AppPageHeader
<AppPage
maxWidth="6xl"
header={<AppPageHeader
title={ticket.title}
description={ticket.ticket_no}
breadcrumbLabel={ticket.ticket_no}
meta={
<>
<AppStatusBadge status={ticket.status} />
@@ -169,38 +168,9 @@ export function TicketDetailPage({ ticketId }: { ticketId: number }) {
})}
</div>
}
/>
/>}
>
<TicketOverviewCard ticket={ticket} />
{nextStatuses.length ? (
<Card>
<CardContent className="grid gap-3 p-4 md:grid-cols-2">
{nextStatuses.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={
<>
@@ -213,20 +183,10 @@ export function TicketDetailPage({ ticketId }: { ticketId: number }) {
refreshEvents()
}}
/>
<AppSection title="AI 支持分析" action={<Badge variant="ai">后续接入</Badge>}>
<div className="grid gap-3">
<div className="flex items-center gap-2 text-sm font-medium text-foreground">
<Bot className="size-4" />
智能排查占位
</div>
<Separator />
<p className="text-sm text-muted-foreground">后续阶段将在这里展示排查建议、相似工单和引用来源。</p>
</div>
</AppSection>
</>
}
>
<AppSection title="问题描述">
<AppSection variant="card" title="问题描述">
<div className="grid gap-4">
<div>
<p className="text-xs font-medium text-muted-foreground">问题分类</p>
@@ -242,7 +202,7 @@ export function TicketDetailPage({ ticketId }: { ticketId: number }) {
</div>
</div>
</AppSection>
<AppSection title="解决方案">
<AppSection variant="card" title="解决方案">
{ticket.resolution ? (
<div className="grid gap-3">
<p className="whitespace-pre-wrap text-sm leading-6">{ticket.resolution}</p>
@@ -289,8 +249,12 @@ function TicketOverviewCard({ ticket }: { ticket: Ticket }) {
]
return (
<Card>
<CardContent className="grid gap-4 p-4 md:grid-cols-[minmax(0,1fr)_auto] md:items-center">
<Card className="gap-0 py-0">
<CardHeader className="border-b bg-muted/30 py-3.5">
<CardTitle className="text-sm font-semibold">工单概览</CardTitle>
<CardDescription className="text-xs">提出方、责任人与业务上下文</CardDescription>
</CardHeader>
<CardContent className="p-4">
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
{items.map((item) => (
<div key={item.label} className="min-w-0">
@@ -299,11 +263,11 @@ function TicketOverviewCard({ ticket }: { ticket: Ticket }) {
</div>
))}
</div>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Clock3 className="size-4" />
创建于 {formatDateTime(ticket.created_at)}
</div>
</CardContent>
<CardFooter className="justify-between bg-muted/45 px-4 py-3 text-xs text-muted-foreground">
<span className="flex items-center gap-2"><Clock3 className="size-4" />创建于 {formatDateTime(ticket.created_at)}</span>
<span className="font-mono">{ticket.ticket_no}</span>
</CardFooter>
</Card>
)
}