refactor: refine ticket center operations view

This commit is contained in:
hailin
2026-07-02 16:18:04 +08:00
parent 942b712c20
commit f8af731bc3

View File

@@ -1,5 +1,5 @@
import * as React from 'react'
import { ArrowRight, Plus } from 'lucide-react'
import { ArrowRight, Clock3, Plus } from 'lucide-react'
import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable'
import { AppEmptyState } from '@/components/app/AppEmptyState'
@@ -7,7 +7,9 @@ import { AppPage } from '@/components/app/AppPage'
import { AppPageHeader } from '@/components/app/AppPageHeader'
import { AppPriorityBadge } from '@/components/app/AppPriorityBadge'
import { AppStatusBadge } from '@/components/app/AppStatusBadge'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import {
Pagination,
PaginationContent,
@@ -18,6 +20,7 @@ import {
} from '@/components/ui/pagination'
import { Skeleton } from '@/components/ui/skeleton'
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { formatDateTime } from '@/lib/format'
import { cn } from '@/lib/utils'
import { listTickets } from './api'
@@ -43,61 +46,77 @@ const viewTabs: Array<{ value: TicketListView; label: string }> = [
{ value: 'high_priority', label: '高优先级' }
]
function displayUser(user: Ticket['assignee']) {
return user?.display_name || user?.username || '未分派'
}
function getTicketAgeLabel(ticket: Ticket) {
const createdAt = new Date(ticket.created_at).getTime()
const diffHours = Math.max(0, Math.floor((Date.now() - createdAt) / 1000 / 60 / 60))
if (diffHours < 1) return '1 小时内'
if (diffHours < 24) return `${diffHours} 小时`
return `${Math.floor(diffHours / 24)} 天`
}
const columns: AppDataTableColumn<Ticket>[] = [
{
key: 'ticket_no',
header: '编号',
className: 'w-[14%]',
cell: (ticket) => <span className="font-medium text-foreground">{ticket.ticket_no}</span>
},
{
key: 'title',
header: '标题',
className: 'w-[24%] whitespace-normal',
key: 'ticket',
header: '工单',
className: 'w-[34%] whitespace-normal',
cell: (ticket) => (
<div className="min-w-0">
<p className="line-clamp-1 font-medium text-foreground">{ticket.title}</p>
<p className="mt-1 line-clamp-1 text-xs text-muted-foreground">
{ticket.requester?.display_name ? `请求人:${ticket.requester.display_name}` : ticket.environment || '暂无环境信息'}
</p>
<div className="flex flex-wrap items-center gap-2">
<Badge variant="outline">{ticket.ticket_no}</Badge>
<span className="text-xs text-muted-foreground">请求人:{displayUser(ticket.requester)}</span>
</div>
<p className="mt-2 line-clamp-1 font-medium text-foreground">{ticket.title}</p>
<p className="mt-1 line-clamp-1 text-xs text-muted-foreground">{ticket.description || '暂无问题描述'}</p>
</div>
)
},
{
key: 'status',
header: '状态',
className: 'w-[8%]',
cell: (ticket) => <AppStatusBadge status={ticket.status} />
key: 'workflow',
header: '流程',
className: 'w-[16%]',
cell: (ticket) => (
<div className="flex flex-wrap gap-1.5">
<AppStatusBadge status={ticket.status} />
<AppPriorityBadge priority={ticket.priority} />
</div>
)
},
{
key: 'priority',
header: '优先级',
className: 'w-[7%]',
cell: (ticket) => <AppPriorityBadge priority={ticket.priority} />
},
{
key: 'related_system',
header: '关联系统',
className: 'w-[10%]',
cell: (ticket) => <span className="block truncate text-muted-foreground">{ticket.related_system || '-'}</span>
},
{
key: 'impact_scope',
header: '影响范围',
className: 'w-[11%]',
cell: (ticket) => <span className="block truncate text-muted-foreground">{ticket.impact_scope || '-'}</span>
key: 'context',
header: '业务上下文',
className: 'w-[22%] whitespace-normal',
cell: (ticket) => (
<div className="grid min-w-0 gap-1">
<p className="truncate text-sm text-foreground">{ticket.related_system || '未填写系统'}</p>
<p className="line-clamp-1 text-xs text-muted-foreground">{ticket.impact_scope || ticket.environment || '暂无影响范围'}</p>
</div>
)
},
{
key: 'assignee',
header: '处理人',
className: 'w-[8%]',
cell: (ticket) => <span className="block truncate text-muted-foreground">{ticket.assignee?.display_name || '未分派'}</span>
header: '责任人',
className: 'w-[12%]',
cell: (ticket) => (
<div className="grid gap-1">
<span className="truncate text-sm text-foreground">{displayUser(ticket.assignee)}</span>
<span className="text-xs text-muted-foreground">{ticket.assignee ? '已分派' : '待分派'}</span>
</div>
)
},
{
key: 'created_at',
header: '创建时间',
key: 'updated_at',
header: '时间',
className: 'w-[10%]',
cell: (ticket) => <span className="text-muted-foreground">{formatTicketTableDate(ticket.created_at)}</span>
cell: (ticket) => (
<div className="grid gap-1 text-muted-foreground">
<span className="text-sm">{formatTicketTableDate(ticket.updated_at)}</span>
<span className="text-xs">已创建 {getTicketAgeLabel(ticket)}</span>
</div>
)
},
{
key: 'actions',
@@ -194,10 +213,14 @@ export function TicketListPage() {
setPage(target)
}
const hasActiveFilters = status !== 'all' || priority !== 'all' || relatedSystem.trim() || q.trim()
return (
<AppPage contentClassName="gap-5">
<AppPageHeader
title="工单中心"
description="集中查看、筛选和推进企业技术支持请求。"
meta={hasActiveFilters ? <Badge variant="info">已筛选</Badge> : <Badge variant="outline">全部范围</Badge>}
actions={
<Button onClick={() => (window.location.hash = '/tickets/new')}>
<Plus data-icon="inline-start" />
@@ -208,6 +231,8 @@ export function TicketListPage() {
<TicketMetricCards tickets={result.items} />
<Card className="py-0">
<CardContent className="flex flex-col gap-3 p-3 lg:flex-row lg:items-center lg:justify-between">
<Tabs value={view} onValueChange={updateView}>
<TabsList>
{viewTabs.map((tab) => (
@@ -217,6 +242,12 @@ export function TicketListPage() {
))}
</TabsList>
</Tabs>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Clock3 className="size-4" />
最近更新:{result.items[0]?.updated_at ? formatDateTime(result.items[0].updated_at) : '暂无数据'}
</div>
</CardContent>
</Card>
<TicketFilterBar
q={q}
@@ -232,11 +263,13 @@ export function TicketListPage() {
{error ? <AppEmptyState title="无法加载工单" description={error} /> : null}
{loading && !error ? (
<div className="grid gap-3 rounded-lg border border-border bg-surface p-4">
<Card>
<CardContent className="grid gap-3 p-4">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-32 w-full" />
<Skeleton className="h-8 w-64" />
</div>
</CardContent>
</Card>
) : null}
{!error && !loading ? (
<AppDataTable
@@ -281,7 +314,7 @@ export function TicketListPage() {
</Pagination>
</div>
}
tableClassName="table-fixed"
tableClassName="table-fixed min-w-[980px]"
onRowClick={(ticket) => (window.location.hash = `/tickets/${ticket.id}`)}
/>
) : null}