| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725 |
- <template>
- <van-sticky>
- <van-nav-bar>
- <!-- <template #left>
- <van-icon name="arrow-left" size="18" @click="goBack" />
- </template> -->
- <template #title> 答题考试 </template>
- <template #right>
- <!-- 提交按钮 -->
- <van-button @click="checkBeforeSubmit" round color="linear-gradient(to right, #18FFFF, #304FFE)"
- style="height: 30px; width: 80px">交卷</van-button>
- </template>
- </van-nav-bar>
- </van-sticky>
- <van-overlay :show="overlayloading">
- <div class="wrapper">
- <van-loading color="#0094ff"> 加载中... </van-loading>
- </div>
- </van-overlay>
-
- <div class="quiz-page">
- <div class="question-number">
- {{ activeIndex + 1 }}/{{ questions.length }}
- </div>
- <div v-if="questions.length > 0" class="question-content">
- <!-- 题干 -->
- <p class="kong">
- <img
- :src="getQuestionTypeImage(currentQuestion.category)"
- class="question-type-img"
- alt=""
- />
- {{ currentQuestion.stem }}
- </p>
-
-
- <!-- 单选题 -->
- <div v-if="currentQuestion.category === '单选'">
- <van-radio-group v-model="userAnswers[currentQuestion.id]">
- <van-radio :name="'A'" class="kong">A.{{ currentQuestion.optionA }}</van-radio>
- <van-radio :name="'B'" class="kong">B.{{ currentQuestion.optionB }}</van-radio>
- <van-radio :name="'C'" class="kong">C.{{ currentQuestion.optionC }}</van-radio>
- <van-radio :name="'D'" class="kong" v-if="currentQuestion.optionD">D.
- {{ currentQuestion.optionD }}</van-radio>
- <van-radio :name="'E'" class="kong" v-if="currentQuestion.optionE">E.
- {{ currentQuestion.optionE }}</van-radio>
- </van-radio-group>
- </div>
-
- <!-- 多选题 -->
- <div v-if="currentQuestion.category === '多选'">
- <van-checkbox-group v-model="userAnswers[currentQuestion.id]" shape="square">
- <van-checkbox :name="'A'" class="kong">A.{{ currentQuestion.optionA }}</van-checkbox>
- <van-checkbox :name="'B'" class="kong">B.{{ currentQuestion.optionB }}</van-checkbox>
- <van-checkbox :name="'C'" class="kong">C.{{ currentQuestion.optionC }}</van-checkbox>
- <van-checkbox :name="'D'" class="kong"
- v-if="currentQuestion.optionD">D.{{ currentQuestion.optionD }}</van-checkbox>
- <van-checkbox :name="'E'" class="kong"
- v-if="currentQuestion.optionE">E.{{ currentQuestion.optionE }}</van-checkbox>
- </van-checkbox-group>
- </div>
-
- <!-- 判断题 -->
- <div v-if="currentQuestion.category === '判断'">
- <van-radio-group v-model="userAnswers[currentQuestion.id]">
- <van-radio :name="'A'" class="kong">A.正确</van-radio>
- <van-radio :name="'B'" class="kong">B.错误</van-radio>
- </van-radio-group>
- </div>
- </div>
-
- <!-- 底部固定栏 -->
- <div class="footer">
- <van-button @click="prevQuestion" :disabled="activeIndex === 0"
- style="height: 40px; width: 45%">上一题</van-button>
- <van-button @click="nextQuestion" :disabled="activeIndex === questions.length - 1" style="
- height: 40px;
- width: 45%;
- background-color: var(--van-radio-checked-icon-color);
- border-color: var(--van-radio-checked-icon-color);
- color: #fff;
- ">下一题</van-button>
- </div>
-
- <!-- 提交前确认弹窗 -->
- <van-dialog v-model:show="confirmSubmitDialog" title="确认交卷" show-cancel-button @confirm="submitForm">
- <p :class="{ 'van-dialog__message': true, 'text-center': true }">
- <span v-if="hasUnanswered">{{ unansweredText }}</span>
- <span v-else>{{ completedText }}</span>
- </p>
- </van-dialog>
-
- <!-- 结果弹窗 -->
- <van-popup v-model:show="showResult" position="top" style="height: 100%">
- <van-sticky>
- <van-nav-bar title="答题结果" />
- </van-sticky>
- <div style="
- margin-top: 10px;
- margin-left: 20px;
- margin-bottom: 20px;
- font-weight: bold;
- ">
- 本次得分:{{ totalScore }}
- <!--取接口成绩-->
- </div>
- <van-divider />
- <!-- 题干 -->
- <div v-for="question in questions" :key="question.id" class="question">
- <p>
- <span v-if="question.category === '单选'">[单选]</span>
- <span v-if="question.category === '多选'">[多选]</span>
- <span v-if="question.category === '判断'">[判断]</span>
- {{ question.stem }}
- </p>
- <!-- 显示提交答案 -->
- <p>
- <span :style="{
- color: Array.isArray(userAnswers[question.id])
- ? userAnswers[question.id].sort().join('') ===
- question.answer
- ? '#007aff'
- : 'red'
- : userAnswers[question.id] === question.answer
- ? '#007aff'
- : 'red',
- }">
- 提交答案:{{
- Array.isArray(userAnswers[question.id])
- ? userAnswers[question.id].sort().join("")
- : userAnswers[question.id] || "未作答"
- }}
- </span>
- </p>
- <!-- 显示正确答案 -->
- <p style="color: #007aff">正确答案:{{ question.answer }}</p>
- <div v-if="question.category === '单选'" class="kong">
- <div>A. {{ question.optionA }}</div>
- <div>B. {{ question.optionB }}</div>
- <div>C. {{ question.optionC }}</div>
- <div v-if="question.optionD">D. {{ question.optionD }}</div>
- <div v-if="question.optionE">E. {{ question.optionE }}</div>
- </div>
- <div v-if="question.category === '多选'" class="kong">
- <div>A. {{ question.optionA }}</div>
- <div>B. {{ question.optionB }}</div>
- <div>C. {{ question.optionC }}</div>
- <div v-if="question.optionD">D. {{ question.optionD }}</div>
- <div v-if="question.optionE">E. {{ question.optionE }}</div>
- </div>
- <div v-if="question.category === '判断'" class="kong">
- <div>A.正确</div>
- <div>B.错误</div>
- </div>
-
- <!-- AI解析按钮和内容 -->
- <div style="margin: 10px 0;">
- <van-button
- type="primary"
- size="small"
- :loading="analysisLoading[question.id]"
- @click="generateAIAnalysis(question,true)"
- >
- {{ aiAnalysis[question.id] ? '重新解析' : 'AI解析' }}
- </van-button>
-
- <!-- 显示AI解析内容 -->
- <div v-if="aiAnalysis[question.id]" class="ai-analysis-content">
- <div v-html="renderAnalysis(aiAnalysis[question.id])"></div>
- </div>
- </div>
-
- <van-divider />
- </div>
- <div style="margin-top: 20px; text-align: center; margin-bottom: 20px">
- <van-button class="questionBtn" type="primary" @click="confirmResult">确定</van-button>
- </div>
- </van-popup>
- </div>
- </template>
-
- <script setup>
- import {
- onMounted,
- ref,
- getCurrentInstance,
- computed
- } from "vue";
- import {
- showConfirmDialog,
- showSuccessToast,
- showFailToast,
- showDialog
- } from 'vant';
- import {
- useRouter,
- useRoute
- } from "vue-router";
- import {
- examResult,
- formNew,
- myDefaultCourse,
- saveScore,
- sortData,
- } from "@/api/dati";
- const {
- proxy
- } = getCurrentInstance()
-
- const router = useRouter();
- const route = useRoute();
- const query = route.query;
-
- // 1. 从本地存储获取当前用户的 userCode(字符串)
- const currentUserCode = localStorage.getItem('userCode');
-
- // 2. 包装成数组(即使为空也安全)
- const userCodeArray = currentUserCode ? [currentUserCode] : [];
-
- // 3. 按原来的方式生成 userDesc 字符串
- const userDesc = userCodeArray.join(',');
- const getExamContext = () => ({
- id: route.query.examId || '',
- testRole: route.query.testRole || '',
- checkTime: route.query.checkTime || '',
- checkName: route.query.checkName || '',
- testType: route.query.testType || '',
- addId: route.query.addId || '',
- userDesc: userDesc,
-
- });
-
- const userId = localStorage.getItem('userId');
- const questions = ref([]);
-
- const userAnswers = ref({});
- const activeIndex = ref(0);
- const totalScore = ref(0);
- const showResult = ref(false);
-
- const confirmSubmitDialog = ref(false);
- const hasUnanswered = ref(false);
- const unansweredText = "有题目未完成,是否确认交卷?";
- const completedText = "已完成所有题目,是否确认交卷?";
- const overlayloading = ref(false);
- // 在组件挂载时获取试卷
- onMounted(async () => {
- overlayloading.value = true;
- await saveChecUser();
- await getForm();
- overlayloading.value = false;
- });
- const handData = ref({});
-
- const saveChecUser = async () => {
- const examContext = getExamContext();
- console.log("接到的参数:",examContext);
- var url = '/sgsafe/ExamHead/saveChecUser';
- var param = {
- params: JSON.stringify(examContext)
- };
- const res = await proxy.$axios.post(url, param);
- if (res.data.code == '0') {
- handData.value = res.data.data;
- } else {
- showFailToast('操作失败!' + res.data.msg)
- }
-
-
- }
- //获取试卷
- const getForm = async () => {
- console.log("headId为",handData.value.id);
- var url = '/sgsafe/ExamLine/query'
- const query1 = ref({
- headId: handData.value.id
- })
- var param = {
- params: JSON.stringify(query1.value)
- }
- try {
- const res = await proxy.$axios.post(url, param);
- if (res.data.code === 0) {
- questions.value = res.data.data
- } else {
- console.log('操作失败!' + res.data.msg);
- }
- } catch (error) {
- console.log('请求出错:', questions);
- }
- };
-
- // 获取当前题目
- const currentQuestion = computed(() => {
- return questions.value[activeIndex.value];
- });
-
- // 获取题目类型对应的图片路径
- import danxuan from '@/assets/img/dx.svg'
- import duoxuanImg from '@/assets/img/ksdx.svg'
- import panduanImg from '@/assets/img/kspd.svg'
- const getQuestionTypeImage = (category) => {
- switch (category) {
- case "单选": // 单选
- return danxuan;
- case "多选": // 多选
- return duoxuanImg;
- case "判断": // 判断
- return panduanImg;
- default:
- return "";
- }
- };
-
- //返回答题首页
- const goBack = () => {
- router.push({
- path: "/dailyproblem"
- });
- };
-
- // 切换到下一题
- const nextQuestion = () => {
- if (activeIndex.value < questions.value.length - 1) {
- activeIndex.value++;
- }
- };
-
- // 切换到上一题
- const prevQuestion = () => {
- if (activeIndex.value > 0) {
- activeIndex.value--;
- }
- };
-
-
- const getUserAnswers = () => {
- let useranswers = [];
- questions.value.forEach((question) => {
- const userAnswer = userAnswers.value[question.id]; // 获取用户的答案
- let userAnswerString;
- if (Array.isArray(userAnswer)) {
- // 多选题,将数组转换为字符串
- userAnswerString = userAnswer.sort().join(""); // 排序并转换为字符串,如 "ABC"
- } else {
- // 单选题,直接是字符串
- userAnswerString = userAnswer || ""; // 如果未选择答案,则设为空字符串
- }
- // 将答案保存到 answers 数组中
- useranswers.push({
- id: question.id, // 题目 ID
- userAnswer: userAnswerString, // 用户的答案
- });
- });
- return useranswers;
- };
-
- //交卷
- const submitForm = async () => {
- overlayloading.value = true;
- try {
- let answers = getUserAnswers();
- //console.log('answers.value', answers)
- var url = '/sgsafe/ExamLine/appSaveMyScore'
- var param = {
- json: JSON.stringify(
- answers
- )
- }
- try {
- const res = await proxy.$axios.post(url, param);
- if (res.data.code === 0) {
- showSuccessToast("保存成功")
- } else {
- console.log('操作失败!' + res.data.msg);
- }
- } catch (error) {
- console.log('请求出错:', questions);
- }
- const courseId = handData.value.id;
- const today = new Date();
- const year = today.getFullYear();
- const month = String(today.getMonth() + 1).padStart(2, '0');
- const day = String(today.getDate()).padStart(2, '0');
- const todayStr = `${year}-${month}-${day}`;// ✅ 来自 saveChecUser 返回
- //开始判卷
- var url2 = '/sgsafe/Package/doProc'
- var param2 = {
- procName: 'safeplat.sxsp_grade_exam_p',
- param: JSON.stringify([courseId])
- }
- try {
- const res2 = await proxy.$axios.post(url2, param2);
- if (res2.data.code === 0) {
- consle.log("courseId:" + courseId + "判卷完成!")
- } else {
- console.log('操作失败!' + res.data.msg);
- }
- } catch (error) {
- console.log('请求出错:', questions);
- }
-
- overlayloading.value = false;
-
- //查询本日答题次数和分数
- var url3 = '/sgsafe/DailyExam/appQueryMyScore'
- const query3 = ref({
- userId: userId,
- examDate: todayStr,
- headId: courseId,
- })
- var param3 = {
- params: JSON.stringify(query3.value)
- }
- const res3 = await proxy.$axios.post(url3, param3);
- if (res3.data.code === 0) {
- console.log(res3.data)
- if (res3.data.data.dailyExamList.length==0){
- var url4='/sgsafe/ExamHead/queryByheadId'
- var param4 = {
- headId: courseId
- }
- const res4 = await proxy.$axios.post(url4, param4);
- if (res4.data.code === 0) {
- totalScore.value = res4.data.data.totalScore;
- }
- showConfirmDialog({
- message: "判卷完成",
- confirmButtonText: "查看本次答题结果",
- cancelButtonText: "退出答题"
- })
- .then(() => {
- showResult.value = true;
- })
- .catch(() => {
-
- router.replace({
- path: "/dailyproblem"
- });
- })
- }else {
- let times = res3.data.data.dailyExamList[0].examCounts;
- totalScore.value = res3.data.data.headScore;
- showConfirmDialog({
- message: "判卷完成",
- confirmButtonText: "查看本次答题结果",
- cancelButtonText: times == "3" ? "退出答题" : "继续答题",
- })
- .then(() => {
- showResult.value = true;
- })
- .catch(() => {
- if (times == "3") {
- router.replace({
- path: "/dailyproblem"
- });
- } else {
- router.push({
- path: "/dailyproblem"
- });
- }
- });
- }
-
-
- } else {
- console.log('操作失败!' + res3.data.msg);
- }
-
- } catch (error) {
- console.error("出错:", error);
- showFailToast("交卷失败");
- }
- };
-
- // 确认结果并返回
- const confirmResult = () => {
- showResult.value = false;
- // router.back();
- router.back()
- };
-
- // 检查是否所有题目都已作答
- const checkBeforeSubmit = () => {
- hasUnanswered.value = questions.value.some((question) => {
- const userAnswer = userAnswers.value[question.id];
- return (
- !userAnswer || (Array.isArray(userAnswer) && userAnswer.length === 0)
- );
- });
- confirmSubmitDialog.value = true;
- };
-
- // AI解析功能
- // AI解析相关变量
- const aiAnalysis = ref({}); // 存储每道题的AI解析内容
- const analysisLoading = ref({}); // 存储每道题的解析加载状态
- import { fetchHuaweiResponse } from "@/tools/deepseek.js";
- // 动态导入依赖
- let marked, DOMPurify;
-
- const initMarkdownLibs = async () => {
- try {
- // 尝试导入marked
- const markedModule = await import('marked');
- marked = markedModule.marked || markedModule.default || markedModule;
-
- // 尝试导入DOMPurify
- const dompurifyModule = await import('dompurify');
- DOMPurify = dompurifyModule.default || dompurifyModule;
- } catch (error) {
- console.warn('Markdown libraries not available, using plain text', error);
- // 如果导入失败,使用基础功能
- marked = {
- parse: (text) => text
- };
- DOMPurify = {
- sanitize: (html) => html
- };
- }
- };
-
- // 在组件挂载时初始化
- onMounted(() => {
- initMarkdownLibs();
- });
-
- // 生成AI解析
- const generateAIAnalysis = async (question, force = false) => {
- // 如果该题已有解析且不是强制重新生成,直接返回
- if (aiAnalysis.value[question.id] && !force) {
- return;
- }
-
- // 如果是重新解析,先清空之前的内容
- if (force) {
- aiAnalysis.value[question.id] = '';
- }
-
- // 确保依赖已加载
- if (!marked || !DOMPurify) {
- await initMarkdownLibs();
- }
-
- // 设置加载状态
- analysisLoading.value[question.id] = true;
-
- try {
- // 构造提示词
- let prompt = `请为以下题目提供详细解析:
- 题目类型:${question.category}题干:${question.stem}`;
-
- // 添加选项
- if (question.optionA) prompt += `\nA. ${question.optionA}`;
- if (question.optionB) prompt += `\nB. ${question.optionB}`;
- if (question.optionC) prompt += `\nC. ${question.optionC}`;
- if (question.optionD) prompt += `\nD. ${question.optionD}`;
- if (question.optionE) prompt += `\nE. ${question.optionE}`;
-
- prompt += `\n正确答案:${question.answer}`;
-
- // 添加用户答案(如果已作答)
- const userAnswer = userAnswers.value[question.id];
- if (userAnswer) {
- const userAnswerString = Array.isArray(userAnswer)
- ? userAnswer.sort().join("")
- : userAnswer;
- prompt += `\n用户答案:${userAnswerString}`;
- }
-
- prompt += `\n\n请提供以下内容:
- 1. 正确答案的解释
- 2. 为什么其他选项不正确(如果用户答案错误)
- 3. 相关知识点说明`;
-
- // 构造消息对象
- const messages = [
- {
- role: "user",
- content: prompt
- }
- ];
-
- // 调用AI接口
- fetchHuaweiResponse(
- messages,
- (content, isThinking, isEnd) => {
- // 实时更新解析内容
- aiAnalysis.value[question.id] = content;
-
- // 如果是最终结果,停止加载状态
- if (isEnd) {
- analysisLoading.value[question.id] = false;
- }
- },
- null
- );
- } catch (error) {
- console.error('AI解析生成失败:', error);
- analysisLoading.value[question.id] = false;
- aiAnalysis.value[question.id] = '解析生成失败';
- }
- };
-
- // 解析内容转换为HTML
- const renderAnalysis = (content) => {
- if (!content) return '';
-
- try {
- // 确保依赖已加载
- if (!marked || !DOMPurify) {
- return content.replace(/\n/g, '<br>');
- }
-
- const html = marked.parse ? marked.parse(content) : marked(content);
- return DOMPurify.sanitize ? DOMPurify.sanitize(html) : html;
- } catch (error) {
- console.error('Markdown解析错误:', error);
- return content.replace(/\n/g, '<br>');
- }
- };
-
-
-
-
- </script>
-
- <style scoped>
- .quiz-page {
- padding: 20px;
- }
-
- .question-type-img {
- width: 54px;
- height: 20px;
- }
-
- .question {
- margin-left: 20px;
- margin-right: 20px;
- }
-
- .kong {
- margin-bottom: 20px;
- }
-
- .footer {
- position: fixed;
- bottom: 0;
- left: 0;
- width: 100%;
- background-color: #fff;
- display: flex;
- justify-content: space-around;
- align-items: center;
- margin-bottom: 10px;
- }
-
- .van-dialog__message {
- text-align: center;
- }
-
- .questionBtn {
- width: 40%;
- }
-
- /* 遮罩 */
- .wrapper {
- display: flex;
- align-items: center;
- justify-content: center;
- height: 100%;
- }
-
- .van-overlay {
- z-index: 2;
- background-color: rgba(0, 0, 0, 0.5);
- }
-
- .ai-analysis-content {
- margin-top: 10px;
- padding: 10px;
- background-color: #f5f5f5;
- border-radius: 4px;
- font-size: 14px;
- line-height: 1.6;
- }
-
- .ai-analysis-content :deep(h1),
- .ai-analysis-content :deep(h2),
- .ai-analysis-content :deep(h3) {
- margin: 10px 0;
- font-weight: bold;
- font-size: 16px;
- }
-
- .ai-analysis-content :deep(p) {
- margin: 8px 0;
- }
-
- .ai-analysis-content :deep(ul),
- .ai-analysis-content :deep ol {
- padding-left: 20px;
- margin: 8px 0;
- }
-
- .ai-analysis-content :deep(li) {
- margin: 4px 0;
- }
-
- .ai-analysis-content :deep(code) {
- padding: 2px 4px;
- background-color: #e0e0e0;
- border-radius: 3px;
- font-family: monospace;
- }
-
- .ai-analysis-content :deep(pre) {
- padding: 10px;
- background-color: #e0e0e0;
- border-radius: 4px;
- overflow-x: auto;
- }
-
- </style>
|