Files
ai-ticket-platform/frontend/src/features/solutions/SolutionCreatePage.tsx
2026-07-27 19:00:35 +08:00

261 lines
10 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 { Save } from 'lucide-react'
import { AppField } from '@/components/app/AppField'
import { AppPage } from '@/components/app/AppPage'
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 { Field, FieldContent, FieldDescription, FieldGroup, FieldLabel, FieldLegend, FieldSet } from '@/components/ui/field'
import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch'
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',
allow_auto_resolve: false,
requires_human_action: false,
requires_external_confirmation: false,
requires_production_change: false
}
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, 'referenced')
window.location.hash = `/tickets/${sourceTicketId}`
return
}
window.location.hash = '/solutions'
} catch (reason) {
setError(reason instanceof Error ? reason.message : '保存解决方案失败,请检查必填信息。')
} finally {
setSubmitting(false)
}
}
return (
<AppPage
maxWidth="6xl"
contentClassName="grid gap-5"
header={<AppPageHeader
title="新建解决方案"
/>}
>
{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="方案标题" htmlFor="solution-title" required>
<Input
id="solution-title"
required
placeholder="例如:认证回调阻塞处理方案"
value={form.title}
onChange={(event) => updateField('title', event.target.value)}
/>
</AppField>
<AppField label="关联系统" htmlFor="solution-related-system">
<Input
id="solution-related-system"
placeholder="例如:CRM、认证中心、支付系统"
value={form.related_system}
onChange={(event) => updateField('related_system', event.target.value)}
/>
</AppField>
<AppField label="环境范围" htmlFor="solution-environment-scope">
<Input
id="solution-environment-scope"
placeholder="例如:生产环境 / Chrome"
value={form.environment_scope}
onChange={(event) => updateField('environment_scope', event.target.value)}
/>
</AppField>
</div>
</AppSection>
<AppSection title="问题特征">
<AppField label="问题特征" htmlFor="solution-problem-signature" required>
<Textarea
id="solution-problem-signature"
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="根因分析" htmlFor="solution-root-cause" required>
<Textarea
id="solution-root-cause"
required
placeholder="说明问题的根因或判断依据。"
value={form.root_cause}
onChange={(event) => updateField('root_cause', event.target.value)}
/>
</AppField>
<AppField label="处理方案" htmlFor="solution-content" required>
<Textarea
id="solution-content"
required
placeholder="写清楚处理步骤、验证方式和回滚注意事项。"
value={form.solution}
onChange={(event) => updateField('solution', event.target.value)}
/>
</AppField>
</div>
</AppSection>
<AppSection title="风险提示">
<div className="grid gap-5">
<AppField label="风险提示" htmlFor="solution-risk-note">
<Textarea
id="solution-risk-note"
placeholder="可选:说明操作风险、影响范围或上线前确认事项。"
value={form.risk_note}
onChange={(event) => updateField('risk_note', event.target.value)}
/>
</AppField>
<FieldSet>
<FieldLegend>自动化边界</FieldLegend>
<FieldGroup>
<PolicySwitch
id="allow-auto-resolve"
label="允许自动解决"
description="仅代表该方案具备自动复用资格,仍需同时满足范围、阈值、权限和全局策略。"
checked={form.allow_auto_resolve}
onCheckedChange={(checked) => updateField('allow_auto_resolve', checked)}
/>
<PolicySwitch
id="requires-human-action"
label="需要人工操作"
description="勾选后该方案只能作为人工建议,不能自动解决。"
checked={form.requires_human_action}
onCheckedChange={(checked) => updateField('requires_human_action', checked)}
/>
<PolicySwitch
id="requires-external-confirmation"
label="需要外部确认"
description="需要客户、第三方或外部系统确认时禁止自动解决。"
checked={form.requires_external_confirmation}
onCheckedChange={(checked) =>
updateField('requires_external_confirmation', checked)}
/>
<PolicySwitch
id="requires-production-change"
label="涉及生产变更"
description="任何生产变更都必须人工处理和确认。"
checked={form.requires_production_change}
onCheckedChange={(checked) =>
updateField('requires_production_change', checked)}
/>
</FieldGroup>
</FieldSet>
</div>
</AppSection>
</div>
<div className="grid content-start gap-4">
<AppSection title="提交">
<div className="grid gap-3">
<p className="text-sm text-muted-foreground">
方案固定保存为草稿,需在知识治理中心提交并由另一名审核人发布。
</p>
{sourceTicketId ? (
<p className="text-sm text-muted-foreground">保存后会关联到当前工单,审核发布后可由处理人正式采纳。</p>
) : null}
<Button type="submit" disabled={submitting}>
<Save data-icon="inline-start" />
{submitting ? '保存中...' : '保存方案'}
</Button>
</div>
</AppSection>
</div>
</form>
</AppPage>
)
}
function PolicySwitch({
id,
label,
description,
checked,
onCheckedChange
}: {
id: string
label: string
description: string
checked: boolean
onCheckedChange: (checked: boolean) => void
}) {
return (
<Field orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor={id}>{label}</FieldLabel>
<FieldDescription>{description}</FieldDescription>
</FieldContent>
<Switch id={id} checked={checked} onCheckedChange={onCheckedChange} />
</Field>
)
}