feat: polish solution ui and design docs
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user