status: proposed created: 2026-06-17 拆分顺序:基线 → 转换层(最高风险,先解决) → 数据/状态 → 画布渲染 → 节点组件 → 配置面板 → 设计器集成 → 查看器 → 入口替换 → 依赖清理 → 验收 原则:后端 Flowable 接口零改动;BPMN XML 双向无损转换;保留全部 BPMN 节点类型(advanced 兜底);UserSelectModal 复用;NodePropertiesPanel 字段 1:1 迁移。
| 阶段 | 目标 | 包含任务 | 交付结果 |
|---|---|---|---|
| Phase 0 | 基线 & 目录 | Task 0 | 目录结构 + 依赖确认 |
| Phase 1 | 转换层 | Task 1-6 | BPMN↔JSON 双向转换 + 自动布局 + 单测 |
| Phase 2 | 数据与状态 | Task 7-8 | flowJson Composable + 撤销/重做 |
| Phase 3 | 画布渲染 | Task 9-12 | FlowCanvas + 布局 + SVG 连线 + 添加菜单 |
| Phase 4 | 节点组件 | Task 13-17 | 12 种节点卡片 + 操作菜单 |
| Phase 5 | 配置面板 | Task 18-25 | 抽屉容器 + 8 类配置组件 |
| Phase 6 | 设计器集成 | Task 26-27 | DingFlowDesigner + design.vue 接入 |
| Phase 7 | 查看器 | Task 28-29 | DingFlowViewer + 状态/详情 |
| Phase 8 | 入口替换 | Task 30-31 | todo/done/started/monitor 等页替换 |
| Phase 9 | 清理与验收 | Task 32-34 | 移除 bpmn-js + 构建 + 冒烟 |
| Task | 阶段 | 名称 | 状态 | 优先级 |
|---|---|---|---|---|
| Task 0 | Phase 0 | 目录骨架与依赖确认 | done | P0 |
| Task 1 | Phase 1 | XML 解析工具与节点类型识别 | done | P0 |
| Task 2 | Phase 1 | BPMN→JSON 转换器(基础节点) | done | P0 |
| Task 3 | Phase 1 | BPMN→JSON UserTask 完整属性提取 | done | P0 |
| Task 4 | Phase 1 | BPMN→JSON 网关分支与汇合识别 | done | P0 |
| Task 5 | Phase 1 | JSON→BPMN 转换器 + 自动布局 | done | P0 |
| Task 6 | Phase 1 | 转换层端到端往返测试 | done* | P0 |
| Task 7 | Phase 2 | useFlowDesigner Composable | done | P0 |
| Task 8 | Phase 2 | useFlowHistory(撤销/重做) | done | P1 |
| Task 9 | Phase 3 | FlowCanvas 容器(缩放/平移) | done | P0 |
| Task 10 | Phase 3 | 布局算法(纵向 + 分支横向) | done | P0 |
| Task 11 | Phase 3 | SVG 连线层(贝塞尔曲线) | done | P0 |
| Task 12 | Phase 3 | 节点间"+"号添加菜单 | done | P0 |
| Task 13 | Phase 4 | StartNode / EndNode 卡片 | done | P0 |
| Task 14 | Phase 4 | ApproverNode / CarbonCopyNode 卡片 | done | P0 |
| Task 15 | Phase 4 | Condition/Parallel/Inclusive 分支卡片 | done | P0 |
| Task 16 | Phase 4 | Service/Script/SubProcess/CallActivity 卡片 | done | P1 |
| Task 17 | Phase 4 | AdvancedNode + 节点右键菜单 | done | P0 |
| Task 18 | Phase 5 | NodeConfigDrawer 抽屉 + Tab 路由 | done | P0 |
| Task 19 | Phase 5 | ApproverConfig(基本+权限) | done | P0 |
| Task 20 | Phase 5 | MultiInstanceConfig(会签) | done | P0 |
| Task 21 | Phase 5 | FormPermissionConfig(表单权限) | done | P0 |
| Task 22 | Phase 5 | ListenerConfig(监听器) | done | P1 |
| Task 23 | Phase 5 | ConditionConfig(条件分支) | done | P0 |
| Task 24 | Phase 5 | Service/Script/Start/End Config | done | P1 |
| Task 25 | Phase 5 | AdvancedConfig(rawXml 只读) | done | P1 |
| Task 26 | Phase 6 | DingFlowDesigner 主组件 | done | P0 |
| Task 27 | Phase 6 | design.vue 数据流改造 | done | P0 |
| Task 28 | Phase 7 | DingFlowViewer 查看器 | done | P0 |
| Task 29 | Phase 7 | 节点状态高亮 + Popover | done | P0 |
| Task 30 | Phase 8 | todo/done/started/monitor 替换 | done | P0 |
| Task 31 | Phase 8 | 版本对比/历史查看入口替换 | done | P1 |
| Task 32 | Phase 9 | 移除 bpmn-js 依赖与旧组件 | done | P0 |
| Task 33 | Phase 9 | 前端构建 + Lint | done | P0 |
| Task 34 | Phase 9 | 端到端冒烟 | done | P0 |
目标: 创建组件目录结构,确认现有依赖(NaiveUI/UnoCSS/Vue3)足够,不引入新依赖。
实际产物: 9 个 index.js 占位文件(含模块说明 JSDoc),无新增依赖,无修改/删除既有文件。详见 execution-log.md。
涉及文件:
forge-admin-ui/src/components/flow-designer/canvas/index.js — 新建空导出forge-admin-ui/src/components/flow-designer/nodes/index.js — 新建空导出forge-admin-ui/src/components/flow-designer/panel/index.js — 新建空导出forge-admin-ui/src/components/flow-designer/viewer/index.js — 新建空导出forge-admin-ui/src/components/flow-designer/converter/index.js — 新建空导出forge-admin-ui/src/components/flow-designer/composables/index.js — 新建空导出forge-admin-ui/src/components/flow-designer/constants/index.js — 新建空导出forge-admin-ui/src/components/flow-designer/utils/index.js — 新建空导出forge-admin-ui/src/components/flow-designer/index.js — 新建,最终导出 DingFlowDesigner / DingFlowViewer验收标准:
index.jspackage.json 不新增依赖src/components/bpmn/ 不动(待 Phase 9 删除)转换层为最高技术风险区域,所有任务必须配 Vitest 单测,先写测试再写实现(TDD)。
目标: 封装基于 DOMParser 的 XML 工具与节点类型映射常量。
实际产物:
converter/xml-utils.js — DOMParser 封装、命名空间常量、flowable 属性三路径解析、扩展元素查找、documentation 读取constants/node-types.js — NODE_TYPE 12 枚举 + bpmnTypeToNodeType() + 反向映射 NODE_TYPE_TO_BPMN_LOCAL_NAMEconverter/__tests__/xml-utils.spec.js + xml-utils-flowable.spec.js + constants/__tests__/node-types.spec.js:39 个单测全部通过vitest@2.1.9 + @vitest/ui + jsdom,新增 vitest.config.js 与 npm scripts(test / test:watch / test:ui)单测命令: NODE_ENV=development pnpm vitest run src/components/flow-designer → 3 文件 / 39 用例通过
涉及文件:
forge-admin-ui/src/components/flow-designer/converter/xml-utils.js — 新建forge-admin-ui/src/components/flow-designer/constants/node-types.js — 新建forge-admin-ui/src/components/flow-designer/converter/__tests__/xml-utils.spec.js — 新建关键签名:
// xml-utils.js
export function parseBpmnXml(xmlString) // → Document
export function getRootProcess(doc) // → Element
export function getChildren(el, tagName) // → Element[]
export function getAttr(el, name) // → string|null
export function getFlowableAttr(el, name) // → string|null
export function getExtensionElement(el, tagName)// → Element|null
export function serializeXml(doc) // → string
// node-types.js
export const NODE_TYPE = {
START: 'start', APPROVER: 'approver', CARBON_COPY: 'carbonCopy',
CONDITION: 'condition', PARALLEL: 'parallel', INCLUSIVE: 'inclusive',
SERVICE: 'service', SCRIPT: 'script', SUB_PROCESS: 'subProcess',
CALL_ACTIVITY: 'callActivity', END: 'end', ADVANCED: 'advanced'
}
export function bpmnTypeToNodeType(bpmnElement) // → NODE_TYPE
节点类型识别规则:
验收标准:
目标: 实现 BPMN XML → flowJson 转换骨架,支持线性流程,UserTask/网关详细解析在 Task 3、4 实现。
实际产物:
converter/bpmn-to-json.js — 主入口 convertBpmnToJson + parseNode + parseEdge + buildAdvancedNode(advanced 兜底)关键设计: NON_NODE_TAGS 排除 sequenceFlow / extensionElements 等结构性元素;XMLSerializer 失败时回退 outerHTML。
涉及文件:
forge-admin-ui/src/components/flow-designer/converter/bpmn-to-json.js — 新建forge-admin-ui/src/components/flow-designer/converter/__tests__/bpmn-to-json.spec.js — 新建关键签名:
export function convertBpmnToJson(xmlString) {
// → { processId, processName, nodes: [], edges: [] }
}
function parseNode(element) // → flowNode
function parseEdge(sequenceFlow) // → flowEdge
function buildAdvancedNode(element)// → flowNode (rawXml 兜底)
flowJson 结构:
{
processId: 'Process_1',
processName: '请假流程',
nodes: [
{ id, nodeType, name, bpmnElementId, bpmnElementType, rawXml: null, config: {} }
],
edges: [
{ id, source, target, bpmnElementId, conditionType, condition, isDefault, branchId }
]
}
本任务实现的节点类型属性:
验收标准:
目标: 1:1 复刻 NodePropertiesPanel.vue:1562-1640 的 UserTask 属性提取逻辑。
实际产物:
converter/user-task-parser.js — parseUserTaskConfig() + parseCompletionExpression()字段对齐: 与现有 NodePropertiesPanel.properties 同名同语义;唯一差异是 spec 用 'ratio' 替换现有 'rate'(按 spec 优先级处理)。
涉及文件:
forge-admin-ui/src/components/flow-designer/converter/user-task-parser.js — 新建forge-admin-ui/src/components/flow-designer/converter/bpmn-to-json.js — 修改:UserTask 调用 parseUserTaskConfigforge-admin-ui/src/components/flow-designer/converter/__tests__/user-task-parser.spec.js — 新建关键签名:
export function parseUserTaskConfig(taskElement) {
// → {
// taskType, assignee, assigneeExpr, candidateUsers, candidateGroups,
// assigneeUserName, candidateUserNames, candidateGroupNames,
// priority, dueDate, formType, formKey, formJson, formUrl,
// allowApprove, allowReject, allowDelegate, allowReturn, allowTerminate,
// requireSignature, requireComment,
// multiInstanceType, completionCondition, passRate,
// taskListeners: [], executionListeners: [],
// formFieldPermissions: []
// }
}
assignee 解析规则:
flowable:assigneeType:
flowable:assignee 值flowable:assignee:
${initiator} / ${initiatorLeader} / ${deptManager} / ${hr} → taskType='assignee', assignee=该静态值${user_xxx} 或其他 → taskType='assignee', assignee='custom'flowable:candidateUsers(逗号分隔字符串) → taskType='candidateUsers', candidateUsers=数组flowable:candidateGroups(逗号分隔字符串) → taskType='candidateGroups', candidateGroups=数组multiInstance 解析:
bpmn:multiInstanceLoopCharacteristics:
bpmn:completionCondition 文本:
${nrOfCompletedInstances/nrOfInstances == 1} → passRate=100, condition='all'${nrOfCompletedInstances >= 1} → passRate=null, condition='any'${nrOfCompletedInstances/nrOfInstances >= 0.N} → passRate=N*100, condition='ratio'listener 解析:
extensionElements > flowable:taskListener:{event, type, value}[],type ∈ class|expression|delegateExpressionextensionElements > flowable:executionListener:同上操作权限:
flowable:allowApprove / allowReject / allowDelegate / allowReturn / allowTerminate / requireSignature / requireComment:字符串 'true'/'false' → boolean验收标准:
目标: 解析 sequenceFlow 上的 conditionExpression 与 default,给分支分配 branchId,识别多入度汇合节点。
实际产物:
converter/branch-parser.js — markBranches() + getNodeInDegree + getNodeOutDegreebranch-parser.spec.js / 7 用例:排他/并行/嵌套分支 + branchId 唯一 + mergeNode 标记 + in/outDegree 正确实现细节: branchSeq 单调递增分配 b1/b2/...;网关 default 属性匹配的 sequenceFlow 自动 isDefault=true 并清空 condition;入度 ≥ 2 的节点 mark 到 node.config.mergeNode=true。
涉及文件:
forge-admin-ui/src/components/flow-designer/converter/branch-parser.js — 新建forge-admin-ui/src/components/flow-designer/converter/bpmn-to-json.js — 修改:转换完成后调用 markBranchesforge-admin-ui/src/components/flow-designer/converter/__tests__/branch-parser.spec.js — 新建关键签名:
export function markBranches(flowJson) {
// 1. 找出所有网关节点(exclusive/parallel/inclusive)
// 2. 网关出边按 XML 顺序分配 branchId(b1, b2, ...)
// 3. 标识 default 边:网关 default 属性引用的 sequenceFlow → edge.isDefault=true
// 4. 提取 conditionExpression 文本(去除 ${} 包裹)→ edge.condition
// 含 'js'/'groovy' 等 language 属性 → edge.conditionType='script', 否则 'expression'
// 5. 计算每个 node 的入度,>=2 标记为 mergeNodes
// 返回 flowJson(mutate)
}
export function getNodeInDegree(nodes, edges) // → Map<nodeId, number>
export function getNodeOutDegree(nodes, edges) // → Map<nodeId, number>
验收标准:
目标: 将 flowJson 序列化为合法的 BPMN 2.0 XML,包含 process 元素 + extensionElements + BPMNDiagram 图形坐标。
实际产物:
converter/json-to-bpmn.js — convertJsonToBpmn() 主入口 + writeNode / writeEdge / writeDiagramconverter/user-task-writer.js — UserTask config → flowable:* 属性 + 子元素串converter/layout-algorithm.js — calculateLayout():纵向 + 分支横向并排 + 汇合回中线 + 孤立节点放右侧converter/completion-condition.js — buildCompletionExpression()(与 parseCompletionExpression 对偶)converter/xml-escape.js — 属性 / 文本节点转义工具关键决策:
涉及文件:
forge-admin-ui/src/components/flow-designer/converter/json-to-bpmn.js — 新建forge-admin-ui/src/components/flow-designer/converter/user-task-writer.js — 新建(UserTask 属性写回)forge-admin-ui/src/components/flow-designer/converter/layout-algorithm.js — 新建(生成 BPMNShape/BPMNEdge 坐标)forge-admin-ui/src/components/flow-designer/converter/__tests__/json-to-bpmn.spec.js — 新建forge-admin-ui/src/components/flow-designer/converter/__tests__/layout-algorithm.spec.js — 新建关键签名:
// json-to-bpmn.js
export function convertJsonToBpmn(flowJson) {
// → BPMN 2.0 XML 字符串,含 <bpmn:definitions> + <bpmn:process> + <bpmndi:BPMNDiagram>
}
function writeNode(node) // → XML 片段
function writeEdge(edge) // → XML 片段
function writeAdvancedNode(node) // → 直接拼接 node.rawXml
function writeUserTask(node) // → 调用 writeUserTaskConfig
// user-task-writer.js
export function writeUserTaskConfig(node)
// 按规则将 node.config 写回为 XML 属性 + 子元素
// layout-algorithm.js
export function calculateLayout(flowJson) {
// → { nodePositions: Map<nodeId, {x,y}>, edgeWaypoints: Map<edgeId, [{x,y}...]> }
}
UserTask 写回规则(与 Task 3 解析对偶):
flowable:assignee="${user_xxx}" (从 candidateUsers[0] 拼)flowable:assignee=assigneeExpr, flowable:assigneeType="spel"flowable:assignee="${value}"flowable:candidateUsers="id1,id2"flowable:candidateGroups="roleId1,roleId2"<multiInstanceLoopCharacteristics isSequential="..."> + completionCondition:
${nrOfCompletedInstances/nrOfInstances == 1}${nrOfCompletedInstances >= 1}${nrOfCompletedInstances/nrOfInstances >= 0.N}<extensionElements><flowable:taskListener event="..." class|expression|delegateExpression="..."/>flowable:allowApprove="true" 等属性<extensionElements><flowable:formProperty> 子元素(兼容现有格式)布局算法(layout-algorithm.js):
常量: NODE_WIDTH=180, NODE_HEIGHT=70, V_GAP=60, H_GAP=40
算法(递归):
1. 从 start 节点开始,y=0,x=0(中线)
2. 线性链:next.y = current.y + NODE_HEIGHT + V_GAP, next.x = current.x
3. 网关节点:
a. 计算每个分支的子树宽度:treeWidth(branch) = max(branchWidth, NODE_WIDTH)
b. 总宽 totalWidth = sum(branchWidths) + (n-1)*H_GAP
c. 各分支起始 x = current.x - totalWidth/2 + offset
d. 递归布局每个分支
e. 汇合节点 x 回到 current.x
4. edge.waypoints:source 底边中点 → target 顶边中点(含拐点)
5. 处理 advanced 节点:放在最末尾区域(避免破坏布局)
6. 处理孤立节点(无连接):放在右侧边缘
验收标准:
目标: 用真实 BPMN XML 做往返一致性验证,确保关键字段无损。
实际产物:
converter/__tests__/roundtrip.spec.js / 4 用例覆盖:
注: 标记 done* 因为暂未引入完整 leave/expense/complex BPMN fixture 文件;现有 4 个 inline fixture 已覆盖关键往返场景。后续若 Phase 6 集成测试发现真实流程模型有遗漏字段,再增补 fixtures/*.bpmn。
涉及文件:
forge-admin-ui/src/components/flow-designer/converter/__tests__/roundtrip.spec.js — 新建forge-admin-ui/src/components/flow-designer/converter/__tests__/fixtures/leave-process.bpmn — 新建(请假流程,含会签/分支)forge-admin-ui/src/components/flow-designer/converter/__tests__/fixtures/expense-process.bpmn — 新建(报销流程,含包容/抄送)forge-admin-ui/src/components/flow-designer/converter/__tests__/fixtures/complex-process.bpmn — 新建(含子流程/调用活动/服务任务)forge-admin-ui/src/components/flow-designer/converter/__tests__/fixtures/edge-cases.bpmn — 新建(含 BoundaryEvent/IntermediateEvent,验证 advanced 兜底)测试矩阵: | Fixture | 验证点 | |---|---| | leave-process | 起止 + 审批人 + 并行会签(passRate=100) + 排他分支 | | expense-process | 包容网关 + 抄送 + 多 listener | | complex-process | 子流程 + 调用活动 + 服务任务 + 脚本任务 | | edge-cases | 边界事件/中间事件落入 advanced,rawXml 保留完整 |
断言策略:
xml1 → json → xml2,对 xml1 与 xml2 进行结构化比对(忽略空白/注释/属性顺序)验收标准:
cd forge-admin-ui && pnpm vitest run src/components/flow-designer/converter目标: 提供 flowJson 状态容器与 CRUD 操作 API,所有节点/连线变更走该 Composable。
实际产物:
composables/useFlowDesigner.js — flowJson reactive + 节点/边 CRUD + 查询 + 整体加载导出composables/flow-designer-helpers.js — deletePlain(笛卡儿重连)+ deleteGateway(递归删除分支链)utils/id-generator.js — createIdGenerator() + collectExistingIds()constants/default-configs.js — 12 种 nodeType 默认 config 工厂 + buildNode 节点骨架关键算法:
涉及文件:
forge-admin-ui/src/components/flow-designer/composables/useFlowDesigner.js — 新建forge-admin-ui/src/components/flow-designer/utils/id-generator.js — 新建(生成 nodeId/edgeId/branchId)forge-admin-ui/src/components/flow-designer/constants/default-configs.js — 新建(每种节点类型默认 config)forge-admin-ui/src/components/flow-designer/composables/__tests__/useFlowDesigner.spec.js — 新建关键签名:
export function useFlowDesigner(initialJson) {
const flowJson = ref(initialJson || createEmptyFlow())
const selectedNodeId = ref(null)
// 节点操作
function addNode(afterNodeId, nodeType, config = {}) // 在指定节点后插入
function addBranchNode(gatewayId, branchType) // 给网关添加分支
function updateNode(nodeId, patch) // 浅合并 config
function deleteNode(nodeId) // 删除节点 + 连接边(自动重连前后)
function moveNodeUp(nodeId) / moveNodeDown(nodeId) // 同分支内调整顺序
function copyNode(nodeId) // 复制节点(不含子分支)
// 连线操作
function updateEdge(edgeId, patch)
function reconnect(edgeId, newSource, newTarget)
// 查询
function getNode(nodeId) / getEdge(edgeId)
function getOutgoingEdges(nodeId) / getIncomingEdges(nodeId)
function findEndNode() / findStartNode()
// 全量
function loadJson(json)
function exportJson() // → 深拷贝
function reset()
return { flowJson, selectedNodeId, addNode, ... }
}
export function createEmptyFlow() {
// → 默认 start → end 两节点 + 一条 edge
}
addNode 算法:
deleteNode 算法:
验收标准:
目标: 基于 JSON 快照的命令栈,支持 Ctrl+Z / Ctrl+Y。
实际产物:
composables/useFlowHistory.js — undo/redo 命令栈 + maxStack 限制 + Ctrl/Cmd+Z/Y/Shift+Z 键盘绑定composables/__tests__/useFlowHistory.spec.js — 13 用例覆盖快照 / 撤销 / 重做 / maxStack / 键盘绑定 / unbind / 深拷贝隔离特性:
涉及文件:
forge-admin-ui/src/components/flow-designer/composables/useFlowHistory.js — 新建forge-admin-ui/src/components/flow-designer/composables/__tests__/useFlowHistory.spec.js — 新建关键签名:
export function useFlowHistory(flowJsonRef, options = { maxStack: 50 }) {
const undoStack = ref([])
const redoStack = ref([])
function snapshot() // 推入 undoStack(深拷贝 JSON),清空 redoStack
function undo() // pop undoStack 推入 redoStack,恢复 flowJsonRef
function redo() // pop redoStack 推入 undoStack,恢复 flowJsonRef
function canUndo() // → boolean
function canRedo() // → boolean
function clear()
function bindKeyboard(el) // 绑定 Ctrl+Z / Ctrl+Y 监听,返回 unbind 函数
return { snapshot, undo, redo, canUndo, canRedo, clear, bindKeyboard }
}
调用约定:
验收标准:
目标: 提供画布容器组件,支持 transform 缩放与平移,鼠标滚轮 zoom,按住空格拖拽 pan。
实际产物:
composables/useCanvasViewport.js — scale/translate reactive + zoomIn/Out/锚点缩放/pan/fitToScreen/坐标转换canvas/FlowCanvas.vue — 画布容器 + Ctrl/Cmd 滚轮 + 中键/空格拖拽 + 双击 reset + 浮动工具栏注: 鼠标 / 空格 / 中键拖拽行为在 jsdom 下无法稳定触发,留 Phase 9 端到端冒烟覆盖。
涉及文件:
forge-admin-ui/src/components/flow-designer/canvas/FlowCanvas.vue — 新建forge-admin-ui/src/components/flow-designer/composables/useCanvasViewport.js — 新建(缩放/平移状态)关键签名:
<!-- FlowCanvas.vue -->
<template>
<div class="flow-canvas" ref="containerRef"
@wheel="handleWheel" @mousedown="handlePan" @contextmenu.prevent>
<div class="canvas-transform" :style="transformStyle">
<slot name="edges" /> <!-- SVG 连线层 -->
<slot name="nodes" /> <!-- HTML 节点层 -->
</div>
<div class="canvas-toolbar"><!-- 缩放工具栏 --></div>
</div>
</template>
<script setup>
const props = defineProps({
minScale: { type: Number, default: 0.3 },
maxScale: { type: Number, default: 2.0 }
})
defineExpose({ resetView, fitToScreen, zoomIn, zoomOut, screenToCanvas, canvasToScreen })
</script>
useCanvasViewport.js:
export function useCanvasViewport(options) {
const scale = ref(1)
const translateX = ref(0)
const translateY = ref(0)
const transformStyle = computed(() => `transform: translate(${translateX.value}px, ${translateY.value}px) scale(${scale.value})`)
function zoomIn() / zoomOut() / setScale(s, centerX, centerY)
function pan(dx, dy)
function resetView()
function fitToScreen(contentWidth, contentHeight, viewportWidth, viewportHeight)
function screenToCanvas(x, y) // → 画布坐标
function canvasToScreen(x, y) // → 屏幕坐标
}
交互规则:
验收标准:
目标: 实现钉钉风格的纵向布局,纯 JS 计算每个节点的 (x,y)。
实际产物:
canvas/layout-engine.js — 复用 converter/layout-algorithm.js 计算节点位置 + 边 polyline,再做画布特化(边类型识别 + canvasBounds)canvas/__tests__/layout-engine.spec.js / 8 用例复用决策: 转换层与画布层共享几何计算,避免重复实现;BPMNDiagram 与画布坐标一致。
涉及文件:
forge-admin-ui/src/components/flow-designer/canvas/layout-engine.js — 新建(与 Task 5 layout-algorithm.js 不同:本任务用于画布渲染坐标,含连线 polyline 计算)forge-admin-ui/src/components/flow-designer/canvas/__tests__/layout-engine.spec.js — 新建关键签名:
export function layoutFlow(flowJson, options = {}) {
// → {
// nodePositions: Map<nodeId, {x, y, width, height}>,
// edgePaths: Map<edgeId, {points: [{x,y}...], type: 'straight'|'bezier'|'orthogonal'}>,
// canvasBounds: {minX, minY, maxX, maxY}
// }
}
const DEFAULT_OPTIONS = {
NODE_WIDTH: 220, NODE_HEIGHT: 80,
V_GAP: 50, // 上下节点间距
H_GAP: 40, // 分支间水平间距
BRANCH_HEAD_GAP: 30, // 网关到分支首节点距离
BRANCH_MERGE_GAP: 30 // 分支末节点到汇合节点距离
}
算法(递归遍历):
function layout(currentNodeId, x, y, ctx):
if visited[currentNodeId]: return
visited[currentNodeId] = true
ctx.positions[currentNodeId] = {x, y}
outgoing = edges where source = currentNodeId
if outgoing.length == 0: return // 终点
if outgoing.length == 1:
// 线性
next = outgoing[0].target
layout(next, x, y + NODE_HEIGHT + V_GAP, ctx)
return
// 分支节点
branchWidths = outgoing.map(e => measureBranch(e.target, edges))
totalWidth = sum(branchWidths) + (n-1) * H_GAP
startX = x + NODE_WIDTH/2 - totalWidth/2
for i, edge in outgoing:
branchX = startX + sum(branchWidths[0..i]) + i * H_GAP + branchWidths[i]/2 - NODE_WIDTH/2
layout(edge.target, branchX, y + NODE_HEIGHT + BRANCH_HEAD_GAP, ctx)
// 找到汇合节点(branches 第一个共同后继)
mergeNodeId = findMerge(outgoing.map(e => e.target), edges)
if mergeNodeId:
mergeY = max(branchEndY) + V_GAP
ctx.positions[mergeNodeId] = {x, y: mergeY}
function measureBranch(headId, edges) → number // 递归计算分支总宽
连线 polyline 规则:
验收标准:
目标: 渲染所有 edges 为 SVG path,支持平滑曲线、箭头、条件标签、状态色。
实际产物:
utils/path-builder.js — 4 种 path 类型(straight/orthogonal/rounded/bezier)+ 中点计算 + 状态颜色 / 虚线canvas/EdgePath.vue — 单条边 + 状态箭头 + 条件标签(截断 20 字符)canvas/EdgeLayer.vue — SVG 容器 + 5 种箭头 marker涉及文件:
forge-admin-ui/src/components/flow-designer/canvas/EdgeLayer.vue — 新建forge-admin-ui/src/components/flow-designer/canvas/EdgePath.vue — 新建(单条边)forge-admin-ui/src/components/flow-designer/utils/path-builder.js — 新建关键签名:
<!-- EdgeLayer.vue -->
<template>
<svg class="edge-layer" :width="canvasWidth" :height="canvasHeight">
<defs>
<marker id="arrow-default" .../>
<marker id="arrow-completed" .../>
<marker id="arrow-rejected" .../>
</defs>
<EdgePath v-for="edge in edges" :key="edge.id" :edge="edge" :path="paths.get(edge.id)" :status="getStatus(edge)" />
</svg>
</template>
<script setup>
defineProps({ edges: Array, paths: Map, nodeStatuses: Object })
</script>
<!-- EdgePath.vue -->
<template>
<g>
<path :d="pathD" :stroke="strokeColor" :stroke-dasharray="dashArray" marker-end="url(#arrow-default)" />
<text v-if="edge.condition" :x="midX" :y="midY" class="edge-label">{{ shortLabel }}</text>
</g>
</template>
path-builder.js:
export function buildPathD(points, type = 'orthogonal') {
// type='straight': M x1,y1 L x2,y2
// type='bezier': M x1,y1 C cx1,cy1 cx2,cy2 x2,y2
// type='orthogonal': M x1,y1 L midX,y1 L midX,y2 L x2,y2 (折线)
// type='rounded': 折线 + 圆角(Q 控制点)
}
export function getEdgeMidpoint(points) // → {x, y}
export function getEdgeColor(edge, status) // → hex color
状态着色:
#94a3b8#10b981 实线#3b82f6 实线 + 脉冲#cbd5e1 虚线#ef4444 虚线#f59e0b + label "默认"验收标准:
目标: 在每个节点下方/分支首端显示"+"按钮,点击弹出节点类型选择菜单。
实际产物:
constants/node-menu.js — NODE_MENU_GROUPS 3 分组 9 类型(advanced 不在添加菜单,仅作为加载兼容兜底)canvas/AddNodePopover.vue — 节点类型选择面板 + allowTypes 过滤canvas/AddNodeButton.vue — 画布上 "+" 按钮 + 弹出 Popover + click outsidecanvas/__tests__/AddNodeButton.spec.js / 7 用例实现细节: click outside 用 mousedown capture 阶段 + closest('.add-node-button-wrap') 判定,不依赖 Naive Popover。
涉及文件:
forge-admin-ui/src/components/flow-designer/canvas/AddNodeButton.vue — 新建forge-admin-ui/src/components/flow-designer/canvas/AddNodePopover.vue — 新建forge-admin-ui/src/components/flow-designer/constants/node-menu.js — 新建关键签名:
<!-- AddNodeButton.vue -->
<NPopover trigger="click" placement="bottom">
<template #trigger><button class="add-node-btn"><i class="i-mdi-plus" /></button></template>
<AddNodePopover @select="handleSelect" :allow-types="allowTypes" />
</NPopover>
node-menu.js(菜单分组):
export const NODE_MENU_GROUPS = [
{ label: '审批流', items: [
{ type: 'approver', label: '审批人', icon: 'i-mdi-account-check' },
{ type: 'carbonCopy', label: '抄送人', icon: 'i-mdi-email' }
]},
{ label: '分支', items: [
{ type: 'condition', label: '条件分支', icon: 'i-mdi-source-branch' },
{ type: 'parallel', label: '并行分支', icon: 'i-mdi-call-split' },
{ type: 'inclusive', label: '包容分支', icon: 'i-mdi-set-merge' }
]},
{ label: '高级', items: [
{ type: 'service', label: '服务任务', icon: 'i-mdi-cog' },
{ type: 'script', label: '脚本任务', icon: 'i-mdi-code-tags' },
{ type: 'subProcess', label: '子流程', icon: 'i-mdi-sitemap' },
{ type: 'callActivity', label: '调用活动', icon: 'i-mdi-phone-forward' }
]}
]
显示位置规则:
验收标准:
所有节点组件统一 props:
{ node, status, selected },emit:select/action。使用 UnoCSS 类,遵循code-copilot/rules/button-style-guide.md。
目标: 起止节点卡片样式,圆角徽章风格,与中间节点视觉区分。
涉及文件:
forge-admin-ui/src/components/flow-designer/nodes/BaseNode.vue — 新建(基础卡片骨架)forge-admin-ui/src/components/flow-designer/nodes/StartNode.vue — 新建forge-admin-ui/src/components/flow-designer/nodes/EndNode.vue — 新建关键签名:
<!-- BaseNode.vue -->
<template>
<div class="flow-node" :class="[`node-${nodeType}`, { selected, [`status-${status}`]: status }]"
@click.stop="$emit('select', node.id)" @contextmenu.prevent="$emit('action', { type: 'menu', event: $event })">
<div class="node-header"><i :class="iconClass" /><span>{{ node.name }}</span></div>
<div class="node-body"><slot /></div>
<div class="node-status" v-if="status"><i :class="statusIcon" /></div>
</div>
</template>
样式规范:
shadow-lg ring-2 ring-primary验收标准:
目标: 审批人/抄送人节点卡片,显示审批模式 + 头像 + 操作摘要。
涉及文件:
forge-admin-ui/src/components/flow-designer/nodes/ApproverNode.vue — 新建forge-admin-ui/src/components/flow-designer/nodes/CarbonCopyNode.vue — 新建forge-admin-ui/src/components/flow-designer/utils/approver-summary.js — 新建关键签名:
// approver-summary.js
export function getApproverSummary(config) {
// → { mode, modeLabel, names, namesLabel, avatars, multi, passRateLabel, missing }
}
摘要规则:
CarbonCopyNode: 类似但样式青色,标题"抄送给:" + 名字列表。
验收标准:
目标: 网关节点 + 分支首部条件徽章 + 汇合点。
涉及文件:
forge-admin-ui/src/components/flow-designer/nodes/ConditionNode.vue — 新建forge-admin-ui/src/components/flow-designer/nodes/ParallelNode.vue — 新建forge-admin-ui/src/components/flow-designer/nodes/InclusiveNode.vue — 新建forge-admin-ui/src/components/flow-designer/canvas/BranchHeader.vue — 新建(分支首部条件徽章)forge-admin-ui/src/components/flow-designer/canvas/MergeNode.vue — 新建(汇合点小圆点)视觉规范:
#f59e0b#8b5cf6#a855f7条件显示:
验收标准:
目标: 高级节点卡片,灰色风格 + 类型徽章。
涉及文件:
forge-admin-ui/src/components/flow-designer/nodes/ServiceNode.vue — 新建forge-admin-ui/src/components/flow-designer/nodes/ScriptNode.vue — 新建forge-admin-ui/src/components/flow-designer/nodes/SubProcessNode.vue — 新建forge-admin-ui/src/components/flow-designer/nodes/CallActivityNode.vue — 新建显示内容:
视觉规范:
border-l-4 border-gray-400验收标准:
目标: 兜底节点卡片(保留 rawXml 不可视化编辑)+ 通用右键操作菜单。
涉及文件:
forge-admin-ui/src/components/flow-designer/nodes/AdvancedNode.vue — 新建forge-admin-ui/src/components/flow-designer/canvas/NodeContextMenu.vue — 新建AdvancedNode 显示:
NodeContextMenu:
<NDropdown :options="menuOptions" trigger="manual" :show="show" :x="position.x" :y="position.y" @select="handleSelect" @clickoutside="show = false" />
菜单项(动态计算):
验收标准:
配置面板按 Tab 拆分组件,所有字段必须 1:1 迁移自
forge-admin-ui/src/components/bpmn/NodePropertiesPanel.vue。配置变更走 useFlowDesigner.updateNode,自动触发 snapshot。
目标: 提供 NDrawer 抽屉壳,根据 selectedNode.nodeType 动态加载对应 Tab 列表与配置组件。
涉及文件:
forge-admin-ui/src/components/flow-designer/panel/NodeConfigDrawer.vue — 新建forge-admin-ui/src/components/flow-designer/panel/tab-resolver.js — 新建(根据 nodeType 计算 tabs)关键签名:
<!-- NodeConfigDrawer.vue -->
<template>
<NDrawer :show="show" :width="480" :on-update:show="$emit('update:show', $event)">
<NDrawerContent :title="`${node.name} - 配置`" closable>
<NTabs v-model:value="activeTab" type="line">
<NTabPane v-for="tab in tabs" :key="tab.key" :name="tab.key" :tab="tab.label">
<component :is="tab.component" :node="node" @update="handleUpdate" />
</NTabPane>
</NTabs>
</NDrawerContent>
</NDrawer>
</template>
tab-resolver.js:
export function resolveTabs(nodeType) {
// approver → [基本信息, 会签, 操作权限, 表单权限, 监听器]
// carbonCopy → [基本信息, 抄送对象]
// condition/inclusive → [分支配置, 监听器]
// parallel → [基本信息, 监听器]
// service → [服务配置, 监听器]
// script → [脚本配置, 监听器]
// subProcess/callActivity → [基本信息, 监听器]
// start → [基本信息, 表单, 监听器]
// end → [基本信息]
// advanced → [只读 XML]
}
验收标准:
目标: 审批人配置面板(taskType + 审批人选择 + 操作权限),1:1 复刻 NodePropertiesPanel.vue:936-1050。
涉及文件:
forge-admin-ui/src/components/flow-designer/panel/configs/ApproverConfig.vue — 新建forge-admin-ui/src/components/flow-designer/panel/configs/PermissionConfig.vue — 新建(操作权限子组件)forge-admin-ui/src/components/flow-designer/panel/widgets/UserPicker.vue — 新建(封装 UserSelectModal)forge-admin-ui/src/components/flow-designer/panel/widgets/RolePicker.vue — 新建字段:
关键签名:
<!-- ApproverConfig.vue -->
<NForm label-placement="left" label-width="100">
<NFormItem label="节点名称"><NInput v-model:value="local.name" /></NFormItem>
<NFormItem label="审批人类型">
<NRadioGroup v-model:value="local.taskType">
<NRadio value="assignee">指定审批人</NRadio>
<NRadio value="candidateUsers">候选用户</NRadio>
<NRadio value="candidateGroups">候选角色</NRadio>
</NRadioGroup>
</NFormItem>
<!-- 按 taskType 切换子表单 -->
</NForm>
复用现有组件:
forge-admin-ui/src/components/bpmn/UserSelectModal.vue — 用户选择弹窗,封装为 UserPicker/system/role/list 接口(参考 NodePropertiesPanel:1900)验收标准:
目标: 会签配置面板(multiInstanceType + completionCondition + passRate),1:1 复刻 NodePropertiesPanel:1100-1200。
涉及文件:
forge-admin-ui/src/components/flow-designer/panel/configs/MultiInstanceConfig.vue — 新建forge-admin-ui/src/components/flow-designer/utils/completion-condition-builder.js — 新建字段:
completion-condition-builder.js:
export function buildCompletionExpression(condition, passRate) {
// all → '${nrOfCompletedInstances/nrOfInstances == 1}'
// any → '${nrOfCompletedInstances >= 1}'
// ratio → `${nrOfCompletedInstances/nrOfInstances >= ${passRate/100}}`
}
export function parseCompletionExpression(expr) {
// 反向解析,→ { condition, passRate }
}
联动规则:
验收标准:
目标: 字段级表单权限(只读/编辑/隐藏),1:1 复刻 NodePropertiesPanel:1300-1400。
涉及文件:
forge-admin-ui/src/components/flow-designer/panel/configs/FormPermissionConfig.vue — 新建forge-admin-ui/src/components/flow-designer/panel/configs/FormConfig.vue — 新建(表单选择 + 字段权限)字段:
/flow/form/list)关键签名:
<NDataTable :columns="columns" :data="local.formFieldPermissions" :max-height="300" />
验收标准:
目标: 任务监听器 + 执行监听器配置,1:1 复刻 NodePropertiesPanel:1450-1550。
涉及文件:
forge-admin-ui/src/components/flow-designer/panel/configs/ListenerConfig.vue — 新建forge-admin-ui/src/components/flow-designer/panel/widgets/ListenerForm.vue — 新建(单个监听器编辑器)字段:
UI:
验收标准:
目标: 条件分支配置(分支列表管理 + 条件表达式 + 默认分支),1:1 复刻 NodePropertiesPanel:1600-1700。
涉及文件:
forge-admin-ui/src/components/flow-designer/panel/configs/ConditionConfig.vue — 新建forge-admin-ui/src/components/flow-designer/panel/widgets/BranchEditor.vue — 新建字段(针对网关节点的所有出边):
关键签名:
<NList>
<NListItem v-for="(edge, idx) in branches" :key="edge.id">
<BranchEditor :edge="edge" :index="idx" :branches="branches" @update="updateBranch" @move="moveBranch" @remove="removeBranch" />
</NListItem>
</NList>
<NButton @click="addBranch">添加分支</NButton>
操作规则:
验收标准:
${...} 语法)目标: 其他节点配置面板(服务任务、脚本任务、起止节点)。
涉及文件:
forge-admin-ui/src/components/flow-designer/panel/configs/ServiceConfig.vue — 新建(ServiceTask)forge-admin-ui/src/components/flow-designer/panel/configs/ScriptConfig.vue — 新建(ScriptTask)forge-admin-ui/src/components/flow-designer/panel/configs/StartConfig.vue — 新建(StartEvent)forge-admin-ui/src/components/flow-designer/panel/configs/EndConfig.vue — 新建(EndEvent)forge-admin-ui/src/components/flow-designer/panel/configs/SubProcessConfig.vue — 新建forge-admin-ui/src/components/flow-designer/panel/configs/CallActivityConfig.vue — 新建字段映射:
验收标准:
目标: 高级节点(advanced)的只读 rawXml 查看面板。
涉及文件:
forge-admin-ui/src/components/flow-designer/panel/configs/AdvancedConfig.vue — 新建字段:
验收标准:
目标: 整合画布 + 节点组件 + 配置抽屉,对外暴露与 FlowModeler 兼容的接口(通过 v-model:modelValue)。
涉及文件:
forge-admin-ui/src/components/flow-designer/DingFlowDesigner.vue — 新建forge-admin-ui/src/components/flow-designer/index.js — 修改:导出 DingFlowDesigner关键签名:
<!-- DingFlowDesigner.vue -->
<template>
<div class="ding-flow-designer">
<Toolbar :can-undo="canUndo" :can-redo="canRedo" @undo="undo" @redo="redo" @zoom-in="..." />
<FlowCanvas ref="canvasRef">
<template #edges><EdgeLayer :edges="flowJson.edges" :paths="layoutPaths" /></template>
<template #nodes>
<component v-for="node in flowJson.nodes" :key="node.id"
:is="resolveNodeComponent(node.nodeType)"
:node="node" :status="null" :selected="node.id === selectedNodeId"
:style="{ left: positions.get(node.id).x + 'px', top: positions.get(node.id).y + 'px' }"
@select="handleSelect" @action="handleAction" />
<AddNodeButton v-for="slot in addSlots" ... />
<BranchHeader v-for="edge in branchEdges" ... />
</template>
</FlowCanvas>
<NodeConfigDrawer v-model:show="drawerShow" :node="selectedNode" @update="handleUpdate" />
<NodeContextMenu ref="contextMenuRef" />
</div>
</template>
<script setup>
const props = defineProps({
modelValue: { type: String, required: true }, // BPMN XML
readonly: Boolean
})
const emit = defineEmits(['update:modelValue', 'change'])
// 内部 flowJson,watch modelValue 转换
const { flowJson, selectedNodeId, addNode, deleteNode, updateNode, ... } = useFlowDesigner()
const { undo, redo, canUndo, canRedo, snapshot } = useFlowHistory(flowJson)
watch(() => props.modelValue, (xml) => {
if (xml) flowJson.value = convertBpmnToJson(xml)
}, { immediate: true })
watch(flowJson, () => {
emit('update:modelValue', convertJsonToBpmn(flowJson.value))
emit('change', flowJson.value)
}, { deep: true })
// 暴露给父组件的方法
defineExpose({
getXML() { return convertJsonToBpmn(flowJson.value) },
importXML(xml) { flowJson.value = convertBpmnToJson(xml) },
reset() { /* ... */ }
})
</script>
resolveNodeComponent 映射:
Toolbar 按钮:
验收标准:
目标: 将 design.vue 主数据从 bpmnXml 字符串改为 flowJson 内部状态,外层接口与后端不变。
涉及文件:
forge-admin-ui/src/views/flow/design.vue — 修改:替换 FlowModeler→DingFlowDesigner修改点:
FlowModeler → DingFlowDesigner<FlowModeler v-model="bpmnXml" /> → <DingFlowDesigner v-model="bpmnXml" />bpmnXml(DingFlowDesigner 内部 watch 自动转换)validateProcess():保留,调用 designerRef.value?.validate()handleSaveDraft()、handleDeploy():通过 designerRef.value.getXML() 获取最新 XMLforge-admin-ui/src/components/ai-flow/(不动)关键签名变更:
<!-- 原 -->
<FlowModeler v-model:value="bpmnXml" ref="modelerRef" />
<!-- 改 -->
<DingFlowDesigner v-model="bpmnXml" ref="designerRef" />
<!-- 保存方法保持兼容 -->
async function handleSaveDraft() {
const xml = designerRef.value.getXML()
await saveModelDraft({ ...form, bpmnXml: xml })
}
验收标准:
目标: 实现钉钉样式的流程查看器,输入 (bpmnXml + nodeStatuses),输出可交互的钉钉风格审批进度图。
涉及文件:
forge-admin-ui/src/components/flow-designer/viewer/DingFlowViewer.vue — 新建forge-admin-ui/src/components/flow-designer/viewer/NodeStatusBadge.vue — 新建(节点状态徽章)forge-admin-ui/src/components/flow-designer/viewer/index.js — 修改:导出 DingFlowViewer关键签名:
<!-- DingFlowViewer.vue -->
<template>
<div class="ding-flow-viewer">
<FlowCanvas ref="canvasRef" :readonly="true">
<template #edges><EdgeLayer :edges="flowJson.edges" :paths="layoutPaths" :node-statuses="statusMap" /></template>
<template #nodes>
<component v-for="node in flowJson.nodes"
:is="resolveNodeComponent(node.nodeType)"
:node="node"
:status="statusMap[node.bpmnElementId]?.status"
:selected="false" :readonly="true"
@click="showNodeDetail(node)" />
</template>
</FlowCanvas>
<NodeDetailPopover v-model:show="popoverShow" :node="popoverNode" :status="popoverStatus" />
</div>
</template>
<script setup>
const props = defineProps({
bpmnXml: { type: String, required: true },
nodeStatuses: { type: Object, default: () => ({}) } // bpmnElementId → { status, assignee, completeTime, comment }
})
const flowJson = computed(() => convertBpmnToJson(props.bpmnXml))
const statusMap = computed(() => buildStatusMap(props.nodeStatuses, flowJson.value))
</script>
NodeStatusBadge.vue:
验收标准:
目标: 点击节点弹出详情 Popover,显示处理人、时间、审批意见、附件。
涉及文件:
forge-admin-ui/src/components/flow-designer/viewer/NodeDetailPopover.vue — 新建forge-admin-ui/src/components/flow-designer/viewer/process-status-builder.js — 新建关键签名:
// process-status-builder.js
export function buildStatusMap(nodeStatuses, flowJson) {
// 把后端返回的 [{ activityId, status, assignee, ... }] 转为 Map<bpmnElementId, info>
// 处理多次任务(重新指派):取最新一条
// 推断未到达节点为 pending
}
export function inferEdgeStatus(edge, nodeStatusMap) {
// 边状态 = 源节点状态
// 默认分支:仅在汇合时才高亮
}
<!-- NodeDetailPopover.vue -->
<NPopover :show="show" :x="x" :y="y" placement="right" trigger="manual">
<div class="node-detail">
<div class="header">
<NAvatar :src="status.assignee?.avatarUrl" />
<div>{{ status.assignee?.userName || '未指派' }}</div>
</div>
<NDescriptions :column="1">
<NDescriptionsItem label="状态">{{ statusLabel }}</NDescriptionsItem>
<NDescriptionsItem label="开始时间">{{ status.startTime }}</NDescriptionsItem>
<NDescriptionsItem label="完成时间">{{ status.completeTime }}</NDescriptionsItem>
<NDescriptionsItem label="审批意见">{{ status.comment }}</NDescriptionsItem>
</NDescriptions>
<NDivider />
<NThing v-for="att in status.attachments">{{ att.name }}</NThing>
<NButton text v-if="status.status === 'completed'" @click="viewHistory">查看处理记录</NButton>
</div>
</NPopover>
显示规则:
验收标准:
目标: 替换流程相关页面中的 ProcessDiagramViewer 为 DingFlowViewer。
涉及文件:
forge-admin-ui/src/views/flow/todo.vue — 修改:流程图查看forge-admin-ui/src/views/flow/done.vue — 修改forge-admin-ui/src/views/flow/started.vue — 修改forge-admin-ui/src/views/flow/monitor.vue — 修改(流程实例监控)修改模式:
<!-- 原 -->
<ProcessDiagramViewer :process-instance-id="instanceId" />
<!-- 改 -->
<DingFlowViewer :bpmn-xml="bpmnXml" :node-statuses="nodeStatuses" />
辅助逻辑:
getProcessDiagramInfo(instanceId) 获取 { bpmnXml, nodeStatuses }验收标准:
目标: 替换流程版本对比页、历史版本查看页中的旧 BPMN viewer。
涉及文件:
forge-admin-ui/src/views/flow/version-compare.vue(如存在) — 修改forge-admin-ui/src/views/flow/version-history.vue(如存在) — 修改forge-admin-ui/src/views/flow/instance-detail.vue(如存在) — 修改修改模式:
验收标准:
目标: 删除旧 BPMN 设计器代码与依赖,验证无遗留引用。
涉及文件:
forge-admin-ui/package.json — 修改:移除 bpmn-js、bpmn-js-properties-panel、inherits-browserforge-admin-ui/src/components/bpmn/ — 删除整个目录
forge-admin-ui/src/styles/bpmn.css(如存在) — 删除forge-admin-ui/src/main.js — 修改:移除 bpmn-js 相关 import验证步骤:
# 1. 全局搜索,确保无遗留引用
cd forge-admin-ui
grep -rn "from 'bpmn-js" src/ && echo "FAIL" || echo "OK"
grep -rn "from '@/components/bpmn" src/ && echo "FAIL" || echo "OK"
grep -rn "ProcessDiagramViewer\|InteractiveProcessDiagram\|FlowModeler\|NodePropertiesPanel" src/ && echo "FAIL" || echo "OK"
# 2. 卸载依赖
pnpm remove bpmn-js bpmn-js-properties-panel inherits-browser
# 3. 重新构建确认无报错
pnpm build
验收标准:
目标: 全量构建 + Lint 修复,确保代码符合项目规范。
涉及文件:
验证步骤:
cd forge-admin-ui
pnpm install
pnpm lint:fix
pnpm build # 期望:构建成功,bundle 体积 < 旧版(移除 bpmn-js 后)
pnpm vitest run src/components/flow-designer # 期望:所有单测通过
验收标准:
目标: 启动开发服务器,按 SDD code-copilot/rules/automated-testing-standard.md 走完关键场景。
测试场景(在 code-copilot/changes/dingflow-approver-style-designer/test-spec.md 中详细列出):
验证步骤:
# 启动后端
cd forge/forge-admin-server && mvn spring-boot:run
# 启动前端
cd forge-admin-ui && pnpm dev
# 浏览器访问 http://localhost:5173,登录 admin/123456
# 按场景手工执行,记录到 execution-log.md
验收标准:
code-copilot/changes/dingflow-approver-style-designer/execution-log.md[x] Spec 覆盖:spec 第 3 章 23 个功能点(F1-F23)均映射到具体 Task:
[x] 占位符扫描:无 TBD/TODO/"按需要补充"。所有"涉及文件"是绝对路径,"关键签名"含完整伪代码。
[x] 类型一致性:
[x] 风险任务专项:转换层(Task 1-6)安排在 Phase 1 最早执行,附完整单测 fixture,进入 Phase 3 前必须 100% 通过