Files
SMANAGE/docs/entity_picker_standard.md
TermiNexus fd56a9e4ff feat(gerp): 部门新增成员改用部门员工关联选择器
UserRoleDepartment 表单人员输入统一为 DEPT_EMPLOYEE_PICKER(与 eHRM/工作组同源单一配置):新增虚拟列置顶,用户/角色列改为纯列表展示,DeptUUID 表单隐藏(上下文优先,否则取选择器解析的部门),删除就地写的 UserRoleSearch 选择器;add_record/modify_record 支持 UserRoleDepartmentUUID 解析 UserRoleUUID 并拒绝选到别的员工。
2026-10-01 14:16:18 +08:00

178 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` / `DEPT_EMPLOYEE_PICKER_MULTI` | 后端 `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_MULTI` |
| 配置层 | 前端 `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) | 部门树分组 + 未分配部门兜底 | 被下面两个常量引用(勿单独就地使用) |
| `DEPT_EMPLOYEE_PICKER` | `UserRoleDepartmentUUID` | 单选 | `EmployeeProfile.UserRoleDepartmentUUID`(eHRM 员工档案)、`UserRoleDepartment.UserRoleDepartmentUUID`(部门新增成员,虚拟列) |
| `DEPT_EMPLOYEE_PICKER_MULTI` | `UserRoleDepartmentUUID` | 多选暂存 + 成员/负责人类型 | `UserRoleWorkgroup.UserRoleDepartmentUUID`(工作组新增成员,虚拟列) |
**三个使用点取值一致**(都是「部门员工关联」= 部门 + 该用户在本公司的用户-角色对),
区别只在单/多选;落库时由 `FormService._ResolveDeptEmployee()` **统一解析**(唯一实现):
- `EmployeeProfile`:取 `UserUUID` / `DeptUUID` / `DeptName` 三列落库(常量 `_PROFILE_DEPT_EMPLOYEE_KEYS` 声明)
- `UserRoleWorkgroup`:取 `UserRoleUUID` 落库到关系列(关系表只存用户-角色对,**部门仅用于选择/分组展示**);
表单里该关系列 `hideInForm`,由虚拟列 `UserRoleDepartmentUUID` 作为唯一人员选择入口
- `UserRoleDepartment`:取 `UserRoleUUID` 落库;`DeptUUID` **表单上下文优先**(部门详情页 = 当前部门),
无上下文时取选择器解析出的部门(员工详情页新增"部门职务"即此场景);
员工详情页上下文里 `UserUUID` 由 query 注入,选择器选到别的员工会被后端拒绝
(`所选「部门员工关联」不属于当前员工`)。两张关联表的 `UserUUID`/`RoleUUID` 列一律
`editable=False`(纯列表展示),人员输入只有选择器一个入口
> 已知取舍:选择器数据源是 `UserRoleDepartment`,因此**尚未挂靠任何部门的"用户-角色对"
> 选不到**。公司员工列表(`UserRole`)新增/编辑时若填了部门会同步建主关联,正常路径不受影响。
**唯一性约束**:员工档案一人一份 —— 新增与改绑均由 `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` 作为验收。
7. **浏览面板标题用 `browseTitle`**(`EntityPicker` / `MultiEntityPicker` 各有一份):占位文案
已以「请选择 / 选择」开头时不再加前缀,避免出现"选择选择部门员工关联…"。面板统一
`width="680px"`、列表 `min-height: 420px / max-height: 560px`,两组件保持一致。
---
## 八、变更检查清单
- [ ] 新配置是否复用了既有常量/预设?没有 → 是否应该抽成常量?
- [ ] `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 改动记得重启服务