Skip to content

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.js

  • Create: designer3.0/src/modules/workflow/validate.js

  • Create: designer3.0/tests/modules/workflow/runtime.spec.js

  • [ ] Step 1: 固定数据结构

js
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: 写失败测试
js
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: 跑测试并提交
bash
pnpm test tests/modules/workflow/runtime.spec.js

Expected: 本文件此刻只有 validate 用例 PASS(runtime 用例下一任务再加)。

bash
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.js

  • Modify: designer3.0/tests/modules/workflow/runtime.spec.js

  • [ ] Step 1: 写失败测试

js
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:

  1. 当前节点必须是 approve。
  2. canAct(node, actor):assignees.type===role 则 actor.roles 与 value 有交集;permission 则看 actor.permissions。
  3. approve:沿无条件出边或第一条出边前进;若到达 end,status='approved'。
  4. reject:status='rejected',current 仍停在当前节点。
  5. transfer:不改节点,把 inst.delegateTo = action.userId 写入,下一 actor 校验若 actor.userId === delegateTo 也通过。
  6. 每次动作 push history: {nodeId, type, userId, comment, at}。
  7. 返回新对象,不改入参(便于测试)。

condition 节点:用 formData 对边的 condition 做简单求值。第一期只支持 formData.field === 'value' 这种由设计器生成的表达式,用已有公式引擎如果能直接调就调 src/utils/formulas.js,否则做白名单比较,禁止 eval。

  • [ ] Step 3: 跑测试并提交
bash
pnpm test tests/modules/workflow/runtime.spec.js

Expected: PASS

bash
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.js

  • Modify: designer3.0/tests/modules/workflow/runtime.spec.js

  • [ ] Step 1: 写失败测试

js
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: 提交
bash
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.vue

  • Create: designer3.0/src/designer/workflow/NodeEditor.vue

  • Modify: 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
bash
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.js

  • Modify: designer3.0/src/behavior-list 文档或 fc-design-pro/src/behavior-list.md

  • [ ] Step 1: startWorkflow

js
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 角色通过 → 无角色点通过应失败。

bash
pnpm test tests/modules/workflow

Expected: PASS

bash
git add src/utils/behavior.js src/modules/shared/adapter.js src/designer src/locale
git commit -m "feat: workflow behaviors and preview actions"