浏览代码

考勤规则新增了复制操作,员工档案的一些bug

lihaiting 1 周之前
父节点
当前提交
48dc0b4017

+ 0 - 1
src/api/hr/attendance.js

@@ -307,7 +307,6 @@ export const ATTENDANCE_RULE_SCOPE_TYPE_OPTIONS = [
   { value: 'DEPT', label: '部门', priority: 3, tone: 'tag-cyan' },
   { value: 'ORG', label: '机构', priority: 4, tone: 'tag-green' },
   { value: 'FACTORY', label: '工厂', priority: 5, tone: 'tag-amber' },
-  { value: 'LEGAL_ENTITY', label: '法人', priority: 6, tone: 'tag-gray' },
 ];
 
 export const ATTENDANCE_RULE_SCOPE_TYPE_LABEL = ATTENDANCE_RULE_SCOPE_TYPE_OPTIONS.reduce(

+ 28 - 13
src/api/organization/index.js

@@ -720,13 +720,26 @@ export function adaptEmployee(raw = {}, extras = {}) {
         : null,
     manager: raw.managerUserName || "",
     managerJobNumber: raw.managerJobNumber || "",
-    // 岗位性质 / 用工性质:直接取后端 employmentType(position_nature 字典 id),
-    // 兼容历史字段 staffingType / positionType;不再用员工状态码硬映射(语义错且写死)
-    employmentType: raw.employmentType ?? raw.staffingType ?? raw.positionType ?? "",
-    // 岗位性质名称(后端可能返回快照 name;无则用 employmentType 兜底,由详情页决定展示)
-    employmentTypeName: raw.employmentTypeName ?? raw.staffingTypeName ?? raw.positionTypeName ?? "",
+    // 岗位性质 / 用工性质:后端返回 positionType / positionTypeName,已随岗位明细放入 positionList;
+    // 表单内部沿用 employmentType / employmentTypeName 命名,便于下游办理入职/转正/离职复用。
+    // 优先从 positionList[0](主岗位)取,兼容历史顶层字段与 staffingType 兜底。
+    employmentType:
+      raw.positionList?.[0]?.positionType ??
+      raw.positionType ??
+      raw.employmentType ??
+      raw.staffingType ??
+      "",
+    // 岗位性质名称:优先后端 positionTypeName;无则用 positionType 兜底(由详情页决定展示)
+    employmentTypeName:
+      raw.positionList?.[0]?.positionTypeName ??
+      raw.positionTypeName ??
+      raw.employmentTypeName ??
+      raw.staffingTypeName ??
+      "",
     status: listStatus,
     formStatus: toFormStatus(raw),
+    // 后端原始状态码:员工档案表单用 hr_user_status 字典编码回显匹配
+    statusCode: raw.status,
     hireDate: formatDate(raw.joinDate || raw.hireDate || raw.entryDate),
     regularDate: formatDate(raw.positiveDate),
     enabled: Number(raw.isEnabled) !== 0,
@@ -938,6 +951,9 @@ function serializePositionList(form = {}, positionIds = []) {
       positionLevelId: toApiId(binding.positionLevelId),
       positionSequenceId: toApiId(binding.positionSequenceId),
       primaryFlag: 1,
+      // 岗位性质(position_nature 字典 id + 名称快照):随岗位明细一起提交到 positionList
+      positionType: form.employmentType || undefined,
+      positionTypeName: form.employmentTypeName || undefined,
     },
   ];
 }
@@ -973,14 +989,12 @@ function serializeCriticalTaskList(form = {}) {
  * 工作履历走独立接口。学历提交 educationBackground / degree / school / major / graduateDate。
  */
 export function serializeEmployee(form = {}) {
-  const statusText = form.formStatus || form.status;
+  // 员工状态:表单 value 为 hr_user_status 字典编码(数字字符串);旧数据/兜底可能是中文
+  const statusAsNumber = Number(form.status);
   const statusCode =
-    EMPLOYEE_STATUS[statusText] ??
-    (statusText === "inactive" || statusText === "disabled"
-      ? 6
-      : statusText === "probation"
-        ? 5
-        : Number(statusText) || 1);
+    Number.isFinite(statusAsNumber) && statusAsNumber > 0
+      ? statusAsNumber
+      : EMPLOYEE_STATUS[form.status] ?? EMPLOYEE_STATUS[form.formStatus] ?? 1;
 
   const education = pickEducation(form);
   const bankCardList = serializeBankCardList(form);
@@ -1041,7 +1055,7 @@ export function serializeEmployee(form = {}) {
     joinDate: form.hireDate || "",
     positiveDate: form.regularDate || "",
     leaveDate: form.leaveInfo?.leaveDate || "",
-    isEnabled: form.enabled === false || statusText === "disabled" ? 0 : 1,
+    isEnabled: form.enabled === false ? 0 : 1,
     // 银行信息只走 bankCardList 一层,不再在主表重复 openingBank / cardNumber / bankBranch
     bankCardList,
     secondLinkName: form.emergencyContact || "",
@@ -1058,6 +1072,7 @@ export function serializeEmployee(form = {}) {
     managerUserId: toApiId(form.managerId || form.managerUserId),
     postId: positionIds.length ? positionIds.join(",") : undefined,
     postName: form.position || undefined,
+    // 岗位性质(positionType / positionTypeName)已随主岗位明细放入 positionList(见 serializePositionList)
     positionList: serializePositionList(form, positionIds),
     factoryId: toApiId(form.factoryId),
     criticalTaskList: serializeCriticalTaskList(form),

+ 2 - 2
src/utils/current-org.js

@@ -47,11 +47,11 @@ export function getLoginGroups() {
   return list.filter((item) => item && isValidId(item.groupId));
 }
 
-/** 机构下拉选项:[{ id, name }](id 即 groupId,name 即 groupName) */
+/** 机构下拉选项:[{ id, name }](id 即 groupId,name 兼容 groupName / name / organName) */
 export function getOrgOptions() {
   const options = getLoginGroups().map((item) => ({
     id: item.groupId,
-    name: item.groupName || '',
+    name: item.groupName || item.name || item.organName || item.orgName || '',
   }));
   if (options.length) return options;
   /* 兜底:缓存机构列表缺失时至少保留当前登录机构,避免下拉与默认筛选值为空 */

+ 0 - 1
src/views/attendance/components/RuleAssistantPanel.vue

@@ -112,7 +112,6 @@ export default {
   name: 'RuleAssistantPanel',
   props: {
     form: { type: Object, required: true },
-    assistantUserOptions: { type: Array, default: () => [] },
     timezoneOptions: { type: Array, default: () => [] },
     reminderOptions: { type: Array, default: () => [] },
     readonly: { type: Boolean, default: false },

+ 0 - 2
src/views/attendance/components/RuleFormPanels.vue

@@ -50,7 +50,6 @@
     <rule-assistant-panel
       v-else-if="activeTab === 'assistant'"
       :form="form"
-      :assistant-user-options="assistantUserOptions"
       :timezone-options="timezoneOptions"
       :reminder-options="reminderOptions"
       :readonly="readonly"
@@ -99,7 +98,6 @@ export default {
     weekOptions: { type: Array, default: () => [] },
     timeOptions: { type: Array, default: () => [] },
     relatedTargetOptions: { type: Array, default: () => [] },
-    assistantUserOptions: { type: Array, default: () => [] },
     timezoneOptions: { type: Array, default: () => [] },
     reminderOptions: { type: Array, default: () => [] },
     orgOptions: { type: Array, default: () => [] },

+ 45 - 51
src/views/attendance/components/RuleMethodPanel.vue

@@ -6,12 +6,7 @@
           <h3><span class="required">*</span>规则名称</h3>
         </header>
         <el-form-item prop="name">
-          <el-input
-            v-model.trim="form.name"
-            maxlength="64"
-            placeholder="请输入打卡规则名称"
-            :disabled="readonly"
-          />
+          <el-input v-model.trim="form.name" maxlength="64" placeholder="请输入打卡规则名称" :disabled="readonly" />
         </el-form-item>
       </section>
 
@@ -21,41 +16,15 @@
           <p>先选择范围类型,再多选具体对象;提交生效时会检测时间范围冲突</p>
         </header>
         <el-form-item label="范围类型">
-          <el-select
-            v-model="uiScopeType"
-            clearable
-            filterable
-            placeholder="请选择范围类型"
-            :disabled="readonly"
-            class="scope-type-select"
-            @change="onScopeTypeChange"
-          >
-            <el-option
-              v-for="item in scopeTypeOptions"
-              :key="item.value"
-              :label="item.label"
-              :value="item.value"
-            />
+          <el-select v-model="uiScopeType" clearable filterable placeholder="请选择范围类型" :disabled="readonly"
+            class="scope-type-select" @change="onScopeTypeChange">
+            <el-option v-for="item in scopeTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
           </el-select>
         </el-form-item>
         <el-form-item v-if="uiScopeType" :label="scopeTargetLabel">
-          <el-select
-            v-model="selectedScopeIds"
-            multiple
-            clearable
-            filterable
-            :placeholder="scopeTargetPlaceholder"
-            :disabled="readonly"
-            :loading="scopeOptionsLoading"
-            class="scope-target-select"
-            @change="onScopeIdsChange"
-          >
-            <el-option
-              v-for="item in scopeOptions"
-              :key="String(item.id)"
-              :label="item.name"
-              :value="item.id"
-            />
+          <el-select v-model="selectedScopeIds" multiple clearable filterable :placeholder="scopeTargetPlaceholder"
+            :disabled="readonly" :loading="scopeOptionsLoading" class="scope-target-select" @change="onScopeIdsChange">
+            <el-option v-for="item in scopeOptions" :key="String(item.id)" :label="item.name" :value="item.id" />
           </el-select>
         </el-form-item>
       </section>
@@ -91,13 +60,8 @@
           </el-table-column>
         </el-table>
         <el-form-item label="范围外打卡" class="range-form-item">
-          <el-select
-            v-model="form.outOfRange"
-            placeholder="选择范围外打卡策略"
-            style="width:240px"
-            :disabled="readonly"
-            @change="onOutOfRangeChange"
-          >
+          <el-select v-model="form.outOfRange" placeholder="选择范围外打卡策略" style="width:240px" :disabled="readonly"
+            @change="onOutOfRangeChange">
             <el-option label="记录为地点异常" value="异常" />
             <el-option label="允许打卡" value="允许" />
             <el-option label="不允许打卡" value="不允许" />
@@ -116,8 +80,7 @@
           <el-table-column prop="bssid" label="MAC 地址" minWidth="200" />
           <el-table-column v-if="!readonly" label="操作" width="80" align="left">
             <template slot-scope="{ $index }">
-              <el-button type="text" size="mini" class="is-danger"
-                @click="form.wifis.splice($index, 1)">删除</el-button>
+              <el-button type="text" size="mini" class="is-danger" @click="form.wifis.splice($index, 1)">删除</el-button>
             </template>
           </el-table-column>
         </el-table>
@@ -163,8 +126,7 @@
                 <el-button type="text" size="mini" @click="$emit('device-test', row)">连接测试</el-button>
                 <el-button type="text" size="mini" @click="$emit('device-sync', row)">同步数据</el-button>
                 <el-button type="text" size="mini" @click="$emit('device-restart', row)">重启设备</el-button>
-                <el-button type="text" size="mini" class="is-danger"
-                  @click="$emit('device-delete', row)">删除</el-button>
+                <el-button type="text" size="mini" class="is-danger" @click="$emit('device-delete', row)">删除</el-button>
                 <el-button type="text" size="mini" @click="$emit('device-log', row)">日志</el-button>
               </template>
             </template>
@@ -182,7 +144,9 @@ import {
   defaultAttendanceRuleScopePriority,
 } from '@/api/hr/attendance';
 import { getPositionPage } from '@/api/hr';
-import { listOrganizations, fetchAllUsers } from '@/api/organization';
+import { listOrganizations, fetchAllUsers, getFactoryAreaPage } from '@/api/organization';
+/* 机构范围选项:取登录缓存机构列表(loginChangeGroupVOList,即用户所属机构) */
+import { getOrgOptions } from '@/utils/current-org';
 
 const ORG_TYPE_BY_SCOPE = {
   DEPT: ['30'],
@@ -384,6 +348,18 @@ export default {
       }));
     },
     async loadOrganizationOptions(type) {
+      // ORG(机构):只取登录缓存机构列表(用户所属机构,如「中赢合力」),
+      // 与列表页「所属机构」筛选同一口径;之前误用全量组织树,把所有公司/部门都列了出来
+      if (type === 'ORG') {
+        const cached = getOrgOptions();
+        if (cached.length) {
+          return cached.map((item) => ({
+            id: normalizeOptionId(item.id),
+            name: item.name || '未命名组织',
+          }));
+        }
+        // 登录缓存为空时兜底走全量组织树,避免下拉无选项
+      }
       let list = [];
       if (Array.isArray(this.orgOptions) && this.orgOptions.length) {
         list = this.orgOptions.map((item) => ({
@@ -401,11 +377,28 @@ export default {
       }
       const allowedTypes = ORG_TYPE_BY_SCOPE[type];
       if (allowedTypes) {
-        return list.filter((item) => allowedTypes.includes(String(item.type)));
+        const filtered = list.filter((item) => allowedTypes.includes(String(item.type)));
+        // FACTORY:组织树里没有 type=40 的工厂节点时,兜底请求工厂区域接口
+        // (/main/factoryarea/page?type=1,与员工档案「工厂」下拉同一数据源)
+        if (type === 'FACTORY' && !filtered.length) {
+          return this.loadFactoryAreaOptions();
+        }
+        return filtered;
       }
       // ORG:机构,展示全部组织单元
       return list;
     },
+    /** 工厂区域列表(type=1 工厂),组织树无工厂节点时的兜底数据源 */
+    async loadFactoryAreaOptions() {
+      const data = await getFactoryAreaPage({ pageNum: 1, size: 999, type: 1 });
+      const rows = Array.isArray(data?.list) ? data.list : Array.isArray(data) ? data : [];
+      return rows
+        .map((item) => ({
+          id: normalizeOptionId(item.id),
+          name: item.name || item.factoryName || item.pathName || `工厂 ${item.id}`,
+        }))
+        .filter((item) => item.id != null && item.id !== '' && item.name);
+    },
     validate(callback) {
       return this.$refs.form?.validate(callback);
     },
@@ -421,6 +414,7 @@ export default {
 .scope-target-select {
   width: 100%;
 }
+
 .required {
   margin-right: 2px;
   color: #f56c6c;

+ 26 - 6
src/views/attendance/rule/index.vue

@@ -77,11 +77,14 @@
           <el-link type="primary" :underline="false" icon="el-icon-edit" @click="openEdit(row)">
             编辑
           </el-link>
-          <el-link v-if="isRuleActive(row.status)" type="primary" :underline="false" icon="el-icon-video-pause"
+          <el-link type="primary" :underline="false" icon="el-icon-document-copy" @click="handleCopy(row)">
+            复制
+          </el-link>
+          <el-link v-if="isRuleActive(row.status)" type="warning" :underline="false" icon="el-icon-video-pause"
             @click="disableRule(row)">
             停用
           </el-link>
-          <el-link v-else type="primary" :underline="false" icon="el-icon-video-play" @click="enableRule(row)">
+          <el-link v-else type="success" :underline="false" icon="el-icon-video-play" @click="enableRule(row)">
             启用
           </el-link>
           <el-link type="danger" :underline="false" icon="el-icon-delete" @click="handleDelete(row)">
@@ -109,7 +112,7 @@
       <div class="rule-dialog-body">
         <rule-form-panels ref="formPanels" :form="form" :active-tab="activeTab" :week-options="weekOptions"
           :time-options="timeOptions" :related-target-options="relatedTargetOptions"
-          :assistant-user-options="assistantUserOptions" :timezone-options="timezoneOptions"
+          :timezone-options="timezoneOptions"
           :reminder-options="reminderOptions" :org-options="orgOptions" :readonly="dialogReadonly"
           @add-location="addLocation" @add-wifi="addWifi" @add-device="openDeviceCard" @device-view="viewDevice"
           @device-edit="editDevice" @device-test="testDevice" @device-sync="syncDevice" @device-restart="restartDevice"
@@ -272,6 +275,7 @@ import {
   attendanceRuleScopeTypeTone,
   normalizeAttendanceRuleScopes,
   ATTENDANCE_RULE_PUNCH_METHOD_FILTER_OPTIONS,
+  copyAttendanceRule,
 } from '@/api/hr/attendance';
 
 function extractCoordNumbers(text) {
@@ -345,7 +349,6 @@ export default {
         { value: 'device', label: '考勤机' },
         { value: 'app', label: '自有 App' }
       ],
-      assistantUserOptions: ['李亚洪', '王小云', '赵磊', '钱敏', '徐建国', '黄芳'],
       timezoneOptions: [
         '(GMT+08:00)中国标准时间-北京',
         '(GMT+00:00)世界标准时间 UTC',
@@ -373,7 +376,7 @@ export default {
         { prop: 'createTime', label: '创建时间', minWidth: 100, slot: 'createTime' },
         { prop: 'updateUserName', label: '修改人', minWidth: 100, slot: 'updateUserName' },
         { prop: 'updateTime', label: '修改时间', minWidth: 100, slot: 'updateTime' },
-        { prop: 'action', label: '操作', width: 280, slot: 'action', align: 'left', headerAlign: 'left', fixed: 'right' }
+        { prop: 'action', label: '操作', width: 340, slot: 'action', align: 'left', headerAlign: 'left', fixed: 'right' }
       ]
     };
   },
@@ -407,7 +410,7 @@ export default {
           DRAFT: "info",
           EXPIRED: "warning",
           ACTIVE: "success",
-          DISABLED: "danger",
+          DISABLED: "warning",
         }[status] || "info"
       );
     },
@@ -1135,6 +1138,23 @@ export default {
         this.loading = false;
       }
     },
+    handleCopy(row) {
+      if (!row?.id) {
+        this.$message.warning('规则主键不能为空');
+        return;
+      }
+      this.$confirm('确认复制「' + row.name + '」为新草稿?不复制历史版本与考勤结果,复制后请修改编码与生效区间。', '复制考勤规则', { type: 'warning' })
+        .then(async () => {
+          try {
+            // POST /hr/attendance/rules/copyRule/{id}
+            await copyAttendanceRule(row.id);
+            this.$message.success('已复制为新草稿,请修改编码与生效区间');
+            this.$refs.table?.reload?.();
+          } catch (error) {
+            this.$message.error(error.message || '复制失败');
+          }
+        }).catch(() => { });
+    },
     handleDelete(row) {
       if (!row?.id) {
         this.$message.warning('规则主键不能为空');

+ 60 - 18
src/views/hrManagement/employeeArchive/components/EmployeeArchiveForm.vue

@@ -134,7 +134,8 @@
                   </el-form-item>
                 </el-col>
                 <el-col :xs="24" :sm="12"><el-form-item label="岗位性质"><el-select v-model="form.employmentType" filterable
-                      clearable class="full-width" :placeholder="form.positionId ? '请选择岗位性质' : '请先选择岗位'">
+                      clearable class="full-width" :placeholder="form.positionId ? '请选择岗位性质' : '请先选择岗位'"
+                      @change="syncEmploymentTypeName">
                       <el-option v-for="item in employmentOptions" :key="item.key || item.value" :label="item.label"
                         :value="item.value" />
                     </el-select></el-form-item></el-col>
@@ -660,7 +661,7 @@ const emptyForm = () => ({
   // 岗位序列(部门 → 序列 → 岗位 → 性质 → 层级,与办理入职一致)
   positionSequenceId: '', positionSequence: '', positionSequenceName: '',
   positionType: '', level: '', staffingLevel: '', dutyLevel: '', manager: '', managerId: null,
-  employmentType: '', status: '全职', hireDate: '', regularDate: '', account: '', enabled: true,
+  employmentType: '', employmentTypeName: '', status: '全职', hireDate: '', regularDate: '', account: '', enabled: true,
   keyProcess: '',
   phone: '', email: '', wechat: '', workAddress: '', currentAddress: '', registeredAddress: '', emergencyContact: '', emergencyRelation: '', emergencyPhone: '', familyMembers: [],
   education: '本科', school: '', major: '', graduationDate: '',
@@ -871,12 +872,21 @@ export default {
     avatarStyle() { const tones = ['#1890ff', '#13a471', '#7c5ce7', '#d08716', '#d25567']; const code = [...(this.form.name || '新')].reduce((sum, char) => sum + char.charCodeAt(0), 0); return { '--avatar-color': tones[code % tones.length] }; },
     currentSectionLabel() { return this.sections.find((item) => item.name === this.activeTab)?.label || ''; },
     // 所属机构:只取登录缓存所属机构(sessionStorage.loginChangeGroupVOList);
-    // 编辑老数据时原机构不在缓存里,则补一条兜底选项,避免下拉只显示 ID
+    // 登录缓存缺失时(兜底选项只有 id 没有名称,会显示成 "1"),回落到组织树里的
+    // 集团/公司节点(type 10/20),保证下拉可读、可选(如「中赢合力」);
+    // 编辑老数据时原机构不在选项里,则补一条兜底选项,避免下拉只显示 ID
     legalEntities() {
-      const list = getOrgOptions().map((item) => ({
+      let list = getOrgOptions().map((item) => ({
         id: String(item.id),
         name: item.name
       }));
+      if (!list.some((item) => item.name)) {
+        const tree = this.organizationTree.length ? this.organizationTree : this.organizations;
+        const companies = flattenOrganizationTree(tree || [])
+          .filter((item) => ['10', '20'].includes(String(item.type)))
+          .map((item) => ({ id: String(item.id), name: item.name, type: item.type }));
+        if (companies.length) list = companies;
+      }
       const currentId = this.form.groupId != null && this.form.groupId !== '' ? String(this.form.groupId) : '';
       if (currentId && !list.some((item) => String(item.id) === currentId)) {
         const tree = this.organizationTree.length ? this.organizationTree : this.organizations;
@@ -992,26 +1002,51 @@ export default {
     visible(value) { if (value) this.loadForm(); }
   },
   methods: {
-    /** 员工状态下拉选项。
-     *  以 EMPLOYEE_STATUS(中文 → 后端数字状态码,src/api/organization/index.js)为唯一映射真相源:
-     *  - 下拉 value 直接取中文 label,与「回显写中文 / 提交用 EMPLOYEE_STATUS[中文] 转数字」口径一致;
-     *  - 字典 employee_status 仅用于提供并排序中文 label,过滤掉表里没有的项,避免选了却提交成默认值;
-     *  - 字典为空或不含标准项时,用 EMPLOYEE_STATUS 的全部键兜底,保证一定能选。 */
+    /** 员工状态下拉选项:接口字典 hr_user_status(label=中文名,value=状态编码)。
+     *  下拉 value 用字典编码,回显按状态码匹配、提交直接传编码;
+     *  字典为空或加载失败时,用 EMPLOYEE_STATUS(中文→数字码)兜底,保证一定能选。 */
     async loadStatusOptions() {
-      const items = await getDictByCode('employee_status');
-      const dictLabels = (Array.isArray(items) ? items : [])
-        .map((item) => String(item.label ?? item.name ?? '').trim())
-        .filter(Boolean)
-        .filter((label) => Object.prototype.hasOwnProperty.call(EMPLOYEE_STATUS, label));
-      const labels = dictLabels.length ? dictLabels : Object.keys(EMPLOYEE_STATUS);
-      this.statusOptions = labels.map((label) => ({ label, value: label }));
+      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;
+      } catch (error) {
+        this.statusOptions = fallback;
+      }
+      // 字典晚于表单加载时,把已回填的中文/旧值重新对齐成字典 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;
+      }
+    },
+    /** 回显员工状态:优先用后端状态码匹配字典 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;
+      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 || '');
     },
     loadForm() {
       this._skipFormValidate = true;
       this.activeTab = 'base';
       const source = this.employee ? JSON.parse(JSON.stringify(this.employee)) : {};
       const empty = emptyForm();
-      const statusMap = { active: '全职', probation: '试用', inactive: '离职', disabled: '离职' };
       this.form = {
         ...empty, ...source,
         groupId: source.groupId != null && source.groupId !== ''
@@ -1019,7 +1054,7 @@ export default {
           : (source.legalEntityId != null && source.legalEntityId !== '' ? String(source.legalEntityId) : null),
         legalEntity: source.legalEntity || source.company || '',
         company: source.company || source.legalEntity || '',
-        status: source.formStatus || statusMap[source.status] || source.status || '全职',
+        status: this.resolveStatusForForm(source),
         hireDate: source.hireDate || source.joinDate || '',
         factoryId: source.factoryId != null && source.factoryId !== '' ? String(source.factoryId) : null,
         factory: source.factory || source.factoryName || '',
@@ -2166,6 +2201,11 @@ export default {
       );
       return hit ? hit.value : text;
     },
+    /** 岗位性质名称快照:按字典 id 反查 label,随 employmentType 一起提交(positionTypeName) */
+    syncEmploymentTypeName(value) {
+      const hit = this.employmentOptions.find((item) => String(item.value) === String(value));
+      this.form.employmentTypeName = hit ? hit.label : '';
+    },
     // 岗位性质(员工档案字段 employmentType):字典 id 作为选项 value,提交后端即字典 id
     async loadEmploymentTypeOptions() {
       try {
@@ -2184,6 +2224,7 @@ export default {
           .filter(Boolean);
         if (this.form.employmentType) {
           this.form.employmentType = this.resolveEmploymentType(this.form.employmentType);
+          this.syncEmploymentTypeName(this.form.employmentType);
         }
       } catch (error) {
         this.employmentOptions = [];
@@ -2383,6 +2424,7 @@ export default {
       }
       if (natureText && (!prime || !this.form.employmentType)) {
         this.form.employmentType = this.resolveEmploymentType(natureText);
+        this.syncEmploymentTypeName(this.form.employmentType);
       }
       const names = [];
       const idMap = {};