|
|
@@ -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);
|