feat: add ticket solution closure loop
This commit is contained in:
199
frontend/src/features/solutions/SolutionCreatePage.tsx
Normal file
199
frontend/src/features/solutions/SolutionCreatePage.tsx
Normal file
@@ -0,0 +1,199 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user