feat: add ticket workflow pages

This commit is contained in:
hailin
2026-06-30 18:49:41 +08:00
parent c80c286227
commit d40fdad6e9
13 changed files with 467 additions and 8 deletions

View File

@@ -0,0 +1,93 @@
import * as React from 'react'
import { AppEmptyState } from '@/components/app/AppEmptyState'
import { AppField } from '@/components/app/AppField'
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 { getTicket, transitionTicket } from './api'
import { TicketComments } from './TicketComments'
import { TicketStatusTimeline } from './TicketStatusTimeline'
import type { Ticket, TicketStatus } from './types'
const NEXT_STATUSES: TicketStatus[] = ['in_progress', 'pending_confirm', 'resolved', 'closed']
export function TicketDetailPage({ ticketId }: { ticketId: number }) {
const [ticket, setTicket] = React.useState<Ticket | null>(null)
const [error, setError] = React.useState('')
React.useEffect(() => {
getTicket(ticketId)
.then(setTicket)
.catch((reason: unknown) => setError(reason instanceof Error ? reason.message : '加载失败'))
}, [ticketId])
async function handleTransition(status: TicketStatus) {
if (!ticket) return
const updated = await transitionTicket(ticket.id, status, `Move to ${status}`)
setTicket(updated)
}
if (error) {
return (
<main className="min-h-screen bg-background p-6 text-foreground">
<AppEmptyState title="Unable to load ticket" description={error} />
</main>
)
}
if (!ticket) {
return (
<main className="min-h-screen bg-background p-6 text-foreground">
<AppEmptyState title="Loading ticket" />
</main>
)
}
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={ticket.title}
description={ticket.ticket_no}
actions={
<div className="flex flex-wrap gap-2">
{NEXT_STATUSES.map((status) => (
<Button key={status} variant="secondary" onClick={() => void handleTransition(status)}>
{status}
</Button>
))}
</div>
}
/>
<Card>
<CardContent className="grid gap-4 md:grid-cols-3">
<AppField label="Status">
<AppStatusBadge status={ticket.status} />
</AppField>
<AppField label="Priority">
<AppPriorityBadge priority={ticket.priority} />
</AppField>
<AppField label="System">
<span className="text-sm">{ticket.related_system || '-'}</span>
</AppField>
<AppField label="Environment">
<span className="text-sm">{ticket.environment || '-'}</span>
</AppField>
<AppField label="Impact">
<span className="text-sm">{ticket.impact_scope || '-'}</span>
</AppField>
<AppField label="Description">
<span className="text-sm">{ticket.description}</span>
</AppField>
</CardContent>
</Card>
<TicketStatusTimeline ticket={ticket} />
<TicketComments ticketId={ticket.id} />
</div>
</main>
)
}