# 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/` 下发,前端只负责渲染 —— 同一张表在列表页、详情页、弹窗里形态天然一致;改一处即全站生效。 --- ## 三、数据契约(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 改动记得重启服务