'use client' import { useState } from 'react' import { Button } from '@/components/ui' import SelectWithInput from '@/components/ui/SelectWithInput' import { Plus, Trash2 } from 'lucide-react' interface TicketData { id: number assign_time: string | null fault_category: string | null fault_subcategory: string | null responsibility: string | null current_status: string } interface TimelineStep { time_node: string handler: string description: string } interface ProcessFormProps { ticket: TicketData currentUserDisplayName: string onSuccess: () => void onCancel: () => void } const FAULT_CATEGORIES = [ '硬件故障', '软件故障', '网络故障', '存储故障', '电源故障', '无故障', ] const TICKET_TYPES = ['OEM诊断', 'OEM维修'] const FAULT_SUBCATEGORIES = [ 'GPU故障', 'CPU故障', '内存故障', '硬盘故障', '电源故障', '风扇故障', 'OS崩溃', '驱动故障', '软件崩溃', '配置错误', '网络丢包', '网络中断', '存储掉盘', 'RAID故障', '其他', ] const HANDLER_OPTIONS = ['腾讯', '图灵'] function beijingNow(): string { const d = new Date(Date.now() + 8 * 60 * 60 * 1000) return d.toISOString().slice(0, 10) + ' ' + d.toISOString().slice(11, 19) } const TIME_RE = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2})?$/ export default function ProcessForm({ ticket, currentUserDisplayName, onSuccess, onCancel }: ProcessFormProps) { const [loading, setLoading] = useState(false) const [error, setError] = useState('') const [closeTimeError, setCloseTimeError] = useState('') const [stepTimeErrors, setStepTimeErrors] = useState>({}) // 工单类型 const [ticketType, setTicketType] = useState((ticket as any).ticket_type || '') // 故障信息修正(in_progress 时预填已有值) const [faultCategory, setFaultCategory] = useState(ticket.fault_category || '') const [faultSubcategory, setFaultSubcategory] = useState(ticket.fault_subcategory || '') const [responsibility, setResponsibility] = useState(ticket.responsibility || '') // 结单信息 const [closeTime, setCloseTime] = useState(beijingNow()) const [processSummary, setProcessSummary] = useState('') const [conclusion, setConclusion] = useState('') const [partsReplaced, setPartsReplaced] = useState<'是' | '否'>('否') const [partsName, setPartsName] = useState('') // 处理时间线(默认至少一个步骤) const [steps, setSteps] = useState([ { time_node: beijingNow(), handler: '图灵', description: '' }, ]) function addStep() { setSteps(prev => [...prev, { time_node: beijingNow(), handler: '', description: '' }]) } function removeStep(index: number) { setSteps(prev => prev.filter((_, i) => i !== index)) } function updateStep(index: number, field: keyof TimelineStep, value: string) { setSteps(prev => prev.map((s, i) => (i === index ? { ...s, [field]: value } : s))) } async function handleSubmit() { setError('') // 必填项校验 if (!faultCategory) { setError('故障大类不能为空') return } if (!faultSubcategory) { setError('故障小类不能为空') return } if (!closeTime) { setError('结单时间不能为空') return } if (closeTime && ticket.assign_time && closeTime < ticket.assign_time) { setError('结单时间不能早于派单时间') return } if (!processSummary.trim()) { setError('处理结果不能为空') return } if (!conclusion.trim()) { setError('结论不能为空') return } if (partsReplaced === '是' && !partsName.trim()) { setError('选择更换配件后,配件名称不能为空') return } if (closeTime && !TIME_RE.test(closeTime)) { setError('结单时间格式不正确,请使用 YYYY-MM-DD HH:mm:ss 格式') return } const badStep = steps.find(s => s.time_node && !TIME_RE.test(s.time_node)) if (badStep) { setError(`处理步骤"${badStep.description.slice(0, 20)}"的时间节点格式不正确,请使用 YYYY-MM-DD HH:mm:ss 格式`) return } // 至少一个处理步骤,且描述必填 const emptySteps = steps.filter(s => !s.description.trim()) if (emptySteps.length > 0) { setError('每个处理步骤的描述不能为空') return } if (!confirm(`确认提交处理?\n\n工单类型:${ticketType || '无'}\n故障大类:${faultCategory}\n结单时间:${closeTime}\n处理结果:${processSummary.slice(0, 50)}${processSummary.length > 50 ? '...' : ''}\n处理步骤:${steps.length} 步\n\n提交后将标记为"已解决"。`)) { return } setLoading(true) try { const assignTime = ticket.assign_time ? new Date(ticket.assign_time).getTime() : 0 const closeMs = new Date(closeTime).getTime() const durationMinutes = assignTime ? Math.round((closeMs - assignTime) / 60000) : 0 const body: Record = { ticket_type: ticketType || null, fault_category: faultCategory || null, fault_subcategory: faultSubcategory || null, responsibility: responsibility || null, close_time: closeTime, process_summary: processSummary || null, conclusion: conclusion || null, parts_replaced: partsReplaced, parts_name: partsReplaced === '是' ? partsName : null, current_status: 'resolved', duration_minutes: durationMinutes, steps: steps.map(s => ({ time_node: s.time_node || null, handler: s.handler || null, description: s.description || null, })), } const res = await fetch(`/api/tickets/${ticket.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) if (!res.ok) { const data = await res.json() setError(data.error || '提交失败') return } onSuccess() } catch { setError('网络错误') } finally { setLoading(false) } } return (

处理工单

{/* 故障信息修正 */}

工单类型与故障信息

故障大类 *} value={faultCategory} onChange={setFaultCategory} options={FAULT_CATEGORIES} placeholder="请选择或输入..." /> 故障小类 *} value={faultSubcategory} onChange={setFaultSubcategory} options={FAULT_SUBCATEGORIES} placeholder="请选择或输入..." />
{/* 结单信息 */}

结单信息

{ setCloseTime(e.target.value); setCloseTimeError('') }} onBlur={() => { if (closeTime && !TIME_RE.test(closeTime)) { setCloseTimeError('时间格式不正确,请使用 YYYY-MM-DD HH:mm:ss 格式') } }} placeholder="2026-05-02 18:34:36" /> {closeTimeError &&

{closeTimeError}

}