| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512 |
- /**
- * 机构 → 部门 → 岗位序列 → 岗位名称 → 岗位性质(自动带出) → 岗位层级 级联选择(移动端版)
- * 与 PC 端 src/views/hrManagement/mixins/positionCascade.js 同源,接口一致:
- * GET /main/group/getGroupList 机构 + 部门(前端拆树)
- * GET /fm/tmaindepartmentposition/sequence/list 部门 → 岗位序列
- * GET /fm/tmaindepartmentposition/position/list 部门 + 序列 → 岗位
- * GET /fm/tmaindepartmentposition/positionType/list → 岗位性质(取首条)
- * GET /fm/tmaindepartmentposition/positionLevel/list → 岗位层级
- *
- * 用法:
- * 1) 页面 mixins: [positionCascadeMixin],表单对象必须是 this.form(与 PC 的 cascadeForm 对齐);
- * 2) 模板里用统一选择器 <hr-picker-row>(components/hr/HrPickerRow.vue)绑定本混入的
- * onCascadeLegalChange / onCascadeDeptPick / onCascadeSequenceChange /
- * onCascadePositionChange / onCascadeLevelChange(@change 直接传这些方法即可);
- * 部门行用 mode="tree" + :tree="cascadeDeptTree";
- * 3) 选人 / 详情回显后调用 await this.primeCascade() 预热各级下拉,保证名称能回显。
- *
- * 字段映射与 PC 的 CASCADE_CONFIG 一致(游离在 mixin 内,避免每个页面重复声明):
- * groupId / company、deptId / department、positionSequenceId / positionSequence、
- * positionId / position、positionType / positionTypeName、positionLevelId / positionLevel
- */
- import { listOrganizations } from "@/api/hr/organization";
- import {
- getDeptPositionSequences,
- getDeptPositions,
- getDeptPositionType,
- getDeptPositionLevels,
- } from "@/api/hr/position";
- import { buildDeptTreeUnderLegal, LEGAL_TYPES } from "@/utils/hr/organization-tree";
- import { getOrgOptionsWithFallback } from "@/utils/hr/current-org";
- import { getDictByCode } from "@/api/common";
- import { dictToPositionNatureOptions } from "@/api/hr/user";
- /** 级联层级顺序:清空时按此后缀裁剪 */
- const CASCADE_ORDER = ["dept", "sequence", "position", "type", "level"];
- /** 表单字段映射(与后端 PO 字段名一致) */
- export const CASCADE_FIELDS = {
- legalId: "groupId",
- legalName: "company",
- deptId: "deptId",
- deptName: "department",
- sequenceId: "positionSequenceId",
- sequenceName: "positionSequence",
- positionId: "positionId",
- positionName: "position",
- typeId: "positionType",
- typeName: "positionTypeName",
- levelId: "positionLevelId",
- levelName: "positionLevel",
- };
- /** 把扁平机构/部门列表拍平成「路径名」,便于在 actionSheet 里区分同名部门 */
- function flattenDepartments(nodes = [], parentPath = "") {
- const result = [];
- nodes.forEach((node) => {
- const path = parentPath ? `${parentPath} / ${node.name}` : node.name;
- // 法人节点只作为路径前缀,不作为可选的部门
- if (!LEGAL_TYPES.has(String(node.type))) result.push({ id: node.id, name: node.name, path });
- result.push(...flattenDepartments(node.children || [], path));
- });
- return result;
- }
- /** 级联接口返回可能是数组 / {list|records|rows|data},统一取数组 */
- function cascadeList(payload) {
- if (Array.isArray(payload)) return payload.filter(Boolean);
- const list = payload && (payload.list || payload.records || payload.rows || payload.data);
- return Array.isArray(list) ? list.filter(Boolean) : [];
- }
- /** 选项里找不到 id 时,把当前表单值的名称塞回列表,保证 actionSheet / 回显可用 */
- function ensureOption(list, id, name) {
- if (id == null || id === "") return list;
- if (list.some((item) => String(item.id) === String(id))) return list;
- return list.concat([{ id: String(id), name: String(name || id), path: String(name || id) }]);
- }
- export const positionCascadeMixin = {
- data() {
- return {
- /** 组织架构原始列表(含 parentId / type) */
- orgList: [],
- /** 机构(顶层组织)下拉 */
- legalOptions: [],
- /** 当前机构下的部门下拉(含路径名) */
- deptOptions: [],
- /** 岗位序列下拉 */
- sequenceOptions: [],
- sequenceLoading: false,
- /** 岗位下拉 */
- cascadePositionOptions: [],
- positionLoading: false,
- /** 岗位层级下拉 */
- levelOptions: [],
- levelLoading: false,
- /** 接口返回的岗位性质编码:查询岗位层级时需原样回传 */
- cascadeRawType: "",
- /** 岗位性质字典(编码 → 名称),接口不给 positionTypeName 时兜底显示 */
- positionNatureOptions: [],
- };
- },
- computed: {
- /** 级联字段所在的表单对象(离职申请 / 转正办理都是 this.form) */
- cascadeForm() {
- return this.form || {};
- },
- /**
- * 部门树(树选择用):与个人档案页同口径 ——
- * 取所选机构节点下的 children 作为根,公司层不再入列。
- */
- cascadeDeptTree() {
- const f = CASCADE_FIELDS;
- const legalId = this.cascadeForm[f.legalId];
- if (!legalId) return [];
- const nodes = buildDeptTreeUnderLegal(this.orgList, legalId);
- const legal = nodes.find((node) => String(node.id) === String(legalId)) || nodes[0];
- return legal && Array.isArray(legal.children) ? legal.children : [];
- },
- },
- methods: {
- /** 机构 + 部门基础数据 */
- async loadCascadeOrganizations() {
- try {
- // 组织树取全量:部门级联要按当前法人从中过滤出部门
- const list = await listOrganizations({}, false);
- this.orgList = Array.isArray(list) ? list : [];
- } catch (error) {
- this.orgList = [];
- }
- // 所属机构候选项与 PC 一致(EmployeeArchiveForm.legalEntities):
- // 主源是登录缓存 loginChangeGroupVOList(当前账号所属公司),不再列全量机构树;
- // 缓存缺名称时回落组织树兜底,编辑老数据时补当前项,避免下拉只显示 ID。
- const f = CASCADE_FIELDS;
- this.legalOptions = getOrgOptionsWithFallback(
- this.orgList,
- this.cascadeForm[f.legalId],
- this.cascadeForm[f.legalName]
- );
- this.rebuildDeptOptions();
- },
- /** 按当前机构重建部门下拉 */
- rebuildDeptOptions() {
- const f = CASCADE_FIELDS;
- const legalId = this.cascadeForm[f.legalId];
- if (legalId == null || legalId === "") {
- this.deptOptions = [];
- return;
- }
- const nodes = buildDeptTreeUnderLegal(this.orgList, legalId);
- this.deptOptions = ensureOption(flattenDepartments(nodes), this.cascadeForm[f.deptId], this.cascadeForm[f.deptName]);
- },
- /** 按当前下拉选项取名称 */
- cascadeNameOf(listName, id) {
- const hit = (this[listName] || []).find((item) => String(item.id) === String(id));
- return hit ? hit.name : "";
- },
- /** 按名称反查主键(回显兜底用) */
- cascadeIdByName(listName, name) {
- const text = String(name || "").trim();
- if (!text) return "";
- const hit = (this[listName] || []).find((item) => String(item.name).trim() === text);
- return hit ? hit.id : "";
- },
- /** 清空指定层级之后的字段与下拉(level: dept/sequence/position) */
- clearCascadeBelow(level) {
- const f = CASCADE_FIELDS;
- const form = this.cascadeForm;
- const rest = CASCADE_ORDER.slice(CASCADE_ORDER.indexOf(level) + 1);
- if (rest.indexOf("sequence") >= 0) {
- form[f.sequenceId] = "";
- form[f.sequenceName] = "";
- this.sequenceOptions = [];
- }
- if (rest.indexOf("position") >= 0) {
- form[f.positionId] = null;
- form[f.positionName] = "";
- this.cascadePositionOptions = [];
- }
- if (rest.indexOf("type") >= 0) {
- form[f.typeId] = "";
- form[f.typeName] = "";
- this.cascadeRawType = "";
- }
- if (rest.indexOf("level") >= 0) {
- form[f.levelId] = "";
- form[f.levelName] = "";
- this.levelOptions = [];
- }
- },
- /** 新增表单默认带出所属机构:登录机构列表第一个 */
- applyDefaultCascadeOrg() {
- const f = CASCADE_FIELDS;
- const form = this.cascadeForm;
- if (form[f.legalId] != null && form[f.legalId] !== "") return;
- const first = (this.legalOptions || [])[0];
- if (!first) return;
- form[f.legalId] = first.id;
- form[f.legalName] = first.name || "";
- this.rebuildDeptOptions();
- },
- /**
- * 新建态页面初始化:先把机构 / 部门候选加载出来并带出默认机构。
- * 背景:loadCascadeOrganizations 原先只在 primeCascade 里被调用,而 primeCascade
- * 只在「选人 / 详情回显」之后触发 —— 从菜单直达发起页时机构下拉会是空的,用户以为「选不出来」。
- * 与 PC 端 created(){ loadOrganizations(); applyDefaultCascadeOrg(); } 对齐。
- */
- async initCascadeOrg() {
- if (!this.orgList.length) await this.loadCascadeOrganizations();
- this.applyDefaultCascadeOrg();
- this.rebuildDeptOptions();
- },
- /* ================= 岗位性质字典(名称兜底显示) =================
- * 接口只回编码、不回 positionTypeName 时,只读框会显示成 "1"。
- * 与 PC 一致地接 /system/dict/getByCode/position_nature 反查文案。
- * 注意:只用于「显示名」,不写回 form.positionType(提交值仍用接口原编码,见下方注释)。
- */
- async loadPositionNatureDict() {
- if ((this.positionNatureOptions || []).length) return this.positionNatureOptions;
- try {
- const dict = await getDictByCode("position_nature");
- this.positionNatureOptions = dictToPositionNatureOptions(dict);
- } catch (error) {
- this.positionNatureOptions = [];
- }
- return this.positionNatureOptions;
- },
- /** 岗位性质编码 → 字典文案;查不到返回空串 */
- positionNatureLabelOf(code) {
- const key = code == null ? "" : String(code).trim();
- if (!key) return "";
- const hit = (this.positionNatureOptions || []).find(
- (item) => String(item.value).trim() === key
- );
- return hit && hit.label ? String(hit.label).trim() : "";
- },
- /* ================= 选择行回调 =================
- * 统一选择器(components/hr/HrPickerRow)的 @change 回传 (value, { label, value }),
- * 历史 actionSheet 回传的是内部选项对象 { id, name } —— 这里统一成内部形态,
- * 所以回调签名必须是 (value, picked),只接一个参数会把「值」当成选项对象、名称丢失。
- */
- /**
- * 把选择器回传项归一化为 { id, name }。
- * 兼容三种入参:① 选项对象 { label, value }(HrPickerRow 的第二参)
- * ② 内部历史形态 { id, name } ③ 纯值(主键/编码)。
- */
- cascadePicked(picked) {
- if (picked == null) return { id: "", name: "" };
- if (typeof picked !== "object") return { id: picked, name: "" };
- return {
- id: picked.value != null ? picked.value : picked.id,
- name: picked.label != null ? picked.label : picked.name || "",
- };
- },
- /**
- * 所属机构
- * 注意签名:HrPickerRow 的 @change 回传 (value, option),
- * 所以第一参是值、第二参才是带名称的选项对象;只接一个参数会拿不到名称。
- */
- onCascadeLegalChange(value, picked) {
- const legal = this.cascadePicked(picked || value);
- const f = CASCADE_FIELDS;
- const form = this.cascadeForm;
- form[f.legalId] = legal.id;
- form[f.legalName] = legal.name || "";
- form[f.deptId] = "";
- form[f.deptName] = "";
- this.clearCascadeBelow("dept");
- this.rebuildDeptOptions();
- },
- /** 所属部门(树选择器回传节点 id,这里补出节点名与路径) */
- onCascadeDeptPick(deptId, picked) {
- void picked;
- const hit = flattenDepartments(this.cascadeDeptTree).find(
- (item) => String(item.id) === String(deptId)
- );
- return this.onCascadeDeptChange(hit || { id: deptId, name: "" });
- },
- async onCascadeDeptChange(value, picked) {
- const dept = this.cascadePicked(picked || value);
- const f = CASCADE_FIELDS;
- const form = this.cascadeForm;
- form[f.deptId] = dept.id;
- form[f.deptName] = dept.name || "";
- this.clearCascadeBelow("dept");
- await this.loadCascadeSequences(dept.id);
- },
- /** 部门 → 岗位序列下拉 */
- async loadCascadeSequences(deptId) {
- const f = CASCADE_FIELDS;
- const current = deptId != null && deptId !== "" ? deptId : this.cascadeForm[f.deptId];
- if (current == null || current === "") {
- this.sequenceOptions = [];
- return [];
- }
- this.sequenceLoading = true;
- try {
- const data = await getDeptPositionSequences(current, false);
- this.sequenceOptions = cascadeList(data)
- .filter((item) => item && item.positionSequenceId != null)
- .map((item) => ({
- id: String(item.positionSequenceId),
- name: item.positionSequenceName || item.positionSequence || "",
- }));
- } catch (error) {
- this.sequenceOptions = [];
- } finally {
- this.sequenceLoading = false;
- }
- this.sequenceOptions = ensureOption(
- this.sequenceOptions,
- this.cascadeForm[f.sequenceId],
- this.cascadeForm[f.sequenceName]
- );
- return this.sequenceOptions;
- },
- /** 岗位序列 */
- async onCascadeSequenceChange(value, picked) {
- const sequence = this.cascadePicked(picked || value);
- const f = CASCADE_FIELDS;
- const form = this.cascadeForm;
- form[f.sequenceId] = sequence.id;
- form[f.sequenceName] = sequence.name || "";
- this.clearCascadeBelow("sequence");
- await this.loadCascadePositions();
- },
- /** 拉取「部门 + 序列」下的岗位(纯查询) */
- async fetchCascadePositions(deptId, positionSequenceId) {
- if (!deptId || !positionSequenceId) return [];
- try {
- const data = await getDeptPositions({ deptId, positionSequenceId }, false);
- return cascadeList(data)
- .filter((item) => item && item.positionId != null)
- .map((item) => ({ id: String(item.positionId), name: item.positionName || "" }));
- } catch (error) {
- return [];
- }
- },
- /** 部门 + 序列 → 岗位下拉 */
- async loadCascadePositions() {
- const f = CASCADE_FIELDS;
- const form = this.cascadeForm;
- const deptId = form[f.deptId];
- const positionSequenceId = form[f.sequenceId];
- if (!deptId || !positionSequenceId) {
- this.cascadePositionOptions = [];
- return [];
- }
- this.positionLoading = true;
- try {
- this.cascadePositionOptions = await this.fetchCascadePositions(deptId, positionSequenceId);
- } finally {
- this.positionLoading = false;
- }
- this.cascadePositionOptions = ensureOption(
- this.cascadePositionOptions,
- this.cascadeForm[f.positionId],
- this.cascadeForm[f.positionName]
- );
- return this.cascadePositionOptions;
- },
- /** 岗位名称 */
- async onCascadePositionChange(value, picked) {
- const position = this.cascadePicked(picked || value);
- const f = CASCADE_FIELDS;
- const form = this.cascadeForm;
- form[f.positionId] = position.id;
- form[f.positionName] = position.name || "";
- this.clearCascadeBelow("position");
- await this.loadCascadeTypeAndLevels();
- },
- /** 部门 + 序列 + 岗位 → 岗位性质(取首条)+ 岗位层级下拉 */
- async loadCascadeTypeAndLevels() {
- const f = CASCADE_FIELDS;
- const form = this.cascadeForm;
- const deptId = form[f.deptId];
- const positionSequenceId = form[f.sequenceId];
- const positionId = form[f.positionId];
- if (!deptId || !positionSequenceId || !positionId) return;
- try {
- const data = await getDeptPositionType({ deptId, positionId, positionSequenceId }, false);
- const first = cascadeList(data)[0] || null;
- const rawType = String((first && first.positionType) || "");
- // 名称:接口 positionTypeName 优先,取不到时用岗位性质字典按编码反查(对齐 PC),
- // 不能拿编码当名称兜底,否则只读框会显示成 "1"
- let typeName = String((first && first.positionTypeName) || "").trim();
- if (!typeName && rawType) {
- await this.loadPositionNatureDict();
- typeName = this.positionNatureLabelOf(rawType);
- }
- this.cascadeRawType = rawType;
- if (typeName) form[f.typeName] = typeName;
- // 编码字段只存编码(对齐 PC:positionType = first.positionType)。
- // 之前误把名称写进 positionType,提交时报「岗位快照与员工当前岗位关系不一致:
- // 员工当前岗位关系为【1】,转正提交为【全职】」
- if (rawType) form[f.typeId] = rawType;
- } catch (error) {
- // 级联接口异常:保留已带出的值
- }
- this.levelLoading = true;
- try {
- const data = await getDeptPositionLevels(
- { deptId, positionId, positionSequenceId, positionType: this.cascadeRawType },
- false
- );
- this.levelOptions = cascadeList(data)
- .filter((item) => item && item.positionLevelId != null)
- .map((item) => ({ id: String(item.positionLevelId), name: item.positionLevel || "" }));
- } catch (error) {
- this.levelOptions = [];
- } finally {
- this.levelLoading = false;
- }
- this.levelOptions = ensureOption(this.levelOptions, form[f.levelId], form[f.levelName]);
- // 层级唯一时自动带出,减少一次点击
- if (!form[f.levelId] && this.levelOptions.length === 1) {
- form[f.levelId] = this.levelOptions[0].id;
- form[f.levelName] = this.levelOptions[0].name || "";
- }
- },
- /** 岗位层级 */
- onCascadeLevelChange(value, picked) {
- const level = this.cascadePicked(picked || value);
- const f = CASCADE_FIELDS;
- const form = this.cascadeForm;
- form[f.levelId] = level.id;
- form[f.levelName] = level.name || "";
- },
- /**
- * 回显兜底:员工档案只带出岗位主键(或名称)而序列为空时,
- * 遍历当前部门下所有岗位序列定位,补齐序列与岗位下拉,避免级联链断裂。
- */
- async locateCascadePosition() {
- const f = CASCADE_FIELDS;
- const form = this.cascadeForm;
- const deptId = form[f.deptId];
- const targetId = form[f.positionId];
- const targetName = String(form[f.positionName] || "").trim();
- if (!deptId || (!targetId && !targetName)) return false;
- if (!this.sequenceOptions.length) await this.loadCascadeSequences(deptId);
- const sequences = this.sequenceOptions.slice();
- if (!sequences.length) return false;
- const isIdText = /^\d{15,}$/.test(targetName);
- for (let i = 0; i < sequences.length; i += 1) {
- const sequence = sequences[i];
- const options = await this.fetchCascadePositions(deptId, sequence.id);
- const hit = options.find((item) =>
- targetId ? String(item.id) === String(targetId) : !isIdText && String(item.name).trim() === targetName
- );
- if (!hit) continue;
- form[f.sequenceId] = sequence.id;
- form[f.sequenceName] = sequence.name || "";
- form[f.positionId] = hit.id;
- form[f.positionName] = hit.name || "";
- this.sequenceOptions = sequences;
- this.cascadePositionOptions = options;
- return true;
- }
- return false;
- },
- /** 选人 / 回显后逐级预热下拉(部门 → 序列 → 岗位 → 层级) */
- async primeCascade() {
- const f = CASCADE_FIELDS;
- const form = this.cascadeForm;
- if (!this.orgList.length) await this.loadCascadeOrganizations();
- // 机构:只有名称时按名称反查
- if (!form[f.legalId]) {
- form[f.legalId] = this.cascadeIdByName("legalOptions", form[f.legalName]);
- }
- this.rebuildDeptOptions();
- if (!form[f.deptId]) {
- form[f.deptId] = this.cascadeIdByName("deptOptions", form[f.deptName]);
- }
- if (!form[f.deptId]) return;
- await this.loadCascadeSequences(form[f.deptId]);
- if (!form[f.sequenceId]) {
- form[f.sequenceId] = this.cascadeIdByName("sequenceOptions", form[f.sequenceName]);
- }
- if (!form[f.sequenceId] && (form[f.positionId] || form[f.positionName])) {
- await this.locateCascadePosition();
- }
- if (!form[f.sequenceId]) {
- this.cascadePositionOptions = ensureOption(
- this.cascadePositionOptions,
- form[f.positionId],
- form[f.positionName]
- );
- return;
- }
- await this.loadCascadePositions();
- if (
- form[f.positionId] &&
- !this.cascadePositionOptions.some((item) => String(item.id) === String(form[f.positionId]))
- ) {
- await this.locateCascadePosition();
- }
- if (!form[f.positionId]) {
- form[f.positionId] = this.cascadeIdByName("cascadePositionOptions", form[f.positionName]);
- }
- if (!form[f.positionId] && form[f.positionName]) {
- await this.locateCascadePosition();
- }
- if (!form[f.positionId]) return;
- await this.loadCascadeTypeAndLevels();
- if (!form[f.levelId]) {
- form[f.levelId] = this.cascadeIdByName("levelOptions", form[f.levelName]);
- }
- },
- },
- };
- export default positionCascadeMixin;
|