163 lines
6.1 KiB
TypeScript
163 lines
6.1 KiB
TypeScript
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 { AppPriorityBadge } from '@/components/app/AppPriorityBadge'
|
|
import { AppStatusBadge } from '@/components/app/AppStatusBadge'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Card, CardContent } from '@/components/ui/card'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
|
import { formatDateTime } from '@/lib/format'
|
|
|
|
import { listTickets } from './api'
|
|
import type { Ticket } from './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: 'related_system',
|
|
header: '关联系统',
|
|
cell: (ticket) => <span className="text-muted-foreground">{ticket.related_system || '-'}</span>
|
|
},
|
|
{
|
|
key: 'impact_scope',
|
|
header: '影响范围',
|
|
cell: (ticket) => <span className="text-muted-foreground">{ticket.impact_scope || '-'}</span>
|
|
},
|
|
{
|
|
key: 'created_at',
|
|
header: '创建时间',
|
|
cell: (ticket) => <span className="text-muted-foreground">{formatDateTime(ticket.created_at)}</span>
|
|
},
|
|
{
|
|
key: 'actions',
|
|
header: '操作',
|
|
cell: (ticket) => (
|
|
<Button
|
|
variant="ghost"
|
|
onClick={(event) => {
|
|
event.stopPropagation()
|
|
window.location.hash = `/tickets/${ticket.id}`
|
|
}}
|
|
>
|
|
查看详情
|
|
</Button>
|
|
)
|
|
}
|
|
]
|
|
|
|
export function TicketListPage() {
|
|
const [tickets, setTickets] = React.useState<Ticket[]>([])
|
|
const [status, setStatus] = React.useState('all')
|
|
const [priority, setPriority] = React.useState('all')
|
|
const [q, setQ] = React.useState('')
|
|
const [error, setError] = React.useState('')
|
|
|
|
React.useEffect(() => {
|
|
listTickets({ status: status === 'all' ? '' : status, q })
|
|
.then(setTickets)
|
|
.catch((reason: unknown) => setError(reason instanceof Error ? reason.message : '加载失败'))
|
|
}, [status, q])
|
|
|
|
const visibleTickets = priority === 'all' ? tickets : tickets.filter((ticket) => ticket.priority === priority)
|
|
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.status === 'pending_confirm').length },
|
|
{ label: '已解决', value: tickets.filter((ticket) => ticket.status === 'resolved' || ticket.status === 'closed').length }
|
|
]
|
|
|
|
function resetFilters() {
|
|
setStatus('all')
|
|
setPriority('all')
|
|
setQ('')
|
|
}
|
|
|
|
return (
|
|
<main className="min-h-screen bg-background p-6 text-foreground">
|
|
<div className="mx-auto flex max-w-7xl flex-col gap-4">
|
|
<AppPageHeader
|
|
title="工单中心"
|
|
description="统一查看、筛选和处理技术支持工单。"
|
|
actions={<Button onClick={() => (window.location.hash = '/tickets/new')}>新建工单</Button>}
|
|
/>
|
|
|
|
<div className="grid gap-3 md: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>
|
|
|
|
<AppFilterBar actions={<Button variant="outline" onClick={resetFilters}>重置筛选</Button>}>
|
|
<Input onChange={(event) => setQ(event.target.value)} placeholder="搜索工单编号或标题" value={q} />
|
|
<Select onValueChange={setStatus} value={status}>
|
|
<SelectTrigger className="w-full">
|
|
<SelectValue placeholder="全部状态" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">全部状态</SelectItem>
|
|
<SelectItem value="pending">待处理</SelectItem>
|
|
<SelectItem value="in_progress">处理中</SelectItem>
|
|
<SelectItem value="pending_confirm">待确认</SelectItem>
|
|
<SelectItem value="resolved">已解决</SelectItem>
|
|
<SelectItem value="closed">已关闭</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<Select onValueChange={setPriority} value={priority}>
|
|
<SelectTrigger className="w-full">
|
|
<SelectValue placeholder="全部优先级" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">全部优先级</SelectItem>
|
|
<SelectItem value="low">低</SelectItem>
|
|
<SelectItem value="medium">中</SelectItem>
|
|
<SelectItem value="high">高</SelectItem>
|
|
<SelectItem value="urgent">紧急</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</AppFilterBar>
|
|
|
|
{error ? <AppEmptyState title="无法加载工单" description={error} /> : null}
|
|
{!error ? (
|
|
<AppDataTable
|
|
columns={columns}
|
|
rows={visibleTickets}
|
|
getRowKey={(ticket) => ticket.id}
|
|
emptyTitle="暂无工单"
|
|
emptyDescription="创建第一张工单后,列表会显示在这里。"
|
|
footer={<p className="text-sm text-muted-foreground">当前显示 {visibleTickets.length} 条工单</p>}
|
|
onRowClick={(ticket) => (window.location.hash = `/tickets/${ticket.id}`)}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
</main>
|
|
)
|
|
}
|