|
|
@@ -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();
|