"use client"; import Link from "next/link"; import { useEffect, useMemo, useState } from "react"; import defaultFlow from "../data/approval-flow.json"; import styles from "./ApprovalDesigner.module.css"; type NodeType = "start" | "approval" | "condition" | "cc" | "end"; type ApprovalMode = "single" | "any" | "all"; type FlowNode = { id: string; type: NodeType; title: string; description: string; approvers: string[]; mode: ApprovalMode; allowTransfer?: boolean; autoPass?: boolean; branches?: Branch[]; }; type Branch = { id: string; label: string; expression: string; node: FlowNode; }; type FlowData = { id: string; name: string; description: string; status: string; version: string; updatedAt: string; nodes: FlowNode[]; }; const library: Array<{ type: NodeType; label: string; description: string; icon: string; tone: string }> = [ { type: "approval", label: "审批人", description: "指定成员或角色审批", icon: "✓", tone: "blue" }, { type: "condition", label: "条件分支", description: "根据表单字段自动分流", icon: "⑂", tone: "orange" }, { type: "cc", label: "抄送人", description: "将审批结果通知成员", icon: "◎", tone: "purple" }, ]; const people = ["直属主管", "部门负责人", "财务负责人", "预算管理员", "采购专员", "法务负责人"]; export default function ApprovalDesignerPage() { const [flow, setFlow] = useState(defaultFlow as unknown as FlowData); const [selectedId, setSelectedId] = useState("approval-1"); const [zoom, setZoom] = useState(1); const [previewOpen, setPreviewOpen] = useState(false); const [jsonOpen, setJsonOpen] = useState(false); const [paletteOpen, setPaletteOpen] = useState(false); const [toast, setToast] = useState(""); const [saved, setSaved] = useState(true); const [loaded, setLoaded] = useState(false); useEffect(() => { const stored = window.localStorage.getItem("orbit-approval-flow"); if (stored) { try { setFlow(JSON.parse(stored)); } catch { window.localStorage.removeItem("orbit-approval-flow"); } } setLoaded(true); }, []); useEffect(() => { if (!loaded) return; setSaved(false); const timer = window.setTimeout(() => { window.localStorage.setItem("orbit-approval-flow", JSON.stringify(flow)); setSaved(true); }, 500); return () => window.clearTimeout(timer); }, [flow, loaded]); const selectedNode = useMemo(() => flow.nodes.find((node) => node.id === selectedId), [flow.nodes, selectedId]); const notify = (message: string) => { setToast(message); window.setTimeout(() => setToast(""), 2400); }; const updateNode = (patch: Partial) => { setFlow((current) => ({ ...current, nodes: current.nodes.map((node) => node.id === selectedId ? { ...node, ...patch } : node), updatedAt: new Date().toLocaleString("zh-CN", { hour12: false }), })); }; const addNode = (type: NodeType) => { const id = `${type}-${Date.now()}`; const common = { id, type, approvers: [] as string[], mode: "single" as ApprovalMode }; let node: FlowNode; if (type === "approval") node = { ...common, title: "新审批节点", description: "请选择审批人", approvers: ["直属主管"], allowTransfer: true }; else if (type === "cc") node = { ...common, title: "新抄送节点", description: "审批通过后自动抄送", approvers: ["申请人"] }; else node = { ...common, title: "新条件分支", description: "根据表单字段设置条件", branches: [ { id: `${id}-a`, label: "条件 1", expression: "totalAmount >= 1000", node: { ...common, id: `${id}-node-a`, type: "approval", title: "条件审批人", description: "满足条件时执行", approvers: ["部门负责人"] } }, { id: `${id}-b`, label: "其他情况", expression: "otherwise", node: { ...common, id: `${id}-node-b`, type: "cc", title: "默认抄送", description: "未满足条件时执行", approvers: ["申请人"] } }, ], }; setFlow((current) => { const endIndex = current.nodes.findIndex((item) => item.type === "end"); const nodes = [...current.nodes]; nodes.splice(endIndex < 0 ? nodes.length : endIndex, 0, node); return { ...current, nodes, updatedAt: new Date().toLocaleString("zh-CN", { hour12: false }) }; }); setSelectedId(id); setPaletteOpen(false); notify(`已添加${type === "approval" ? "审批" : type === "condition" ? "条件" : "抄送"}节点`); }; const deleteNode = () => { if (!selectedNode || selectedNode.type === "start" || selectedNode.type === "end") return; setFlow((current) => ({ ...current, nodes: current.nodes.filter((node) => node.id !== selectedId) })); setSelectedId("start-1"); notify("节点已删除"); }; const validateFlow = () => { const emptyApprovers = flow.nodes.filter((node) => (node.type === "approval" || node.type === "cc") && node.approvers.length === 0); const invalidBranches = flow.nodes.filter((node) => node.type === "condition" && (!node.branches || node.branches.length < 2)); if (emptyApprovers.length || invalidBranches.length) notify(`发现 ${emptyApprovers.length + invalidBranches.length} 项配置需要完善`); else notify(`流程校验通过,共 ${flow.nodes.length} 个主节点`); }; const publish = () => { window.localStorage.setItem("orbit-approval-flow", JSON.stringify({ ...flow, status: "published" })); setFlow((current) => ({ ...current, status: "published" })); notify("演示流程已发布到本地"); }; const resetFlow = () => { setFlow(defaultFlow as unknown as FlowData); setSelectedId("approval-1"); window.localStorage.removeItem("orbit-approval-flow"); notify("已恢复 JSON 示例流程"); }; return (
setFlow((current) => ({ ...current, name: event.target.value }))} aria-label="流程名称" />{flow.status === "published" ? "已发布" : "草稿"}
{saved ? "✓ 已自动保存到本地" : "正在保存…"} · {flow.version}
event.preventDefault()} onDrop={(event) => { const type = event.dataTransfer.getData("nodeType") as NodeType; if (type) addNode(type); }}>
审批管理/流程设计/{flow.name}
{Math.round(zoom * 100)}%
{flow.nodes.map((node, index) => (
{node.type === "condition" ? ( setSelectedId(node.id)} /> ) : ( setSelectedId(node.id)} /> )} {index < flow.nodes.length - 1 &&
}
))}
{previewOpen && setPreviewOpen(false)} />} {jsonOpen && setJsonOpen(false)} notify={notify} />} {toast &&
{toast}
}
); } function ProcessNode({ node, selected, onSelect }: { node: FlowNode; selected: boolean; onSelect: () => void }) { const meta = { start: { icon: "▶", tone: "green", eyebrow: "流程发起" }, approval: { icon: "✓", tone: "blue", eyebrow: node.mode === "all" ? "会签审批" : node.mode === "any" ? "或签审批" : "单人审批" }, cc: { icon: "◎", tone: "purple", eyebrow: "抄送通知" }, end: { icon: "■", tone: "gray", eyebrow: "流程完成" }, condition: { icon: "⑂", tone: "orange", eyebrow: "条件分支" }, }[node.type]; return ( ); } function ConditionNode({ node, selected, onSelect }: { node: FlowNode; selected: boolean; onSelect: () => void }) { return (
{node.branches?.map((branch, index) => (
{index + 1}{branch.label}{branch.expression}
))}
); } function NodeSettings({ node, updateNode, deleteNode, notify }: { node: FlowNode; updateNode: (patch: Partial) => void; deleteNode: () => void; notify: (message: string) => void }) { const [tab, setTab] = useState("节点设置"); const addPerson = () => { const next = people.find((person) => !node.approvers.includes(person)); if (!next) return notify("可选示例审批人已全部添加"); updateNode({ approvers: [...node.approvers, next] }); }; return ( <>
{node.type === "condition" ? "⑂" : node.type === "cc" ? "◎" : "✓"}{node.title}节点 ID · {node.id}
{node.type !== "start" && node.type !== "end" && }
{["节点设置", "表单权限", "高级"].map((item) => )}
{tab === "节点设置" && <>