docs: add OA person select control design spec.

Capture FormCreate person field (dept-aligned UI), form_data contract,
and contains-only condition branching for workflow.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-07-30 12:03:38 +08:00
1 parent be41e8f202
commit 39d1031f6c
1 file changed
+261
@@ -0,0 +1,261 @@
# OA「人员」控件设计文档
**日期:** 2026-07-30
**范围:** `web-admin` 审批设计器步骤 2(表单设计)+ 步骤 3(条件分支)+ 发起/详情/打印运行时;`backend` 发布白名单、发起校验、条件匹配
**关联:** `docs/superpowers/specs/2026-07-27-oa-dept-select-design.md`;`docs/superpowers/specs/2026-07-15-oa-condition-from-form-design.md`;`docs/superpowers/specs/2026-07-30-oa-money-field-design.md`
**状态:** 已评审待实现
---
## 1. 背景与目标
OA 审批「2 表单设计」需提供可拖拽的 **人员** 控件:从本租户系统用户表选择一人或多名人员;展示与交互对齐已落地的 **部门** 控件。勾选「参与流程条件分支」后,可在「3 流程设计」中配置钉钉式「包含」条件(右侧可选多人,任一命中即通过)。
### 1.1 产品结论(已确认)
| 项 | 结论 |
|----|------|
| 展示/交互 | 对齐「部门」:PC 下拉 + 窄屏抽屉;多选 tag 在选择框内 |
| 选择能力 | 默认可多选;设计器可改为单选;可配 `max` |
| 数据源 | `sys_user`(`fetchUserList`,`status=1` 启用用户);租户隔离走现有 admin API |
| 存值 | 单选 `{ id, name }`;多选同结构数组(`name` 为展示名快照) |
| 条件运算符 | **仅「包含」**(`contains`);不提供等于/不等于 |
| 包含语义 | 条件右侧可多选人;表单人员 id 集合 ∩ 条件 id 集合 **非空** 即命中(OR) |
| 实现路径 | 自定义 FormCreate `type=person`(四层扩展,同部门/金额) |
### 1.2 非目标
- 等于 / 不等于 运算符
- 按部门树筛人(可后续加)
- 用本控件结果直接改写审批人解析(仍用现有 `assigneeType` 策略)
- 本期小程序运行时渲染器(Schema/`form_data` 须可映射)
- 行业通讯录 / 外部联系人
---
## 2. 架构
对齐 FormCreate / FcDesigner **四层扩展**:
| 层 | 内容 |
|----|------|
| Runtime | `OaPersonSelect.vue`:`v-model` 为对象或对象数组 |
| FormCreate 组件注册 | `formCreate.component('person', …)` |
| DragRule | `menu: 'main'`;`label: '人员'`;`props.multiple` 默认 `true` |
| 双侧注册 | 主应用 + `oa-fc-host` 均调用 `setupOaPersonSelect` |
```
[设计器 DragRule] → form_schema_json.type = "person"
↓
[OaPersonSelect] ← fetchUserList(status=1)
↓
form_data[field] = { id, name } | { id, name }[]
↓
[条件] useInCondition → op=contains, value=string[] (user ids)
↓
[引擎] id 集合交集非空 → 命中分支
[详情/打印] 优先展示 name 快照
```
---
## 3. Schema 与属性
### 3.1 规则形态(示例)
```json
{
"type": "person",
"field": "Fxxxx",
"title": "协同人",
"info": "",
"$required": false,
"useInCondition": false,
"props": {
"multiple": true,
"max": null,
"useInCondition": false
}
}
```
### 3.2 设计器可配属性
| 属性 | 说明 |
|------|------|
| 标题 / 提示信息 / 必填 | 与现有控件一致 |
| 允许多选 | Switch,**默认 `true`** |
| 最大可选数 | 可选;仅多选时生效;不填不限制 |
| 参与流程条件分支 | 基础配置全局开关(`person` 加入 `CONDITION_ELIGIBLE_TYPES` 后可见);勾选后强制必填 |
---
## 4. `form_data` 契约(锁定)
```ts
type OaPersonValueItem = {
id: string // sys_user.id (UUID)
name: string // 提交时展示名快照:优先 nickName,空则 userNo
}
// 单选(props.multiple === false)
type PersonFormValueSingle = OaPersonValueItem | null
// 多选(props.multiple === true,默认)
type PersonFormValueMulti = OaPersonValueItem[]
```
约束:
- `id`、`name` 均为非空字符串。
- 禁止与 `multiple` 不一致的标量/对象混用。
- 用户改名后,历史实例 **SHALL** 仍展示提交时的 `name` 快照。
- 后端发起校验 **SHALL** 校验值形态;**MAY** 校验 id 对应当前租户启用用户(建议校验,失败返回中文错误)。
---
## 5. 运行时交互
### 5.1 PC(视口宽度 ≥ 768)
- `el-select`:`filterable`、`clearable`;`multiple` 时多选,已选以 select 内 tag 展示(对齐部门 tree-select 多选表现)。
- 选项 label:`nickName`(空则 `userNo`);可附带次要信息区分重名,写入 `name` 的仍为主展示名。
### 5.2 窄屏(&lt; 768)
- 触发按钮 + 底部 `el-drawer`(仿部门)。
- 抽屉内:搜索 + 单选点选 / 多选勾选;底栏取消(Close)/ 确定(Check)。
### 5.3 只读
- 单选:展示 `name`;空为 `—`。
- 多选:姓名用「、」拼接。
### 5.4 超限
- `max` 有值且超限:前端 `ElMessage.warning`,不写入;后端校验兜底。
### 5.5 数据加载
- `fetchUserList({ status: '1', page, pageSize })` 分页拉全量(与发起页 / 流程设计器选人一致)。
- 组件内维护 id→name 映射,将 select 的 id 还原为 `{ id, name }`。
---
## 6. 条件分支
### 6.1 资格
- 前端 `CONDITION_ELIGIBLE_TYPES` 与后端 `oa_publish.CONDITION_ELIGIBLE_TYPES` **均加入** `person`。
- 勾选 `useInCondition` 后出现在条件候选列表;发布前强制 `$required` / `required`(现有归一化逻辑)。
### 6.2 规则形态
```ts
// ConditionOp 扩展
type ConditionOp = '>' | '>=' | '<' | '<=' | '==' | '!=' | 'contains'
interface ConditionRule {
field: string
op: 'contains' // person 字段仅允许此 op
value: string[] // 用户 id 列表,长度 ≥ 1
}
```
### 6.3 命中语义
```
formIds = extract_user_ids(form_data[field]) // 单对象或数组 → Set<id>
condIds = Set(rule.value)
hit = |formIds ∩ condIds| > 0
```
类比钉钉审批「发起人属于这些人」:条件侧多人,**任一**出现在表单已选人员中即命中。
### 6.4 条件编辑器 UI
- 选中 `person` 字段:运算符固定为「包含」(可只读展示)。
- 右侧:人员多选(可搜),`value` 为 `string[]`。
- 预览:`{标题} 包含 张三、李四`。
- 发布/保存流程时:`contains` 且 `value` 为空 → 拒绝。
### 6.5 引擎实现要点
- **SHALL NOT** 将人员对象塞进现有 `{field} == 'x'` 公式字符串做比较。
- 在 `_match_condition_group`(或等价路径)对 `op === 'contains'` 走专用集合判断;从 form 值中抽取 `id`(兼容单对象与数组)。
- 其他字段的 `==` / 数值比较逻辑保持不变。
---
## 7. 文件改动清单
### 7.1 前端(拟新增)
| 路径 | 职责 |
|------|------|
| `web-admin/src/components/oa-approval/OaPersonSelect.vue` | 运行时控件 |
| `web-admin/src/utils/oa-person-value.ts` | normalize / 展示 |
| `web-admin/src/utils/oa-person-select-rule.ts` | DragRule |
| `web-admin/src/utils/oa-form-create-person.ts` | 注册入口 |
### 7.2 前端(拟修改)
| 路径 | 职责 |
|------|------|
| `web-admin/src/main.ts`、`oa-fc-host/main.ts` | `setupOaPersonSelect` |
| `oa-condition-fields.ts` | 合格类型 + `contains` + `opsForFieldType` |
| `flow-tree/types.ts` | `ConditionOp` / `ConditionRule.value` |
| `ConditionRuleEditor.vue` | person 右侧多选 |
| `utils/oa-approval-print.ts` 等 | `person` 打印展示 |
### 7.3 后端
| 路径 | 职责 |
|------|------|
| `app/services/oa_publish.py` | `FILLABLE_TYPES` / `CONDITION_ELIGIBLE_TYPES` 加入 `person` |
| `app/services/oa_person_select.py`(新建) | form_data 值形态校验(仿 `oa_dept_select`) |
| `app/services/oa_engine.py` | `contains` 集合匹配 |
| `app/services/oa_related_approval.py` | 摘要格式化(仿 dept) |
| 发起/提交入口 | 挂接 person 校验 |
---
## 8. 错误处理
| 场景 | 行为 |
|------|------|
| 用户列表加载失败 | 选项空;可提示;不阻断设计器 |
| 超 `max` | 前端拦截 + 后端校验 |
| 条件 `contains` 且 value 空 | 发布/保存流程拒绝 |
| 值形态非法 | 发起返回中文错误列表 |
---
## 9. 测试要点
- 单选/多选存值与回显;窄屏抽屉确认写入。
- `useInCondition` 对 `person` 可见;勾选后必填。
- 条件:表单选 A,条件 `[A,B]` → 命中;条件仅 `[B]` 且表单无 B → 不命中。
- 发布含 `person` 的应用不报未知控件。
- 打印 / 关联审批摘要显示姓名快照。
- 回归:金额数值条件、部门存值与打印不受影响。
---
## 10. 实现顺序建议
1. 值工具 + 后端 `FILLABLE`/`CONDITION` 白名单 + person 校验骨架
2. `OaPersonSelect` + DragRule + 双侧注册
3. 条件类型/`contains` + ConditionRuleEditor 人员多选
4. 引擎集合匹配 + 单测
5. 打印/关联审批格式化 + 端到端手测
---
## 11. 验收标准
- [ ] 设计器左侧可拖入「人员」,可配单多选与 max
- [ ] 发起页可选人并正确提交 `{ id, name }` / 数组
- [ ] 勾选参与条件后,流程条件仅「包含」,右侧可多选人,任一命中走对应分支
- [ ] 详情与打印展示姓名;历史快照不随用户改名变化
- [ ] 未知控件发布校验通过;既有金额/部门行为无回归