weizhong 1 год назад
Родитель
Сommit
06731fd6e0

+ 10 - 0
backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsTrainingRecord.java

@@ -127,4 +127,14 @@ public class RsTrainingRecord extends BaseEntity {
     private String courseName;
     private Integer duration;
     private String location;
+    
+    // 关联字段 - 从申请表关联查询(不存储在记录表中)
+    @TableField(exist = false)
+    private String organizerUnit;      // 主办单位
+    
+    @TableField(exist = false)
+    private String trainingType;       // 培训类型
+    
+    @TableField(exist = false)
+    private String companions;         // 同行人员
 } 

+ 8 - 1
backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsTrainingRecordMapper.xml

@@ -37,6 +37,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="duration" column="duration"/>
         <result property="location" column="location"/>
         <result property="deptId" column="dept_id"/>
+        
+        <!-- 从申请表关联查询的字段 -->
+        <result property="organizerUnit" column="organizer_unit"/>
+        <result property="trainingType" column="training_type"/>
+        <result property="companions" column="companions"/>
     </resultMap>
 
     <sql id="selectRsTrainingRecordVo">
@@ -44,8 +49,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         r.completion_status, r.completion_time, r.score, r.certificate_path, r.certificate_number, r.attachment_ids,
         r.feedback, r.tenant_id, r.create_dept, r.create_by, r.create_time, r.update_by, r.update_time,
         r.department, r.position, r.reason, r.status, r.apply_time, r.current_approver, r.start_time, r.end_time,
-        r.course_name, r.duration, r.location, r.dept_id
+        r.course_name, r.duration, r.location, r.dept_id,
+        a.course_name as organizer_unit, a.training_type, a.companions
         from rs_training_record r
+        left join rs_training_application a on r.application_id = a.application_id and a.deleted = '0'
     </sql>
 
     <select id="selectRsTrainingRecordList" parameterType="RsTrainingRecord" resultMap="RsTrainingRecordResult">

+ 6 - 0
web/src/api/rs/training.ts

@@ -163,6 +163,12 @@ export interface RecordVO {
   createBy: string;
   updateBy: string;
   remark: string;
+  // 关联字段 - 从申请表获取
+  organizerUnit?: string;     // 主办单位
+  trainingType?: string;      // 培训类型
+  companions?: string;        // 同行人员
+  startTime?: string;         // 开始时间
+  endTime?: string;           // 结束时间
 }
 
 // 培训记录表单类型

+ 209 - 177
web/src/views/rs/training/index.vue

@@ -213,20 +213,44 @@
               @row-dblclick="handleViewRecord"
             >
               <el-table-column type="selection" width="55" align="center" />
-              <el-table-column label="课程名称" align="center" prop="courseName" min-width="150" :show-overflow-tooltip="true" />
-              <el-table-column label="培训地点" align="center" prop="location" min-width="110" :show-overflow-tooltip="true" />
-              <el-table-column label="部门" align="center" prop="department" min-width="110" :show-overflow-tooltip="true" />
-              <el-table-column label="完成状态" align="center" prop="completionStatus" width="100">
+              <!-- 主办单位:从申请表course_name字段关联获取 -->
+              <el-table-column label="主办单位" align="center" prop="organizerUnit" min-width="150" :show-overflow-tooltip="true">
                 <template #default="scope">
-                  <el-tag :type="scope.row.completionStatus === '0' ? 'warning' : scope.row.completionStatus === '1' ? 'success' : 'info'">
-                    {{ scope.row.completionStatus === '0' ? '未完成' : scope.row.completionStatus === '1' ? '已完成' : '未知' }}
-                  </el-tag>
+                  <span>{{ scope.row.organizerUnit || '暂无' }}</span>
+                </template>
+              </el-table-column>
+              <!-- 培训类型:从申请表training_type字段关联获取 -->
+              <el-table-column label="培训类型" align="center" prop="trainingType" min-width="110" :show-overflow-tooltip="true">
+                <template #default="scope">
+                  <span>{{ scope.row.trainingType || '暂无' }}</span>
                 </template>
               </el-table-column>
-              <el-table-column label="学时" align="center" prop="duration" width="80" />
-              <el-table-column label="完成时间" align="center" prop="completionTime" width="160">
+              <!-- 开始时间:从记录表start_time字段获取 -->
+              <el-table-column label="开始时间" align="center" prop="startTime" width="160">
                 <template #default="scope">
-                  <span>{{ parseTime(scope.row.completionTime) || '未完成' }}</span>
+                  <span>{{ parseTime(scope.row.startTime) || '暂无' }}</span>
+                </template>
+              </el-table-column>
+              <!-- 结束时间:从记录表end_time字段获取 -->
+              <el-table-column label="结束时间" align="center" prop="endTime" width="160">
+                <template #default="scope">
+                  <span>{{ parseTime(scope.row.endTime) || '暂无' }}</span>
+                </template>
+              </el-table-column>
+              <!-- 同行人员:从申请表companions字段关联获取,格式化为教师姓名 -->
+              <el-table-column label="同行人员" align="center" prop="companions" min-width="120" :show-overflow-tooltip="true">
+                <template #default="scope">
+                  <span v-if="scope.row.companions && scope.row.companions !== '[]'">
+                    {{ formatCompanions(scope.row.companions) }}
+                  </span>
+                  <span v-else>无</span>
+                </template>
+              </el-table-column>
+              <el-table-column label="完成状态" align="center" prop="completionStatus" width="100">
+                <template #default="scope">
+                  <el-tag :type="getCompletionStatusType(scope.row.completionStatus)">
+                    {{ getCompletionStatusLabel(scope.row.completionStatus) }}
+                  </el-tag>
                 </template>
               </el-table-column>
 
@@ -742,7 +766,6 @@
         </el-form-item>
         <el-form-item label="完成状态" prop="completionStatus">
           <el-radio-group v-model="recordForm.completionStatus">
-            <el-radio label="0">进行中</el-radio>
             <el-radio label="1">已完成</el-radio>
             <el-radio label="2">未完成</el-radio>
           </el-radio-group>
@@ -860,79 +883,58 @@
     </el-dialog>
 
     <!-- 培训记录详情对话框 -->
-    <el-dialog title="培训记录详情" v-model="recordDetailDialogVisible" width="700px" append-to-body>
+    <el-dialog title="培训记录详情" v-model="recordDetailDialogVisible" width="800px" append-to-body>
       <el-descriptions :column="2" border>
-        <el-descriptions-item label="课程名称">{{ recordDetail.courseName }}</el-descriptions-item>
+        <!-- 学员基本信息 -->
         <el-descriptions-item label="学员姓名">{{ recordDetail.userName }}</el-descriptions-item>
+        <el-descriptions-item label="所在部门(系部)">{{ recordDetail.department || '暂无' }}</el-descriptions-item>
+        <el-descriptions-item label="现职称">{{ recordDetail.currentTitle || '暂无' }}</el-descriptions-item>
+        <el-descriptions-item label="现岗位">{{ recordDetail.currentPosition || '暂无' }}</el-descriptions-item>
+        <el-descriptions-item label="参加工作时间">{{ parseTime(recordDetail.workStartTime) || '暂无' }}</el-descriptions-item>
+        <el-descriptions-item label="联系电话">{{ recordDetail.phone || '暂无' }}</el-descriptions-item>
+        
+        <!-- 培训情况信息 -->
+        <el-descriptions-item label="主办单位">{{ recordDetail.organizerUnit || recordDetail.courseName || '暂无' }}</el-descriptions-item>
+        <el-descriptions-item label="培训地点">{{ recordDetail.location || '暂无' }}</el-descriptions-item>
+        <el-descriptions-item label="开始时间">{{ parseTime(recordDetail.startTime) || '暂无' }}</el-descriptions-item>
+        <el-descriptions-item label="结束时间">{{ parseTime(recordDetail.endTime) || '暂无' }}</el-descriptions-item>
+        <el-descriptions-item label="培训类型">{{ recordDetail.trainingType || '暂无' }}</el-descriptions-item>
+        <el-descriptions-item label="培训费用">{{ recordDetail.trainingFee ? `${recordDetail.trainingFee}元` : '暂无' }}</el-descriptions-item>
+        <el-descriptions-item label="申请时间">{{ parseTime(recordDetail.applyTime) || '暂无' }}</el-descriptions-item>
         <el-descriptions-item label="完成状态">
-          <el-tag :type="recordDetail.completionStatus === '0' ? 'warning' : recordDetail.completionStatus === '1' ? 'success' : 'info'">
-            {{ recordDetail.completionStatus === '0' ? '进行中' : recordDetail.completionStatus === '1' ? '已完成' : '未完成' }}
+          <el-tag :type="getCompletionStatusType(recordDetail.completionStatus)">
+            {{ getCompletionStatusLabel(recordDetail.completionStatus) }}
           </el-tag>
         </el-descriptions-item>
-        <el-descriptions-item label="学时">{{ recordDetail.duration || '暂无' }}</el-descriptions-item>
-        <el-descriptions-item label="完成时间">{{ parseTime(recordDetail.completionTime) || '未完成' }}</el-descriptions-item>
-        <el-descriptions-item label="证书" :span="2">
-          <div v-if="getCertificatePath(recordDetail)" class="certificate-preview">
-            <!-- 证书预览区域 -->
-            <div class="certificate-container">
-              <!-- 统一显示为图片预览,支持OSS ID和传统路径 -->
-              <div class="image-preview">
-                <el-image
-                  v-if="certificatePreviewUrl"
-                  :src="certificatePreviewUrl"
-                  :preview-src-list="[certificatePreviewUrl]"
-                  fit="contain"
-                  style="width: 300px; height: 200px; border: 1px solid #dcdfe6; border-radius: 4px"
-                  preview-teleported
-                >
-                  <template #error>
-                    <div class="image-slot">
-                      <el-icon><Picture /></el-icon>
-                      <div>加载失败</div>
-                    </div>
-                  </template>
-                </el-image>
-                <div
-                  v-else
-                  class="loading-preview"
-                  style="
-                    width: 300px;
-                    height: 200px;
-                    border: 1px solid #dcdfe6;
-                    border-radius: 4px;
-                    display: flex;
-                    align-items: center;
-                    justify-content: center;
-                  "
-                >
-                  <el-icon class="is-loading"><Loading /></el-icon>
-                  <span style="margin-left: 8px">加载中...</span>
-                </div>
-              </div>
-            </div>
-            <!-- 操作按钮 -->
-            <div class="certificate-actions" style="margin-top: 10px">
-              <el-button type="primary" size="small" @click="viewCertificateDetail(recordDetail)">
-                <el-icon><View /></el-icon>
-                查看原图
-              </el-button>
-              <el-button type="success" size="small" @click="downloadCertificateFile(recordDetail)">
-                <el-icon><Download /></el-icon>
-                下载证书
-              </el-button>
-            </div>
-          </div>
-          <div v-else class="no-certificate">
-            <el-empty description="暂无证书" :image-size="60" />
-          </div>
-        </el-descriptions-item>
-        <el-descriptions-item label="证书编号">{{ recordDetail.certificateNumber || '无' }}</el-descriptions-item>
-        <el-descriptions-item label="创建时间">{{ parseTime(recordDetail.createTime) }}</el-descriptions-item>
-        <el-descriptions-item label="培训反馈" :span="2">
-          <div class="detail-text">{{ recordDetail.feedback || '暂无反馈' }}</div>
-        </el-descriptions-item>
+        <el-descriptions-item label="同行人" :span="2">{{ getCompanionNames(recordDetail.companions) || '无' }}</el-descriptions-item>
+        <el-descriptions-item label="培训目的" :span="2">{{ recordDetail.trainingPurpose || recordDetail.reason || '暂无' }}</el-descriptions-item>
         <el-descriptions-item label="备注" :span="2">{{ recordDetail.remark || '暂无备注' }}</el-descriptions-item>
       </el-descriptions>
+
+      <!-- 申请附件显示区域 -->
+      <div v-if="recordDetailAttachmentList.length > 0" class="attachment-section" style="margin-top: 20px;">
+        <div class="section-title" style="margin-bottom: 15px; padding: 10px; border-left: 4px solid #409eff; background-color: #f5f7fa;">
+          <el-icon style="margin-right: 8px;"><Paperclip /></el-icon>
+          <strong>申请附件</strong>
+        </div>
+        <el-table :data="recordDetailAttachmentList" border size="small">
+          <el-table-column label="序号" type="index" width="60" align="center" />
+          <el-table-column label="文件名称" prop="name" align="left" show-overflow-tooltip />
+          <el-table-column label="上传时间" prop="uploadTime" width="180" align="center">
+            <template #default="scope">
+              {{ parseTime(scope.row.uploadTime) }}
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" width="100" align="center">
+            <template #default="scope">
+              <el-button type="primary" link icon="Download" @click="handleDownloadApplicationAttachment(scope.row)" size="small">
+                下载
+              </el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+
       <template #footer>
         <div class="dialog-footer">
           <el-button @click="recordDetailDialogVisible = false">关 闭</el-button>
@@ -1270,8 +1272,7 @@ const recordRules = reactive({
 
 // 培训记录详情
 const recordDetailDialogVisible = ref(false);
-const certificatePreviewUrl = ref('');
-const recordDetail = reactive<RecordVO>({
+const recordDetail = reactive<any>({
   recordId: undefined,
   courseId: undefined,
   courseName: '',
@@ -1285,7 +1286,22 @@ const recordDetail = reactive<RecordVO>({
   updateTime: '',
   createBy: '',
   updateBy: '',
-  remark: ''
+  remark: '',
+  // 从培训申请获取的详细信息
+  department: '',
+  currentTitle: '',
+  currentPosition: '',
+  workStartTime: '',
+  phone: '',
+  organizerUnit: '',
+  trainingType: '',
+  trainingFee: '',
+  companions: '',
+  trainingPurpose: '',
+  location: '',
+  startTime: '',
+  endTime: '',
+  applyTime: ''
 });
 
 // 培训记录多选框选中数据
@@ -1323,6 +1339,7 @@ const applicationDialogLoading = ref(false);
 const applicationFormRef = ref<any>(null);
 const applicationAttachmentFileList = ref<any[]>([]); // 申请培训附件文件列表
 const applicationDetailAttachmentList = ref<any[]>([]); // 申请详情附件文件列表
+const recordDetailAttachmentList = ref<any[]>([]); // 培训记录详情附件文件列表
 const applicationForm = reactive<{
   applicationId?: number | string;
   // 情况基本信息
@@ -1620,6 +1637,24 @@ function getApplicationList() {
     });
 }
 
+/** 获取完成状态的标签类型 */
+const getCompletionStatusType = (status: string) => {
+  // 培训记录的完成状态逻辑
+  if (status === '1') return 'success';  // 已完成 - 绿色
+  if (status === '2') return 'danger';   // 未完成 - 红色  
+  // 对于空状态或其他状态,由于培训记录都是基于已通过审批的申请生成的,默认显示为已通过
+  return 'success';                      // 已通过 - 绿色
+};
+
+/** 获取完成状态的标签文本 */
+const getCompletionStatusLabel = (status: string) => {
+  // 培训记录的完成状态逻辑
+  if (status === '1') return '已完成';
+  if (status === '2') return '未完成';
+  // 对于空状态或其他状态,由于培训记录都是基于已通过审批的申请生成的,默认显示为已通过
+  return '已通过';
+};
+
 /** 获取教师列表 */
 function getTeacherList() {
   listClassTeacherCounselorRel({})
@@ -1712,6 +1747,8 @@ async function handleApplicationDetailAttachmentData(attachmentIds: string | nul
   }
 }
 
+
+
 /** 新增培训申请 */
 async function handleAddApplication() {
   resetApplicationForm();
@@ -2029,6 +2066,11 @@ function getCompanionNames(companions: (string | number)[] | string | null | und
   }).join('、');
 }
 
+/** 格式化同行人员显示(表格列使用) */
+function formatCompanions(companions: string | undefined): string {
+  return getCompanionNames(companions);
+}
+
 /** 查看申请详情 */
 async function handleViewApplicant(row: any) {
   // 如果行数据中没有完整的课程名称,尝试从课程列表获取
@@ -2372,8 +2414,16 @@ function handleRecordSelectionChange(selection: RecordVO[]) {
 function handleTrainingSubTabClick() {
   if (trainingSubTab.value === 'application') {
     getApplicationList();
+    // 确保教师列表已加载(用于同行人显示)
+    if (teacherList.value.length === 0) {
+      loadTeacherListForDisplay();
+    }
   } else if (trainingSubTab.value === 'record') {
     getRecordList();
+    // 确保教师列表已加载(用于同行人显示)
+    if (teacherList.value.length === 0) {
+      loadTeacherListForDisplay();
+    }
   } else {
     getTrainingList();
   }
@@ -2430,6 +2480,11 @@ function getRecordList() {
       recordList.value = response.rows;
       recordTotal.value = response.total;
       recordLoading.value = false;
+      
+      // 确保教师列表已加载(用于同行人显示)
+      if (teacherList.value.length === 0) {
+        loadTeacherListForDisplay();
+      }
     })
     .catch(() => {
       recordLoading.value = false;
@@ -2453,7 +2508,7 @@ function resetRecordForm() {
   recordForm.recordId = undefined;
   recordForm.courseId = '';
   recordForm.userId = '';
-  recordForm.completionStatus = '0';
+  recordForm.completionStatus = '1';
   recordForm.score = '';
   recordForm.completionTime = '';
   recordForm.certificatePath = '';
@@ -2670,31 +2725,78 @@ async function handleUpdateRecord(row: RecordVO) {
 
 /** 查看培训记录详情 */
 async function handleViewRecord(row: RecordVO) {
-  // 如果行数据中没有完整的课程名称,尝试从课程选项获取
-  if (!row.courseName && row.courseId) {
-    const course = courseOptions.value.find((c) => c.courseId === row.courseId);
-    if (course) {
-      row.courseName = course.title;
+  try {
+    // 确保教师列表已加载(用于显示同行人姓名)
+    if (teacherList.value.length === 0) {
+      await loadTeacherListForDisplay();
     }
-  }
 
-  // 复制记录详情
-  Object.assign(recordDetail, row);
+    // 首先复制记录的基本信息
+    Object.assign(recordDetail, row);
 
-  // 异步获取证书预览URL
-  const certificatePath = getCertificatePath(row);
-  if (certificatePath) {
-    try {
-      certificatePreviewUrl.value = await getSecureCertificatePreviewUrl(certificatePath);
-    } catch (error) {
-      console.error('获取证书预览URL失败:', error);
-      certificatePreviewUrl.value = '';
+    // 如果行数据中没有完整的课程名称,尝试从课程选项获取
+    if (!row.courseName && row.courseId) {
+      const course = courseOptions.value.find((c) => c.courseId === row.courseId);
+      if (course) {
+        recordDetail.courseName = course.title;
+      }
     }
-  } else {
-    certificatePreviewUrl.value = '';
-  }
 
-  recordDetailDialogVisible.value = true;
+    // 如果有申请ID,获取完整的申请详情来补充缺失的字段
+    if (row.applicationId) {
+      try {
+        const applicationResponse = await getTrainingApplication(row.applicationId);
+        const applicationData = applicationResponse.data;
+        
+                  // 合并申请数据中的详细信息到记录详情
+          Object.assign(recordDetail, {
+            // 保留记录的核心信息
+            recordId: row.recordId,
+            completionStatus: row.completionStatus,
+            completionTime: row.completionTime,
+            certificatePath: row.certificatePath,
+            certificateNumber: row.certificateNumber,
+            feedback: row.feedback,
+            
+            // 从申请数据中获取详细信息
+            department: applicationData.department || row.department,
+            currentTitle: applicationData.currentTitle,
+            currentPosition: applicationData.currentPosition,
+            workStartTime: applicationData.workStartTime,
+            phone: applicationData.phone,
+            organizerUnit: applicationData.organizerUnit,
+            trainingType: applicationData.trainingType,
+            trainingFee: applicationData.trainingFee,
+            companions: applicationData.companions,
+            trainingPurpose: applicationData.trainingPurpose || applicationData.reason,
+            location: applicationData.location || row.location,
+            startTime: applicationData.startTime,
+            endTime: applicationData.endTime,
+            applyTime: applicationData.applyTime,
+            
+            // 其他基本信息
+            courseName: applicationData.courseName || recordDetail.courseName,
+            userName: applicationData.userName || row.userName,
+            duration: applicationData.duration || row.duration,
+            remark: row.remark
+          });
+
+          // 加载申请附件数据
+          if (applicationData.attachmentIds) {
+            await handleApplicationDetailAttachmentData(applicationData.attachmentIds);
+            recordDetailAttachmentList.value = [...applicationDetailAttachmentList.value];
+          }
+      } catch (error) {
+        console.warn('获取申请详情失败,使用记录基本信息:', error);
+        // 如果获取申请详情失败,继续使用记录的基本信息
+      }
+    }
+
+    recordDetailDialogVisible.value = true;
+  } catch (error) {
+    console.error('查看培训记录详情失败:', error);
+    proxy?.$modal.msgError('获取培训记录详情失败,请重试');
+  }
 }
 
 /** 删除培训记录 */
@@ -3234,82 +3336,6 @@ function getCertificateFileName(filePath: string): string {
   return fileName;
 }
 
-/** 查看证书详情 */
-async function viewCertificateDetail(row: any) {
-  const certificatePath = getCertificatePath(row);
-  if (!certificatePath) {
-    proxy?.$modal.msgWarning('该培训记录暂无证书');
-    return;
-  }
-
-  try {
-    const previewUrl = await getSecureCertificatePreviewUrl(certificatePath);
-    if (previewUrl) {
-      window.open(previewUrl, '_blank');
-    } else {
-      proxy?.$modal.msgError('无法获取证书预览链接');
-    }
-  } catch (error) {
-    console.error('获取证书预览链接失败:', error);
-    proxy?.$modal.msgError('获取证书预览链接失败');
-  }
-}
-
-/** 下载证书文件 */
-async function downloadCertificateFile(row: any) {
-  const certificatePath = getCertificatePath(row);
-  if (!certificatePath) {
-    proxy?.$modal.msgWarning('该培训记录暂无证书');
-    return;
-  }
-
-  try {
-    // 如果是OSS ID,使用专门的下载方法
-    if (/^\d+$/.test(certificatePath.trim())) {
-      // 使用request下载,带认证信息
-      const loading = ElLoading.service({
-        text: '正在下载证书...',
-        background: 'rgba(0, 0, 0, 0.7)'
-      });
-
-      try {
-        const response = await request({
-          url: `/resource/oss/download/${certificatePath.trim()}`,
-          method: 'get',
-          responseType: 'blob',
-          timeout: 300000
-        });
-
-        if (response) {
-          const blob = response instanceof Blob ? response : response.data;
-          const url = window.URL.createObjectURL(blob);
-          const link = document.createElement('a');
-          link.href = url;
-          link.download = getCertificateFileName(certificatePath) || `certificate_${certificatePath}.pdf`;
-          document.body.appendChild(link);
-          link.click();
-          document.body.removeChild(link);
-          window.URL.revokeObjectURL(url);
-        }
-      } finally {
-        loading?.close();
-      }
-    } else {
-      // 普通文件路径的处理
-      const downloadUrl = getCertificatePreviewUrl(certificatePath);
-      const link = document.createElement('a');
-      link.href = downloadUrl;
-      link.download = getCertificateFileName(certificatePath);
-      document.body.appendChild(link);
-      link.click();
-      document.body.removeChild(link);
-    }
-  } catch (error) {
-    console.error('下载证书失败:', error);
-    proxy?.$modal.msgError('下载证书失败');
-  }
-}
-
 /** 下载证书 */
 function downloadCertificate(row: RecordVO) {
   if (!row.certificatePath) {
@@ -3635,6 +3661,10 @@ onActivated(() => {
 
   // 同时加载课程选项,确保下拉选项可用
   loadCourseOptions();
+  // 确保教师列表已加载(用于同行人显示)
+  if (teacherList.value.length === 0) {
+    loadTeacherListForDisplay();
+  }
 });
 
 onMounted(() => {
@@ -3646,6 +3676,8 @@ onMounted(() => {
   getRecordList();
   loadCourseOptions();
   getDepartmentList();
+  // 加载教师列表(用于同行人显示)
+  loadTeacherListForDisplay();
 });
 
 // ===== 申请培训附件上传相关方法 =====