feat: polish solution ui and design docs

This commit is contained in:
hailin
2026-07-01 11:52:07 +08:00
parent cba807befb
commit 5f558832f1
12 changed files with 216 additions and 142 deletions

View File

@@ -1,8 +1,9 @@
import * as React from 'react'
import { AppEmptyState } from '@/components/app/AppEmptyState'
import { AppSection } from '@/components/app/AppSection'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { linkSolutionToTicket, listSolutions } from './api'
import type { Solution } from './types'
@@ -17,32 +18,32 @@ export function SolutionLinkPanel({ ticketId }: { ticketId: number }) {
async function handleLink(solution: Solution, linkType: string) {
await linkSolutionToTicket(solution.id, ticketId, linkType)
setMessage(`${solution.title} linked as ${linkType}`)
setMessage(`已将「${solution.title}」标记为${linkType === 'adopted' ? '已采纳' : '推荐方案'}`)
}
return (
<Card>
<CardHeader>
<h2 className="text-sm font-medium">Reusable Solutions</h2>
</CardHeader>
<CardContent className="grid gap-3">
<AppSection title="关联解决方案" description="从已发布方案中选择推荐或采纳项。">
<div className="grid gap-3">
{message ? <p className="text-sm text-muted-foreground">{message}</p> : null}
{solutions.length === 0 ? <AppEmptyState title="No published solutions" /> : null}
{solutions.length === 0 ? <AppEmptyState title="暂无关联方案" description="发布解决方案后,可在这里推荐或采纳。" /> : null}
{solutions.map((solution) => (
<div className="grid gap-2 rounded-md border border-border p-3" key={solution.id}>
<div>
<p className="text-sm font-medium">{solution.title}</p>
<div className="flex items-center justify-between gap-2">
<p className="text-sm font-medium">{solution.title}</p>
<Badge variant="info">引用 {solution.usage_count}</Badge>
</div>
<p className="text-sm text-muted-foreground">{solution.problem_signature}</p>
</div>
<div className="flex gap-2">
<Button variant="secondary" onClick={() => void handleLink(solution, 'referenced')}>
Reference
推荐方案
</Button>
<Button onClick={() => void handleLink(solution, 'adopted')}>Adopt</Button>
<Button onClick={() => void handleLink(solution, 'adopted')}>已采纳</Button>
</div>
</div>
))}
</CardContent>
</Card>
</div>
</AppSection>
)
}

View File

@@ -1,46 +1,131 @@
import * as React from 'react'
import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable'
import { AppEmptyState } from '@/components/app/AppEmptyState'
import { AppFilterBar } from '@/components/app/AppFilterBar'
import { AppPageHeader } from '@/components/app/AppPageHeader'
import { Badge } from '@/components/ui/badge'
import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { formatDateTime } from '@/lib/format'
import { listSolutions } from './api'
import type { Solution } from './types'
const STATUS_LABELS: Record<string, string> = {
published: '已发布',
draft: '草稿'
}
const columns: AppDataTableColumn<Solution>[] = [
{
key: 'title',
header: '方案标题',
cell: (solution) => (
<div>
<p className="font-medium text-foreground">{solution.title}</p>
<p className="mt-1 line-clamp-1 text-sm text-muted-foreground">{solution.problem_signature}</p>
</div>
)
},
{
key: 'status',
header: '状态',
cell: (solution) => <Badge variant={solution.status === 'published' ? 'success' : 'secondary'}>{STATUS_LABELS[solution.status] ?? solution.status}</Badge>
},
{
key: 'related_system',
header: '关联系统',
cell: (solution) => <span className="text-muted-foreground">{solution.related_system || '-'}</span>
},
{
key: 'usage_count',
header: '引用次数',
cell: (solution) => <Badge variant="info">{solution.usage_count}</Badge>
},
{
key: 'updated_at',
header: '更新时间',
cell: (solution) => <span className="text-muted-foreground">{formatDateTime(solution.updated_at)}</span>
}
]
export function SolutionListPage() {
const [solutions, setSolutions] = React.useState<Solution[]>([])
const [status, setStatus] = React.useState('all')
const [q, setQ] = React.useState('')
const [error, setError] = React.useState('')
React.useEffect(() => {
listSolutions()
listSolutions(status === 'all' ? {} : { status })
.then(setSolutions)
.catch((reason: unknown) => setError(reason instanceof Error ? reason.message : '加载失败'))
}, [])
}, [status])
const visibleSolutions = q.trim()
? solutions.filter((solution) => {
const keyword = q.trim().toLowerCase()
return `${solution.title} ${solution.problem_signature} ${solution.solution}`.toLowerCase().includes(keyword)
})
: solutions
return (
<main className="min-h-screen bg-background p-6 text-foreground">
<div className="mx-auto flex max-w-6xl flex-col gap-4">
<AppPageHeader title="Solutions" description="Reusable support solutions linked to ticket handling." />
{error ? <AppEmptyState title="Unable to load solutions" description={error} /> : null}
{!error && solutions.length === 0 ? <AppEmptyState title="No solutions" description="Published reusable fixes will appear here." /> : null}
<div className="grid gap-3">
{solutions.map((solution) => (
<Card key={solution.id}>
<CardContent className="grid gap-2">
<div className="flex items-center justify-between gap-3">
<h2 className="text-sm font-semibold">{solution.title}</h2>
<div className="flex items-center gap-2">
<Badge variant={solution.status === 'published' ? 'success' : 'default'}>{solution.status}</Badge>
<Badge variant="info">Used {solution.usage_count}</Badge>
</div>
</div>
<p className="text-sm text-muted-foreground">{solution.problem_signature}</p>
<p className="text-sm">{solution.solution}</p>
</CardContent>
</Card>
))}
<div className="mx-auto flex max-w-7xl flex-col gap-4">
<AppPageHeader title="解决方案库" description="沉淀可复用的问题特征、排查步骤和处理方案。" />
<div className="grid gap-3 md:grid-cols-3">
<Card>
<CardContent>
<p className="text-sm text-muted-foreground">全部方案</p>
<p className="mt-2 text-2xl font-semibold text-foreground">{solutions.length}</p>
</CardContent>
</Card>
<Card>
<CardContent>
<p className="text-sm text-muted-foreground">已发布</p>
<p className="mt-2 text-2xl font-semibold text-foreground">
{solutions.filter((solution) => solution.status === 'published').length}
</p>
</CardContent>
</Card>
<Card>
<CardContent>
<p className="text-sm text-muted-foreground">累计引用</p>
<p className="mt-2 text-2xl font-semibold text-foreground">
{solutions.reduce((total, solution) => total + solution.usage_count, 0)}
</p>
</CardContent>
</Card>
</div>
<AppFilterBar actions={<Button variant="outline" onClick={() => { setQ(''); setStatus('all') }}>重置筛选</Button>}>
<Input placeholder="搜索解决方案" value={q} onChange={(event) => setQ(event.target.value)} />
<Select value={status} onValueChange={setStatus}>
<SelectTrigger className="w-full">
<SelectValue placeholder="全部状态" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">全部状态</SelectItem>
<SelectItem value="published">已发布</SelectItem>
<SelectItem value="draft">草稿</SelectItem>
</SelectContent>
</Select>
</AppFilterBar>
{error ? <AppEmptyState title="无法加载解决方案" description={error} /> : null}
{!error ? (
<AppDataTable
columns={columns}
rows={visibleSolutions}
getRowKey={(solution) => solution.id}
emptyTitle="暂无解决方案"
emptyDescription="已发布的可复用处理方案会显示在这里。"
footer={<p className="text-sm text-muted-foreground">当前显示 {visibleSolutions.length} 条方案</p>}
/>
) : null}
</div>
</main>
)