# 发起审批 · 条件可确定后展示最终审批链(钉钉式) **日期:** 2026-07-17 **范围:** `web-admin` `/#/system-manage/oa-approval/start` 发起弹窗底部「审批流程」;`backend` `preview_start_flow` / `flow-preview` API **关联:** - `docs/superpowers/specs/2026-07-15-oa-start-flow-preview-design.md`(首版方案 B:条件全展) - `docs/superpowers/specs/2026-07-15-oa-condition-from-form-design.md`(条件字段来源与必填约定) - `backend/app/services/oa_engine.py` — `_match_condition_group`、`preview_start_flow` **关系:** 本规格**取代**首版中「条件分支全部支路展开」与非目标「条件随表单实时裁剪」;人员解析、卡片样式、线性流程展示仍沿用 2026-07-15 规格。 --- ## 1. 背景与目标 ### 现状问题 含条件分支的流程在发起预览中并列展示所有支路(如「条件1」与「默认」),并标注「实际路径以提交时表单为准」。用户无法在提交前看到**将实际走的那一条链**,与钉钉「填齐必填后再出流程」的体验不一致。 ### 目标 1. 条件相关表单字段**未齐**时:底部不展开支路,仅提示「必填信息填写后,流程将自动显示」。 2. 字段**齐且可匹配**后:只展示**命中路径**上的节点,并解析审批人/抄送人(与提交后引擎行为一致)。 3. 无条件分支的线性流程:打开弹窗即可展示完整链(行为不变)。 ### 非目标 - 全展 + 命中支路高亮(备选路径仍可见)。 - 条件规则依赖「已解析出的审批人是谁」(条件仍只认表单字段)。 - 改造审批详情页进度轴。 - 在流程节点卡片内点选自选审批人(自选仍用弹窗顶部选择器)。 --- ## 2. 产品约定(已确认) | 项 | 结论 | |----|------| | 未确定时 UI | 钉钉式占位文案,**不**展开条件1/默认等支路 | | 占位文案 | 「必填信息填写后,流程将自动显示」 | | 已确定时 UI | 仅最终路径时间轴(节点名 + 签批方式 + meta + 人员 chip) | | 「可确定」依据 | 流程树中条件分支所引用的表单字段在 `formData` 中均已有有效值 | | 发起人自选 | **不**作为整段预览的门闩;路径已确定即出链,未选人时该节点 `people` 为空 | | 与提交一致性 | 分支匹配复用 `_match_condition_group(form_data)`,与 `advance_from` 同源 | | 刷新时机 | 表单值变化防抖刷新;自选人变化立即/短防抖刷新 | --- ## 3. 状态机 ``` ┌─────────────────────┐ │ 打开发起弹窗 / 改表单 │ └──────────┬──────────┘ ▼ 流程树是否含 condition? / \ 否 是 │ │ ▼ ▼ 【已确定】 条件引用字段是否均有值? 展示完整链 / \ 否 是 │ │ ▼ ▼ 【待确定】 【已确定】 仅占位文案 只走命中支路 + 解析人 ``` | 状态 | `resolved` | 前端展示 | |------|------------|----------| | 待确定 | `false` | 标题「审批流程」+ 占位文案;`steps` 为空或忽略 | | 已确定 | `true` | 标题 + 时间轴 `steps`(可含右侧 hint,如「按当前发起人解析审批人」) | 加载中:沿用现有 `v-loading`,不闪占位与时间轴之间的错误态。 --- ## 4. 「可确定」判定细则 ### 4.1 收集条件字段 从发布版 `flow_tree_json` 根节点递归: - 对每个 `type=condition` 节点,遍历 `conditionGroups`; - 跳过默认分支(`isDefault` 或无 `rules`/`when` 的空规则组); - 从非默认组的 `rules[]` 收集 `field`;若仅有历史 `when` 字符串,用与引擎一致的方式解析出 `{field}` 占位符(若无法解析,视为该组不增加门闩字段,匹配仍走引擎)。 去重后得到集合 `conditionFields`。 ### 4.2 字段「有值」 对每个 `field`,取 `formData[field]`: - `null` / `undefined` / `''`(trim 后)→ 未填; - 数组:长度为 0 → 未填; - 数字 `0`、布尔 `false` → **视为已填**(合法取值)。 任一 `conditionFields` 未填 → `resolved=false`。 无 `condition` 节点,或 `conditionFields` 为空(仅有默认支)→ 直接 `resolved=true`,按主链(或默认支)预览。 ### 4.3 匹配后行走 `resolved=true` 时: - 遇到 `condition`:调用 `_match_condition_group(groups, form_data)`,**仅**递归命中组的 `child`,**不**再把其它组写入 `steps`; - **不**再输出 `type=branch` 的「条件1 / 默认」占位步(最终链上无需分支标题卡,与钉钉「直接出后续审批人」一致); - 条件节点汇合后的 `child`(若有)继续行走; - `approval` / `cc` / `start` / `end` 人员解析逻辑与现网 `preview_start_flow` 一致。 若字段齐但 `_match_condition_group` 返回 `None`(异常配置):`resolved=false`,占位文案可改为「流程条件无法匹配,请检查表单或联系管理员」(可选;首版可仍用统一占位句,并在 `resolveError` 级日志/接口字段暴露)。 --- ## 5. API ### 5.1 变更 将 `GET /api/admin/oa-approval/templates/{appId}/flow-preview?initiatorSelectUserIds=` 改为(推荐): `POST /api/admin/oa-approval/templates/{appId}/flow-preview` Body: ```json { "formData": { "days": 5 }, "initiatorSelectUserIds": ["user-id-1"] } ``` 兼容:若短期内保留 GET,则无法可靠传复杂 `formData`;**本期以 POST 为准**,前端切换后可删除 GET 或让 GET 仅支持无条件线性预览。 ### 5.2 响应 ```json { "resolved": true, "message": "", "steps": [ { "id": "n_start", "type": "start", "name": "发起人", "signMode": "", "assigneeType": "", "meta": "", "people": [{ "id": "...", "nickName": "系统管理员" }], "resolveError": "" } ] } ``` | 字段 | 说明 | |------|------| | `resolved` | `false` 时前端只显示 `message`(或默认占位句),不渲染时间轴 | | `message` | `resolved=false` 时默认「必填信息填写后,流程将自动显示」;可覆盖异常文案 | | `steps` | `resolved=true` 时为最终路径;`false` 时建议 `[]` | 单步结构与现有 `OaFlowPreviewStep` 一致。 --- ## 6. 前端 ### 6.1 组件 - `OaFlowPreview.vue`:增加 `resolved`(或由父级根据接口决定);`resolved===false` 时展示占位区块(样式对齐 Fresh Mint:浅底、居中/左对齐 muted 文案),隐藏 chain。 - `start/index.vue`: - `loadFlowPreview` 改为 POST,传入当前 `formData` 与自选人; - `watch(formData, …, { deep: true })` + debounce(建议 300ms); - 保留对 `initiatorSelectUserIds` 的 watch; - 打开弹窗、详情加载完成后拉一次预览。 ### 6.2 文案与 hint - 待确定:主文案固定为「必填信息填写后,流程将自动显示」;标题旁原 hint「按当前发起人解析审批人」可不显示。 - 已确定:可保留 hint。 ### 6.3 API 模块 `web-admin/src/api/oaApproval.ts` — `fetchOaFlowPreview(appId, { formData, initiatorSelectUserIds })` 改为 POST;类型增加 `resolved`、`message`。 --- ## 7. 后端 ### 7.1 主要改动 - `oa_approval.py`:新增/替换为 POST handler,解析 body。 - `oa_engine.preview_start_flow`:增加 `form_data` 参数;实现字段收集、`resolved` 门闩、条件单路行走。 - 抽取 `_collect_condition_fields(root) -> List[str]`、`_form_value_present(form_data, field) -> bool`,便于单测。 - 删除或停用「全展 + meta=条件分支(实际路径以提交时表单为准)」分支路径(仅 `resolved=false` 时根本不 walk 条件子树亦可:直接返回空 steps)。 ### 7.2 测试建议 - 无条件:始终 `resolved=true`,steps 含 start→approval→end。 - 有条件、字段空:`resolved=false`,steps=`[]`,message 为占位句。 - 有条件、字段满足条件1:仅条件1子树节点出现,无默认支、无 branch 占位步。 - 有条件、字段走默认:仅默认子树。 - 自选人未选但字段齐:`resolved=true`,initiator_select 节点 people 为空。 --- ## 8. 主要文件 | 文件 | 职责 | |------|------| | `backend/app/services/oa_engine.py` | `preview_start_flow` 门闩 + 单路 walk | | `backend/app/api/oa_approval.py` | POST `flow-preview` | | `backend/tests/…` | 预览解析单测(新建或挂现有 oa 测试) | | `web-admin/src/api/oaApproval.ts` | POST 客户端 | | `web-admin/src/components/oa-approval/OaFlowPreview.vue` | 待确定占位 UI | | `web-admin/src/views/oa-approval/start/index.vue` | formData 防抖刷新 | | `docs/superpowers/specs/2026-07-15-oa-start-flow-preview-design.md` | 文首增加「条件展示见 2026-07-17 规格」指向(实现时可补一行) | --- ## 9. 验收标准 1. 含条件分支的应用:打开发起弹窗且条件字段未填时,底部仅见「必填信息填写后,流程将自动显示」,**不出现**「条件1 / 默认」双支路。 2. 填齐条件字段后,底部自动变为单路径时间轴,节点与人员与按该表单提交后的引擎路径一致。 3. 修改条件字段导致走另一支时,预览链随之切换。 4. 无条件分支的应用:行为与改前一致,可立即看到审批人。 5. 仅缺自选审批人、条件已齐:仍显示最终链,自选节点可无人 chip。 6. 遮罩无 blur、弹窗仍为 `dialog-form-layout`;预览区块视觉延续 Fresh Mint / 现有 `OaFlowPreview` 风格。 --- ## 10. 分阶段 | 阶段 | 内容 | |------|------| | P0(本期) | POST + resolved 门闩 + 单路预览 + 占位文案 + 前端防抖 | | P1(可选) | 匹配失败专用文案;从 `when` 公式提取字段的更完整解析;mockup HTML 更新 | --- ## 11. 修订记录 | 日期 | 说明 | |------|------| | 2026-07-17 | 初稿:钉钉式待确定占位 + 确定后最终路径;用户确认自选不挡整段预览 |