200 lines
7.5 KiB
TypeScript
200 lines
7.5 KiB
TypeScript
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>
|
||
)
|
||
}
|