| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858 |
- <template>
- <view class="page-no-tab profile-edit">
- <uni-nav-bar
- fixed="true"
- statusBar="true"
- left-icon="back"
- title="个人档案"
- background-color="#ffffff"
- color="#1f2329"
- @clickLeft="goBack"
- ></uni-nav-bar>
- <!-- 概要卡片 -->
- <view class="profile-card card section">
- <view class="pc-row">
- <text class="pc-name">{{ form.name || "—" }}</text>
- <view v-if="form.employeeNo" class="job-number">工号 {{ form.employeeNo }}</view>
- </view>
- <view class="pc-main">{{ [form.positionName, form.deptName].filter(Boolean).join(" · ") || "暂无任职信息" }}</view>
- <text v-if="form.company" class="pc-sub">{{ form.company }}</text>
- </view>
- <!-- 模块切换 -->
- <scroll-view class="tabs" scroll-x :show-scrollbar="false">
- <view class="tabs-inner">
- <view
- v-for="tab in tabs"
- :key="tab.name"
- class="tab press"
- :class="{ active: activeTab === tab.name }"
- @click="activeTab = tab.name"
- >{{ tab.label }}</view>
- </view>
- </scroll-view>
- <view class="tab-body">
- <block v-for="group in activeGroups" :key="group.title">
- <view class="group-title">
- <view class="gt-left">
- <view class="gt-bar"></view>
- <text class="gt-label">{{ group.title }}</text>
- </view>
- <text v-if="group.list" class="gt-more press" @click="addItem(group)">+ {{ group.addText || "添加" }}</text>
- </view>
- <!-- 字段分组 -->
- <view v-if="group.fields" class="form-card card section">
- <block v-for="(field, i) in group.fields" :key="field.key">
- <view v-if="i > 0" class="divider"></view>
- <!-- 文本 / 数字 -->
- <view v-if="field.type === 'text' || field.type === 'number'" class="field">
- <text class="label" :class="{ required: field.required }">{{ field.label }}</text>
- <input
- class="value-input"
- :type="field.type === 'number' ? 'number' : 'text'"
- :maxlength="field.maxlength || 60"
- :placeholder="field.placeholder || ''"
- v-model="form[field.key]"
- />
- </view>
- <!-- 只读 -->
- <view v-else-if="field.type === 'readonly'" class="field">
- <text class="label">{{ field.label }}</text>
- <text class="value muted">{{ form[field.key] || "—" }}</text>
- </view>
- <!-- 直属上级:选人弹窗 -->
- <view v-else-if="field.type === 'enum' && field.source === 'manager'" class="field press" @click="openManagerPicker">
- <text class="label">{{ field.label }}</text>
- <text :class="form.manager ? 'value' : 'placeholder'">{{ form.manager || "请选择" }}</text>
- <text class="arrow">›</text>
- </view>
- <!-- 下拉选择 -->
- <picker
- v-else-if="field.type === 'enum'"
- mode="selector"
- :range="fieldRange(field)"
- range-key="label"
- :value="fieldIndex(field)"
- :disabled="enumDisabled(field)"
- @change="onFieldPick(field, $event)"
- >
- <view class="field press">
- <text class="label" :class="{ required: field.required }">{{ field.label }}</text>
- <text :class="fieldText(field) ? 'value' : 'placeholder'">{{ fieldText(field) || enumHint(field) || "请选择" }}</text>
- <text class="arrow">›</text>
- </view>
- </picker>
- <!-- 日期 -->
- <picker
- v-else-if="field.type === 'date'"
- mode="date"
- :value="form[field.key]"
- :start="field.start || '1940-01-01'"
- :end="field.end || '2099-12-31'"
- @change="onFieldDate(field, $event)"
- >
- <view class="field press">
- <text class="label" :class="{ required: field.required }">{{ field.label }}</text>
- <text :class="form[field.key] ? 'value' : 'placeholder'">{{ form[field.key] || "请选择" }}</text>
- <text class="arrow">›</text>
- </view>
- </picker>
- <!-- 隶属部门:树形选择(层级不限,公司层已在 deptTree 里去掉) -->
- <view v-else-if="field.type === 'dept'" class="field press" @click="openDeptPicker">
- <text class="label" :class="{ required: field.required }">{{ field.label }}</text>
- <text v-if="!form.groupId" class="placeholder">请先选择所属机构</text>
- <text v-else :class="form.deptName ? 'value' : 'placeholder'">{{ form.deptName || "请选择" }}</text>
- <text class="arrow">›</text>
- </view>
- <!-- 开关 -->
- <view v-else-if="field.type === 'switch'" class="field">
- <text class="label">{{ field.label }}</text>
- <view class="switch-wrap">
- <switch :checked="form[field.key]" color="#157a2c" @change="onFieldSwitch(field, $event)" />
- <text class="switch-text">{{ form[field.key] ? "启用" : "待岗" }}</text>
- </view>
- </view>
- <!-- 多行文本 -->
- <view v-else-if="field.type === 'textarea'" class="field field-block">
- <text class="label">{{ field.label }}</text>
- <textarea
- class="value-textarea"
- :maxlength="field.maxlength || 500"
- :placeholder="field.placeholder || ''"
- v-model="form[field.key]"
- />
- </view>
- <!-- 附件 -->
- <view v-else-if="field.type === 'file'" class="field field-block">
- <text class="label">{{ field.label }}</text>
- <view class="field-file"><fileMain v-model="form[field.key]" /></view>
- </view>
- </block>
- </view>
- <!-- 可编辑明细列表 -->
- <block v-if="group.list">
- <view v-for="(item, idx) in form[group.list]" :key="item.key || idx" class="list-item card section">
- <view class="li-head">
- <text class="li-title">{{ listItemTitle(group, item, idx) }}</text>
- <view class="li-actions">
- <text class="li-action press" @click="openItemEditor(group, item, idx)">编辑</text>
- <text class="li-action danger press" @click="removeItem(group, idx)">删除</text>
- </view>
- </view>
- <view v-for="(line, li) in listItemLines(group, item)" :key="li" class="li-line">{{ line }}</view>
- </view>
- <view v-if="!form[group.list].length" class="empty card section">
- <text>{{ group.emptyText || "暂无记录" }}</text>
- </view>
- </block>
- <!-- 只读明细列表 -->
- <block v-if="group.readonlyList">
- <view v-for="(item, idx) in readonlyList(group)" :key="idx" class="list-item card section">
- <view class="li-head">
- <text class="li-title">{{ item.title }}</text>
- <text v-if="item.tag" class="li-tag">{{ item.tag }}</text>
- </view>
- <view v-for="(line, li) in item.lines" :key="li" class="li-line">{{ line }}</view>
- </view>
- <view v-if="!readonlyList(group).length" class="empty card section">
- <text>{{ group.emptyText || "暂无记录(请在 PC 端维护)" }}</text>
- </view>
- </block>
- <!-- 劳动合同(只读) -->
- <view v-if="group.readonlyContract" class="form-card card section">
- <view v-for="(row, ri) in contractRows" :key="ri">
- <view v-if="ri > 0" class="divider"></view>
- <view class="field">
- <text class="label">{{ row.label }}</text>
- <text class="value muted">{{ row.value || "—" }}</text>
- </view>
- </view>
- </view>
- <!-- 业务记录(只读) -->
- <block v-if="group.records">
- <view v-for="sec in recordSections" :key="sec.title" class="card section record-card">
- <view class="record-head">
- <text class="record-title">{{ sec.title }}</text>
- <text class="record-count">{{ sec.items.length }}</text>
- </view>
- <view v-for="(line, li) in sec.items" :key="li" class="record-line">{{ line }}</view>
- <view v-if="!sec.items.length" class="record-empty">暂无记录</view>
- </view>
- </block>
- </block>
- </view>
- <view class="footer-bar">
- <view class="ghost-btn press" @click="resetForm">重置</view>
- <view class="primary-btn press" :class="{ disabled: saving }" @click="submit">
- {{ saving ? "保存中…" : "保存" }}
- </view>
- </view>
- <!-- 明细编辑弹层 -->
- <view v-if="editor.visible" class="popup-mask" @click="closeEditor">
- <view class="popup" @click.stop>
- <view class="popup-head">
- <text class="popup-title">{{ editor.title }}</text>
- <text class="popup-close press" @click="closeEditor">✕</text>
- </view>
- <scroll-view class="popup-body" scroll-y>
- <view v-for="f in visibleEditorFields" :key="f.key" class="p-field">
- <text class="p-label" :class="{ required: f.required }">{{ f.label }}</text>
- <input
- v-if="f.type === 'text' || f.type === 'number'"
- class="p-input"
- :type="f.type === 'number' ? 'digit' : 'text'"
- :maxlength="f.maxlength || 60"
- :placeholder="f.placeholder || ''"
- v-model="editor.values[f.key]"
- />
- <textarea
- v-else-if="f.type === 'textarea'"
- class="p-textarea"
- :maxlength="f.maxlength || 300"
- :placeholder="f.placeholder || ''"
- v-model="editor.values[f.key]"
- />
- <picker
- v-else-if="f.type === 'date'"
- mode="date"
- :value="editor.values[f.key]"
- @change="onEditorDate(f, $event)"
- >
- <view class="p-picker">
- <text :class="editor.values[f.key] ? 'p-value' : 'p-placeholder'">{{ editor.values[f.key] || "请选择" }}</text>
- <text class="arrow">›</text>
- </view>
- </picker>
- <picker
- v-else-if="f.type === 'select'"
- mode="selector"
- :range="editorFieldRange(f)"
- range-key="label"
- :value="editorFieldIndex(f)"
- @change="onEditorPick(f, $event)"
- >
- <view class="p-picker">
- <text :class="editorFieldText(f) ? 'p-value' : 'p-placeholder'">{{ editorFieldText(f) || "请选择" }}</text>
- <text class="arrow">›</text>
- </view>
- </picker>
- <view v-else-if="f.type === 'file'" class="p-file"><fileMain v-model="editor.values[f.key]" /></view>
- </view>
- </scroll-view>
- <view class="popup-actions">
- <text class="popup-btn press" @click="closeEditor">取消</text>
- <text class="popup-btn primary press" @click="confirmEditor">确定</text>
- </view>
- </view>
- </view>
- <selectUserDialog ref="userPicker" @confirm="onUserPicked" />
- <!-- 隶属部门:树形选择器(层级不限,可展开折叠,公司层已去掉) -->
- <ba-tree-picker
- ref="deptPicker"
- :multiple="false"
- :localdata="deptTree"
- valueKey="id"
- textKey="name"
- childrenKey="children"
- title="选择部门"
- :border="true"
- confirm-color="#157a2c"
- switch-color="#157a2c"
- @select-change="onDeptTreeSelect"
- />
- </view>
- </template>
- <script>
- import fileMain from "@/pages/doc/index.vue";
- import selectUserDialog from "@/components/selectUserDialog.vue";
- import baTreePicker from "@/components/ba-tree-picker/ba-tree-picker.vue";
- import { getDictByCode } from "@/api/common";
- import { listOrganizations } from "@/api/hr/organization";
- import { pagePosition, getDeptPositionSequences } from "@/api/hr/position";
- import { getPositionSequenceList } from "@/api/hr/positionSequence";
- import { back } from "@/utils/hr/router";
- import { buildDeptTreeUnderLegal } from "@/utils/hr/organization-tree";
- import { getOrgOptionsWithFallback } from "@/utils/hr/current-org";
- import {
- ALLOWANCE_UNIT_OPTIONS,
- CERT_TYPE_OPTIONS,
- DEGREE_OPTIONS,
- EDUCATION_OPTIONS,
- EMPLOYEE_STATUS_LABEL,
- EMPLOYEE_STATUS_OPTIONS,
- ETHNIC_OPTIONS,
- MARITAL_OPTIONS,
- SEX_OPTIONS,
- SOCIAL_SECURITY_OPTIONS,
- STOP_REASON_OPTIONS,
- allowanceUnitLabel,
- buildProfileForm,
- buildUserPayload,
- compactPayload,
- currentUserId,
- dictToPositionNatureOptions,
- dictToStatusOptions,
- enumLabel,
- fetchUserProfile,
- keepIdString,
- refetchMyProfile,
- saveEmployee,
- syncWorkExperiences,
- listFactories,
- CHANGE_TYPE_LABELS,
- APPROVAL_STATUS_LABELS,
- EFFECTIVE_STATUS_LABELS,
- REGULARIZATION_TYPE_LABELS,
- REGULARIZATION_RESULT_LABELS,
- RESIGNATION_TYPE_LABELS,
- PAYSLIP_STATUS_LABELS,
- } from "@/api/hr/user";
- /** 数字/字符串统一成 picker 需要的 {label,value} */
- function toOptions(list) {
- if (!Array.isArray(list)) return [];
- return list.map((item) =>
- item && typeof item === "object"
- ? { label: String(item.label != null ? item.label : item.name != null ? item.name : item.value), value: item.value }
- : { label: String(item), value: item }
- );
- }
- /** 去掉非数字字符(卡号展示用) */
- function digitsOnly(value) {
- return String(value == null ? "" : value).replace(/\D/g, "");
- }
- /** 在部门树里按 id 找节点,返回「一级 / 二级 / 三级」路径名(与详情接口回填、树选择展示口径一致) */
- function findDeptPath(tree = [], id, parents = []) {
- for (let i = 0; i < tree.length; i += 1) {
- const node = tree[i];
- const path = parents.concat(node.name);
- if (String(node.id) === String(id)) return path.filter(Boolean).join(" / ");
- const hit = findDeptPath(node.children || [], id, path);
- if (hit) return hit;
- }
- return "";
- }
- function formatCardNumber(value) {
- const text = digitsOnly(value);
- return text ? text.replace(/(.{4})/g, "$1 ").trim() : "";
- }
- function tailOf(value) {
- const text = digitsOnly(value);
- return text ? text.slice(-4) : "----";
- }
- function calcAge(birthday) {
- if (!birthday) return "";
- const d = new Date(String(birthday).replace(/-/g, "/"));
- if (isNaN(d.getTime())) return "";
- const now = new Date();
- let age = now.getFullYear() - d.getFullYear();
- const m = now.getMonth() - d.getMonth();
- if (m < 0 || (m === 0 && now.getDate() < d.getDate())) age -= 1;
- return age >= 0 && age < 130 ? String(age) : "";
- }
- const JOB_FIELDS = [
- { key: "startDate", label: "工作开始日期", type: "date", required: true },
- { key: "endDate", label: "工作结束日期", type: "date", required: true },
- { key: "company", label: "前公司名称", type: "text", required: true, maxlength: 50, placeholder: "请输入公司名称" },
- { key: "position", label: "前公司岗位", type: "text", maxlength: 30, placeholder: "请输入岗位" },
- { key: "department", label: "前公司部门", type: "text", maxlength: 30, placeholder: "请输入部门" },
- { key: "leaveReason", label: "离职原因", type: "text", maxlength: 40, placeholder: "请输入" },
- { key: "description", label: "工作描述", type: "textarea", maxlength: 300, placeholder: "请输入工作描述" },
- ];
- const BANK_FIELDS = [
- { key: "bankName", label: "开户行", type: "text", required: true, maxlength: 50, placeholder: "如:中国工商银行" },
- { key: "bankBranch", label: "银行网点", type: "text", maxlength: 60, placeholder: "请输入银行网点" },
- { key: "bankAccount", label: "银行卡号", type: "number", required: true, maxlength: 24, placeholder: "请输入银行卡号" },
- { key: "bankAccountName", label: "开户人姓名", type: "text", required: true, maxlength: 20, placeholder: "请输入开户人" },
- ];
- const SOCIAL_FIELDS = [
- { key: "type", label: "社保类型", type: "select", required: true, options: SOCIAL_SECURITY_OPTIONS },
- {
- key: "isEnabled",
- label: "是否启用",
- type: "select",
- options: [
- { label: "是", value: 1 },
- { label: "否", value: 0 },
- ],
- },
- { key: "base", label: "缴费基数", type: "number", maxlength: 12, placeholder: "元" },
- { key: "insuredDate", label: "参保日期", type: "date" },
- { key: "stopDate", label: "停保日期", type: "date" },
- { key: "stopReason", label: "停缴记录", type: "select", options: STOP_REASON_OPTIONS },
- ];
- const CERT_FIELDS = [
- { key: "type", label: "证照类型", type: "select", required: true, options: CERT_TYPE_OPTIONS },
- { key: "number", label: "证照编号", type: "text", required: true, maxlength: 30, placeholder: "请输入证照编号" },
- { key: "level", label: "等级", type: "select", options: ["初级", "中级", "高级"] },
- { key: "startDate", label: "有效期开始", type: "date", required: true },
- { key: "endDate", label: "有效期结束", type: "date", required: true },
- { key: "issueDate", label: "颁布日期", type: "date" },
- { key: "status", label: "状态", type: "select", required: true, options: ["有效", "失效"] },
- {
- key: "hasPositionAllowance",
- label: "是否有岗位津贴",
- type: "select",
- options: [
- { label: "是", value: 1 },
- { label: "否", value: 0 },
- ],
- },
- {
- key: "positionAllowanceAmount",
- label: "津贴金额",
- type: "number",
- maxlength: 12,
- placeholder: "请输入",
- showIf: (values) => Number(values.hasPositionAllowance) === 1,
- },
- {
- key: "positionAllowanceUnit",
- label: "津贴单位",
- type: "select",
- options: ALLOWANCE_UNIT_OPTIONS,
- showIf: (values) => Number(values.hasPositionAllowance) === 1,
- },
- { key: "attachment", label: "证照附件", type: "file" },
- { key: "remark", label: "备注", type: "textarea", maxlength: 200, placeholder: "请输入" },
- ];
- const EMPTY_JOB = { startDate: "", endDate: "", company: "", position: "", department: "", leaveReason: "", description: "" };
- const EMPTY_BANK = { bankName: "", bankBranch: "", bankAccount: "", bankAccountName: "" };
- const EMPTY_SOCIAL = { type: "", isEnabled: 0, base: "", insuredDate: "", stopDate: "", stopReason: "" };
- const EMPTY_CERT = {
- type: "",
- number: "",
- level: "",
- startDate: "",
- endDate: "",
- issueDate: "",
- status: "有效",
- hasPositionAllowance: 0,
- positionAllowanceAmount: "",
- positionAllowanceUnit: "",
- attachment: [],
- remark: "",
- };
- let seq = 0;
- function newKey(prefix) {
- seq += 1;
- return `${prefix}_new_${Date.now().toString(36)}_${seq}`;
- }
- export default {
- components: { fileMain, selectUserDialog, baTreePicker },
- data() {
- return {
- tabs: [
- { name: "base", label: "基本信息" },
- { name: "experience", label: "教育履历" },
- { name: "payroll", label: "薪酬社保" },
- { name: "qualification", label: "资质证照" },
- { name: "contract", label: "合同记录" },
- ],
- activeTab: "base",
- saving: false,
- form: buildProfileForm({}),
- raw: {},
- rawPositionList: [],
- rawPrimaryPositionId: "",
- originalJobs: [],
- jobsDirty: false,
- // 选项数据
- orgList: [],
- legalOptions: [],
- factoryOptions: [],
- positionSource: [],
- sequenceList: [],
- sequenceOptions: [],
- positionNatureOptions: [
- { label: "全职", value: "全职" },
- { label: "兼职", value: "兼职" },
- { label: "实习", value: "实习" },
- { label: "劳务", value: "劳务" },
- ],
- statusChoices: EMPLOYEE_STATUS_OPTIONS,
- deptTree: [],
- // 明细编辑弹层
- editor: { visible: false, title: "", fields: [], values: {}, index: -1, list: "" },
- baseGroups: [],
- experienceGroups: [],
- payrollGroups: [],
- qualificationGroups: [],
- contractGroups: [],
- };
- },
- computed: {
- activeGroups() {
- if (this.activeTab === "base") return this.baseGroups;
- if (this.activeTab === "experience") return this.experienceGroups;
- if (this.activeTab === "payroll") return this.payrollGroups;
- if (this.activeTab === "qualification") return this.qualificationGroups;
- return this.contractGroups;
- },
- /** 岗位下拉:按岗位序列 + 部门收敛 */
- positionChoices() {
- let list = this.positionSource || [];
- const seqName = this.form.positionSequenceName;
- if (seqName) {
- const filtered = list.filter((p) => !p.sequence || String(p.sequence) === String(seqName));
- if (filtered.length) list = filtered;
- }
- if (this.form.deptId) {
- const filtered = list.filter((p) => !p.deptId || String(p.deptId) === String(this.form.deptId));
- if (filtered.length) list = filtered;
- }
- return list.map((p) => ({ label: p.level ? `${p.name}(${p.level})` : p.name, value: p.id }));
- },
- /** 岗位层级下拉:取自所选岗位的层级集合 */
- levelChoices() {
- const pos = (this.positionSource || []).find((p) => String(p.id) === String(this.form.positionId));
- const levels = pos && Array.isArray(pos.levels) ? pos.levels : [];
- const options = levels.map((lv) => ({
- label: typeof lv === "object" ? lv.name || lv.level : String(lv),
- value: typeof lv === "object" ? lv.name || lv.level : lv,
- }));
- if (options.length) return options;
- // 岗位未回填层级集合时,至少保留当前值可选,避免选择器为空
- return this.form.staffingLevel ? [{ label: this.form.staffingLevel, value: this.form.staffingLevel }] : [];
- },
- contractRows() {
- const c = this.form.contract || {};
- return [
- { label: "合同分类", value: c.category },
- { label: "合同编号", value: c.name },
- { label: "签订时间", value: c.signDate },
- { label: "开始时间", value: c.startDate },
- { label: "结束时间", value: c.endDate },
- { label: "生效状态", value: c.status },
- { label: "备注", value: c.remark },
- ];
- },
- recordSections() {
- const r = this.raw || {};
- const changes = (r.changes || []).map((item) => {
- const type = enumLabel(CHANGE_TYPE_LABELS, item.changeType) || "异动";
- const status =
- enumLabel(APPROVAL_STATUS_LABELS, item.approvalStatus) ||
- enumLabel(EFFECTIVE_STATUS_LABELS, item.effectiveStatus);
- return `${type}|生效 ${item.effectiveDate || "—"}|${status || "—"}`;
- });
- const regulars = (r.regularizations || []).map((item) => {
- const type = enumLabel(REGULARIZATION_TYPE_LABELS, item.regularizationType) || "转正";
- const result =
- enumLabel(REGULARIZATION_RESULT_LABELS, item.result) ||
- enumLabel(APPROVAL_STATUS_LABELS, item.approvalStatus);
- return `${type}|计划 ${item.plannedRegularDate || "—"}|实际 ${item.effectiveDate || "—"}|${result || "—"}`;
- });
- const resignations = (r.resignations || []).map((item) => {
- const type = enumLabel(RESIGNATION_TYPE_LABELS, item.resignationType) || "离职";
- const status =
- enumLabel(APPROVAL_STATUS_LABELS, item.approvalStatus) ||
- enumLabel(EFFECTIVE_STATUS_LABELS, item.effectiveStatus);
- return `${type}|申请 ${item.applyDate || "—"}|生效 ${item.effectiveDate || "—"}|${status || "—"}`;
- });
- const performances = (r.performanceRecords || []).map(
- (item) =>
- `${item.cycleCode || item.cycleName || "绩效"}|得分 ${item.finalScore != null ? item.finalScore : "—"}|等级 ${item.finalLevel || "—"}`
- );
- const payslips = (r.payslipSummaries || []).map((item) => {
- const status = enumLabel(PAYSLIP_STATUS_LABELS, item.payslipStatus);
- return `${item.payrollMonth || "—"}|${status || "—"}`;
- });
- const rewards = (r.rewardsPunishments || []).map(
- (item) =>
- `${Number(item.type) === 2 ? "惩罚" : "奖励"}|${item.desce || ""}|${item.startTime || ""}`
- );
- return [
- { title: "人事异动", items: changes },
- { title: "转正记录", items: regulars },
- { title: "离职记录", items: resignations },
- { title: "绩效结果", items: performances },
- { title: "工资条", items: payslips },
- { title: "奖惩记录", items: rewards },
- ];
- },
- visibleEditorFields() {
- return (this.editor.fields || []).filter((f) => (typeof f.showIf === "function" ? f.showIf(this.editor.values) : true));
- },
- },
- onLoad() {
- this.buildGroups();
- this.loadOptions();
- this.loadProfile();
- },
- methods: {
- goBack() {
- back("/pages/index/index");
- },
- /* ================= 配置 ================= */
- buildGroups() {
- this.baseGroups = [
- {
- title: "身份信息",
- fields: [
- { key: "name", label: "姓名", type: "text", required: true, maxlength: 20, placeholder: "请输入姓名" },
- { key: "gender", label: "性别", type: "enum", options: SEX_OPTIONS },
- { key: "idCard", label: "身份证号", type: "text", maxlength: 18, placeholder: "请输入 18 位身份证号" },
- { key: "birthDate", label: "出生日期", type: "date" },
- { key: "age", label: "年龄", type: "readonly" },
- { key: "ethnic", label: "民族", type: "enum", options: ETHNIC_OPTIONS },
- { key: "politicalStatus", label: "政治面貌", type: "text", maxlength: 20, placeholder: "党员、群众等" },
- { key: "maritalStatus", label: "婚姻状况", type: "enum", options: MARITAL_OPTIONS },
- { key: "nativePlace", label: "籍贯", type: "text", maxlength: 30, placeholder: "省 / 市" },
- { key: "censusAddress", label: "户籍地址", type: "text", maxlength: 100, placeholder: "请输入户籍地址" },
- { key: "currentAddress", label: "现居住地址", type: "text", maxlength: 100, placeholder: "请输入现居住地址" },
- { key: "workAddress", label: "工作地址", type: "text", maxlength: 60, placeholder: "实际办公地点" },
- ],
- },
- {
- title: "组织与任职",
- fields: [
- { key: "employeeNo", label: "工号", type: "readonly" },
- { key: "account", label: "用户账号", type: "readonly" },
- { key: "groupId", label: "所属机构", type: "enum", source: "legal", displayKey: "company", required: true },
- { key: "factoryId", label: "所属工厂", type: "enum", source: "factory", displayKey: "factory" },
- { key: "dept", label: "隶属部门", type: "dept", required: true },
- { key: "positionSequenceId", label: "岗位序列", type: "enum", source: "sequence", displayKey: "positionSequenceName", required: true },
- { key: "positionId", label: "岗位", type: "enum", source: "position", displayKey: "positionName", required: true },
- { key: "employmentType", label: "岗位性质", type: "enum", source: "positionNature", displayKey: "employmentTypeName", required: true },
- { key: "staffingLevel", label: "岗位层级", type: "enum", source: "level", required: true },
- { key: "managerId", label: "直属上级", type: "enum", source: "manager", displayKey: "manager" },
- { key: "hireDate", label: "入职时间", type: "date", required: true },
- { key: "regularDate", label: "转正日期", type: "date" },
- { key: "status", label: "员工状态", type: "enum", source: "status", required: true },
- { key: "keyProcess", label: "关键工序", type: "text", maxlength: 30, placeholder: "多个用英文逗号分隔" },
- { key: "enabled", label: "是否启用", type: "switch" },
- ],
- },
- {
- title: "联系方式",
- fields: [
- { key: "phone", label: "手机号码", type: "number", required: true, maxlength: 11, placeholder: "11 位手机号" },
- { key: "email", label: "邮箱", type: "text", maxlength: 50, placeholder: "name@company.com" },
- { key: "wechat", label: "微信号", type: "text", maxlength: 30, placeholder: "请输入微信号" },
- { key: "emergencyContact", label: "紧急联系人", type: "text", maxlength: 20, placeholder: "请输入姓名" },
- { key: "emergencyRelation", label: "联系人关系", type: "text", maxlength: 20, placeholder: "如配偶、父母" },
- { key: "emergencyPhone", label: "紧急联系电话", type: "number", maxlength: 11, placeholder: "请输入电话" },
- ],
- },
- ];
- this.experienceGroups = [
- {
- title: "教育经历",
- fields: [
- { key: "education", label: "最高学历", type: "enum", options: EDUCATION_OPTIONS },
- { key: "degree", label: "学位", type: "enum", options: DEGREE_OPTIONS },
- { key: "school", label: "毕业院校", type: "text", maxlength: 40, placeholder: "请输入毕业院校" },
- { key: "major", label: "所学专业", type: "text", maxlength: 40, placeholder: "请输入所学专业" },
- { key: "graduationDate", label: "毕业时间", type: "date" },
- ],
- },
- {
- title: "工作履历",
- list: "previousJobs",
- listFields: JOB_FIELDS,
- addText: "添加履历",
- emptyText: "暂无入职前工作履历",
- },
- ];
- this.payrollGroups = [
- {
- title: "薪酬银行",
- list: "bankAccounts",
- listFields: BANK_FIELDS,
- addText: "添加银行",
- emptyText: "暂无银行账户",
- },
- {
- title: "社保公积金",
- list: "socialSecurityRecords",
- listFields: SOCIAL_FIELDS,
- addText: "添加记录",
- emptyText: "暂无社保公积金记录",
- },
- ];
- this.qualificationGroups = [
- {
- title: "资质证照",
- list: "certificates",
- listFields: CERT_FIELDS,
- addText: "添加证照",
- emptyText: "暂无资质证照记录",
- },
- { title: "工种", readonlyList: "professions", emptyText: "暂无工种记录(请在 PC 端维护)" },
- { title: "技能", readonlyList: "skills", emptyText: "暂无技能记录(请在 PC 端维护)" },
- {
- title: "档案备注",
- fields: [
- {
- key: "remarks",
- label: "备注",
- type: "textarea",
- maxlength: 500,
- placeholder: "补充需要说明的员工档案信息",
- },
- ],
- },
- ];
- this.contractGroups = [{ title: "劳动合同", readonlyContract: true }, { title: "业务记录", records: true }];
- },
- /* ================= 选项加载 ================= */
- async loadOptions() {
- this.loadOrganizations();
- this.loadFactories();
- this.loadPositions();
- this.loadSequenceMeta();
- this.loadSequences();
- this.loadDicts();
- },
- async loadOrganizations() {
- try {
- // 组织树仍取全量:隶属部门的级联要按当前机构从中过滤出部门
- this.orgList = await listOrganizations({}, false);
- } catch (error) {
- this.orgList = [];
- }
- // 所属机构候选项与 PC 一致(EmployeeArchiveForm.legalEntities):
- // 主源是登录缓存 loginChangeGroupVOList(当前账号所属公司),不再把全量机构树全列出来;
- // 缓存缺名称时回落组织树兜底,编辑老档案时补当前项,避免下拉只显示 ID。
- this.legalOptions = getOrgOptionsWithFallback(
- this.orgList,
- this.form && this.form.groupId,
- this.form && this.form.company
- ).map((item) => ({ label: item.name || String(item.id), value: item.id }));
- this.refreshDeptTree();
- this.hydrateFromOptions();
- },
- async loadFactories() {
- const list = await listFactories(false);
- this.factoryOptions = list.map((item) => ({ label: item.name, value: item.id }));
- this.hydrateFromOptions();
- },
- async loadPositions() {
- try {
- const page = await pagePosition({ pageNum: 1, size: 1000 });
- const rows = (page && page.list) || [];
- this.positionSource = rows.map((raw) => ({
- id: String(raw.id != null ? raw.id : raw.positionId || ""),
- name: raw.positionName || raw.name || "",
- deptId: raw.deptId != null ? raw.deptId : "",
- level: raw.positionLevel || "",
- levels: Array.isArray(raw.positionLevels) ? raw.positionLevels : [],
- sequence: raw.positionSequence || "",
- }));
- } catch (error) {
- this.positionSource = [];
- }
- this.hydrateFromOptions();
- },
- /**
- * 全局岗位序列列表:只作「序列名称 → 主键」的回填兜底(PC 的 sequenceLevelMap 同用途),
- * **不再当作下拉选项** —— /hr/positionSequence/getPositionSequenceList 在部分环境返回空,
- * 之前用它填下拉,导致「岗位序列」选择器一片空白(2026-10-15 用户报障)。
- */
- async loadSequenceMeta() {
- try {
- const list = await getPositionSequenceList();
- this.sequenceList = Array.isArray(list) ? list : [];
- } catch (error) {
- this.sequenceList = [];
- }
- },
- /**
- * 岗位序列下拉:与 PC EmployeeArchiveForm.loadPositionSequences 同口径 ——
- * 按「隶属部门」级联拉 /fm/tmaindepartmentposition/sequence/list?deptId=,
- * 未选部门时清空(PC 同样是「请先选择隶属部门」+ 禁用)。
- * @param {object} options resetStale=true 时,把不属于新部门的已选序列一并清掉(换部门时才用)
- */
- async loadSequences(options = {}) {
- const deptId = this.form && this.form.deptId;
- if (!deptId) {
- this.sequenceOptions = [];
- return;
- }
- try {
- const data = await getDeptPositionSequences(deptId, false);
- const rows = Array.isArray(data) ? data : (data && (data.list || data.records)) || [];
- const map = new Map();
- rows.forEach((item) => {
- const id = item && (item.positionSequenceId != null ? item.positionSequenceId : item.id);
- if (id == null || id === "") return;
- const key = String(id);
- if (map.has(key)) return;
- map.set(key, { label: item.positionSequence || item.name || "", value: key });
- });
- this.sequenceOptions = Array.from(map.values());
- } catch (error) {
- this.sequenceOptions = [];
- console.log("[hr/profile/edit] 岗位序列加载失败:", (error && error.message) || error);
- }
- if (options.resetStale && this.form.positionSequenceId) {
- const stillValid = this.sequenceOptions.some(
- (item) => String(item.value) === String(this.form.positionSequenceId)
- );
- if (!stillValid) {
- this.form.positionSequenceId = "";
- this.form.positionSequenceName = "";
- }
- }
- this.hydrateFromOptions();
- },
- async loadDicts() {
- try {
- const dict = await getDictByCode("hr_user_status");
- const list = dictToStatusOptions(dict);
- if (list.length) this.statusChoices = list;
- } catch (error) {
- // 字典缺失时保留内置兜底选项
- }
- try {
- const dict = await getDictByCode("position_nature");
- const list = dictToPositionNatureOptions(dict);
- if (list.length) this.positionNatureOptions = list;
- } catch (error) {
- // 岗位性质字典缺失时留空,由后端按岗位收敛
- }
- this.hydrateFromOptions();
- },
- refreshDeptTree() {
- if (!this.form.groupId) {
- this.deptTree = [];
- return;
- }
- // 所属机构已锁定为当前公司,公司这一层不再作为部门级联的可选项:
- // 直接从部门层开始(否则第一列会是一列只有一个「公司」的冗余项,
- // 选出来的路径也会带上公司名,与详情接口回填的「研发部」口径不一致)。
- const nodes = buildDeptTreeUnderLegal(this.orgList, this.form.groupId);
- const legal = nodes.find((node) => String(node.id) === String(this.form.groupId)) || nodes[0];
- this.deptTree = legal && Array.isArray(legal.children) ? legal.children : [];
- },
- /* ================= 名称 / 主键兜底回填(对齐 PC attachOrgMeta、resolveSequenceForEdit) ================= */
- /** 只补空值,不覆盖已回填内容;组织 / 工厂 / 岗位等候选加载完成后都要再跑一次 */
- hydrateFromOptions() {
- const form = this.form;
- if (!form) return;
- // ① 所属机构:档案接口常只回 groupId 不回 groupName,用组织列表补名称
- if (form.groupId && !form.company) {
- const hit = (this.legalOptions || []).find((item) => String(item.value) === String(form.groupId));
- if (hit) form.company = hit.label;
- }
- // ② 所属工厂:只有 factoryId 时用工厂列表补名称
- if (form.factoryId && !form.factory) {
- const hit = (this.factoryOptions || []).find((item) => String(item.value) === String(form.factoryId));
- if (hit) form.factory = hit.label;
- }
- // ③ 隶属部门:只有 deptId 时按部门树补「一级 / 二级 / 三级」路径名
- if (form.deptId && !form.deptName) {
- form.deptName = findDeptPath(this.deptTree, form.deptId);
- }
- // ④ 岗位序列:只有名称时按名称反查主键(序列主键用于提交 positionSequenceId)
- if (!form.positionSequenceId && form.positionSequenceName) {
- // 先在下拉的部门级序列里找,找不到再用全局序列列表兜底(老档案的序列可能不在该部门级联里)
- const hit =
- (this.sequenceOptions || []).find(
- (item) => String(item.label) === String(form.positionSequenceName)
- ) ||
- (this.sequenceList || []).find(
- (item) => String(item.name || item.sequenceName || "") === String(form.positionSequenceName)
- );
- if (hit) form.positionSequenceId = hit.value != null ? hit.value : hit.id;
- }
- // ⑤ 岗位:只有名称时按名称反查主键;名称为「岗位A、岗位B」时取第一个
- if (!form.positionId && form.positionName) {
- const first =
- String(form.positionName)
- .split(/[、,,]/)
- .map((text) => text.trim())
- .filter(Boolean)[0] || "";
- const hit = (this.positionSource || []).find((item) => String(item.name) === first);
- if (hit) {
- form.positionId = hit.id;
- if (!form.positionSequenceName && hit.sequence) form.positionSequenceName = hit.sequence;
- }
- }
- // ⑥ 岗位性质:接口只回字典码时按字典补中文名(已回填中文名则不动)
- if (form.employmentType) {
- const hit = (this.positionNatureOptions || []).find(
- (item) => String(item.value) === String(form.employmentType)
- );
- if (hit && (!form.employmentTypeName || /^[\dA-Za-z_]+$/.test(String(form.employmentTypeName)))) {
- form.employmentTypeName = hit.label;
- }
- }
- // ⑦ 年龄:接口不回 age 时按出生日期补算
- if (!form.age && form.birthDate) form.age = calcAge(form.birthDate);
- },
- /* ================= 档案加载 ================= */
- async loadProfile() {
- const login = uni.getStorageSync("userInfo") || {};
- this.applyProfile(buildProfileForm(login, {}), login);
- try {
- const profile = await fetchUserProfile(currentUserId());
- if (!profile) return;
- const workExperiences = profile.workExperiences || [];
- const form = buildProfileForm(profile, { workExperiences });
- this.applyProfile(form, profile);
- } catch (error) {
- console.warn("[hr/profile/edit] 档案加载失败:", error && error.message);
- uni.showToast({ title: "档案加载失败,已展示本地缓存", icon: "none" });
- }
- // 岗位序列按部门级联,必须等档案带出 deptId 之后再拉一次
- this.loadSequences();
- },
- applyProfile(form, raw) {
- this.form = form;
- this.raw = raw || {};
- // 注意:三元的两支都必须保证是数组。原来的写法
- // `Array.isArray((raw && raw.positionList) || []) ? raw.positionList : []`
- // 在 raw.positionList 为 undefined 时会命中条件却取回 undefined
- // (判断的是 []、赋值的是 undefined),随后 .find 就抛
- // TypeError: Cannot read property 'find' of undefined。
- // 首次进入页面用登录缓存 userInfo 调本方法,而登录对象没有 positionList,必然触发。
- const positionList = (raw && raw.positionList) || [];
- this.rawPositionList = Array.isArray(positionList) ? positionList : [];
- const primary = this.rawPositionList.find((item) => Number(item.primaryFlag) === 1) || this.rawPositionList[0];
- this.rawPrimaryPositionId = primary && primary.positionId != null ? String(primary.positionId) : "";
- this.originalJobs = (form.previousJobs || []).map((item) => ({ id: item.id, company: item.company, startDate: item.startDate, endDate: item.endDate }));
- this.jobsDirty = false;
- this.refreshDeptTree();
- this.hydrateFromOptions();
- },
- /* ================= 字段取值 / 展示 ================= */
- fieldRange(field) {
- if (field.source) return this.sourceList(field.source);
- return toOptions(field.options);
- },
- /**
- * 依赖上级字段的枚举:上级没选时禁用选择器(PC 的岗位序列同样是 :disabled="!hasDept"),
- * 避免点开一个空选择器。
- */
- enumDisabled(field) {
- if (!field || field.source !== "sequence") return false;
- return !this.form || !this.form.deptId;
- },
- /** 被禁用时的占位提示 */
- enumHint(field) {
- return this.enumDisabled(field) ? "请先选择隶属部门" : "";
- },
- sourceList(source) {
- if (source === "legal") return this.legalOptions;
- if (source === "factory") return this.factoryOptions;
- if (source === "sequence") return this.sequenceOptions;
- if (source === "position") return this.positionChoices;
- if (source === "level") return this.levelChoices;
- if (source === "positionNature") return this.positionNatureOptions;
- if (source === "status") return this.statusChoices;
- return [];
- },
- fieldText(field) {
- const value = this.form[field.key];
- if (field.displayKey) {
- const text = this.form[field.displayKey];
- if (text) return String(text);
- // 名称字段缺失时按主键在候选里反查,避免「已回填主键却显示请选择」
- if (value === "" || value === null || value === undefined) return "";
- const hit = this.fieldRange(field).find((item) => String(item.value) === String(value));
- // 候选里也没有(字典/组织未配全)时兜底显示原始值,一眼可看出缺配置
- return hit ? hit.label : String(value);
- }
- if (value === "" || value === null || value === undefined) return "";
- if (field.source === "status") {
- const hit = (this.statusChoices || []).find((item) => String(item.value) === String(value));
- if (hit) return hit.label;
- // 字典没配到该码时用接口给的中文名兜底,最后才原样显示码
- if (this.form.statusName) return String(this.form.statusName);
- return EMPLOYEE_STATUS_LABEL[Number(value)] || String(value);
- }
- const options = this.fieldRange(field);
- const matched = options.find((item) => String(item.value) === String(value));
- return matched ? matched.label : String(value);
- },
- fieldIndex(field) {
- const options = this.fieldRange(field);
- const value = this.form[field.key];
- const idx = options.findIndex((item) => String(item.value) === String(value));
- return idx < 0 ? 0 : idx;
- },
- onFieldPick(field, e) {
- const options = this.fieldRange(field);
- const hit = options[Number(e.detail.value)];
- if (!hit) return;
- this.form[field.key] = hit.value;
- if (field.displayKey) this.form[field.displayKey] = hit.label;
- this.afterPick(field);
- },
- onFieldDate(field, e) {
- this.form[field.key] = (e && e.detail && e.detail.value) || "";
- if (field.key === "birthDate") this.form.age = calcAge(this.form.birthDate);
- },
- onFieldSwitch(field, e) {
- this.form[field.key] = Boolean(e.detail.value);
- },
- afterPick(field) {
- if (field.key === "groupId") {
- this.form.deptIds = [];
- this.form.deptId = "";
- this.form.deptName = "";
- this.refreshDeptTree();
- this.form.positionSequenceId = "";
- this.form.positionSequenceName = "";
- this.form.positionId = "";
- this.form.positionName = "";
- this.form.staffingLevel = "";
- } else if (field.key === "positionSequenceId") {
- const pos = this.positionSource.find((p) => String(p.id) === String(this.form.positionId));
- if (pos && this.form.positionSequenceName && pos.sequence && String(pos.sequence) !== String(this.form.positionSequenceName)) {
- this.form.positionId = "";
- this.form.positionName = "";
- this.form.staffingLevel = "";
- }
- } else if (field.key === "positionId") {
- const pos = this.positionSource.find((p) => String(p.id) === String(this.form.positionId));
- if (pos) {
- this.form.positionName = pos.name;
- this.form.staffingLevel = pos.level || this.form.staffingLevel;
- }
- this.syncSequenceByPosition();
- }
- },
- syncSequenceByPosition() {
- if (this.form.positionSequenceName) return;
- const pos = this.positionSource.find((p) => String(p.id) === String(this.form.positionId));
- if (pos && pos.sequence) {
- this.form.positionSequenceName = String(pos.sequence);
- const hit = this.sequenceOptions.find((item) => String(item.label) === String(pos.sequence));
- if (hit) this.form.positionSequenceId = hit.value;
- }
- },
- openManagerPicker() {
- if (this.$refs.userPicker) this.$refs.userPicker.open("2");
- },
- onUserPicked(list) {
- const user = Array.isArray(list) ? list[0] : null;
- if (!user) return;
- this.form.managerId = user.id;
- this.form.manager = user.name || user.userName || "";
- },
- /* ================= 隶属部门(树形选择) ================= */
- /** 打开部门树选择器(层级不限,可展开折叠) */
- openDeptPicker() {
- if (!this.form.groupId) {
- uni.showToast({ title: "请先选择所属机构", icon: "none" });
- return;
- }
- if (!this.deptTree.length) {
- uni.showToast({ title: "该机构下暂无可选部门", icon: "none" });
- return;
- }
- const picker = this.$refs.deptPicker;
- if (picker && typeof picker._show === "function") picker._show();
- },
- /**
- * 树节点选中回调:ba-tree-picker 单选时回传 (ids, names)。
- * 名称统一按部门树路径生成(如「制造部 / 装配车间」),与详情接口回填口径一致。
- */
- onDeptTreeSelect(ids, names) {
- const id = Array.isArray(ids) ? ids[0] : ids;
- if (id == null || id === "") return;
- this.form.deptIds = [String(id)];
- this.form.deptId = String(id);
- this.form.deptName = findDeptPath(this.deptTree, id) || names || "";
- this.syncPositionWithDept();
- // 岗位序列跟着部门走:重新拉该部门下的序列,并把已不属于新部门的旧序列清掉
- this.loadSequences({ resetStale: true });
- },
- /** 部门变化后,已选岗位不属于该部门时清掉岗位(与岗位下拉的收敛口径一致) */
- syncPositionWithDept() {
- if (!this.form.positionId) return;
- const pos = (this.positionSource || []).find((p) => String(p.id) === String(this.form.positionId));
- if (pos && pos.deptId && String(pos.deptId) !== String(this.form.deptId)) {
- this.form.positionId = "";
- this.form.positionName = "";
- this.form.staffingLevel = "";
- }
- },
- /* ================= 明细列表 ================= */
- listItemTitle(group, item, idx) {
- if (group.list === "previousJobs") return item.company || `工作履历 ${idx + 1}`;
- if (group.list === "bankAccounts") return item.bankName || `银行账户 ${idx + 1}`;
- if (group.list === "socialSecurityRecords") return item.type || `社保记录 ${idx + 1}`;
- if (group.list === "certificates") return [item.type, item.number].filter(Boolean).join(" · ") || `证照 ${idx + 1}`;
- return group.title;
- },
- listItemLines(group, item) {
- const lines = [];
- if (group.list === "previousJobs") {
- if (item.startDate || item.endDate) lines.push(`时间:${item.startDate || "—"} ~ ${item.endDate || "—"}`);
- if (item.position) lines.push(`岗位:${item.position}`);
- if (item.department) lines.push(`部门:${item.department}`);
- if (item.leaveReason) lines.push(`离职原因:${item.leaveReason}`);
- if (item.description) lines.push(`工作描述:${item.description}`);
- } else if (group.list === "bankAccounts") {
- if (item.bankAccount) lines.push(`卡号:${formatCardNumber(item.bankAccount)}`);
- if (item.bankBranch) lines.push(`网点:${item.bankBranch}`);
- if (item.bankAccountName) lines.push(`开户人:${item.bankAccountName}`);
- } else if (group.list === "socialSecurityRecords") {
- lines.push(`状态:${Number(item.isEnabled) === 1 ? "启用" : "停用"}`);
- if (item.base !== "" && item.base != null) lines.push(`基数:${item.base} 元`);
- if (item.insuredDate) lines.push(`参保:${item.insuredDate}`);
- if (item.stopDate) lines.push(`停保:${item.stopDate}`);
- if (item.stopReason) lines.push(`停缴记录:${item.stopReason}`);
- } else if (group.list === "certificates") {
- lines.push(`状态:${item.status || "—"}|等级:${item.level || "—"}`);
- if (item.startDate || item.endDate) lines.push(`有效期:${item.startDate || "—"} ~ ${item.endDate || "—"}`);
- if (Number(item.hasPositionAllowance) === 1) {
- lines.push(`岗位津贴:${item.positionAllowanceAmount || 0} ${allowanceUnitLabel(item.positionAllowanceUnit)}`);
- }
- if (item.remark) lines.push(`备注:${item.remark}`);
- }
- return lines;
- },
- openItemEditor(group, item, idx) {
- const fields = group.listFields || [];
- const values = {};
- fields.forEach((f) => {
- const raw = item[f.key];
- if (f.type === "file") values[f.key] = Array.isArray(raw) ? raw.slice() : [];
- else values[f.key] = raw === null || raw === undefined ? "" : raw;
- });
- this.editor = {
- visible: true,
- title: `编辑${group.title}`,
- fields,
- values,
- index: idx,
- list: group.list,
- group,
- };
- },
- addItem(group) {
- const fields = group.listFields || [];
- const emptyMap = {
- previousJobs: EMPTY_JOB,
- bankAccounts: EMPTY_BANK,
- socialSecurityRecords: EMPTY_SOCIAL,
- certificates: EMPTY_CERT,
- };
- const template = emptyMap[group.list] || {};
- const values = {};
- fields.forEach((f) => {
- const raw = template[f.key];
- if (f.type === "file") values[f.key] = Array.isArray(raw) ? raw.slice() : [];
- else values[f.key] = raw === null || raw === undefined ? "" : raw;
- });
- if (group.list === "bankAccounts") values.bankAccountName = values.bankAccountName || this.form.name || "";
- this.editor = {
- visible: true,
- title: `新增${group.title}`,
- fields,
- values,
- index: -1,
- list: group.list,
- group,
- };
- },
- closeEditor() {
- this.editor = { visible: false, title: "", fields: [], values: {}, index: -1, list: "" };
- },
- editorFieldRange(field) {
- return toOptions(field.options);
- },
- editorFieldText(field) {
- const value = this.editor.values[field.key];
- if (value === "" || value === null || value === undefined) return "";
- const hit = this.editorFieldRange(field).find((item) => String(item.value) === String(value));
- return hit ? hit.label : String(value);
- },
- editorFieldIndex(field) {
- const options = this.editorFieldRange(field);
- const idx = options.findIndex((item) => String(item.value) === String(this.editor.values[field.key]));
- return idx < 0 ? 0 : idx;
- },
- onEditorPick(field, e) {
- const hit = this.editorFieldRange(field)[Number(e.detail.value)];
- if (hit) this.editor.values[field.key] = hit.value;
- },
- onEditorDate(field, e) {
- this.editor.values[field.key] = (e && e.detail && e.detail.value) || "";
- },
- validateEditor() {
- const { fields, values } = this.editor;
- for (const f of fields) {
- if (typeof f.showIf === "function" && !f.showIf(values)) continue;
- if (f.required && !String(values[f.key] == null ? "" : values[f.key]).trim()) {
- return `请填写${f.label}`;
- }
- }
- if (this.editor.list === "previousJobs") {
- if (values.startDate && values.endDate && values.startDate > values.endDate) {
- return "工作结束日期不能早于开始日期";
- }
- }
- if (this.editor.list === "socialSecurityRecords") {
- if (values.insuredDate && values.stopDate && values.insuredDate > values.stopDate) {
- return "停保日期不能早于参保日期";
- }
- }
- if (this.editor.list === "certificates") {
- if (values.startDate && values.endDate && values.startDate > values.endDate) {
- return "证照有效期结束不能早于开始";
- }
- if (Number(values.hasPositionAllowance) === 1 && !String(values.positionAllowanceAmount || "").trim()) {
- return "请填写岗位津贴金额";
- }
- if (Number(values.hasPositionAllowance) === 1 && !String(values.positionAllowanceUnit || "").trim()) {
- return "请选择岗位津贴单位";
- }
- }
- return null;
- },
- confirmEditor() {
- const error = this.validateEditor();
- if (error) {
- uni.showToast({ title: error, icon: "none" });
- return;
- }
- const { list, index, values } = this.editor;
- const copy = Object.assign({}, values);
- if (Array.isArray(copy.attachment)) copy.attachment = copy.attachment.slice();
- if (index < 0) {
- copy.key = newKey(list);
- this.form[list].push(copy);
- } else {
- const target = this.form[list][index];
- copy.key = target.key || newKey(list);
- this.$set(this.form[list], index, copy);
- }
- if (list === "previousJobs") this.jobsDirty = true;
- this.closeEditor();
- },
- removeItem(group, idx) {
- const item = this.form[group.list][idx];
- const label = this.listItemTitle(group, item, idx);
- uni.showModal({
- title: "删除确认",
- content: `确定删除「${label}」吗?`,
- confirmColor: "#ee4d4d",
- success: (r) => {
- if (!r.confirm) return;
- this.form[group.list].splice(idx, 1);
- if (group.list === "previousJobs") this.jobsDirty = true;
- },
- });
- },
- readonlyList(group) {
- const source = (this.raw && this.raw[group.readonlyList]) || [];
- if (!Array.isArray(source)) return [];
- if (group.readonlyList === "professions") {
- return source.map((item) => ({
- title: item.name || "工种",
- tag: item.level || "",
- lines: [item.code ? `编码:${item.code}` : "", item.type ? `类型:${item.type}` : ""].filter(Boolean),
- }));
- }
- if (group.readonlyList === "skills") {
- return source.map((item) => ({
- title: item.name || "技能",
- tag: item.level || "",
- lines: [item.years != null ? `经验:${item.years} 年` : "", item.remark ? `备注:${item.remark}` : ""].filter(Boolean),
- }));
- }
- return [];
- },
- /* ================= 保存 ================= */
- validate() {
- const f = this.form;
- if (!String(f.name || "").trim()) return "请填写姓名";
- if (!f.groupId) return "请选择所属机构";
- // deptIds 做兜底:档案接口/兜底表单可能不带该字段,直接 .length 会抛错
- if (!(f.deptIds || []).length && !f.deptId) return "请选择隶属部门";
- // 岗位四件套:按「部门 → 序列 → 岗位 → 性质 → 层级」的级联顺序依次校验
- if (!f.positionSequenceId) return "请选择岗位序列";
- if (!f.positionId) return "请选择岗位";
- if (!f.employmentType) return "请选择岗位性质";
- if (!f.staffingLevel) return "请选择岗位层级";
- if (!f.status) return "请选择员工状态";
- if (!f.hireDate) return "请选择入职时间";
- if (f.phone && !/^1[3-9]\d{9}$/.test(String(f.phone).trim())) return "手机号格式不正确";
- if (f.email && !/^[\w.+-]+@[\w-]+\.[\w.-]+$/.test(String(f.email).trim())) return "邮箱格式不正确";
- if (f.idCard && !/^\d{17}[\dXx]$/.test(String(f.idCard).trim())) return "身份证号格式不正确";
- if (f.birthDate && f.hireDate && f.birthDate > f.hireDate) return "入职日期不能早于出生日期";
- if (f.hireDate && f.regularDate && f.hireDate > f.regularDate) return "转正日期不能早于入职日期";
- return null;
- },
- /** 由所选岗位解析 positionList(岗位主键 + 层级主键 + 序列主键) */
- resolvePositionList() {
- const id = this.form.positionId;
- if (!id) return [];
- const pos = this.positionSource.find((p) => String(p.id) === String(id));
- const levelName = this.form.staffingLevel || (pos ? pos.level : "");
- let positionLevelId;
- // 主键一律保持字符串:雪花 id 转 Number 会丢精度(岗位 / 序列都是主键)
- let positionSequenceId = keepIdString(this.form.positionSequenceId);
- (this.sequenceList || []).forEach((sequence) => {
- (sequence.levelList || []).forEach((level) => {
- if (String(level.level) === String(levelName)) {
- if (positionLevelId == null) positionLevelId = level.id;
- if (!positionSequenceId) positionSequenceId = level.sequenceId;
- }
- });
- });
- if (positionLevelId == null && this.rawPositionList.length && String(this.rawPrimaryPositionId) === String(id)) {
- // 层级解析不到且岗位没变:保留原绑定,避免清空
- return this.rawPositionList;
- }
- return [
- compactPayload({
- positionId: keepIdString(id),
- positionLevelId,
- positionSequenceId,
- primaryFlag: 1,
- positionType: this.form.employmentType || undefined,
- positionTypeName: this.form.employmentTypeName || undefined,
- }),
- ];
- },
- async submit() {
- if (this.saving) return;
- const error = this.validate();
- if (error) {
- uni.showToast({ title: error, icon: "none" });
- return;
- }
- const dirtyJobs = (this.form.previousJobs || []).filter(
- (item) => item.company && item.startDate && item.endDate
- );
- if ((this.form.previousJobs || []).length !== dirtyJobs.length) {
- uni.showToast({ title: "工作履历需填写公司、开始与结束日期", icon: "none" });
- return;
- }
- this.saving = true;
- uni.showLoading({ title: "保存中..." });
- try {
- const payload = buildUserPayload(this.form, {
- raw: this.raw,
- positionList: this.resolvePositionList(),
- });
- await saveEmployee(payload);
- const userId = this.form.id || currentUserId();
- if (this.jobsDirty) {
- const saved = await syncWorkExperiences(userId, this.form.previousJobs, this.originalJobs);
- this.form.previousJobs = (saved || []).map((item, index) => ({
- ...item,
- key: item.key || newKey("job"),
- id: item.id,
- }));
- this.originalJobs = (saved || []).map((item) => ({
- id: item.id,
- company: item.company,
- startDate: item.startDate,
- endDate: item.endDate,
- }));
- this.jobsDirty = false;
- }
- await refetchMyProfile();
- uni.hideLoading();
- uni.showToast({ title: "已保存", icon: "success" });
- await this.loadProfile();
- } catch (e) {
- uni.hideLoading();
- uni.showToast({ title: (e && e.message) || "保存失败", icon: "none" });
- } finally {
- this.saving = false;
- }
- },
- resetForm() {
- uni.showModal({
- title: "重置档案",
- content: "将放弃未保存的修改并重新拉取档案,确定继续?",
- success: async (r) => {
- if (!r.confirm) return;
- await this.loadProfile();
- uni.showToast({ title: "已重置", icon: "none" });
- },
- });
- },
- },
- };
- </script>
- <style scoped lang="scss">
- @import "@/pages/hr/common-style.scss";
- .profile-edit {
- padding-bottom: calc(180rpx + env(safe-area-inset-bottom));
- }
- .profile-card {
- margin: 24rpx;
- padding: 34rpx 32rpx 30rpx;
- background: #fff;
- border: 1rpx solid #e8f2ea;
- border-top: 6rpx solid #157a2c;
- }
- .pc-row {
- display: flex;
- align-items: center;
- gap: 16rpx;
- }
- .pc-name {
- font-size: 36rpx;
- font-weight: 700;
- color: #1f2329;
- max-width: 420rpx;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .profile-card .job-number {
- flex: none;
- padding: 5rpx 18rpx;
- border-radius: 999rpx;
- background: #f0f8f2;
- color: #157a2c;
- font-size: 20rpx;
- }
- .pc-main {
- margin-top: 14rpx;
- font-size: 25rpx;
- color: #4b5563;
- }
- .pc-sub {
- display: block;
- margin-top: 6rpx;
- font-size: 22rpx;
- color: #8b939f;
- }
- .tabs {
- white-space: nowrap;
- background: #f5f6f7;
- }
- .tabs-inner {
- display: inline-flex;
- padding: 12rpx 18rpx 4rpx;
- }
- .tab {
- flex: none;
- height: 64rpx;
- line-height: 64rpx;
- padding: 0 28rpx;
- margin-right: 14rpx;
- border-radius: 999rpx;
- background: #fff;
- color: #5f6b7a;
- font-size: 26rpx;
- border: 1rpx solid #eceff2;
- }
- .tab.active {
- background: #157a2c;
- border-color: #157a2c;
- color: #fff;
- font-weight: 600;
- }
- .group-title {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 26rpx 32rpx 12rpx;
- }
- .gt-left {
- display: flex;
- align-items: center;
- }
- .gt-bar {
- width: 6rpx;
- height: 26rpx;
- margin-right: 12rpx;
- border-radius: 3rpx;
- background: #157a2c;
- }
- .gt-label {
- color: #1f2329;
- font-size: 27rpx;
- font-weight: 600;
- }
- .gt-more {
- color: #157a2c;
- font-size: 25rpx;
- }
- .form-card {
- padding: 4rpx 28rpx;
- }
- .field {
- display: flex;
- align-items: center;
- min-height: 96rpx;
- font-size: 28rpx;
- }
- .field .label {
- width: 190rpx;
- flex: none;
- color: #4b5563;
- }
- .field .label.required::before {
- content: "*";
- color: #ee4d4d;
- margin-right: 4rpx;
- }
- .field .value,
- .field .value-input,
- .field .placeholder {
- flex: 1;
- min-width: 0;
- text-align: right;
- color: #1f2329;
- font-size: 28rpx;
- }
- .field .placeholder {
- color: #b5bac3;
- }
- .field .muted {
- color: #8b939f;
- }
- .field-block {
- display: block;
- padding: 20rpx 0 24rpx;
- }
- .field-block .label {
- display: block;
- width: auto;
- margin-bottom: 14rpx;
- }
- .value-textarea {
- width: 100%;
- min-height: 130rpx;
- padding: 0;
- border: 0;
- border-radius: 0;
- background: transparent;
- outline: none;
- color: #1f2329;
- font-size: 28rpx;
- line-height: 1.6;
- box-sizing: border-box;
- }
- .field-file {
- padding: 6rpx 0;
- }
- .value-input {
- height: 96rpx;
- padding: 0;
- border: 0;
- border-radius: 0;
- background: transparent;
- outline: none;
- }
- /* H5 端 uni-input / uni-textarea 内部真实节点兜底:不留边框与底色 */
- :deep(.uni-input-input),
- :deep(.uni-textarea-textarea) {
- border: 0;
- border-radius: 0;
- background: transparent;
- outline: none;
- text-align: inherit;
- font-size: 28rpx;
- color: #1f2329;
- }
- .divider {
- height: 1rpx;
- background: #edf0f2;
- margin-left: 190rpx;
- }
- .arrow {
- flex: none;
- margin-left: 12rpx;
- color: #c4c8ce;
- font-size: 32rpx;
- line-height: 1;
- }
- .switch-wrap {
- flex: 1;
- display: flex;
- align-items: center;
- justify-content: flex-end;
- }
- .switch-text {
- margin-left: 14rpx;
- color: #5f6b7a;
- font-size: 26rpx;
- }
- .list-item {
- padding: 24rpx 28rpx;
- }
- .li-head {
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
- .li-title {
- flex: 1;
- min-width: 0;
- font-size: 29rpx;
- font-weight: 600;
- color: #1f2329;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .li-tag {
- flex: none;
- margin-left: 12rpx;
- padding: 4rpx 14rpx;
- border-radius: 999rpx;
- background: #f0f8f2;
- color: #157a2c;
- font-size: 21rpx;
- }
- .li-actions {
- flex: none;
- display: flex;
- align-items: center;
- gap: 24rpx;
- margin-left: 16rpx;
- }
- .li-action {
- color: #157a2c;
- font-size: 24rpx;
- }
- .li-action.danger {
- color: #ee4d4d;
- }
- .li-line {
- margin-top: 12rpx;
- color: #5f6b7a;
- font-size: 24rpx;
- line-height: 1.6;
- }
- .empty {
- padding: 46rpx 0;
- text-align: center;
- color: #8b939f;
- font-size: 25rpx;
- }
- .record-card {
- padding: 24rpx 28rpx;
- }
- .record-head {
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
- .record-title {
- font-size: 27rpx;
- font-weight: 600;
- color: #1f2329;
- }
- .record-count {
- color: #8b939f;
- font-size: 23rpx;
- }
- .record-line {
- margin-top: 12rpx;
- color: #5f6b7a;
- font-size: 24rpx;
- line-height: 1.6;
- }
- .record-empty {
- margin-top: 12rpx;
- color: #b5bac3;
- font-size: 23rpx;
- }
- .footer-tip {
- padding: 24rpx 32rpx 0;
- color: #a0a7b0;
- font-size: 22rpx;
- text-align: center;
- line-height: 1.6;
- }
- .footer-bar {
- position: fixed;
- left: 0;
- right: 0;
- bottom: 0;
- z-index: 30;
- display: flex;
- gap: 20rpx;
- padding: 20rpx 24rpx calc(20rpx + env(safe-area-inset-bottom));
- background: #fff;
- box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
- }
- .footer-bar > view {
- flex: 1;
- }
- .primary-btn.disabled {
- opacity: 0.6;
- }
- /* 明细编辑弹层 */
- .popup-mask {
- position: fixed;
- left: 0;
- right: 0;
- top: 0;
- bottom: 0;
- z-index: 900;
- background: rgba(0, 0, 0, 0.45);
- display: flex;
- align-items: flex-end;
- }
- .popup {
- width: 100%;
- max-height: 86vh;
- display: flex;
- flex-direction: column;
- border-radius: 24rpx 24rpx 0 0;
- background: #fff;
- }
- .popup-head {
- flex: none;
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 30rpx 32rpx 20rpx;
- border-bottom: 1rpx solid #f2f4f7;
- }
- .popup-title {
- font-size: 31rpx;
- font-weight: 600;
- color: #1f2329;
- }
- .popup-close {
- color: #8b939f;
- font-size: 32rpx;
- }
- .popup-body {
- flex: 1;
- min-height: 0;
- padding: 10rpx 32rpx 20rpx;
- box-sizing: border-box;
- }
- .p-field {
- padding: 24rpx 0;
- border-bottom: 1rpx solid #f2f4f7;
- }
- .p-field:last-child {
- border-bottom: 0;
- }
- .p-label {
- display: block;
- margin-bottom: 12rpx;
- color: #5f6b7a;
- font-size: 24rpx;
- }
- .p-label.required::before {
- content: "*";
- color: #ee4d4d;
- margin-right: 4rpx;
- }
- .p-input,
- .p-textarea {
- width: 100%;
- padding: 0;
- border: 0;
- border-radius: 0;
- background: transparent;
- outline: none;
- color: #1f2329;
- font-size: 28rpx;
- box-sizing: border-box;
- }
- .p-input {
- height: 64rpx;
- line-height: 64rpx;
- }
- .p-textarea {
- min-height: 140rpx;
- line-height: 1.6;
- }
- .p-picker {
- display: flex;
- align-items: center;
- justify-content: space-between;
- min-height: 64rpx;
- padding: 0;
- background: transparent;
- font-size: 28rpx;
- }
- .p-value {
- color: #1f2329;
- }
- .p-placeholder {
- color: #b5bac3;
- }
- .p-file {
- padding: 6rpx 0;
- }
- .popup-actions {
- flex: none;
- display: flex;
- border-top: 1rpx solid #f2f4f7;
- }
- .popup-btn {
- flex: 1;
- height: 96rpx;
- line-height: 96rpx;
- text-align: center;
- color: #5f6b7a;
- font-size: 30rpx;
- }
- .popup-btn.primary {
- color: #157a2c;
- font-weight: 600;
- }
- </style>
|