feat: finish ticket create visual experience

This commit is contained in:
hailin
2026-07-01 15:34:41 +08:00
parent 3aff628f52
commit 0ea4a3c853
4 changed files with 159 additions and 86 deletions

View File

@@ -45,7 +45,7 @@ Frontend:
- Modify: `backend/apps/tickets/services.py` - Modify: `backend/apps/tickets/services.py`
- Modify: `backend/apps/tickets/tests/test_ticket_api.py` - Modify: `backend/apps/tickets/tests/test_ticket_api.py`
- [ ] **Step 1: Write failing pagination/filter/display tests** - [x] **Step 1: Write failing pagination/filter/display tests**
Add these tests to `backend/apps/tickets/tests/test_ticket_api.py`: Add these tests to `backend/apps/tickets/tests/test_ticket_api.py`:
@@ -125,7 +125,7 @@ def test_transition_ticket_requires_non_empty_remark(admin_client, db):
assert response.status_code == 400 assert response.status_code == 400
``` ```
- [ ] **Step 2: Run tests to verify RED** - [x] **Step 2: Run tests to verify RED**
Run: Run:
@@ -136,7 +136,7 @@ python -m pytest apps/tickets/tests/test_ticket_api.py -v
Expected: fails because list response is currently a list, display users are missing, advanced filters are missing, and empty transition remark is allowed. Expected: fails because list response is currently a list, display users are missing, advanced filters are missing, and empty transition remark is allowed.
- [ ] **Step 3: Implement schemas and serialization** - [x] **Step 3: Implement schemas and serialization**
Add schemas to `backend/apps/tickets/schemas.py`: Add schemas to `backend/apps/tickets/schemas.py`:
@@ -196,7 +196,7 @@ def serialize_ticket(ticket: Ticket) -> dict:
} }
``` ```
- [ ] **Step 4: Implement paginated list and filters** - [x] **Step 4: Implement paginated list and filters**
Change `list_tickets` to return `TicketListOut` and accept: Change `list_tickets` to return `TicketListOut` and accept:
@@ -229,7 +229,7 @@ page = max(page, 1)
page_size = min(max(page_size, 1), 100) page_size = min(max(page_size, 1), 100)
``` ```
- [ ] **Step 5: Require transition remarks** - [x] **Step 5: Require transition remarks**
In `update_ticket_status`, reject blank remarks: In `update_ticket_status`, reject blank remarks:
@@ -240,7 +240,7 @@ if not payload.remark.strip():
Import `HttpError` from `ninja.errors`. Import `HttpError` from `ninja.errors`.
- [ ] **Step 6: Update existing tests for new list shape** - [x] **Step 6: Update existing tests for new list shape**
Change existing list assertions in `test_list_tickets_filters_by_status`: Change existing list assertions in `test_list_tickets_filters_by_status`:
@@ -250,7 +250,7 @@ assert payload["total"] == 1
assert payload["items"][0]["ticket_no"] == "TK202606300001" assert payload["items"][0]["ticket_no"] == "TK202606300001"
``` ```
- [ ] **Step 7: Verify GREEN** - [x] **Step 7: Verify GREEN**
Run: Run:
@@ -262,7 +262,7 @@ python -m ruff check .
Expected: all selected tests pass and ruff has no errors. Expected: all selected tests pass and ruff has no errors.
- [ ] **Step 8: Commit** - [x] **Step 8: Commit**
```bash ```bash
git add backend/apps/tickets git add backend/apps/tickets
@@ -278,7 +278,7 @@ git commit -m "feat: add ticket list visual support api"
- Create: `frontend/src/features/tickets/TicketMetricCards.tsx` - Create: `frontend/src/features/tickets/TicketMetricCards.tsx`
- Create: `frontend/src/features/tickets/TicketFilterBar.tsx` - Create: `frontend/src/features/tickets/TicketFilterBar.tsx`
- [ ] **Step 1: Refresh shadcn docs** - [x] **Step 1: Refresh shadcn docs**
Run: Run:
@@ -289,7 +289,7 @@ npx shadcn@latest docs tabs table card badge select input pagination skeleton so
Use installed components first. Do not overwrite existing locally patched components. Use installed components first. Do not overwrite existing locally patched components.
- [ ] **Step 2: Update ticket frontend types** - [x] **Step 2: Update ticket frontend types**
In `frontend/src/features/tickets/types.ts`, add: In `frontend/src/features/tickets/types.ts`, add:
@@ -332,7 +332,7 @@ export type TicketListParams = {
} }
``` ```
- [ ] **Step 3: Update ticket API** - [x] **Step 3: Update ticket API**
In `frontend/src/features/tickets/api.ts`, update `listTickets` to return `TicketListResponse` and include all query fields: In `frontend/src/features/tickets/api.ts`, update `listTickets` to return `TicketListResponse` and include all query fields:
@@ -349,7 +349,7 @@ export function listTickets(params: TicketListParams = {}) {
} }
``` ```
- [ ] **Step 4: Create TicketMetricCards** - [x] **Step 4: Create TicketMetricCards**
Create `frontend/src/features/tickets/TicketMetricCards.tsx` with compact shadcn `Card` metrics for: Create `frontend/src/features/tickets/TicketMetricCards.tsx` with compact shadcn `Card` metrics for:
@@ -368,7 +368,7 @@ type TicketMetricCardsProps = {
} }
``` ```
- [ ] **Step 5: Create TicketFilterBar** - [x] **Step 5: Create TicketFilterBar**
Create `frontend/src/features/tickets/TicketFilterBar.tsx` using `AppFilterBar`, `Input`, `Select`, and `Button`. Create `frontend/src/features/tickets/TicketFilterBar.tsx` using `AppFilterBar`, `Input`, `Select`, and `Button`.
@@ -388,7 +388,7 @@ type TicketFilterBarProps = {
} }
``` ```
- [ ] **Step 6: Redesign TicketListPage** - [x] **Step 6: Redesign TicketListPage**
Update `TicketListPage`: Update `TicketListPage`:
@@ -400,7 +400,7 @@ Update `TicketListPage`:
- Table columns must be Chinese and include requester/assignee display when available. - Table columns must be Chinese and include requester/assignee display when available.
- Empty/error/loading states must be Chinese. - Empty/error/loading states must be Chinese.
- [ ] **Step 7: Verify frontend GREEN** - [x] **Step 7: Verify frontend GREEN**
Run: Run:
@@ -412,7 +412,7 @@ npm run build
Expected: both pass. Expected: both pass.
- [ ] **Step 8: Commit** - [x] **Step 8: Commit**
```bash ```bash
git add frontend/src/features/tickets git add frontend/src/features/tickets
@@ -428,7 +428,7 @@ git commit -m "feat: redesign ticket list experience"
- Create: `frontend/src/features/tickets/TicketTransitionDialog.tsx` - Create: `frontend/src/features/tickets/TicketTransitionDialog.tsx`
- Create: `frontend/src/features/tickets/TicketMetaPanel.tsx` - Create: `frontend/src/features/tickets/TicketMetaPanel.tsx`
- [ ] **Step 1: Refresh shadcn docs** - [x] **Step 1: Refresh shadcn docs**
Run: Run:
@@ -437,7 +437,7 @@ cd frontend
npx shadcn@latest docs dialog textarea card badge separator tabs sonner npx shadcn@latest docs dialog textarea card badge separator tabs sonner
``` ```
- [ ] **Step 2: Create TicketTransitionDialog** - [x] **Step 2: Create TicketTransitionDialog**
Create a dialog component with: Create a dialog component with:
@@ -461,7 +461,7 @@ Visible text:
The submit button is disabled until `remark.trim()` is non-empty. The submit button is disabled until `remark.trim()` is non-empty.
- [ ] **Step 3: Create TicketMetaPanel** - [x] **Step 3: Create TicketMetaPanel**
Create `TicketMetaPanel` with shadcn `Card`, `Badge`, `Separator`, `AppField`, `AppStatusBadge`, and `AppPriorityBadge`. Create `TicketMetaPanel` with shadcn `Card`, `Badge`, `Separator`, `AppField`, `AppStatusBadge`, and `AppPriorityBadge`.
@@ -477,7 +477,7 @@ Display:
影响范围 影响范围
``` ```
- [ ] **Step 4: Update TicketStatusTimeline** - [x] **Step 4: Update TicketStatusTimeline**
Make every event label Chinese: Make every event label Chinese:
@@ -491,7 +491,7 @@ const EVENT_LABELS: Record<string, string> = {
} }
``` ```
- [ ] **Step 5: Update TicketComments** - [x] **Step 5: Update TicketComments**
Use Chinese labels and distinguish: Use Chinese labels and distinguish:
@@ -506,7 +506,7 @@ Use shadcn `Tabs` or `Select` to choose visibility. Existing API can still recei
{ comment_type: 'internal_note' | 'user_reply', visibility: 'internal' | 'public' } { comment_type: 'internal_note' | 'user_reply', visibility: 'internal' | 'public' }
``` ```
- [ ] **Step 6: Redesign TicketDetailPage** - [x] **Step 6: Redesign TicketDetailPage**
Update page: Update page:
@@ -516,7 +516,7 @@ Update page:
- Main column contains `问题描述`, `处理记录`, `评论与协作`. - Main column contains `问题描述`, `处理记录`, `评论与协作`.
- Side column uses `TicketMetaPanel`, `SolutionLinkPanel`, and restrained AI placeholder. - Side column uses `TicketMetaPanel`, `SolutionLinkPanel`, and restrained AI placeholder.
- [ ] **Step 7: Verify frontend GREEN** - [x] **Step 7: Verify frontend GREEN**
Run: Run:
@@ -528,7 +528,7 @@ npm run build
Expected: both pass. Expected: both pass.
- [ ] **Step 8: Commit** - [x] **Step 8: Commit**
```bash ```bash
git add frontend/src/features/tickets git add frontend/src/features/tickets
@@ -540,7 +540,7 @@ git commit -m "feat: redesign ticket detail experience"
**Files:** **Files:**
- Modify: `frontend/src/features/tickets/TicketCreatePage.tsx` - Modify: `frontend/src/features/tickets/TicketCreatePage.tsx`
- [ ] **Step 1: Refresh shadcn docs** - [x] **Step 1: Refresh shadcn docs**
Run: Run:
@@ -549,7 +549,7 @@ cd frontend
npx shadcn@latest docs card input select textarea badge alert npx shadcn@latest docs card input select textarea badge alert
``` ```
- [ ] **Step 2: Redesign create page** - [x] **Step 2: Redesign create page**
Update `TicketCreatePage` into grouped sections: Update `TicketCreatePage` into grouped sections:
@@ -563,7 +563,7 @@ Update `TicketCreatePage` into grouped sections:
Visible copy must be Chinese. Use `Card`, `Input`, `Select`, `Textarea`, `Badge`, and `Button`. Visible copy must be Chinese. Use `Card`, `Input`, `Select`, `Textarea`, `Badge`, and `Button`.
- [ ] **Step 3: Use Chinese error feedback** - [x] **Step 3: Use Chinese error feedback**
On create failure, show: On create failure, show:
@@ -571,7 +571,7 @@ On create failure, show:
创建工单失败,请检查必填信息或稍后重试。 创建工单失败,请检查必填信息或稍后重试。
``` ```
- [ ] **Step 4: Run full verification** - [x] **Step 4: Run full verification**
Run: Run:
@@ -588,7 +588,7 @@ npx shadcn@latest info
Expected: all commands pass. Expected: all commands pass.
- [ ] **Step 5: Browser visual smoke test** - [x] **Step 5: Browser visual smoke test**
With local backend/frontend running: With local backend/frontend running:
@@ -597,7 +597,7 @@ With local backend/frontend running:
- Open a ticket detail page and verify transition dialog requires handling note. - Open a ticket detail page and verify transition dialog requires handling note.
- Open `#/tickets/new` and verify grouped create form has no obvious overlap. - Open `#/tickets/new` and verify grouped create form has no obvious overlap.
- [ ] **Step 6: Commit** - [x] **Step 6: Commit**
```bash ```bash
git add frontend/src/features/tickets git add frontend/src/features/tickets
@@ -606,15 +606,15 @@ git commit -m "feat: redesign ticket create experience"
## Final Review Checklist ## Final Review Checklist
- [ ] Ticket list uses shadcn tabs, cards, table, filters, and pagination. - [x] Ticket list uses shadcn tabs, cards, table, filters, and pagination.
- [ ] Ticket detail uses a Chinese transition dialog and no English loading/error copy. - [x] Ticket detail uses a Chinese transition dialog and no English loading/error copy.
- [ ] Ticket comments distinguish internal notes and user-visible replies. - [x] Ticket comments distinguish internal notes and user-visible replies.
- [ ] Ticket create page uses grouped Chinese form sections. - [x] Ticket create page uses grouped Chinese form sections.
- [ ] Backend ticket list supports pagination and visual filters. - [x] Backend ticket list supports pagination and visual filters.
- [ ] Ticket responses include requester and assignee display fields. - [x] Ticket responses include requester and assignee display fields.
- [ ] Empty transition remarks are rejected. - [x] Empty transition remarks are rejected.
- [ ] Backend ticket tests pass. - [x] Backend ticket tests pass.
- [ ] Backend ruff passes. - [x] Backend ruff passes.
- [ ] Frontend typecheck passes. - [x] Frontend typecheck passes.
- [ ] Frontend build passes. - [x] Frontend build passes.
- [ ] shadcn info still reports Vite/Tailwind v3 and installed components. - [x] shadcn info still reports Vite/Tailwind v3 and installed components.

View File

@@ -19,6 +19,7 @@ type AppDataTableProps<Row> = {
emptyDescription?: string emptyDescription?: string
toolbar?: React.ReactNode toolbar?: React.ReactNode
footer?: React.ReactNode footer?: React.ReactNode
tableClassName?: string
onRowClick?: (row: Row) => void onRowClick?: (row: Row) => void
} }
@@ -30,6 +31,7 @@ export function AppDataTable<Row>({
emptyDescription, emptyDescription,
toolbar, toolbar,
footer, footer,
tableClassName,
onRowClick onRowClick
}: AppDataTableProps<Row>) { }: AppDataTableProps<Row>) {
return ( return (
@@ -41,7 +43,7 @@ export function AppDataTable<Row>({
</div> </div>
) : ( ) : (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<Table> <Table className={tableClassName}>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
{columns.map((column) => ( {columns.map((column) => (

View File

@@ -1,9 +1,10 @@
import * as React from 'react' import * as React from 'react'
import { Bot } from 'lucide-react' import { AlertCircle, Bot, CheckCircle2, Lightbulb, Send } from 'lucide-react'
import { AppField } from '@/components/app/AppField' import { AppField } from '@/components/app/AppField'
import { AppPageHeader } from '@/components/app/AppPageHeader' import { AppPageHeader } from '@/components/app/AppPageHeader'
import { AppSection } from '@/components/app/AppSection' import { AppSection } from '@/components/app/AppSection'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
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 { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
@@ -25,6 +26,7 @@ const initialForm: TicketInput = {
export function TicketCreatePage() { export function TicketCreatePage() {
const [form, setForm] = React.useState<TicketInput>(initialForm) const [form, setForm] = React.useState<TicketInput>(initialForm)
const [error, setError] = React.useState('') const [error, setError] = React.useState('')
const [submitting, setSubmitting] = React.useState(false)
function updateField<Key extends keyof TicketInput>(field: Key, value: TicketInput[Key]) { function updateField<Key extends keyof TicketInput>(field: Key, value: TicketInput[Key]) {
setForm((current) => ({ ...current, [field]: value })) setForm((current) => ({ ...current, [field]: value }))
@@ -33,21 +35,35 @@ export function TicketCreatePage() {
async function handleSubmit(event: React.FormEvent) { async function handleSubmit(event: React.FormEvent) {
event.preventDefault() event.preventDefault()
setError('') setError('')
setSubmitting(true)
try { try {
const ticket = await createTicket(form) const ticket = await createTicket(form)
window.location.hash = `/tickets/${ticket.id}` window.location.hash = `/tickets/${ticket.id}`
} catch (reason) { } catch (reason) {
setError(reason instanceof Error ? reason.message : '创建失败') setError(reason instanceof Error ? reason.message : '创建工单失败,请检查必填信息或稍后重试。')
} finally {
setSubmitting(false)
} }
} }
return ( return (
<main className="min-h-screen bg-background p-6 text-foreground"> <main className="min-h-screen bg-background p-6 text-foreground">
<div className="mx-auto grid max-w-6xl gap-4"> <div className="mx-auto grid max-w-6xl gap-5">
<AppPageHeader title="创建工单" description="记录明确的技术支持请求,便于后续分派、排查和复用方案。" /> <AppPageHeader
title="创建工单"
description="记录明确的技术支持请求,便于后续分派、排查和复用方案。"
/>
<form className="grid gap-4 xl:grid-cols-[minmax(0,1fr)_340px]" onSubmit={handleSubmit}> <form className="grid gap-4 xl:grid-cols-[minmax(0,1fr)_340px]" onSubmit={handleSubmit}>
<div className="grid gap-4"> <div className="grid gap-4">
<AppSection title="问题描述" description="请先说明现象、出现时间和可复现条件。"> {error ? (
<Alert variant="destructive">
<AlertCircle className="size-4" />
<AlertTitle>创建工单失败</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
) : null}
<AppSection title="基础信息" description="先说明这张工单要解决什么问题。">
<AppField label="工单标题"> <AppField label="工单标题">
<Input <Input
required required
@@ -56,18 +72,9 @@ export function TicketCreatePage() {
onChange={(event) => updateField('title', event.target.value)} onChange={(event) => updateField('title', event.target.value)}
/> />
</AppField> </AppField>
<div className="mt-4">
<AppField label="问题描述">
<Textarea
required
placeholder="请描述影响范围、报错信息、操作步骤和已尝试的处理方式。"
value={form.description}
onChange={(event) => updateField('description', event.target.value)}
/>
</AppField>
</div>
</AppSection> </AppSection>
<AppSection title="处理属性" description="用于技术支持人员快速判断优先级和处理路径。">
<AppSection title="影响与优先级" description="帮助支持团队判断响应顺序和处理风险。">
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<AppField label="优先级"> <AppField label="优先级">
<Select value={form.priority} onValueChange={(value) => updateField('priority', value as TicketInput['priority'])}> <Select value={form.priority} onValueChange={(value) => updateField('priority', value as TicketInput['priority'])}>
@@ -82,13 +89,6 @@ export function TicketCreatePage() {
</SelectContent> </SelectContent>
</Select> </Select>
</AppField> </AppField>
<AppField label="关联系统">
<Input
placeholder="例如:CRM、财务系统、数据中台"
value={form.related_system}
onChange={(event) => updateField('related_system', event.target.value)}
/>
</AppField>
<AppField label="影响范围"> <AppField label="影响范围">
<Input <Input
placeholder="例如:单个用户、某部门、全部用户" placeholder="例如:单个用户、某部门、全部用户"
@@ -96,6 +96,18 @@ export function TicketCreatePage() {
onChange={(event) => updateField('impact_scope', event.target.value)} onChange={(event) => updateField('impact_scope', event.target.value)}
/> />
</AppField> </AppField>
</div>
</AppSection>
<AppSection title="环境与系统" description="补充问题发生的系统域和运行环境。">
<div className="grid gap-4 md:grid-cols-2">
<AppField label="关联系统">
<Input
placeholder="例如:CRM、财务系统、数据中台"
value={form.related_system}
onChange={(event) => updateField('related_system', event.target.value)}
/>
</AppField>
<AppField label="运行环境"> <AppField label="运行环境">
<Input <Input
placeholder="例如:生产环境、测试环境、Windows 终端" placeholder="例如:生产环境、测试环境、Windows 终端"
@@ -105,20 +117,60 @@ export function TicketCreatePage() {
</AppField> </AppField>
</div> </div>
</AppSection> </AppSection>
{error ? <p className="text-sm text-danger">{error}</p> : null}
<div className="flex justify-end"> <AppSection title="问题描述" description="请描述现象、出现时间、复现步骤和已尝试的处理方式。">
<Button type="submit">提交工单</Button> <AppField label="详细描述">
<Textarea
required
placeholder="请描述影响范围、报错信息、操作步骤和已尝试的处理方式。"
value={form.description}
onChange={(event) => updateField('description', event.target.value)}
/>
</AppField>
</AppSection>
<AppSection title="提交确认" description="提交后工单会进入待处理队列。">
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div className="flex items-start gap-2 text-sm text-muted-foreground">
<CheckCircle2 className="mt-0.5 size-4" />
<span>请确认标题和问题描述已足够支持技术人员开始排查。</span>
</div>
<Button type="submit" disabled={submitting}>
<Send className="size-4" />
{submitting ? '提交中...' : '提交工单'}
</Button>
</div> </div>
</AppSection>
</div>
<div className="grid content-start gap-4">
<AppSection title="建单提示" description="高质量工单能减少来回确认。">
<div className="grid gap-3 text-sm text-muted-foreground">
<div className="flex gap-2">
<Lightbulb className="mt-0.5 size-4" />
<span>标题建议包含系统、场景和异常现象。</span>
</div>
<div className="flex gap-2">
<Lightbulb className="mt-0.5 size-4" />
<span>描述中写清楚复现步骤、报错信息和影响范围。</span>
</div>
<div className="flex gap-2">
<Lightbulb className="mt-0.5 size-4" />
<span>紧急优先级应只用于阻断生产或大范围影响问题。</span>
</div>
</div>
</AppSection>
<AppSection
title="AI 建单辅助"
description="后续阶段能力预留"
action={<Badge variant="ai"><Bot className="size-3" /> 即将接入</Badge>}
>
<p className="text-sm text-muted-foreground">
后续阶段将接入建单 Agent,用于补全字段、追问缺失信息和推荐相似问题。
</p>
</AppSection>
</div> </div>
<AppSection
title="AI 建单辅助"
description="后续阶段能力预留"
action={<Badge variant="ai"><Bot className="size-3" /> 即将接入</Badge>}
>
<p className="text-sm text-muted-foreground">
后续阶段将接入建单 Agent,用于补全字段、追问缺失信息和推荐相似问题。
</p>
</AppSection>
</form> </form>
</div> </div>
</main> </main>

View File

@@ -17,7 +17,6 @@ import {
} from '@/components/ui/pagination' } from '@/components/ui/pagination'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { formatDateTime } from '@/lib/format'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { listTickets } from './api' import { listTickets } from './api'
@@ -27,6 +26,15 @@ import type { Ticket, TicketListResponse, TicketListView } from './types'
const PAGE_SIZE = 20 const PAGE_SIZE = 20
function formatTicketTableDate(value: string) {
return new Intl.DateTimeFormat('zh-CN', {
month: 'numeric',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
}).format(new Date(value))
}
const viewTabs: Array<{ value: TicketListView; label: string }> = [ const viewTabs: Array<{ value: TicketListView; label: string }> = [
{ value: 'all', label: '全部工单' }, { value: 'all', label: '全部工单' },
{ value: 'assigned_to_me', label: '我的待办' }, { value: 'assigned_to_me', label: '我的待办' },
@@ -38,13 +46,15 @@ const columns: AppDataTableColumn<Ticket>[] = [
{ {
key: 'ticket_no', key: 'ticket_no',
header: '编号', header: '编号',
className: 'w-[14%]',
cell: (ticket) => <span className="font-medium text-foreground">{ticket.ticket_no}</span> cell: (ticket) => <span className="font-medium text-foreground">{ticket.ticket_no}</span>
}, },
{ {
key: 'title', key: 'title',
header: '标题', header: '标题',
className: 'w-[24%] whitespace-normal',
cell: (ticket) => ( cell: (ticket) => (
<div className="min-w-[240px]"> <div className="min-w-0">
<p className="line-clamp-1 font-medium text-foreground">{ticket.title}</p> <p className="line-clamp-1 font-medium text-foreground">{ticket.title}</p>
<p className="mt-1 line-clamp-1 text-xs text-muted-foreground"> <p className="mt-1 line-clamp-1 text-xs text-muted-foreground">
{ticket.requester?.display_name ? `请求人:${ticket.requester.display_name}` : ticket.environment || '暂无环境信息'} {ticket.requester?.display_name ? `请求人:${ticket.requester.display_name}` : ticket.environment || '暂无环境信息'}
@@ -55,46 +65,54 @@ const columns: AppDataTableColumn<Ticket>[] = [
{ {
key: 'status', key: 'status',
header: '状态', header: '状态',
className: 'w-[8%]',
cell: (ticket) => <AppStatusBadge status={ticket.status} /> cell: (ticket) => <AppStatusBadge status={ticket.status} />
}, },
{ {
key: 'priority', key: 'priority',
header: '优先级', header: '优先级',
className: 'w-[7%]',
cell: (ticket) => <AppPriorityBadge priority={ticket.priority} /> cell: (ticket) => <AppPriorityBadge priority={ticket.priority} />
}, },
{ {
key: 'related_system', key: 'related_system',
header: '关联系统', header: '关联系统',
cell: (ticket) => <span className="text-muted-foreground">{ticket.related_system || '-'}</span> className: 'w-[10%]',
cell: (ticket) => <span className="block truncate text-muted-foreground">{ticket.related_system || '-'}</span>
}, },
{ {
key: 'impact_scope', key: 'impact_scope',
header: '影响范围', header: '影响范围',
cell: (ticket) => <span className="text-muted-foreground">{ticket.impact_scope || '-'}</span> className: 'w-[11%]',
cell: (ticket) => <span className="block truncate text-muted-foreground">{ticket.impact_scope || '-'}</span>
}, },
{ {
key: 'assignee', key: 'assignee',
header: '处理人', header: '处理人',
cell: (ticket) => <span className="text-muted-foreground">{ticket.assignee?.display_name || '未分派'}</span> className: 'w-[8%]',
cell: (ticket) => <span className="block truncate text-muted-foreground">{ticket.assignee?.display_name || '未分派'}</span>
}, },
{ {
key: 'created_at', key: 'created_at',
header: '创建时间', header: '创建时间',
cell: (ticket) => <span className="text-muted-foreground">{formatDateTime(ticket.created_at)}</span> className: 'w-[10%]',
cell: (ticket) => <span className="text-muted-foreground">{formatTicketTableDate(ticket.created_at)}</span>
}, },
{ {
key: 'actions', key: 'actions',
header: '操作', header: '操作',
className: 'w-[8%] text-right',
cell: (ticket) => ( cell: (ticket) => (
<Button <Button
variant="ghost" variant="ghost"
className="gap-1" size="sm"
className="gap-1 px-2"
onClick={(event) => { onClick={(event) => {
event.stopPropagation() event.stopPropagation()
window.location.hash = `/tickets/${ticket.id}` window.location.hash = `/tickets/${ticket.id}`
}} }}
> >
查看详情 详情
<ArrowRight className="size-4" /> <ArrowRight className="size-4" />
</Button> </Button>
) )
@@ -264,6 +282,7 @@ export function TicketListPage() {
</Pagination> </Pagination>
</div> </div>
} }
tableClassName="table-fixed"
onRowClick={(ticket) => (window.location.hash = `/tickets/${ticket.id}`)} onRowClick={(ticket) => (window.location.hash = `/tickets/${ticket.id}`)}
/> />
) : null} ) : null}