feat: rebuild support workspace frontend
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user