瀏覽代碼

员工档案筛选与部门语义拆分:列表「关键词」拆成「工号」「姓名」两个独立条件(getUserPage 只有 jobNumber/name 独立参数,不再一个框并行打两次请求),去掉「性别」筛选,员工状态由筛选下拉改为表格上方分段式 tab(默认在职、末尾固定「全部」,空值不带 status 参数,状态不再进已选条件标签),离职员工靠 isQueryLZ=1 拉取;机构/部门名称一律直读后端字段(deptName/deptNames/groupName),不再拿组织树反查拼凑;表单把「主所属部门」(deptId/deptName 单值) 与「隶属部门」(deptIds/deptNames 多值) 语义拆开,编辑态主部门改只读文本回显接口 deptName,两者互斥(同一部门不能既主又隶属,选中主部门自动从隶属部门剔除),部门级联里机构节点置灰不可选,切换机构清空两处部门选择;性别、身份证改必填,必填 x/y 计数与 rules 对齐(原取 form 上不存在的 bankAccount/insuredDate);所属机构名称缺失时如实标注「机构名称未返回」,不再用 id/company 冒充;详情页拆出「主所属部门」与「隶属部门」两行;adaptEmployee 的 deptId 不再取 deptIds[0] 冒充主部门、deptName 不再回退 deptNames 并补 deptNames;新增员工状态 tab 与工号/姓名输入框宽度样式

lihaiting 6 天之前
父節點
當前提交
6d34666905

+ 9 - 2
src/api/organization/index.js

@@ -882,7 +882,10 @@ export function adaptEmployee(raw = {}, extras = {}) {
         .filter(Boolean),
     ),
   );
-  const deptId = deptIds[0] || "";
+  /* 主所属部门 = 后端 deptId(单值);隶属部门 = deptIds(多值)。
+     ★ 2026-10-28 修正:原先这里写的是 `deptIds[0]`,等于把「隶属部门的第一个」冒充成主所属部门,
+       接口给的 deptId 被丢掉(实例:deptId=财务部、deptIds=[物资部] → 页面主部门显示物资部)。 */
+  const deptId = raw.deptId != null && raw.deptId !== "" ? String(raw.deptId) : "";
   const listStatus = toListStatus(raw);
   const leaveDate = formatDate(raw.leaveDate);
   const positionBindings = adaptPositionBindings(raw);
@@ -942,7 +945,11 @@ export function adaptEmployee(raw = {}, extras = {}) {
       .filter(Boolean),
     deptIds,
     deptId: deptId ? String(deptId) : "",
-    deptName: raw.deptName || raw.deptNames || "",
+    /* 主所属部门 = 后端 deptName;隶属部门(可多个)= 后端 deptNames(字符串,后端已拼好)。
+       2026-10-28 起 deptName 不再回退到 deptNames:两者语义不同(单主部门 vs 多隶属部门),
+       混用会把「财务部、综合部」当成主部门展示。 */
+    deptName: raw.deptName || "",
+    deptNames: raw.deptNames || "",
     position: positionNames.join("、") || raw.postName || "",
     positionIds,
     positionBindings,

+ 10 - 3
src/styles/hr-list-standard.scss

@@ -418,8 +418,15 @@ $hr-list-pages: (
   width: 160px !important;
 }
 
-/* 员工档案:状态/性别为默认搜索框一半宽 */
-.employee-archive-page .ele-form-search .status-item.el-form-item .el-select,
-.employee-archive-page .ele-form-search .gender-item.el-form-item .el-select {
+/* 员工档案:状态筛选为默认搜索框一半宽(性别查询条件已于 2026-10-28 去掉,只留状态) */
+.employee-archive-page .ele-form-search .status-item.el-form-item .el-select {
   width: 110px !important;
 }
+
+/* 员工档案:工号 / 姓名是两个独立查询条件(2026-10-28,各传 jobNumber / name)。
+   默认 220px 两个并排会把筛选区挤到第二行,收窄到 148px;
+   两者都带 name-item 类,先让上面 220px 规则跳过(那个规则排除 name-item) */
+.employee-archive-page .ele-form-search .empno-item.el-form-item .el-input:not(.el-range-editor),
+.employee-archive-page .ele-form-search .name-item.el-form-item .el-input:not(.el-range-editor) {
+  width: 148px !important;
+}

+ 47 - 0
src/styles/views/employeeArchive/index.scss

@@ -44,6 +44,47 @@
   background: #fff;
   box-shadow: 0 9px 28px rgba(40, 67, 106, .06);
 }
+/* 工具栏插槽内的两行容器:第一行操作按钮,第二行员工状态 tab(紧贴表格表头) */
+.table-toolbar-block {
+  display: flex;
+  flex-direction: column;
+  align-items: stretch;
+  width: 100%;
+  min-width: 0;
+}
+/* 员工状态 tab(2026-10-28 用户用局部截图定稿,已按像素量过):
+   分段式按钮组 —— 外层 1px 浅灰描边 + 4px 圆角 + 白底;
+   每段之间 1px 竖分割线;选中只把文字变蓝(不加底色 / 下划线 / 顶部条);
+   实测:整体高约 40px、字号 14px、每段左右留白约 18px。
+   位置:搜索区下方、表格工具栏之外(不在表头区域内) */
+.status-tab-bar {
+  display: inline-flex;
+  align-items: stretch;
+  max-width: 100%;
+  margin: 2px 0 0;
+  overflow: hidden;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  background: #fff;
+}
+.status-tab {
+  height: 40px;
+  padding: 0 18px;
+  border: 0;
+  border-right: 1px solid #e5e5e5;
+  background: #fff;
+  color: #606266;
+  font-size: 14px;
+  font-weight: 400;
+  line-height: 40px;
+  white-space: nowrap;
+  cursor: pointer;
+  transition: color .18s ease, background-color .18s ease;
+}
+.status-tab:last-child { border-right: 0; }
+.status-tab:hover { color: #1890ff; background: #f7fbff; }
+.status-tab:focus { outline: none; }
+.status-tab.active { color: #1890ff; font-weight: 400; background: #fff; }
 /* 面板与右侧表格同高(grid stretch),树要吃掉面板剩余高度,否则底部留一截空白 */
 .organization-panel { display: flex; flex-direction: column; min-width: 0; padding: 22px 18px 18px; transition: width .2s; }
 .organization-panel.collapsed { width: 58px; padding: 14px 8px; }
@@ -73,6 +114,8 @@
 .collapsed-tree-button { width: 40px; min-height: 90px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; border: 0; border-radius: 4px; color: #607087; background: #f4f6f9; cursor: pointer; }
 .collapsed-tree-button span { writing-mode: vertical-rl; font-size: 14px; }
 .table-panel { min-width: 0; overflow: hidden; padding: 14px 0 0; }
+/* 操作按钮行:按钮保持组件默认样式(用户 2026-10-28 明确不要改按钮外观),
+   这里只负责按钮之间的间距——导入/导出被 el-dropdown 包裹,默认相邻 margin 不生效 */
 .table-toolbar-actions { display: flex; align-items: center; justify-content: flex-start; flex-wrap: nowrap; gap: 8px; margin-right: 12px; white-space: nowrap; }
 .selected-count { color: #1890ff; font-size: 14px; }
 .active-filters { min-height: 41px; display: flex; align-items: center; flex-wrap: wrap; gap: 7px; margin: 0 20px 12px; padding: 8px 12px; border: 1px solid #dfebfa; border-radius: 8px; background: #f7faff; }
@@ -146,4 +189,8 @@
   .employee-table ::v-deep .ele-table-tool-title { align-items: flex-start; flex-direction: column; gap: 10px; }
   .table-toolbar-actions { width: 100%; margin-left: 0; overflow-x: auto; justify-content: flex-start; }
   .table-toolbar-actions > * { flex: 0 0 auto; }
+  /* 窄屏:状态 tab 横向滚动,不换行挤压表头 */
+  .status-tab-bar { display: flex; width: 100%; overflow-x: auto; }
+  .status-tab { padding: 0 14px; }
+  .status-tab:last-child { border-right: 0; }
 }

+ 2 - 1
src/views/hrManagement/employeeArchive/components/EmployeeArchiveDetail.vue

@@ -75,7 +75,8 @@
                 <div><span>用户账号</span><strong>{{ employee.account || '-' }}</strong></div>
                 <div><span>所属机构</span><strong>{{ employee.company || employee.legalEntity || '-' }}</strong></div>
                 <div><span>所属工厂</span><strong>{{ employee.factory || '-' }}</strong></div>
-                <div><span>隶属部门</span><strong>{{ employee.deptName || '-' }}</strong></div>
+                <div><span>主所属部门</span><strong>{{ employee.deptName || '-' }}</strong></div>
+                <div><span>隶属部门</span><strong>{{ employee.deptNames || '-' }}</strong></div>
                 <div><span>直属上级</span><strong>{{ employee.manager || '未设置' }}</strong></div>
                 <div><span>入职时间</span><strong>{{ employee.hireDate || '-' }}</strong></div>
                 <div><span>转正日期</span><strong>{{ employee.regularDate || '-' }}</strong></div>

+ 183 - 80
src/views/hrManagement/employeeArchive/components/EmployeeArchiveForm.vue

@@ -80,13 +80,9 @@
 
               <div class="subsection-title"><strong>组织与任职</strong></div>
               <el-row :gutter="16">
-                <el-col :xs="24" :sm="12"><el-form-item label="工号" prop="employeeNo"><el-input
-                      v-model.trim="form.employeeNo" maxlength="20" placeholder="集团全局唯一" disabled><el-button
-                        v-if="!isEdit" slot="append"
-                        @click="generateEmployeeNo">生成</el-button></el-input></el-form-item></el-col>
-                <el-col :xs="24" :sm="12"><el-form-item label="用户账号"><el-input v-model.trim="form.account"
-                      maxlength="30" placeholder="系统登录账号,支持自动生成、手动修改"><template
-                        slot="prepend">@</template></el-input></el-form-item></el-col>
+                <!-- 左列:组织归属 + 岗位链路(机构 → 主所属部门 → 隶属部门 → 岗位序列 → 岗位 → 性质 → 层级)。
+                     用「列容器」而不是 el-col 平铺:主所属部门只在编辑态渲染,平铺会让它之后的字段
+                     整体错位一格,新增态「工号」就会挤回左列。 -->
                 <el-col :xs="24" :sm="12">
                   <el-form-item label="所属机构" prop="groupId">
                     <el-select v-model="form.groupId" filterable class="full-width" placeholder="请选择法人组织"
@@ -95,24 +91,28 @@
                         :value="String(item.id)" />
                     </el-select>
                   </el-form-item>
-                </el-col>
-                <el-col :xs="24" :sm="12">
-                  <el-form-item label="所属工厂">
-                    <el-select v-model="form.factoryId" filterable clearable class="full-width"
-                      :loading="factoryLoading" :disabled="isEdit" placeholder="请选择所属工厂" @change="handleFactoryChange">
-                      <el-option v-for="item in factoryOptions" :key="String(item.id)" :label="item.name"
-                        :value="item.id" />
-                    </el-select>
+                  <!-- 主所属部门:与所属机构级联,树状单选(与 kd-aiot-frontend 的 org-user-edit 同款交互)。
+                       新增态:选项 = 机构节点(置灰不可选)+ 机构下所有部门;选完从隶属部门里剔除,避免重复。
+                       ★ 2026-10-28 编辑态回显改成接口 deptName 文本:原来按 deptId 去部门树里找节点显示名称,
+                         节点匹配不到或撞上机构根节点时就会显示成机构名字(用户实拍:主所属部门显示成机构名)。
+                         名称只认详情接口 deptName,取不到就留空提示,不拿机构名 / 隶属部门拼串顶替。 -->
+                  <el-form-item label="主所属部门" :prop="isEdit ? undefined : 'primaryDeptId'">
+                    <el-input v-if="isEdit" :value="primaryDeptName" disabled
+                      :placeholder="primaryDeptName ? '' : '接口未返回部门名称(deptName),请后端补充'" />
+                    <el-cascader v-else :key="`primary-dept-${form.groupId || 'none'}`" v-model="form.primaryDeptId"
+                      :options="mainDeptOptions" :props="organizationProps" filterable clearable
+                      :show-all-levels="false" :append-to-body="true" popper-class="employee-archive-dept-cascader"
+                      class="full-width" :disabled="!form.groupId"
+                      :placeholder="form.groupId ? '请选择主所属部门' : '请先选择所属机构'"
+                      @change="handlePrimaryDeptChange" />
                   </el-form-item>
-                </el-col>
-                <el-col :xs="24" :sm="12"><el-form-item label="隶属部门" prop="deptIds"><el-cascader
+                  <el-form-item label="隶属部门" prop="deptIds"><el-cascader
                       :key="`dept-cascader-${form.groupId || 'none'}-${deptCascaderKey}`" v-model="form.deptIds"
-                      :options="deptOptions" :props="deptCascaderProps" filterable clearable :show-all-levels="false"
+                      :options="affiDeptOptions" :props="deptCascaderProps" filterable clearable :show-all-levels="false"
                       :append-to-body="true" popper-class="employee-archive-dept-cascader" ref="deptCascader"
                       class="full-width dept-multi-cascader" :disabled="isEdit || !form.groupId"
                       :placeholder="deptCascaderPlaceholder"
-                      @change="handleOrganizationChange" /></el-form-item></el-col>
-                <el-col :xs="24" :sm="12">
+                      @change="handleOrganizationChange" /></el-form-item>
                   <el-form-item label="岗位序列" prop="positionSequenceId">
                     <el-select v-model="form.positionSequenceId" filterable clearable class="full-width"
                       :loading="sequenceLoading" :disabled="isEdit || !hasDept" :placeholder="hasDept ? '请选择岗位序列' : '请先选择隶属部门'"
@@ -121,8 +121,6 @@
                         :value="String(item.id)" />
                     </el-select>
                   </el-form-item>
-                </el-col>
-                <el-col :xs="24" :sm="12">
                   <el-form-item label="岗位" prop="positionId">
                     <el-select :key="`position-select-${positionSelectKey}`" v-model="form.positionId" filterable
                       clearable class="full-width" :loading="positionLoading" :disabled="isEdit || !hasPositionSequence"
@@ -131,21 +129,42 @@
                         :label="positionOptionLabel(item)" :value="String(item.id)" />
                     </el-select>
                   </el-form-item>
-                </el-col>
-                <el-col :xs="24" :sm="12"><el-form-item label="岗位性质" prop="employmentType"><el-select
+                  <el-form-item label="岗位性质" prop="employmentType"><el-select
                       v-model="form.employmentType" filterable
                       clearable class="full-width" :disabled="isEdit" :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>
-                <el-col :xs="24" :sm="12"><el-form-item label="岗位层级" prop="staffingLevel"><el-select
+                    </el-select></el-form-item>
+                  <el-form-item label="岗位层级" prop="staffingLevel"><el-select
                       v-model="form.staffingLevel"
                       class="full-width" :disabled="isEdit || !staffingLevelOptions.length"
                       :placeholder="form.positionId ? '请选择岗位层级' : '请先选择岗位'" @change="handleStaffingLevelChange">
                       <el-option v-for="item in staffingLevelOptions" :key="item" :label="item"
-                        :value="item" /></el-select></el-form-item></el-col>
+                        :value="item" /></el-select></el-form-item>
+                  <!-- 是否启用:2026-10-28 用户要求放到左列末尾(岗位层级之下),与右列时间/状态区分开 -->
+                  <el-form-item label="是否启用">
+                    <div class="enable-field"><el-switch v-model="form.enabled" active-color="#18a875" /><span>{{
+                      form.enabled ? '启用' :
+                        '待岗' }}</span></div>
+                  </el-form-item>
+                </el-col>
+                <!-- 右列:账号、工厂、直属上级与时间状态 -->
                 <el-col :xs="24" :sm="12">
+                  <el-form-item label="用户账号"><el-input v-model.trim="form.account"
+                      maxlength="30" placeholder="系统登录账号,支持自动生成、手动修改"><template
+                        slot="prepend">@</template></el-input></el-form-item>
+                  <el-form-item label="工号" prop="employeeNo"><el-input
+                      v-model.trim="form.employeeNo" maxlength="20" placeholder="集团全局唯一" disabled><el-button
+                        v-if="!isEdit" slot="append"
+                        @click="generateEmployeeNo">生成</el-button></el-input></el-form-item>
+                  <el-form-item label="所属工厂">
+                    <el-select v-model="form.factoryId" filterable clearable class="full-width"
+                      :loading="factoryLoading" :disabled="isEdit" placeholder="请选择所属工厂" @change="handleFactoryChange">
+                      <el-option v-for="item in factoryOptions" :key="String(item.id)" :label="item.name"
+                        :value="item.id" />
+                    </el-select>
+                  </el-form-item>
                   <el-form-item label="直属上级">
                     <el-select v-model="form.managerId" filterable remote clearable class="full-width"
                       :loading="managerLoading" :disabled="isEdit" :remote-method="searchManagers" placeholder="输入姓名/工号搜索全员"
@@ -154,21 +173,16 @@
                         :value="item.id" />
                     </el-select>
                   </el-form-item>
-                </el-col>
-                <el-col :xs="24" :sm="12"><el-form-item label="入职时间" prop="hireDate"><el-date-picker
+                  <el-form-item label="入职时间" prop="hireDate"><el-date-picker
                       v-model="form.hireDate" value-format="yyyy-MM-dd" type="date" placeholder="请选择入职日期"
-                      class="full-width" :disabled="isEdit" /></el-form-item></el-col>
-                <el-col :xs="24" :sm="12"><el-form-item label="转正日期"><el-date-picker v-model="form.regularDate"
+                      class="full-width" :disabled="isEdit" /></el-form-item>
+                  <el-form-item label="转正日期"><el-date-picker v-model="form.regularDate"
                       value-format="yyyy-MM-dd" type="date" placeholder="转正生效日期"
-                      class="full-width" :disabled="isEdit" /></el-form-item></el-col>
-                <el-col :xs="24" :sm="12"><el-form-item label="员工状态" prop="status"><el-select v-model="form.status"
+                      class="full-width" :disabled="isEdit" /></el-form-item>
+                  <el-form-item label="员工状态" prop="status"><el-select v-model="form.status"
                       class="full-width" :disabled="isEdit"><el-option v-for="item in statusOptions" :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="是否启用">
-                    <div class="enable-field"><el-switch v-model="form.enabled" active-color="#18a875" /><span>{{
-                      form.enabled ? '启用' :
-                        '待岗' }}</span></div>
-                  </el-form-item></el-col>
+                        :value="item.value" /></el-select></el-form-item>
+                </el-col>
               </el-row>
 
               <div class="subsection-title"><strong>联系方式</strong></div>
@@ -661,7 +675,11 @@ const emptyContract = () => ({ category: '', name: '', signDate: '', startDate:
 const emptyForm = () => ({
   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: '',
+  /* 所属机构只认 groupId + 登录缓存里的机构名称:不再有写死的公司名 / legalEntity 缓存字段 */
+  groupId: null, factory: '', factoryId: null, deptId: null, deptIds: [], deptName: '',
+  /* 主所属部门:树状单选,回显详情接口的 deptId(deptId/deptName 与 kd-aiot-frontend 同源)。
+     只在前端做选择与去重,**不参与提交**(saveNew 入参没有 deptId)。 */
+  primaryDeptId: '',
   position: '', positionId: null, positionIds: [], positionBindings: [],
   // 岗位序列(部门 → 序列 → 岗位 → 性质 → 层级,与办理入职一致)
   positionSequenceId: '', positionSequence: '', positionSequenceName: '',
@@ -698,13 +716,13 @@ export default {
       else callback(new Error('请输入正确的 11 位手机号码'));
     };
     /**
-     * 身份证只校验位数(15 位纯数字 / 18 位且末位可为 X),与「办理入职」口径一致;
-     * 不再校验出生日期合法性、不再算校验位——避免真实/历史数据被前端拦下。
+     * 身份证:必填(2026-10-28 用户要求),只校验位数(15 位纯数字 / 18 位且末位可为 X),
+     * 与「办理入职」口径一致;不再校验出生日期合法性、不再算校验位——避免真实/历史数据被前端拦下。
      */
     const validateIdCard = (rule, value, callback) => {
       if (this._skipFormValidate) return callback();
       const text = String(value || '').trim();
-      if (!text) return callback();
+      if (!text) return callback(new Error('请输入身份证号码'));
       // 脱敏展示(如 4301********1234)允许通过
       if (/^\d{6}\*{8}[\dXx]{4}$/.test(text)) return callback();
       if (!/^\d{15}$|^\d{17}[\dXx]$/.test(text)) {
@@ -772,7 +790,9 @@ export default {
       rules: {
         name: [{ required: true, message: '请输入员工姓名', trigger: 'blur' }],
         employeeNo: [{ required: true, message: '请输入工号', trigger: 'blur' }, { pattern: /^[A-Za-z0-9_-]{2,20}$/, message: '仅支持字母、数字、下划线和短横线', trigger: 'blur' }, { validator: validateNumber, trigger: 'blur' }],
+        /* 性别:必填(2026-10-28 用户要求)。required 只负责显示必填星号,中文提示由 validator 出 */
         gender: [{
+          required: true,
           validator: (rule, value, callback) => {
             if (this._skipFormValidate) return callback();
             if (!value) callback(new Error('请选择性别'));
@@ -801,6 +821,20 @@ export default {
           },
           trigger: 'change',
         }],
+        /* 主所属部门(form.primaryDeptId):必填,只在新增态校验——编辑态该字段是只读回显文本,
+           用户改不了,若历史数据没主部门会永远卡住保存(编辑态 form-item 的 prop 已置空)。 */
+        primaryDeptId: [{
+          required: true,
+          validator: (rule, value, callback) => {
+            if (this._skipFormValidate) return callback();
+            if (value == null || value === '') {
+              callback(new Error('请选择主所属部门'));
+              return;
+            }
+            callback();
+          },
+          trigger: 'change',
+        }],
         /* 员工状态:必填。required 只负责显示必填星号,实际校验走 validator(_skipFormValidate 时跳过) */
         status: [{
           required: true,
@@ -878,7 +912,8 @@ export default {
         }],
         phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }, { validator: validatePhone, trigger: 'blur' }],
         email: [{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }],
-        idCard: [{ validator: validateIdCard, trigger: ['blur', 'change'] }],
+        /* 身份证:必填(required 只为星号,空值/格式提示都在 validateIdCard 里) */
+        idCard: [{ required: true, validator: validateIdCard, trigger: ['blur', 'change'] }],
       }
     };
   },
@@ -891,34 +926,63 @@ export default {
       }
     },
     isEdit() { return Boolean(this.employee?.id); },
+    /* 编辑态「主所属部门」回显文本:只认详情接口的 deptName(adaptEmployee 直读 raw.deptName)。
+       ★ 不能用 form.deptName:打开弹窗后 normalizeDeptAgainstOptions → applyDeptIds 会把它重写成
+         「隶属部门拼接串」,主部门就显示成隶属部门了(历史 bug,用户实拍物资部/机构名)。 */
+    primaryDeptName() {
+      if (!this.isEdit) return '';
+      return this.employee?.deptName || '';
+    },
+    /* 主所属部门选项:机构节点本身置灰(公司级不可选)+ 其下部门(工厂层已剥掉,与隶属部门同一棵部门树)。
+       与所属机构级联:换机构后 form.groupId 变化 → 这里重算,页面靠 :key 重建级联组件。 */
+    mainDeptOptions() {
+      if (!this.form.groupId) return [];
+      const orgNode = this.findOrgNodeById(this.form.groupId);
+      if (!orgNode) return [];
+      const company = {
+        id: String(orgNode.id),
+        // 根节点名与「所属机构」保持同一来源(编辑态优先接口 groupName)
+        name: this.employee?.company || orgNode.name || '',
+        disabled: true,
+        children: this.deptOptions,
+      };
+      return [company];
+    },
     nameInitial() { return (this.form.name || '新').slice(-1); },
     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 没有名称,会显示成 "1"),回落到组织树里的
-    // 集团/公司节点(type 10/20),保证下拉可读、可选(如「中赢合力」);
-    // 编辑老数据时原机构不在选项里,则补一条兜底选项,避免下拉只显示 ID
+    // 所属机构:名称只认登录缓存「所属机构」(sessionStorage.loginChangeGroupVOList)里的 name。
+    // ★ 2026-10-28 清掉三处编造:名称缺失的机构不进下拉(原来会退化成显示 id),
+    //   编辑态补出的选项也不再用 form.legalEntity / form.company / 裸 id 顶替名称,
+    //   组织树里查不到就如实标注「机构名称未返回」,暴露接口缺口。
     legalEntities() {
-      let list = getOrgOptions().map((item) => ({
-        id: String(item.id),
-        name: item.name
-      }));
-      if (!list.some((item) => item.name)) {
+      let list = getOrgOptions()
+        .map((item) => ({ id: String(item.id), name: item.name || '' }))
+        .filter((item) => item.name);
+      if (!list.length) {
         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;
+        list = flattenOrganizationTree(tree || [])
+          .filter((item) => ['10', '20'].includes(String(item.type)) && item.name)
+          .map((item) => ({ id: String(item.id), name: item.name }));
       }
       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;
-        const hit = flattenOrganizationTree(tree || []).find((item) => String(item.id) === currentId);
-        list.unshift({
-          id: currentId,
-          name: hit?.name || this.form.legalEntity || this.form.company || currentId,
-          type: hit?.type || '20'
-        });
+      /* 编辑态:所属机构的展示名一律以详情接口返回的 groupName(adaptEmployee → company)为准。
+         原来只按 id 去组织树反查名称,命中的节点可能不是机构(实例:机构位被显示成「财务部」),
+         所以接口给了 groupName 就直接用它覆盖同名 id 的选项名称。 */
+      const apiName = this.employee?.company || '';
+      if (currentId) {
+        const idx = list.findIndex((item) => String(item.id) === currentId);
+        if (idx >= 0) {
+          if (apiName) list[idx] = { ...list[idx], name: apiName };
+        } else {
+          const tree = this.organizationTree.length ? this.organizationTree : this.organizations;
+          const hit = flattenOrganizationTree(tree || []).find((item) => String(item.id) === currentId);
+          list.unshift({
+            id: currentId,
+            // 接口 groupName 优先,其次组织树真实名称,都没有就如实标注缺口(不拿 id 冒充)
+            name: apiName || hit?.name || `机构名称未返回(id: ${currentId})`
+          });
+        }
       }
       return list;
     },
@@ -971,6 +1035,18 @@ export default {
     deptCascaderProps() {
       return { ...this.organizationProps, multiple: true };
     },
+    /* 隶属部门选项:部门树里把「主所属部门」那个节点置灰(同一部门不能既是主部门又是隶属部门,
+       与 kd-aiot-frontend 的 affiDeptTree 一致)。只做浅拷贝标记,不动 deptOptions 本身 */
+    affiDeptOptions() {
+      const primaryId = this.form.primaryDeptId == null ? '' : String(this.form.primaryDeptId);
+      const mark = (nodes) =>
+        (nodes || []).map((node) => {
+          const next = { ...node, disabled: Boolean(primaryId) && String(node.id) === primaryId };
+          if (node.children) next.children = mark(node.children);
+          return next;
+        });
+      return mark(this.deptOptions);
+    },
     hasDept() {
       return this.normalizeDeptIds(this.form.deptIds).length > 0;
     },
@@ -978,8 +1054,8 @@ export default {
       return Boolean(this.form.positionSequenceId);
     },
     requiredProgress() {
-      // 与 form rules 一一对应(含隶属部门 / 岗位序列 / 岗位性质 / 岗位层级),保证「必填 x/y」口径准确
-      const keys = ['name', 'employeeNo', 'gender', 'groupId', 'deptIds', 'status', 'hireDate', 'positionSequenceId', 'positionId', 'employmentType', 'staffingLevel', 'phone'];
+      // 与 form rules 一一对应(含主所属部门 / 身份证 / 隶属部门 / 岗位序列 / 岗位性质 / 岗位层级),保证「必填 x/y」口径准确
+      const keys = ['name', 'employeeNo', 'gender', 'idCard', 'groupId', 'primaryDeptId', 'deptIds', 'status', 'hireDate', 'positionSequenceId', 'positionId', 'employmentType', 'staffingLevel', 'phone'];
       return { base: `${keys.filter((key) => { const value = this.form[key]; return Array.isArray(value) ? value.length : Boolean(value); }).length}/${keys.length}` };
     },
     sectionProgress() {
@@ -994,7 +1070,10 @@ export default {
       };
     },
     completionRate() {
-      const values = [this.form.name, this.form.employeeNo, this.form.groupId, this.form.positionId, this.form.status, this.form.hireDate, this.form.phone, this.form.gender, this.form.email, this.form.birthDate, this.form.education, this.form.school, this.form.currentAddress, this.form.contract.number, this.form.bankAccount, this.form.insuredDate, this.form.skills.length, this.form.files.length];
+      /* ★ 2026-10-28 修正:原来取 form.bankAccount / form.insuredDate,这两个字段在 form 上根本不存在
+         (它们是 bankAccounts[] / socialSecurityRecords[] 的行内字段),恒为 false → 完整度最多 16/18、
+         永远填不到 100%。改成按数组是否有行来判断。 */
+      const values = [this.form.name, this.form.employeeNo, this.form.groupId, this.form.positionId, this.form.status, this.form.hireDate, this.form.phone, this.form.gender, this.form.email, this.form.birthDate, this.form.education, this.form.school, this.form.currentAddress, this.form.contract.number, this.form.bankAccounts?.length, this.form.socialSecurityRecords?.length, this.form.skills?.length, this.form.files?.length];
       return Math.round(values.filter(Boolean).length / values.length * 100);
     },
     contractState() { return this.form.contract.name ? (this.form.contract.endDate ? '合同资料已填写' : '待补结束日期') : '待录入合同'; },
@@ -1116,8 +1195,7 @@ export default {
         groupId: source.groupId != null && source.groupId !== ''
           ? String(source.groupId)
           : (source.legalEntityId != null && source.legalEntityId !== '' ? String(source.legalEntityId) : null),
-        legalEntity: source.legalEntity || source.company || '',
-        company: source.company || source.legalEntity || '',
+        // 所属机构名称不再单独缓存到表单(legalEntities 直接读登录缓存 / 组织树),不编造名称
         status: this.resolveStatusForForm(source),
         /* 性别:下拉 value 就是接口码(1 男 / 2 女),回填统一走 normalizeGender;
            接口没给值(或值无法识别)就留空,不编造「男」也不显示「未知」 */
@@ -1127,6 +1205,9 @@ export default {
         factory: source.factory || source.factoryName || '',
         deptIds: this.normalizeDeptIds(source.deptIds?.length ? source.deptIds : source.deptId),
         deptName: source.deptName || '',
+        /* 主所属部门:回显只认详情接口的 deptId(名称由级联组件按树上的节点显示);
+           取不到就留空,不从 deptIds 反查、不拿隶属部门拼串顶替 */
+        primaryDeptId: source.deptId != null && source.deptId !== '' ? String(source.deptId) : '',
         managerId: source.managerId != null && source.managerId !== '' ? String(source.managerId) : null,
         manager: source.manager || '',
         positionIds: this.normalizePositionIds(source).slice(0, 1),
@@ -1766,10 +1847,15 @@ export default {
       );
       this.form.deptIds = list;
       this.form.deptId = list[0] || null;
-      this.form.deptName = list
-        .map((id) => this.findDeptNode(id)?.name)
-        .filter(Boolean)
-        .join('、');
+      /* ★ 2026-10-28 编辑态不重写 deptName:deptName 在详情接口里是「主所属部门」,
+         这里重写成隶属部门拼接串会(a)让主所属部门回显成隶属部门(b)保存时把后端的主部门名覆盖掉。
+         新增态沿用原来「隶属部门名拼接」的提交口径(saveNew 只认 deptName 一个字符串)。 */
+      if (!this.isEdit) {
+        this.form.deptName = list
+          .map((id) => this.findDeptNode(id)?.name)
+          .filter(Boolean)
+          .join('、');
+      }
       if (resetDependents) {
         /* 直属上级不再随部门联动,这里只重置岗位 */
         this.resetPositionsByDepartment();
@@ -1787,8 +1873,6 @@ export default {
           : null) || list[0];
       if (!hit) return;
       this.form.groupId = String(hit.id);
-      this.form.legalEntity = hit.name || '';
-      this.form.company = hit.name || this.form.company;
     },
     handleOrganizationChange(ids) {
       this.applyDeptIds(ids, { resetDependents: true });
@@ -1811,14 +1895,33 @@ export default {
     /* 所属机构切换:只重置依赖机构的部门 / 岗位级联。
        所属工厂与机构、部门互不关联(工厂列表是独立的平铺数据),这里保持已选工厂不动;
        直属上级已改为全员搜索,与机构/部门解耦,同样保持已选直属上级不动 */
-    handleLegalEntityChange(id) {
-      const selected = this.legalEntities.find((item) => String(item.id) === String(id));
-      this.form.legalEntity = selected?.name || '';
-      this.form.company = selected?.name || this.form.company;
+    handleLegalEntityChange() {
       this.deptCascaderKey += 1;
+      /* 机构变了,机构下的部门树整棵换掉 → 已选主所属部门 / 隶属部门都失效,一并清空 */
+      this.form.primaryDeptId = '';
       this.applyDeptIds([]);
       this.resetPositionsByDepartment();
     },
+    /* 主所属部门选中:把该部门从隶属部门里剔除(两者不允许重复,与参考项目 checkedKeys 一致) */
+    handlePrimaryDeptChange(id) {
+      const picked = id == null || id === '' ? '' : String(id);
+      if (!picked) return;
+      const current = this.normalizeDeptIds(this.form.deptIds);
+      const rest = current.filter((item) => String(item) !== picked);
+      if (rest.length !== current.length) {
+        this.applyDeptIds(rest);
+      }
+    },
+    /* 在组织树里按 id 找节点(主所属部门要拿机构节点做置灰根节点) */
+    findOrgNodeById(id, nodes = this.organizationTree.length ? this.organizationTree : this.organizations) {
+      for (const node of nodes || []) {
+        if (!node) continue;
+        if (String(node.id) === String(id)) return node;
+        const matched = this.findOrgNodeById(id, node.children || []);
+        if (matched) return matched;
+      }
+      return null;
+    },
     normalizeDeptAgainstOptions() {
       const ids = this.normalizeDeptIds(
         this.form.deptIds?.length ? this.form.deptIds : this.form.deptId,

+ 162 - 163
src/views/hrManagement/employeeArchive/index.vue

@@ -42,26 +42,18 @@
         <div class="table-panel">
           <el-form label-width="80px" class="ele-form-search" @keyup.enter.native="applyFilters" @submit.native.prevent>
             <el-row :gutter="15">
+              <!-- 工号 / 姓名各自独立成条件(2026-10-28 用户要求):
+                   后端 getUserPage 只有 jobNumber / name 独立参数,没有统一关键词参数,
+                   原先「关键词」一个框要并行打 name + jobNumber 两次请求,现改为各填各的、各传各的。
+                   name-item 只是让全局 220px 宽度规则跳过(hr-list-standard.scss),宽度由本页样式给 -->
               <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
-                <el-form-item label="关键词">
-                  <el-input v-model.trim="filters.keyword" clearable placeholder="姓名 / 工号 / 手机号" />
+                <el-form-item label="工号" class="empno-item name-item">
+                  <el-input v-model.trim="filters.jobNumber" clearable placeholder="请输入员工工号" />
                 </el-form-item>
               </el-col>
               <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
-                <el-form-item label="员工状态" class="status-item">
-                  <!-- 选项来自 hr_user_status 字典,选中值即字典值(直接作为接口 status 参数) -->
-                  <el-select v-model="filters.status" clearable placeholder="请选择" style="width: 100%">
-                    <el-option v-for="item in filterStatusOptions" :key="item.value" :label="item.label"
-                      :value="item.value" />
-                  </el-select>
-                </el-form-item>
-              </el-col>
-              <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
-                <el-form-item label="性别" class="gender-item">
-                  <el-select v-model="filters.gender" clearable placeholder="请选择" style="width: 100%">
-                    <el-option v-for="item in genderOptions" :key="item.value" :label="item.label"
-                      :value="item.value" />
-                  </el-select>
+                <el-form-item label="姓名" class="name-item">
+                  <el-input v-model.trim="filters.name" clearable placeholder="请输入员工姓名" />
                 </el-form-item>
               </el-col>
               <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
@@ -83,40 +75,59 @@
               </el-col>
             </el-row>
           </el-form>
+          <!-- 员工状态 tab(2026-10-28 用户指定按订单页样式):独立方块 tab,位于搜索区下方、
+               表格工具栏(灰色框)之外,不在表头区域内。
+               顺序固定 在职 → 试用期 → 已离职 → 待岗 → 退休,默认「在职」;
+               选项完全来自 hr_user_status 字典,字典里没有的状态不渲染(不编造状态码)。 -->
+          <div v-if="statusTabs.length" class="status-tab-bar">
+            <button v-for="tab in statusTabs" :key="tab.value" type="button" class="status-tab"
+              :class="{ active: String(appliedFilters.status) === String(tab.value) }"
+              @click="switchStatusTab(tab.value)">
+              {{ tab.label }}
+            </button>
+          </div>
           <ele-pro-table ref="table" row-key="id" :columns="columns" :datasource="fetchPage" :need-page="true"
             :init-load="false" :page-size="20" :height="tableHeight" :cache-key="$tableCacheKey(cacheKeyUrl)" stripe
             class="employee-table" @selection-change="selectedRows = $event">
+            <!-- 工具栏插槽:只放操作按钮(状态 tab 在搜索区下方,不在表头区域内) -->
             <template v-slot:toolbar>
-              <el-button v-if="$hasPermission('main:hrEmployee:save')" type="primary" icon="el-icon-plus"
-                class="ele-btn-icon" @click="openCreate">
-                新增
-              </el-button>
-              <el-dropdown v-if="$hasPermission('main:hrEmployee:import')
-                || $hasPermission('main:hrEmployee:importTemplate')" trigger="click" @command="handleImportCommand">
-                <el-button icon="el-icon-upload2" class="ele-btn-icon">
-                  导入<i class="el-icon-arrow-down el-icon--right"></i>
-                </el-button>
-                <el-dropdown-menu slot="dropdown">
-                  <el-dropdown-item v-if="$hasPermission('main:hrEmployee:importTemplate')" command="template"
-                    icon="el-icon-document">下载导入模板</el-dropdown-item>
-                  <el-dropdown-item v-if="$hasPermission('main:hrEmployee:import')" command="upload"
-                    icon="el-icon-upload2">上传 Excel 导入</el-dropdown-item>
-                </el-dropdown-menu>
-              </el-dropdown>
-              <input ref="importInput" type="file" accept=".xlsx,.xls" style="display: none" @change="onImportFile" />
-              <el-dropdown v-if="$hasPermission('main:hrEmployee:export')" trigger="click" @command="handleExport">
-                <el-button icon="el-icon-download" class="ele-btn-icon">
-                  导出<i class="el-icon-arrow-down el-icon--right"></i>
-                </el-button>
-                <el-dropdown-menu slot="dropdown">
-                  <el-dropdown-item command="filtered" icon="el-icon-document">导出当前结果</el-dropdown-item>
-                  <el-dropdown-item command="selected" icon="el-icon-finished"
-                    :disabled="!selectedRows.length">导出已选员工</el-dropdown-item>
-                </el-dropdown-menu>
-              </el-dropdown>
-              <template v-if="selectedRows.length">
-                <span class="selected-count">已选 {{ selectedRows.length }} 人</span>
-              </template>
+              <div class="table-toolbar-block">
+                <div class="table-toolbar-actions">
+                  <el-button v-if="$hasPermission('main:hrEmployee:save')" type="primary" icon="el-icon-plus"
+                    class="ele-btn-icon" @click="openCreate">
+                    新增
+                  </el-button>
+                  <el-dropdown v-if="$hasPermission('main:hrEmployee:import')
+                    || $hasPermission('main:hrEmployee:importTemplate')" trigger="click"
+                    @command="handleImportCommand">
+                    <el-button icon="el-icon-upload2" class="ele-btn-icon">
+                      导入<i class="el-icon-arrow-down el-icon--right"></i>
+                    </el-button>
+                    <el-dropdown-menu slot="dropdown">
+                      <el-dropdown-item v-if="$hasPermission('main:hrEmployee:importTemplate')" command="template"
+                        icon="el-icon-document">下载导入模板</el-dropdown-item>
+                      <el-dropdown-item v-if="$hasPermission('main:hrEmployee:import')" command="upload"
+                        icon="el-icon-upload2">上传 Excel 导入</el-dropdown-item>
+                    </el-dropdown-menu>
+                  </el-dropdown>
+                  <input ref="importInput" type="file" accept=".xlsx,.xls" style="display: none"
+                    @change="onImportFile" />
+                  <el-dropdown v-if="$hasPermission('main:hrEmployee:export')" trigger="click"
+                    @command="handleExport">
+                    <el-button icon="el-icon-download" class="ele-btn-icon">
+                      导出<i class="el-icon-arrow-down el-icon--right"></i>
+                    </el-button>
+                    <el-dropdown-menu slot="dropdown">
+                      <el-dropdown-item command="filtered" icon="el-icon-document">导出当前结果</el-dropdown-item>
+                      <el-dropdown-item command="selected" icon="el-icon-finished"
+                        :disabled="!selectedRows.length">导出已选员工</el-dropdown-item>
+                    </el-dropdown-menu>
+                  </el-dropdown>
+                  <template v-if="selectedRows.length">
+                    <span class="selected-count">已选 {{ selectedRows.length }} 人</span>
+                  </template>
+                </div>
+              </div>
             </template>
             <template v-slot:name="{ row }">
               <div class="employee-cell" @click.stop="openDetail(row)">
@@ -177,7 +188,6 @@ import {
   listWorkExperiences,
   importUsers,
   fetchAllUsers,
-  searchUsersByKeyword,
   getFactoryAreaPage,
   listTeams
 } from '@/api/organization';
@@ -191,8 +201,6 @@ import { getOrgOptions, getCurrentOrgId } from '@/utils/current-org';
 import { getCurrentUser } from '@/utils/token-util';
 /* 「状态」筛选项:与新增/编辑员工档案共用 hr_user_status 字典(value = 字典值 = 后端 status) */
 import { loadEmployeeStatusOptions } from '@/utils/employee-status-dict';
-/* 性别:接口口径 1 男 / 2 女(筛选项与新增/编辑同一套码,前端按码过滤) */
-import { GENDER_OPTIONS, normalizeGender } from '@/utils/gender';
 
 /* 列表「状态」列的文案已改为**直接用接口返回的 statusName**(后端新增字段),
    本地不再维护 active/probation/inactive/disabled 四态文案表(原 STATUS_OPTIONS 已删);
@@ -212,8 +220,6 @@ export default {
       orgById: {},
       /* 「状态」筛选的下拉选项:hr_user_status 字典(与新增/编辑员工档案同一份) */
       filterStatusOptions: [],
-      /* 「性别」筛选项:与新增/编辑同一套接口码(1 男 / 2 女) */
-      genderOptions: [...GENDER_OPTIONS],
       treeProps: { children: 'children', label: 'name' },
       treeKeyword: '',
       treeCollapsed: false,
@@ -224,9 +230,11 @@ export default {
       scopedOrgs: [],
       scopedOrgIds: [],
       defaultOrganizationId: '',
-      filters: { keyword: '', status: '', gender: '', hireRange: [] },
-      appliedFilters: { keyword: '', status: '', gender: '', hireRange: [] },
-      cacheKeyUrl: 'hr-pc-employee-archive-v4',
+      /* 查询条件:工号 / 姓名 / 入职区间。员工状态 2026-10-28 从筛选下拉改为表头上方的 tab,
+         status 字段仍保留在 filters / appliedFilters 里,值由 statusTabs 选中项写入(默认「在职」) */
+      filters: { jobNumber: '', name: '', status: '', hireRange: [] },
+      appliedFilters: { jobNumber: '', name: '', status: '', hireRange: [] },
+      cacheKeyUrl: 'hr-pc-employee-archive-v5',
       selectedRows: [],
       formVisible: false,
       editingEmployee: null,
@@ -275,9 +283,11 @@ export default {
     },
     /**
      * 列结构对齐另一套系统的员工列表(2026-10-21 用户拍板):
-     * 姓名 / 所属机构 / 部门 / 工号 / 手机号 / 岗位 / 所属工厂 / 用户账号 /
+     * 姓名 / 所属机构 / 主所属部门 / 隶属部门 / 工号 / 手机号 / 岗位 / 所属工厂 / 用户账号 /
      * 性别 / 状态 / 是否启用 / 创建时间 / 操作。
-     * 列结构变了,cacheKeyUrl 要跟着 bump(v3),否则旧的列显隐缓存会串。
+     * 2026-10-28 用户指定字段口径:所属机构 = groupName、主所属部门 = deptName(新增列)、
+     * 原「部门」改名「隶属部门」= deptNames。
+     * 列结构变了,cacheKeyUrl 要跟着 bump(v5),否则旧的列显隐缓存会串。
      */
     columns() {
       return [
@@ -285,7 +295,8 @@ export default {
         { type: 'index', label: '序号', width: 60, fixed: 'left', align: 'center', headerAlign: 'center' },
         { prop: 'name', label: '姓名', minWidth: 120, fixed: 'left', slot: 'name' },
         { prop: 'company', label: '所属机构', minWidth: 160, showOverflowTooltip: true },
-        { prop: 'deptNames', label: '部门', minWidth: 150, showOverflowTooltip: true },
+        { prop: 'deptName', label: '主所属部门', minWidth: 130, showOverflowTooltip: true },
+        { prop: 'deptNames', label: '隶属部门', minWidth: 160, showOverflowTooltip: true },
         { prop: 'employeeNo', label: '工号', width: 110, align: 'center' },
         { prop: 'phone', label: '手机号', width: 122, align: 'center' },
         { prop: 'position', label: '岗位', minWidth: 140, showOverflowTooltip: true },
@@ -302,16 +313,47 @@ export default {
       if (!this.selectedOrganizationId) return '全部员工';
       return this.orgById[String(this.selectedOrganizationId)]?.name || '全部员工';
     },
+    /**
+     * 员工状态 tab:顺序 在职 → 试用期 → 已离职 → 待岗 → 退休 → 全部(2026-10-28 用户指定)。
+     * 前五项一律来自 hr_user_status 字典(value 就是接口 status 参数),字典里没有的状态**不渲染**——
+     * 例如字典目前只有 1 正式在职 / 6 试用期 / 7 待岗 / 8 已离职,没有「退休」,
+     * 需要后端在字典里加「退休」项后这个 tab 才会出现(前端不编状态码)。
+     * 最后一项「全部」是前端固定项(value 空串 → 不传 status,查全量)。
+     */
+    statusTabs() {
+      const defs = [
+        { key: 'active', test: (label) => label.includes('在职') && !label.includes('离职') },
+        { key: 'probation', test: (label) => label.includes('试用') },
+        { key: 'resigned', test: (label) => label.includes('离职') },
+        { key: 'idle', test: (label) => label.includes('待岗') },
+        { key: 'retired', test: (label) => label.includes('退休') },
+      ];
+      const options = this.filterStatusOptions || [];
+      const missing = [];
+      const tabs = defs.reduce((list, def) => {
+        const hit = options.find((item) => def.test(String(item.label || '')));
+        if (hit) list.push({ key: def.key, label: hit.label, value: String(hit.value) });
+        else missing.push(def.key);
+        return list;
+      }, []);
+      if (missing.length) {
+        console.warn('[员工档案] 状态 tab 在 hr_user_status 字典中缺少对应项:', missing.join(', '));
+      }
+      /* 末尾固定追加「全部」(2026-10-28 用户要求):value 为空串 → 不带 status 参数,
+         查全量员工(含离职,见 buildBaseParams 的 isQueryLZ) */
+      tabs.push({ key: 'all', label: '全部', value: '' });
+      return tabs;
+    },
+    /** 默认状态(tab 第一项 = 在职):进入页面 / 点重置都回到它 */
+    defaultStatusValue() {
+      return this.statusTabs[0]?.value ?? '';
+    },
     activeFilterTags() {
       const tags = [];
       if (this.selectedOrganizationId) tags.push({ key: 'organization', label: this.currentOrganizationName });
-      if (this.appliedFilters.keyword) tags.push({ key: 'keyword', label: `关键词:${this.appliedFilters.keyword}` });
-      if (this.appliedFilters.status !== '' && this.appliedFilters.status != null) {
-        tags.push({ key: 'status', label: `员工状态:${this.filterStatusLabel(this.appliedFilters.status)}` });
-      }
-      if (this.appliedFilters.gender) {
-        tags.push({ key: 'gender', label: `性别:${this.genderFilterLabel(this.appliedFilters.gender)}` });
-      }
+      if (this.appliedFilters.jobNumber) tags.push({ key: 'jobNumber', label: `工号:${this.appliedFilters.jobNumber}` });
+      if (this.appliedFilters.name) tags.push({ key: 'name', label: `姓名:${this.appliedFilters.name}` });
+      /* 员工状态不再出标签:状态已经由上方 tab 直接表示(2026-10-28) */
       if (this.appliedFilters.hireRange?.length) {
         tags.push({ key: 'hireRange', label: `入职:${this.appliedFilters.hireRange.join(' 至 ')}` });
       }
@@ -327,17 +369,25 @@ export default {
       this.$nextTick(() => this.computeTableHeight());
     }
   },
-  created() {
+  async created() {
     this.syncOrgScope();
-    this.bootstrap();
-    // 字典不阻塞首屏(首屏仍是「组织列表 + 一页员工」两个请求),异步补齐筛选项
-    this.loadFilterStatusOptions();
-    this.handleWindowResize = () => this.computeTableHeight();
-    window.addEventListener('resize', this.handleWindowResize);
-    /* 高度重算的延迟计时器与容器尺寸观察器(非响应式,不走 data) */
+    /* ★ 非响应式的运行时字段必须在这里(第一个 await 之前)初始化:
+       created 是 async 时 Vue 不会等 await 就继续挂载,mounted 会先跑,
+       而 mounted 里已经在用 tableHeightTimers.push —— 放在 await 后面就会
+       报「Cannot read properties of undefined (reading 'push')」。
+       高度重算的延迟计时器与容器尺寸观察器(不走 data,避免无谓响应式) */
     this.tableHeightTimers = [];
     this.tableResizeObserver = null;
     this.tableResizeFrame = 0;
+    this.handleWindowResize = () => this.computeTableHeight();
+    window.addEventListener('resize', this.handleWindowResize);
+    /* 状态 tab 依赖 hr_user_status 字典才能拿到「在职」的码值,所以先取字典再拉首屏;
+       字典失败不阻塞列表(loadFilterStatusOptions 内部已提示,statusTabs 为空时不传 status) */
+    this.loading = true;
+    await this.loadFilterStatusOptions();
+    this.filters.status = this.defaultStatusValue;
+    this.appliedFilters.status = this.defaultStatusValue;
+    this.bootstrap();
   },
   mounted() {
     /* 首帧 + 多次延迟重算:首次进入时外层宿主(嵌入平台 iframe、菜单/tab 容器)
@@ -369,7 +419,7 @@ export default {
   },
   beforeDestroy() {
     window.removeEventListener('resize', this.handleWindowResize);
-    this.tableHeightTimers.forEach((t) => clearTimeout(t));
+    (this.tableHeightTimers || []).forEach((t) => clearTimeout(t));
     this.tableResizeObserver?.disconnect();
     if (this.tableResizeFrame) window.cancelAnimationFrame(this.tableResizeFrame);
     this.tableResizeFrame = 0;
@@ -443,44 +493,21 @@ export default {
     attachOrgMeta(employee) {
       const deptId = employee.deptId || '';
       const groupId = employee.groupId || employee.legalEntityId || '';
-      const deptOrg = this.orgById[String(deptId || '')];
-      const groupOrg = this.orgById[String(groupId || '')];
-      let company = employee.company || employee.legalEntity || '';
-      // profile 常只回 groupId、不回 groupName:用组织表匹配机构名称
-      if ((!company || /^\d+$/.test(String(company))) && groupOrg?.name) {
-        company = groupOrg.name;
-      }
-      if (!company && deptOrg) {
-        let node = deptOrg;
-        const seen = new Set();
-        while (node && !seen.has(String(node.id))) {
-          seen.add(String(node.id));
-          if ([10, 20, '10', '20', 'group', 'company'].includes(node.type)) {
-            company = node.name;
-            break;
-          }
-          const parent = node.parentId != null ? this.orgById[String(node.parentId)] : null;
-          if (!parent) break;
-          node = parent;
-        }
-        if (!company) company = deptOrg.name || '';
-      }
       return {
         ...employee,
         groupId: groupId !== '' && groupId != null ? String(groupId) : null,
         deptId: deptId !== '' && deptId != null ? String(deptId) : '',
-        deptName: employee.deptName || deptOrg?.name || '',
-        // 隶属部门(可多选):deptIds 逐个查组织字典翻译名称;接口没给 id 就留空,不编造
-        deptNames: (employee.deptIds || [])
-          .map((id) => this.orgById[String(id)]?.name)
-          .filter(Boolean)
-          .join('、'),
+        /* 机构 / 部门名称 2026-10-28 用户拍板:全部直接读后端字段,不再拿组织树反查拼凑——
+           所属机构 = groupName(adaptEmployee → company)、主所属部门 = deptName、隶属部门 = deptNames。
+           接口没给就留空暴露出来,前端不做「groupOrg / deptOrg 兜底」也不拿 deptId 翻组织表。 */
+        company: employee.company || '',
+        legalEntity: employee.legalEntity || '',
+        deptName: employee.deptName || '',
+        deptNames: employee.deptNames || '',
         // 所属工厂:人员行只带 factoryId,名称按工厂字典翻译(与导出同源口径),查不到留空
         factoryName: employee.factoryId != null
           ? (this.factoryById[String(employee.factoryId)] ?? '')
-          : '',
-        company: company || employee.company || '',
-        legalEntity: company || employee.legalEntity || ''
+          : ''
       };
     },
     /**
@@ -544,13 +571,9 @@ export default {
       }
     },
     needsClientFilter() {
-      // 状态筛选已改走接口(字典值当 status 参数),不再需要前端全量拉取后过滤
-      const { gender, hireRange, keyword } = this.appliedFilters;
-      return Boolean(
-        gender
-        || hireRange?.length
-        || (keyword && keyword.trim())
-      );
+      // 状态 / 工号 / 姓名 都走接口参数,只有性别、入职区间是前端口径,才需要全量拉取后过滤
+      const { hireRange } = this.appliedFilters;
+      return Boolean(hireRange?.length);
     },
     /** 当前选中组织 id:空串 = 「全部员工」(不传 groupId,全公司口径) */
     scopeGroupId() {
@@ -562,18 +585,24 @@ export default {
       return groupId ? { groupId } : {};
     },
     buildBaseParams() {
-      const { status } = this.appliedFilters;
+      const { status, jobNumber, name } = this.appliedFilters;
       const params = {
         ...this.scopeParams(),
         /* affiDeptId:归属部门 id(2026-10-26 用户指定固定传 1) */
         affiDeptId: 1,
-        // isQueryLZ=1 才会返回离职员工:选中字典里的「离职」项时必须带上,否则筛出来是空的
-        isQueryLZ: this.isLeaveStatusValue(status) ? 1 : 0
+        /* isQueryLZ=1 才会返回离职员工:
+           ① 选中「已离职」字典项 → 必须带,否则筛出来是空的;
+           ② 选中「全部」tab(status 为空串)→ 也带,否则「全部」里看不到离职的人 */
+        isQueryLZ: status === '' || status == null || this.isLeaveStatusValue(status) ? 1 : 0
       };
       // 状态直接用字典值当接口参数(后端 status,string)
       if (status !== '' && status != null) {
         params.status = status;
       }
+      /* 工号 / 姓名:后端 getUserPage 的两个独立模糊查询参数。
+         空值不传(后端对空串的处理不可依赖),也不做任何拼接或改写。 */
+      if (jobNumber) params.jobNumber = jobNumber;
+      if (name) params.name = name;
       return params;
     },
     /** 该筛选值是否「离职」状态(字典码 8 或文案含「离职」),用于决定 isQueryLZ。
@@ -595,49 +624,25 @@ export default {
       }
     },
     applyClientFilters(rows) {
-      // 状态筛选由接口按字典值过滤(见 buildBaseParams),此处只处理性别 / 入职区间 / 关键词
-      const { gender, hireRange, keyword } = this.appliedFilters;
-      const normalized = String(keyword || '').trim().toLowerCase();
+      // 状态 / 工号 / 姓名由接口过滤(见 buildBaseParams),入职区间没有接口参数,只能本地过滤
+      const { hireRange } = this.appliedFilters;
       return rows.filter((employee) => {
-        // 性别按接口码比较(后端码优先,兼容接口已转好的中文);没有性别值的行不属于任何性别筛选
-        if (gender && this.resolveGenderCode(employee) !== String(gender)) return false;
         if (hireRange?.length) {
           if (!employee.hireDate || employee.hireDate < hireRange[0] || employee.hireDate > hireRange[1]) return false;
         }
-        if (normalized) {
-          const haystack = [employee.name, employee.employeeNo, employee.phone, employee.position, employee.deptName, employee.email]
-            .join(' ')
-            .toLowerCase();
-          if (!haystack.includes(normalized)) return false;
-        }
         return true;
       });
     },
     async fetchPage({ page, limit }) {
       try {
         const base = this.buildBaseParams();
-        const { keyword } = this.appliedFilters;
         const clientMode = this.needsClientFilter();
 
         if (clientMode) {
-          let rawList = [];
-          if (keyword?.trim()) {
-            // 关键词:先并行字段检索,再全量兜底过滤岗位等字段
-            const searched = await searchUsersByKeyword(keyword.trim(), {
-              ...base,
-              pageNum: 1,
-              size: 200
-            });
-            rawList = Array.isArray(searched?.list) ? searched.list : [];
-            if (!rawList.length) {
-              const all = await fetchAllUsers(base, { pageSize: 200, maxPages: 30 });
-              rawList = all.list;
-            }
-          } else {
-            const all = await fetchAllUsers(base, { pageSize: 200, maxPages: 30 });
-            rawList = all.list;
-          }
-          let list = rawList.map((item) => this.attachOrgMeta(adaptEmployee(item)));
+          /* 入职区间没有接口参数,只能拉全量再本地过滤;
+             工号 / 姓名 / 状态已在 base 里下发,后端先筛过一轮,这里不再做二次过滤。 */
+          const all = await fetchAllUsers(base, { pageSize: 200, maxPages: 30 });
+          let list = all.list.map((item) => this.attachOrgMeta(adaptEmployee(item)));
           list = this.applyClientFilters(list);
           this.tableTotal = list.length;
           this.existingNumbers = Array.from(
@@ -676,22 +681,7 @@ export default {
       if (text.includes('在职') || text.includes('正式') || text.includes('全职')) return 'success';
       return 'info';
     },
-    /** 「员工状态」筛选值的文案:入参是 hr_user_status 字典值 */
-    filterStatusLabel(value) {
-      const hit = this.filterStatusOptions.find((item) => String(item.value) === String(value));
-      return hit ? hit.label : String(value);
-    },
-    /** 行的性别码:后端原始码优先(adaptEmployee 保留了 raw.sex),其次接口转好的中文文案 */
-    resolveGenderCode(employee = {}) {
-      const rawSex = employee.raw?.sex ?? employee.sex;
-      if (rawSex != null && rawSex !== '') return normalizeGender(rawSex);
-      return normalizeGender(employee.gender);
-    },
-    /** 「性别」筛选值的文案:入参是接口码(1 / 2 / 0) */
-    genderFilterLabel(value) {
-      const hit = GENDER_OPTIONS.find((item) => item.value === String(value));
-      return hit ? hit.label : String(value);
-    },
+    /** 「员工状态」tab 的文案映射不再需要:tab 标签直接用字典 label(原 filterStatusLabel 已删) */
     isCompanyNode(data) {
       return [10, 20, '10', '20', 'group', 'company'].includes(data.type);
     },
@@ -714,6 +704,14 @@ export default {
       this.$refs.organizationTree?.setCurrentKey(null);
       this.$refs.table?.reload({ page: 1 });
     },
+    /** 状态 tab 切换:直接生效(不需要点查询),并回到第一页 */
+    switchStatusTab(value) {
+      const next = value == null ? '' : String(value);
+      if (String(this.appliedFilters.status) === next) return;
+      this.appliedFilters.status = next;
+      this.filters.status = next;
+      this.$refs.table?.reload({ page: 1 });
+    },
     applyFilters() {
       this.appliedFilters = {
         ...this.filters,
@@ -722,8 +720,9 @@ export default {
       this.$refs.table?.reload({ page: 1 });
     },
     resetFilters() {
-      this.filters = { keyword: '', status: '', gender: '', hireRange: [] };
-      this.appliedFilters = { keyword: '', status: '', gender: '', hireRange: [] };
+      /* 重置:清工号 / 姓名 / 入职区间 + 回到「全部员工」,状态回到默认 tab(在职) */
+      this.filters = { jobNumber: '', name: '', status: this.defaultStatusValue, hireRange: [] };
+      this.appliedFilters = { jobNumber: '', name: '', status: this.defaultStatusValue, hireRange: [] };
       this.selectAllOrganizations();
     },
     clearFilterTag(key) {
@@ -929,8 +928,8 @@ export default {
         }
         return;
       }
-      // 「导出当前结果」必须与页面所见完全一致:关键词/性别/入职日期等筛选只有前端口径,
-      // 服务端导出不认这些条件,直接调会把没筛选过的数据导出去(表格信息对不上)→ 本地按模板列导出
+      // 「导出当前结果」必须与页面所见完全一致:性别/入职日期是前端口径、服务端导出不认,
+      // 直接调服务端导出会把没筛选过的数据导出去(表格信息对不上)→ 复用 fetchPage 取数后本地按模板列导出
       try {
         this.loading = true;
         const total = Math.max(Number(this.tableTotal) || 0, 200);