evaluation-view.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673
  1. <template>
  2. <PageShell>
  3. <div class="evaluation-view-page" v-loading="loading">
  4. <div class="left-card">
  5. <div class="card-title">考生信息</div>
  6. <div class="info-list">
  7. <div class="info-item"><span>姓名:</span><span>{{ studentName }}</span></div>
  8. <div class="info-item"><span>报名岗位:</span><span>{{ positionName }}</span></div>
  9. <div class="info-item"><span>开始时间:</span><span>{{ startTime || '-' }}</span></div>
  10. <div class="info-item"><span>结束时间:</span><span>{{ commitTime || '-' }}</span></div>
  11. <div class="info-item"><span>答题时长:</span><span>{{ ansTime || '-' }}</span></div>
  12. </div>
  13. <div class="card-title second">答题信息</div>
  14. <div class="answer-list">
  15. <div v-for="ab in scores" :key="ab.abilityName" class="answer-item">
  16. <span>{{ ab.abilityName }}:{{ ab.score }}/{{ ab.totalScore }}</span>
  17. <el-tag :type="ab.pass ? 'success' : 'danger'">{{ ab.pass ? '通过' : '未通过' }}</el-tag>
  18. </div>
  19. <div v-if="!scores.length" class="answer-item" style="color:#999">暂无答题记录</div>
  20. </div>
  21. <div class="card-title second">维度分析</div>
  22. <div v-if="scores.length >= 3" ref="radarChartRef" class="radar-chart"></div>
  23. <div v-else class="chart-placeholder">维度数据不足,至少需要3项能力</div>
  24. <div class="footer-btn">
  25. <el-button @click="handleBack">返回</el-button>
  26. <el-button type="primary" :loading="exportLoading" @click="handleExport">导出测评</el-button>
  27. </div>
  28. </div>
  29. <div class="right-card">
  30. <template v-if="questionList.length > 0">
  31. <div v-for="(item, index) in questionList" :key="item.id" class="question-block">
  32. <div class="question-head">
  33. <span>第{{ index + 1 }}题 <el-tag size="small" type="info">{{ item.typeName }}</el-tag></span>
  34. <div class="question-result">
  35. <el-tag :type="item.correct ? 'success' : 'danger'">{{ item.correct ? '正确' : '错误' }}</el-tag>
  36. <span>得分</span>
  37. <el-input :model-value="String(item.score)" class="score-input" readonly />
  38. </div>
  39. </div>
  40. <div class="question-title" v-html="item.questionTitle"></div>
  41. <!-- 选择题:显示选项,标记用户选择和正确答案 -->
  42. <div v-if="item.options && item.options.length > 0" class="question-options">
  43. <div v-for="option in item.options" :key="option.label" class="question-option" :class="{ 'is-user-answer': item.answer && item.answer.includes(option.label) }">
  44. <!-- 单选题 -->
  45. <el-radio v-if="item.typeName === '单选题'" :model-value="item.answer" :label="option.label" disabled>{{ option.label }}.<span v-html="option.text"></span></el-radio>
  46. <!-- 多选题 -->
  47. <el-checkbox v-else :model-value="item.answer ? item.answer.includes(option.label) : false" :label="option.label" disabled>{{ option.label }}.<span v-html="option.text"></span></el-checkbox>
  48. <el-tag v-if="option.correct" size="small" type="success">正确答案</el-tag>
  49. <el-tag v-if="item.answer && item.answer.includes(option.label) && !option.correct" size="small" type="danger">考生选择</el-tag>
  50. </div>
  51. </div>
  52. <!-- 问答题:显示考生作答 -->
  53. <div v-if="item.answer && (!item.options || item.options.length === 0)" class="question-user-answer">
  54. <span>考生作答:</span><span v-html="item.answer"></span>
  55. </div>
  56. <!-- 正确答案(问答题) -->
  57. <div v-if="item.typeName === '问答题' && item.testAnsRight" class="question-right-answer">
  58. <span>参考答案:</span><span v-html="item.testAnsRight"></span>
  59. </div>
  60. <div v-if="item.analysis" class="question-analysis">
  61. <div>答案解析:</div>
  62. <div v-html="item.analysis"></div>
  63. </div>
  64. </div>
  65. </template>
  66. <el-empty v-else description="暂无答题详情数据" />
  67. </div>
  68. </div>
  69. </PageShell>
  70. </template>
  71. <script setup name="EvaluationView" lang="ts">
  72. import { getCurrentInstance, type ComponentInternalInstance, ref, onMounted, nextTick, watch } from 'vue';
  73. import { useRoute, useRouter } from 'vue-router';
  74. import * as echarts from 'echarts';
  75. import html2canvas from 'html2canvas';
  76. import jsPDF from 'jspdf';
  77. import PageShell from '@/components/PageShell/index.vue';
  78. import { getEvaluation, getExamScoreList, getExamPaperQuestions, getExamAnswerList } from '@/api/main/evaluation/index';
  79. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  80. const router = useRouter();
  81. const route = useRoute();
  82. const loading = ref(false);
  83. const evaluationId = route.query.evaluationId as string;
  84. const studentId = route.query.studentId as string;
  85. const studentName = ref((route.query.name as string) || '');
  86. const positionName = ref('');
  87. const startTime = ref('');
  88. const commitTime = ref('');
  89. const ansTime = ref('');
  90. const scores = ref<any[]>([]);
  91. const questionList = ref<any[]>([]);
  92. const radarChartRef = ref<HTMLElement | null>(null);
  93. const exportLoading = ref(false);
  94. let radarChart: echarts.ECharts | null = null;
  95. /** 解析接口返回的 msg 字段(JSON 字符串),提取 bizContent */
  96. const parseBizContent = (res: any) => {
  97. const msgStr = res?.msg;
  98. if (typeof msgStr !== 'string') return {};
  99. try {
  100. const parsed = JSON.parse(msgStr);
  101. return parsed?.bizContent || parsed || {};
  102. } catch (e) {
  103. return {};
  104. }
  105. };
  106. /** 从 score-list 的 bizContent.rows 中查找指定学生的最新记录 */
  107. const findStudentInScoreList = (bizContent: any, sid: string) => {
  108. const rows: any[] = bizContent?.rows || [];
  109. if (!Array.isArray(rows) || !sid) return null;
  110. // 同一考生可能有多条记录,取最后一条(最新的)
  111. let result: any = null;
  112. for (const item of rows) {
  113. if (String(item.userId) === String(sid)) {
  114. result = item;
  115. }
  116. }
  117. return result;
  118. };
  119. /** 将 com_ans 中的 key1~key4 转为 A/B/C/D 标签 */
  120. const convertComAnsToLabel = (comAns: string): string => {
  121. if (!comAns) return '';
  122. const keyMap: Record<string, string> = { key1: 'A', key2: 'B', key3: 'C', key4: 'D' };
  123. // com_ans 格式可能是 "key2," 或 "key1,key3," 等
  124. const keys = comAns.split(',').map((k: string) => k.trim()).filter(Boolean);
  125. return keys.map((k: string) => keyMap[k] || k).join('');
  126. };
  127. const TYPE_NAME_MAP: Record<number, string> = {
  128. 1: '单选题',
  129. 2: '多选题',
  130. 3: '判断题',
  131. 5: '问答题'
  132. };
  133. /** 从 paper-questions 的 bizContent.rows 中提取题目列表
  134. * 注意:paper-questions 的 testId 是 "--",真实的 test_id 在 answer-list 中
  135. * 所以这里需要用 answer-list 中的信息来确定题目 ID
  136. */
  137. const extractQuestions = (bizContent: any, ansMap: Map<string, any>) => {
  138. const rows: any[] = bizContent?.rows || [];
  139. if (!Array.isArray(rows)) return [];
  140. // 建立 question 文本 -> answer-map 中 test_id 的映射
  141. const questionToTestId = new Map<string, string>();
  142. ansMap.forEach((detail: any, testId: string) => {
  143. const q = detail.question || '';
  144. if (q) {
  145. questionToTestId.set(q, testId);
  146. }
  147. });
  148. const questions: any[] = [];
  149. let seq = 0;
  150. rows.forEach((row: any) => {
  151. const tc = row.testContent || {};
  152. const qType = Number(tc.type);
  153. const questionTitle = tc.question || '';
  154. const analysis = tc.analysis || '';
  155. // 优先用 answer-list 中的 test_id 匹配
  156. let questionId = questionToTestId.get(questionTitle);
  157. if (!questionId) {
  158. questionId = tc.test_id || row.testId || `q_${seq}`;
  159. }
  160. // 构建选项:问答题无选项,单选/多选题从 answer1~answer4 + key1~key4 构建
  161. const options: { label: string; text: string; correct: boolean }[] = [];
  162. if (qType === 1 || qType === 2) {
  163. const answerFields = ['answer1', 'answer2', 'answer3', 'answer4'];
  164. const keyFields = ['key1', 'key2', 'key3', 'key4'];
  165. const labels = ['A', 'B', 'C', 'D'];
  166. for (let i = 0; i < 4; i++) {
  167. const text = tc[answerFields[i]];
  168. if (text) {
  169. options.push({
  170. label: labels[i],
  171. text,
  172. correct: tc[keyFields[i]] === '1'
  173. });
  174. }
  175. }
  176. }
  177. questions.push({
  178. id: questionId,
  179. questionTitle,
  180. analysis,
  181. options,
  182. type: qType,
  183. typeName: TYPE_NAME_MAP[qType] || '其他',
  184. testName: row.testName || ''
  185. });
  186. seq++;
  187. });
  188. return questions;
  189. };
  190. /** 从 answer-list 的 bizContent.rows 中查找指定学生,并提取其作答信息 */
  191. const extractAnswers = (bizContent: any, sid: string) => {
  192. const rows: any[] = bizContent?.rows || [];
  193. if (!Array.isArray(rows) || !sid) return new Map<string, any>();
  194. const studentRow = rows.find((item: any) => String(item.userId) === String(sid));
  195. if (!studentRow?.ansAndScore) return new Map<string, any>();
  196. const ansMap = new Map<string, any>();
  197. const ansAndScore: any[] = studentRow.ansAndScore;
  198. for (const group of ansAndScore) {
  199. if (!Array.isArray(group)) continue;
  200. for (let i = 1; i < group.length; i++) {
  201. const entry = group[i];
  202. if (Array.isArray(entry) && entry.length >= 2) {
  203. const testId = String(entry[0]);
  204. const detail = entry[1] || {};
  205. ansMap.set(testId, detail);
  206. }
  207. }
  208. }
  209. return ansMap;
  210. };
  211. /** 将题目和作答合并为展示数据 */
  212. const combineQA = (questions: any[], ansMap: Map<string, any>) => {
  213. return questions.map((q, index) => {
  214. const ans = ansMap.get(String(q.id));
  215. let correct = false;
  216. if (ans) {
  217. if (ans.is_ok === 'right') correct = true;
  218. }
  219. // 用户答案
  220. let answer = '';
  221. if (ans) {
  222. const comAns = ans.com_ans || '';
  223. if (comAns && (q.type === 1 || q.type === 2)) {
  224. // 选择题:将 key1~key4 转为 A~D
  225. answer = convertComAnsToLabel(comAns);
  226. } else if (comAns) {
  227. // 问答题:直接使用 HTML 内容
  228. answer = comAns;
  229. }
  230. }
  231. // 正确答案文本(问答题用)
  232. const testAnsRight = ans?.test_ans_right || '';
  233. const options = q.options.map((o: any) => ({ ...o }));
  234. return {
  235. id: q.id || `q_${index}`,
  236. questionTitle: q.questionTitle || '未命名题目',
  237. correct,
  238. score: ans?.score ?? 0,
  239. answer,
  240. options: options.length > 0 ? options : q.type === 5 ? [] : [{ label: 'A', text: '选项A' }, { label: 'B', text: '选项B' }],
  241. analysis: q.analysis || '',
  242. typeName: q.typeName,
  243. testName: q.testName,
  244. testAnsRight
  245. };
  246. });
  247. };
  248. /** 渲染雷达图 */
  249. const renderRadarChart = () => {
  250. if (!radarChartRef.value || scores.value.length < 3) return;
  251. if (!radarChart) {
  252. radarChart = echarts.init(radarChartRef.value);
  253. }
  254. const indicator = scores.value.map((s: any) => ({
  255. name: s.abilityName,
  256. max: Number(s.totalScore) || 100
  257. }));
  258. const dataValues = scores.value.map((s: any) => Number(s.score) || 0);
  259. radarChart.setOption({
  260. tooltip: {
  261. trigger: 'item'
  262. },
  263. radar: {
  264. indicator,
  265. radius: '65%',
  266. name: {
  267. textStyle: {
  268. fontSize: 11
  269. }
  270. },
  271. splitArea: {
  272. areaStyle: {
  273. color: ['rgba(64,158,255,0.05)', 'rgba(64,158,255,0.1)', 'rgba(64,158,255,0.15)', 'rgba(64,158,255,0.2)']
  274. }
  275. }
  276. },
  277. series: [{
  278. type: 'radar',
  279. data: [{
  280. value: dataValues,
  281. name: '得分',
  282. areaStyle: {
  283. color: 'rgba(64,158,255,0.2)'
  284. },
  285. lineStyle: {
  286. color: '#409EFF'
  287. },
  288. itemStyle: {
  289. color: '#409EFF'
  290. }
  291. }]
  292. }]
  293. });
  294. };
  295. const loadData = async () => {
  296. if (!evaluationId) return;
  297. loading.value = true;
  298. try {
  299. const res = await getEvaluation(evaluationId);
  300. const evalData = res.data;
  301. positionName.value = evalData.positionName || evalData.position || '未知岗位';
  302. const abilityConfigs = evalData.abilityConfigs || [];
  303. let allQuestions: any[] = [];
  304. let scoreItems: any[] = [];
  305. for (const ability of abilityConfigs) {
  306. const examId = ability.thirdExamInfoId;
  307. if (!examId) continue;
  308. // 获取成绩列表
  309. const scoreRes = await getExamScoreList({ examInfoId: examId, page: 1 }).catch(() => ({ msg: '{}' }));
  310. const scoreBiz = parseBizContent(scoreRes);
  311. const myScoreObj = findStudentInScoreList(scoreBiz, studentId);
  312. const score = myScoreObj?.score || 0;
  313. const passMark = ability.thirdExamPassMark || ability.passingScore || 0;
  314. const pass = myScoreObj ? String(myScoreObj.isPass) === '1' : Number(score) >= Number(passMark);
  315. // 取考生时间信息(只需取第一次有数据的记录)
  316. if (myScoreObj && !startTime.value) {
  317. startTime.value = myScoreObj.startTime || '';
  318. commitTime.value = myScoreObj.commitTime || '';
  319. ansTime.value = myScoreObj.ansTime || '';
  320. }
  321. scoreItems.push({
  322. abilityName: ability.abilityName || ability.thirdExamName || '考核',
  323. score: score,
  324. totalScore: ability.thirdExamTotalScore || ability.score || 100,
  325. pass
  326. });
  327. // 获取答题详情(先获取,因为需要用 test_id 来匹配 paper-questions)
  328. const answerRes = await getExamAnswerList({ examInfoId: examId }).catch(() => ({ msg: '{}' }));
  329. const answerBiz = parseBizContent(answerRes);
  330. const ansMap = extractAnswers(answerBiz, studentId);
  331. // 获取试卷题目
  332. const paperRes = await getExamPaperQuestions({ examInfoId: examId }).catch(() => ({ msg: '{}' }));
  333. const paperBiz = parseBizContent(paperRes);
  334. const questions = extractQuestions(paperBiz, ansMap);
  335. allQuestions.push(...combineQA(questions, ansMap));
  336. }
  337. scores.value = scoreItems;
  338. questionList.value = allQuestions;
  339. // 渲染雷达图
  340. await nextTick();
  341. renderRadarChart();
  342. } catch (e) {
  343. console.error(e);
  344. proxy?.$modal?.msgError('获取测评数据失败');
  345. } finally {
  346. loading.value = false;
  347. }
  348. };
  349. const handleExport = async () => {
  350. if (exportLoading.value) return;
  351. exportLoading.value = true;
  352. try {
  353. const container = document.createElement('div');
  354. 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;';
  355. const title = document.createElement('h2');
  356. title.style.cssText = 'text-align:center;margin-bottom:24px;font-size:20px;';
  357. title.textContent = `测评报告 - ${positionName.value || '未知岗位'}`;
  358. container.appendChild(title);
  359. const infoSection = document.createElement('div');
  360. infoSection.style.cssText = 'margin-bottom:20px;padding:16px;border:1px solid #e4e7ed;border-radius:6px;';
  361. infoSection.innerHTML = `
  362. <div style="font-size:16px;font-weight:600;margin-bottom:10px;">考生信息</div>
  363. <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;font-size:13px;">
  364. <div><strong>姓名:</strong>${studentName.value || '-'}</div>
  365. <div><strong>报名岗位:</strong>${positionName.value || '未知岗位'}</div>
  366. <div><strong>开始时间:</strong>${startTime.value || '-'}</div>
  367. <div><strong>结束时间:</strong>${commitTime.value || '-'}</div>
  368. <div><strong>答题时长:</strong>${ansTime.value || '-'}</div>
  369. </div>
  370. `;
  371. container.appendChild(infoSection);
  372. if (scores.value.length > 0) {
  373. const scoreSection = document.createElement('div');
  374. scoreSection.style.cssText = 'margin-bottom:20px;padding:16px;border:1px solid #e4e7ed;border-radius:6px;';
  375. let scoreHtml = '<div style="font-size:16px;font-weight:600;margin-bottom:10px;">答题信息</div>';
  376. scoreHtml += '<table style="width:100%;border-collapse:collapse;font-size:13px;">';
  377. 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>';
  378. scores.value.forEach((s: any) => {
  379. const passColor = s.pass ? '#67c23a' : '#f56c6c';
  380. const passText = s.pass ? '通过' : '未通过';
  381. 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>`;
  382. });
  383. scoreHtml += '</table>';
  384. scoreSection.innerHTML = scoreHtml;
  385. container.appendChild(scoreSection);
  386. }
  387. if (questionList.value.length > 0) {
  388. const qSection = document.createElement('div');
  389. qSection.style.cssText = 'padding:16px;border:1px solid #e4e7ed;border-radius:6px;';
  390. let qHtml = '<div style="font-size:16px;font-weight:600;margin-bottom:10px;">答题详情</div>';
  391. questionList.value.forEach((item: any, index: number) => {
  392. const resultColor = item.correct ? '#67c23a' : '#f56c6c';
  393. const resultText = item.correct ? '正确' : '错误';
  394. qHtml += `<div style="margin-bottom:16px;padding-bottom:12px;border-bottom:1px dashed #ebeef5;">`;
  395. qHtml += `<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">`;
  396. qHtml += `<span>第${index + 1}题 <span style="background:#f0f2f5;color:#909399;padding:2px 6px;border-radius:3px;font-size:12px;">${item.typeName}</span></span>`;
  397. qHtml += `<span><span style="color:${resultColor};font-weight:600;">${resultText}</span> | 得分:${item.score}</span>`;
  398. qHtml += `</div>`;
  399. const plainTitle = item.questionTitle?.replace(/<[^>]*>/g, '') || '未命名题目';
  400. qHtml += `<div style="margin-bottom:8px;">${plainTitle}</div>`;
  401. if (item.options && item.options.length > 0) {
  402. item.options.forEach((opt: any) => {
  403. const isCorrect = opt.correct;
  404. const isUserAnswer = item.answer && item.answer.includes(opt.label);
  405. let optStyle = 'padding:2px 0;';
  406. if (isCorrect) optStyle += 'color:#67c23a;font-weight:600;';
  407. if (isUserAnswer && !isCorrect) optStyle += 'color:#f56c6c;';
  408. const tags = [];
  409. if (isCorrect) tags.push('【正确答案】');
  410. if (isUserAnswer && !isCorrect) tags.push('【考生选择】');
  411. const plainText = opt.text?.replace(/<[^>]*>/g, '') || '';
  412. qHtml += `<div style="${optStyle}">${opt.label}. ${plainText} ${tags.join('')}</div>`;
  413. });
  414. }
  415. if (item.answer && (!item.options || item.options.length === 0)) {
  416. const plainAnswer = item.answer?.replace(/<[^>]*>/g, '') || '';
  417. qHtml += `<div style="margin-top:6px;padding:6px 10px;background:#f5f7fa;border-radius:4px;font-size:13px;">考生作答:${plainAnswer}</div>`;
  418. }
  419. if (item.testAnsRight) {
  420. const plainRight = item.testAnsRight?.replace(/<[^>]*>/g, '') || '';
  421. qHtml += `<div style="margin-top:6px;padding:6px 10px;background:#f0f9eb;border-radius:4px;font-size:13px;color:#67c23a;">参考答案:${plainRight}</div>`;
  422. }
  423. if (item.analysis) {
  424. const plainAnalysis = item.analysis?.replace(/<[^>]*>/g, '') || '';
  425. qHtml += `<div style="margin-top:6px;font-size:13px;color:#606266;">答案解析:${plainAnalysis}</div>`;
  426. }
  427. qHtml += `</div>`;
  428. });
  429. qSection.innerHTML = qHtml;
  430. container.appendChild(qSection);
  431. }
  432. document.body.appendChild(container);
  433. await nextTick();
  434. await new Promise(resolve => setTimeout(resolve, 300));
  435. const canvas = await html2canvas(container, { scale: 2, useCORS: true, backgroundColor: '#ffffff' });
  436. document.body.removeChild(container);
  437. const imgData = canvas.toDataURL('image/jpeg', 0.95);
  438. const imgWidth = 210;
  439. const pageHeight = 297;
  440. const imgHeight = (canvas.height * imgWidth) / canvas.width;
  441. const pdf = new jsPDF('p', 'mm', 'a4');
  442. let heightLeft = imgHeight;
  443. let position = 0;
  444. pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight);
  445. heightLeft -= pageHeight;
  446. while (heightLeft > 0) {
  447. position = heightLeft - imgHeight;
  448. pdf.addPage();
  449. pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight);
  450. heightLeft -= pageHeight;
  451. }
  452. const fileName = `测评报告_${studentName.value || '考生'}_${positionName.value || '未知岗位'}.pdf`;
  453. pdf.save(fileName);
  454. } catch (e) {
  455. console.error('导出测评失败:', e);
  456. proxy?.$modal?.msgError('导出测评失败');
  457. } finally {
  458. exportLoading.value = false;
  459. }
  460. };
  461. const handleBack = () => {
  462. proxy?.$tab.closePage();
  463. router.back();
  464. };
  465. onMounted(() => {
  466. loadData();
  467. });
  468. </script>
  469. <style scoped>
  470. .evaluation-view-page {
  471. display: grid;
  472. grid-template-columns: 280px 1fr;
  473. gap: 16px;
  474. }
  475. .left-card {
  476. background: #fff;
  477. border-radius: 6px;
  478. padding: 16px;
  479. height: calc(100vh - 180px);
  480. overflow-y: auto;
  481. }
  482. .right-card {
  483. background: #fff;
  484. border-radius: 6px;
  485. padding: 16px;
  486. height: calc(100vh - 180px);
  487. overflow-y: auto;
  488. }
  489. .card-title {
  490. font-size: 16px;
  491. font-weight: 600;
  492. margin-bottom: 12px;
  493. }
  494. .card-title.second {
  495. margin-top: 20px;
  496. }
  497. .info-list,
  498. .answer-list {
  499. display: flex;
  500. flex-direction: column;
  501. gap: 8px;
  502. font-size: 12px;
  503. color: #606266;
  504. }
  505. .info-item,
  506. .answer-item {
  507. display: flex;
  508. justify-content: space-between;
  509. gap: 8px;
  510. }
  511. .chart-placeholder {
  512. height: 180px;
  513. border: 1px dashed #dcdfe6;
  514. border-radius: 8px;
  515. display: flex;
  516. align-items: center;
  517. justify-content: center;
  518. color: #909399;
  519. font-size: 12px;
  520. }
  521. .radar-chart {
  522. height: 220px;
  523. margin-top: 8px;
  524. }
  525. .footer-btn {
  526. margin-top: 16px;
  527. display: flex;
  528. justify-content: flex-end;
  529. gap: 12px;
  530. }
  531. .question-block {
  532. padding-bottom: 18px;
  533. margin-bottom: 18px;
  534. border-bottom: 1px dashed #ebeef5;
  535. }
  536. .question-head {
  537. display: flex;
  538. justify-content: space-between;
  539. align-items: center;
  540. margin-bottom: 16px;
  541. }
  542. .question-result {
  543. display: flex;
  544. align-items: center;
  545. gap: 8px;
  546. }
  547. .score-input {
  548. width: 56px;
  549. }
  550. .question-title {
  551. margin-bottom: 12px;
  552. }
  553. .question-options {
  554. display: flex;
  555. flex-direction: column;
  556. gap: 10px;
  557. }
  558. .question-option {
  559. display: flex;
  560. align-items: center;
  561. gap: 8px;
  562. padding: 4px 8px;
  563. border-radius: 4px;
  564. }
  565. .question-option.is-user-answer {
  566. background: #ecf5ff;
  567. }
  568. .correct-text {
  569. color: #67c23a;
  570. font-size: 12px;
  571. }
  572. .question-analysis {
  573. margin-top: 16px;
  574. color: #606266;
  575. font-size: 13px;
  576. }
  577. .question-user-answer {
  578. margin-top: 8px;
  579. padding: 8px 12px;
  580. background: #f5f7fa;
  581. border-radius: 4px;
  582. color: #606266;
  583. font-size: 13px;
  584. }
  585. .question-right-answer {
  586. margin-top: 8px;
  587. padding: 8px 12px;
  588. background: #f0f9eb;
  589. border-radius: 4px;
  590. color: #67c23a;
  591. font-size: 13px;
  592. }
  593. </style>