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

1410 lines
49 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="recruitName">
<el-input
v-model="queryParams.recruitName"
placeholder="请输入招聘项目名称"
clearable
style="width: 240px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="职位名称" prop="posName">
<el-input
v-model="queryParams.posName"
placeholder="请输入职位名称"
clearable
style="width: 240px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="候选人姓名" prop="candName">
<el-input
v-model="queryParams.candName"
placeholder="请输入候选人姓名"
clearable
style="width: 240px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="证件号码" prop="candId">
<el-input
v-model="queryParams.candId"
placeholder="请输入证件号码"
clearable
style="width: 240px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="录用情况" prop="admitStatus">
<el-select v-model="queryParams.admitStatus" placeholder="请选择录用情况" clearable style="width: 240px">
<el-option label="录用" value="录用" />
<el-option label="未录用" value="未录用" />
<el-option label="放弃" value="放弃" />
</el-select>
</el-form-item>
<el-form-item label="招聘类型" prop="recruitType">
<el-select v-model="queryParams.recruitType" placeholder="请选择招聘类型" clearable style="width: 240px">
<el-option
v-for="item in recruitTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</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
v-if="isPreviewMode()"
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
>新增</el-button>
<el-button
v-else
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-hasPermi="['ccdi:staffRecruitment:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
v-if="isPreviewMode()"
type="success"
plain
icon="el-icon-upload2"
size="mini"
@click="handleImport"
>导入</el-button>
<el-button
v-else
type="success"
plain
icon="el-icon-upload2"
size="mini"
@click="handleImport"
v-hasPermi="['ccdi:staffRecruitment:import']"
>导入</el-button>
</el-col>
<el-col :span="1.5">
<el-button
v-if="isPreviewMode()"
type="info"
plain
icon="el-icon-upload"
size="mini"
@click="handleWorkImport"
>导入工作经历</el-button>
<el-button
v-else
type="info"
plain
icon="el-icon-upload"
size="mini"
@click="handleWorkImport"
v-hasPermi="['ccdi:staffRecruitment:import']"
>导入工作经历</el-button>
</el-col>
<el-col :span="1.5">
<el-button
v-if="isPreviewMode()"
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
>导出</el-button>
<el-button
v-else
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
v-hasPermi="['ccdi:staffRecruitment:export']"
>导出</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>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="recruitmentList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="招聘记录编号" align="center" prop="recruitId" width="150" :show-overflow-tooltip="true"/>
<el-table-column label="招聘项目名称" align="center" prop="recruitName" min-width="220" :show-overflow-tooltip="true"/>
<el-table-column label="职位名称" align="center" prop="posName" :show-overflow-tooltip="true"/>
<el-table-column label="候选人姓名" align="center" prop="candName" width="120"/>
<el-table-column label="录用情况" align="center" prop="admitStatus" width="100">
<template slot-scope="scope">
<el-tag v-if="scope.row.admitStatus === '录用'" type="success" size="small">录用</el-tag>
<el-tag v-else-if="scope.row.admitStatus === '未录用'" type="info" size="small">未录用</el-tag>
<el-tag v-else type="warning" size="small">放弃</el-tag>
</template>
</el-table-column>
<el-table-column label="学历 / 毕业学校" align="center" min-width="180">
<template slot-scope="scope">
<span>{{ formatEducationSchool(scope.row) }}</span>
</template>
</el-table-column>
<el-table-column label="招聘类型" align="center" prop="recruitType" width="100">
<template slot-scope="scope">
<el-tag :type="scope.row.recruitType === 'SOCIAL' ? 'success' : 'info'" size="small">
{{ formatRecruitType(scope.row.recruitType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="历史工作经历" align="center" prop="workExperienceCount" width="120">
<template slot-scope="scope">
{{ formatWorkExperienceCount(scope.row) }}
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200">
<template slot-scope="scope">
<el-button
v-if="isPreviewMode()"
size="mini"
type="text"
icon="el-icon-view"
@click="handleDetail(scope.row)"
>详情</el-button>
<el-button
v-else
size="mini"
type="text"
icon="el-icon-view"
@click="handleDetail(scope.row)"
v-hasPermi="['ccdi:staffRecruitment:query']"
>详情</el-button>
<el-button
v-if="isPreviewMode()"
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
>编辑</el-button>
<el-button
v-else
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['ccdi:staffRecruitment:edit']"
>编辑</el-button>
<el-button
v-if="isPreviewMode()"
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
>删除</el-button>
<el-button
v-else
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['ccdi:staffRecruitment: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="900px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="120px">
<el-divider content-position="left">招聘岗位信息</el-divider>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="招聘记录编号" prop="recruitId">
<el-input v-model="form.recruitId" placeholder="请输入招聘记录编号" maxlength="32" :disabled="!isAdd" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="招聘项目名称" prop="recruitName">
<el-input v-model="form.recruitName" placeholder="请输入招聘项目名称" maxlength="100" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="职位名称" prop="posName">
<el-input v-model="form.posName" placeholder="请输入职位名称" maxlength="100" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="职位类别" prop="posCategory">
<el-input v-model="form.posCategory" placeholder="请输入职位类别" maxlength="50" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="职位描述" prop="posDesc">
<el-input v-model="form.posDesc" type="textarea" :rows="3" placeholder="请输入职位描述" />
</el-form-item>
<el-divider content-position="left">录用情况</el-divider>
<el-form-item label="录用情况" prop="admitStatus">
<el-radio-group v-model="form.admitStatus">
<el-radio label="录用">录用</el-radio>
<el-radio label="未录用">未录用</el-radio>
<el-radio label="放弃">放弃</el-radio>
</el-radio-group>
</el-form-item>
<el-divider content-position="left">候选人情况</el-divider>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="候选人姓名" prop="candName">
<el-input v-model="form.candName" placeholder="请输入候选人姓名" maxlength="20" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="招聘类型" prop="recruitType">
<el-radio-group v-model="form.recruitType">
<el-radio
v-for="item in recruitTypeOptions"
:key="item.value"
:label="item.value"
>
{{ item.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="学历" prop="candEdu">
<el-input v-model="form.candEdu" placeholder="请输入学历" maxlength="20" />
</el-form-item>
</el-col>
<el-col :span="12"></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="证件号码" prop="candId">
<el-input v-model="form.candId" placeholder="请输入18位证件号码" maxlength="18" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="毕业年月" prop="candGrad">
<el-input v-model="form.candGrad" placeholder="格式:YYYYMM" maxlength="6" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="毕业院校" prop="candSchool">
<el-input v-model="form.candSchool" placeholder="请输入毕业院校" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="专业" prop="candMajor">
<el-input v-model="form.candMajor" placeholder="请输入专业" maxlength="30" />
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">面试官信息</el-divider>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="面试官1姓名">
<el-input v-model="form.interviewerName1" placeholder="请输入面试官1姓名" maxlength="20" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="面试官1工号">
<el-input v-model="form.interviewerId1" placeholder="请输入面试官1工号" maxlength="10" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="面试官2姓名">
<el-input v-model="form.interviewerName2" placeholder="请输入面试官2姓名" maxlength="20" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="面试官2工号">
<el-input v-model="form.interviewerId2" placeholder="请输入面试官2工号" maxlength="10" />
</el-form-item>
</el-col>
</el-row>
</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="900px" append-to-body>
<div class="detail-container">
<el-divider content-position="left">招聘岗位信息</el-divider>
<el-descriptions :column="2" border>
<el-descriptions-item label="招聘记录编号">{{ recruitmentDetail.recruitId || '-' }}</el-descriptions-item>
<el-descriptions-item label="招聘项目名称">{{ recruitmentDetail.recruitName || '-' }}</el-descriptions-item>
<el-descriptions-item label="职位名称">{{ recruitmentDetail.posName || '-' }}</el-descriptions-item>
<el-descriptions-item label="职位类别">{{ recruitmentDetail.posCategory || '-' }}</el-descriptions-item>
<el-descriptions-item label="职位描述" :span="2">{{ recruitmentDetail.posDesc || '-' }}</el-descriptions-item>
</el-descriptions>
<el-divider content-position="left">录用情况</el-divider>
<el-descriptions :column="2" border>
<el-descriptions-item label="录用情况">
<el-tag v-if="recruitmentDetail.admitStatus === '录用'" type="success" size="small">录用</el-tag>
<el-tag v-else-if="recruitmentDetail.admitStatus === '未录用'" type="info" size="small">未录用</el-tag>
<el-tag v-else type="warning" size="small">放弃</el-tag>
</el-descriptions-item>
</el-descriptions>
<el-divider content-position="left">候选人情况</el-divider>
<el-descriptions :column="2" border>
<el-descriptions-item label="候选人姓名">{{ recruitmentDetail.candName || '-' }}</el-descriptions-item>
<el-descriptions-item label="招聘类型">{{ formatRecruitType(recruitmentDetail.recruitType) }}</el-descriptions-item>
<el-descriptions-item label="学历">{{ recruitmentDetail.candEdu || '-' }}</el-descriptions-item>
<el-descriptions-item label="证件号码">{{ recruitmentDetail.candId || '-' }}</el-descriptions-item>
<el-descriptions-item label="毕业年月">{{ recruitmentDetail.candGrad || '-' }}</el-descriptions-item>
<el-descriptions-item label="毕业院校">{{ recruitmentDetail.candSchool || '-' }}</el-descriptions-item>
<el-descriptions-item label="专业">{{ recruitmentDetail.candMajor || '-' }}</el-descriptions-item>
</el-descriptions>
<template v-if="isSocialRecruitment(recruitmentDetail)">
<el-divider content-position="left">候选人历史工作经历</el-divider>
<el-table
v-if="recruitmentDetail.workExperienceList && recruitmentDetail.workExperienceList.length"
:data="recruitmentDetail.workExperienceList"
border
>
<el-table-column label="序号" align="center" prop="sortOrder" width="80" />
<el-table-column label="工作单位" align="center" prop="companyName" min-width="180" :show-overflow-tooltip="true" />
<el-table-column label="岗位" align="center" prop="positionName" min-width="140" :show-overflow-tooltip="true" />
<el-table-column label="任职时间" align="center" min-width="160">
<template slot-scope="scope">
{{ getEmploymentPeriod(scope.row) }}
</template>
</el-table-column>
<el-table-column label="离职原因" align="center" prop="departureReason" min-width="220" :show-overflow-tooltip="true" />
</el-table>
<el-empty
v-else
description="暂无历史工作经历"
:image-size="72"
class="work-experience-empty"
/>
</template>
<el-divider content-position="left">面试官信息</el-divider>
<el-descriptions :column="2" border>
<el-descriptions-item label="面试官1姓名">{{ recruitmentDetail.interviewerName1 || '-' }}</el-descriptions-item>
<el-descriptions-item label="面试官1工号">{{ recruitmentDetail.interviewerId1 || '-' }}</el-descriptions-item>
<el-descriptions-item label="面试官2姓名">{{ recruitmentDetail.interviewerName2 || '-' }}</el-descriptions-item>
<el-descriptions-item label="面试官2工号">{{ recruitmentDetail.interviewerId2 || '-' }}</el-descriptions-item>
</el-descriptions>
</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">
<el-upload
ref="upload"
:limit="1"
accept=".xlsx, .xls"
:headers="upload.headers"
:action="upload.url"
: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-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>{{ upload.tip }}</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="导入失败记录"
: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="recruitId" align="center" width="150" />
<el-table-column label="招聘项目名称" prop="recruitName" align="center" :show-overflow-tooltip="true"/>
<el-table-column label="职位名称" prop="posName" align="center" :show-overflow-tooltip="true"/>
<el-table-column label="候选人姓名" prop="candName" align="center" width="120"/>
<el-table-column
v-if="currentImportType !== 'work'"
label="证件号码"
prop="candId"
align="center"
width="180"
/>
<el-table-column
v-if="currentImportType === 'work'"
label="工作单位"
prop="companyName"
align="center"
min-width="180"
:show-overflow-tooltip="true"
/>
<el-table-column
v-if="currentImportType === 'work'"
label="岗位"
prop="positionName"
align="center"
min-width="140"
:show-overflow-tooltip="true"
/>
<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>
</div>
</template>
<script>
import {
addStaffRecruitment,
delStaffRecruitment,
getImportFailures,
getImportStatus,
getStaffRecruitment,
listStaffRecruitment,
updateStaffRecruitment
} from "@/api/ccdiStaffRecruitment";
import {getToken} from "@/utils/auth";
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]$/;
// 毕业年月校验正则 (YYYYMM)
const gradPattern = /^((19|20)\d{2})(0[1-9]|1[0-2])$/;
const previewRecruitmentList = [
{
recruitId: "RC2025001205",
recruitName: "2024年社会招聘-技术部",
posName: "Java开发工程师",
posCategory: "技术研发",
posDesc: "负责核心业务系统设计、开发与维护。",
candName: "杨丽思思",
recruitType: "SOCIAL",
candEdu: "本科",
candId: "330101199403150021",
candSchool: "四川大学",
candMajor: "法学",
candGrad: "202110",
admitStatus: "录用",
workExperienceCount: 2,
interviewerName1: "陈志远",
interviewerId1: "I0001",
interviewerName2: "王晨",
interviewerId2: "I0002",
createdBy: "admin",
updatedBy: "admin",
createTime: "2026-04-15 09:00:00",
updateTime: "2026-04-15 09:20:00",
workExperienceList: [
{
sortOrder: 1,
companyName: "杭州数联科技有限公司",
positionName: "Java开发工程师",
jobStartMonth: "2022-07",
jobEndMonth: "2024-12",
departureReason: "个人职业发展需要,期望参与更大规模系统建设"
},
{
sortOrder: 2,
companyName: "成都云启信息技术有限公司",
positionName: "初级开发工程师",
jobStartMonth: "2021-07",
jobEndMonth: "2022-06",
departureReason: "项目阶段结束后选择新的发展机会"
}
]
},
{
recruitId: "RC2025001206",
recruitName: "2024年社会招聘-技术部",
posName: "数据分析师",
posCategory: "数据分析",
posDesc: "负责经营分析、指标体系建设与专题分析。",
candName: "罗军晓东",
recruitType: "SOCIAL",
candEdu: "本科",
candId: "420106199603120018",
candSchool: "华中科技大学",
candMajor: "软件工程",
candGrad: "202003",
admitStatus: "录用",
workExperienceCount: 1,
interviewerName1: "李倩",
interviewerId1: "I0003",
interviewerName2: "周腾",
interviewerId2: "I0004",
createdBy: "admin",
updatedBy: "admin",
createTime: "2026-04-15 09:05:00",
updateTime: "2026-04-15 09:25:00",
workExperienceList: [
{
sortOrder: 1,
companyName: "上海明策数据服务有限公司",
positionName: "数据分析师",
jobStartMonth: "2021-03",
jobEndMonth: "2025-01",
departureReason: "期望转向更深入的数据建模分析工作"
}
]
},
{
recruitId: "RC2025001003",
recruitName: "2024年春季校园招聘",
posName: "Java开发工程师",
posCategory: "技术研发",
posDesc: "参与项目需求开发与系统维护。",
candName: "黄伟梓萱",
recruitType: "CAMPUS",
candEdu: "本科",
candId: "440105200001018888",
candSchool: "中山大学",
candMajor: "建筑学",
candGrad: "202108",
admitStatus: "录用",
workExperienceCount: 0,
interviewerName1: "陈志远",
interviewerId1: "I0001",
interviewerName2: "王晨",
interviewerId2: "I0002",
createdBy: "admin",
updatedBy: "admin",
createTime: "2026-04-15 09:10:00",
updateTime: "2026-04-15 09:30:00",
workExperienceList: []
}
];
export default {
name: "StaffRecruitment",
components: { ImportResultDialog },
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 招聘信息表格数据
recruitmentList: [],
recruitTypeOptions: [
{ value: "SOCIAL", label: "社招" },
{ value: "CAMPUS", label: "校招" }
],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 是否显示详情弹出层
detailOpen: false,
// 招聘信息详情
recruitmentDetail: {},
// 是否为新增操作
isAdd: false,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
recruitName: null,
posName: null,
candName: null,
candId: null,
admitStatus: null,
recruitType: null,
interviewerName: null,
interviewerId: null
},
// 表单参数
form: {},
// 表单校验
rules: {
recruitId: [
{ required: true, message: "招聘记录编号不能为空", trigger: "blur" },
{ max: 32, message: "招聘记录编号长度不能超过32个字符", trigger: "blur" }
],
recruitName: [
{ required: true, message: "招聘项目名称不能为空", trigger: "blur" },
{ max: 100, message: "招聘项目名称长度不能超过100个字符", trigger: "blur" }
],
posName: [
{ required: true, message: "职位名称不能为空", trigger: "blur" },
{ max: 100, message: "职位名称长度不能超过100个字符", trigger: "blur" }
],
posCategory: [
{ required: true, message: "职位类别不能为空", trigger: "blur" },
{ max: 50, message: "职位类别长度不能超过50个字符", trigger: "blur" }
],
posDesc: [
{ required: true, message: "职位描述不能为空", trigger: "blur" }
],
candName: [
{ required: true, message: "候选人姓名不能为空", trigger: "blur" },
{ max: 20, message: "候选人姓名长度不能超过20个字符", trigger: "blur" }
],
recruitType: [
{ required: true, message: "招聘类型不能为空", trigger: "change" }
],
candEdu: [
{ required: true, message: "学历不能为空", trigger: "blur" },
{ max: 20, message: "学历长度不能超过20个字符", trigger: "blur" }
],
candId: [
{ required: true, message: "证件号码不能为空", trigger: "blur" },
{ pattern: idCardPattern, message: "请输入正确的18位身份证号", trigger: "blur" }
],
candSchool: [
{ required: true, message: "毕业院校不能为空", trigger: "blur" },
{ max: 50, message: "毕业院校长度不能超过50个字符", trigger: "blur" }
],
candMajor: [
{ required: true, message: "专业不能为空", trigger: "blur" },
{ max: 30, message: "专业长度不能超过30个字符", trigger: "blur" }
],
candGrad: [
{ required: true, message: "毕业年月不能为空", trigger: "blur" },
{ pattern: gradPattern, message: "毕业年月格式不正确,应为YYYYMM", trigger: "blur" }
],
admitStatus: [
{ required: true, message: "请选择录用情况", trigger: "change" }
]
},
// 导入参数
upload: {
// 是否显示弹出层
open: false,
// 弹出层标题
title: "",
// 导入类型
importType: "recruitment",
// 弹窗提示
tip: "仅允许导入\"xls\"或\"xlsx\"格式文件。",
// 是否禁用上传
isUploading: false,
// 设置上传的请求头部
headers: { Authorization: "Bearer " + getToken() },
// 上传的地址
url: process.env.VUE_APP_BASE_API + "/ccdi/staffRecruitment/importData"
},
// 导入结果弹窗
importResultVisible: false,
importResultContent: "",
// 导入轮询定时器
importPollingTimer: null,
// 是否显示查看失败记录按钮
showFailureButton: false,
// 当前导入任务ID
currentTaskId: null,
// 当前导入类型
currentImportType: "recruitment",
// 失败记录对话框
failureDialogVisible: false,
failureList: [],
failureLoading: false,
failureTotal: 0,
failureQueryParams: {
pageNum: 1,
pageSize: 10
}
};
},
computed: {
/**
* 上次导入信息摘要
*/
lastImportInfo() {
const savedTask = this.getImportTaskFromStorage();
if (savedTask && savedTask.totalCount) {
return `导入类型: ${this.getImportTypeLabel(savedTask.importType || 'recruitment')} | 导入时间: ${this.parseTime(savedTask.saveTime)} | 总数: ${savedTask.totalCount}条 | 成功: ${savedTask.successCount}条 | 失败: ${savedTask.failureCount}`;
}
return '';
}
},
created() {
if (this.isPreviewMode()) {
this.loadPreviewPage();
return;
}
this.getList();
this.restoreImportState(); // 恢复导入状态
},
beforeDestroy() {
// 清理定时器
if (this.importPollingTimer) {
clearInterval(this.importPollingTimer);
this.importPollingTimer = null;
}
},
methods: {
/** 查询招聘信息列表 */
getList() {
if (this.isPreviewMode()) {
this.loadPreviewList();
return;
}
this.loading = true;
listStaffRecruitment(this.queryParams).then(response => {
this.recruitmentList = response.rows;
this.total = response.total;
this.loading = false;
});
},
// 取消按钮
cancel() {
this.open = false;
this.reset();
},
// 表单重置
reset() {
this.form = {
recruitId: null,
recruitName: null,
posName: null,
posCategory: null,
posDesc: null,
candName: null,
recruitType: "SOCIAL",
candEdu: null,
candId: null,
candSchool: null,
candMajor: null,
candGrad: null,
admitStatus: "录用",
workExperienceCount: 0,
workExperienceList: [],
interviewerName1: null,
interviewerId1: null,
interviewerName2: null,
interviewerId2: null
};
this.resetForm("form");
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm");
this.handleQuery();
},
/** 多选框选中数据 */
handleSelectionChange(selection) {
this.ids = selection.map(item => item.recruitId);
this.single = selection.length !== 1;
this.multiple = !selection.length;
},
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.open = true;
this.title = "添加招聘信息";
this.isAdd = true;
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset();
const recruitId = row.recruitId || this.ids[0];
if (this.isPreviewMode()) {
const target = this.findPreviewRecruitment(recruitId);
if (target) {
this.form = {
...this.form,
...target,
workExperienceList: this.normalizeWorkExperienceList(target.workExperienceList)
};
}
this.open = true;
this.title = "修改招聘信息";
this.isAdd = false;
return;
}
getStaffRecruitment(recruitId).then(response => {
this.form = {
...this.form,
...response.data,
workExperienceList: this.normalizeWorkExperienceList(response.data && response.data.workExperienceList)
};
this.open = true;
this.title = "修改招聘信息";
this.isAdd = false;
});
},
/** 详情按钮操作 */
handleDetail(row) {
const recruitId = row.recruitId;
if (this.isPreviewMode()) {
const target = this.findPreviewRecruitment(recruitId);
if (target) {
this.recruitmentDetail = {
...target,
workExperienceList: this.normalizeWorkExperienceList(target.workExperienceList)
};
this.detailOpen = true;
}
return;
}
getStaffRecruitment(recruitId).then(response => {
this.recruitmentDetail = {
...response.data,
workExperienceList: this.normalizeWorkExperienceList(response.data && response.data.workExperienceList)
};
this.detailOpen = true;
});
},
/** 招聘类型格式化 */
formatRecruitType(value) {
const matched = this.recruitTypeOptions.find(item => item.value === value);
return matched ? matched.label : "-";
},
/** 学历与毕业学校格式化 */
formatEducationSchool(row) {
if (!row) {
return "-";
}
const edu = row.candEdu || "-";
const school = row.candSchool || "-";
return `${edu} / ${school}`;
},
/** 历史工作经历展示 */
formatWorkExperienceCount(row) {
if (!row || row.recruitType !== "SOCIAL") {
return "-";
}
const count = Number(row.workExperienceCount || 0);
return `${count}`;
},
/** 是否为社招 */
isSocialRecruitment(row) {
return row && row.recruitType === "SOCIAL";
},
/** 任职时间展示 */
getEmploymentPeriod(row) {
if (!row) {
return "-";
}
const start = row.jobStartMonth || "-";
const end = row.jobEndMonth || "至今";
return `${start} ~ ${end}`;
},
/** 工作经历列表归一化 */
normalizeWorkExperienceList(list) {
if (!Array.isArray(list)) {
return [];
}
return list.slice().sort((a, b) => {
const first = Number(a.sortOrder || 0);
const second = Number(b.sortOrder || 0);
return first - second;
});
},
/** 是否为预览模式 */
isPreviewMode() {
return this.$route && this.$route.query && this.$route.query.preview === "1";
},
/** 加载预览页面 */
loadPreviewPage() {
this.loadPreviewList();
const mode = this.$route.query.mode;
const recruitId = this.$route.query.recruitId || "RC2025001205";
if (mode === "detail") {
this.handleDetail({ recruitId });
} else if (mode === "edit") {
this.handleUpdate({ recruitId });
} else if (mode === "workImport") {
this.handleWorkImport();
} else if (mode === "import") {
this.handleImport();
} else if (mode === "add") {
this.handleAdd();
}
},
/** 加载预览列表 */
loadPreviewList() {
this.loading = false;
this.recruitmentList = previewRecruitmentList.map(item => ({
...item,
workExperienceList: this.normalizeWorkExperienceList(item.workExperienceList)
}));
this.total = this.recruitmentList.length;
},
/** 查找预览记录 */
findPreviewRecruitment(recruitId) {
return previewRecruitmentList.find(item => item.recruitId === recruitId) || null;
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.isPreviewMode()) {
this.$modal.msgSuccess(this.isAdd ? "预览模式:新增成功" : "预览模式:修改成功");
this.open = false;
return;
}
if (this.isAdd) {
addStaffRecruitment(this.form).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();
});
} else {
updateStaffRecruitment(this.form).then(response => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
});
}
}
});
},
/** 删除按钮操作 */
handleDelete(row) {
const recruitIds = row.recruitId || this.ids;
if (this.isPreviewMode()) {
this.$modal.msgSuccess(`预览模式:已模拟删除 ${recruitIds}`);
return;
}
this.$modal.confirm('是否确认删除招聘信息编号为"' + recruitIds + '"的数据项?').then(function() {
return delStaffRecruitment(recruitIds);
}).then(() => {
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {});
},
/** 导出按钮操作 */
handleExport() {
this.download('ccdi/staffRecruitment/export', {
...this.queryParams
}, `招聘信息_${new Date().getTime()}.xlsx`);
},
/** 导入按钮操作 */
handleImport() {
this.openImportDialog("recruitment");
},
/** 导入工作经历按钮操作 */
handleWorkImport() {
this.openImportDialog("work");
},
/** 打开导入弹窗 */
openImportDialog(importType) {
const isWorkImport = importType === "work";
this.upload.importType = importType;
this.currentImportType = importType;
this.upload.title = isWorkImport ? "历史工作经历数据导入" : "招聘信息数据导入";
this.upload.url = process.env.VUE_APP_BASE_API + (isWorkImport
? "/ccdi/staffRecruitment/importWorkData"
: "/ccdi/staffRecruitment/importData");
this.upload.tip = isWorkImport
? "仅允许导入\"xls\"或\"xlsx\"格式文件;招聘记录编号用于匹配,姓名/项目/职位用于校验。"
: "仅允许导入\"xls\"或\"xlsx\"格式文件。";
if (this.isPreviewMode()) {
this.upload.open = true;
return;
}
this.upload.open = true;
},
/** 下载模板操作 */
importTemplate() {
if (this.upload.importType === "work") {
this.download('ccdi/staffRecruitment/workImportTemplate', {}, `历史工作经历导入模板_${new Date().getTime()}.xlsx`);
return;
}
this.download('ccdi/staffRecruitment/importTemplate', {}, `招聘信息导入模板_${new Date().getTime()}.xlsx`);
},
// 文件上传中处理
handleFileUploadProgress(event, file, fileList) {
this.upload.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.importPollingTimer) {
clearInterval(this.importPollingTimer);
this.importPollingTimer = null;
}
this.clearImportTaskFromStorage();
// 保存新任务的初始状态
this.saveImportTaskToStorage({
taskId: taskId,
status: 'PROCESSING',
timestamp: Date.now(),
hasFailures: false,
importType: this.upload.importType
});
// 重置状态
this.showFailureButton = false;
this.currentTaskId = taskId;
this.currentImportType = this.upload.importType;
// 显示后台处理提示
this.$notify({
title: '导入任务已提交',
message: `${this.getImportTypeLabel(this.upload.importType)}正在后台处理中,处理完成后将通知您`,
type: 'info',
duration: 3000
});
// 开始轮询检查状态
this.startImportStatusPolling(taskId);
} else {
this.$modal.msgError(response.msg);
}
},
/** 开始轮询导入状态 */
startImportStatusPolling(taskId) {
let pollCount = 0;
const maxPolls = 150; // 最多轮询150次(5分钟)
this.importPollingTimer = setInterval(async () => {
try {
pollCount++;
// 超时检查
if (pollCount > maxPolls) {
clearInterval(this.importPollingTimer);
this.$modal.msgWarning('导入任务处理超时,请联系管理员');
return;
}
const response = await getImportStatus(taskId);
if (response.data && response.data.status !== 'PROCESSING') {
clearInterval(this.importPollingTimer);
this.handleImportComplete(response.data);
}
} catch (error) {
clearInterval(this.importPollingTimer);
this.$modal.msgError('查询导入状态失败: ' + error.message);
}
}, 2000); // 每2秒轮询一次
},
/** 处理导入完成 */
handleImportComplete(statusResult) {
// 更新localStorage中的任务状态
this.saveImportTaskToStorage({
taskId: statusResult.taskId,
status: statusResult.status,
hasFailures: statusResult.failureCount > 0,
totalCount: statusResult.totalCount,
successCount: statusResult.successCount,
failureCount: statusResult.failureCount,
importType: this.currentImportType
});
if (statusResult.status === 'SUCCESS') {
// 全部成功
this.$notify({
title: '导入完成',
message: `${this.getImportTypeLabel(this.currentImportType)}全部成功!共导入${statusResult.totalCount}条数据`,
type: 'success',
duration: 5000
});
this.showFailureButton = false;
this.getList();
} else if (statusResult.failureCount > 0) {
// 部分失败
this.$notify({
title: '导入完成',
message: `${this.getImportTypeLabel(this.currentImportType)}成功${statusResult.successCount}条,失败${statusResult.failureCount}`,
type: 'warning',
duration: 5000
});
// 显示查看失败记录按钮
this.showFailureButton = true;
this.currentTaskId = statusResult.taskId;
// 刷新列表
this.getList();
}
},
/** 查询失败记录列表 */
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);
}
});
},
/** 查看导入失败记录 */
viewImportFailures() {
this.failureDialogVisible = true;
this.getFailureList();
},
/** 恢复导入状态 */
restoreImportState() {
const savedTask = this.getImportTaskFromStorage();
if (!savedTask) {
this.showFailureButton = false;
this.currentTaskId = null;
return;
}
// 如果有失败记录,恢复按钮显示
if (savedTask.hasFailures && savedTask.taskId) {
this.currentTaskId = savedTask.taskId;
this.currentImportType = savedTask.importType || "recruitment";
this.showFailureButton = true;
}
},
/** 获取上次导入的提示信息 */
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 `上次${this.getImportTypeLabel(savedTask.importType || 'recruitment')}: ${timeStr}`;
}
return '';
},
/** 保存导入任务到localStorage */
saveImportTaskToStorage(taskData) {
try {
const data = {
...taskData,
saveTime: Date.now()
};
localStorage.setItem('staff_recruitment_import_last_task', JSON.stringify(data));
} catch (error) {
console.error('保存导入任务状态失败:', error);
}
},
/** 从localStorage读取导入任务 */
getImportTaskFromStorage() {
try {
const data = localStorage.getItem('staff_recruitment_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;
}
},
/** 清除导入历史记录 */
clearImportHistory() {
this.$confirm('确认清除上次导入记录?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.clearImportTaskFromStorage();
this.showFailureButton = false;
this.currentTaskId = null;
this.failureDialogVisible = false;
this.$message.success('已清除');
}).catch(() => {});
},
/** 清除localStorage中的导入任务 */
clearImportTaskFromStorage() {
try {
localStorage.removeItem('staff_recruitment_import_last_task');
} catch (error) {
console.error('清除导入任务状态失败:', error);
}
},
// 导入结果弹窗关闭
handleImportResultClose() {
this.importResultVisible = false;
this.importResultContent = "";
},
// 提交上传文件
submitFileForm() {
if (this.isPreviewMode()) {
this.$modal.msgSuccess(`预览模式:已模拟提交${this.getImportTypeLabel(this.upload.importType)}`);
this.upload.open = false;
return;
}
this.$refs.upload.submit();
},
// 关闭导入对话框
handleImportDialogClose() {
this.upload.isUploading = false;
this.$refs.upload.clearFiles();
},
/** 导入类型展示 */
getImportTypeLabel(importType) {
return importType === "work" ? "历史工作经历导入" : "招聘信息导入";
}
}
};
</script>
<style scoped>
.detail-container {
padding: 0 20px;
}
.el-divider {
margin: 16px 0;
}
.work-experience-empty {
padding: 24px 0 8px;
}
</style>