Files
ccdi/ruoyi-ui/src/views/ccdiBaseStaff/index.vue

1692 lines
56 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="100px">
<el-form-item label="姓名" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入姓名"
clearable
style="width: 240px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="柜员号" prop="staffId">
<el-input
v-model="queryParams.staffId"
placeholder="请输入7位柜员号"
clearable
maxlength="7"
@input="queryParams.staffId = queryParams.staffId.replace(/[^\d]/g, '')"
style="width: 240px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="所属部门" prop="deptId">
<treeselect v-model="queryParams.deptId" :options="deptOptions" :show-count="true" placeholder="请选择所属部门" clearable style="width: 240px" />
</el-form-item>
<el-form-item label="身份证号" prop="idCard">
<el-input
v-model="queryParams.idCard"
placeholder="请输入身份证号"
clearable
style="width: 240px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="状态" clearable style="width: 240px">
<el-option label="全部" value="" />
<el-option label="在职" value="0" />
<el-option label="离职" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-hasPermi="['ccdi:employee:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="el-icon-upload2"
size="mini"
@click="handleImport"
v-hasPermi="['ccdi:employee:import']"
>导入</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="el-icon-upload"
size="mini"
@click="handleAssetImport"
v-hasPermi="['ccdi:employee:import']"
>导入资产信息</el-button>
</el-col>
<el-col :span="1.5" v-if="showFailureButton">
<el-tooltip
:content="getLastImportTooltip()"
placement="top"
>
<el-button
type="warning"
plain
icon="el-icon-warning"
size="mini"
@click="viewImportFailures"
>查看导入失败记录</el-button>
</el-tooltip>
</el-col>
<el-col :span="1.5" v-if="assetShowFailureButton">
<el-tooltip
:content="getLastAssetImportTooltip()"
placement="top"
>
<el-button
type="warning"
plain
icon="el-icon-warning-outline"
size="mini"
@click="viewAssetImportFailures"
>查看员工资产导入失败记录</el-button>
</el-tooltip>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="baseStaffList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="姓名" align="center" prop="name" :show-overflow-tooltip="true"/>
<el-table-column label="柜员号" align="center" prop="staffId" :show-overflow-tooltip="true"/>
<el-table-column label="身份证号" align="center" prop="idCard" :show-overflow-tooltip="true"/>
<el-table-column label="所属部门" align="center" prop="deptName" :show-overflow-tooltip="true"/>
<el-table-column label="电话" align="center" prop="phone" width="120"/>
<el-table-column label="状态" align="center" prop="status" width="100">
<template slot-scope="scope">
<el-tag v-if="scope.row.status === '0'" type="success">在职</el-tag>
<el-tag v-else type="danger">离职</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-view"
@click="handleDetail(scope.row)"
v-hasPermi="['ccdi:employee:query']"
>详情</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['ccdi:employee:edit']"
>编辑</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['ccdi:employee:remove']"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改对话框 -->
<el-dialog :title="title" :visible.sync="open" width="80%" append-to-body class="employee-edit-dialog">
<el-form ref="form" :model="form" :rules="rules" label-width="90px">
<!-- 基本信息 -->
<div class="section-header">基本信息</div>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="姓名" prop="name">
<el-input v-model="form.name" placeholder="请输入姓名" maxlength="100" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="柜员号" prop="staffId" v-if="!form.staffId || isAdd">
<el-input
v-model="form.staffId"
placeholder="请输入7位柜员号"
maxlength="7"
@input="form.staffId = form.staffId.replace(/[^\d]/g, '')"
/>
</el-form-item>
<el-form-item label="柜员号" prop="staffId" v-else>
<el-input v-model="form.staffId" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="所属部门" prop="deptId">
<treeselect v-model="form.deptId" :options="enabledDeptOptions" :show-count="true" placeholder="请选择所属部门" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="身份证号" prop="idCard">
<el-input v-model="form.idCard" placeholder="请输入身份证号" maxlength="18" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="电话" prop="phone">
<el-input v-model="form.phone" placeholder="请输入电话" maxlength="11" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="入职时间" prop="hireDate">
<el-date-picker v-model="form.hireDate" type="date" placeholder="选择入职时间" value-format="yyyy-MM-dd" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio label="0">在职</el-radio>
<el-radio label="1">离职</el-radio>
</el-radio-group>
</el-form-item>
<div class="section-header">
<span>资产信息</span>
<el-button type="primary" plain size="mini" icon="el-icon-plus" @click="handleAddAsset">新增资产</el-button>
</div>
<div class="assets-helper">
<div>新增编辑时无需填写实际持有人身份证号</div>
<div>系统会默认带入并保留已有归属信息</div>
</div>
<el-form-item label-width="0" prop="assetInfoList">
<div v-if="!form.assetInfoList || !form.assetInfoList.length" class="empty-assets">
<i class="el-icon-office-building"></i>
<span>暂无资产信息请点击新增资产</span>
</div>
<div v-else class="assets-table-wrapper">
<el-table :data="form.assetInfoList" border class="assets-table">
<el-table-column label="资产大类" min-width="140">
<template slot-scope="scope">
<el-input v-model="scope.row.assetMainType" placeholder="请输入资产大类" />
</template>
</el-table-column>
<el-table-column label="资产小类" min-width="140">
<template slot-scope="scope">
<el-input v-model="scope.row.assetSubType" placeholder="请输入资产小类" />
</template>
</el-table-column>
<el-table-column label="资产名称" min-width="180">
<template slot-scope="scope">
<el-input v-model="scope.row.assetName" placeholder="请输入资产名称" />
</template>
</el-table-column>
<el-table-column label="产权占比" min-width="120">
<template slot-scope="scope">
<el-input v-model="scope.row.ownershipRatio" placeholder="请输入产权占比" />
</template>
</el-table-column>
<el-table-column label="购买/评估日期" min-width="160">
<template slot-scope="scope">
<el-date-picker v-model="scope.row.purchaseEvalDate" type="date" value-format="yyyy-MM-dd" placeholder="选择日期" style="width: 100%" />
</template>
</el-table-column>
<el-table-column label="资产原值" min-width="120">
<template slot-scope="scope">
<el-input v-model="scope.row.originalValue" placeholder="请输入资产原值" />
</template>
</el-table-column>
<el-table-column label="当前估值" min-width="120">
<template slot-scope="scope">
<el-input v-model="scope.row.currentValue" placeholder="请输入当前估值" />
</template>
</el-table-column>
<el-table-column label="估值截止日期" min-width="160">
<template slot-scope="scope">
<el-date-picker v-model="scope.row.valuationDate" type="date" value-format="yyyy-MM-dd" placeholder="选择日期" style="width: 100%" />
</template>
</el-table-column>
<el-table-column label="资产状态" min-width="140">
<template slot-scope="scope">
<el-select v-model="scope.row.assetStatus" placeholder="请选择资产状态">
<el-option
v-for="option in assetStatusOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="备注" min-width="180">
<template slot-scope="scope">
<el-input v-model="scope.row.remarks" placeholder="请输入备注" />
</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template slot-scope="scope">
<el-button type="text" size="mini" icon="el-icon-delete" @click="handleRemoveAsset(scope.$index)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="cancel">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</div>
</el-dialog>
<!-- 详情对话框 -->
<el-dialog title="员工详情" :visible.sync="detailOpen" width="80%" append-to-body class="employee-detail-dialog">
<div class="detail-container">
<!-- 基本信息卡片 -->
<div class="info-section">
<div class="section-title">
<i class="el-icon-user"></i>
<span>基本信息</span>
</div>
<el-descriptions :column="2" border>
<el-descriptions-item label="姓名">{{ employeeDetail.name || '-' }}</el-descriptions-item>
<el-descriptions-item label="柜员号">{{ employeeDetail.staffId || '-' }}</el-descriptions-item>
<el-descriptions-item label="所属部门">{{ employeeDetail.deptName || '-' }}</el-descriptions-item>
<el-descriptions-item label="身份证号">{{ employeeDetail.idCard || '-' }}</el-descriptions-item>
<el-descriptions-item label="电话">{{ employeeDetail.phone || '-' }}</el-descriptions-item>
<el-descriptions-item label="入职时间">
{{ employeeDetail.hireDate ? parseTime(employeeDetail.hireDate, '{y}-{m}-{d}') : '-' }}
</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag v-if="employeeDetail.status === '0'" type="success" size="small">在职</el-tag>
<el-tag v-else type="danger" size="small">离职</el-tag>
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ employeeDetail.createTime ? parseTime(employeeDetail.createTime) : '-' }}
</el-descriptions-item>
</el-descriptions>
</div>
<div class="info-section">
<div class="section-title">
<i class="el-icon-office-building"></i>
<span>资产信息</span>
</div>
<div v-if="!employeeDetail.assetInfoList || !employeeDetail.assetInfoList.length" class="empty-assets-detail">
暂无资产信息
</div>
<el-table v-else :data="employeeDetail.assetInfoList" border class="detail-assets-table">
<el-table-column label="资产实际持有人身份证号" prop="personId" min-width="220" />
<el-table-column label="归属类型" prop="ownerType" min-width="100">
<template slot-scope="scope">
<span>{{ formatAssetOwnerType(scope.row) }}</span>
</template>
</el-table-column>
<el-table-column label="资产大类" prop="assetMainType" min-width="120" />
<el-table-column label="资产小类" prop="assetSubType" min-width="120" />
<el-table-column label="资产名称" prop="assetName" min-width="140" />
<el-table-column label="产权占比" prop="ownershipRatio" min-width="100" />
<el-table-column label="当前估值" prop="currentValue" min-width="120" />
<el-table-column label="资产状态" prop="assetStatus" min-width="120" />
<el-table-column label="备注" prop="remarks" min-width="160" show-overflow-tooltip />
</el-table>
</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="detailOpen = false" icon="el-icon-close"> </el-button>
</div>
</el-dialog>
<!-- 导入对话框 -->
<el-dialog :title="upload.title" :visible.sync="upload.open" width="400px" append-to-body @close="handleImportDialogClose" v-loading="upload.isUploading" element-loading-text="正在导入数据,请稍候..." element-loading-spinner="el-icon-loading" element-loading-background="rgba(0, 0, 0, 0.7)">
<el-upload
ref="upload"
:limit="1"
accept=".xlsx, .xls"
:headers="upload.headers"
:action="upload.url + '?updateSupport=' + upload.updateSupport"
:disabled="upload.isUploading"
:on-progress="handleFileUploadProgress"
:on-success="handleFileSuccess"
:auto-upload="false"
drag
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<div class="el-upload__tip" slot="tip">
<el-checkbox v-model="upload.updateSupport" />是否更新已经存在的员工数据
<el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline;" @click="importTemplate">下载模板</el-link>
</div>
<div class="el-upload__tip" slot="tip">
<span>仅允许导入"xls""xlsx"格式文件</span>
</div>
</el-upload>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitFileForm" :loading="upload.isUploading"> </el-button>
<el-button @click="upload.open = false" :disabled="upload.isUploading"> </el-button>
</div>
</el-dialog>
<!-- 导入结果对话框 -->
<import-result-dialog
:visible.sync="importResultVisible"
:content="importResultContent"
title="导入结果"
@close="handleImportResultClose"
/>
<el-dialog :title="assetUpload.title" :visible.sync="assetUpload.open" width="400px" append-to-body @close="handleAssetImportDialogClose" v-loading="assetUpload.isUploading" element-loading-text="正在导入员工资产数据,请稍候..." element-loading-spinner="el-icon-loading" element-loading-background="rgba(0, 0, 0, 0.7)">
<el-upload
ref="assetUpload"
:limit="1"
accept=".xlsx, .xls"
:headers="assetUpload.headers"
:action="assetUpload.url"
:disabled="assetUpload.isUploading"
:on-progress="handleAssetFileUploadProgress"
:on-success="handleAssetFileSuccess"
:auto-upload="false"
drag
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<div class="el-upload__tip" slot="tip">
<el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline;" @click="importAssetTemplate">下载员工资产模板</el-link>
</div>
<div class="el-upload__tip" slot="tip">
<span>仅允许导入"xls""xlsx"格式文件系统将根据 personId/person_id 自动识别归属员工</span>
</div>
</el-upload>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitAssetFileForm" :loading="assetUpload.isUploading"> </el-button>
<el-button @click="assetUpload.open = false" :disabled="assetUpload.isUploading"> </el-button>
</div>
</el-dialog>
<import-result-dialog
:visible.sync="assetImportResultVisible"
:content="assetImportResultContent"
title="员工资产导入结果"
@close="handleAssetImportResultClose"
/>
<!-- 导入失败记录对话框 -->
<el-dialog
title="导入失败记录"
:visible.sync="failureDialogVisible"
width="1200px"
append-to-body
>
<el-alert
v-if="lastImportInfo"
:title="lastImportInfo"
type="info"
:closable="false"
style="margin-bottom: 15px"
/>
<el-table :data="failureList" v-loading="failureLoading">
<el-table-column label="姓名" prop="name" align="center" />
<el-table-column label="柜员号" prop="staffId" align="center" />
<el-table-column label="身份证号" prop="idCard" align="center" />
<el-table-column label="电话" prop="phone" align="center" />
<el-table-column label="失败原因" prop="errorMessage" align="center" min-width="200" :show-overflow-tooltip="true" />
</el-table>
<pagination
v-show="failureTotal > 0"
:total="failureTotal"
:page.sync="failureQueryParams.pageNum"
:limit.sync="failureQueryParams.pageSize"
@pagination="getFailureList"
/>
<div slot="footer" class="dialog-footer">
<el-button @click="failureDialogVisible = false">关闭</el-button>
<el-button type="danger" plain @click="clearImportHistory">清除历史记录</el-button>
</div>
</el-dialog>
<el-dialog
title="员工资产导入失败记录"
:visible.sync="assetFailureDialogVisible"
width="1200px"
append-to-body
>
<el-alert
v-if="lastAssetImportInfo"
:title="lastAssetImportInfo"
type="info"
:closable="false"
style="margin-bottom: 15px"
/>
<el-table :data="assetFailureList" v-loading="assetFailureLoading">
<el-table-column label="归属员工身份证号" prop="familyId" align="center" min-width="180" />
<el-table-column label="资产实际持有人身份证号" prop="personId" align="center" min-width="180" />
<el-table-column label="资产大类" prop="assetMainType" align="center" />
<el-table-column label="资产小类" prop="assetSubType" align="center" />
<el-table-column label="资产名称" prop="assetName" align="center" />
<el-table-column label="失败原因" prop="errorMessage" align="center" min-width="200" :show-overflow-tooltip="true" />
</el-table>
<pagination
v-show="assetFailureTotal > 0"
:total="assetFailureTotal"
:page.sync="assetFailureQueryParams.pageNum"
:limit.sync="assetFailureQueryParams.pageSize"
@pagination="getAssetFailureList"
/>
<div slot="footer" class="dialog-footer">
<el-button @click="assetFailureDialogVisible = false">关闭</el-button>
<el-button type="danger" plain @click="clearAssetImportHistory">清除资产导入历史</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
addBaseStaff,
delBaseStaff,
getBaseStaff,
getImportFailures,
getImportStatus,
listBaseStaff,
updateBaseStaff
} from "@/api/ccdiBaseStaff";
import {
getAssetImportFailures,
getAssetImportStatus
} from "@/api/ccdiAssetInfo";
import {deptTreeSelect} from "@/api/system/user";
import {getToken} from "@/utils/auth";
import Treeselect from "@riophae/vue-treeselect";
import "@riophae/vue-treeselect/dist/vue-treeselect.css";
import ImportResultDialog from "@/components/ImportResultDialog.vue";
// 身份证号校验正则
const idCardPattern = /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/;
// 手机号校验正则
const phonePattern = /^1[3|4|5|6|7|8|9][0-9]\d{8}$/;
export default {
name: "Employee",
components: { Treeselect, ImportResultDialog },
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 员工表格数据
baseStaffList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 是否显示详情弹出层
detailOpen: false,
// 员工详情
employeeDetail: {},
// 是否为新增操作
isAdd: false,
// 所有部门树选项
deptOptions: undefined,
// 过滤掉已禁用部门树选项
enabledDeptOptions: undefined,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
name: null,
staffId: null,
deptId: null,
idCard: null,
status: null
},
// 表单参数
form: {},
assetStatusOptions: [
{ label: "正常", value: "正常" },
{ label: "冻结", value: "冻结" },
{ label: "处置中", value: "处置中" },
{ label: "报废", value: "报废" }
],
// 表单校验
rules: {
name: [
{ required: true, message: "姓名不能为空", trigger: "blur" },
{ max: 100, message: "姓名长度不能超过100个字符", trigger: "blur" }
],
staffId: [
{ required: true, message: "柜员号不能为空", trigger: "blur" },
{ pattern: /^\d{7}$/, message: "柜员号必须为7位数字", trigger: "blur" }
],
deptId: [
{ required: true, message: "所属部门不能为空", trigger: "change" }
],
idCard: [
{ required: true, message: "身份证号不能为空", trigger: "blur" },
{ pattern: idCardPattern, message: "请输入正确的18位身份证号", trigger: "blur" }
],
phone: [
{ required: true, message: "电话不能为空", trigger: "blur" },
{ pattern: phonePattern, message: "请输入正确的11位手机号", trigger: "blur" }
],
status: [
{ required: true, message: "请选择状态", trigger: "change" }
]
},
// 导入参数
upload: {
// 是否显示弹出层
open: false,
// 弹出层标题
title: "",
// 是否禁用上传
isUploading: false,
// 是否更新已经存在的数据
updateSupport: 0,
// 设置上传的请求头部
headers: { Authorization: "Bearer " + getToken() },
// 上传的地址
url: process.env.VUE_APP_BASE_API + "/ccdi/baseStaff/importData"
},
// 导入结果弹窗
importResultVisible: false,
importResultContent: "",
assetImportResultVisible: false,
assetImportResultContent: "",
// 轮询定时器
pollingTimer: null,
assetPollingTimer: null,
// 是否显示查看失败记录按钮
showFailureButton: false,
assetShowFailureButton: false,
// 当前导入任务ID
currentTaskId: null,
assetCurrentTaskId: null,
// 失败记录对话框
failureDialogVisible: false,
failureList: [],
failureLoading: false,
failureTotal: 0,
failureQueryParams: {
pageNum: 1,
pageSize: 10
},
assetUpload: {
open: false,
title: "",
isUploading: false,
headers: { Authorization: "Bearer " + getToken() },
url: process.env.VUE_APP_BASE_API + "/ccdi/assetInfo/importData"
},
assetFailureDialogVisible: false,
assetFailureList: [],
assetFailureLoading: false,
assetFailureTotal: 0,
assetFailureQueryParams: {
pageNum: 1,
pageSize: 10
}
};
},
computed: {
/**
* 上次导入信息摘要
*/
lastImportInfo() {
const savedTask = this.getImportTaskFromStorage();
if (savedTask && savedTask.totalCount) {
return `导入时间: ${this.parseTime(savedTask.saveTime)} | 总数: ${savedTask.totalCount}条 | 成功: ${savedTask.successCount}条 | 失败: ${savedTask.failureCount}`;
}
return '';
},
lastAssetImportInfo() {
const savedTask = this.getAssetImportTaskFromStorage();
if (savedTask && savedTask.totalCount) {
return `导入时间: ${this.parseTime(savedTask.saveTime)} | 总数: ${savedTask.totalCount}条 | 成功: ${savedTask.successCount}条 | 失败: ${savedTask.failureCount}`;
}
return '';
}
},
watch: {
'form.idCard'(newIdCard, oldIdCard) {
this.syncAssetPersonIds(newIdCard, oldIdCard);
}
},
created() {
this.getList();
this.getDeptTree();
this.restoreImportState(); // 新增:恢复导入状态
this.restoreAssetImportState();
},
beforeDestroy() {
// 组件销毁时清除定时器
if (this.pollingTimer) {
clearInterval(this.pollingTimer);
this.pollingTimer = null;
}
if (this.assetPollingTimer) {
clearInterval(this.assetPollingTimer);
this.assetPollingTimer = null;
}
},
methods: {
/**
* 保存导入任务到localStorage
* @param {Object} taskData - 任务数据
*/
saveImportTaskToStorage(taskData) {
try {
const data = {
...taskData,
saveTime: Date.now()
};
localStorage.setItem('employee_import_last_task', JSON.stringify(data));
} catch (error) {
console.error('保存导入任务状态失败:', error);
}
},
/**
* 从localStorage读取导入任务
* @returns {Object|null} 任务数据或null
*/
getImportTaskFromStorage() {
try {
const data = localStorage.getItem('employee_import_last_task');
if (!data) return null;
const task = JSON.parse(data);
// 数据格式校验
if (!task || !task.taskId) {
this.clearImportTaskFromStorage();
return null;
}
// 时间戳校验
if (task.saveTime && typeof task.saveTime !== 'number') {
this.clearImportTaskFromStorage();
return null;
}
// 过期检查(7天)
const sevenDays = 7 * 24 * 60 * 60 * 1000;
if (Date.now() - task.saveTime > sevenDays) {
this.clearImportTaskFromStorage();
return null;
}
return task;
} catch (error) {
console.error('读取导入任务状态失败:', error);
this.clearImportTaskFromStorage();
return null;
}
},
/**
* 清除localStorage中的导入任务
*/
clearImportTaskFromStorage() {
try {
localStorage.removeItem('employee_import_last_task');
} catch (error) {
console.error('清除导入任务状态失败:', error);
}
},
saveAssetImportTaskToStorage(taskData) {
try {
const data = {
...taskData,
saveTime: Date.now()
};
localStorage.setItem('employee_asset_import_last_task', JSON.stringify(data));
} catch (error) {
console.error('保存资产导入任务状态失败:', error);
}
},
getAssetImportTaskFromStorage() {
try {
const data = localStorage.getItem('employee_asset_import_last_task');
if (!data) return null;
const task = JSON.parse(data);
if (!task || !task.taskId) {
this.clearAssetImportTaskFromStorage();
return null;
}
const sevenDays = 7 * 24 * 60 * 60 * 1000;
if (task.saveTime && Date.now() - task.saveTime > sevenDays) {
this.clearAssetImportTaskFromStorage();
return null;
}
return task;
} catch (error) {
console.error('读取资产导入任务状态失败:', error);
this.clearAssetImportTaskFromStorage();
return null;
}
},
clearAssetImportTaskFromStorage() {
try {
localStorage.removeItem('employee_asset_import_last_task');
} catch (error) {
console.error('清除资产导入任务状态失败:', error);
}
},
/**
* 恢复导入状态
* 在created()钩子中调用
*/
restoreImportState() {
const savedTask = this.getImportTaskFromStorage();
if (!savedTask) {
this.showFailureButton = false;
this.currentTaskId = null;
return;
}
// 如果有失败记录,恢复按钮显示
if (savedTask.hasFailures && savedTask.taskId) {
this.currentTaskId = savedTask.taskId;
this.showFailureButton = true;
}
},
restoreAssetImportState() {
const savedTask = this.getAssetImportTaskFromStorage();
if (!savedTask) {
this.assetShowFailureButton = false;
this.assetCurrentTaskId = null;
return;
}
if (savedTask.hasFailures && savedTask.taskId) {
this.assetCurrentTaskId = savedTask.taskId;
this.assetShowFailureButton = true;
}
},
/**
* 获取上次导入的提示信息
* @returns {String} 提示文本
*/
getLastImportTooltip() {
const savedTask = this.getImportTaskFromStorage();
if (savedTask && savedTask.saveTime) {
const date = new Date(savedTask.saveTime);
const timeStr = this.parseTime(date, '{y}-{m}-{d} {h}:{i}');
return `上次导入: ${timeStr}`;
}
return '';
},
getLastAssetImportTooltip() {
const savedTask = this.getAssetImportTaskFromStorage();
if (savedTask && savedTask.saveTime) {
const date = new Date(savedTask.saveTime);
const timeStr = this.parseTime(date, '{y}-{m}-{d} {h}:{i}');
return `上次员工资产导入: ${timeStr}`;
}
return '';
},
/**
* 清除导入历史记录
* 用户手动触发
*/
clearImportHistory() {
this.$confirm('确认清除上次导入记录?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.clearImportTaskFromStorage();
this.showFailureButton = false;
this.currentTaskId = null;
this.failureDialogVisible = false;
this.$message.success('已清除');
}).catch(() => {});
},
clearAssetImportHistory() {
this.$confirm('确认清除上次员工资产导入记录?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.clearAssetImportTaskFromStorage();
this.assetShowFailureButton = false;
this.assetCurrentTaskId = null;
this.assetFailureDialogVisible = false;
this.$message.success('已清除');
}).catch(() => {});
},
/** 查询员工列表 */
getList() {
this.loading = true;
listBaseStaff(this.queryParams).then(response => {
this.baseStaffList = response.rows;
this.total = response.total;
this.loading = false;
});
},
/** 查询部门下拉树结构 */
getDeptTree() {
deptTreeSelect().then(response => {
this.deptOptions = response.data;
this.enabledDeptOptions = this.filterDisabledDept(JSON.parse(JSON.stringify(response.data)));
});
},
// 过滤禁用的部门
filterDisabledDept(deptList) {
return deptList.filter(dept => {
if (dept.disabled) {
return false;
}
if (dept.children && dept.children.length) {
dept.children = this.filterDisabledDept(dept.children);
}
return true;
});
},
// 取消按钮
cancel() {
this.open = false;
this.isAdd = false;
this.reset();
},
// 表单重置
reset() {
this.form = {
staffId: null,
name: null,
deptId: null,
idCard: null,
phone: null,
hireDate: null,
status: "0",
relatives: [],
assetInfoList: []
};
this.resetForm("form");
},
normalizeAssetInfoList() {
const assetInfoList = Array.isArray(this.form.assetInfoList)
? this.form.assetInfoList
: [];
return assetInfoList.filter(item => {
if (!item || typeof item !== "object") {
return false;
}
return Object.keys(item).some(key => {
const value = item[key];
return value !== null && value !== undefined && String(value).trim() !== "";
});
});
},
validateAssetInfoList(assetInfoList) {
const requiredFields = [
{ key: "personId", label: "资产实际持有人身份证号" },
{ key: "assetMainType", label: "资产大类" },
{ key: "assetSubType", label: "资产小类" },
{ key: "assetName", label: "资产名称" },
{ key: "currentValue", label: "当前估值" },
{ key: "assetStatus", label: "资产状态" }
];
const numericFields = [
{ key: "ownershipRatio", label: "产权占比" },
{ key: "originalValue", label: "资产原值" },
{ key: "currentValue", label: "当前估值" }
];
for (let index = 0; index < assetInfoList.length; index++) {
const asset = assetInfoList[index];
const rowNo = index + 1;
for (const field of requiredFields) {
const value = asset[field.key];
if (value === null || value === undefined || String(value).trim() === "") {
this.$modal.msgError(`${rowNo}条资产的${field.label}不能为空`);
return false;
}
}
if (!idCardPattern.test(asset.personId)) {
this.$modal.msgError(`${rowNo}条资产的资产实际持有人身份证号格式不正确`);
return false;
}
for (const field of numericFields) {
const value = asset[field.key];
if (value !== null && value !== undefined && String(value).trim() !== "") {
if (!/^-?\d+(\.\d+)?$/.test(String(value).trim())) {
this.$modal.msgError(`${rowNo}条资产的${field.label}格式不正确`);
return false;
}
}
}
if (!this.assetStatusOptions.some(option => option.value === asset.assetStatus)) {
this.$modal.msgError(`${rowNo}条资产的资产状态不在允许范围内`);
return false;
}
}
return true;
},
createEmptyAssetRow(defaultPersonId = "") {
return {
personId: defaultPersonId || "",
assetMainType: "",
assetSubType: "",
assetName: "",
ownershipRatio: "",
purchaseEvalDate: "",
originalValue: "",
currentValue: "",
valuationDate: "",
assetStatus: "",
remarks: ""
};
},
hasAssetContent(row) {
if (!row || typeof row !== "object") {
return false;
}
return Object.keys(row).some(key => {
const value = row[key];
return value !== null && value !== undefined && String(value).trim() !== "";
});
},
syncAssetPersonIds(newIdCard, oldIdCard) {
if (!Array.isArray(this.form.assetInfoList)) {
return;
}
this.form.assetInfoList = this.form.assetInfoList.map(asset => {
if (!asset || typeof asset !== "object") {
return asset;
}
const shouldSync = !asset.personId || asset.personId === oldIdCard;
if (!shouldSync) {
return asset;
}
return {
...asset,
personId: newIdCard || ""
};
});
},
handleAddAsset() {
if (!Array.isArray(this.form.assetInfoList)) {
this.form.assetInfoList = [];
}
this.form.assetInfoList.push(this.createEmptyAssetRow(this.form.idCard));
},
handleRemoveAsset(index) {
if (!Array.isArray(this.form.assetInfoList)) {
return;
}
this.form.assetInfoList.splice(index, 1);
},
formatAssetOwnerType(asset) {
if (!asset) {
return "-";
}
return asset.personId && asset.personId === this.employeeDetail.idCard ? "本人" : "亲属";
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm");
this.handleQuery();
},
/** 多选框选中数据 */
handleSelectionChange(selection) {
this.ids = selection.map(item => item.staffId);
this.single = selection.length !== 1;
this.multiple = !selection.length;
},
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.form.assetInfoList = [];
this.isAdd = true;
this.open = true;
this.title = "新增员工";
},
/** 详情按钮操作 */
handleDetail(row) {
const staffId = row.staffId;
getBaseStaff(staffId).then(response => {
this.employeeDetail = {
...response.data,
assetInfoList: response.data.assetInfoList || []
};
this.detailOpen = true;
});
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset();
this.isAdd = false;
const staffId = row.staffId || this.ids[0];
getBaseStaff(staffId).then(response => {
this.form = {
...response.data,
assetInfoList: response.data.assetInfoList || []
};
this.open = true;
this.title = "编辑员工";
});
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
this.form.assetInfoList = this.normalizeAssetInfoList();
if (!this.validateAssetInfoList(this.form.assetInfoList)) {
return;
}
if (this.isAdd) {
addBaseStaff(this.form).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.isAdd = false;
this.getList();
});
} else {
updateBaseStaff(this.form).then(response => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
});
}
}
});
},
/** 删除按钮操作 */
handleDelete(row) {
const staffIds = row.staffId || this.ids;
this.$modal.confirm('是否确认删除员工编号为"' + staffIds + '"的数据项?').then(function() {
return delBaseStaff(staffIds);
}).then(() => {
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {});
},
/** 导入按钮操作 */
handleImport() {
this.upload.title = "员工数据导入";
this.upload.open = true;
},
handleAssetImport() {
this.assetUpload.title = "员工资产数据导入";
this.assetUpload.open = true;
},
/** 导入对话框关闭事件 */
handleImportDialogClose() {
this.$nextTick(() => {
if (this.$refs.upload) {
this.$refs.upload.clearFiles();
}
});
},
handleAssetImportDialogClose() {
this.$nextTick(() => {
if (this.$refs.assetUpload) {
this.$refs.assetUpload.clearFiles();
}
});
},
/** 下载模板操作 */
importTemplate() {
this.download('ccdi/baseStaff/importTemplate', {}, `员工信息模板_${new Date().getTime()}.xlsx`)
},
importAssetTemplate() {
this.download('ccdi/assetInfo/importTemplate', {}, `员工资产信息模板_${new Date().getTime()}.xlsx`)
},
// 文件上传中处理
handleFileUploadProgress(event, file, fileList) {
this.upload.isUploading = true;
},
handleAssetFileUploadProgress() {
this.assetUpload.isUploading = true;
},
// 文件上传成功处理
handleFileSuccess(response, file, fileList) {
this.upload.isUploading = false;
this.upload.open = false;
if (response.code === 200) {
// 验证响应数据完整性
if (!response.data || !response.data.taskId) {
this.$modal.msgError('导入任务创建失败:缺少任务ID');
this.upload.isUploading = false;
this.upload.open = true;
return;
}
const taskId = response.data.taskId;
// 清除旧的导入记录(防止并发)
if (this.pollingTimer) {
clearInterval(this.pollingTimer);
this.pollingTimer = null;
}
this.clearImportTaskFromStorage();
// 保存新任务的初始状态
this.saveImportTaskToStorage({
taskId: taskId,
status: 'PROCESSING',
timestamp: Date.now(),
hasFailures: false
});
// 重置状态
this.showFailureButton = false;
this.currentTaskId = taskId;
// 显示后台处理提示
this.$notify({
title: '导入任务已提交',
message: '正在后台处理中,处理完成后将通知您',
type: 'info',
duration: 3000
});
// 开始轮询检查状态
this.startImportStatusPolling(taskId);
} else {
this.$modal.msgError(response.msg);
}
},
handleAssetFileSuccess(response) {
this.assetUpload.isUploading = false;
this.assetUpload.open = false;
if (response.code === 200) {
if (!response.data || !response.data.taskId) {
this.$modal.msgError('员工资产导入任务创建失败:缺少任务ID');
this.assetUpload.isUploading = false;
this.assetUpload.open = true;
return;
}
const taskId = response.data.taskId;
if (this.assetPollingTimer) {
clearInterval(this.assetPollingTimer);
this.assetPollingTimer = null;
}
this.clearAssetImportTaskFromStorage();
this.saveAssetImportTaskToStorage({
taskId: taskId,
status: 'PROCESSING',
timestamp: Date.now(),
hasFailures: false
});
this.assetShowFailureButton = false;
this.assetCurrentTaskId = taskId;
this.$notify({
title: '员工资产导入任务已提交',
message: '正在后台处理中,处理完成后将通知您',
type: 'info',
duration: 3000
});
this.startAssetImportStatusPolling(taskId);
} else if (response.code === 601) {
this.$modal.msgWarning(response.msg);
} else {
this.$modal.msgError(response.msg);
}
},
// 导入结果弹窗关闭
handleImportResultClose() {
this.importResultVisible = false;
this.importResultContent = "";
},
handleAssetImportResultClose() {
this.assetImportResultVisible = false;
this.assetImportResultContent = "";
},
/** 开始轮询导入状态 */
startImportStatusPolling(taskId) {
let pollCount = 0;
const maxPolls = 150; // 最多轮询150次(5分钟)
this.pollingTimer = setInterval(async () => {
try {
pollCount++;
// 超时检查
if (pollCount > maxPolls) {
clearInterval(this.pollingTimer);
this.$modal.msgWarning('导入任务处理超时,请联系管理员');
return;
}
const response = await getImportStatus(taskId);
if (response.data && response.data.status !== 'PROCESSING') {
clearInterval(this.pollingTimer);
this.handleImportComplete(response.data);
}
} catch (error) {
clearInterval(this.pollingTimer);
this.$modal.msgError('查询导入状态失败: ' + error.message);
}
}, 2000); // 每2秒轮询一次
},
startAssetImportStatusPolling(taskId) {
let pollCount = 0;
const maxPolls = 150;
this.assetPollingTimer = setInterval(async () => {
try {
pollCount++;
if (pollCount > maxPolls) {
clearInterval(this.assetPollingTimer);
this.$modal.msgWarning('员工资产导入任务处理超时,请联系管理员');
return;
}
const response = await getAssetImportStatus(taskId);
if (response.data && response.data.status !== 'PROCESSING') {
clearInterval(this.assetPollingTimer);
this.handleAssetImportComplete(response.data);
}
} catch (error) {
clearInterval(this.assetPollingTimer);
this.$modal.msgError('查询员工资产导入状态失败: ' + error.message);
}
}, 2000);
},
/** 处理导入完成 */
handleImportComplete(statusResult) {
// 更新localStorage中的任务状态
this.saveImportTaskToStorage({
taskId: statusResult.taskId,
status: statusResult.status,
hasFailures: statusResult.failureCount > 0,
totalCount: statusResult.totalCount,
successCount: statusResult.successCount,
failureCount: statusResult.failureCount
});
if (statusResult.status === 'SUCCESS') {
this.$notify({
title: '导入完成',
message: `全部成功!共导入${statusResult.totalCount}条数据`,
type: 'success',
duration: 5000
});
this.showFailureButton = false; // 成功时清除失败按钮显示
this.getList();
} else if (statusResult.failureCount > 0) {
this.$notify({
title: '导入完成',
message: `成功${statusResult.successCount}条,失败${statusResult.failureCount}`,
type: 'warning',
duration: 5000
});
// 显示查看失败记录按钮
this.showFailureButton = true;
this.currentTaskId = statusResult.taskId;
// 刷新列表
this.getList();
}
},
handleAssetImportComplete(statusResult) {
this.saveAssetImportTaskToStorage({
taskId: statusResult.taskId,
status: statusResult.status,
hasFailures: statusResult.failureCount > 0,
totalCount: statusResult.totalCount,
successCount: statusResult.successCount,
failureCount: statusResult.failureCount
});
if (statusResult.status === 'SUCCESS') {
this.$notify({
title: '员工资产导入完成',
message: `全部成功!共导入${statusResult.totalCount}条数据`,
type: 'success',
duration: 5000
});
this.assetShowFailureButton = false;
this.getList();
} else if (statusResult.failureCount > 0) {
this.$notify({
title: '员工资产导入完成',
message: `成功${statusResult.successCount}条,失败${statusResult.failureCount}`,
type: 'warning',
duration: 5000
});
this.assetShowFailureButton = true;
this.assetCurrentTaskId = statusResult.taskId;
this.getList();
}
},
/** 查看导入失败记录 */
viewImportFailures() {
this.failureDialogVisible = true;
this.getFailureList();
},
viewAssetImportFailures() {
this.assetFailureDialogVisible = true;
this.getAssetFailureList();
},
/** 查询失败记录列表 */
getFailureList() {
this.failureLoading = true;
getImportFailures(
this.currentTaskId,
this.failureQueryParams.pageNum,
this.failureQueryParams.pageSize
).then(response => {
this.failureList = response.rows;
this.failureTotal = response.total;
this.failureLoading = false;
}).catch(error => {
this.failureLoading = false;
// 处理不同类型的错误
if (error.response) {
const status = error.response.status;
if (status === 404) {
// 记录不存在或已过期
this.$modal.msgWarning('导入记录已过期,无法查看失败记录');
this.clearImportTaskFromStorage();
this.showFailureButton = false;
this.currentTaskId = null;
this.failureDialogVisible = false;
} else if (status === 500) {
this.$modal.msgError('服务器错误,请稍后重试');
} else {
this.$modal.msgError(`查询失败: ${error.response.data.msg || '未知错误'}`);
}
} else if (error.request) {
// 请求发送了但没有收到响应
this.$modal.msgError('网络连接失败,请检查网络');
} else {
this.$modal.msgError('查询失败记录失败: ' + error.message);
}
});
},
getAssetFailureList() {
this.assetFailureLoading = true;
getAssetImportFailures(
this.assetCurrentTaskId,
this.assetFailureQueryParams.pageNum,
this.assetFailureQueryParams.pageSize
).then(response => {
this.assetFailureList = response.rows;
this.assetFailureTotal = response.total;
this.assetFailureLoading = false;
}).catch(error => {
this.assetFailureLoading = false;
if (error.response) {
const status = error.response.status;
if (status === 404) {
this.$modal.msgWarning('员工资产导入记录已过期,无法查看失败记录');
this.clearAssetImportTaskFromStorage();
this.assetShowFailureButton = false;
this.assetCurrentTaskId = null;
this.assetFailureDialogVisible = false;
} else if (status === 500) {
this.$modal.msgError('服务器错误,请稍后重试');
} else {
this.$modal.msgError(`查询失败: ${error.response.data.msg || '未知错误'}`);
}
} else if (error.request) {
this.$modal.msgError('网络连接失败,请检查网络');
} else {
this.$modal.msgError('查询员工资产失败记录失败: ' + error.message);
}
});
},
// 提交上传文件
submitFileForm() {
this.$refs.upload.submit();
},
submitAssetFileForm() {
this.$refs.assetUpload.submit();
}
}
};
</script>
<style scoped>
.detail-form .el-form-item {
margin-bottom: 10px;
}
/* 详情弹窗样式 */
.employee-detail-dialog .detail-container {
padding: 10px 0;
}
.employee-detail-dialog .info-section {
background: #f9fafb;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
}
.employee-detail-dialog .section-title {
display: flex;
align-items: center;
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-bottom: 10px;
border-bottom: 1px solid #e4e7ed;
}
.employee-detail-dialog .section-title i {
margin-right: 6px;
color: #409eff;
font-size: 16px;
}
.employee-detail-dialog .el-descriptions {
background: #fff;
}
.employee-detail-dialog .detail-assets-table {
background: #fff;
border-radius: 4px;
overflow: hidden;
}
.employee-detail-dialog .empty-assets-detail {
padding: 32px 0;
text-align: center;
color: #909399;
background: #fff;
border-radius: 4px;
}
.employee-detail-dialog .relatives-container {
background: #fff;
border-radius: 4px;
padding: 10px;
}
.employee-detail-dialog .empty-relatives {
text-align: center;
padding: 30px 0;
color: #909399;
background: #fff;
border-radius: 4px;
}
.employee-detail-dialog .empty-relatives i {
font-size: 40px;
color: #c0c4cc;
margin-bottom: 10px;
}
.employee-detail-dialog .empty-relatives span {
display: block;
font-size: 14px;
}
/* 编辑弹窗样式 */
.employee-edit-dialog .section-header {
display: flex;
align-items: center;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid #e4e7ed;
font-size: 14px;
font-weight: 500;
color: #303133;
}
.employee-edit-dialog .section-header span:first-child {
flex: 1;
}
.employee-edit-dialog .relative-count {
color: #909399;
font-size: 12px;
margin-right: 8px;
}
.employee-edit-dialog .el-form-item {
margin-bottom: 16px;
}
.employee-edit-dialog .el-radio {
margin-right: 20px;
}
.employee-edit-dialog .assets-helper {
margin: -4px 0 12px;
padding: 10px 12px;
background: #f4f8ff;
border: 1px solid #d9ecff;
border-radius: 6px;
color: #606266;
line-height: 1.8;
}
.employee-edit-dialog .assets-table-wrapper {
width: 100%;
overflow-x: auto;
}
.employee-edit-dialog .assets-table {
min-width: 1460px;
}
.employee-edit-dialog .assets-table .el-input,
.employee-edit-dialog .assets-table .el-date-editor {
width: 100%;
}
.employee-edit-dialog .empty-assets {
text-align: center;
padding: 30px 0;
color: #909399;
border: 1px dashed #dcdfe6;
border-radius: 4px;
background: #fafafa;
}
.employee-edit-dialog .empty-assets i {
display: block;
font-size: 30px;
color: #c0c4cc;
margin-bottom: 8px;
}
.employee-edit-dialog .empty-assets span {
font-size: 13px;
}
.employee-edit-dialog .relatives-table {
width: 100%;
}
.employee-edit-dialog .relatives-table .el-form-item {
margin-bottom: 0;
}
.employee-edit-dialog .empty-relatives {
text-align: center;
padding: 30px 0;
color: #909399;
border: 1px dashed #dcdfe6;
border-radius: 4px;
}
.employee-edit-dialog .empty-relatives i {
font-size: 32px;
color: #c0c4cc;
margin-bottom: 6px;
}
.employee-edit-dialog .empty-relatives span {
font-size: 13px;
margin-right: 8px;
}
</style>
<!-- 导入结果弹窗已抽离为独立组件 ImportResultDialog -->