|
|
@@ -45,8 +45,8 @@
|
|
|
<el-col :xs="24" :sm="12"><el-form-item label="姓名" prop="name"><el-input v-model.trim="form.name"
|
|
|
maxlength="20" placeholder="请输入员工真实姓名" /></el-form-item></el-col>
|
|
|
<el-col :xs="24" :sm="12"><el-form-item label="性别" prop="gender"><el-select v-model="form.gender"
|
|
|
- class="full-width"><el-option v-for="item in genderOptions" :key="item" :label="item"
|
|
|
- :value="item" /></el-select></el-form-item></el-col>
|
|
|
+ class="full-width"><el-option v-for="item in genderOptions" :key="item.value"
|
|
|
+ :label="item.label" :value="item.value" /></el-select></el-form-item></el-col>
|
|
|
<el-col :xs="24" :sm="12"><el-form-item label="身份证" prop="idCard"><el-input v-model.trim="form.idCard"
|
|
|
maxlength="18" placeholder="请输入18位身份证号" @input="handleIdCardInput"
|
|
|
@blur="parseIdCard" /></el-form-item></el-col>
|
|
|
@@ -639,8 +639,11 @@ import {
|
|
|
getDepartmentPositionType,
|
|
|
getDepartmentPositionLevels,
|
|
|
} from '@/api/hr';
|
|
|
-import { listOrganizations, getFactoryAreaPage, getUserPage, EMPLOYEE_STATUS, CERT_TYPE } from '@/api/organization';
|
|
|
+import { listOrganizations, getFactoryAreaPage, getUserPage, EMPLOYEE_STATUS, EMPLOYEE_STATUS_CODE, CERT_TYPE } from '@/api/organization';
|
|
|
import { getDictByCode } from '@/api/sys';
|
|
|
+import { loadEmployeeStatusOptions } from '@/utils/employee-status-dict';
|
|
|
+/* 性别:接口口径 1 男 / 2 女(下拉 value 即接口码);没有值就留空,不提交该字段 */
|
|
|
+import { GENDER_OPTIONS, normalizeGender } from '@/utils/gender';
|
|
|
import { buildOrganizationTree, flattenOrganizationTree } from '@/utils/organization-tree';
|
|
|
import { toDocIds } from '@/components/addDoc/util';
|
|
|
import { positionAllowanceUnitOptions } from '@/enum/dict.js';
|
|
|
@@ -651,14 +654,15 @@ import { getCurrentOrgId, getOrgOptions } from '@/utils/current-org';
|
|
|
|
|
|
const emptyContract = () => ({ category: '', name: '', signDate: '', startDate: '', endDate: '', files: [], status: '生效', remark: '' });
|
|
|
const emptyForm = () => ({
|
|
|
- id: null, avatar: '', name: '', employeeNo: '', gender: '男', birthDate: '', age: null, idCard: '', idCardExpiry: '', ethnic: '汉族',
|
|
|
+ id: null, avatar: '', name: '', employeeNo: '', gender: '', birthDate: '', age: null, idCard: '', idCardExpiry: '', ethnic: '汉族',
|
|
|
maritalStatus: '未婚', politicalStatus: '群众', nativePlace: '', householdType: '', healthStatus: '优',
|
|
|
company: '中盈产业集团有限公司', legalEntity: '', groupId: null, factory: '', factoryId: null, deptId: null, deptIds: [], deptName: '',
|
|
|
position: '', positionId: null, positionIds: [], positionBindings: [],
|
|
|
// 岗位序列(部门 → 序列 → 岗位 → 性质 → 层级,与办理入职一致)
|
|
|
positionSequenceId: '', positionSequence: '', positionSequenceName: '',
|
|
|
positionType: '', level: '', staffingLevel: '', dutyLevel: '', manager: '', managerId: null,
|
|
|
- employmentType: '', employmentTypeName: '', status: '全职', hireDate: '', regularDate: '', account: '', enabled: true,
|
|
|
+ // 新建默认「正式在职」:下拉 value 是 hr_user_status 字典的数字状态码,'1' 即「正式在职」
|
|
|
+ employmentType: '', employmentTypeName: '', status: '1', hireDate: '', regularDate: '', account: '', enabled: true,
|
|
|
phone: '', email: '', wechat: '', workAddress: '', currentAddress: '', registeredAddress: '', emergencyContact: '', emergencyRelation: '', emergencyPhone: '', familyMembers: [],
|
|
|
education: '本科', school: '', major: '', graduationDate: '',
|
|
|
educations: [],
|
|
|
@@ -761,7 +765,8 @@ export default {
|
|
|
managerOptions: [],
|
|
|
managerLoading: false,
|
|
|
ethnicOptions: ['汉族', '土家族', '苗族', '侗族', '回族', '瑶族', '其他'],
|
|
|
- genderOptions: ['男', '女', '未知'],
|
|
|
+ genderOptions: [...GENDER_OPTIONS],
|
|
|
+ // 员工状态下拉:由 hr_user_status 字典填充(见 loadStatusOptions)
|
|
|
statusOptions: [],
|
|
|
maritalOptions: ['未婚', '已婚', '离异', '丧偶'],
|
|
|
educationOptions: ['小学', '初中', '高中', '大专', '本科', '硕士', '博士'],
|
|
|
@@ -998,45 +1003,85 @@ export default {
|
|
|
visible(value) { if (value) this.loadForm(); }
|
|
|
},
|
|
|
methods: {
|
|
|
- /** 员工状态下拉选项:接口字典 hr_user_status(label=中文名,value=状态编码)。
|
|
|
- * 下拉 value 用字典编码,回显按状态码匹配、提交直接传编码;
|
|
|
- * 字典为空或加载失败时,用 EMPLOYEE_STATUS(中文→数字码)兜底,保证一定能选。 */
|
|
|
+ /**
|
|
|
+ * 员工状态下拉选项:直接取 hr_user_status 字典(value = 字典值,即后端 status),
|
|
|
+ * 新增/编辑与列表「状态」筛选共用同一个加载器(utils/employee-status-dict)。
|
|
|
+ * 字典拿不到就明确报错并留空,不再用内置码表静默顶替。
|
|
|
+ */
|
|
|
async loadStatusOptions() {
|
|
|
- const fallback = Object.entries(EMPLOYEE_STATUS).map(([label, code]) => ({ label, value: String(code) }));
|
|
|
try {
|
|
|
- const items = await getDictByCode('hr_user_status');
|
|
|
- const options = (Array.isArray(items) ? items : [])
|
|
|
- .map((item) => {
|
|
|
- const label = String(item.label ?? item.name ?? '').trim();
|
|
|
- const value = item.value ?? item.id ?? item.key;
|
|
|
- if (!label || value == null || value === '') return null;
|
|
|
- return { label, value: String(value) };
|
|
|
- })
|
|
|
- .filter(Boolean);
|
|
|
- this.statusOptions = options.length ? options : fallback;
|
|
|
+ this.statusOptions = await loadEmployeeStatusOptions();
|
|
|
} catch (error) {
|
|
|
- this.statusOptions = fallback;
|
|
|
+ this.statusOptions = [];
|
|
|
+ console.error('[员工档案] 员工状态字典加载失败:', error);
|
|
|
+ this.$message.error(error.message || '员工状态字典加载失败');
|
|
|
}
|
|
|
- // 字典晚于表单加载时,把已回填的中文/旧值重新对齐成字典 value
|
|
|
- if (this.form.status) {
|
|
|
- const hit = this.statusOptions.find(
|
|
|
- (opt) => String(opt.value) === String(this.form.status) || opt.label === this.form.status,
|
|
|
- );
|
|
|
- if (hit) this.form.status = hit.value;
|
|
|
+ // 字典可能在弹框打开之后才回来:用字典值把已回填的值对齐一次(只对齐字典里存在的值)
|
|
|
+ if (this.visible && this.employee) {
|
|
|
+ const next = this.resolveStatusForForm(this.employee);
|
|
|
+ if (this.statusOptions.some((opt) => String(opt.value) === String(next))) {
|
|
|
+ this.form.status = next;
|
|
|
+ }
|
|
|
}
|
|
|
},
|
|
|
- /** 回显员工状态:优先用后端状态码匹配字典 value,其次按中文 label 匹配 */
|
|
|
- resolveStatusForForm(source) {
|
|
|
- const code = source.statusCode ?? source.raw?.status;
|
|
|
- const byCode = code != null && code !== ''
|
|
|
- ? this.statusOptions.find((opt) => String(opt.value) === String(code))
|
|
|
- : null;
|
|
|
- if (byCode) return byCode.value;
|
|
|
+ /**
|
|
|
+ * 回显员工状态:下拉 value 一律是**字典里的数字状态码**,所以按数字码匹配。
|
|
|
+ * ★ 字典实测码空间:1=正式在职、6=试用期、7=待岗、8=已离职(swagger 注释 1-6 与实际不符)。
|
|
|
+ * ① 字典里有该状态码 → 直接用(正常路径,如 status=1 → 「正式在职」);
|
|
|
+ * ② 字典里没有 → 用 EMPLOYEE_STATUS_CODE 把码转成中文再按字典文案匹配,命中即用字典值,并 warn;
|
|
|
+ * ③ 仍命中不了 → 原码原样回显(会显示成数字,一眼能看出字典缺项),不编造文案。
|
|
|
+ * ④ 历史数据没有状态码时才按中文文案反查。
|
|
|
+ * ⚠ 取码只认 `statusCode`(adaptEmployee 里 = 后端原始 status),其次是 `raw.status`:
|
|
|
+ * 千万别读 `source.status`,它在 adaptEmployee 里是列表态
|
|
|
+ * (active / probation / inactive / disabled)。
|
|
|
+ * ⚠ 原实现 `return this.statusOptions[0]` 已删除——两个条件都失配时它会直接回落成
|
|
|
+ * 字典第一项,这正是「列表已离职、编辑里变待入职」的来源。
|
|
|
+ */
|
|
|
+ resolveStatusForForm(source = {}) {
|
|
|
+ const rawCode = source.statusCode ?? source.raw?.status;
|
|
|
+ const code = rawCode == null || rawCode === '' ? null : Number(rawCode);
|
|
|
+ if (code != null && Number.isFinite(code) && code > 0) {
|
|
|
+ const value = String(code);
|
|
|
+ if (this.statusOptions.some((opt) => String(opt.value) === value)) return value;
|
|
|
+ const backendLabel = EMPLOYEE_STATUS_CODE[code] || '';
|
|
|
+ const byDictLabel = backendLabel
|
|
|
+ ? this.statusOptions.find(
|
|
|
+ (opt) => opt.label === backendLabel || opt.label.includes(backendLabel),
|
|
|
+ )
|
|
|
+ : null;
|
|
|
+ if (byDictLabel) {
|
|
|
+ console.warn(
|
|
|
+ `[员工档案] 员工状态字典缺 ${value}(后端语义「${backendLabel}」),已按文案回显为「${byDictLabel.label}」`,
|
|
|
+ );
|
|
|
+ return byDictLabel.value;
|
|
|
+ }
|
|
|
+ console.warn(`[员工档案] 员工状态字典缺 ${value}(后端语义「${backendLabel}」),该状态下拉按原码显示`);
|
|
|
+ return value;
|
|
|
+ }
|
|
|
+ // 历史数据没有状态码时才按中文反查;仍匹配不到就保留原值(不编造一个状态出来)
|
|
|
const byLabel = this.statusOptions.find(
|
|
|
(opt) => opt.label === source.formStatus || opt.label === source.status,
|
|
|
);
|
|
|
if (byLabel) return byLabel.value;
|
|
|
- return this.statusOptions.length ? this.statusOptions[0].value : (source.formStatus || '');
|
|
|
+ return source.formStatus || source.status || '';
|
|
|
+ },
|
|
|
+ /** 离职页签强制写状态时用的值:优先取字典里的「离职」项,字典缺项才退回后端码 */
|
|
|
+ resolveLeaveStatusValue() {
|
|
|
+ const hit = this.statusOptions.find((opt) => opt.label.includes('离职'));
|
|
|
+ if (hit) return hit.value;
|
|
|
+ console.warn('[员工档案] 员工状态字典里没有「离职」项,离职保存将退回后端码 8');
|
|
|
+ return String(EMPLOYEE_STATUS['离职']);
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 回显性别。
|
|
|
+ * ★ 接口码优先:adaptEmployee 里 `raw.sex` 是后端原始码(1 男 / 2 女),
|
|
|
+ * `gender` 只是它转好的中文文案;两者都过 normalizeGender 落到下拉 value('1'/'2')。
|
|
|
+ * 取不到值 → 留空(下拉显示 placeholder),不编造「男」,也不显示「未知」选项。
|
|
|
+ */
|
|
|
+ resolveGenderForForm(source = {}) {
|
|
|
+ const rawSex = source.raw?.sex ?? source.sex;
|
|
|
+ if (rawSex != null && rawSex !== '') return normalizeGender(rawSex);
|
|
|
+ return normalizeGender(source.gender);
|
|
|
},
|
|
|
loadForm() {
|
|
|
this._skipFormValidate = true;
|
|
|
@@ -1051,6 +1096,9 @@ export default {
|
|
|
legalEntity: source.legalEntity || source.company || '',
|
|
|
company: source.company || source.legalEntity || '',
|
|
|
status: this.resolveStatusForForm(source),
|
|
|
+ /* 性别:下拉 value 就是接口码(1 男 / 2 女),回填统一走 normalizeGender;
|
|
|
+ 接口没给值(或值无法识别)就留空,不编造「男」也不显示「未知」 */
|
|
|
+ gender: this.resolveGenderForForm(source),
|
|
|
hireDate: source.hireDate || source.joinDate || '',
|
|
|
factoryId: source.factoryId != null && source.factoryId !== '' ? String(source.factoryId) : null,
|
|
|
factory: source.factory || source.factoryName || '',
|
|
|
@@ -2530,7 +2578,8 @@ export default {
|
|
|
school: firstEducation.school || this.form.school || '',
|
|
|
major: firstEducation.major || this.form.major || '',
|
|
|
graduationDate: firstEducation.graduationDate || this.form.graduationDate || '',
|
|
|
- status: leaveEnabled ? '离职' : this.form.status,
|
|
|
+ // 离职页签一旦启用,状态固定为「离职」;取字典里的离职项值(与下拉同一套值空间)
|
|
|
+ status: leaveEnabled ? this.resolveLeaveStatusValue() : this.form.status,
|
|
|
age: this.calculateAge(this.form.birthDate),
|
|
|
};
|
|
|
// done 必须被调用;用回调而不是依赖 resolve/reject,避免漏调导致永久 await
|