feat: add category-driven ticket workflows

This commit is contained in:
hailin
2026-07-03 16:17:32 +08:00
parent 6586889733
commit 7692374067
16 changed files with 777 additions and 87 deletions

View File

@@ -1,5 +1,5 @@
import * as React from 'react'
import { AlertCircle, Bot, CheckCircle2, Lightbulb, Send } from 'lucide-react'
import { AlertCircle, CheckCircle2, Lightbulb, Send } from 'lucide-react'
import { AppField } from '@/components/app/AppField'
import { AppPage } from '@/components/app/AppPage'
@@ -13,17 +13,25 @@ import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectVa
import { Textarea } from '@/components/ui/textarea'
import { createTicket } from './api'
import { TICKET_CATEGORIES } from './ticketCategories'
import type { TicketInput } from './types'
const initialForm: TicketInput = {
title: '',
description: '',
category_code: 'business_issue',
priority: 'medium',
problem_performance: '',
problem_object: '',
submitter_name: '',
submitter_org: '',
impact_scope: '',
environment: '',
related_system: ''
}
const ENVIRONMENT_OPTIONS = ['生产环境', '测试环境', '预发布环境', '本地环境']
export function TicketCreatePage() {
const [form, setForm] = React.useState<TicketInput>(initialForm)
const [error, setError] = React.useState('')
@@ -36,6 +44,10 @@ export function TicketCreatePage() {
async function handleSubmit(event: React.FormEvent) {
event.preventDefault()
setError('')
if (!form.environment || !form.problem_performance || !form.problem_object || !form.submitter_name || !form.submitter_org) {
setError('请完整填写问题表现、问题对象、提出人、提出机构和环境。')
return
}
setSubmitting(true)
try {
const ticket = await createTicket(form)
@@ -61,18 +73,94 @@ export function TicketCreatePage() {
) : null}
<AppSection title="基础信息">
<AppField label="工单标题" htmlFor="ticket-title" required>
<Input
id="ticket-title"
<div className="grid gap-4 md:grid-cols-2">
<AppField label="问题分类" required>
<Select
value={form.category_code}
onValueChange={(value) => updateField('category_code', value as TicketInput['category_code'])}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="请选择问题分类" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{TICKET_CATEGORIES.map((category) => (
<SelectItem key={category.code} value={category.code}>
{category.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</AppField>
<AppField label="工单标题" htmlFor="ticket-title" required>
<Input
id="ticket-title"
required
placeholder="例如:查询客户采集进度"
value={form.title}
onChange={(event) => updateField('title', event.target.value)}
/>
</AppField>
</div>
</AppSection>
<AppSection title="问题信息">
<div className="grid gap-4 md:grid-cols-2">
<AppField label="问题表现" htmlFor="ticket-problem-performance" required>
<Input
id="ticket-problem-performance"
required
placeholder="例如:采集进度长时间未更新"
value={form.problem_performance}
onChange={(event) => updateField('problem_performance', event.target.value)}
/>
</AppField>
<AppField label="问题对象" htmlFor="ticket-problem-object" required>
<Input
id="ticket-problem-object"
required
placeholder="例如:客户名称、账号、机构或任务编号"
value={form.problem_object}
onChange={(event) => updateField('problem_object', event.target.value)}
/>
</AppField>
</div>
<AppField label="问题描述" htmlFor="ticket-description" required>
<Textarea
id="ticket-description"
required
placeholder="例如:生产环境登录后跳转失败"
value={form.title}
onChange={(event) => updateField('title', event.target.value)}
placeholder="请填写问题背景、期望结果、已知线索或需要支持团队确认的内容。"
value={form.description}
onChange={(event) => updateField('description', event.target.value)}
/>
</AppField>
</AppSection>
<AppSection title="影响与优先级">
<AppSection title="提出信息">
<div className="grid gap-4 md:grid-cols-2">
<AppField label="提出人" htmlFor="ticket-submitter-name" required>
<Input
id="ticket-submitter-name"
required
placeholder="请输入提出人"
value={form.submitter_name}
onChange={(event) => updateField('submitter_name', event.target.value)}
/>
</AppField>
<AppField label="提出机构" htmlFor="ticket-submitter-org" required>
<Input
id="ticket-submitter-org"
required
placeholder="请输入提出机构"
value={form.submitter_org}
onChange={(event) => updateField('submitter_org', event.target.value)}
/>
</AppField>
</div>
</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'])}>
@@ -92,15 +180,12 @@ export function TicketCreatePage() {
<AppField label="影响范围" htmlFor="ticket-impact-scope">
<Input
id="ticket-impact-scope"
placeholder="例如:单个用户、某部门、全部用户"
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
@@ -110,34 +195,30 @@ export function TicketCreatePage() {
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 label="环境" required>
<Select value={form.environment} onValueChange={(value) => updateField('environment', value)}>
<SelectTrigger className="w-full">
<SelectValue placeholder="请选择问题环境" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{ENVIRONMENT_OPTIONS.map((environment) => (
<SelectItem key={environment} value={environment}>
{environment}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</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>
<span>请确认分类、对象和描述已足够支持处理人员判断下一步动作。</span>
</div>
<Button type="submit" disabled={submitting}>
<Send data-icon="inline-start" />
@@ -152,25 +233,25 @@ export function TicketCreatePage() {
<div className="grid gap-3 text-sm text-muted-foreground">
<div className="flex gap-2">
<Lightbulb className="mt-0.5 size-4" />
<span>标题建议包含系统、场景和异常现象。</span>
<span>业务、操作类问题可在详情页直接填写解决方案并完成。</span>
</div>
<div className="flex gap-2">
<Lightbulb className="mt-0.5 size-4" />
<span>描述中写清楚复现步骤、报错信息和影响范围。</span>
<span>技术类问题会保留开始处理、提交确认、解决和关闭流程。</span>
</div>
<div className="flex gap-2">
<Lightbulb className="mt-0.5 size-4" />
<span>紧急优先级应只用于阻断生产或大范围影响问题。</span>
<span>数据类问题支持处理后解决,也支持明确后直接解决。</span>
</div>
</div>
</AppSection>
<AppSection
title="AI 建单辅助"
title="AI 解决方案"
action={<Badge variant="ai">即将接入</Badge>}
>
<p className="text-sm text-muted-foreground">
后续阶段将接入建单 Agent,用于补全字段、追问缺失信息和推荐相似问题。
后续阶段将由 AI 识别问题后自动回填,当前不作为人工必填项。
</p>
</AppSection>
</div>