|
|
@@ -19,17 +19,20 @@
|
|
|
<div class="all-organization" :class="{ active: !selectedOrganizationId }" @click="selectAllOrganizations">
|
|
|
<span><i class="el-icon-menu"></i>全部员工</span><em>{{ metricsTotal }}</em>
|
|
|
</div>
|
|
|
- <el-tree ref="organizationTree" :data="organizationTree" node-key="id" :props="treeProps"
|
|
|
- :filter-node-method="filterTreeNode" :expand-on-click-node="false" :default-expand-all="true"
|
|
|
- highlight-current class="organization-tree" @node-click="selectOrganization">
|
|
|
- <span slot-scope="{ data }" class="tree-node-content">
|
|
|
- <span class="tree-node-label">
|
|
|
- <i :class="isCompanyNode(data) ? 'el-icon-office-building' : 'el-icon-folder'" />
|
|
|
- <span>{{ data.name }}</span>
|
|
|
+ <!-- 树滚动区:绝对定位撑满剩余空间,树内容再长也不会把面板撑高(面板高度跟右侧表格走) -->
|
|
|
+ <div class="tree-scroll">
|
|
|
+ <el-tree ref="organizationTree" :data="organizationTree" node-key="id" :props="treeProps"
|
|
|
+ :filter-node-method="filterTreeNode" :expand-on-click-node="false" :default-expand-all="true"
|
|
|
+ highlight-current class="organization-tree" @node-click="selectOrganization">
|
|
|
+ <span slot-scope="{ data }" class="tree-node-content">
|
|
|
+ <span class="tree-node-label">
|
|
|
+ <i :class="isCompanyNode(data) ? 'el-icon-office-building' : 'el-icon-folder'" />
|
|
|
+ <span>{{ data.name }}</span>
|
|
|
+ </span>
|
|
|
+ <em>{{ data.userCount == null ? '—' : data.userCount }}</em>
|
|
|
</span>
|
|
|
- <em>{{ data.userCount == null ? '—' : data.userCount }}</em>
|
|
|
- </span>
|
|
|
- </el-tree>
|
|
|
+ </el-tree>
|
|
|
+ </div>
|
|
|
</template>
|
|
|
<button v-else type="button" class="collapsed-tree-button" @click="treeCollapsed = false">
|
|
|
<i class="el-icon-office-building"></i><span>组织</span>
|
|
|
@@ -81,23 +84,27 @@
|
|
|
</el-row>
|
|
|
</el-form>
|
|
|
<ele-pro-table ref="table" row-key="id" :columns="columns" :datasource="fetchPage" :need-page="true"
|
|
|
- :init-load="false" :page-size="20" :cache-key="$tableCacheKey(cacheKeyUrl)" stripe class="employee-table"
|
|
|
- @selection-change="selectedRows = $event">
|
|
|
+ :init-load="false" :page-size="20" :height="520" :cache-key="$tableCacheKey(cacheKeyUrl)" stripe
|
|
|
+ class="employee-table" @selection-change="selectedRows = $event">
|
|
|
<template v-slot:toolbar>
|
|
|
- <el-button type="primary" icon="el-icon-plus" class="ele-btn-icon" @click="openCreate">
|
|
|
+ <el-button v-if="$hasPermission('main:hrEmployee:save')" type="primary" icon="el-icon-plus"
|
|
|
+ class="ele-btn-icon" @click="openCreate">
|
|
|
新增
|
|
|
</el-button>
|
|
|
- <el-dropdown trigger="click" @command="handleImportCommand">
|
|
|
+ <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 command="template" icon="el-icon-document">下载导入模板</el-dropdown-item>
|
|
|
- <el-dropdown-item command="upload" icon="el-icon-upload2">上传 Excel 导入</el-dropdown-item>
|
|
|
+ <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 trigger="click" @command="handleExport">
|
|
|
+ <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>
|
|
|
@@ -117,25 +124,10 @@
|
|
|
<img v-if="row.avatar" :src="row.avatar" :alt="row.name" />
|
|
|
<span v-else>{{ (row.name || '-').slice(-1) }}</span>
|
|
|
</span>
|
|
|
- <span class="employee-cell-main">
|
|
|
- <!-- 员工姓名:与其它列表页一致的蓝色可点击样式 -->
|
|
|
- <el-link type="primary" :underline="false" @click.stop="openDetail(row)">
|
|
|
- {{ row.name || '-' }}
|
|
|
- </el-link>
|
|
|
- <small>{{ row.employeeNo }}</small>
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- </template>
|
|
|
- <template v-slot:dept="{ row }">
|
|
|
- <div class="department-cell">
|
|
|
- <span>{{ row.deptName }}</span>
|
|
|
- <!-- <small>{{ row.company }}</small> -->
|
|
|
- </div>
|
|
|
- </template>
|
|
|
- <template v-slot:position="{ row }">
|
|
|
- <div class="position-cell">
|
|
|
- <span>{{ row.position }}</span>
|
|
|
- <el-tag v-if="row.level" size="mini" type="info">{{ row.level }}</el-tag>
|
|
|
+ <!-- 姓名:可点击进详情;工号已独立成列,这里不再重复展示 -->
|
|
|
+ <el-link type="primary" :underline="false" @click.stop="openDetail(row)">
|
|
|
+ {{ row.name || '-' }}
|
|
|
+ </el-link>
|
|
|
</div>
|
|
|
</template>
|
|
|
<template v-slot:status="{ row }">
|
|
|
@@ -145,9 +137,14 @@
|
|
|
</el-tag>
|
|
|
<span v-else>-</span>
|
|
|
</template>
|
|
|
+ <template v-slot:enabled="{ row }">
|
|
|
+ <!-- 是否启用:接口只给 isEnabled(0/1),文案由前端渲染成 启用/停用(暂不加后端文案字段) -->
|
|
|
+ <span>{{ row.enabled ? '启用' : '停用' }}</span>
|
|
|
+ </template>
|
|
|
<template v-slot:action="{ row }">
|
|
|
<!-- 员工列已可点开详情,此处移除原「查看」入口 -->
|
|
|
- <el-link type="primary" :underline="false" icon="el-icon-edit" @click.stop="openEdit(row)">
|
|
|
+ <el-link v-if="$hasPermission('main:hrEmployee:update')" type="primary" :underline="false"
|
|
|
+ icon="el-icon-edit" @click.stop="openEdit(row)">
|
|
|
编辑
|
|
|
</el-link>
|
|
|
</template>
|
|
|
@@ -191,6 +188,7 @@ import {
|
|
|
import { writeEmployeeTemplateXlsx, downloadEmployeeTemplate } from '@/utils/employee-template';
|
|
|
/* 员工档案可见机构范围:只取登录缓存所属机构(sessionStorage.loginChangeGroupVOList) */
|
|
|
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 女(筛选项与新增/编辑同一套码,前端按码过滤) */
|
|
|
@@ -226,7 +224,7 @@ export default {
|
|
|
defaultOrganizationId: '',
|
|
|
filters: { keyword: '', status: '', gender: '', hireRange: [] },
|
|
|
appliedFilters: { keyword: '', status: '', gender: '', hireRange: [] },
|
|
|
- cacheKeyUrl: 'hr-pc-employee-archive-v2',
|
|
|
+ cacheKeyUrl: 'hr-pc-employee-archive-v3',
|
|
|
selectedRows: [],
|
|
|
formVisible: false,
|
|
|
editingEmployee: null,
|
|
|
@@ -238,6 +236,7 @@ export default {
|
|
|
employeeNumbersLoaded: false,
|
|
|
/* 导出用字典(工厂 / 班组):人员行只有 factoryId / teamId,名称要查字典;导出时按需加载一次并缓存 */
|
|
|
factoryById: {},
|
|
|
+ factoryDictLoaded: false,
|
|
|
teamById: {},
|
|
|
exportDictLoaded: false
|
|
|
};
|
|
|
@@ -263,18 +262,28 @@ export default {
|
|
|
const count = node?.userCount;
|
|
|
return count == null || count === '' ? null : Number(count);
|
|
|
},
|
|
|
+ /**
|
|
|
+ * 列结构对齐另一套系统的员工列表(2026-10-21 用户拍板):
|
|
|
+ * 姓名 / 所属机构 / 部门 / 工号 / 手机号 / 岗位 / 所属工厂 / 用户账号 /
|
|
|
+ * 性别 / 状态 / 是否启用 / 创建时间 / 操作。
|
|
|
+ * 列结构变了,cacheKeyUrl 要跟着 bump(v3),否则旧的列显隐缓存会串。
|
|
|
+ */
|
|
|
columns() {
|
|
|
return [
|
|
|
{ type: 'selection', width: 56, fixed: 'left', align: 'center', headerAlign: 'center' },
|
|
|
{ type: 'index', label: '序号', width: 60, fixed: 'left', align: 'center', headerAlign: 'center' },
|
|
|
- { prop: 'name', label: '员工', minWidth: 170, fixed: 'left', slot: 'name' },
|
|
|
- { prop: 'deptName', label: '所属部门', minWidth: 150, slot: 'dept' },
|
|
|
- { prop: 'position', label: '岗位 / 职级', minWidth: 230, slot: 'position' },
|
|
|
- { prop: 'hireDate', label: '入职日期', width: 118, align: 'center', sortable: 'custom' },
|
|
|
+ { prop: 'name', label: '姓名', minWidth: 120, fixed: 'left', slot: 'name' },
|
|
|
+ { prop: 'company', label: '所属机构', minWidth: 160, showOverflowTooltip: true },
|
|
|
+ { prop: 'deptNames', label: '部门', minWidth: 150, showOverflowTooltip: true },
|
|
|
+ { prop: 'employeeNo', label: '工号', width: 110, align: 'center' },
|
|
|
{ prop: 'phone', label: '手机号', width: 122, align: 'center' },
|
|
|
+ { prop: 'position', label: '岗位', minWidth: 140, showOverflowTooltip: true },
|
|
|
+ { prop: 'factoryName', label: '所属工厂', minWidth: 120, showOverflowTooltip: true },
|
|
|
+ { prop: 'account', label: '用户账号', width: 120, showOverflowTooltip: true },
|
|
|
{ prop: 'gender', label: '性别', width: 74, align: 'center' },
|
|
|
- { prop: 'age', label: '年龄', width: 74, align: 'center', sortable: 'custom' },
|
|
|
- { prop: 'status', label: '状态', width: 100, align: 'center', slot: 'status' },
|
|
|
+ { prop: 'statusName', label: '状态', width: 100, align: 'center', slot: 'status' },
|
|
|
+ { prop: 'enabled', label: '是否启用', width: 90, align: 'center', slot: 'enabled' },
|
|
|
+ { prop: 'createTime', label: '创建时间', width: 100, align: 'center' },
|
|
|
{ columnKey: 'action', label: '操作', width: 140, fixed: 'right', align: 'left', resizable: false, slot: 'action' }
|
|
|
];
|
|
|
},
|
|
|
@@ -324,6 +333,8 @@ export default {
|
|
|
this.loading = true;
|
|
|
try {
|
|
|
await this.loadOrganizations();
|
|
|
+ // 工厂字典:列表「所属工厂」列翻译 factoryId 用,就绪后再发列表请求(否则首屏该列全空)
|
|
|
+ await this.ensureFactoryDictionary();
|
|
|
// 页面加载只发两个请求:组织列表(含 userCount)+ 当前这一页员工列表。
|
|
|
// 「全部员工」人数、左树人数都取组织列表下发的 userCount,前端不再额外统计。
|
|
|
// 表格 :init-load="false":只在这里发唯一一次列表请求(等 orgById 就绪,部门/公司名映射完整)
|
|
|
@@ -332,6 +343,26 @@ export default {
|
|
|
this.loading = false;
|
|
|
}
|
|
|
},
|
|
|
+ /**
|
|
|
+ * 工厂字典(列表「所属工厂」列翻译 factoryId → 名称)。
|
|
|
+ * 加载失败明确报错,字典缺失时该列留空,不编造名称;下次刷新重试。
|
|
|
+ */
|
|
|
+ async ensureFactoryDictionary() {
|
|
|
+ if (this.factoryDictLoaded || this.exportDictLoaded) return;
|
|
|
+ try {
|
|
|
+ const data = await getFactoryAreaPage();
|
|
|
+ const list = Array.isArray(data) ? data : data?.list || [];
|
|
|
+ const map = {};
|
|
|
+ list.forEach((item) => {
|
|
|
+ if (item?.id == null) return;
|
|
|
+ map[String(item.id)] = item.name || '';
|
|
|
+ });
|
|
|
+ this.factoryById = map;
|
|
|
+ this.factoryDictLoaded = true;
|
|
|
+ } catch (error) {
|
|
|
+ this.$message.error(error?.message || '工厂字典加载失败,所属工厂列暂无数据');
|
|
|
+ }
|
|
|
+ },
|
|
|
attachOrgMeta(employee) {
|
|
|
const deptId = employee.deptId || '';
|
|
|
const groupId = employee.groupId || employee.legalEntityId || '';
|
|
|
@@ -362,6 +393,15 @@ export default {
|
|
|
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('、'),
|
|
|
+ // 所属工厂:人员行只带 factoryId,名称按工厂字典翻译(与导出同源口径),查不到留空
|
|
|
+ factoryName: employee.factoryId != null
|
|
|
+ ? (this.factoryById[String(employee.factoryId)] ?? '')
|
|
|
+ : '',
|
|
|
company: company || employee.company || '',
|
|
|
legalEntity: company || employee.legalEntity || ''
|
|
|
};
|
|
|
@@ -373,7 +413,11 @@ export default {
|
|
|
*/
|
|
|
async loadOrganizations() {
|
|
|
try {
|
|
|
- const list = await listOrganizationsWithUserCount();
|
|
|
+ /* 按文档传 currentUserId(当前登录用户 id),让后端按当前用户口径返回组织列表 */
|
|
|
+ const currentUserId = getCurrentUser()?.id;
|
|
|
+ const list = await listOrganizationsWithUserCount(
|
|
|
+ currentUserId != null ? { currentUserId } : {},
|
|
|
+ );
|
|
|
const records = Array.isArray(list) ? list : [];
|
|
|
const map = {};
|
|
|
records.forEach((item) => { map[String(item.id)] = item; });
|
|
|
@@ -758,8 +802,11 @@ export default {
|
|
|
if (!file) return;
|
|
|
try {
|
|
|
this.loading = true;
|
|
|
- await importUsers(file);
|
|
|
- this.$message.success('导入成功');
|
|
|
+ // 导入走后端「按 HR 任职链路导入」接口,返回 message 时原样展示(如「成功 10 条,失败 2 条」),
|
|
|
+ // 不吞后端的统计/失败原因;没有 message 才用通用成功文案
|
|
|
+ const result = await importUsers(file);
|
|
|
+ const tip = typeof result === 'string' && result.trim() ? result.trim() : '导入成功';
|
|
|
+ this.$message.success(tip);
|
|
|
await this.refreshOrganizations();
|
|
|
this.$refs.table?.reload({ page: 1 });
|
|
|
} catch (error) {
|
|
|
@@ -775,27 +822,31 @@ export default {
|
|
|
*/
|
|
|
async ensureExportDictionaries() {
|
|
|
if (this.exportDictLoaded) return;
|
|
|
- const [factoryData, teamData] = await Promise.all([
|
|
|
- getFactoryAreaPage().catch((error) => {
|
|
|
- throw new Error(`工厂字典加载失败:${error?.message || '未知错误'}`);
|
|
|
- }),
|
|
|
+ // 工厂字典可能已被列表页拉过(factoryDictLoaded),避免重复请求
|
|
|
+ const [factoryList, teamList] = await Promise.all([
|
|
|
+ this.factoryDictLoaded
|
|
|
+ ? Promise.resolve([])
|
|
|
+ : getFactoryAreaPage().catch((error) => {
|
|
|
+ throw new Error(`工厂字典加载失败:${error?.message || '未知错误'}`);
|
|
|
+ }),
|
|
|
listTeams().catch((error) => {
|
|
|
throw new Error(`班组字典加载失败:${error?.message || '未知错误'}`);
|
|
|
})
|
|
|
]);
|
|
|
- const factoryList = Array.isArray(factoryData) ? factoryData : factoryData?.list || [];
|
|
|
- const teamList = Array.isArray(teamData) ? teamData : teamData?.list || [];
|
|
|
- const factoryById = {};
|
|
|
- factoryList.forEach((item) => {
|
|
|
- if (item?.id == null) return;
|
|
|
- factoryById[String(item.id)] = item.name || '';
|
|
|
- });
|
|
|
+ if (!this.factoryDictLoaded) {
|
|
|
+ const factoryById = {};
|
|
|
+ (Array.isArray(factoryList) ? factoryList : factoryList?.list || []).forEach((item) => {
|
|
|
+ if (item?.id == null) return;
|
|
|
+ factoryById[String(item.id)] = item.name || '';
|
|
|
+ });
|
|
|
+ this.factoryById = factoryById;
|
|
|
+ this.factoryDictLoaded = true;
|
|
|
+ }
|
|
|
const teamById = {};
|
|
|
- teamList.forEach((item) => {
|
|
|
+ (Array.isArray(teamList) ? teamList : teamList?.list || []).forEach((item) => {
|
|
|
if (item?.id == null) return;
|
|
|
teamById[String(item.id)] = item.name || '';
|
|
|
});
|
|
|
- this.factoryById = factoryById;
|
|
|
this.teamById = teamById;
|
|
|
this.exportDictLoaded = true;
|
|
|
},
|