|
@@ -0,0 +1,310 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <el-dialog
|
|
|
|
|
+ title="导入培训人员"
|
|
|
|
|
+ :visible.sync="visible"
|
|
|
|
|
+ width="760px"
|
|
|
|
|
+ append-to-body
|
|
|
|
|
+ :close-on-click-modal="false"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-alert
|
|
|
|
|
+ type="info"
|
|
|
|
|
+ :closable="false"
|
|
|
|
|
+ show-icon
|
|
|
|
|
+ title="按「工号」列导入,姓名可留空"
|
|
|
|
|
+ description="导入时以系统人员信息为准;模板第二行为示例行,填写前请先删除;工号建议设置为文本格式,避免前导 0 丢失。"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <div class="op-bar">
|
|
|
|
|
+ <el-button
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ icon="el-icon-download"
|
|
|
|
|
+ @click="downloadTemplate"
|
|
|
|
|
+ >下载模板</el-button
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-upload
|
|
|
|
|
+ action="#"
|
|
|
|
|
+ :show-file-list="false"
|
|
|
|
|
+ :http-request="handleFile"
|
|
|
|
|
+ :before-upload="beforeUpload"
|
|
|
|
|
+ accept=".xls,.xlsx"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-button size="small" icon="el-icon-folder-opened" :loading="parsing"
|
|
|
|
|
+ >选择文件</el-button
|
|
|
|
|
+ >
|
|
|
|
|
+ </el-upload>
|
|
|
|
|
+ <span class="file-name" v-if="fileName">{{ fileName }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <el-table
|
|
|
|
|
+ :data="rows"
|
|
|
|
|
+ v-loading="parsing"
|
|
|
|
|
+ border
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ max-height="320"
|
|
|
|
|
+ empty-text="请选择要导入的 Excel 文件"
|
|
|
|
|
+ style="width: 100%"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-table-column type="index" label="序号" width="60" align="center" />
|
|
|
|
|
+ <el-table-column
|
|
|
|
|
+ prop="jobNumber"
|
|
|
|
|
+ label="工号"
|
|
|
|
|
+ align="center"
|
|
|
|
|
+ min-width="120"
|
|
|
|
|
+ show-overflow-tooltip
|
|
|
|
|
+ />
|
|
|
|
|
+ <el-table-column
|
|
|
|
|
+ prop="fileName"
|
|
|
|
|
+ label="文件姓名"
|
|
|
|
|
+ align="center"
|
|
|
|
|
+ min-width="100"
|
|
|
|
|
+ show-overflow-tooltip
|
|
|
|
|
+ />
|
|
|
|
|
+ <el-table-column
|
|
|
|
|
+ prop="userName"
|
|
|
|
|
+ label="导入姓名"
|
|
|
|
|
+ align="center"
|
|
|
|
|
+ min-width="100"
|
|
|
|
|
+ show-overflow-tooltip
|
|
|
|
|
+ />
|
|
|
|
|
+ <el-table-column
|
|
|
|
|
+ prop="groupName"
|
|
|
|
|
+ label="所属部门"
|
|
|
|
|
+ align="center"
|
|
|
|
|
+ min-width="150"
|
|
|
|
|
+ show-overflow-tooltip
|
|
|
|
|
+ />
|
|
|
|
|
+ <el-table-column label="结果" align="center" min-width="180">
|
|
|
|
|
+ <template slot-scope="scope">
|
|
|
|
|
+ <el-tag size="mini" :type="statusTagType(scope.row.status)">{{
|
|
|
|
|
+ statusText(scope.row.status)
|
|
|
|
|
+ }}</el-tag>
|
|
|
|
|
+ <span v-if="scope.row.reason" class="reason-text">
|
|
|
|
|
+ {{ scope.row.reason }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+
|
|
|
|
|
+ <div slot="footer">
|
|
|
|
|
+ <span class="summary-text">
|
|
|
|
|
+ 共 {{ rows.length }} 行,可导入 {{ matchedRows.length }} 人<template
|
|
|
|
|
+ v-if="ignoredRows.length"
|
|
|
|
|
+ >
|
|
|
|
|
+ ,已忽略 {{ ignoredRows.length }} 行</template
|
|
|
|
|
+ >
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <el-button size="small" @click="visible = false">取 消</el-button>
|
|
|
|
|
+ <el-button
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ :disabled="!matchedRows.length"
|
|
|
|
|
+ @click="confirm"
|
|
|
|
|
+ >确定导入</el-button
|
|
|
|
|
+ >
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-dialog>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script>
|
|
|
|
|
+ import * as XLSX from 'xlsx';
|
|
|
|
|
+ import { getUserPage } from '@/api/system/organization';
|
|
|
|
|
+
|
|
|
|
|
+ // 单元格取值:统一转成去空格的字符串,数字型工号直接转字符串
|
|
|
|
|
+ function cellText(value) {
|
|
|
|
|
+ if (value === undefined || value === null) return '';
|
|
|
|
|
+ return String(value).trim();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ export default {
|
|
|
|
|
+ data() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ visible: false,
|
|
|
|
|
+ parsing: false,
|
|
|
|
|
+ fileName: '',
|
|
|
|
|
+ rows: [],
|
|
|
|
|
+ // 当前任务已选人员 id,用于标记重复
|
|
|
|
|
+ existIds: []
|
|
|
|
|
+ };
|
|
|
|
|
+ },
|
|
|
|
|
+ computed: {
|
|
|
|
|
+ matchedRows() {
|
|
|
|
|
+ return this.rows.filter((item) => item.status === 'success');
|
|
|
|
|
+ },
|
|
|
|
|
+ ignoredRows() {
|
|
|
|
|
+ return this.rows.filter((item) => item.status !== 'success');
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ methods: {
|
|
|
|
|
+ open(existIds = []) {
|
|
|
|
|
+ this.fileName = '';
|
|
|
|
|
+ this.rows = [];
|
|
|
|
|
+ this.existIds = (existIds || []).map((item) => String(item));
|
|
|
|
|
+ this.visible = true;
|
|
|
|
|
+ },
|
|
|
|
|
+ statusText(status) {
|
|
|
|
|
+ return { success: '可导入', warn: '已忽略', error: '失败' }[status];
|
|
|
|
|
+ },
|
|
|
|
|
+ statusTagType(status) {
|
|
|
|
|
+ return { success: 'success', warn: 'info', error: 'danger' }[status];
|
|
|
|
|
+ },
|
|
|
|
|
+ // 上传前校验:只允许 xls/xlsx,且不超过 5MB
|
|
|
|
|
+ beforeUpload(file) {
|
|
|
|
|
+ const ext = file.name
|
|
|
|
|
+ .slice(file.name.lastIndexOf('.') + 1)
|
|
|
|
|
+ .toLowerCase();
|
|
|
|
|
+ if (!['xls', 'xlsx'].includes(ext)) {
|
|
|
|
|
+ this.$message.error('只能导入 Excel 文件(.xls/.xlsx)');
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (file.size / 1024 / 1024 > 5) {
|
|
|
|
|
+ this.$message.error('导入文件大小不能超过 5MB');
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+ },
|
|
|
|
|
+ // 生成并下载导入模板
|
|
|
|
|
+ downloadTemplate() {
|
|
|
|
|
+ const sheet = XLSX.utils.aoa_to_sheet([
|
|
|
|
|
+ ['工号', '姓名'],
|
|
|
|
|
+ ['A1001', '张三']
|
|
|
|
|
+ ]);
|
|
|
|
|
+ sheet['!cols'] = [{ wch: 18 }, { wch: 14 }];
|
|
|
|
|
+ const book = XLSX.utils.book_new();
|
|
|
|
|
+ XLSX.utils.book_append_sheet(book, sheet, '培训人员');
|
|
|
|
|
+ XLSX.writeFile(book, '培训人员导入模板.xlsx');
|
|
|
|
|
+ },
|
|
|
|
|
+ // 解析文件并用工号反查系统人员
|
|
|
|
|
+ async handleFile(param) {
|
|
|
|
|
+ this.parsing = true;
|
|
|
|
|
+ this.fileName = param.file.name;
|
|
|
|
|
+ this.rows = [];
|
|
|
|
|
+ try {
|
|
|
|
|
+ const buffer = await param.file.arrayBuffer();
|
|
|
|
|
+ const book = XLSX.read(buffer, { type: 'array' });
|
|
|
|
|
+ const sheet = book.Sheets[book.SheetNames[0]];
|
|
|
|
|
+ const aoa = XLSX.utils.sheet_to_json(sheet, {
|
|
|
|
|
+ header: 1,
|
|
|
|
|
+ defval: '',
|
|
|
|
|
+ blankrows: false
|
|
|
|
|
+ });
|
|
|
|
|
+ if (!aoa.length) {
|
|
|
|
|
+ this.$message.error('文件内容为空');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const header = aoa[0].map((item) => cellText(item));
|
|
|
|
|
+ const jobCol = header.findIndex((item) => item.includes('工号'));
|
|
|
|
|
+ const nameCol = header.findIndex((item) => item.includes('姓名'));
|
|
|
|
|
+ if (jobCol < 0) {
|
|
|
|
|
+ this.$message.error('未找到「工号」列,请使用模板格式');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const list = [];
|
|
|
|
|
+ aoa.slice(1).forEach((row) => {
|
|
|
|
|
+ const jobNumber = cellText(row[jobCol]);
|
|
|
|
|
+ const fileName = nameCol < 0 ? '' : cellText(row[nameCol]);
|
|
|
|
|
+ if (!jobNumber && !fileName) return;
|
|
|
|
|
+ list.push({ jobNumber, fileName });
|
|
|
|
|
+ });
|
|
|
|
|
+ if (!list.length) {
|
|
|
|
|
+ this.$message.warning('文件内没有可导入的数据');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const userList = await getUserPage({ pageNum: 1, size: -1 });
|
|
|
|
|
+ this.rows = this.buildRows(list, userList.list || []);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ this.$message.error(error.message || '文件解析失败');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.parsing = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ // 按工号匹配系统人员,逐行标记可导入/忽略/失败
|
|
|
|
|
+ buildRows(list, users) {
|
|
|
|
|
+ const userMap = new Map();
|
|
|
|
|
+ users.forEach((item) => {
|
|
|
|
|
+ const key = cellText(item.jobNumber);
|
|
|
|
|
+ if (key) userMap.set(key, item);
|
|
|
|
|
+ });
|
|
|
|
|
+ const usedJobNumbers = new Set();
|
|
|
|
|
+ return list.map((item) => {
|
|
|
|
|
+ const base = {
|
|
|
|
|
+ jobNumber: item.jobNumber,
|
|
|
|
|
+ fileName: item.fileName,
|
|
|
|
|
+ userName: '',
|
|
|
|
|
+ groupName: ''
|
|
|
|
|
+ };
|
|
|
|
|
+ if (!item.jobNumber) {
|
|
|
|
|
+ return { ...base, status: 'error', reason: '工号为空' };
|
|
|
|
|
+ }
|
|
|
|
|
+ if (usedJobNumbers.has(item.jobNumber)) {
|
|
|
|
|
+ return { ...base, status: 'warn', reason: '文件内工号重复' };
|
|
|
|
|
+ }
|
|
|
|
|
+ usedJobNumbers.add(item.jobNumber);
|
|
|
|
|
+ const user = userMap.get(item.jobNumber);
|
|
|
|
|
+ if (!user) {
|
|
|
|
|
+ return { ...base, status: 'error', reason: '系统中不存在该工号' };
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this.existIds.includes(String(user.id))) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ ...base,
|
|
|
|
|
+ userName: user.name,
|
|
|
|
|
+ groupName: user.groupName || '',
|
|
|
|
|
+ status: 'warn',
|
|
|
|
|
+ reason: '已在当前任务人员列表中'
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+ // 文件里的姓名只作参考,最终以系统人员信息为准
|
|
|
|
|
+ const reason =
|
|
|
|
|
+ item.fileName && item.fileName !== user.name
|
|
|
|
|
+ ? '姓名与系统不一致,已按系统为准'
|
|
|
|
|
+ : '';
|
|
|
|
|
+ return {
|
|
|
|
|
+ ...base,
|
|
|
|
|
+ id: user.id,
|
|
|
|
|
+ userName: user.name,
|
|
|
|
|
+ groupName: user.groupName || '',
|
|
|
|
|
+ status: 'success',
|
|
|
|
|
+ reason
|
|
|
|
|
+ };
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+ confirm() {
|
|
|
|
|
+ const persons = this.matchedRows.map((item) => ({
|
|
|
|
|
+ id: item.id,
|
|
|
|
|
+ name: item.userName,
|
|
|
|
|
+ jobNumber: item.jobNumber,
|
|
|
|
|
+ groupName: item.groupName
|
|
|
|
|
+ }));
|
|
|
|
|
+ this.visible = false;
|
|
|
|
|
+ this.$emit('confirm', persons, this.ignoredRows.length);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style lang="scss" scoped>
|
|
|
|
|
+ .op-bar {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ margin: 12px 0;
|
|
|
|
|
+
|
|
|
|
|
+ .file-name {
|
|
|
|
|
+ margin-left: 10px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .reason-text {
|
|
|
|
|
+ margin-left: 6px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .summary-text {
|
|
|
|
|
+ float: left;
|
|
|
|
|
+ line-height: 32px;
|
|
|
|
|
+ color: #606266;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+</style>
|