import { useState, useEffect, useCallback } from 'react'; import { Plus, Edit2, Eye, ToggleLeft, ToggleRight, GitBranch, Users, CheckCircle, ArrowRight, Shield, Trash2, X, ChevronDown, ChevronUp, Save, Play, Copy, Loader2, Star, ArrowUp, ArrowDown, UserPlus, } from 'lucide-react'; import { Card, CardContent } from '@/components/ui/card'; import { workflowApi } from '@/api/workflow'; import { userApi } from '@/api/user'; import { ConfirmDialog } from '@/components/ConfirmDialog'; /* ------------------------------------------------------------------ */ /* Types */ /* ------------------------------------------------------------------ */ interface WorkflowStep { id?: number; workflow_id?: number; step_number: number; name: string; step_type: string; approver_role: string | null; approver_ids: string | null; is_required: boolean; timeout_hours: number; auto_approve_on_timeout: boolean; } interface WorkflowDetail { id: number; name: string; description: string | null; is_default: boolean; is_active: boolean; created_at: string | null; steps: WorkflowStep[]; } interface UserItem { id: number; username: string; display_name: string | null; role: string; } interface StepDraft { name: string; step_type: 'submit' | 'audit' | 'execute'; approver_ids: number[]; } const STEP_TYPE_LABELS: Record = { submit: '提交阶段', serial: '串行审批', parallel: '并行审批', any_one: '任一审批', audit: '审核阶段', execute: '执行阶段', }; const STEP_TYPE_COLORS: Record = { submit: 'bg-blue-500/15 text-blue-400 border-blue-500/30', serial: 'bg-purple-500/15 text-purple-400 border-purple-500/30', parallel: 'bg-orange-500/15 text-orange-400 border-orange-500/30', any_one: 'bg-green-500/15 text-green-400 border-green-500/30', audit: 'bg-purple-500/15 text-purple-400 border-purple-500/30', execute: 'bg-green-500/15 text-green-400 border-green-500/30', }; const STEP_TYPE_ICONS: Record = { submit: Users, serial: Shield, parallel: Users, any_one: CheckCircle, audit: Shield, execute: Play, }; const STEP_TYPES = [ { value: 'submit' as const, label: '提交阶段', desc: '工单提交人' }, { value: 'audit' as const, label: '审核阶段', desc: '审核人员审批' }, { value: 'execute' as const, label: '执行阶段', desc: '执行人员操作' }, ]; /* ------------------------------------------------------------------ */ /* UserDropdown — 下拉多选用户 */ /* ------------------------------------------------------------------ */ interface UserDropdownProps { users: UserItem[]; selectedIds: number[]; label: string; onToggle: (uid: number) => void; onToggleAll: () => void; onClear: () => void; getUserName: (uid: number) => string; } function UserDropdown({ users, selectedIds, label, onToggle, onToggleAll, onClear, getUserName }: UserDropdownProps) { const [open, setOpen] = useState(false); const ids = Array.isArray(selectedIds) ? selectedIds : []; const allSelected = ids.length === users.length && users.length > 0; return (
{/* Trigger */} {/* Selected tags */} {ids.length > 1 && (
{ids.map((uid) => ( {getUserName(uid)} ))}
)} {/* Dropdown */} {open && ( <>
setOpen(false)} />
{/* Select all / Clear */}
{selectedIds.length > 0 && ( )}
{/* User list */} {users.map((u) => { const checked = selectedIds.includes(u.id); return (
onToggle(u.id)} className="flex items-center gap-2.5 px-3 py-2 cursor-pointer hover:bg-[var(--dg-bg-hover)] transition-colors" >
{checked && }
{u.display_name || u.username} {u.role}
); })}
)}
); } /* ------------------------------------------------------------------ */ /* WorkflowEditorModal — 创建/编辑流程弹框 */ /* ------------------------------------------------------------------ */ interface EditorProps { workflow?: WorkflowDetail | null; users: UserItem[]; onClose: () => void; onSaved: () => void; } function WorkflowEditorModal({ workflow, users, onClose, onSaved }: EditorProps) { const isEdit = !!workflow; const [name, setName] = useState(workflow?.name || ''); const [description, setDescription] = useState(workflow?.description || ''); const [steps, setSteps] = useState([]); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(''); // 初始化步骤 useEffect(() => { if (workflow?.steps?.length) { setSteps( workflow.steps.map((s) => ({ name: s.name, step_type: (s.step_type === 'serial' || s.step_type === 'parallel' || s.step_type === 'any_one') ? 'audit' as const : s.step_type as 'submit' | 'audit' | 'execute', approver_ids: Array.isArray(s.approver_ids) ? s.approver_ids : (s.approver_ids ? ((): number[] => { try { const p = JSON.parse(s.approver_ids as string); return Array.isArray(p) ? p : []; } catch { return []; } })() : []), })) ); } else { // 新建默认:提交→执行 setSteps([ { name: '提交阶段', step_type: 'submit', approver_ids: [] }, { name: '执行阶段', step_type: 'execute', approver_ids: [] }, ]); } }, [workflow]); const addStep = (type: 'submit' | 'audit' | 'execute') => { const labels: Record = { submit: '提交阶段', audit: '审核阶段', execute: '执行阶段' }; const count = steps.filter((s) => s.step_type === type).length; const suffix = count > 0 ? ` ${count + 1}` : ''; setSteps([...steps, { name: `${labels[type]}${suffix}`, step_type: type, approver_ids: [] }]); }; const removeStep = (idx: number) => setSteps(steps.filter((_, i) => i !== idx)); const moveStep = (idx: number, dir: -1 | 1) => { const target = idx + dir; if (target < 0 || target >= steps.length) return; const next = [...steps]; [next[idx], next[target]] = [next[target], next[idx]]; setSteps(next); }; const updateStep = (idx: number, patch: Partial) => { setSteps(steps.map((s, i) => (i === idx ? { ...s, ...patch } : s))); }; const toggleUser = (idx: number, uid: number) => { const step = steps[idx]; const has = step.approver_ids.includes(uid); updateStep(idx, { approver_ids: has ? step.approver_ids.filter((id) => id !== uid) : [...step.approver_ids, uid], }); }; const toggleAllUsers = (idx: number) => { const step = steps[idx]; if (step.approver_ids.length === users.length) { updateStep(idx, { approver_ids: [] }); } else { updateStep(idx, { approver_ids: users.map((u) => u.id) }); } }; const handleSubmit = async () => { if (!name.trim()) { setError('请输入流程名称'); return; } if (steps.length === 0) { setError('请至少添加一个步骤'); return; } // 验证非 submit 步骤至少有一个人员 for (let i = 0; i < steps.length; i++) { if (steps[i].step_type !== 'submit' && steps[i].approver_ids.length === 0) { setError(`步骤"${steps[i].name}"需要关联至少一个人员`); return; } } setSubmitting(true); setError(''); try { const payload = { name: name.trim(), description: description.trim() || null, steps: steps.map((s, i) => ({ step_number: i + 1, name: s.name, step_type: s.step_type, approver_ids: s.approver_ids.length > 0 ? JSON.stringify(s.approver_ids) : null, is_required: true, timeout_hours: 24, auto_approve_on_timeout: false, })), }; if (isEdit && workflow) { await workflowApi.update(workflow.id, payload); } else { await workflowApi.create(payload); } onSaved(); } catch (e: unknown) { const msg = (e as { response?: { data?: { detail?: string } } })?.response?.data?.detail || '保存失败'; setError(msg); } finally { setSubmitting(false); } }; const getUserName = (uid: number) => { const u = users.find((u) => u.id === uid); return u ? (u.display_name || u.username) : `#${uid}`; }; const inputCls = 'w-full px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none transition-colors'; return (
e.stopPropagation()} > {/* Header */}

{isEdit ? '编辑审批流程' : '新建审批流程'}

{/* Body */}
{error &&
{error}
}
setName(e.target.value)} placeholder="输入流程名称" />
setDescription(e.target.value)} placeholder="可选" />
{/* Steps */}
{STEP_TYPES.map((t) => ( ))}
{steps.map((step, idx) => { const typeConf = STEP_TYPES.find((t) => t.value === step.step_type)!; const colorCls = STEP_TYPE_COLORS[step.step_type]; const Icon = STEP_TYPE_ICONS[step.step_type] || Shield; return (
{/* Step header */}
updateStep(idx, { name: e.target.value })} placeholder="步骤名称" /> {typeConf.label}
{/* User selection — submit 步骤不需要选人 */} {step.step_type !== 'submit' && (
toggleUser(idx, uid)} onToggleAll={() => toggleAllUsers(idx)} onClear={() => updateStep(idx, { approver_ids: [] })} getUserName={getUserName} />
)}
); })}
{steps.length === 0 && (
请点击上方按钮添加流程步骤
)}
{/* Footer */}
); } /* ------------------------------------------------------------------ */ /* Main Page */ /* ------------------------------------------------------------------ */ export default function WorkflowPage() { const [items, setItems] = useState([]); const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [expandedId, setExpandedId] = useState(null); const [previewWorkflow, setPreviewWorkflow] = useState(null); const [editorWorkflow, setEditorWorkflow] = useState(undefined); const [deleteConfirm, setDeleteConfirm] = useState<{msg: string; onConfirm: () => void} | null>(null); const fetchWorkflows = useCallback(async () => { try { setLoading(true); setError(null); const data = await workflowApi.list(); const normalized = (Array.isArray(data) ? data : []).map((w: any) => ({ ...w, steps: w.steps || [] })); setItems(normalized); } catch (err: unknown) { const msg = (err as { response?: { data?: { detail?: string } } })?.response?.data?.detail || '获取审批流程失败'; setError(msg); } finally { setLoading(false); } }, []); const fetchUsers = useCallback(async () => { try { const data = await userApi.list(); setUsers(Array.isArray(data) ? data : []); } catch {} }, []); useEffect(() => { fetchWorkflows(); fetchUsers(); }, [fetchWorkflows, fetchUsers]); const toggleWorkflow = useCallback(async (wf: WorkflowDetail) => { try { await workflowApi.update(wf.id, { is_active: !wf.is_active }); setItems((prev) => prev.map((w) => (w.id === wf.id ? { ...w, is_active: !w.is_active } : w))); } catch {} }, []); const handleDelete = useCallback(async (id: number) => { setDeleteConfirm({msg: '确认删除该审批流程?', onConfirm: async () => { try { await workflowApi.delete(id); setItems((prev) => prev.filter((w) => w.id !== id)); } catch {} }}); return; }, []); const setDefault = useCallback(async (id: number) => { try { await workflowApi.setDefault(id); setItems((prev) => prev.map((w) => ({ ...w, is_default: w.id === id }))); } catch {} }, []); const handleClone = useCallback(async (id: number) => { try { await workflowApi.clone(id); await fetchWorkflows(); } catch {} }, [fetchWorkflows]); const getUserName = useCallback( (uid: number) => { const u = users.find((u) => u.id === uid); return u ? (u.display_name || u.username) : `#${uid}`; }, [users] ); if (loading) { return (
); } if (error) { return (

{error}

); } return (
{/* Header */}

审批流程配置

配置多级审批流程,支持审核阶段叠加和多人执行

{items.length === 0 ? (

暂无审批流程

) : ( <> {/* Stats */}
{[ { label: '流程总数', value: items.length, icon: GitBranch, color: 'text-blue-400', bg: 'bg-blue-400/10' }, { label: '已启用', value: items.filter((w) => w.is_active).length, icon: CheckCircle, color: 'text-green-400', bg: 'bg-green-400/10' }, { label: '默认流程', value: items.filter((w) => w.is_default).length, icon: Star, color: 'text-yellow-400', bg: 'bg-yellow-400/10' }, { label: '总步骤数', value: items.reduce((acc, w) => acc + (w.steps?.length || 0), 0), icon: Users, color: 'text-purple-400', bg: 'bg-purple-400/10' }, ].map((s, i) => (
{s.value}
{s.label}
))}
{/* Workflow list */}
{items.map((wf) => { const isExpanded = expandedId === wf.id; return (

{wf.name}

{wf.is_default && ( 默认 )} {wf.is_active ? '已启用' : '已禁用'} {(wf.steps || []).length} 个阶段
{wf.description &&

{wf.description}

} {/* Flow visualization */}
{(wf.steps || []).map((step, i) => { const StepIcon = STEP_TYPE_ICONS[step.step_type] || Shield; let uids: number[] = []; try { const parsed = JSON.parse(step.approver_ids); if (Array.isArray(parsed)) uids = parsed; } catch {} const stepsArr = wf.steps || []; return (
{step.name}
{uids.length > 0 && (
{uids.map((uid) => getUserName(uid)).join('、')}
)}
{i < stepsArr.length - 1 && }
); })}
{/* Expanded details */} {isExpanded && (

流程步骤详情

{(wf.steps || []).map((step, si) => { const StepIcon = STEP_TYPE_ICONS[step.step_type] || Shield; const colorClass = STEP_TYPE_COLORS[step.step_type] || STEP_TYPE_COLORS.serial; const uids: number[] = step.approver_ids ? JSON.parse(step.approver_ids) : []; return (
{step.name} {STEP_TYPE_LABELS[step.step_type] || step.step_type}
{uids.length > 0 && (
关联人员: {uids.map((uid) => ( {getUserName(uid)} ))}
)}
); })}
)}
{/* Actions */}
{!wf.is_default && ( )}
); })}
)} {/* Preview Modal */} {previewWorkflow && (
setPreviewWorkflow(null)}>
e.stopPropagation()}>

{previewWorkflow.name}

{previewWorkflow.is_default && ( 默认 )}
{previewWorkflow.description &&

{previewWorkflow.description}

}
{previewWorkflow.steps.map((step, i) => { const StepIcon = STEP_TYPE_ICONS[step.step_type] || Shield; const colorClass = STEP_TYPE_COLORS[step.step_type] || STEP_TYPE_COLORS.serial; const uids: number[] = step.approver_ids ? JSON.parse(step.approver_ids) : []; return (
{step.name} {STEP_TYPE_LABELS[step.step_type] || step.step_type}
{uids.length > 0 && (
关联人员: {uids.map((uid) => ( {getUserName(uid)} ))}
)}
{i < previewWorkflow.steps.length - 1 && }
); })}
)} {/* Editor Modal */} {editorWorkflow !== undefined && ( setEditorWorkflow(undefined)} onSaved={() => { setEditorWorkflow(undefined); fetchWorkflows(); }} /> )} { deleteConfirm?.onConfirm(); }} onCancel={() => setDeleteConfirm(null)} />
); }