feat: add chinese workbench page
This commit is contained in:
@@ -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 />
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
131
frontend/src/features/workbench/WorkbenchPage.tsx
Normal file
131
frontend/src/features/workbench/WorkbenchPage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user