|
@@ -23,6 +23,19 @@
|
|
|
<el-button icon="el-icon-search" size="mini" style="margin-left: 20px;" type="cyan" @click="handleDeviceQuery">
|
|
|
{{ $t('搜索') }}
|
|
|
</el-button>
|
|
|
+ <el-button icon="el-icon-download" size="mini" style="margin-left: 10px;" type="success" @click="handleSnapshotData" :disabled="!devicelevelList || devicelevelList.length === 0">
|
|
|
+ {{ $t('固定数据') }}
|
|
|
+ </el-button>
|
|
|
+ <el-button icon="el-icon-view" size="mini" style="margin-left: 10px;" type="primary" @click="handleViewSnapshot">
|
|
|
+ {{ $t('查看快照') }}
|
|
|
+ </el-button>
|
|
|
+ <el-button icon="el-icon-picture-outline" size="mini" style="margin-left: 10px;" type="warning" @click="exportTableAsImage" :disabled="!devicelevelList || devicelevelList.length === 0 || deviceTrainingLoad">
|
|
|
+ {{ $t('导出图片') }}
|
|
|
+ </el-button>
|
|
|
+ <el-button icon="el-icon-arrow-down" size="mini" style="margin-left: 10px;" type="primary" @click="increaseHeight">
|
|
|
+ 增加高度
|
|
|
+ </el-button>
|
|
|
+
|
|
|
<el-row style="text-align: right">
|
|
|
<svg-icon class="rectangleGreen" icon-class="rectangleGreen"></svg-icon>
|
|
|
<span>{{ $t('需参加培训,已经完成培训人员') }}</span>
|
|
@@ -32,13 +45,28 @@
|
|
|
<span style="line-height: 40px;margin-left: 22px;">⚪ 不需要参加培训</span>
|
|
|
</el-row>
|
|
|
</el-form>
|
|
|
- <vue-draggable-resizable :draggable="dragMove" h="auto" style="background-color:white" w="auto" >
|
|
|
+ <vue-draggable-resizable
|
|
|
+ :draggable="dragMove"
|
|
|
+ h="auto"
|
|
|
+ style="background-color:white"
|
|
|
+ w="auto"
|
|
|
+ :w="vdr.width"
|
|
|
+ :h="vdr.height"
|
|
|
+ :min-width="500"
|
|
|
+ :min-height="300"
|
|
|
+ @activated="onVdrActivated"
|
|
|
+ @deactivated="onVdrDeactivated"
|
|
|
+ @dragstop="onVdrDragStop"
|
|
|
+ @resizestop="onVdrResizeStop"
|
|
|
+ :x="vdr.x"
|
|
|
+ :y="vdr.y"
|
|
|
+ >
|
|
|
<div ref="branch" class="zoom" @wheel.prevent="handleTableWheel($event)">
|
|
|
<el-table v-if="tableShow" ref="deviceTable" v-loading="deviceTrainingLoad"
|
|
|
:cell-class-name="tableCellClassName"
|
|
|
:data="devicelevelList" :span-method="colspanDeviceMethod"
|
|
|
border
|
|
|
- :height="height-100"
|
|
|
+ :height="clientHeight"
|
|
|
class="companyLevelTable">
|
|
|
<el-table-column :label="$t('课程代码')" align="center" fixed width="100">
|
|
|
<template slot-scope="scope">
|
|
@@ -62,6 +90,51 @@
|
|
|
</el-table>
|
|
|
</div>
|
|
|
</vue-draggable-resizable>
|
|
|
+ <!-- 用户导入对话框 -->
|
|
|
+ <el-dialog :title="upload.title" :visible.sync="upload.open" width="400px" append-to-body>
|
|
|
+ <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 text-center" slot="tip">
|
|
|
+ <span>仅允许导入xls、xlsx格式文件。</span>
|
|
|
+ <el-link type="primary" :underline="false" style="font-size:12px;vertical-align: baseline;" @click="importTemplate">下载模板</el-link>
|
|
|
+ </div>
|
|
|
+ </el-upload>
|
|
|
+ <div slot="footer" class="dialog-footer">
|
|
|
+ <el-button type="primary" @click="submitFileForm">确 定</el-button>
|
|
|
+ <el-button @click="upload.open = false">取 消</el-button>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 数据快照对话框 -->
|
|
|
+ <el-dialog title="创建数据快照" :visible.sync="snapshotDialog.visible" width="500px" append-to-body>
|
|
|
+ <el-form ref="snapshotForm" :model="snapshotDialog.form" :rules="snapshotDialog.rules" label-width="100px">
|
|
|
+ <el-form-item label="快照名称" prop="snapshotName">
|
|
|
+ <el-input v-model="snapshotDialog.form.snapshotName" placeholder="请输入快照名称,如:2024年培训数据快照" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="快照描述" prop="description">
|
|
|
+ <el-input type="textarea" v-model="snapshotDialog.form.description" placeholder="请输入快照描述,说明此快照的用途和内容" :rows="3" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="快照年份" prop="snapshotYear">
|
|
|
+ <el-date-picker v-model="snapshotDialog.form.snapshotYear" type="year" value-format="yyyy" placeholder="选择快照年份" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <div slot="footer" class="dialog-footer">
|
|
|
+ <el-button type="primary" @click="submitSnapshotData">确 定</el-button>
|
|
|
+ <el-button @click="snapshotDialog.visible = false">取 消</el-button>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
@@ -86,6 +159,8 @@ import Treeselect from "@riophae/vue-treeselect";
|
|
|
import "@riophae/vue-treeselect/dist/vue-treeselect.css";
|
|
|
import {addCommonfile, allFileList, delCommonfile, updateCommonfile} from "@/api/common/commonfile";
|
|
|
import VueDraggableResizable from 'vue-draggable-resizable';
|
|
|
+import { createSnapshot, querySnapshot } from '@/api/training/snapshot';
|
|
|
+import html2canvas from 'html2canvas';
|
|
|
|
|
|
export default {
|
|
|
name: "records",
|
|
@@ -95,6 +170,12 @@ export default {
|
|
|
return (() => {
|
|
|
window.screenHeight = document.body.clientHeight - 155;
|
|
|
this.height = window.screenHeight;
|
|
|
+ // 同步容器尺寸
|
|
|
+ this.vdr.width = document.body.clientWidth - 80;
|
|
|
+ this.vdr.height = (document.body.clientHeight - 80) * 0.8;
|
|
|
+ this.$nextTick(() => {
|
|
|
+ if (this.$refs.deviceTable) this.$refs.deviceTable.doLayout();
|
|
|
+ });
|
|
|
})();
|
|
|
};
|
|
|
},
|
|
@@ -150,6 +231,16 @@ export default {
|
|
|
deviceTrainingLoad: false,
|
|
|
tableShow: true,
|
|
|
trainingTimeOpen: false,
|
|
|
+ // 可拖拽缩放容器状态
|
|
|
+ vdr: {
|
|
|
+ x: 0,
|
|
|
+ y: 0,
|
|
|
+ width: document.body.clientWidth - 80,
|
|
|
+ height: (document.body.clientHeight - 80) * 0.8,
|
|
|
+ minWidth: 600,
|
|
|
+ minHeight: 300
|
|
|
+ },
|
|
|
+
|
|
|
// 装置名称字典
|
|
|
plantCodeOptions: [],
|
|
|
// 班值字典
|
|
@@ -290,6 +381,20 @@ export default {
|
|
|
{required: true, message: this.$t('部门编号') + this.$t('不能为空'), trigger: "blur"}
|
|
|
]
|
|
|
},
|
|
|
+ // 数据快照对话框
|
|
|
+ snapshotDialog: {
|
|
|
+ visible: false,
|
|
|
+ form: {
|
|
|
+ snapshotName: '',
|
|
|
+ description: '',
|
|
|
+ snapshotYear: ''
|
|
|
+ },
|
|
|
+ rules: {
|
|
|
+ snapshotName: [{ required: true, message: '请输入快照名称', trigger: 'blur' }],
|
|
|
+ description: [{ required: true, message: '请输入快照描述', trigger: 'blur' }],
|
|
|
+ snapshotYear: [{ required: true, message: '请选择快照年份', trigger: 'change' }]
|
|
|
+ }
|
|
|
+ },
|
|
|
};
|
|
|
|
|
|
},
|
|
@@ -358,6 +463,7 @@ export default {
|
|
|
this.trainingTypeOptions = response.data;
|
|
|
});
|
|
|
},
|
|
|
+
|
|
|
methods: {
|
|
|
handleTableWheel(event) {
|
|
|
let obj = this.$refs['branch']
|
|
@@ -373,7 +479,39 @@ export default {
|
|
|
}
|
|
|
return false
|
|
|
},
|
|
|
- colspanMethod({row, column, rowIndex, columnIndex}) {
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ // vue-draggable-resizable 相关方法
|
|
|
+ onVdrActivated() {
|
|
|
+ console.log('容器激活');
|
|
|
+ },
|
|
|
+
|
|
|
+ onVdrDeactivated() {
|
|
|
+ console.log('容器失活');
|
|
|
+ },
|
|
|
+
|
|
|
+ onVdrDragStop(x, y) {
|
|
|
+ console.log('拖拽停止:', { x, y });
|
|
|
+ this.vdr.x = x;
|
|
|
+ this.vdr.y = y;
|
|
|
+ },
|
|
|
+
|
|
|
+ // 增加容器高度
|
|
|
+ increaseHeight() {
|
|
|
+ console.log('增加高度前:', this.vdr.height);
|
|
|
+ this.vdr.height += 200; // 每次增加200px
|
|
|
+ console.log('增加高度后:', this.vdr.height);
|
|
|
+
|
|
|
+ // 同时更新表格高度
|
|
|
+ this.clientHeight = (this.vdr.height - 80) * 0.8;
|
|
|
+ console.log('更新表格高度:', this.clientHeight);
|
|
|
+ },
|
|
|
+
|
|
|
+ colspanMethod({row, column, rowIndex, columnIndex}) {
|
|
|
if (columnIndex === 0) {
|
|
|
const _row = this.setTable(this.companylevelList).merge[rowIndex];
|
|
|
const _col = _row > 0 ? 1 : 0;
|
|
@@ -1030,6 +1168,265 @@ export default {
|
|
|
this.devicelevelList = []
|
|
|
this.tableShow = false
|
|
|
},
|
|
|
+ /** 创建数据快照 */
|
|
|
+ handleSnapshotData() {
|
|
|
+ // 设置默认年份为当前查询的年份
|
|
|
+ this.snapshotDialog.form.snapshotYear = this.deviceParams.year || this.getNowTime();
|
|
|
+ this.snapshotDialog.visible = true;
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.$refs.snapshotForm.resetFields();
|
|
|
+ // 重新设置年份
|
|
|
+ this.snapshotDialog.form.snapshotYear = this.deviceParams.year || this.getNowTime();
|
|
|
+ });
|
|
|
+ },
|
|
|
+ /** 提交数据快照 */
|
|
|
+ submitSnapshotData() {
|
|
|
+ this.$refs.snapshotForm.validate(valid => {
|
|
|
+ if (valid) {
|
|
|
+ // 准备快照数据(忽略培训类型)
|
|
|
+ const snapshotData = {
|
|
|
+ snapshotName: this.snapshotDialog.form.snapshotName,
|
|
|
+ description: this.snapshotDialog.form.description,
|
|
|
+ snapshotYear: this.snapshotDialog.form.snapshotYear,
|
|
|
+ deviceData: this.devicelevelList,
|
|
|
+ staffData: this.deviceStaffmgrs,
|
|
|
+ queryParams: this.deviceParams
|
|
|
+ };
|
|
|
+ // 调用后端API创建快照
|
|
|
+ createSnapshot(snapshotData).then(response => {
|
|
|
+ if (response.code === 200) {
|
|
|
+ this.msgSuccess('数据快照创建成功!');
|
|
|
+ this.snapshotDialog.visible = false;
|
|
|
+ } else {
|
|
|
+ this.msgError(response.msg || '创建快照失败');
|
|
|
+ }
|
|
|
+ }).catch(error => {
|
|
|
+ this.msgError('创建快照失败:' + error.message);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ /** 查看快照 */
|
|
|
+ handleViewSnapshot() {
|
|
|
+ if (!this.deviceParams.year) {
|
|
|
+ this.msgError('请选择年份');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 先显示表格并开启loading
|
|
|
+ this.tableShow = true;
|
|
|
+ this.deviceTrainingOpen = true;
|
|
|
+ this.deviceTrainingLoad = true;
|
|
|
+ const params = {
|
|
|
+ snapshotYear: this.deviceParams.year
|
|
|
+ };
|
|
|
+ querySnapshot(params)
|
|
|
+ .then(res => {
|
|
|
+ if (res.code !== 200 || !res.data) {
|
|
|
+ this.msgError(res.msg || '未找到对应快照');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const snapshotData = res.data.snapshotData ? JSON.parse(res.data.snapshotData) : [];
|
|
|
+ if (Array.isArray(snapshotData)) {
|
|
|
+ this.devicelevelList = snapshotData;
|
|
|
+ } else if (snapshotData && snapshotData.deviceData) {
|
|
|
+ this.devicelevelList = snapshotData.deviceData || [];
|
|
|
+ if (snapshotData.staffData && Array.isArray(snapshotData.staffData)) {
|
|
|
+ this.deviceStaffmgrs = snapshotData.staffData;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ this.devicelevelList = [];
|
|
|
+ }
|
|
|
+ this.deviceTrainingTitle = `${this.$t('查看')} ${this.deviceParams.year} ${this.$t('装置级')} ${this.$t('培训人员')} ${this.$t('名单')}(${this.$t('快照')})`;
|
|
|
+ this.$nextTick(() => {
|
|
|
+ if (this.$refs.deviceTable) this.$refs.deviceTable.doLayout();
|
|
|
+ });
|
|
|
+ } catch (e) {
|
|
|
+ console.error(e);
|
|
|
+ this.msgError('快照数据解析失败');
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .finally(() => {
|
|
|
+ this.deviceTrainingLoad = false;
|
|
|
+ });
|
|
|
+ },
|
|
|
+ /** 查询快照列表 */
|
|
|
+ querySnapshotList() {
|
|
|
+ this.loading = true;
|
|
|
+ querySnapshot(this.snapshotDialog.form).then(response => {
|
|
|
+ this.snapshotList = response.rows;
|
|
|
+ this.total = response.total;
|
|
|
+ this.loading = false;
|
|
|
+ });
|
|
|
+ },
|
|
|
+ /** 加载快照详情 */
|
|
|
+ loadSnapshotDetail(row) {
|
|
|
+ this.snapshotDialog.form.snapshotName = row.snapshotName;
|
|
|
+ this.snapshotDialog.form.description = row.description;
|
|
|
+ this.snapshotDialog.form.snapshotYear = row.snapshotYear;
|
|
|
+ this.snapshotDialog.visible = true;
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.$refs.snapshotForm.resetFields();
|
|
|
+ });
|
|
|
+ },
|
|
|
+ /** 导出图片 */
|
|
|
+ exportTableAsImage() {
|
|
|
+ if (!this.devicelevelList || this.devicelevelList.length === 0) {
|
|
|
+ this.msgError('没有可导出的数据');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 构建离屏纯HTML表格,避免Element UI结构影响
|
|
|
+ const { container, width } = this.buildPlainExportTable();
|
|
|
+ const height = container.scrollHeight;
|
|
|
+ const scale = Math.min((window.devicePixelRatio || 2), 2);
|
|
|
+
|
|
|
+ this.msgSuccess('正在导出,请稍候...');
|
|
|
+ html2canvas(container, {
|
|
|
+ backgroundColor: '#FFFFFF',
|
|
|
+ scale,
|
|
|
+ width,
|
|
|
+ height,
|
|
|
+ windowWidth: width,
|
|
|
+ windowHeight: height,
|
|
|
+ scrollX: 0,
|
|
|
+ scrollY: 0,
|
|
|
+ useCORS: true,
|
|
|
+ allowTaint: true
|
|
|
+ }).then(canvas => {
|
|
|
+ const dataURL = canvas.toDataURL('image/png');
|
|
|
+ const link = document.createElement('a');
|
|
|
+ const fileName = `培训矩阵_${this.deviceParams.year || ''}.png`;
|
|
|
+ link.href = dataURL;
|
|
|
+ link.download = fileName;
|
|
|
+ document.body.appendChild(link);
|
|
|
+ link.click();
|
|
|
+ document.body.removeChild(link);
|
|
|
+ }).catch(err => {
|
|
|
+ console.error(err);
|
|
|
+ this.msgError('导出失败,请重试');
|
|
|
+ }).finally(() => {
|
|
|
+ // 移除离屏容器
|
|
|
+ if (container && container.parentNode) {
|
|
|
+ container.parentNode.removeChild(container);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ // 构建离屏纯HTML表格,返回 { container, width }
|
|
|
+ buildPlainExportTable() {
|
|
|
+ const padding = 24;
|
|
|
+ const codeColWidth = 100;
|
|
|
+ const nameColWidth = 250;
|
|
|
+ const staffColWidth = 150;
|
|
|
+
|
|
|
+ // 计算总宽
|
|
|
+ const staffCount = Array.isArray(this.deviceStaffmgrs) ? this.deviceStaffmgrs.length : 0;
|
|
|
+ const totalWidth = padding * 2 + codeColWidth + nameColWidth + staffCount * staffColWidth;
|
|
|
+
|
|
|
+ // 创建离屏容器
|
|
|
+ const container = document.createElement('div');
|
|
|
+ container.style.position = 'fixed';
|
|
|
+ container.style.left = '-10000px';
|
|
|
+ container.style.top = '0';
|
|
|
+ container.style.background = '#FFFFFF';
|
|
|
+ container.style.zIndex = '-1';
|
|
|
+ container.style.padding = padding + 'px';
|
|
|
+ container.style.width = totalWidth + 'px';
|
|
|
+ container.style.boxSizing = 'border-box';
|
|
|
+ container.style.color = '#000';
|
|
|
+
|
|
|
+ // 标题
|
|
|
+ const title = document.createElement('div');
|
|
|
+ title.style.fontSize = '16px';
|
|
|
+ title.style.fontWeight = '600';
|
|
|
+ title.style.marginBottom = '12px';
|
|
|
+ title.innerText = `${this.deviceParams.year || ''} 装置级 培训人员 名单`;
|
|
|
+ container.appendChild(title);
|
|
|
+
|
|
|
+ // 说明
|
|
|
+ const tip = document.createElement('div');
|
|
|
+ tip.style.fontSize = '12px';
|
|
|
+ tip.style.color = '#666';
|
|
|
+ tip.style.marginBottom = '8px';
|
|
|
+ tip.innerText = '(导出自系统视图,含完整数据)';
|
|
|
+ container.appendChild(tip);
|
|
|
+
|
|
|
+ // 生成纯表格
|
|
|
+ const table = document.createElement('table');
|
|
|
+ table.style.borderCollapse = 'collapse';
|
|
|
+ table.style.width = totalWidth - padding * 2 + 'px';
|
|
|
+ table.style.tableLayout = 'fixed';
|
|
|
+ table.style.fontSize = '12px';
|
|
|
+
|
|
|
+ const borderStyle = '1px solid #ccc';
|
|
|
+
|
|
|
+ // thead
|
|
|
+ const thead = document.createElement('thead');
|
|
|
+ const headTr = document.createElement('tr');
|
|
|
+ const thStyle = (w) => `border:${borderStyle};padding:6px;background:#f7f7f7;word-break:break-all;width:${w}px;`;
|
|
|
+ const tdStyle = (w) => `border:${borderStyle};padding:6px;word-break:break-all;width:${w}px;`;
|
|
|
+
|
|
|
+ const thCode = document.createElement('th');
|
|
|
+ thCode.style.cssText = thStyle(codeColWidth);
|
|
|
+ thCode.innerText = '课程代码';
|
|
|
+ headTr.appendChild(thCode);
|
|
|
+
|
|
|
+ const thName = document.createElement('th');
|
|
|
+ thName.style.cssText = thStyle(nameColWidth);
|
|
|
+ thName.innerText = '培训课程名称';
|
|
|
+ headTr.appendChild(thName);
|
|
|
+
|
|
|
+ // 人员列
|
|
|
+ if (staffCount > 0) {
|
|
|
+ for (let i = 0; i < staffCount; i++) {
|
|
|
+ const th = document.createElement('th');
|
|
|
+ th.style.cssText = thStyle(staffColWidth);
|
|
|
+ const staff = this.deviceStaffmgrs[i] || {};
|
|
|
+ th.innerText = `${staff.name || ''}${staff.actualpost ? '(' + staff.actualpost + ')' : ''}`;
|
|
|
+ headTr.appendChild(th);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ thead.appendChild(headTr);
|
|
|
+ table.appendChild(thead);
|
|
|
+
|
|
|
+ // tbody
|
|
|
+ const tbody = document.createElement('tbody');
|
|
|
+ const rows = Array.isArray(this.devicelevelList) ? this.devicelevelList : [];
|
|
|
+ for (let r = 0; r < rows.length; r++) {
|
|
|
+ const tr = document.createElement('tr');
|
|
|
+ const row = rows[r] || [];
|
|
|
+
|
|
|
+ const tdCode = document.createElement('td');
|
|
|
+ tdCode.style.cssText = tdStyle(codeColWidth);
|
|
|
+ tdCode.innerText = row[0] != null ? String(row[0]) : '';
|
|
|
+ tr.appendChild(tdCode);
|
|
|
+
|
|
|
+ const tdName = document.createElement('td');
|
|
|
+ tdName.style.cssText = tdStyle(nameColWidth);
|
|
|
+ tdName.innerText = row[1] != null ? String(row[1]) : '';
|
|
|
+ tr.appendChild(tdName);
|
|
|
+
|
|
|
+ for (let c = 0; c < staffCount; c++) {
|
|
|
+ const td = document.createElement('td');
|
|
|
+ const cell = row[c + 2];
|
|
|
+ td.style.cssText = tdStyle(staffColWidth) + ';text-align:center;';
|
|
|
+ td.innerText = cell != null ? String(cell) : '';
|
|
|
+ tr.appendChild(td);
|
|
|
+ }
|
|
|
+ tbody.appendChild(tr);
|
|
|
+ }
|
|
|
+ table.appendChild(tbody);
|
|
|
+
|
|
|
+ container.appendChild(table);
|
|
|
+ document.body.appendChild(container);
|
|
|
+
|
|
|
+ return { container, width: totalWidth };
|
|
|
+ },
|
|
|
+
|
|
|
+ // 根据单元格值返回导出时的内联样式(背景/字体颜色),与页面配色保持一致语义
|
|
|
+ getExportCellStyle(value) {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
}
|
|
|
};
|
|
|
</script>
|