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 { TicketCreatePage } from '@/features/tickets/TicketCreatePage'
|
||||||
import { TicketDetailPage } from '@/features/tickets/TicketDetailPage'
|
import { TicketDetailPage } from '@/features/tickets/TicketDetailPage'
|
||||||
import { TicketListPage } from '@/features/tickets/TicketListPage'
|
import { TicketListPage } from '@/features/tickets/TicketListPage'
|
||||||
|
import { WorkbenchPage } from '@/features/workbench/WorkbenchPage'
|
||||||
|
|
||||||
function getRoute() {
|
function getRoute() {
|
||||||
return window.location.hash.replace(/^#/, '') || '/tickets'
|
return window.location.hash.replace(/^#/, '') || '/'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Router() {
|
export function Router() {
|
||||||
@@ -23,6 +24,10 @@ export function Router() {
|
|||||||
return <DesignSystemPreview />
|
return <DesignSystemPreview />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (route === '/') {
|
||||||
|
return <WorkbenchPage />
|
||||||
|
}
|
||||||
|
|
||||||
if (route === '/solutions') {
|
if (route === '/solutions') {
|
||||||
return <SolutionListPage />
|
return <SolutionListPage />
|
||||||
}
|
}
|
||||||
@@ -36,5 +41,5 @@ export function Router() {
|
|||||||
return <TicketDetailPage ticketId={Number(ticketMatch[1])} />
|
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 { AppPageHeader } from '@/components/app/AppPageHeader'
|
||||||
import { AppPriorityBadge } from '@/components/app/AppPriorityBadge'
|
import { AppPriorityBadge } from '@/components/app/AppPriorityBadge'
|
||||||
|
import { AppSection } from '@/components/app/AppSection'
|
||||||
import { AppStatusBadge } from '@/components/app/AppStatusBadge'
|
import { AppStatusBadge } from '@/components/app/AppStatusBadge'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Card, CardContent, CardHeader } from '@/components/ui/card'
|
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() {
|
export function DesignSystemPreview() {
|
||||||
return (
|
return (
|
||||||
@@ -11,37 +37,71 @@ export function DesignSystemPreview() {
|
|||||||
<div className="mx-auto flex max-w-6xl flex-col gap-6">
|
<div className="mx-auto flex max-w-6xl flex-col gap-6">
|
||||||
<AppPageHeader
|
<AppPageHeader
|
||||||
title="设计系统预览"
|
title="设计系统预览"
|
||||||
description="业务页面必须基于这些 token 和组件开发。"
|
description="业务页面必须基于 shadcn 原子组件和 App 组件层开发。"
|
||||||
actions={<Button>创建工单</Button>}
|
actions={<Button>创建工单</Button>}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<h2 className="text-sm font-medium">Buttons</h2>
|
<h2 className="text-sm font-medium">按钮</h2>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-wrap gap-2">
|
<CardContent className="flex flex-wrap gap-2">
|
||||||
<Button>Primary</Button>
|
<Button>主要操作</Button>
|
||||||
<Button variant="secondary">Secondary</Button>
|
<Button variant="secondary">次要操作</Button>
|
||||||
<Button variant="ghost">Ghost</Button>
|
<Button variant="ghost">轻量操作</Button>
|
||||||
<Button variant="destructive">Danger</Button>
|
<Button variant="destructive">高风险操作</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<h2 className="text-sm font-medium">Badges</h2>
|
<h2 className="text-sm font-medium">状态与优先级</h2>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-wrap gap-2">
|
<CardContent className="flex flex-wrap gap-2">
|
||||||
<Badge>Default</Badge>
|
<Badge>默认</Badge>
|
||||||
<Badge variant="success">Success</Badge>
|
<Badge variant="success">成功</Badge>
|
||||||
<Badge variant="warning">Warning</Badge>
|
<Badge variant="warning">提醒</Badge>
|
||||||
<Badge variant="danger">Danger</Badge>
|
<Badge variant="danger">风险</Badge>
|
||||||
<Badge variant="info">Info</Badge>
|
<Badge variant="info">信息</Badge>
|
||||||
<Badge variant="ai">AI</Badge>
|
<Badge variant="ai">AI</Badge>
|
||||||
<AppStatusBadge status="in_progress" />
|
<AppStatusBadge status="in_progress" />
|
||||||
<AppPriorityBadge priority="urgent" />
|
<AppPriorityBadge priority="urgent" />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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>
|
</div>
|
||||||
</main>
|
</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