Files
ai-ticket-platform/frontend/src/features/solutions/SolutionCreatePage.tsx
2026-07-02 10:11:30 +08:00

200 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 { ArrowLeft, Save } 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 { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Textarea } from '@/components/ui/textarea'
import { getTicket } from '@/features/tickets/api'
import { createSolution, linkSolutionToTicket } from './api'
import type { SolutionInput } from './types'
const initialForm: SolutionInput = {
title: '',
problem_signature: '',
related_system: '',
environment_scope: '',
root_cause: '',
solution: '',
risk_note: '',
status: 'draft'
}
function getSourceTicketId() {
const route = window.location.hash.replace(/^#/, '')
const query = route.includes('?') ? route.slice(route.indexOf('?') + 1) : ''
const ticketId = new URLSearchParams(query).get('ticketId')
return ticketId ? Number(ticketId) : null
}
export function SolutionCreatePage() {
const [form, setForm] = React.useState<SolutionInput>(initialForm)
const [sourceTicketId] = React.useState(getSourceTicketId)
const [error, setError] = React.useState('')
const [submitting, setSubmitting] = React.useState(false)
React.useEffect(() => {
if (!sourceTicketId) return
getTicket(sourceTicketId)
.then((ticket) => {
setForm((current) => ({
...current,
title: ticket.title,
problem_signature: ticket.description,
related_system: ticket.related_system,
environment_scope: ticket.environment
}))
})
.catch(() => undefined)
}, [sourceTicketId])
function updateField<Key extends keyof SolutionInput>(field: Key, value: SolutionInput[Key]) {
setForm((current) => ({ ...current, [field]: value }))
}
async function handleSubmit(event: React.FormEvent) {
event.preventDefault()
setError('')
setSubmitting(true)
try {
const solution = await createSolution(form)
if (sourceTicketId) {
await linkSolutionToTicket(solution.id, sourceTicketId, 'adopted')
window.location.hash = `/tickets/${sourceTicketId}`
return
}
window.location.hash = '/solutions'
} 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="新建解决方案"
actions={
<Button variant="outline" onClick={() => (window.location.hash = sourceTicketId ? `/tickets/${sourceTicketId}` : '/solutions')}>
<ArrowLeft className="size-4" />
返回
</Button>
}
/>
{error ? (
<Alert variant="destructive">
<AlertTitle>保存失败</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
) : null}
<form className="grid gap-4 xl:grid-cols-[minmax(0,1fr)_320px]" onSubmit={handleSubmit}>
<div className="grid gap-4">
<AppSection title="基础信息">
<div className="grid gap-4 md:grid-cols-2">
<AppField label="方案标题">
<Input
required
placeholder="例如:认证回调阻塞处理方案"
value={form.title}
onChange={(event) => updateField('title', event.target.value)}
/>
</AppField>
<AppField label="状态">
<Select value={form.status} onValueChange={(value) => updateField('status', value)}>
<SelectTrigger className="w-full">
<SelectValue placeholder="选择状态" />
</SelectTrigger>
<SelectContent>
<SelectItem value="draft">草稿</SelectItem>
<SelectItem value="published">发布</SelectItem>
</SelectContent>
</Select>
</AppField>
<AppField label="关联系统">
<Input
placeholder="例如:CRM、认证中心、支付系统"
value={form.related_system}
onChange={(event) => updateField('related_system', event.target.value)}
/>
</AppField>
<AppField label="环境范围">
<Input
placeholder="例如:生产环境 / Chrome"
value={form.environment_scope}
onChange={(event) => updateField('environment_scope', event.target.value)}
/>
</AppField>
</div>
</AppSection>
<AppSection title="问题特征">
<AppField label="问题特征">
<Textarea
required
placeholder="描述可复用的问题表现、报错特征、触发条件。"
value={form.problem_signature}
onChange={(event) => updateField('problem_signature', event.target.value)}
/>
</AppField>
</AppSection>
<AppSection title="根因与方案">
<div className="grid gap-4">
<AppField label="根因分析">
<Textarea
required
placeholder="说明问题的根因或判断依据。"
value={form.root_cause}
onChange={(event) => updateField('root_cause', event.target.value)}
/>
</AppField>
<AppField label="处理方案">
<Textarea
required
placeholder="写清楚处理步骤、验证方式和回滚注意事项。"
value={form.solution}
onChange={(event) => updateField('solution', event.target.value)}
/>
</AppField>
</div>
</AppSection>
<AppSection title="风险提示">
<AppField label="风险提示">
<Textarea
placeholder="可选:说明操作风险、影响范围或上线前确认事项。"
value={form.risk_note}
onChange={(event) => updateField('risk_note', event.target.value)}
/>
</AppField>
</AppSection>
</div>
<div className="grid content-start gap-4">
<AppSection title="提交">
<div className="grid gap-3">
{sourceTicketId ? (
<p className="text-sm text-muted-foreground">保存后会自动关联到当前工单,并标记为已采纳方案。</p>
) : null}
<Button type="submit" disabled={submitting}>
<Save className="size-4" />
{submitting ? '保存中...' : '保存方案'}
</Button>
</div>
</AppSection>
</div>
</form>
</div>
</main>
)
}