docs(gerp): 新增实体选择器标准文档

记录 EntityPicker/MultiEntityPicker 的分层与单一来源、Search 接口数据契约、配置字段(含 treeGroup 与多选暂存)、部门员工关联常量的取值与落库对应、el-tree 自绘场景的 useDeptEmployeeTree,以及新增/改动时必须遵守的约定与变更检查清单。
This commit is contained in:
TermiNexus committed 2026-10-01 12:37:15 +08:00
1 parent edd4a85f98
commit 3859aa1762
1 file changed
+165
+165
View File
@@ -0,0 +1,165 @@
# GERP 实体选择器(EntityPicker)标准 — 设计文档
> 适用范围:GERP 前端所有「选人 / 选部门 / 选职务 / 选应用实体」控件。
> 目的:**一个数据源一份配置**,禁止"功能相同、两套代码"(历史上出过:同一份
> 「部门员工关联」配置在后端写了两遍、批量职务弹窗又在前端自己分页拼了一次部门树)。
---
## 一、结论速览(先看这个)
| 场景 | 用什么 | 配置写在哪 |
|------|--------|-----------|
| 表单里选单个实体(列 `type='entityPicker'`) | `EntityPicker.vue` | 后端列定义 `form_service.py`(`ENTITY_PICKER_MAP` 或就地 `entityPicker` 字段) |
| 表单里选多个实体(列 `multiValue=true`,如职务) | `MultiEntityPicker.vue` | 同上 |
| 需要"按部门树 → 成员"两级浏览面板 | `EntityPicker` + `config.treeGroup` | 复用 `DEPT_TREE_GROUP`(后端) |
| 需要"部门员工关联"(成员 + 所属部门) | `DEPT_EMPLOYEE_PICKER` / `..._BY_USER_ROLE` | 后端 `form_service.py` 常量,勿复制 |
| 页面自绘 `el-tree` 勾选(如批量调整职务) | `composables/useDeptEmployeeTree.ts` | 前端 composable(内部调 `DeptEmployeeSearch`) |
| 页面里直接用 `EntityPicker` 组件(非列驱动,如菜单编辑器选公司) | `api/entityPickerPresets.ts` 的预设 | 前端预设,勿就地写 api/labelField/valueField |
---
## 二、分层与单一来源
| 层 | 位置 | 职责 |
|----|------|------|
| 渲染层 | `GERP/src/components/EntityPicker.vue` | 搜索下拉 + 分页加载更多 + 浏览面板 + `treeGroup` 两级面板 + 多选暂存 |
| 渲染层 | `GERP/src/components/MultiEntityPicker.vue` | 多值(标签 + 下拉 + 面板多选),支持 `treeGroup` |
| 承载层 | `GERP/src/components/CrudFormPanel.vue` | 按列定义渲染表单控件(含 `required` 校验、级联参数透传) |
| 承载层 | `GERP/src/components/CrudTable.vue` | 列表页新增/编辑;`picker-multi` → 批量新增 |
| 配置层(**权威**) | 后端 `gvsds/apps/gerp/form_service.py` | `ENTITY_PICKER_MAP`(通用实体预设)、`DEPT_TREE_GROUP`、`DEPT_EMPLOYEE_PICKER`、`DEPT_EMPLOYEE_PICKER_BY_USER_ROLE` |
| 配置层 | 前端 `GERP/src/api/entityPickerPresets.ts` | 页面直接使用组件时的 6 类实体预设 + `epColumn()` |
| 自绘树数据源 | `GERP/src/composables/useDeptEmployeeTree.ts` | `el-tree` 场景的"部门树 → 成员"组装(同源 `DeptEmployeeSearch`) |
| 后端数据源 | `gvsds/apps/gerp/views/form_api.py` | `DeptEmployeeSearchView` 等 Search 接口 |
**为什么配置放后端**:列定义由 `/API/FormAPI/Columns/<Table>` 下发,前端只负责渲染 ——
同一张表在列表页、详情页、弹窗里形态天然一致;改一处即全站生效。
---
## 三、数据契约(Search 接口)
### 3.1 响应结构
```jsonc
{ "code": 0, "data": {
"list": [ { /* 行 */ } ],
"has_more": true,
"tree": [ { "uuid": "…", "name": "…", "parentUuid": "…" } ] // 传 tree=1 时返回
} }
```
- 分页:`offset`(步长 20,前端"加载更多"/自动拉全)
- 关键字:`keyword`(同时匹配多个字段,见各接口 docstring)
- 公司过滤:`CompanyUUID`(显式传入强制该公司;不传则回退当前角色公司;无公司上下文返回空列表)
- 去重:`Dedup=UserRole`(`DeptEmployeeSearch` 专用:同一"用户-角色对"多部门只留一条)
### 3.2 接口清单
| 接口 | 值字段(valueField) | 备注 |
|------|---------------------|------|
| `/API/FormAPI/UserSearch` | `UserUUID` | 副文本 `UserEmail` |
| `/API/FormAPI/RoleSearch` | `RoleUUID` | 支持 `dependsOn=UserUUID` 级联 |
| `/API/FormAPI/CompanySearch` | `CompanyUUID` | |
| `/API/FormAPI/TenantSearch` | `TenantUUID` | |
| `/API/FormAPI/DepartmentSearch` | `DeptUUID` | |
| `/API/FormAPI/WorkgroupSearch` | `WorkgroupUUID` | |
| `/API/FormAPI/PermissionSearch` | `PermUUID` | 副文本 `PermCode` |
| `/API/FormAPI/PositionTemplateSearch` | `PositionTemplateUUID` | |
| `/API/FormAPI/PositionSearch` | `PositionUUID` | 带 `tree`(通用部门树),groupField `CategoryUUID` |
| `/API/FormAPI/PositionCategorySearch` | `PositionCategoryUUID` | |
| `/API/FormAPI/MenuSearch` | `MenuUUID` | |
| `/API/FormAPI/ShopSearch` | `ShopUUID` | 本地 7 库无表,预留 |
| `/API/FormAPI/UserRoleSearch` | `UserRoleUUID` | 用户-角色对;带 `tree`(部门树) |
| `/API/FormAPI/DeptEmployeeSearch` | `UserRoleDepartmentUUID` / `UserRoleUUID` | **部门员工关联**(部门 + 用户-角色对);带 `tree`、`DeptEmployeeLabel`、`RoleName`;支持 `Dedup=UserRole`、`DeptUUID` 过滤 |
---
## 四、配置字段(`EntityPickerConfig`)
定义见 `GERP/src/types/api.ts`。
| 字段 | 说明 |
|------|------|
| `api` / `method` | 搜索接口(默认 GET) |
| `labelField` / `valueField` | 显示字段 / **取值字段(必须等于落库列)** |
| `secondaryField` | 副文本字段(如 `RoleName`、`UserEmail`) |
| `placeholder` | 占位文案 |
| `fetchOnOpen` / `paged` | 打开即加载前 20 条 / 底部"加载更多"(默认均开启) |
| `searchParam` / `resultPath` | 关键字参数名(默认 `keyword`)/ 响应数据路径(默认 `data`) |
| `dependsOn` | 级联:请求时附带表单中该字段的当前值(如 `RoleUUID` 依赖 `UserUUID`) |
| `dependCompany` | 请求时附带表单 `CompanyUUID`(表单无该字段则不发送,后端回退当前角色公司) |
| `treeGroup` | 浏览面板改为"树分组 + 成员"两级结构,见下 |
### `treeGroup` 子字段
| 子字段 | 说明 |
|--------|------|
| `groupField` | 记录上的归组字段(如 `DeptUUID`);配置后前端自动附带 `tree=1` |
| `treeField` | 响应中树节点数组字段名(默认 `tree`) |
| `ungroupedTitle` | 未归组记录的兜底分组标题(默认"未分组") |
| `multiSelect` | 开启后浏览面板出现"多选"开关,成员进暂存区、底部"全部提交"批量返回(`submit-multi` → `CrudTable` 批量新增) |
| `roleTypeOptions` | 暂存区成员类型下拉(如 成员/负责人),值随 `submit-multi` 返回写入 `RoleType` |
---
## 五、「部门员工关联」选择器(单一来源示例)
后端常量(`gvsds/apps/gerp/form_service.py` 顶部,含完整注释):
| 常量 | valueField | 附加行为 | 使用点 |
|------|-----------|---------|--------|
| `DEPT_TREE_GROUP` | —(公共 treeGroup) | 部门树分组 + 未分配部门兜底 | 下列两处 + `UserRoleDepartment.UserRoleUUID` |
| `DEPT_EMPLOYEE_PICKER` | `UserRoleDepartmentUUID` | 单选,`secondaryField=RoleName` | `EmployeeProfile.UserRoleDepartmentUUID`(eHRM 员工档案) |
| `DEPT_EMPLOYEE_PICKER_BY_USER_ROLE` | `UserRoleUUID` | `Dedup=UserRole` + 多选暂存 + 成员/负责人 | `UserRoleWorkgroup.UserRoleUUID`(工作组新增成员) |
**取值与落库的对应关系**(改取值列必须同步后端解析):
- `UserRoleUUID` → 直接落 `UserRoleWorkgroup.UserRoleUUID`(关系表只存用户-角色对,**不含部门**;部门仅用于选择器分组展示)
- `UserRoleDepartmentUUID` → 由 `FormService._ResolveDeptEmployee()` 解析出
`UserUUID` / `DeptUUID` / `DeptName` 再落 `EmployeeProfile`
**唯一性约束**:员工档案一人一份 —— 新增与改绑均由 `form_service.add_record/modify_record` 拦截
(前端在已有档案时隐藏新增入口)。
---
## 六、`el-tree` 自绘场景
批量调整职务弹窗(`components/BatchPositionDialog.vue`)需要"树 + 勾选",无法直接复用下拉形态的
`EntityPicker`,因此抽 `composables/useDeptEmployeeTree.ts`:
- 数据源与 `EntityPicker(treeGroup)` **同源**(`/API/FormAPI/DeptEmployeeSearch`)
- 分页拉全 → 用服务端 `tree` 组装 `el-tree` 数据 → 成员按 `DeptUUID` 归组,未挂部门进"未分组"
- 单部门模式(传 `DeptUUID`,部门详情页)返回单个分组节点
- 返回 `{ tree, groupIds }`:`groupIds` 用于提交时过滤掉分组节点,只提交成员 PK
- 成员叶子取值列由 `ValueField` 指定(公司/部门列表页 `UserRoleUUID`;部门详情页 `UserRoleDepartmentUUID`)
对应提交表:`UserRole`(公司页)/ `UserRoleDepartment`(部门页)/ `UserRoleWorkgroup`(工作组页),
统一走 `formApi.batchPositions()` → 后端 `FormService.batch_positions`。
---
## 七、约定(必须遵守)
1. **禁止就地复制 `entityPicker` 配置**:能命中 `ENTITY_PICKER_MAP` 或上述部门常量就复用;
确有新形态时,先把配置抽成常量(后端)或预设(前端),再在两个以上使用点引用。
2. **`valueField` 必须等于落库列**;改取值列要同步后端解析/校验逻辑与唯一性约束。
3. **"部门树分组"一律走 `DeptEmployeeSearch` + `DEPT_TREE_GROUP`**,不要另拉 `UserRoleSearch`
在前端自己拼树(`UserRoleSearch` 仅用于"用户-角色对"场景)。
4. **`el-tree` 自绘场景用 `useDeptEmployeeTree`**,不要自己写分页 + 拼树 + 标签组装。
5. **前端预设 `entityPickerPresets.ts` 只在页面直接使用 `EntityPicker` 组件时使用**;
列驱动场景一律由后端下发配置。
6. 改动任何一处共享配置后,**先 grep 使用点做回归**,再以 `GERP\deploy.ps1`
(`vue-tsc -b` + `vite build`)与 `python manage.py check` 作为验收。
---
## 八、变更检查清单
- [ ] 新配置是否复用了既有常量/预设?没有 → 是否应该抽成常量?
- [ ] `valueField` 与落库列一致?后端解析/唯一性约束已同步?
- [ ] `treeGroup` 是否用了公共 `DEPT_TREE_GROUP`?数据源是否 `DeptEmployeeSearch`?
- [ ] 自绘树是否走了 `useDeptEmployeeTree`?
- [ ] `grep -rn "DeptEmployeeSearch\|treeGroup\|entityPicker:" GERP/src gvsds/apps/gerp` 是否出现新的重复定义?
- [ ] 前端 `deploy.ps1` 全绿;后端 `manage.py check` 0 issue;涉及 Python 改动记得重启服务