viewAnswer.vue 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147
  1. <template>
  2. <!-- 查看作答页。路由固定为 /teachingHomework/viewAnswer,不使用地址栏参数 -->
  3. <div class="homework_view_answer" v-if="homework">
  4. <!-- 顶部:返回、科目、任务信息 -->
  5. <div class="answer_header">
  6. <el-button class="back_btn" @click="goBack">
  7. <i class="iconfont icon_return"></i>返回
  8. </el-button>
  9. <div class="header_main">
  10. <div class="title_row">
  11. <span class="subject_badge" :style="{ background: subjectColor }">{{ homework.subjectName }}</span>
  12. <span class="task_title">{{ taskTitle }}</span>
  13. </div>
  14. <div class="meta_row">
  15. <span>布置教师:{{ homework.teacherName }}</span>
  16. <span>试题数量:{{ questions.length }}</span>
  17. </div>
  18. </div>
  19. </div>
  20. <!-- 主体:左侧试题序号,右侧题干/作答/解析 -->
  21. <div class="answer_body" v-loading="loading">
  22. <div class="question_nav">
  23. <div class="nav_title">试题列表</div>
  24. <div class="nav_group" v-for="group in questionGroups" :key="group.type">
  25. <div class="group_name">{{ group.name }}</div>
  26. <div class="num_grid">
  27. <button
  28. v-for="item in group.list"
  29. :key="item.id"
  30. type="button"
  31. class="num_btn"
  32. :class="numClass(item)"
  33. @click="selectQuestion(item.index)">
  34. {{ item.index + 1 }}
  35. </button>
  36. </div>
  37. </div>
  38. </div>
  39. <div class="question_panel" v-if="currentQuestion">
  40. <!-- 锚点 tabs:点击滚动到对应区块 -->
  41. <div class="section_tabs">
  42. <span
  43. v-for="tab in sectionTabs"
  44. :key="tab.key"
  45. class="section_tab"
  46. :class="{ active: activeSection === tab.key }"
  47. @click="scrollToSection(tab.key)">
  48. {{ tab.label }}
  49. </span>
  50. </div>
  51. <!-- 右侧内容区固定高度,内部滚动 -->
  52. <div class="question_scroll" ref="scrollBox" @scroll="onScroll">
  53. <!-- 题干 -->
  54. <div class="content_section" ref="sectionStem" data-section="stem">
  55. <div class="q_tags">
  56. <span class="q_no">{{ currentQuestion.index + 1 }}</span>
  57. <span class="q_type">{{ currentQuestion.typeName }}</span>
  58. <span class="q_level">{{ currentQuestion.difficulty }}</span>
  59. </div>
  60. <div class="q_stem" v-html="currentQuestion.stem"></div>
  61. <div v-if="currentQuestion.type === 'single' && hasOptionText" class="q_options">
  62. <div class="q_option" v-for="opt in currentQuestion.options" :key="opt.key">
  63. <span class="opt_key">{{ opt.key }}.</span>
  64. <span>{{ opt.text }}</span>
  65. </div>
  66. </div>
  67. </div>
  68. <!-- 我的作答(只读) -->
  69. <div class="content_section" ref="sectionAnswer" data-section="answer">
  70. <div class="block_box my_answer_box">
  71. <div class="dock_answer">
  72. <span class="dock_label">我的作答:</span>
  73. <span v-if="isSingle" class="dock_sub_no">(1)</span>
  74. <el-radio-group
  75. v-if="isJudge"
  76. class="choice_radio"
  77. :value="currentJudgeAnswer"
  78. disabled>
  79. <el-radio label="T">对</el-radio>
  80. <el-radio label="F">错</el-radio>
  81. </el-radio-group>
  82. <el-checkbox-group
  83. v-else-if="isSingle"
  84. class="choice_radio"
  85. :value="currentUserAnswer"
  86. disabled>
  87. <el-checkbox
  88. v-for="opt in currentQuestion.options"
  89. :key="opt.key"
  90. :label="opt.key">
  91. {{ opt.key }}
  92. </el-checkbox>
  93. </el-checkbox-group>
  94. <div v-else class="photo_list">
  95. <div
  96. class="photo_item"
  97. v-for="(img, imgIndex) in currentPhotos"
  98. :key="imgIndex">
  99. <img :src="img" alt="" @click="previewPhoto(imgIndex)">
  100. </div>
  101. <span v-if="!currentPhotos.length" class="empty_answer">暂无作答图片</span>
  102. </div>
  103. </div>
  104. </div>
  105. </div>
  106. <!-- 答案解析 -->
  107. <div class="content_section" ref="sectionAnalysis" data-section="analysis">
  108. <div class="block_box analysis_box">
  109. <div class="analysis_row">
  110. <span class="analysis_label">【知识点】</span>
  111. <div class="knowledge_tags">
  112. <span
  113. class="knowledge_tag"
  114. v-for="(tag, idx) in currentQuestion.knowledgePoints"
  115. :key="idx">
  116. {{ tag }}
  117. </span>
  118. </div>
  119. </div>
  120. <div class="analysis_row" v-if="hasAnalysis">
  121. <span class="analysis_label" >【 答 案 】</span>
  122. <div class="analysis_content">
  123. <span class="correct_answer" v-html="displayCorrectAnswer"></span>
  124. </div>
  125. </div>
  126. <div class="analysis_row" v-if="hasAnalysis" style="margin-top: 0;">
  127. <span class="analysis_label" >【 详 解 】</span>
  128. <div class="analysis_content">
  129. <div class="analysis_text" v-html="currentQuestion.analysis"></div>
  130. </div>
  131. </div>
  132. </div>
  133. </div>
  134. </div>
  135. <!-- 底部操作区固定 -->
  136. <div class="view_dock">
  137. <!-- <el-button class="ai_btn" @click="handleAiExplain">AI讲题</el-button> -->
  138. <div class="dock_actions">
  139. <el-button
  140. class="nav_btn"
  141. :disabled="currentQuestion.index === 0"
  142. @click="prevQuestion">
  143. 上一题
  144. </el-button>
  145. <el-button
  146. class="nav_btn"
  147. :disabled="isLastQuestion"
  148. @click="nextQuestion">
  149. 下一题
  150. </el-button>
  151. </div>
  152. </div>
  153. </div>
  154. <div v-else-if="!loading" class="empty_panel no_content_data">
  155. <span>暂无数据</span>
  156. </div>
  157. </div>
  158. <el-image-viewer
  159. v-if="imgVisible"
  160. :z-index="9999"
  161. :initial-index="previewIndex"
  162. :url-list="currentPhotos"
  163. :on-close="closePhotoPreview" />
  164. </div>
  165. </template>
  166. <script>
  167. import ElImageViewer from 'element-ui/packages/image/src/image-viewer';
  168. // 查看作答状态存 sessionStorage,刷新仍停留本页,地址栏不带参数
  169. const STORAGE_KEY = 'teachingHomeworkViewAnswer';
  170. const OPTION_KEYS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
  171. // 客观题(选择题/判断题)渲染为单选项,其余按主观题作答图片展示
  172. function isChoiceQuestion(item) {
  173. return Number(item.isObjective) === 1 || Number(item.isJudge) === 1;
  174. }
  175. function buildOptions(item) {
  176. const count = Number(item.optionsNum) || (Number(item.isJudge) === 1 ? 2 : 4);
  177. const options = [];
  178. for (let i = 0; i < count && i < OPTION_KEYS.length; i += 1) {
  179. options.push({ key: OPTION_KEYS.charAt(i), text: '' });
  180. }
  181. return options;
  182. }
  183. // 知识点可能是字符串数组,也可能是对象数组,统一收敛成字符串
  184. // 把接口答案归一化为合法选项 key 数组,兼容 "A,C"、"AC"、"A|C"、["A"] 等格式
  185. function normalizeChoiceAnswer(value, options) {
  186. const validKeys = (options || []).map(opt => opt.key);
  187. let keys = [];
  188. if (Array.isArray(value)) {
  189. keys = value;
  190. } else if (value !== null && value !== undefined && String(value).trim() !== '') {
  191. keys = String(value).toUpperCase().match(/[A-Z]/g) || [];
  192. }
  193. const result = [];
  194. keys.forEach(key => {
  195. const k = String(key);
  196. if (validKeys.indexOf(k) !== -1 && result.indexOf(k) === -1) result.push(k);
  197. });
  198. return result;
  199. }
  200. // 判断题回显:T 为对,F 为错
  201. function normalizeJudgeAnswer(value) {
  202. const text = String(value == null ? '' : value).trim();
  203. if (text === 'T' || text === 't' || text === '对' || text.toLowerCase() === 'true') return 'T';
  204. if (text === 'F' || text === 'f' || text === '错' || text.toLowerCase() === 'false') return 'F';
  205. return '';
  206. }
  207. function mapKnowledge(list) {
  208. if (!Array.isArray(list)) return [];
  209. return list.map(item => {
  210. if (item == null) return '';
  211. if (typeof item === 'string') return item;
  212. return item.knowledgeName || item.name || item.title || '';
  213. }).filter(Boolean);
  214. }
  215. /**
  216. * 接口试题数据映射为页面结构
  217. * type: single 客观题 / photo 主观题
  218. */
  219. function mapQuestion(item, index) {
  220. const judge = Number(item.isJudge) === 1;
  221. const choice = judge || isChoiceQuestion(item);
  222. const id = item.homeWorkStuQuesId || item.questionId || index;
  223. return {
  224. id,
  225. questionId: item.questionId,
  226. homeWorkStuQuesId: item.homeWorkStuQuesId,
  227. isJudge: judge,
  228. type: choice ? 'single' : 'photo',
  229. typeName: item.questionTypeName || (judge ? '判断题' : (choice ? '客观题' : '主观题')),
  230. groupName: item.questionTypeName || (judge ? '判断题' : (choice ? '客观题' : '主观题')),
  231. difficulty: item.difficultyName || '',
  232. stem: item.questionContent || '',
  233. options: judge ? [] : (choice ? buildOptions(item) : []),
  234. // 判断题取 T / F,客观题取 ABCD,主观题取作答图片地址
  235. userAnswer: choice
  236. ? (item.writeAnswer || '')
  237. : (Array.isArray(item.writeUrls) ? item.writeUrls : []),
  238. correctAnswer: item.questionAnswer || '',
  239. knowledgePoints: mapKnowledge(item.knowledge),
  240. analysis: item.questionAnalysis || '',
  241. submitEndTime: item.submitEndTime || '',
  242. };
  243. }
  244. export default {
  245. name: 'TeachingHomeworkViewAnswer',
  246. components: {
  247. ElImageViewer,
  248. },
  249. data() {
  250. return {
  251. homework: null,
  252. questions: [],
  253. loading: false,
  254. currentIndex: 0,
  255. activeSection: 'stem',
  256. sectionTabs: [
  257. { key: 'stem', label: '题干' },
  258. { key: 'answer', label: '我的作答' },
  259. { key: 'analysis', label: '答案解析' },
  260. ],
  261. imgVisible: false,
  262. previewIndex: 0,
  263. scrollLock: false,
  264. scrollTimer: null,
  265. // 点击 tab 滚动结束后的短暂宽限期,忽略残余 scroll 事件对高亮的覆盖
  266. syncSuppressUntil: 0,
  267. };
  268. },
  269. computed: {
  270. taskTitle() {
  271. if (!this.homework) return '';
  272. const name = this.homework.homeworkName || '';
  273. const className = this.homework.className || '';
  274. if (!className || name.indexOf(className) !== -1) return name;
  275. return `${name} ${className}`;
  276. },
  277. subjectColor() {
  278. const name = (this.homework && this.homework.subjectName) || '';
  279. return this.$global.getSubjectsName(name, null, 'color');
  280. },
  281. // 按接口返回的 questionTypeName 分组,有几种题型就显示几组
  282. questionGroups() {
  283. const groups = [];
  284. this.questions.forEach((item, index) => {
  285. const name = item.groupName || item.typeName || '其他';
  286. let group = groups.find(g => g.name === name);
  287. if (!group) {
  288. group = { type: name, name, list: [] };
  289. groups.push(group);
  290. }
  291. group.list.push({ ...item, index });
  292. });
  293. return groups;
  294. },
  295. currentQuestion() {
  296. const item = this.questions[this.currentIndex];
  297. if (!item) return null;
  298. return { ...item, index: this.currentIndex };
  299. },
  300. isSingle() {
  301. return !!(this.currentQuestion && this.currentQuestion.type === 'single');
  302. },
  303. // isJudge 为 1 时是判断题,详情只展示对 / 错
  304. isJudge() {
  305. return !!(this.currentQuestion && this.currentQuestion.isJudge);
  306. },
  307. hasOptionText() {
  308. const options = (this.currentQuestion && this.currentQuestion.options) || [];
  309. return options.some(opt => opt.text);
  310. },
  311. // 解析为 HTML,空串或 <p><br></p> 这类空标签都视为无内容;仅含公式图片也算有内容
  312. hasAnalysis() {
  313. const raw = String((this.currentQuestion && this.currentQuestion.analysis) || '');
  314. if (/<img[^>]+>/i.test(raw)) return true;
  315. const text = raw
  316. .replace(/<[^>]*>/g, '')
  317. .replace(/&nbsp;/g, '')
  318. .trim();
  319. return text !== '';
  320. },
  321. isLastQuestion() {
  322. return this.currentIndex >= this.questions.length - 1;
  323. },
  324. currentUserAnswer() {
  325. if (!this.currentQuestion || !this.isSingle || this.isJudge) return [];
  326. return normalizeChoiceAnswer(this.currentQuestion.userAnswer, this.currentQuestion.options);
  327. },
  328. // 判断题作答回显:T 选中「对」,F 选中「错」
  329. currentJudgeAnswer() {
  330. if (!this.currentQuestion || !this.isJudge) return '';
  331. return normalizeJudgeAnswer(this.currentQuestion.userAnswer);
  332. },
  333. // 判断题标准答案同样把 T / F 显示为对 / 错
  334. displayCorrectAnswer() {
  335. const raw = (this.currentQuestion && this.currentQuestion.correctAnswer) || '';
  336. if (!this.isJudge) return raw;
  337. const judge = normalizeJudgeAnswer(raw);
  338. if (judge === 'T') return '对';
  339. if (judge === 'F') return '错';
  340. return raw;
  341. },
  342. currentPhotos() {
  343. if (!this.currentQuestion || this.isSingle) return [];
  344. const val = this.currentQuestion.userAnswer;
  345. return Array.isArray(val) ? val : [];
  346. },
  347. },
  348. created() {
  349. this.restoreState();
  350. },
  351. beforeDestroy() {
  352. clearInterval(this.scrollTimer);
  353. this.persistState();
  354. },
  355. methods: {
  356. // 进入/刷新时恢复;没有作业记录则回列表
  357. restoreState() {
  358. let cache = null;
  359. try {
  360. cache = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || 'null');
  361. } catch (e) {
  362. cache = null;
  363. }
  364. if (!cache || !cache.homework) {
  365. this.$router.replace('/teachingHomework/list');
  366. return;
  367. }
  368. this.homework = cache.homework;
  369. this.currentIndex = Number(cache.currentIndex) || 0;
  370. this.activeSection = 'stem';
  371. this.fetchDetail();
  372. },
  373. fetchDetail() {
  374. const homework = this.homework || {};
  375. if (!homework.homeWorkStuId && !homework.homeWorkId) {
  376. this.questions = [];
  377. return;
  378. }
  379. this.loading = true;
  380. // 参数与作答页一致
  381. const params = {
  382. homeWorkStuId: homework.homeWorkStuId,
  383. homeWorkId: homework.homeWorkId,
  384. };
  385. this.$api.teachingHomework.viewHomeWorkWriteDetail(params).then(res => {
  386. this.applyDetail(res);
  387. }).catch(err => {
  388. const body = this.pickBody(err);
  389. if (body) {
  390. this.applyDetail(body);
  391. return;
  392. }
  393. this.questions = [];
  394. }).finally(() => {
  395. this.loading = false;
  396. });
  397. },
  398. // 公共请求把 code 为 0 的成功响应放进了 reject,这里两种都认
  399. pickBody(payload) {
  400. if (!payload) return null;
  401. if (payload.code === 0 || payload.code === 200) return payload;
  402. if (payload.data && (payload.data.code === 0 || payload.data.code === 200)) return payload.data;
  403. if (payload.response) return this.pickBody(payload.response);
  404. return null;
  405. },
  406. applyDetail(res) {
  407. const detail = this.pickDetail(res);
  408. this.mergeHomework(detail);
  409. this.questions = detail.questionDetails.map(mapQuestion);
  410. if (this.currentIndex < 0 || this.currentIndex >= this.questions.length) {
  411. this.currentIndex = 0;
  412. }
  413. this.activeSection = 'stem';
  414. this.persistState();
  415. this.$nextTick(() => {
  416. const box = this.$refs.scrollBox;
  417. if (box) box.scrollTop = 0;
  418. });
  419. },
  420. // 详情 data 为作业对象,题目在 questionDetails;兼容旧的题目数组
  421. pickDetail(res) {
  422. const data = res && res.data;
  423. if (Array.isArray(data)) return { questionDetails: data };
  424. if (data && typeof data === 'object') {
  425. return {
  426. ...data,
  427. questionDetails: Array.isArray(data.questionDetails) ? data.questionDetails : [],
  428. };
  429. }
  430. return { questionDetails: [] };
  431. },
  432. // 用详情里的作业信息补齐头部:名称、教师、科目
  433. mergeHomework(detail) {
  434. const current = this.homework || {};
  435. this.homework = {
  436. ...current,
  437. homeWorkId: detail.homeWorkId != null ? detail.homeWorkId : current.homeWorkId,
  438. homeWorkStuId: detail.homeWorkStuId != null ? detail.homeWorkStuId : current.homeWorkStuId,
  439. homeworkName: detail.homeWorkName || current.homeworkName || '',
  440. teacherName: detail.createUserName || current.teacherName || '',
  441. subjectName: detail.courseName || current.subjectName || '',
  442. courseCode: detail.courseCode != null ? detail.courseCode : current.courseCode,
  443. endTime: detail.submitEndTime || current.endTime || '',
  444. questionNum: detail.questionNumber != null ? detail.questionNumber : current.questionNum,
  445. };
  446. },
  447. persistState() {
  448. if (!this.homework) return;
  449. sessionStorage.setItem(STORAGE_KEY, JSON.stringify({
  450. homework: this.homework,
  451. currentIndex: this.currentIndex,
  452. }));
  453. },
  454. numClass(item) {
  455. return {
  456. current: item.index === this.currentIndex,
  457. };
  458. },
  459. selectQuestion(index) {
  460. clearInterval(this.scrollTimer);
  461. this.scrollTimer = null;
  462. this.scrollLock = false;
  463. this.currentIndex = index;
  464. this.activeSection = 'stem';
  465. this.persistState();
  466. this.$nextTick(() => {
  467. const box = this.$refs.scrollBox;
  468. if (box) box.scrollTop = 0;
  469. });
  470. },
  471. prevQuestion() {
  472. if (this.currentIndex <= 0) return;
  473. this.selectQuestion(this.currentIndex - 1);
  474. },
  475. nextQuestion() {
  476. if (this.currentIndex >= this.questions.length - 1) return;
  477. this.selectQuestion(this.currentIndex + 1);
  478. },
  479. getSections() {
  480. return [
  481. { key: 'stem', el: this.$refs.sectionStem },
  482. { key: 'answer', el: this.$refs.sectionAnswer },
  483. { key: 'analysis', el: this.$refs.sectionAnalysis },
  484. ];
  485. },
  486. // 点击 tab 滚动到对应区块
  487. scrollToSection(key) {
  488. const box = this.$refs.scrollBox;
  489. const target = this.getSections().find(item => item.key === key);
  490. const targetEl = target && target.el;
  491. if (!targetEl || !box) return;
  492. this.activeSection = key;
  493. this.scrollLock = true;
  494. // 用视口差值 + 当前滚动距离计算目标位置,offsetTop 受 offsetParent 影响会不准
  495. const distance = targetEl.getBoundingClientRect().top - box.getBoundingClientRect().top;
  496. // 下方区块高度不足时,目标顶部无法对齐容器顶部,最多只能滚到底部
  497. const maxScrollTop = box.scrollHeight - box.clientHeight;
  498. const targetTop = Math.max(0, Math.min(box.scrollTop + distance - 8, maxScrollTop));
  499. if (typeof box.scrollTo === 'function') {
  500. box.scrollTo({ top: targetTop, behavior: 'smooth' });
  501. } else {
  502. box.scrollTop = targetTop;
  503. }
  504. clearInterval(this.scrollTimer);
  505. // 平滑滚动期间锁定 onScroll 的高亮覆盖;连续两帧位置不变视为滚动结束
  506. let lastTop = box.scrollTop;
  507. let stableTicks = 0;
  508. this.scrollTimer = setInterval(() => {
  509. const nowTop = box.scrollTop;
  510. if (Math.abs(nowTop - lastTop) < 1) {
  511. stableTicks += 1;
  512. } else {
  513. stableTicks = 0;
  514. lastTop = nowTop;
  515. }
  516. if (stableTicks >= 2) {
  517. clearInterval(this.scrollTimer);
  518. this.scrollTimer = null;
  519. // 保持用户点击的 tab:下方高度不足停在底部时,按位置重算会误判到后一个区块
  520. this.activeSection = key;
  521. // 解锁后宽限 150ms,吞掉滚动停止瞬间的残余 scroll 事件
  522. this.syncSuppressUntil = Date.now() + 150;
  523. this.scrollLock = false;
  524. }
  525. }, 80);
  526. },
  527. // 滚动时同步高亮 tab
  528. onScroll() {
  529. if (this.scrollLock) return;
  530. if (this.syncSuppressUntil && Date.now() < this.syncSuppressUntil) return;
  531. this.syncActiveSection();
  532. },
  533. // 依据各区块相对滚动容器的位置计算当前高亮;滚到底部时高亮最后一个可见区块
  534. syncActiveSection() {
  535. const box = this.$refs.scrollBox;
  536. if (!box) return;
  537. const boxTop = box.getBoundingClientRect().top;
  538. const reachedBottom = box.scrollTop + box.clientHeight >= box.scrollHeight - 2;
  539. let current = 'stem';
  540. this.getSections().forEach(item => {
  541. if (!item.el) return;
  542. const rect = item.el.getBoundingClientRect();
  543. if (rect.top - boxTop <= 24) {
  544. current = item.key;
  545. }
  546. if (reachedBottom && rect.bottom > boxTop) {
  547. current = item.key;
  548. }
  549. });
  550. this.activeSection = current;
  551. },
  552. previewPhoto(index) {
  553. this.previewIndex = index;
  554. this.imgVisible = true;
  555. },
  556. closePhotoPreview() {
  557. this.imgVisible = false;
  558. },
  559. handleAiExplain() {
  560. this.$message.info('AI讲题待对接');
  561. },
  562. goBack() {
  563. this.persistState();
  564. this.$router.push('/teachingHomework/list');
  565. },
  566. },
  567. };
  568. </script>
  569. <style lang="scss" scoped>
  570. /* 布局、间距、字号、颜色与 answer.vue 保持一致 */
  571. .homework_view_answer {
  572. display: flex;
  573. flex-direction: column;
  574. width: 100%;
  575. height: 100%;
  576. min-width: 0;
  577. min-height: 0;
  578. box-sizing: border-box;
  579. overflow: hidden;
  580. .answer_header {
  581. flex-shrink: 0;
  582. display: flex;
  583. align-items: flex-start;
  584. gap: 16px;
  585. padding: 10px;
  586. background: #fff;
  587. border-radius: 10px;
  588. box-sizing: border-box;
  589. .back_btn {
  590. flex-shrink: 0;
  591. height: 36px;
  592. padding: 0 12px;
  593. background: #fff;
  594. border: 1px solid #DCDFE6;
  595. color: #666;
  596. :deep(span) {
  597. display: inline-flex;
  598. align-items: center;
  599. gap: 2px;
  600. }
  601. }
  602. .header_main {
  603. flex: 1;
  604. min-width: 0;
  605. .title_row {
  606. display: flex;
  607. align-items: center;
  608. gap: 10px;
  609. margin-bottom: 10px;
  610. margin-top: 6px;
  611. .subject_badge {
  612. flex-shrink: 0;
  613. display: inline-flex;
  614. align-items: center;
  615. justify-content: center;
  616. min-width: 40px;
  617. height: 25px;
  618. padding: 0 4px;
  619. border-radius: 4px;
  620. color: #fff;
  621. font-size: 12px;
  622. line-height: 1;
  623. }
  624. .task_title {
  625. font-size: 16px;
  626. font-weight: 550;
  627. color: #333;
  628. overflow: hidden;
  629. text-overflow: ellipsis;
  630. white-space: nowrap;
  631. }
  632. }
  633. .meta_row {
  634. display: flex;
  635. flex-wrap: wrap;
  636. gap: 16px;
  637. font-size: 14px;
  638. color: #999;
  639. }
  640. }
  641. }
  642. .answer_body {
  643. flex: 1;
  644. min-height: 0;
  645. display: flex;
  646. gap: 10px;
  647. margin-top: 10px;
  648. overflow: hidden;
  649. .empty_panel {
  650. flex: 1;
  651. min-width: 0;
  652. height: 100%;
  653. background: #fff;
  654. border-radius: 10px;
  655. }
  656. .question_nav {
  657. flex-shrink: 0;
  658. width: 240px;
  659. height: 100%;
  660. background: #fff;
  661. border-radius: 10px;
  662. box-sizing: border-box;
  663. overflow-y: auto;
  664. .nav_title {
  665. font-size: 14px;
  666. font-weight: 550;
  667. color: #333;
  668. padding: 10px;
  669. border-bottom: 1px solid #EBEEF5;
  670. }
  671. .nav_group {
  672. padding: 10px;
  673. & + .nav_group {
  674. margin-top: 24px;
  675. }
  676. .group_name {
  677. font-size: 14px;
  678. color: #333;
  679. font-weight: 550;
  680. padding-bottom: 12px;
  681. }
  682. .num_grid {
  683. display: grid;
  684. grid-template-columns: repeat(5, 32px);
  685. gap: 15px;
  686. /* 查看作答:未选中灰框,当前题绿底白字 */
  687. .num_btn {
  688. width: 32px;
  689. height: 32px;
  690. padding: 0;
  691. border-radius: 4px;
  692. border: 1px solid #2BC644;
  693. background: #fff;
  694. color: #2BC644;
  695. font-size: 14px;
  696. cursor: pointer;
  697. &.current {
  698. background: #2BC644;
  699. border-color: #2BC644;
  700. color: #fff;
  701. }
  702. }
  703. }
  704. }
  705. }
  706. .question_panel {
  707. flex: 1;
  708. min-width: 0;
  709. height: 100%;
  710. display: flex;
  711. flex-direction: column;
  712. background: #fff;
  713. border-radius: 10px;
  714. overflow: hidden;
  715. .section_tabs {
  716. flex-shrink: 0;
  717. display: flex;
  718. align-items: center;
  719. gap: 20px;
  720. padding:16px;
  721. box-sizing: border-box;
  722. .section_tab {
  723. position: relative;
  724. height: 28px;
  725. line-height: 28px;
  726. font-size: 14px;
  727. color: #909399;
  728. cursor: pointer;
  729. user-select: none;
  730. &.active {
  731. color: #2E64FA;
  732. font-weight: 550;
  733. &::after {
  734. content: '';
  735. position: absolute;
  736. left: 0;
  737. right: 0;
  738. bottom: 0;
  739. height: 3px;
  740. background: #2E64FA;
  741. border-radius: 1px;
  742. }
  743. }
  744. }
  745. }
  746. .question_scroll {
  747. flex: 1;
  748. min-height: 0;
  749. overflow-y: auto;
  750. padding: 16px;
  751. box-sizing: border-box;
  752. .content_section {
  753. margin-bottom: 20px;
  754. &:last-child {
  755. margin-bottom: 0;
  756. }
  757. }
  758. .q_tags {
  759. display: flex;
  760. align-items: center;
  761. gap: 8px;
  762. margin-bottom: 16px;
  763. .q_no {
  764. display: inline-flex;
  765. align-items: center;
  766. justify-content: center;
  767. min-width: 24px;
  768. height: 24px;
  769. padding: 0 6px;
  770. background: #F3F3F3;
  771. border-radius: 2px;
  772. font-weight: 550;
  773. font-size: 14px;
  774. color: #333333;
  775. box-sizing: border-box;
  776. }
  777. .q_type {
  778. font-weight: 550;
  779. font-size: 14px;
  780. color: #333333;
  781. }
  782. .q_level {
  783. font-weight: 400;
  784. font-size: 14px;
  785. color: #90CB75;
  786. margin-left: 8px;
  787. }
  788. }
  789. .q_stem {
  790. font-size: 15px;
  791. line-height: 1.7;
  792. color: #333;
  793. :deep(img) {
  794. max-width: 100%;
  795. }
  796. }
  797. .q_options {
  798. .q_option {
  799. display: flex;
  800. gap: 6px;
  801. margin-bottom: 10px;
  802. font-size: 14px;
  803. color: #333;
  804. line-height: 1.6;
  805. .opt_key {
  806. flex-shrink: 0;
  807. }
  808. }
  809. }
  810. .block_box {
  811. padding: 14px 16px;
  812. background: #F8F8F9;
  813. border-radius: 10px;
  814. border: 1px solid #DCDFE6;
  815. box-sizing: border-box;
  816. }
  817. .dock_answer {
  818. display: flex;
  819. align-items: center;
  820. gap: 16px;
  821. min-height: 36px;
  822. .dock_label,
  823. .dock_sub_no {
  824. flex-shrink: 0;
  825. display: inline-flex;
  826. align-items: center;
  827. height: 20px;
  828. line-height: 20px;
  829. font-size: 14px;
  830. color: #333;
  831. }
  832. .choice_radio {
  833. display: inline-flex;
  834. align-items: center;
  835. min-height: 20px;
  836. gap: 20px;
  837. :deep(.el-checkbox) {
  838. margin-right: 0 !important;
  839. height: 20px;
  840. display: inline-flex !important;
  841. align-items: center;
  842. cursor: default;
  843. }
  844. :deep(.el-checkbox__input) {
  845. display: inline-flex;
  846. align-items: center;
  847. height: 16px;
  848. line-height: 16px;
  849. cursor: default;
  850. }
  851. :deep(.el-checkbox__inner) {
  852. width: 16px;
  853. height: 16px;
  854. border: 1px solid #DCDFE6;
  855. background: #fff;
  856. border-radius: 2px;
  857. cursor: default;
  858. &::after {
  859. top: 1px;
  860. left: 5px;
  861. width: 3px;
  862. height: 8px;
  863. }
  864. }
  865. :deep(.el-checkbox__input.is-disabled .el-checkbox__inner) {
  866. background-color: #fff;
  867. border-color: #DCDFE6;
  868. }
  869. :deep(.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner) {
  870. background-color: #2E64FA;
  871. border-color: #2E64FA;
  872. }
  873. :deep(.el-checkbox__input.is-disabled + .el-checkbox__label) {
  874. color: #333;
  875. cursor: default;
  876. }
  877. :deep(.el-checkbox__label) {
  878. display: inline-flex;
  879. align-items: center;
  880. height: 20px;
  881. padding-left: 8px;
  882. font-size: 14px;
  883. color: #333;
  884. line-height: 20px;
  885. }
  886. :deep(.el-radio) {
  887. margin-right: 0 !important;
  888. height: 20px;
  889. display: inline-flex !important;
  890. align-items: center;
  891. cursor: default;
  892. }
  893. :deep(.el-radio__input) {
  894. display: inline-flex;
  895. align-items: center;
  896. height: 16px;
  897. line-height: 16px;
  898. cursor: default;
  899. }
  900. :deep(.el-radio__inner) {
  901. width: 16px;
  902. height: 16px;
  903. border: 1px solid #DCDFE6;
  904. background: #fff;
  905. cursor: default;
  906. }
  907. :deep(.el-radio__input.is-disabled .el-radio__inner) {
  908. background-color: #fff;
  909. border-color: #DCDFE6;
  910. }
  911. :deep(.el-radio__input.is-disabled.is-checked .el-radio__inner) {
  912. background-color: #2E64FA;
  913. border-color: #2E64FA;
  914. }
  915. :deep(.el-radio__input.is-disabled.is-checked .el-radio__inner::after) {
  916. background-color: #fff;
  917. }
  918. :deep(.el-radio__input.is-disabled + .el-radio__label),
  919. :deep(.el-radio__label) {
  920. padding-left: 8px;
  921. font-size: 14px;
  922. color: #333;
  923. line-height: 20px;
  924. cursor: default;
  925. }
  926. }
  927. .photo_list {
  928. display: flex;
  929. flex-wrap: wrap;
  930. align-items: center;
  931. gap: 10px;
  932. min-width: 0;
  933. .photo_item {
  934. width: 64px;
  935. height: 64px;
  936. border-radius: 4px;
  937. overflow: hidden;
  938. background: #fff;
  939. img {
  940. width: 100%;
  941. height: 100%;
  942. object-fit: cover;
  943. display: block;
  944. cursor: zoom-in;
  945. }
  946. }
  947. .empty_answer {
  948. font-size: 14px;
  949. color: #999;
  950. }
  951. }
  952. }
  953. .analysis_box {
  954. .analysis_row {
  955. display: flex;
  956. align-items: flex-start;
  957. gap: 10px;
  958. & + .analysis_row {
  959. margin-top: 14px;
  960. }
  961. }
  962. .analysis_label {
  963. flex-shrink: 0;
  964. font-size: 14px;
  965. font-weight: 550;
  966. color: #333;
  967. line-height: 28px;
  968. }
  969. .knowledge_tags {
  970. display: flex;
  971. flex-wrap: wrap;
  972. gap: 8px;
  973. .knowledge_tag {
  974. display: inline-flex;
  975. align-items: center;
  976. height: 28px;
  977. padding: 0 10px;
  978. border: 1px solid #2E64FA;
  979. border-radius: 4px;
  980. color: #2E64FA;
  981. font-size: 13px;
  982. background: #fff;
  983. box-sizing: border-box;
  984. }
  985. }
  986. .analysis_content {
  987. flex: 1;
  988. min-width: 0;
  989. .correct_answer {
  990. display: inline-block;
  991. margin-right: 8px;
  992. font-size: 14px;
  993. font-weight: 550;
  994. color: #333;
  995. line-height: 28px;
  996. }
  997. .analysis_text {
  998. margin-top: 6px;
  999. font-size: 14px;
  1000. line-height: 1.7;
  1001. color: #666;
  1002. :deep(img) {
  1003. max-width: 100%;
  1004. vertical-align: middle;
  1005. }
  1006. }
  1007. }
  1008. }
  1009. }
  1010. .view_dock {
  1011. flex-shrink: 0;
  1012. display: flex;
  1013. align-items: center;
  1014. justify-content: flex-start;
  1015. gap: 16px;
  1016. padding: 16px;
  1017. background: #fff;
  1018. box-sizing: border-box;
  1019. .ai_btn {
  1020. height: 36px;
  1021. padding: 0 14px;
  1022. border-radius: 4px;
  1023. border: none;
  1024. background: linear-gradient( 270deg, #6659FF 0%, #9A57FF 60.38%, #F292FF 100%);
  1025. color: #fff;
  1026. // &:hover,
  1027. // &:focus {
  1028. // background: #847BE6;
  1029. // color: #fff;
  1030. // opacity: 0.92;
  1031. // }
  1032. }
  1033. .dock_actions {
  1034. display: flex;
  1035. align-items: center;
  1036. gap: 16px;
  1037. .nav_btn {
  1038. height: 36px;
  1039. padding: 0 14px;
  1040. border-radius: 4px;
  1041. line-height: 34px;
  1042. color: #2E64FA;
  1043. border-color: #2E64FA;
  1044. background: #fff;
  1045. margin:0;
  1046. &.is-disabled,
  1047. &.is-disabled:hover,
  1048. &.is-disabled:focus {
  1049. background: #F3F3F3;
  1050. color: #C0C4CC;
  1051. border: 1px solid #C0C4CC;
  1052. }
  1053. }
  1054. }
  1055. }
  1056. }
  1057. }
  1058. }
  1059. </style>