feat: add chinese workbench page

This commit is contained in:
hailin
2026-07-01 11:41:53 +08:00
parent 52bb354a1b
commit df8684f37d
3 changed files with 210 additions and 14 deletions

View File

@@ -5,9 +5,10 @@ import { SolutionListPage } from '@/features/solutions/SolutionListPage'
import { TicketCreatePage } from '@/features/tickets/TicketCreatePage'
import { TicketDetailPage } from '@/features/tickets/TicketDetailPage'
import { TicketListPage } from '@/features/tickets/TicketListPage'
import { WorkbenchPage } from '@/features/workbench/WorkbenchPage'
function getRoute() {
return window.location.hash.replace(/^#/, '') || '/tickets'
return window.location.hash.replace(/^#/, '') || '/'
}
export function Router() {
@@ -23,6 +24,10 @@ export function Router() {
return <DesignSystemPreview />
}
if (route === '/') {
return <WorkbenchPage />
}
if (route === '/solutions') {
return <SolutionListPage />
}
@@ -36,5 +41,5 @@ export function Router() {
return <TicketDetailPage ticketId={Number(ticketMatch[1])} />
}
return <TicketListPage />
return <WorkbenchPage />
}

View File

@@ -1,9 +1,35 @@
import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable'
import { AppDetailLayout } from '@/components/app/AppDetailLayout'
import { AppEmptyState } from '@/components/app/AppEmptyState'
import { AppFilterBar } from '@/components/app/AppFilterBar'
import { AppPageHeader } from '@/components/app/AppPageHeader'
import { AppPriorityBadge } from '@/components/app/AppPriorityBadge'
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, CardHeader } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
type PreviewRow = {
id: number
name: string
status: string
priority: string
}
const previewRows: PreviewRow[] = [
{ id: 1, name: '登录失败无法进入生产系统', status: 'in_progress', priority: 'urgent' },
{ id: 2, name: '报表导出结果缺少字段', status: 'pending', priority: 'medium' }
]
const previewColumns: AppDataTableColumn<PreviewRow>[] = [
{ key: 'name', header: '问题', cell: (row) => row.name },
{ key: 'status', header: '状态', cell: (row) => <AppStatusBadge status={row.status} /> },
{ key: 'priority', header: '优先级', cell: (row) => <AppPriorityBadge priority={row.priority} /> },
{ key: 'action', header: '操作', cell: () => <Button variant="ghost">查看</Button> }
]
export function DesignSystemPreview() {
return (
@@ -11,37 +37,71 @@ export function DesignSystemPreview() {
<div className="mx-auto flex max-w-6xl flex-col gap-6">
<AppPageHeader
title="设计系统预览"
description="业务页面必须基于这些 token 和组件开发。"
description="业务页面必须基于 shadcn 原子组件和 App 组件层开发。"
actions={<Button>创建工单</Button>}
/>
<Card>
<CardHeader>
<h2 className="text-sm font-medium">Buttons</h2>
<h2 className="text-sm font-medium">按钮</h2>
</CardHeader>
<CardContent className="flex flex-wrap gap-2">
<Button>Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Danger</Button>
<Button>主要操作</Button>
<Button variant="secondary">次要操作</Button>
<Button variant="ghost">轻量操作</Button>
<Button variant="destructive">高风险操作</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<h2 className="text-sm font-medium">Badges</h2>
<h2 className="text-sm font-medium">状态与优先级</h2>
</CardHeader>
<CardContent className="flex flex-wrap gap-2">
<Badge>Default</Badge>
<Badge variant="success">Success</Badge>
<Badge variant="warning">Warning</Badge>
<Badge variant="danger">Danger</Badge>
<Badge variant="info">Info</Badge>
<Badge>默认</Badge>
<Badge variant="success">成功</Badge>
<Badge variant="warning">提醒</Badge>
<Badge variant="danger">风险</Badge>
<Badge variant="info">信息</Badge>
<Badge variant="ai">AI</Badge>
<AppStatusBadge status="in_progress" />
<AppPriorityBadge priority="urgent" />
</CardContent>
</Card>
<AppFilterBar actions={<Button variant="outline">重置筛选</Button>}>
<Input placeholder="搜索工单标题或编号" />
<Select defaultValue="all">
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">全部状态</SelectItem>
<SelectItem value="pending">待处理</SelectItem>
<SelectItem value="in_progress">处理中</SelectItem>
</SelectContent>
</Select>
</AppFilterBar>
<AppDataTable
columns={previewColumns}
rows={previewRows}
getRowKey={(row) => row.id}
emptyTitle="暂无数据"
emptyDescription="符合条件的数据会显示在这里。"
/>
<AppDetailLayout
side={
<AppSection title="右侧面板" description="用于关键字段、关联方案和 AI 分析。">
<AppEmptyState title="等待接入" description="后续阶段会接入 Agent 执行结果。" />
</AppSection>
}
>
<AppSection title="详情区块" description="用于承载工单概览、问题描述和处理动态。">
<p className="text-sm text-muted-foreground">页面级布局只复用 App 组件,业务字段由页面自行传入。</p>
</AppSection>
</AppDetailLayout>
</div>
</main>
)

View File

@@ -0,0 +1,131 @@
import * as React from 'react'
import { ArrowRight, Plus } from 'lucide-react'
import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable'
import { AppEmptyState } from '@/components/app/AppEmptyState'
import { AppPageHeader } from '@/components/app/AppPageHeader'
import { AppPriorityBadge } from '@/components/app/AppPriorityBadge'
import { AppSection } from '@/components/app/AppSection'
import { AppStatusBadge } from '@/components/app/AppStatusBadge'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { formatDateTime } from '@/lib/format'
import { listTickets } from '@/features/tickets/api'
import type { Ticket } from '@/features/tickets/types'
const columns: AppDataTableColumn<Ticket>[] = [
{
key: 'ticket_no',
header: '编号',
cell: (ticket) => <span className="font-medium text-foreground">{ticket.ticket_no}</span>
},
{
key: 'title',
header: '标题',
cell: (ticket) => <span className="line-clamp-1">{ticket.title}</span>
},
{
key: 'status',
header: '状态',
cell: (ticket) => <AppStatusBadge status={ticket.status} />
},
{
key: 'priority',
header: '优先级',
cell: (ticket) => <AppPriorityBadge priority={ticket.priority} />
},
{
key: 'created_at',
header: '创建时间',
cell: (ticket) => <span className="text-muted-foreground">{formatDateTime(ticket.created_at)}</span>
}
]
export function WorkbenchPage() {
const [tickets, setTickets] = React.useState<Ticket[]>([])
const [error, setError] = React.useState('')
React.useEffect(() => {
listTickets()
.then(setTickets)
.catch((reason: unknown) => setError(reason instanceof Error ? reason.message : '加载失败'))
}, [])
const metrics = [
{ label: '待处理工单', value: tickets.filter((ticket) => ticket.status === 'pending').length },
{ label: '处理中', value: tickets.filter((ticket) => ticket.status === 'in_progress').length },
{ label: '高优先级', value: tickets.filter((ticket) => ticket.priority === 'high' || ticket.priority === 'urgent').length },
{ label: '已解决', value: tickets.filter((ticket) => ticket.status === 'resolved' || ticket.status === 'closed').length }
]
const recentTickets = tickets.slice(0, 6)
return (
<main className="min-h-screen bg-background p-6 text-foreground">
<div className="mx-auto grid max-w-7xl gap-5">
<AppPageHeader
title="今日工作台"
description="集中查看待处理事项、近期工单和常用入口。"
actions={
<Button onClick={() => (window.location.hash = '/tickets/new')}>
<Plus className="size-4" />
新建工单
</Button>
}
/>
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
{metrics.map((metric) => (
<Card key={metric.label}>
<CardContent>
<p className="text-sm text-muted-foreground">{metric.label}</p>
<p className="mt-2 text-2xl font-semibold text-foreground">{metric.value}</p>
</CardContent>
</Card>
))}
</div>
{error ? <AppEmptyState title="无法加载工作台数据" description={error} /> : null}
<div className="grid gap-4 xl:grid-cols-[minmax(0,1fr)_320px]">
<AppSection
title="最近工单"
description="按当前接口返回顺序展示近期工单,后续可接入专门的工作台聚合接口。"
action={
<Button variant="ghost" onClick={() => (window.location.hash = '/tickets')}>
查看全部
<ArrowRight className="size-4" />
</Button>
}
>
<AppDataTable
columns={columns}
rows={recentTickets}
getRowKey={(ticket) => ticket.id}
emptyTitle="暂无工单"
emptyDescription="创建第一张工单后,最近动态会显示在这里。"
onRowClick={(ticket) => (window.location.hash = `/tickets/${ticket.id}`)}
/>
</AppSection>
<AppSection title="快捷入口" description="高频操作集中放置,减少跨页面跳转。">
<div className="grid gap-2">
<Button className="justify-between" variant="secondary" onClick={() => (window.location.hash = '/tickets/new')}>
创建工单
<ArrowRight className="size-4" />
</Button>
<Button className="justify-between" variant="outline" onClick={() => (window.location.hash = '/tickets')}>
工单中心
<ArrowRight className="size-4" />
</Button>
<Button className="justify-between" variant="outline" onClick={() => (window.location.hash = '/solutions')}>
解决方案库
<ArrowRight className="size-4" />
</Button>
</div>
</AppSection>
</div>
</div>
</main>
)
}