Sin descripción
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

datistart.vue 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745
  1. <template>
  2. <van-sticky>
  3. <van-nav-bar>
  4. <!-- <template #left>
  5. <van-icon name="arrow-left" size="18" @click="goBack" />
  6. </template> -->
  7. <template #title> 答题考试 </template>
  8. <template #right>
  9. <!-- 提交按钮 -->
  10. <van-button @click="checkBeforeSubmit" round color="linear-gradient(to right, #18FFFF, #304FFE)"
  11. style="height: 30px; width: 80px">交卷</van-button>
  12. </template>
  13. </van-nav-bar>
  14. </van-sticky>
  15. <van-overlay :show="overlayloading">
  16. <div class="wrapper">
  17. <van-loading color="#0094ff"> 加载中... </van-loading>
  18. </div>
  19. </van-overlay>
  20. <div class="quiz-page">
  21. <div class="question-number">
  22. {{ activeIndex + 1 }}/{{ questions.length }}
  23. </div>
  24. <div v-if="questions.length > 0" class="question-content">
  25. <!-- 题干 -->
  26. <p class="kong">
  27. <img
  28. :src="getQuestionTypeImage(currentQuestion.category)"
  29. class="question-type-img"
  30. alt=""
  31. />
  32. {{ currentQuestion.stem }}
  33. </p>
  34. <!-- 附件区域:图片或视频 -->
  35. <div v-if="currentQuestion.fileUrl" class="question-attachment-wrapper">
  36. <img
  37. v-if="isImageType(currentQuestion.fileType)"
  38. :src="currentQuestion.fileUrl"
  39. alt="题目图片"
  40. class="media-preview"
  41. />
  42. <video
  43. v-else-if="isVideoType(currentQuestion.fileType)"
  44. controls
  45. preload="metadata"
  46. class="media-preview"
  47. >
  48. <source
  49. :src="currentQuestion.fileUrl"
  50. :type="getMimeType(currentQuestion.fileType)"
  51. />
  52. 您的浏览器不支持视频播放。
  53. </video>
  54. </div>
  55. <!-- 单选题 -->
  56. <div v-if="currentQuestion.category === '单选'">
  57. <van-radio-group v-model="userAnswers[currentQuestion.id]">
  58. <van-radio :name="'A'" class="kong">A.{{ currentQuestion.optionA }}</van-radio>
  59. <van-radio :name="'B'" class="kong">B.{{ currentQuestion.optionB }}</van-radio>
  60. <van-radio :name="'C'" class="kong">C.{{ currentQuestion.optionC }}</van-radio>
  61. <van-radio :name="'D'" class="kong" v-if="currentQuestion.optionD">D.
  62. {{ currentQuestion.optionD }}</van-radio>
  63. <van-radio :name="'E'" class="kong" v-if="currentQuestion.optionE">E.
  64. {{ currentQuestion.optionE }}</van-radio>
  65. </van-radio-group>
  66. </div>
  67. <!-- 多选题 -->
  68. <div v-if="currentQuestion.category === '多选'">
  69. <van-checkbox-group v-model="userAnswers[currentQuestion.id]" shape="square">
  70. <van-checkbox :name="'A'" class="kong">A.{{ currentQuestion.optionA }}</van-checkbox>
  71. <van-checkbox :name="'B'" class="kong">B.{{ currentQuestion.optionB }}</van-checkbox>
  72. <van-checkbox :name="'C'" class="kong">C.{{ currentQuestion.optionC }}</van-checkbox>
  73. <van-checkbox :name="'D'" class="kong"
  74. v-if="currentQuestion.optionD">D.{{ currentQuestion.optionD }}</van-checkbox>
  75. <van-checkbox :name="'E'" class="kong"
  76. v-if="currentQuestion.optionE">E.{{ currentQuestion.optionE }}</van-checkbox>
  77. </van-checkbox-group>
  78. </div>
  79. <!-- 判断题 -->
  80. <div v-if="currentQuestion.category === '判断'">
  81. <van-radio-group v-model="userAnswers[currentQuestion.id]">
  82. <van-radio :name="'A'" class="kong">A.正确</van-radio>
  83. <van-radio :name="'B'" class="kong">B.错误</van-radio>
  84. </van-radio-group>
  85. </div>
  86. </div>
  87. <!-- 底部固定栏 -->
  88. <div class="footer">
  89. <van-button @click="prevQuestion" :disabled="activeIndex === 0"
  90. style="height: 40px; width: 45%">上一题</van-button>
  91. <van-button @click="nextQuestion" :disabled="activeIndex === questions.length - 1" style="
  92. height: 40px;
  93. width: 45%;
  94. background-color: var(--van-radio-checked-icon-color);
  95. border-color: var(--van-radio-checked-icon-color);
  96. color: #fff;
  97. ">下一题</van-button>
  98. </div>
  99. <!-- 提交前确认弹窗 -->
  100. <van-dialog v-model:show="confirmSubmitDialog" title="确认交卷" show-cancel-button @confirm="submitForm">
  101. <p :class="{ 'van-dialog__message': true, 'text-center': true }">
  102. <span v-if="hasUnanswered">{{ unansweredText }}</span>
  103. <span v-else>{{ completedText }}</span>
  104. </p>
  105. </van-dialog>
  106. <!-- 结果弹窗 -->
  107. <van-popup v-model:show="showResult" position="top" style="height: 100%">
  108. <van-sticky>
  109. <van-nav-bar title="答题结果" />
  110. </van-sticky>
  111. <div style="
  112. margin-top: 10px;
  113. margin-left: 20px;
  114. margin-bottom: 20px;
  115. font-weight: bold;
  116. ">
  117. 本次得分:{{ totalScore }}
  118. <!--取接口成绩-->
  119. </div>
  120. <van-divider />
  121. <!-- 题干 -->
  122. <div v-for="question in questions" :key="question.id" class="question">
  123. <p>
  124. <span v-if="question.category === '单选'">[单选]</span>
  125. <span v-if="question.category === '多选'">[多选]</span>
  126. <span v-if="question.category === '判断'">[判断]</span>
  127. {{ question.stem }}
  128. </p>
  129. <!-- 显示提交答案 -->
  130. <p>
  131. <span :style="{
  132. color: Array.isArray(userAnswers[question.id])
  133. ? userAnswers[question.id].sort().join('') ===
  134. question.answer
  135. ? '#007aff'
  136. : 'red'
  137. : userAnswers[question.id] === question.answer
  138. ? '#007aff'
  139. : 'red',
  140. }">
  141. 提交答案:{{
  142. Array.isArray(userAnswers[question.id])
  143. ? userAnswers[question.id].sort().join("")
  144. : userAnswers[question.id] || "未作答"
  145. }}
  146. </span>
  147. </p>
  148. <!-- 显示正确答案 -->
  149. <p style="color: #007aff">正确答案:{{ question.answer }}</p>
  150. <div v-if="question.category === '单选'" class="kong">
  151. <div>A. {{ question.optionA }}</div>
  152. <div>B. {{ question.optionB }}</div>
  153. <div>C. {{ question.optionC }}</div>
  154. <div v-if="question.optionD">D. {{ question.optionD }}</div>
  155. <div v-if="question.optionE">E. {{ question.optionE }}</div>
  156. </div>
  157. <div v-if="question.category === '多选'" class="kong">
  158. <div>A. {{ question.optionA }}</div>
  159. <div>B. {{ question.optionB }}</div>
  160. <div>C. {{ question.optionC }}</div>
  161. <div v-if="question.optionD">D. {{ question.optionD }}</div>
  162. <div v-if="question.optionE">E. {{ question.optionE }}</div>
  163. </div>
  164. <div v-if="question.category === '判断'" class="kong">
  165. <div>A.正确</div>
  166. <div>B.错误</div>
  167. </div>
  168. <!-- AI解析按钮和内容 -->
  169. <div style="margin: 10px 0;">
  170. <van-button
  171. type="primary"
  172. size="small"
  173. :loading="analysisLoading[question.id]"
  174. @click="generateAIAnalysis(question,true)"
  175. >
  176. {{ aiAnalysis[question.id] ? '重新解析' : 'AI解析' }}
  177. </van-button>
  178. <!-- 显示AI解析内容 -->
  179. <div v-if="aiAnalysis[question.id]" class="ai-analysis-content">
  180. <div v-html="renderAnalysis(aiAnalysis[question.id])"></div>
  181. </div>
  182. </div>
  183. <van-divider />
  184. </div>
  185. <div style="margin-top: 20px; text-align: center; margin-bottom: 20px">
  186. <van-button class="questionBtn" type="primary" @click="confirmResult">确定</van-button>
  187. </div>
  188. </van-popup>
  189. </div>
  190. </template>
  191. <script setup>
  192. import {
  193. onMounted,
  194. ref,
  195. getCurrentInstance,
  196. computed
  197. } from "vue";
  198. import {
  199. showConfirmDialog,
  200. showSuccessToast,
  201. showFailToast,
  202. showDialog
  203. } from 'vant';
  204. import {
  205. useRouter,
  206. useRoute
  207. } from "vue-router";
  208. import {
  209. examResult,
  210. formNew,
  211. myDefaultCourse,
  212. saveScore,
  213. sortData,
  214. } from "@/api/dati";
  215. const {
  216. proxy
  217. } = getCurrentInstance()
  218. const router = useRouter();
  219. const route = useRoute();
  220. const courseId = route.query.courseId;
  221. const userId = route.query.userId;
  222. if (userId == '' || userId == 'undefined') userId = localStorage.getItem('userId')
  223. const todayStr = route.query.todayStr;
  224. // 判断是否为图片类型
  225. const isImageType = (type) => {
  226. return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes((type || '').toLowerCase());
  227. };
  228. // 判断是否为视频类型
  229. const isVideoType = (type) => {
  230. return ['mp4', 'mov', 'avi', 'wmv', 'flv', 'webm'].includes((type || '').toLowerCase());
  231. };
  232. // 获取 MIME 类型(用于 video 的 source)
  233. const getMimeType = (type) => {
  234. const map = {
  235. mp4: 'video/mp4',
  236. mov: 'video/quicktime',
  237. avi: 'video/x-msvideo',
  238. wmv: 'video/x-ms-wmv',
  239. flv: 'video/x-flv',
  240. webm: 'video/webm'
  241. };
  242. return map[type?.toLowerCase()] || 'video/mp4';
  243. };
  244. const questions = ref([]);
  245. const userAnswers = ref({});
  246. const activeIndex = ref(0);
  247. const totalScore = ref(0);
  248. const showResult = ref(false);
  249. const confirmSubmitDialog = ref(false);
  250. const hasUnanswered = ref(false);
  251. const unansweredText = "有题目未完成,是否确认交卷?";
  252. const completedText = "已完成所有题目,是否确认交卷?";
  253. const overlayloading = ref(false);
  254. // 在组件挂载时获取试卷
  255. onMounted(async () => {
  256. overlayloading.value = true;
  257. await getForm();
  258. overlayloading.value = false;
  259. });
  260. //获取试卷
  261. const getForm = async () => {
  262. var url = '/sgsafe/ExamLine/query'
  263. const query = ref({
  264. headId: courseId
  265. })
  266. var param = {
  267. params: JSON.stringify(query.value)
  268. }
  269. try {
  270. const res = await proxy.$axios.post(url, param);
  271. if (res.data.code === 0) {
  272. questions.value = res.data.data
  273. } else {
  274. console.log('操作失败!' + res.data.msg);
  275. }
  276. } catch (error) {
  277. console.log('请求出错:', questions);
  278. }
  279. };
  280. // 获取当前题目
  281. const currentQuestion = computed(() => {
  282. return questions.value[activeIndex.value];
  283. });
  284. // 获取题目类型对应的图片路径
  285. import danxuan from '@/assets/img/dx.svg'
  286. import duoxuanImg from '@/assets/img/ksdx.svg'
  287. import panduanImg from '@/assets/img/kspd.svg'
  288. const getQuestionTypeImage = (category) => {
  289. switch (category) {
  290. case "单选": // 单选
  291. return danxuan;
  292. case "多选": // 多选
  293. return duoxuanImg;
  294. case "判断": // 判断
  295. return panduanImg;
  296. default:
  297. return "";
  298. }
  299. };
  300. //返回答题首页
  301. const goBack = () => {
  302. router.push({
  303. path: "/dailyproblem"
  304. });
  305. };
  306. // 切换到下一题
  307. const nextQuestion = () => {
  308. if (activeIndex.value < questions.value.length - 1) {
  309. activeIndex.value++;
  310. }
  311. };
  312. // 切换到上一题
  313. const prevQuestion = () => {
  314. if (activeIndex.value > 0) {
  315. activeIndex.value--;
  316. }
  317. };
  318. const getUserAnswers = () => {
  319. let useranswers = [];
  320. questions.value.forEach((question) => {
  321. const userAnswer = userAnswers.value[question.id]; // 获取用户的答案
  322. let userAnswerString;
  323. if (Array.isArray(userAnswer)) {
  324. // 多选题,将数组转换为字符串
  325. userAnswerString = userAnswer.sort().join(""); // 排序并转换为字符串,如 "ABC"
  326. } else {
  327. // 单选题,直接是字符串
  328. userAnswerString = userAnswer || ""; // 如果未选择答案,则设为空字符串
  329. }
  330. // 将答案保存到 answers 数组中
  331. useranswers.push({
  332. id: question.id, // 题目 ID
  333. userAnswer: userAnswerString, // 用户的答案
  334. });
  335. });
  336. return useranswers;
  337. };
  338. //交卷
  339. const submitForm = async () => {
  340. overlayloading.value = true;
  341. try {
  342. let answers = getUserAnswers();
  343. //console.log('answers.value', answers)
  344. var url = '/sgsafe/ExamLine/appSaveMyScore'
  345. var param = {
  346. json: JSON.stringify(
  347. answers
  348. )
  349. }
  350. try {
  351. const res = await proxy.$axios.post(url, param);
  352. if (res.data.code === 0) {
  353. showSuccessToast("保存成功")
  354. } else {
  355. console.log('操作失败!' + res.data.msg);
  356. }
  357. } catch (error) {
  358. console.log('请求出错:', questions);
  359. }
  360. //开始判卷
  361. var url2 = '/sgsafe/Package/doProc'
  362. var param2 = {
  363. procName: 'safeplat.sxsp_grade_exam_p',
  364. param: JSON.stringify([courseId])
  365. }
  366. try {
  367. const res2 = await proxy.$axios.post(url2, param2);
  368. if (res2.data.code === 0) {
  369. consle.log("courseId:" + courseId + "判卷完成!")
  370. } else {
  371. console.log('操作失败!' + res.data.msg);
  372. }
  373. } catch (error) {
  374. console.log('请求出错:', questions);
  375. }
  376. overlayloading.value = false;
  377. //查询本日答题次数和分数
  378. var url3 = '/sgsafe/DailyExam/appQueryMyScore'
  379. const query3 = ref({
  380. userId: userId,
  381. examDate: todayStr,
  382. headId: courseId,
  383. })
  384. var param3 = {
  385. params: JSON.stringify(query3.value)
  386. }
  387. const res3 = await proxy.$axios.post(url3, param3);
  388. if (res3.data.code === 0) {
  389. console.log(res3.data)
  390. if (res3.data.data.dailyExamList.length==0){
  391. var url4='/sgsafe/ExamHead/queryByheadId'
  392. var param4 = {
  393. headId: courseId
  394. }
  395. const res4 = await proxy.$axios.post(url4, param4);
  396. if (res4.data.code === 0) {
  397. totalScore.value = res4.data.data.totalScore;
  398. }
  399. showConfirmDialog({
  400. message: "判卷完成",
  401. confirmButtonText: "查看本次答题结果",
  402. cancelButtonText: "退出答题"
  403. })
  404. .then(() => {
  405. showResult.value = true;
  406. })
  407. .catch(() => {
  408. router.replace({
  409. path: "/dailyproblem"
  410. });
  411. })
  412. }else {
  413. let times = res3.data.data.dailyExamList[0].examCounts;
  414. totalScore.value = res3.data.data.headScore;
  415. showConfirmDialog({
  416. message: "判卷完成",
  417. confirmButtonText: "查看本次答题结果",
  418. cancelButtonText: times == "3" ? "退出答题" : "继续答题",
  419. })
  420. .then(() => {
  421. showResult.value = true;
  422. })
  423. .catch(() => {
  424. if (times == "3") {
  425. router.replace({
  426. path: "/dailyproblem"
  427. });
  428. } else {
  429. router.push({
  430. path: "/dailyproblem"
  431. });
  432. }
  433. });
  434. }
  435. } else {
  436. console.log('操作失败!' + res3.data.msg);
  437. }
  438. } catch (error) {
  439. console.error("出错:", error);
  440. showFailToast("交卷失败");
  441. }
  442. };
  443. // 确认结果并返回
  444. const confirmResult = () => {
  445. showResult.value = false;
  446. // router.back();
  447. router.back()
  448. };
  449. // 检查是否所有题目都已作答
  450. const checkBeforeSubmit = () => {
  451. hasUnanswered.value = questions.value.some((question) => {
  452. const userAnswer = userAnswers.value[question.id];
  453. return (
  454. !userAnswer || (Array.isArray(userAnswer) && userAnswer.length === 0)
  455. );
  456. });
  457. confirmSubmitDialog.value = true;
  458. };
  459. // AI解析功能
  460. // AI解析相关变量
  461. const aiAnalysis = ref({}); // 存储每道题的AI解析内容
  462. const analysisLoading = ref({}); // 存储每道题的解析加载状态
  463. import { fetchHuaweiResponse } from "@/tools/deepseek.js";
  464. // 动态导入依赖
  465. let marked, DOMPurify;
  466. const initMarkdownLibs = async () => {
  467. try {
  468. // 尝试导入marked
  469. const markedModule = await import('marked');
  470. marked = markedModule.marked || markedModule.default || markedModule;
  471. // 尝试导入DOMPurify
  472. const dompurifyModule = await import('dompurify');
  473. DOMPurify = dompurifyModule.default || dompurifyModule;
  474. } catch (error) {
  475. console.warn('Markdown libraries not available, using plain text', error);
  476. // 如果导入失败,使用基础功能
  477. marked = {
  478. parse: (text) => text
  479. };
  480. DOMPurify = {
  481. sanitize: (html) => html
  482. };
  483. }
  484. };
  485. // 在组件挂载时初始化
  486. onMounted(() => {
  487. initMarkdownLibs();
  488. });
  489. // 生成AI解析
  490. const generateAIAnalysis = async (question, force = false) => {
  491. // 如果该题已有解析且不是强制重新生成,直接返回
  492. if (aiAnalysis.value[question.id] && !force) {
  493. return;
  494. }
  495. // 如果是重新解析,先清空之前的内容
  496. if (force) {
  497. aiAnalysis.value[question.id] = '';
  498. }
  499. // 确保依赖已加载
  500. if (!marked || !DOMPurify) {
  501. await initMarkdownLibs();
  502. }
  503. // 设置加载状态
  504. analysisLoading.value[question.id] = true;
  505. try {
  506. // 构造提示词
  507. let prompt = `请为以下题目提供详细解析:
  508. 题目类型:${question.category}题干:${question.stem}`;
  509. // 添加选项
  510. if (question.optionA) prompt += `\nA. ${question.optionA}`;
  511. if (question.optionB) prompt += `\nB. ${question.optionB}`;
  512. if (question.optionC) prompt += `\nC. ${question.optionC}`;
  513. if (question.optionD) prompt += `\nD. ${question.optionD}`;
  514. if (question.optionE) prompt += `\nE. ${question.optionE}`;
  515. prompt += `\n正确答案:${question.answer}`;
  516. // 添加用户答案(如果已作答)
  517. const userAnswer = userAnswers.value[question.id];
  518. if (userAnswer) {
  519. const userAnswerString = Array.isArray(userAnswer)
  520. ? userAnswer.sort().join("")
  521. : userAnswer;
  522. prompt += `\n用户答案:${userAnswerString}`;
  523. }
  524. prompt += `\n\n请提供以下内容:
  525. 1. 正确答案的解释
  526. 2. 为什么其他选项不正确(如果用户答案错误)
  527. 3. 相关知识点说明`;
  528. // 构造消息对象
  529. const messages = [
  530. {
  531. role: "user",
  532. content: prompt
  533. }
  534. ];
  535. // 调用AI接口
  536. fetchHuaweiResponse(
  537. messages,
  538. (content, isThinking, isEnd) => {
  539. // 实时更新解析内容
  540. aiAnalysis.value[question.id] = content;
  541. // 如果是最终结果,停止加载状态
  542. if (isEnd) {
  543. analysisLoading.value[question.id] = false;
  544. }
  545. },
  546. null
  547. );
  548. } catch (error) {
  549. console.error('AI解析生成失败:', error);
  550. analysisLoading.value[question.id] = false;
  551. aiAnalysis.value[question.id] = '解析生成失败';
  552. }
  553. };
  554. // 解析内容转换为HTML
  555. const renderAnalysis = (content) => {
  556. if (!content) return '';
  557. try {
  558. // 确保依赖已加载
  559. if (!marked || !DOMPurify) {
  560. return content.replace(/\n/g, '<br>');
  561. }
  562. const html = marked.parse ? marked.parse(content) : marked(content);
  563. return DOMPurify.sanitize ? DOMPurify.sanitize(html) : html;
  564. } catch (error) {
  565. console.error('Markdown解析错误:', error);
  566. return content.replace(/\n/g, '<br>');
  567. }
  568. };
  569. </script>
  570. <style scoped>
  571. .quiz-page {
  572. padding: 20px;
  573. }
  574. .question-type-img {
  575. width: 54px;
  576. height: 20px;
  577. }
  578. .question {
  579. margin-left: 20px;
  580. margin-right: 20px;
  581. }
  582. .kong {
  583. margin-bottom: 20px;
  584. }
  585. .footer {
  586. position: fixed;
  587. bottom: 0;
  588. left: 0;
  589. width: 100%;
  590. background-color: #fff;
  591. display: flex;
  592. justify-content: space-around;
  593. align-items: center;
  594. margin-bottom: 10px;
  595. }
  596. .van-dialog__message {
  597. text-align: center;
  598. }
  599. .questionBtn {
  600. width: 40%;
  601. }
  602. /* 遮罩 */
  603. .wrapper {
  604. display: flex;
  605. align-items: center;
  606. justify-content: center;
  607. height: 100%;
  608. }
  609. .van-overlay {
  610. z-index: 2;
  611. background-color: rgba(0, 0, 0, 0.5);
  612. }
  613. .ai-analysis-content {
  614. margin-top: 10px;
  615. padding: 10px;
  616. background-color: #f5f5f5;
  617. border-radius: 4px;
  618. font-size: 14px;
  619. line-height: 1.6;
  620. }
  621. .ai-analysis-content :deep(h1),
  622. .ai-analysis-content :deep(h2),
  623. .ai-analysis-content :deep(h3) {
  624. margin: 10px 0;
  625. font-weight: bold;
  626. font-size: 16px;
  627. }
  628. .ai-analysis-content :deep(p) {
  629. margin: 8px 0;
  630. }
  631. .ai-analysis-content :deep(ul),
  632. .ai-analysis-content :deep ol {
  633. padding-left: 20px;
  634. margin: 8px 0;
  635. }
  636. .ai-analysis-content :deep(li) {
  637. margin: 4px 0;
  638. }
  639. .ai-analysis-content :deep(code) {
  640. padding: 2px 4px;
  641. background-color: #e0e0e0;
  642. border-radius: 3px;
  643. font-family: monospace;
  644. }
  645. .ai-analysis-content :deep(pre) {
  646. padding: 10px;
  647. background-color: #e0e0e0;
  648. border-radius: 4px;
  649. overflow-x: auto;
  650. }
  651. /* 附件容器:在题干下方,左侧缩进对齐题干文字 */
  652. .question-attachment-wrapper {
  653. margin: 12px 0 20px 54px; /* 54px = .question-type-img 宽度,确保对齐文字起始位置 */
  654. }
  655. /* 图片 & 视频统一预览 */
  656. .media-preview {
  657. display: block;
  658. max-width: 100%;
  659. max-height: 320px;
  660. width: auto;
  661. height: auto;
  662. object-fit: contain;
  663. border-radius: 6px;
  664. background-color: #f8f9fa;
  665. box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
  666. }
  667. </style>