Gqingci 1 неделя назад
Родитель
Сommit
560027d885

+ 2 - 0
package.json

@@ -33,9 +33,11 @@
     "element-plus": "2.11.7",
     "file-saver": "2.0.5",
     "highlight.js": "11.11.1",
+    "html2canvas": "^1.4.1",
     "image-conversion": "2.1.1",
     "js-cookie": "3.0.5",
     "jsencrypt": "3.5.4",
+    "jspdf": "^4.2.1",
     "nprogress": "0.2.0",
     "pinia": "3.0.3",
     "screenfull": "6.0.2",

+ 1 - 0
src/api/main/evaluation/types.ts

@@ -3,6 +3,7 @@ export interface MainExamEvaluationVO extends BaseEntity {
   evaluationName?: string;
   grade?: string;
   position?: string;
+  positionName?: string;
   positionType?: string;
   tags?: string;
   mainImage?: string | number;

+ 128 - 1
src/views/evaluation/components/evaluation-view.vue

@@ -26,6 +26,7 @@
 
         <div class="footer-btn">
           <el-button @click="handleBack">返回</el-button>
+          <el-button type="primary" :loading="exportLoading" @click="handleExport">导出测评</el-button>
         </div>
       </div>
 
@@ -76,6 +77,8 @@
 import { getCurrentInstance, type ComponentInternalInstance, ref, onMounted, nextTick, watch } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 import * as echarts from 'echarts';
+import html2canvas from 'html2canvas';
+import jsPDF from 'jspdf';
 import PageShell from '@/components/PageShell/index.vue';
 import { getEvaluation, getExamScoreList, getExamPaperQuestions, getExamAnswerList } from '@/api/main/evaluation/index';
 
@@ -95,6 +98,7 @@ const ansTime = ref('');
 const scores = ref<any[]>([]);
 const questionList = ref<any[]>([]);
 const radarChartRef = ref<HTMLElement | null>(null);
+const exportLoading = ref(false);
 let radarChart: echarts.ECharts | null = null;
 
 /** 解析接口返回的 msg 字段(JSON 字符串),提取 bizContent */
@@ -327,7 +331,7 @@ const loadData = async () => {
   try {
     const res = await getEvaluation(evaluationId);
     const evalData = res.data;
-    positionName.value = evalData.position || '未知岗位';
+    positionName.value = evalData.positionName || evalData.position || '未知岗位';
 
     const abilityConfigs = evalData.abilityConfigs || [];
     let allQuestions: any[] = [];
@@ -388,6 +392,129 @@ const loadData = async () => {
   }
 };
 
+const handleExport = async () => {
+  if (exportLoading.value) return;
+  exportLoading.value = true;
+  try {
+    const container = document.createElement('div');
+    container.style.cssText = 'position:fixed;left:-9999px;top:0;width:794px;background:#fff;padding:40px;font-family:"Microsoft YaHei",sans-serif;font-size:14px;color:#333;line-height:1.6;';
+
+    const title = document.createElement('h2');
+    title.style.cssText = 'text-align:center;margin-bottom:24px;font-size:20px;';
+    title.textContent = `测评报告 - ${positionName.value || '未知岗位'}`;
+    container.appendChild(title);
+
+    const infoSection = document.createElement('div');
+    infoSection.style.cssText = 'margin-bottom:20px;padding:16px;border:1px solid #e4e7ed;border-radius:6px;';
+    infoSection.innerHTML = `
+      <div style="font-size:16px;font-weight:600;margin-bottom:10px;">考生信息</div>
+      <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;font-size:13px;">
+        <div><strong>姓名:</strong>${studentName.value || '-'}</div>
+        <div><strong>报名岗位:</strong>${positionName.value || '未知岗位'}</div>
+        <div><strong>开始时间:</strong>${startTime.value || '-'}</div>
+        <div><strong>结束时间:</strong>${commitTime.value || '-'}</div>
+        <div><strong>答题时长:</strong>${ansTime.value || '-'}</div>
+      </div>
+    `;
+    container.appendChild(infoSection);
+
+    if (scores.value.length > 0) {
+      const scoreSection = document.createElement('div');
+      scoreSection.style.cssText = 'margin-bottom:20px;padding:16px;border:1px solid #e4e7ed;border-radius:6px;';
+      let scoreHtml = '<div style="font-size:16px;font-weight:600;margin-bottom:10px;">答题信息</div>';
+      scoreHtml += '<table style="width:100%;border-collapse:collapse;font-size:13px;">';
+      scoreHtml += '<tr style="background:#f5f7fa;"><th style="border:1px solid #e4e7ed;padding:8px;text-align:left;">能力项</th><th style="border:1px solid #e4e7ed;padding:8px;text-align:center;">得分</th><th style="border:1px solid #e4e7ed;padding:8px;text-align:center;">满分</th><th style="border:1px solid #e4e7ed;padding:8px;text-align:center;">结果</th></tr>';
+      scores.value.forEach((s: any) => {
+        const passColor = s.pass ? '#67c23a' : '#f56c6c';
+        const passText = s.pass ? '通过' : '未通过';
+        scoreHtml += `<tr><td style="border:1px solid #e4e7ed;padding:8px;">${s.abilityName}</td><td style="border:1px solid #e4e7ed;padding:8px;text-align:center;">${s.score}</td><td style="border:1px solid #e4e7ed;padding:8px;text-align:center;">${s.totalScore}</td><td style="border:1px solid #e4e7ed;padding:8px;text-align:center;color:${passColor};font-weight:600;">${passText}</td></tr>`;
+      });
+      scoreHtml += '</table>';
+      scoreSection.innerHTML = scoreHtml;
+      container.appendChild(scoreSection);
+    }
+
+    if (questionList.value.length > 0) {
+      const qSection = document.createElement('div');
+      qSection.style.cssText = 'padding:16px;border:1px solid #e4e7ed;border-radius:6px;';
+      let qHtml = '<div style="font-size:16px;font-weight:600;margin-bottom:10px;">答题详情</div>';
+      questionList.value.forEach((item: any, index: number) => {
+        const resultColor = item.correct ? '#67c23a' : '#f56c6c';
+        const resultText = item.correct ? '正确' : '错误';
+        qHtml += `<div style="margin-bottom:16px;padding-bottom:12px;border-bottom:1px dashed #ebeef5;">`;
+        qHtml += `<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">`;
+        qHtml += `<span>第${index + 1}题 <span style="background:#f0f2f5;color:#909399;padding:2px 6px;border-radius:3px;font-size:12px;">${item.typeName}</span></span>`;
+        qHtml += `<span><span style="color:${resultColor};font-weight:600;">${resultText}</span> | 得分:${item.score}</span>`;
+        qHtml += `</div>`;
+        const plainTitle = item.questionTitle?.replace(/<[^>]*>/g, '') || '未命名题目';
+        qHtml += `<div style="margin-bottom:8px;">${plainTitle}</div>`;
+        if (item.options && item.options.length > 0) {
+          item.options.forEach((opt: any) => {
+            const isCorrect = opt.correct;
+            const isUserAnswer = item.answer && item.answer.includes(opt.label);
+            let optStyle = 'padding:2px 0;';
+            if (isCorrect) optStyle += 'color:#67c23a;font-weight:600;';
+            if (isUserAnswer && !isCorrect) optStyle += 'color:#f56c6c;';
+            const tags = [];
+            if (isCorrect) tags.push('【正确答案】');
+            if (isUserAnswer && !isCorrect) tags.push('【考生选择】');
+            const plainText = opt.text?.replace(/<[^>]*>/g, '') || '';
+            qHtml += `<div style="${optStyle}">${opt.label}. ${plainText} ${tags.join('')}</div>`;
+          });
+        }
+        if (item.answer && (!item.options || item.options.length === 0)) {
+          const plainAnswer = item.answer?.replace(/<[^>]*>/g, '') || '';
+          qHtml += `<div style="margin-top:6px;padding:6px 10px;background:#f5f7fa;border-radius:4px;font-size:13px;">考生作答:${plainAnswer}</div>`;
+        }
+        if (item.testAnsRight) {
+          const plainRight = item.testAnsRight?.replace(/<[^>]*>/g, '') || '';
+          qHtml += `<div style="margin-top:6px;padding:6px 10px;background:#f0f9eb;border-radius:4px;font-size:13px;color:#67c23a;">参考答案:${plainRight}</div>`;
+        }
+        if (item.analysis) {
+          const plainAnalysis = item.analysis?.replace(/<[^>]*>/g, '') || '';
+          qHtml += `<div style="margin-top:6px;font-size:13px;color:#606266;">答案解析:${plainAnalysis}</div>`;
+        }
+        qHtml += `</div>`;
+      });
+      qSection.innerHTML = qHtml;
+      container.appendChild(qSection);
+    }
+
+    document.body.appendChild(container);
+    await nextTick();
+    await new Promise(resolve => setTimeout(resolve, 300));
+
+    const canvas = await html2canvas(container, { scale: 2, useCORS: true, backgroundColor: '#ffffff' });
+    document.body.removeChild(container);
+
+    const imgData = canvas.toDataURL('image/jpeg', 0.95);
+    const imgWidth = 210;
+    const pageHeight = 297;
+    const imgHeight = (canvas.height * imgWidth) / canvas.width;
+    const pdf = new jsPDF('p', 'mm', 'a4');
+    let heightLeft = imgHeight;
+    let position = 0;
+
+    pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight);
+    heightLeft -= pageHeight;
+
+    while (heightLeft > 0) {
+      position = heightLeft - imgHeight;
+      pdf.addPage();
+      pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight);
+      heightLeft -= pageHeight;
+    }
+
+    const fileName = `测评报告_${studentName.value || '考生'}_${positionName.value || '未知岗位'}.pdf`;
+    pdf.save(fileName);
+  } catch (e) {
+    console.error('导出测评失败:', e);
+    proxy?.$modal?.msgError('导出测评失败');
+  } finally {
+    exportLoading.value = false;
+  }
+};
+
 const handleBack = () => {
   proxy?.$tab.closePage();
   router.back();

+ 1 - 1
src/views/postManage/apply-detail.vue

@@ -258,7 +258,7 @@ const jobList = computed(() =>
     status: item.employmentStatus === 'onboard' ? '在职' : item.employmentStatus === 'left' ? '已离职' : displayText(item.employmentStatus),
     entryTime: formatDateTime(item.entryTime),
     leaveTime: formatDateTime(item.leaveTime),
-    leaveReason: displayText(item.leaveReason)
+    leaveReason: item.employmentStatus === 'onboard' ? '--' : displayText(item.leaveReason)
   }))
 );
 

+ 147 - 1
src/views/postManage/evaluation-view.vue

@@ -26,6 +26,7 @@
 
         <div class="footer-btn">
           <el-button @click="handleBack">返回</el-button>
+          <el-button type="primary" :loading="exportLoading" @click="handleExport">导出测评</el-button>
         </div>
       </div>
 
@@ -71,6 +72,8 @@
 import { getCurrentInstance, type ComponentInternalInstance, ref, onMounted, nextTick } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 import * as echarts from 'echarts';
+import html2canvas from 'html2canvas';
+import jsPDF from 'jspdf';
 import PageShell from '@/components/PageShell/index.vue';
 import { getEvaluation, getExamScoreList, getExamPaperQuestions, getExamAnswerList } from '@/api/main/evaluation/index';
 
@@ -90,6 +93,7 @@ const ansTime = ref('');
 const scores = ref<any[]>([]);
 const questionList = ref<any[]>([]);
 const radarChartRef = ref<HTMLElement | null>(null);
+const exportLoading = ref(false);
 let radarChart: echarts.ECharts | null = null;
 
 const parseBizContent = (res: any) => {
@@ -301,7 +305,7 @@ const loadData = async () => {
   try {
     const res = await getEvaluation(evaluationId);
     const evalData = res.data;
-    positionName.value = evalData.position || '未知岗位';
+    positionName.value = evalData.positionName || evalData.position || '未知岗位';
 
     const abilityConfigs = evalData.abilityConfigs || [];
     let allQuestions: any[] = [];
@@ -358,6 +362,148 @@ const loadData = async () => {
   }
 };
 
+const handleExport = async () => {
+  if (exportLoading.value) return;
+  exportLoading.value = true;
+  try {
+    // 构建导出用的临时 DOM
+    const container = document.createElement('div');
+    container.style.cssText = 'position:fixed;left:-9999px;top:0;width:794px;background:#fff;padding:40px;font-family:"Microsoft YaHei",sans-serif;font-size:14px;color:#333;line-height:1.6;';
+
+    // 标题
+    const title = document.createElement('h2');
+    title.style.cssText = 'text-align:center;margin-bottom:24px;font-size:20px;';
+    title.textContent = `测评报告 - ${positionName.value || '未知岗位'}`;
+    container.appendChild(title);
+
+    // 考生信息
+    const infoSection = document.createElement('div');
+    infoSection.style.cssText = 'margin-bottom:20px;padding:16px;border:1px solid #e4e7ed;border-radius:6px;';
+    infoSection.innerHTML = `
+      <div style="font-size:16px;font-weight:600;margin-bottom:10px;">考生信息</div>
+      <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;font-size:13px;">
+        <div><strong>姓名:</strong>${studentName.value || '-'}</div>
+        <div><strong>报名岗位:</strong>${positionName.value || '未知岗位'}</div>
+        <div><strong>开始时间:</strong>${startTime.value || '-'}</div>
+        <div><strong>结束时间:</strong>${commitTime.value || '-'}</div>
+        <div><strong>答题时长:</strong>${ansTime.value || '-'}</div>
+      </div>
+    `;
+    container.appendChild(infoSection);
+
+    // 答题信息
+    if (scores.value.length > 0) {
+      const scoreSection = document.createElement('div');
+      scoreSection.style.cssText = 'margin-bottom:20px;padding:16px;border:1px solid #e4e7ed;border-radius:6px;';
+      let scoreHtml = '<div style="font-size:16px;font-weight:600;margin-bottom:10px;">答题信息</div>';
+      scoreHtml += '<table style="width:100%;border-collapse:collapse;font-size:13px;">';
+      scoreHtml += '<tr style="background:#f5f7fa;"><th style="border:1px solid #e4e7ed;padding:8px;text-align:left;">能力项</th><th style="border:1px solid #e4e7ed;padding:8px;text-align:center;">得分</th><th style="border:1px solid #e4e7ed;padding:8px;text-align:center;">满分</th><th style="border:1px solid #e4e7ed;padding:8px;text-align:center;">结果</th></tr>';
+      scores.value.forEach((s: any) => {
+        const passColor = s.pass ? '#67c23a' : '#f56c6c';
+        const passText = s.pass ? '通过' : '未通过';
+        scoreHtml += `<tr><td style="border:1px solid #e4e7ed;padding:8px;">${s.abilityName}</td><td style="border:1px solid #e4e7ed;padding:8px;text-align:center;">${s.score}</td><td style="border:1px solid #e4e7ed;padding:8px;text-align:center;">${s.totalScore}</td><td style="border:1px solid #e4e7ed;padding:8px;text-align:center;color:${passColor};font-weight:600;">${passText}</td></tr>`;
+      });
+      scoreHtml += '</table>';
+      scoreSection.innerHTML = scoreHtml;
+      container.appendChild(scoreSection);
+    }
+
+    // 题目详情
+    if (questionList.value.length > 0) {
+      const qSection = document.createElement('div');
+      qSection.style.cssText = 'padding:16px;border:1px solid #e4e7ed;border-radius:6px;';
+      let qHtml = '<div style="font-size:16px;font-weight:600;margin-bottom:10px;">答题详情</div>';
+      questionList.value.forEach((item: any, index: number) => {
+        const resultColor = item.correct ? '#67c23a' : '#f56c6c';
+        const resultText = item.correct ? '正确' : '错误';
+        qHtml += `<div style="margin-bottom:16px;padding-bottom:12px;border-bottom:1px dashed #ebeef5;">`;
+        qHtml += `<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">`;
+        qHtml += `<span>第${index + 1}题 <span style="background:#f0f2f5;color:#909399;padding:2px 6px;border-radius:3px;font-size:12px;">${item.typeName}</span></span>`;
+        qHtml += `<span><span style="color:${resultColor};font-weight:600;">${resultText}</span> | 得分:${item.score}</span>`;
+        qHtml += `</div>`;
+        // 题目内容(去除 HTML 标签)
+        const plainTitle = item.questionTitle?.replace(/<[^>]*>/g, '') || '未命名题目';
+        qHtml += `<div style="margin-bottom:8px;">${plainTitle}</div>`;
+        // 选项
+        if (item.options && item.options.length > 0) {
+          item.options.forEach((opt: any) => {
+            const isCorrect = opt.correct;
+            const isUserAnswer = item.answer && item.answer.includes(opt.label);
+            let optStyle = 'padding:2px 0;';
+            if (isCorrect) optStyle += 'color:#67c23a;font-weight:600;';
+            if (isUserAnswer && !isCorrect) optStyle += 'color:#f56c6c;';
+            const tags = [];
+            if (isCorrect) tags.push('【正确答案】');
+            if (isUserAnswer && !isCorrect) tags.push('【考生选择】');
+            const plainText = opt.text?.replace(/<[^>]*>/g, '') || '';
+            qHtml += `<div style="${optStyle}">${opt.label}. ${plainText} ${tags.join('')}</div>`;
+          });
+        }
+        // 考生作答(问答题等)
+        if (item.answer && (!item.options || item.options.length === 0)) {
+          const plainAnswer = item.answer?.replace(/<[^>]*>/g, '') || '';
+          qHtml += `<div style="margin-top:6px;padding:6px 10px;background:#f5f7fa;border-radius:4px;font-size:13px;">考生作答:${plainAnswer}</div>`;
+        }
+        // 参考答案
+        if (item.testAnsRight) {
+          const plainRight = item.testAnsRight?.replace(/<[^>]*>/g, '') || '';
+          qHtml += `<div style="margin-top:6px;padding:6px 10px;background:#f0f9eb;border-radius:4px;font-size:13px;color:#67c23a;">参考答案:${plainRight}</div>`;
+        }
+        // 解析
+        if (item.analysis) {
+          const plainAnalysis = item.analysis?.replace(/<[^>]*>/g, '') || '';
+          qHtml += `<div style="margin-top:6px;font-size:13px;color:#606266;">答案解析:${plainAnalysis}</div>`;
+        }
+        qHtml += `</div>`;
+      });
+      qSection.innerHTML = qHtml;
+      container.appendChild(qSection);
+    }
+
+    document.body.appendChild(container);
+
+    // 等待渲染
+    await nextTick();
+    await new Promise(resolve => setTimeout(resolve, 300));
+
+    // 使用 html2canvas 截图
+    const canvas = await html2canvas(container, {
+      scale: 2,
+      useCORS: true,
+      backgroundColor: '#ffffff'
+    });
+
+    document.body.removeChild(container);
+
+    // 生成 PDF
+    const imgData = canvas.toDataURL('image/jpeg', 0.95);
+    const imgWidth = 210; // A4 宽度 mm
+    const pageHeight = 297; // A4 高度 mm
+    const imgHeight = (canvas.height * imgWidth) / canvas.width;
+    const pdf = new jsPDF('p', 'mm', 'a4');
+    let heightLeft = imgHeight;
+    let position = 0;
+
+    pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight);
+    heightLeft -= pageHeight;
+
+    while (heightLeft > 0) {
+      position = heightLeft - imgHeight;
+      pdf.addPage();
+      pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight);
+      heightLeft -= pageHeight;
+    }
+
+    const fileName = `测评报告_${studentName.value || '考生'}_${positionName.value || '未知岗位'}.pdf`;
+    pdf.save(fileName);
+  } catch (e) {
+    console.error('导出测评失败:', e);
+    proxy?.$modal?.msgError('导出测评失败');
+  } finally {
+    exportLoading.value = false;
+  }
+};
+
 const handleBack = () => {
   proxy?.$tab.closePage();
   router.back();