177 lines
7.1 KiB
TypeScript
177 lines
7.1 KiB
TypeScript
import * as React from 'react'
|
||
import { AlertCircle, Bot, CheckCircle2, Lightbulb, Send } from 'lucide-react'
|
||
|
||
import { AppField } from '@/components/app/AppField'
|
||
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 (
|
||
<main className="min-h-screen bg-background p-6 text-foreground">
|
||
<div className="mx-auto grid max-w-6xl 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="工单标题">
|
||
<Input
|
||
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="优先级">
|
||
<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="影响范围">
|
||
<Input
|
||
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="关联系统">
|
||
<Input
|
||
placeholder="例如:CRM、财务系统、数据中台"
|
||
value={form.related_system}
|
||
onChange={(event) => updateField('related_system', event.target.value)}
|
||
/>
|
||
</AppField>
|
||
<AppField label="运行环境">
|
||
<Input
|
||
placeholder="例如:生产环境、测试环境、Windows 终端"
|
||
value={form.environment}
|
||
onChange={(event) => updateField('environment', event.target.value)}
|
||
/>
|
||
</AppField>
|
||
</div>
|
||
</AppSection>
|
||
|
||
<AppSection title="问题描述">
|
||
<AppField label="详细描述">
|
||
<Textarea
|
||
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>
|
||
</div>
|
||
</main>
|
||
)
|
||
}
|