Files
ai-ticket-platform/frontend/src/features/tickets/TicketCreatePage.tsx
2026-07-02 15:39:03 +08:00

181 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as React from 'react'
import { AlertCircle, Bot, CheckCircle2, Lightbulb, Send } from 'lucide-react'
import { AppField } from '@/components/app/AppField'
import { AppPage } from '@/components/app/AppPage'
import { AppPageHeader } from '@/components/app/AppPageHeader'
import { AppSection } from '@/components/app/AppSection'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Textarea } from '@/components/ui/textarea'
import { createTicket } from './api'
import type { TicketInput } from './types'
const initialForm: TicketInput = {
title: '',
description: '',
priority: 'medium',
impact_scope: '',
environment: '',
related_system: ''
}
export function TicketCreatePage() {
const [form, setForm] = React.useState<TicketInput>(initialForm)
const [error, setError] = React.useState('')
const [submitting, setSubmitting] = React.useState(false)
function updateField<Key extends keyof TicketInput>(field: Key, value: TicketInput[Key]) {
setForm((current) => ({ ...current, [field]: value }))
}
async function handleSubmit(event: React.FormEvent) {
event.preventDefault()
setError('')
setSubmitting(true)
try {
const ticket = await createTicket(form)
window.location.hash = `/tickets/${ticket.id}`
} catch (reason) {
setError(reason instanceof Error ? reason.message : '创建工单失败,请检查必填信息或稍后重试。')
} finally {
setSubmitting(false)
}
}
return (
<AppPage maxWidth="6xl" contentClassName="grid gap-5">
<AppPageHeader title="创建工单" />
<form className="grid gap-4 xl:grid-cols-[minmax(0,1fr)_340px]" onSubmit={handleSubmit}>
<div className="grid gap-4">
{error ? (
<Alert variant="destructive">
<AlertCircle className="size-4" />
<AlertTitle>创建工单失败</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
) : null}
<AppSection title="基础信息">
<AppField label="工单标题" htmlFor="ticket-title" required>
<Input
id="ticket-title"
required
placeholder="例如:生产环境登录后跳转失败"
value={form.title}
onChange={(event) => updateField('title', event.target.value)}
/>
</AppField>
</AppSection>
<AppSection title="影响与优先级">
<div className="grid gap-4 md:grid-cols-2">
<AppField label="优先级" required>
<Select value={form.priority} onValueChange={(value) => updateField('priority', value as TicketInput['priority'])}>
<SelectTrigger className="w-full">
<SelectValue placeholder="请选择优先级" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="low">低</SelectItem>
<SelectItem value="medium">中</SelectItem>
<SelectItem value="high">高</SelectItem>
<SelectItem value="urgent">紧急</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</AppField>
<AppField label="影响范围" htmlFor="ticket-impact-scope">
<Input
id="ticket-impact-scope"
placeholder="例如:单个用户、某部门、全部用户"
value={form.impact_scope}
onChange={(event) => updateField('impact_scope', event.target.value)}
/>
</AppField>
</div>
</AppSection>
<AppSection title="环境与系统">
<div className="grid gap-4 md:grid-cols-2">
<AppField label="关联系统" htmlFor="ticket-related-system">
<Input
id="ticket-related-system"
placeholder="例如:CRM、财务系统、数据中台"
value={form.related_system}
onChange={(event) => updateField('related_system', event.target.value)}
/>
</AppField>
<AppField label="运行环境" htmlFor="ticket-environment">
<Input
id="ticket-environment"
placeholder="例如:生产环境、测试环境、Windows 终端"
value={form.environment}
onChange={(event) => updateField('environment', event.target.value)}
/>
</AppField>
</div>
</AppSection>
<AppSection title="问题描述">
<AppField label="详细描述" htmlFor="ticket-description" required>
<Textarea
id="ticket-description"
required
placeholder="请描述影响范围、报错信息、操作步骤和已尝试的处理方式。"
value={form.description}
onChange={(event) => updateField('description', event.target.value)}
/>
</AppField>
</AppSection>
<AppSection title="提交确认">
<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 data-icon="inline-start" />
{submitting ? '提交中...' : '提交工单'}
</Button>
</div>
</AppSection>
</div>
<div className="grid content-start gap-4">
<AppSection title="建单提示">
<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 建单辅助"
action={<Badge variant="ai">即将接入</Badge>}
>
<p className="text-sm text-muted-foreground">
后续阶段将接入建单 Agent,用于补全字段、追问缺失信息和推荐相似问题。
</p>
</AppSection>
</div>
</form>
</AppPage>
)
}