Browse Source

ly 培训优化

ly 2 months ago
parent
commit
406db6cd66

+ 30 - 22
master/src/main/java/com/ruoyi/project/training/controller/TTrainingParticipantsController.java

@@ -70,7 +70,7 @@ public class TTrainingParticipantsController extends BaseController
         List<List> companyLevelList = new ArrayList<>();
         List<TTrainingCompanylevel> tTrainingCompanylevels = tTrainingCompanylevelService.selectTTrainingCompanylevelList(new TTrainingCompanylevel());
         TStaffmgr staffmgr = new TStaffmgr();
-        staffmgr.setUnits("10,18,20,30");
+        staffmgr.setUnits("10,18,20,30,34,38");
         staffmgr.setLeftYear(String.valueOf(Calendar.getInstance().YEAR));
         List<TStaffmgr> staffmgrs = tStaffmgrService.selectRecordList(staffmgr);
         try {
@@ -87,21 +87,33 @@ public class TTrainingParticipantsController extends BaseController
         }catch (Exception e) {
             logger.error(e.toString());
         }
+        
+        // 【性能优化】使用HashMap建立索引,避免三层嵌套循环
+        // 构建 staffId+companyId -> TTrainingParticipants 的映射
+        Map<String, TTrainingParticipants> participantsMap = new HashMap<>();
+        for (TTrainingParticipants t : list) {
+            String key = t.getStaffId() + "_" + t.getCompanyId();
+            participantsMap.put(key, t);
+        }
+        
         for (int i = 0; i < tTrainingCompanylevels.size(); i++) {
             //每一类公司级培训数据
             List<String> company = new ArrayList<>();
             List<String> companyDate = new ArrayList<>();
-            company.add(tTrainingCompanylevels.get(i).getCourseCode());
-            companyDate.add(tTrainingCompanylevels.get(i).getCourseCode());
-            company.add(tTrainingCompanylevels.get(i).getItem());
-            companyDate.add(tTrainingCompanylevels.get(i).getItem());
-            if (tTrainingCompanylevels.get(i).getFrequency() != null) {
-                company.add("每" + tTrainingCompanylevels.get(i).getFrequency() + "年一次");
-                companyDate.add("每" + tTrainingCompanylevels.get(i).getFrequency() + "年一次");
+            TTrainingCompanylevel currentCourse = tTrainingCompanylevels.get(i);
+            
+            company.add(currentCourse.getCourseCode());
+            companyDate.add(currentCourse.getCourseCode());
+            company.add(currentCourse.getItem());
+            companyDate.add(currentCourse.getItem());
+            
+            if (currentCourse.getFrequency() != null) {
+                company.add("每" + currentCourse.getFrequency() + "年一次");
+                companyDate.add("每" + currentCourse.getFrequency() + "年一次");
             }else {
                 TTrainingMatrix matrix = new TTrainingMatrix();
-                matrix.setCourseCode(tTrainingCompanylevels.get(i).getCourseCode());
-                matrix.setDeptId(tTrainingCompanylevels.get(i).getDeptId());
+                matrix.setCourseCode(currentCourse.getCourseCode());
+                matrix.setDeptId(currentCourse.getDeptId());
                 List<TTrainingMatrix> matrixList = tTrainingMatrixService.selectTTrainingMatrixList(matrix);
                 if (matrixList.size() > 0) {
                     company.add(matrixList.get(0).getFrequency());
@@ -111,23 +123,19 @@ public class TTrainingParticipantsController extends BaseController
                     companyDate.add("一次");
                 }
             }
+            
+            // 【性能优化】使用HashMap查找,O(1)时间复杂度
             for (TStaffmgr s : staffmgrs) {
-                //按人员筛
-                int m = 0;
-                TTrainingParticipants f = new TTrainingParticipants();
-                for (TTrainingParticipants t : list) {
-                    if (t.getStaffId().equals(s.getStaffid()) && t.getCompanyId().equals(tTrainingCompanylevels.get(i).getId())) {
-                        m = 1;
-                        f = t;
-                    }
-                }
-                if (m == 0) {
+                String key = s.getStaffid() + "_" + currentCourse.getId();
+                TTrainingParticipants participant = participantsMap.get(key);
+                
+                if (participant == null) {
                     company.add("⚪");
                     companyDate.add("N.A");
                 }else {
                     company.add("╳");
-                    if (f.getStartDate() != null) {
-                        companyDate.add(sdf.format(f.getStartDate()));
+                    if (participant.getStartDate() != null) {
+                        companyDate.add(sdf.format(participant.getStartDate()));
                     }else {
                         companyDate.add("-");
                     }

+ 38 - 17
master/src/main/java/com/ruoyi/project/training/controller/TTrainingrecordsController.java

@@ -83,27 +83,48 @@ public class TTrainingrecordsController extends BaseController
     }
 
     public AjaxResult syncData() {
+        // 删除需要删除的记录
         List<String> delList = tTrainingrecordsMapper.queryNeedDeleteIds();
-        if (delList.size() > 0){
-            for (String id : delList
-            ) {
-                TTrainingrecords del = tTrainingrecordsMapper.selectTTrainingrecordsBystaffId(Long.parseLong(id));
-                tTrainingrecordsMapper.deleteTTrainingrecordsById(del.getId());
+        if (delList != null && delList.size() > 0){
+            for (String id : delList) {
+                if (id == null || id.isEmpty()) {
+                    continue;
+                }
+                try {
+                    TTrainingrecords del = tTrainingrecordsMapper.selectTTrainingrecordsBystaffId(Long.parseLong(id));
+                    // 【修复】添加null检查,避免空指针异常
+                    if (del != null && del.getId() != null) {
+                        tTrainingrecordsMapper.deleteTTrainingrecordsById(del.getId());
+                    }
+                } catch (NumberFormatException e) {
+                    logger.error("Invalid staff id for deletion: " + id, e);
+                }
             }
         }
+        
+        // 添加需要新增的记录
         List<String> addList = tTrainingrecordsMapper.queryNeedInsertIds();
-        if (addList.size() > 0) {
-            for (String id: addList
-            ) {
-                TStaffmgr tStaffmgr = tStaffmgrMapper.selectTStaffmgrByStaffId(id);
-                TTrainingrecords tTrainingrecords = new TTrainingrecords();
-                tTrainingrecords.setStaffId(tStaffmgr.getId());
-                tTrainingrecords.setPlantCode(tStaffmgr.getPlantCode());
-                tTrainingrecords.setName(tStaffmgr.getName());
-                tTrainingrecords.setEmployeeid(tStaffmgr.getStaffid());
-                tTrainingrecords.setClasses(tStaffmgr.getTeam());
-                tTrainingrecords.setDeptId(tStaffmgr.getDeptId());
-                tTrainingrecordsService.insertTTrainingrecords(tTrainingrecords);
+        if (addList != null && addList.size() > 0) {
+            for (String id : addList) {
+                if (id == null || id.isEmpty()) {
+                    continue;
+                }
+                try {
+                    TStaffmgr tStaffmgr = tStaffmgrMapper.selectTStaffmgrByStaffId(id);
+                    // 【修复】添加null检查,避免空指针异常
+                    if (tStaffmgr != null) {
+                        TTrainingrecords tTrainingrecords = new TTrainingrecords();
+                        tTrainingrecords.setStaffId(tStaffmgr.getId());
+                        tTrainingrecords.setPlantCode(tStaffmgr.getPlantCode());
+                        tTrainingrecords.setName(tStaffmgr.getName());
+                        tTrainingrecords.setEmployeeid(tStaffmgr.getStaffid());
+                        tTrainingrecords.setClasses(tStaffmgr.getTeam());
+                        tTrainingrecords.setDeptId(tStaffmgr.getDeptId());
+                        tTrainingrecordsService.insertTTrainingrecords(tTrainingrecords);
+                    }
+                } catch (Exception e) {
+                    logger.error("Error adding training record for staff id: " + id, e);
+                }
             }
         }
 

+ 2 - 2
master/src/main/resources/mybatis/training/TTrainingrecordsMapper.xml

@@ -224,11 +224,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     <select id="queryNeedDeleteIds" resultType="String">
         SELECT b.EMPLOYEEID from t_trainingrecords b where b.del_flag = 0
             minus
-        SELECT a.STAFFID FROM "T_STAFFMGR" a where  a.del_flag = 0 and unit in (10,20,30)
+        SELECT a.STAFFID FROM "T_STAFFMGR" a where  a.del_flag = 0 and unit in (10,20,30,34,38)
     </select>
 
     <select id="queryNeedInsertIds" resultType="String">
-        SELECT a.STAFFID FROM "T_STAFFMGR" a where  a.del_flag = 0 and unit in (10,20,30)
+        SELECT a.STAFFID FROM "T_STAFFMGR" a where  a.del_flag = 0 and unit in (10,20,30,34,38)
             minus
         SELECT b.EMPLOYEEID from t_trainingrecords b where b.del_flag = 0
     </select>

+ 3 - 0
ui/src/main.js

@@ -28,6 +28,8 @@ import RightToolbar from "@/components/RightToolbar"
 //echarts
 import echarts from '@/api/cpms/echarts.min.js'
 import officeConvert from '@/utils/officeConvert.js'  //全局预览方法
+// 全局常量
+import * as constants from '@/utils/constants'
 //import { Loading } from 'element-ui'; //全局loading
 // 按需引入vue-awesome图标
 import Icon from 'vue-awesome/components/Icon';
@@ -75,6 +77,7 @@ Vue.prototype.download2 = download2
 Vue.prototype.handleTree = handleTree
 Vue.prototype.echarts = echarts
 Vue.prototype.officeConvert = officeConvert
+Vue.prototype.$constants = constants
 
 
 Vue.prototype.msgSuccess = function (msg) {

+ 12 - 0
ui/src/utils/constants.js

@@ -0,0 +1,12 @@
+/**
+ * 全局常量配置
+ */
+
+// 默认查询的部门单位ID列表
+export const DEFAULT_UNITS = "10,18,20,30,34,38"
+
+// 其他全局常量可以添加在这里
+// 例如:
+// export const PAGE_SIZE = 20
+// export const API_TIMEOUT = 5000
+

+ 1 - 1
ui/src/views/plant/staffmgr/index.vue

@@ -863,7 +863,7 @@
           isRetire: null
         },
         querypIdParams: {
-          units: '10,18,20,30,34,38',
+          units: this.$constants.DEFAULT_UNITS,
         },
         educations: [],
         units: [],

+ 4 - 1
ui/src/views/training/companylevel/index.vue

@@ -125,7 +125,10 @@
       <el-table-column :label="$t('课程名称')" align="center" prop="item" width="500" :show-overflow-tooltip="true"/>
       <el-table-column :label="$t('频率')" align="center" prop="frequency" width="100" :show-overflow-tooltip="true" >
         <template slot-scope="scope">
-          <span v-if="scope.row.frequency !== null">{{scope.row.frequency}}</span>
+          <span v-if="scope.row.frequency !== null">
+            <span v-if="!isNaN(scope.row.frequency)">每{{scope.row.frequency}}年一次</span>
+            <span v-else>{{scope.row.frequency}}</span>
+          </span>
           <span v-else>一次</span>
         </template>
       </el-table-column>

+ 149 - 3
ui/src/views/training/elearn/paper/result.vue

@@ -106,12 +106,68 @@
 
     </el-card>
 
+    <!-- 错题订正卡片 -->
+    <el-card v-if="showCorrection" style="margin-top: 20px">
+      <div slot="header" class="clearfix">
+        <span style="font-weight: bold; font-size: 16px;">📝 错题订正记录</span>
+      </div>
+
+      <div v-if="wrongQuestions.length === 0" style="text-align: center; padding: 30px; color: #67C23A;">
+        <i class="el-icon-success" style="font-size: 50px;"></i>
+        <p style="margin-top: 10px; font-size: 16px;">本次考试全部答对,无需订正。</p>
+      </div>
+
+      <div v-else v-for="(item, index) in wrongQuestions" :key="item.id" class="correction-item">
+        <div class="correction-header">
+          <span class="correction-number">错题 {{ index + 1 }}</span>
+          <el-tag type="danger" size="small">扣分:{{ item.actualScore || 0 }}</el-tag>
+        </div>
+
+        <div class="correction-question">
+          <p><strong>题目:</strong>{{ item.content }}</p>
+          <p v-if="item.image" style="margin-top: 10px;">
+            <el-image :src="item.image" style="max-width:100%;" />
+          </p>
+        </div>
+
+        <div v-if="item.quType === 1 || item.quType === 3" class="correction-answer">
+          <div class="answer-section wrong-answer">
+            <span class="answer-label">❌ 您的答案:</span>
+            <span>{{ myRadio[item.id] || '未答' }}</span>
+          </div>
+          <div class="answer-section right-answer">
+            <span class="answer-label">✅ 订正答案:</span>
+            <span>{{ radioRights[item.id] }}</span>
+          </div>
+        </div>
+
+        <div v-if="item.quType === 2" class="correction-answer">
+          <div class="answer-section wrong-answer">
+            <span class="answer-label">❌ 您的答案:</span>
+            <span>{{ myMulti[item.id] ? myMulti[item.id].join(', ') : '未答' }}</span>
+          </div>
+          <div class="answer-section right-answer">
+            <span class="answer-label">✅ 订正答案:</span>
+            <span>{{ multiRights[item.id] ? multiRights[item.id].join(', ') : '' }}</span>
+          </div>
+        </div>
+
+        <div v-if="item.quType === 4" class="correction-answer">
+          <div class="answer-section">
+            <span class="answer-label">您的回答:</span>
+            <p style="margin-left: 20px; padding: 10px; background: #f5f5f5; border-radius: 4px;">{{ item.answer || '未答' }}</p>
+          </div>
+        </div>
+      </div>
+    </el-card>
+
   </div>
 </template>
 
 <script>
 
 import { paperResult } from '@/api/training/elearn/paper'
+import { getDevice } from '@/api/training/bccdevice'
 
 export default {
   data() {
@@ -126,7 +182,11 @@ export default {
       radioRights: {},
       multiRights: {},
       myRadio: {},
-      myMulti: {}
+      myMulti: {},
+      // 错题订正相关
+      showCorrection: false,
+      wrongQuestions: [],
+      trainingCompleted: false
     }
   },
   created() {
@@ -185,9 +245,34 @@ export default {
           this.myMulti[item.id] = myMulti
         })
 
-        console.log(this.multiValues)
-        console.log(this.radioValues)
+        // 检查是否需要显示错题订正
+        this.checkAndShowCorrection()
       })
+    },
+    
+    /** 检查是否显示错题订正 */
+    checkAndShowCorrection() {
+      // 如果试卷包含deviceId,查询培训完成情况
+      if (this.paperData.deviceId) {
+        getDevice(this.paperData.deviceId).then(response => {
+          const deviceData = response.data
+          // 判断考试状态是否为1(已完成)
+          if (deviceData && deviceData.examState === 1) {
+            this.trainingCompleted = true
+            // 筛选错题
+            this.wrongQuestions = this.paperData.quList.filter(item => {
+              return !item.isRight || !item.answered
+            })
+            this.showCorrection = true
+          }
+        }).catch(() => {
+          // 查询失败,不显示订正卡片
+          this.showCorrection = false
+        })
+      } else {
+        // 没有deviceId,不显示订正卡片
+        this.showCorrection = false
+      }
     }
   }
 }
@@ -224,5 +309,66 @@ export default {
     margin: 2px;
   }
 
+  /* 错题订正样式 */
+  .correction-item {
+    border-bottom: 1px solid #eee;
+    padding: 20px 0;
+  }
+  
+  .correction-item:last-child {
+    border-bottom: none;
+  }
+  
+  .correction-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 15px;
+  }
+  
+  .correction-number {
+    font-size: 16px;
+    font-weight: bold;
+    color: #E6A23C;
+  }
+  
+  .correction-question {
+    background: #f9f9f9;
+    padding: 15px;
+    border-radius: 4px;
+    margin-bottom: 15px;
+  }
+  
+  .correction-question p {
+    line-height: 1.8;
+    margin: 5px 0;
+  }
+  
+  .correction-answer {
+    padding-left: 20px;
+  }
+  
+  .answer-section {
+    padding: 10px 15px;
+    border-radius: 4px;
+    margin-bottom: 10px;
+    line-height: 1.8;
+  }
+  
+  .wrong-answer {
+    background: #fef0f0;
+    border-left: 3px solid #F56C6C;
+  }
+  
+  .right-answer {
+    background: #f0f9ff;
+    border-left: 3px solid #67C23A;
+  }
+  
+  .answer-label {
+    font-weight: bold;
+    margin-right: 10px;
+  }
+
 </style>
 

+ 3 - 3
ui/src/views/training/matrix/index.vue

@@ -507,17 +507,17 @@ export default {
      },
     // 判断是否需要隐藏列
     shouldHideColumn(columnLabel) {
-      // 基础隐藏的列
+      // 基础隐藏的列(精确匹配)
       const baseHideColumns = ['工程师', '见习生', '机械维修经理', '电仪维修经理'];
 
       // 如果用户homeType为6,额外隐藏更多列
       if (this.$store.state.user.homeType == 6) {
         const additionalHideColumns = ['首席专家', '片区工长', 'OTS培训专员'];
         const allHideColumns = [...baseHideColumns, ...additionalHideColumns];
-        return allHideColumns.some(hideLabel => columnLabel.includes(hideLabel));
+        return allHideColumns.some(hideLabel => columnLabel === hideLabel);
       }
 
-      return baseHideColumns.some(hideLabel => columnLabel.includes(hideLabel));
+      return baseHideColumns.some(hideLabel => columnLabel === hideLabel);
     },
     // 获取岗位在原始数组中的索引
     getOriginalIndex(item) {

+ 2 - 2
ui/src/views/training/records/index.vue

@@ -242,7 +242,7 @@ export default {
       },
       //人员表查询参数
       staffmgrQueryParams: {
-        units: "10,18,20,30",
+        units: this.$constants.DEFAULT_UNITS,
         leftYear: this.getNowTime(),
       },
       queryCompanyParams: {
@@ -256,7 +256,7 @@ export default {
         trainingType: null
       },
       deviceParams: {
-        units: "10,18,20,30",
+        units: this.$constants.DEFAULT_UNITS,
         staffId: null,
         regularId: null,
         year: null,

+ 1 - 1
ui/src/views/training/trainingbcc/deviceList.vue

@@ -53,7 +53,7 @@
           size="mini"
           :disabled="single"
           @click="handleUpdate"
-          v-hasPermi="['training:training:edit']"
+          v-hasPermi="['training:trainingbcc:edit']"
         >{{ $t('修改') }}</el-button>
       </el-col>
 	  <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>

+ 9 - 9
ui/src/views/training/trainingbcc/index.vue

@@ -90,7 +90,7 @@
           icon="el-icon-plus"
           size="mini"
           @click="handleAdd"
-          v-hasPermi="['training:training:add']"
+          v-hasPermi="['training:trainingbcc:add']"
         >{{ $t('新增') }}</el-button>
       </el-col>
       <el-col :span="1.5">
@@ -100,7 +100,7 @@
           size="mini"
           :disabled="single"
           @click="handleUpdate"
-          v-hasPermi="['training:training:edit']"
+          v-hasPermi="['training:trainingbcc:edit']"
         >{{ $t('修改') }}</el-button>
       </el-col>
       <el-col :span="1.5">
@@ -119,7 +119,7 @@
           icon="el-icon-download"
           size="mini"
           @click="handleExport"
-          v-hasPermi="['training:training:export']"
+          v-hasPermi="['training:trainingbcc:export']"
         >{{ $t('导出') }}</el-button>
       </el-col>
       <el-col :span="1.5">
@@ -222,21 +222,21 @@
             type="text"
             icon="el-icon-edit"
             @click="handleUpdate(scope.row)"
-            v-hasPermi="['training:training:edit']"
+            v-hasPermi="['training:trainingbcc:edit']"
           >{{ $t('修改') }}</el-button>
           <el-button
             size="mini"
             type="text"
             icon="el-icon-edit"
             @click="handleStaffmgr(scope.row)"
-            v-hasPermi="['training:training:edit']"
+            v-hasPermi="['training:trainingbcc:edit']"
           >人员名单</el-button>
           <el-button
             size="mini"
             type="text"
             icon="el-icon-edit"
             @click="handleDetail(scope.row)"
-            v-hasPermi="['training:training:edit']"
+            v-hasPermi="['training:trainingbcc:edit']"
           >培训详情</el-button>
           <el-button
             size="mini"
@@ -494,7 +494,7 @@
               @click="handleSee(scope.row)"
             >{{ $t('预览') }}
             </el-button>
-            <el-button v-hasPermi="['training:trainingrecords:file']" type="text" size="small" v-if="scope.row.isEdit"
+            <el-button v-hasPermi="['training:trainingbcc:edit']" type="text" size="small" v-if="scope.row.isEdit"
                        @click="save(scope.row)">保存
             </el-button>
             <el-button type="text" size="small" v-if="scope.row.isEdit" @click="cancelFile(scope.row, scope.$index)">
@@ -512,7 +512,7 @@
               type="text"
               icon="el-icon-delete"
               @click="handleDeleteDoc(scope.row)"
-              v-hasPermi="['training:trainingrecords:file']"
+              v-hasPermi="['training:trainingbcc:edit']"
             >{{ $t('删除') }}
             </el-button>
           </template>
@@ -560,7 +560,7 @@ import {listExam} from "@/api/training/elearn/exam";
 import _ from 'lodash';
 
 export default {
-  name: "Training",
+  name: "Trainingbcc",
   components: { Treeselect, nonAdd, newAdd},
   data() {
     return {

+ 75 - 156
ui/src/views/training/trainingbcc/record.vue

@@ -29,13 +29,13 @@
       <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 icon="el-icon-download" size="mini" style="margin-left: 10px;" type="success" @click="exportToExcel" :disabled="!devicelevelList || devicelevelList.length === 0 || deviceTrainingLoad">
+        {{ $t('导出Excel') }}
       </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>
@@ -160,7 +160,6 @@ 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",
@@ -334,7 +333,7 @@ export default {
       },
       //人员表查询参数
       staffmgrQueryParams: {
-        units: "10,18,20,30",
+        units: this.$constants.DEFAULT_UNITS,
         leftYear: this.getNowTime(),
       },
       queryCompanyParams: {
@@ -348,7 +347,7 @@ export default {
         trainingType: null
       },
       deviceParams: {
-        units: "10,18,20,30",
+        units: this.$constants.DEFAULT_UNITS,
         staffId: null,
         regularId: null,
         year: null,
@@ -482,35 +481,35 @@ export default {
 
 
 
-      
 
-      
+
+
       // 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];
@@ -1269,163 +1268,83 @@ export default {
         this.$refs.snapshotForm.resetFields();
       });
     },
-    /** 导出图片 */
-    exportTableAsImage() {
+    /** 导出Excel */
+    exportToExcel() {
       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';
+      try {
+        // 构建表格HTML
+        let html = '<table border="1" cellspacing="0" cellpadding="5">';
 
-      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);
+        // 构建表头
+        html += '<thead><tr>';
+        html += '<th style="background:#f0f0f0;">课程代码</th>';
+        html += '<th style="background:#f0f0f0;">培训课程名称</th>';
+        if (Array.isArray(this.deviceStaffmgrs)) {
+          this.deviceStaffmgrs.forEach(staff => {
+            const headerText = `${staff.name || ''}${staff.actualpost ? '(' + staff.actualpost + ')' : ''}`;
+            html += `<th style="background:#f0f0f0;">${this.escapeHtml(headerText)}</th>`;
+          });
         }
-      }
-      thead.appendChild(headTr);
-      table.appendChild(thead);
+        html += '</tr></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] || [];
+        // 构建数据行
+        html += '<tbody>';
+        if (Array.isArray(this.devicelevelList)) {
+          this.devicelevelList.forEach(row => {
+            html += '<tr>';
+            if (Array.isArray(row)) {
+              row.forEach(cell => {
+                const cellValue = cell != null ? String(cell) : '';
+                html += `<td>${this.escapeHtml(cellValue)}</td>`;
+              });
+            }
+            html += '</tr>';
+          });
+        }
+        html += '</tbody></table>';
 
-        const tdCode = document.createElement('td');
-        tdCode.style.cssText = tdStyle(codeColWidth);
-        tdCode.innerText = row[0] != null ? String(row[0]) : '';
-        tr.appendChild(tdCode);
+        // 创建Blob并下载
+        const blob = new Blob(['\ufeff' + html], {
+          type: 'application/vnd.ms-excel;charset=utf-8;'
+        });
 
-        const tdName = document.createElement('td');
-        tdName.style.cssText = tdStyle(nameColWidth);
-        tdName.innerText = row[1] != null ? String(row[1]) : '';
-        tr.appendChild(tdName);
+        const fileName = `装置级培训人员名单_${this.deviceParams.year || new Date().getFullYear()}.xls`;
 
-        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);
+        // 创建下载链接
+        if (window.navigator.msSaveOrOpenBlob) {
+          // IE浏览器
+          window.navigator.msSaveOrOpenBlob(blob, fileName);
+        } else {
+          // 其他浏览器
+          const link = document.createElement('a');
+          link.href = URL.createObjectURL(blob);
+          link.download = fileName;
+          document.body.appendChild(link);
+          link.click();
+          document.body.removeChild(link);
+          URL.revokeObjectURL(link.href);
         }
-        tbody.appendChild(tr);
-      }
-      table.appendChild(tbody);
-
-      container.appendChild(table);
-      document.body.appendChild(container);
 
-      return { container, width: totalWidth };
+        this.msgSuccess('导出成功');
+      } catch (error) {
+        console.error('导出Excel失败:', error);
+        this.msgError('导出失败,请重试');
+      }
     },
-
-    // 根据单元格值返回导出时的内联样式(背景/字体颜色),与页面配色保持一致语义
-    getExportCellStyle(value) {
-      return '';
+    /** HTML转义 */
+    escapeHtml(text) {
+      const map = {
+        '&': '&amp;',
+        '<': '&lt;',
+        '>': '&gt;',
+        '"': '&quot;',
+        "'": '&#039;'
+      };
+      return String(text).replace(/[&<>"']/g, m => map[m]);
     }
   }
 };

+ 2 - 2
ui/src/views/training/trainingrecords/companyRecrods.vue

@@ -228,7 +228,7 @@ export default {
       },
       //人员表查询参数
       staffmgrQueryParams: {
-        units: "10,18,20,30",
+        units: this.$constants.DEFAULT_UNITS,
         leftYear: this.getNowTime(),
       },
       queryCompanyParams: {
@@ -242,7 +242,7 @@ export default {
         trainingType: null
       },
       deviceParams: {
-        units: "10,18,20,30",
+        units: this.$constants.DEFAULT_UNITS,
         staffId: null,
         regularId: null,
         year: null,

+ 2 - 2
ui/src/views/training/trainingrecords/index.vue

@@ -860,7 +860,7 @@ export default {
       },
       //人员表查询参数
       staffmgrQueryParams: {
-        units: "10,18,20,30",
+        units: this.$constants.DEFAULT_UNITS,
         leftYear: this.getNowTime(),
       },
       queryCompanyParams: {
@@ -874,7 +874,7 @@ export default {
         trainingType: null
       },
       deviceParams: {
-        units: "10,18,20,30",
+        units: this.$constants.DEFAULT_UNITS,
         staffId: null,
         regularId: null,
         year: null,