# 2026-09-15
## 合同管理:发起/变更/续签弹框区块调整(图片需求,多轮完成)
- 需求:①员工信息卡挪到「合同基础信息」模块下面;②标题按模式切换:`createBaseSectionTitle` = 变更合同基础信息 / 续签合同基础信息 / 合同基础信息(computed,模板引用);③变更/续签时基础信息可修改:所属机构/所属部门/岗位名称由 `disabled` 改为 `:disabled="isCreateMode"` + `v-model`(发起模式仍禁用自动带出,即"现有禁用不变");④三者必填带星号:`prop="legalEntityName/departmentName/positionName"` + `createRules` required(blur 触发)。
- 提交链路:`buildCreatePayload` 里 `departmentName = form.departmentName || emp.department`、`legalEntityName = form.legalEntityName || emp.legalEntity`(表单优先);变更模式 `changedValues` 含 legalEntity/department/position 的 id+name 六项;续签 `renewContract(id, { contract: {...payload} })` 全量展开。`fillCreateFormFromContract` 回填 detail 的 id/name,员工档案兜底"为空才补"。
- 员工信息卡最终放置:基础信息区块之后、对比区(`v-if="isCompareMode"`)之前,class `cm-create-employee-card cm-employee-below`;scss 新增 `.cm-employee-below{margin:14px 22px 0}` 与 `.cm-employee-item{margin-left:24px}`(注意要放 `.cm-detail-page` 规则之前,避免覆盖详情页卡片的 margin)。
- 弹框字段最终顺序:签订员工 / 合同编号 / 合同类型 / 拟定薪资 / 所属机构 / 所属部门 / 岗位名称 / 合同起始日期 / 签署方式 / 合同终止日期 / 合同模版(线上)或合同附件(线下 is-span-2)。
## 解除/终止劳动合同弹框 UI 精简
- 去掉弹框内部标题栏(圈出来的大标题)和底部提示行(圈出来的 footer info)— 用户用图片明确圈出这两处。
- 原 header 里的员工/合同编号信息改为独立的 `.cm-terminate-info` 信息条,位于表单区上方:label「员工姓名」「合同编号」,值用更大更粗的字体,两者间距 48px。
- 新增样式 `.cm-terminate-info{padding:18px 22px 0;display:flex;gap:48px;align-items:center}`、label 15px #7e8b9d、b 17px #243955 font-weight:600。
## 合同详情弹框 UI 精简
- 按图片圈出位置,移除合同详情弹框底部 footer-info(合同编号 / 状态提示),仅保留「关闭」按钮。
- 校验:lint 干净。
## 解除弹框:签署方式联动离职证明模板/附件
- 签署方式=线上:下拉「离职证明模板」(prop `templateId`,选项复用 `contractTemplates`,选择后 `onTerminateTemplateChange` 解析 templateVersionId);必填。
- 签署方式=线下:单个上传入口「离职证明附件」(prop `offlineFileIds`);必填。
- 移除原「线下协议/解除依据文件」+「解除依据附件」双上传及 `evidenceFileIds` 字段。
- 提交 payload:ONLINE 传 templateId/templateVersionId,OFFLINE 传 offlineFileId。
- 校验:模板编译 0 error、lint 干净。
## 解除弹框顶部信息条样式对齐合同详情
- 将原 `.cm-terminate-info` 改为复用 `.cm-create-employee-card` 员工信息卡:左侧头像、员工姓名、合同编号,外观与合同详情 `.cm-detail-employee` 一致。
- 新增 `.cm-terminate-employee{margin:16px 22px 0;padding:14px 18px}`,移除旧的信息条样式。
- 校验:模板编译 0 error、lint 干净。
## 解除弹框:模板/附件位置调整到生效日期下方
- 将「离职证明模板 / 离职证明附件」表单项从表单底部上移到「生效日期」之后、「最后工作日期」之前。
- 线上/线下两种场景均占满整行(`grid-column: span 2`)。
- 校验:模板编译 0 error、lint 干净。
## 解除弹框顶部员工信息卡间距调大
- 用户反馈「员工姓名」与「合同编号」挤在一起;新增 `.cm-terminate-employee .cm-employee-no{margin-left:48px}`,把两个标签间距拉开。
- 校验:lint 干净。
## 人员结构看板两页 vs 接口文档差异盘点(仅分析,未改代码)
- 目标模块:接口文档 hr 分组里 tag = **人力资源-人员结构报表**(Hr Personnel Structure Report Controller),共 **13 个 GET**,全部挂在 `/hr/personnel-structure-reports/` 下:
- `/dashboard/summary`(顶部指标:员工总量/在岗人数/在岗率/人才库人数/当年入职/当年离职)
- `/dashboard/structure-trend`(最近12个月在职/入职/离职/净增,仅对应"结构分析"趋势)
- `/dashboard/overall-distribution`(用工类型 + 岗位性质)
- `/dashboard/age-distribution`(年龄分段)
- `/dashboard/organization-distribution`(按部门聚合)
- `/dashboard/talent-structure`(岗位序列/岗位层级/职级)
- `/dashboard/quality-analysis`(学历 + 政治面貌;职称留空)
- `/analysis/summary`(在职/离职/累计入职 + 同比环比)
- `/analysis/{dimensionCode}`(11 个白名单维度:POSITION_LEVEL、WORK_LOCATION、POSITION_SEQUENCE、DEPARTMENT、GENDER、CONTRACT_TYPE、EDUCATION、AGE、TENURE、POSITION_TENURE、PROFESSIONAL_TITLE,职称返回空值占位)
- `/mobility-analysis`(当年入职/离职/当月净增 + 12月趋势)
- `/effectiveness-analysis`(在岗人数 + 司龄结构;人均产出/编制使用率明确 DATA_UNAVAILABLE)
- `/details`(分页明细,仅统计日在岗人员,支持 dimensionCode + dimensionValueCode)
- `/details/export`(导出,复用明细口径)
- 统一入参:`organizationId`/`departmentId`(int64) + `year`/`month`(int, 1-12) + `contractType`(string,字典 code)。
- 统一返回包装:`{code,message,data}`,业务体 = `人员结构报表响应`{dataStatus, reportMonth(yyyy-MM), statisticsDate(yyyy-MM-dd), message, kpis[], dimensions[], trends[], scopeDepartmentIds[]};`dimensions[].items[]` = `{valueCode,valueName,count,percentage}`;`kpis[]` = `{metricCode,metricName,value,unit,rateValue,monthOnMonthRate,yearOnYearRate,dataStatus,message}`。
- **前端现状(最大问题)**:`src/views/hrAnalysis/personnelStructureDashboard` 与 `personnelStructureAnalysisBoard` 两页 **100% 用本地 `./mock.js`,`src/api/hr/` 下没有任何 personnel-structure 封装,13 个接口一个都没接**。
- 典型错位:字段名(页面 `{name,value}` vs 接口 `{valueName,count}`;页面 `metrics[].key/label` vs 接口 `kpis[].metricCode/metricName`);筛选(页面月分是 `'2026-08'` 字符串、组织是假枚举/部门中文名,无 departmentId);页面B 4 个筛选只 `chart.resize()` 不取数;页面B「导出」是 `window.print()` 不是 `/details/export`;两页明细都是本地切片无分页(接口是 Pagination);页面B 明细列「招聘类型」接口无此字段;页面A「司龄」tab 用的是年龄数据;页面A「职称分析」「职业属性」(无任何维度编码)会展示假数据;页面A 用 `scaleRate` 按组织比例前端硬乘伪造数值;两页都没有 dataStatus/message 空值协议。
## 已按接口完成两页对接(本次实际改代码)
- 新增 `src/api/hr/personnelStructure.js`:13 个接口(`/hr/personnel-structure-reports/**`)+ `adaptDimensions/adaptMetrics/adaptTrends/adaptDetailRows/buildQuery/pickMetric` + `DATA_STATUS`/`DIMENSION` 常量 + `downloadBlob` 导出。
- 新增 `src/views/hrAnalysis/shared.js`:合同类型枚举、`buildOrganizationOptions/rootOrganizationOptions/departmentOptions`(基于 `listOrganizations` + `buildOrganizationTree`)、`formatCount/formatRate/trendTone/ratioWidth/niceMax/recentMonths/splitMonth/monthTick/tooltipStyle`。
- 重写 `personnelStructureDashboard/index.vue`(`psd-screen`):月份用 `el-date-picker type="month"` 取 `yyyy-MM`,另加组织/部门/合同类型三个筛选;6 指标卡走 `dashboard/summary`(按 metricCode→关键字→顺序三级匹配);趋势面板 3 个 tab 分别走 `structure-trend` / `mobility-analysis` / `effectiveness-analysis`(效能无趋势时回退接口返回的司龄结构柱状图);整体人员分布走 `overall-distribution`(按 `dimensions[]` 动态分组成行)、年龄/司龄走 `age-distribution` + `analysis/TENURE`、各单位走 `organization-distribution`、人才结构走 `talent-structure`(动态页签)、素质分析走 `quality-analysis`(动态页签,含职称空值占位);明细弹框走 `/details`,支持关键字、`dimensionCode`+`dimensionValueCode` 下钻、分页。
- 重写 `personnelStructureAnalysisBoard/index.vue`(`psa-board`):3 指标卡走 `analysis/summary`;11 个维度图表全部走 `analysis/{dimensionCode}`;新增「人员结构趋势」(`structure-trend`) 与「整体人员分布」(`overall-distribution`) 两个缺失面板,面板数 10→12,改为 4 列 × 3 行网格;「工作地点」因接口不提供经纬度,改为按 `valueName/count/percentage` 的排名条形列表(保留地图底纹);学历/职称合并成一个带页签的面板;明细弹框走 `/details`(分页 + 关键字 + 维度下钻),「导出」改为调用 `/details/export`(blob 下载),不再 `window.print()`。
- 删除两个页面的 `mock.js`;两页补上 `dataStatus/message` 空值协议(`--` + `title`)、不可用态占位、「统计日」显示(取 `statisticsDate`,不再前端拼 `-31`)。
- 顺手修掉两个自身 bug:① `buildQuery` 原本只输出 5 个 scope 字段,会把明细的 `pageNum/size/dimensionCode` 吃掉,已改成调用方 `compact({ ...buildQuery(filters), ... })`;② `buildQuery` 不识别 `'yyyy-MM'`,月份选择器传进来的值会被丢掉而回退当前自然月,已加正则兼容。
- 验证:`npx vue-cli-service build` 通过(45s),产物里有 `view-hrAnalysis-personnelStructure*` 的 js/css chunk。注意:构建生成了 `dist/`(已在 .gitignore 中,批量删除被安全策略拦截,未清理,保留无影响)。
## 隐藏职称相关模块
- 新增可开关的隐藏机制 `src/views/hrAnalysis/shared.js`:`HIDDEN_DIMENSION_CODES = [DIMENSION.PROFESSIONAL_TITLE]` + `isDimensionVisible(code)` + `filterVisibleGroups(groups)`。**职称功能上线后,把该数组置空(或移除 PROFESSIONAL_TITLE)即可恢复**,页面代码不用再动。
- 人员结构看板:`quality-analysis` 返回的 dimensions 先过 `filterVisibleGroups`,素质分析面板的页签自动少掉「职称」(剩学历/政治面貌)。
- 人员结构分析看板:`DIMENSION_CODES` 末尾 `.filter(isDimensionVisible)` —— 职称的请求也不再发起;`educationTabs` 由 data 改为 computed,只有一个维度时 `PanelTitle` 不再渲染页签(`tabs.length > 1` 才渲染),面板保持「学历分析」标题。
- 回归:`node tmp/check-blocks.js`(模板编译 + babel 解析)与 `npx vue-cli-service build` 均通过。
## 人员结构报表模块的接口缺口盘点(查过文档确认)
- 全 hr 分组 **没有任何 `enum` 定义**(`Swagger definitions` 里零个 enum),所以 `metricCode` 取值表根本不存在,`dimensionCode` 也只写在 description 文本里。
- 已确认的缺口要点(可直接拿去和后端对齐):
1. `metricCode` 无枚举 → 指标卡只能"编码猜测+名称关键字+返回顺序"兜底。
2. 无"可用统计月份/快照可用性"接口;人员结构报表文档只说"按该月末重算",而**编制看板**每个接口都写了"历史月份仅查询已生成快照",两者口径不一致,前端月份选择器只能硬生成 12 个月。
3. 无维度值字典接口 → `dimensionValueCode` 只能从图表返回里取,做不了筛选下拉。
4. 无"当前用户可统计范围(组织+部门)"接口,`organizationId/departmentId` 的范围只能靠报错;前端复用 main 分组 `/main/group/getGroupList`,两边 id 是否同一套需确认。
5. `contractType` 无字典接口(文档只说"由劳动合同模块维护"),前端硬编码 6 个 code。
6. `details` **同时有 `deptId`(机构id) 与 `departmentId`(统计部门主键)**,语义重叠、前端不知道该传哪个;且维度筛选只支持单个 `dimensionCode`+`dimensionValueCode`,做不了"部门+学历+年龄"组合筛选或维度×维度二次下钻。
7. `details` 只返回统计日在岗人员,但明细模型里有 `statusOnStatisticsDate`(在职/离职)→ 该字段实际恒为"在职"。
8. `/details/export` 的 200 响应 schema 是空的(只有 `{"description":"OK"}`)、`produces: ["*/*"]`,没有文件名/格式契约,也没有异步导出任务机制;入参里还混着 `pageNum/size/offset/sortName/orderBy`(导出应为全量,语义冲突)。
9. `structure-trend` 不支持 `dimensionCode` → 看不了"按部门/序列/学历的趋势"。
10. `talent-structure` 描述含"**职级**分布",但 `/analysis/{dimensionCode}` 的 11 个白名单维度里**没有职级编码** → 职级只能从 talent-structure 的 dimensions 拿。
11. `overall-distribution` 只描述"用工类型和岗位性质",**没给 dimensionCode/dimensionName 取值** → 只能动态渲染。
12. `effectiveness-analysis` 的人均产出/编制使用率固定 `DATA_UNAVAILABLE`,但**编制使用率在编制看板 `/hr/headcount/dashboard/summary` 有**(不过编制看板的 organizationId/departmentId 口径与人员结构报表是否一致需确认)。
13. `mobility-analysis` 与 `structure-trend` 的 `trends` 字段完全同构(month/onJobCount/onboardCount/leaveCount/netChangeCount),数据源重叠,两处易数字不一致。
14. `rateValue` 与 `value`/`unit` 的组合规则没写(比例型是否同时填 value);前端用 `rateValue ?? value` 容忍两种写法。
15. 未被利用的返回字段:`scopeDepartmentIds`("实际参与统计的部门主键集合")目前前端没用,可用来展示"本次统计覆盖 N 个部门"或校验所选部门是否真的参与统计。
## `fm/directory/createPersonal/{userId}` 调用逻辑梳理(问答,未改代码)
- 后端:fm 分组 `POST /fm/directory/createPersonal/{userId}`,summary「创建个人文档区」,path 参数 `userId`(integer, 必填),返回 `响应«R»`(泛型)。
- 前端封装:`src/api/organization/index.js` 的 `createPersonalDirectory(userId)`(空 id 直接返回 null 不发请求)+ `hasJobNumber(record)`(`record.jobNumber ?? record.user?.jobNumber` **有值即视为已入库归档**)+ `ensurePersonalDirectory(userId, record)`(**导出但全项目无调用,死代码**)。
- 唯一调用点:`src/views/hrManagement/onboardingHandling/index.vue`,入口是保存主流程 `persistOnboarding()` → `maybeCreatePersonalDirectory(merged, alreadyArchived)`。
- 语义:**"员工首次入库归档"这一刻才建个人文档区**。判定顺序:
1. 表单当前有工号 → `alreadyArchived=true` → 只记名单、不发请求;
2. 表单没工号但 `userIdBefore` 存在 → 再拉一次 `getOnboardingByUserId` 看详情有没有工号,有则回填 `editing.jobNumber` 并记名单(拉失败就"以保存前表单中的工号为准",不打断);
3. 保存(`updateOnboarding`/`createOnboarding`)→ `mergeSavedResult`(会把 `saved.jobNumber || editing.jobNumber` 带出来)→ 若 `alreadyArchived` 为 false 就**发请求建目录**(即"保存这一步刚拿到工号"= 首次归档,正是要建的时刻);成功后把 userId 记进 `personalDirectoryUserIds`。
- 幂等三层:组件内存名单(本次会话同一 userId 只请求一次)+ 保存前两次工号检查 + "有工号=已归档"业务约定;**失败只 `showRequestError('创建个人文档区失败')`,不 reject、不打断入职保存**。
- 遗留疑点(待与后端确认):接口文档没写是否幂等。若后端保存/详情都不回填 `jobNumber`,则刷新页面(内存名单丢失)后对同一条记录再次保存会**重复调用**该接口。
### 后续调整:清理死代码 + 跨会话幂等(已改)
- `src/api/organization/index.js`:删掉 `ensurePersonalDirectory`(导出但全项目零调用)。
- `src/views/hrManagement/onboardingHandling/index.vue`:新增 `PERSONAL_DIRECTORY_CACHE_KEY = "hr:onboarding:createdPersonalDirectory"` + `readCreatedDirectoryUsers()/writeCreatedDirectoryUsers()`(localStorage,最多保留 500 条、try/catch 兜底隐私模式);data 加 `createdDirectoryUsers`;新增 `rememberCreatedDirectoryUser()`(**仅创建成功才落盘**,失败不落盘以便下次重试);`maybeCreatePersonalDirectory` 增加"已成功创建过就不重复调"这一层判断。
- 判断顺序最终为:`!userId` → `existedJobNumber`(已归档,只记内存名单)→ 会话内存名单 → 持久化已建名单 → 调接口 → 成功后同时记两份。
- **没有**加"保存后仍无工号就不建"的判断:那会把"后端不回填 jobNumber"的场景变成永远不建目录(功能回归),因此只用持久化去重解决刷新后重复调用的问题。
## 合同管理:发起合同弹框补「岗位名称」禁用输入框
- 文件 `src/views/contractManagement/index.vue`("发起合同弹窗" `ele-modal`,`:visible.sync="createVisible"`,三态复用:发起/变更/续签)。
- 在「所属机构」之后插入 `