Lightweight Approval Workflow Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 为单张表单提供可设计的轻量审批流(通过/驳回/转交 + 节点字段权限),运行时是纯函数状态机,持久化交给 HostAdapter。
Architecture: WorkflowDef 与 EventFlow 数据分离,避免把待办塞进行为节点。设计器左侧「流程」用简化节点编辑器(不必复用混淆的 utils/flow)。运行时 applyAction 不碰 DOM。新增 setBehavior:startWorkflow、completeTask。
Tech Stack: 现有 $roles / $permissions、ConditionGroup、behavior.js。
Depends on: 共享 HostAdapter;不依赖 Form-to-App。
File map
- Create:
designer3.0/src/modules/workflow/types.js - Create:
designer3.0/src/modules/workflow/validate.js - Create:
designer3.0/src/modules/workflow/runtime.js - Create:
designer3.0/tests/modules/workflow/runtime.spec.js - Create:
designer3.0/src/designer/workflow/WorkflowPanel.vue - Create:
designer3.0/src/designer/workflow/NodeEditor.vue - Modify:
designer3.0/src/utils/behavior.js— 两个新行为 - Modify:
designer3.0/src/designer/FcDesigner.vue— 入口、options.workflow 存盘 - Modify:
designer3.0/src/locale/zh-cn.js/en.js/jp.js - Modify:
designer3.0/types/index.d.ts - Create:
fc-design-pro/src/help/feature/workflow.md
Task 1: 校验 WorkflowDef
Files:
Create:
designer3.0/src/modules/workflow/types.jsCreate:
designer3.0/src/modules/workflow/validate.jsCreate:
designer3.0/tests/modules/workflow/runtime.spec.js[ ] Step 1: 固定数据结构
export const NODE_TYPES = ['start', 'approve', 'condition', 'end'];
// WorkflowDef = { id, formId, nodes: WorkflowNode[], edges: {id, source, target, condition?}[] }
// WorkflowNode = { id, type, name, assignees?: {type:'role'|'permission', value:string[]}, fields?: {writable:string[], hidden:string[]} }- [ ] Step 2: 写失败测试
import { describe, it, expect } from 'vitest';
import { validateWorkflow } from '../../../src/modules/workflow/validate.js';
const valid = {
id: 'wf1',
formId: 'leave',
nodes: [
{ id: 's', type: 'start', name: '开始' },
{ id: 'a', type: 'approve', name: '主管', assignees: { type: 'role', value: ['manager'] } },
{ id: 'e', type: 'end', name: '结束' },
],
edges: [
{ id: 'e1', source: 's', target: 'a' },
{ id: 'e2', source: 'a', target: 'e' },
],
};
it('accepts a linear approval', () => {
expect(validateWorkflow(valid).ok).toBe(true);
});
it('rejects missing start', () => {
const bad = { ...valid, nodes: valid.nodes.filter((n) => n.type !== 'start') };
expect(validateWorkflow(bad).ok).toBe(false);
expect(validateWorkflow(bad).errors).toContain('start_required');
});- [ ] Step 3: 实现校验规则
必须检查:恰好 1 个 start、至少 1 个 end、所有边的 source/target 存在、approve 必须有 assignees、从 start 能走到某个 end(BFS)、condition 节点出边至少 2 条。
- [ ] Step 4: 跑测试并提交
pnpm test tests/modules/workflow/runtime.spec.jsExpected: 本文件此刻只有 validate 用例 PASS(runtime 用例下一任务再加)。
git add src/modules/workflow tests/modules/workflow
git commit -m "feat: validate lightweight workflow definition"Task 2: applyAction 状态机
Files:
Create:
designer3.0/src/modules/workflow/runtime.jsModify:
designer3.0/tests/modules/workflow/runtime.spec.js[ ] Step 1: 写失败测试
import { createInstance, applyAction } from '../../../src/modules/workflow/runtime.js';
const actor = { userId: 'u1', roles: ['manager'], permissions: [] };
it('starts at first approve node', () => {
const inst = createInstance(valid, { formData: { name: '张三' } });
expect(inst.status).toBe('running');
expect(inst.currentNodeId).toBe('a');
});
it('approve moves to end', () => {
const inst = createInstance(valid, { formData: {} });
const next = applyAction(valid, inst, { type: 'approve', comment: 'ok' }, actor);
expect(next.status).toBe('approved');
expect(next.currentNodeId).toBe('e');
});
it('reject finishes rejected', () => {
const inst = createInstance(valid, { formData: {} });
const next = applyAction(valid, inst, { type: 'reject', comment: 'no' }, actor);
expect(next.status).toBe('rejected');
});
it('blocks actor without role', () => {
const inst = createInstance(valid, { formData: {} });
expect(() => applyAction(valid, inst, { type: 'approve' }, { userId: 'x', roles: [], permissions: [] })).toThrow('not_assignee');
});- [ ] Step 2: 实现
createInstance:currentNodeId 设为 start 的唯一出边目标。history 空数组。
applyAction:
- 当前节点必须是
approve。 canAct(node, actor):assignees.type===role 则actor.roles与 value 有交集;permission 则看actor.permissions。approve:沿无条件出边或第一条出边前进;若到达 end,status='approved'。reject:status='rejected',current 仍停在当前节点。transfer:不改节点,把inst.delegateTo = action.userId写入,下一 actor 校验若actor.userId === delegateTo也通过。- 每次动作 push
history: {nodeId, type, userId, comment, at}。 - 返回新对象,不改入参(便于测试)。
condition 节点:用 formData 对边的 condition 做简单求值。第一期只支持 formData.field === 'value' 这种由设计器生成的表达式,用已有公式引擎如果能直接调就调 src/utils/formulas.js,否则做白名单比较,禁止 eval。
- [ ] Step 3: 跑测试并提交
pnpm test tests/modules/workflow/runtime.spec.jsExpected: PASS
git add src/modules/workflow tests/modules/workflow
git commit -m "feat: workflow state machine for approve reject transfer"Task 3: 节点字段权限
Files:
Modify:
designer3.0/src/modules/workflow/runtime.jsModify:
designer3.0/tests/modules/workflow/runtime.spec.js[ ] Step 1: 写失败测试
it('exposes writable and hidden fields for current node', () => {
const def = {
...valid,
nodes: valid.nodes.map((n) =>
n.id === 'a' ? { ...n, fields: { writable: ['reason'], hidden: ['salary'] } } : n
),
};
const inst = createInstance(def, { formData: {} });
const view = getNodeFieldPolicy(def, inst);
expect(view.writable).toEqual(['reason']);
expect(view.hidden).toEqual(['salary']);
});- [ ] Step 2: 实现 getNodeFieldPolicy
运行时渲染器根据 policy 调已有 api.hidden(true) / disable。本任务只交付纯函数;挂到预览的步骤在 Task 5。
- [ ] Step 3: 提交
git add src/modules/workflow tests/modules/workflow
git commit -m "feat: per-node field policy for workflow"Task 4: 设计器流程面板
Files:
Create:
designer3.0/src/designer/workflow/WorkflowPanel.vueCreate:
designer3.0/src/designer/workflow/NodeEditor.vueModify:
designer3.0/src/designer/FcDesigner.vue[ ] Step 1: 数据存哪里
存在 options.workflow(与 formName 同级)。getOptionsJson 会自动带出,宿主 @save 不用改协议。
- [ ] Step 2: UI
第一期不用 utils/flow 混淆包。用纵向列表:节点卡片 + 「添加审批人」+ 上下移动。边上按数组顺序连接。condition 节点用现有 ConditionGroup 配一条「走下一路」的条件,不满足则结束为 reject。
NodeEditor 字段:名称、assignees(复用权限/角色树 config.roles / config.permissions)、writable/hidden 多选(选项来自当前表单 field 列表)。
保存前跑 validateWorkflow,失败 ElMessage.error。
- [ ] Step 3: 入口
config.showWorkflow === true 时左侧显示。默认 false。
- [ ] Step 4: Commit
git add src/designer/workflow src/designer/FcDesigner.vue src/locale
git commit -m "feat: workflow designer panel stored in options.workflow"Task 5: 行为与预览
Files:
Modify:
designer3.0/src/utils/behavior.jsModify:
designer3.0/src/behavior-list文档或fc-design-pro/src/behavior-list.md[ ] Step 1: startWorkflow
startWorkflow(config, inject) {
const adapter = inject.api.getData('$adapter');
if (!adapter || !adapter.startWorkflow) {
return Promise.reject(new Error('[fc-adapter] startWorkflow is not implemented'));
}
return adapter.startWorkflow(config.formId || inject.api.getData('$formId'), inject.api.formData());
}- [ ] Step 2: completeTask
参数:action: approve|reject|transfer,comment,userId(转交)。调用 adapter.completeTask。官方 memory adapter 内部用 applyAction 更新实例。
给 createMemoryAdapter 增加 instances Map:startWorkflow 调 createInstance,completeTask 调 applyAction。补测试。
- [ ] Step 3: 预览
预览表单时如果 options.workflow 存在,把 $adapter 和当前实例政策注入。按 getNodeFieldPolicy 隐藏/禁用字段。工具条模拟「通过/驳回」。
- [ ] Step 4: 文档与验收
fc-design-pro/src/help/feature/workflow.md:节点类型、谁来执行状态机、如何对接 Flowable(adapter 翻译即可)。
手动:配主管审批 → 预览用 manager 角色通过 → 无角色点通过应失败。
pnpm test tests/modules/workflowExpected: PASS
git add src/utils/behavior.js src/modules/shared/adapter.js src/designer src/locale
git commit -m "feat: workflow behaviors and preview actions"

