Files
tra-app/src/pages/wrongQuestionRecord/correct.vue
T
2025-09-05 15:59:18 +08:00

767 lines
20 KiB
Vue

<template>
<view class="main_div max_page">
<nav-bar :is_seat="true"></nav-bar>
<view class="nav-div">
<view class="title ellipsis-text">
{{ paperData.name }}
</view>
<view class="back_div" @click="exit_practice">
<image :src="practiceExitIcon()" class="img"></image>
</view>
</view>
<view class="countdown-and-question-number-div">
<view class="top">
<view class="countdown">
<text class="grey">答题进度</text>
<text class="bold">{{ questionNumber + 1 }}</text>
<text class="grey">/{{ storageIdList.length }}</text>
</view>
<view class="question"></view>
</view>
<view class="progress">
<uv-line-progress
:percentage="progress"
:showText="false"
activeColor="#FFFFFF"
inactiveColor="#89B8FF"
:height="8"
></uv-line-progress>
</view>
</view>
<view class="scroll_div">
<swiper
class="swiper"
:indicator-dots="false"
@change="swiperChange"
:current="questionNumber"
easing-function="linear"
@transition="transition"
@animationfinish="animationfinish"
>
<swiper-item v-for="(item, index) in topicList">
<view class="content">
<view class="expose_shadow"></view>
<scroll-view :scroll-y="true" class="scroll-Y" :show-scrollbar="false" :refresher-threshold="0">
<view class="" v-if="item === '00'">加载中</view>
<view class="" v-if="item === '01'">加载失败</view>
<view class="" v-if="item === '02'">加载失败</view>
<Subject
v-else
:item="item"
:mode="mode"
:isPreview="item.isPreview"
:clearResult="item.clearResult"
@subject_click="subject_click"
v-model="item.my_answer"
></Subject>
</scroll-view>
</view>
</swiper-item>
</swiper>
</view>
<view class="fixed-box font_pf" :style="{ marginBottom: popup_input_bottom }">
<view class="touch-btn-div" :class="{ show: showTouchBtn }">
<TouchBtn
ref="touchBtnRef"
class="talk-btns"
@submit="touchBtnSubmit"
disabledText="只能进行一条回答"
:isDisabled="touchIsDisabled"
/>
</view>
</view>
<!-- 反馈组件 -->
<Feedback ref="feedbackRef"></Feedback>
<!-- 结尾弹出层 -->
<Dialog ref="dialogRef" @button_click="result_click"></Dialog>
</view>
</template>
<script setup>
import { ref, reactive, computed, nextTick } from 'vue';
import { onLoad, onHide, onShow, onUnload } from '@dcloudio/uni-app';
import Feedback from '@/pages/examination/components/feedback.vue';
import Dialog from '@/pages/examination/components/dialog.vue';
import Subject from '@/components/examination/subject.vue';
import TouchBtn from '@/components/examination/touchBtn.vue';
import common from '@/common/common';
import { getPageCache, setupKeyboardHeightListener } from '@/common/common';
import { commonUploadVoiceFile } from '@/api/common.js';
import { practiceExitIcon } from '@/common/imgSvg';
import { queryMistakesAnswerByMisId, mistakesCorrect, queryMistakesCorrectByMisId } from '@/api/wrongQuestionRecord.js';
const feedbackRef = ref(null);
const dialogRef = ref(null);
const touchBtnRef = ref(null);
const mode = ref('practice'); // 考试 examination 练习practice
const paperData = reactive({
name: '' // 名称
}); // 试卷信息
const topicList = reactive([]); // 问题列表
const storageIdList = ref([]); // 所有题ID列表
const storageIdDict = ref({}); // 所有题ID为键的字典
const popup_input_bottom = ref('0px');
const questionNumber = ref(0); // 题号
// 根据课程ID获取课程的题目 undefined为没有, 00为加载中, 02为加载失败
const topic = computed({
get: () => topicList[questionNumber.value],
set: (val) => (topicList[questionNumber.value] = val)
});
const showTouchBtn = computed(() => {
return topic.value?.qnsTyp === 'ES' && !topic.value?.my_answer.anserResult;
});
// 下面语音组件的禁用状态判断
const touchIsDisabled = computed(() => topic.value?.es_status !== '' && topic.value?.es_status !== undefined);
const progress = computed(() => {
// 计算进度百分比(保留两位小数)
const total = storageIdList.value.length;
if (total === 0) return 0;
const completed = questionNumber.value + 1;
return Math.min(Math.round((completed / total) * 10000) / 100, 100);
});
// 滑动切换
const swiperChange = (item) => {
questionNumber.value = item.detail.current;
};
const isTriggered = ref(false); // 下面这个得防抖
// 滑动发生改变
const finishCurrent = ref(questionNumber.value);
// 滑动组件结束事件
const animationfinish = (event) => {
finishCurrent.value = event.detail.current;
};
// 判断是否滑动结束
const isLastTopic = computed(
() => topicList.length === finishCurrent.value + 1 && finishCurrent.value === questionNumber.value
);
const transition = (event) => {
if (isTriggered.value) return;
const dx = event.detail.dx;
if (dx > 90 && dx < 250 && isLastTopic.value && ['P', 'U'].includes(topic.value.judgeResult)) {
// 如果是最后一题并且右滑动大于50,并且这题已经答了,就触发练习完成
isTriggered.value = true;
hand_in_paper_button_click();
}
};
const subject_click = (type, params) => {
if (type === 'feedback') {
// 反馈
feedbackRef.value.open(params.qnsId);
} else if (type === 'answer') {
//多选点击按钮答题
if (params.qnsTyp === 'MU') {
submitProblem({
anserResult: params.answer.join(',')
});
}
} else if (type === 'choose') {
//单选判断点击选项答题
if (['JD', 'SN'].includes(params.qnsTyp)) {
submitProblem({ anserResult: params.answer.join(',') });
}
} else if (type === 'complete') {
// 问答题点击完成
// ossAddr
// anserResult 作答结果
// ossKey 对象存储
submitProblem(params.answer);
} else if (type === 'again') {
// 问答题点击重答
topic.value.my_answer = {};
topic.value.isPreview = false;
topic.value.es_status = ''; // 取消转圈
}
};
// 单题回答
const submitProblem = (answer) => {
console.log('答题内容', answer);
console.log('答单题', topic);
common.loading('答题中');
mistakesCorrect({
misId: topic.value.misId,
ossKey: '',
...answer
})
.then(async (res) => {
let traQnsInfoVo;
// 是问答题
if (topic.value.qnsTyp === 'ES') {
try {
const result = await queryWithRetry(topic.value.misId);
console.log(result, 'result');
traQnsInfoVo = result.body.traQnsInfoVo;
} catch (e) {
common.hideLoading();
topic.value.isPreview = false;
return;
}
} else {
traQnsInfoVo = res.body;
}
topic.value.isPreview = true; // 不能在点击了,打开预览模式
common.hideLoading();
topic.value.clearResult = false; // 不清除正确答案
topic.value.analyContent = traQnsInfoVo.analyContent; // 答案解析
// 更新选项正确项目
topic.value.itemVos = traQnsInfoVo.itemVos; // 答案选项(todo应该不这样替换)
topic.value.anserResult = traQnsInfoVo.anserResult; // 正确答案字符串分割
topic.value.judgeResult = traQnsInfoVo.judgeResult; // 正确答案字符串分割
const judgeResultStatus = traQnsInfoVo.judgeResult === 'P'; // P为正确U为错误
// 加载下一题
if (!addProblem(questionNumber.value + 1)) {
// 返回false说明到了最后一题
console.log('最后一题', topic.value.qnsTyp, !judgeResultStatus);
if (topic.value.qnsTyp === 'ES' || !judgeResultStatus) {
// 最后一题是问答题,或者答错了,都不自动弹出
return;
} else {
await hand_in_paper_button_click();
}
} else {
addRequestProblem(questionNumber.value + 1);
// 自动切换下一题的条件
if (
judgeResultStatus && // 条件1.答题正确
topic.value.qnsTyp !== 'ES' // 问答题答对打答错都不切换
) {
nextTick(() => {
setTimeout(() => {
questionNumber.value++;
}, 200);
});
} else {
console.log('为什么不跳转2', judgeResultStatus);
console.log('为什么不跳转3', topic.value.qnsTyp !== 'ES');
}
}
})
.catch(() => {
common.hideLoading();
});
};
// 问答题轮询结果方法
const queryWithRetry = (execLogId, maxRetries = 10) => {
// 递归终止条件:达到最大重试次数
if (maxRetries <= 0) {
return Promise.reject(new Error('超过最大重试次数,查询失败'));
}
return queryMistakesCorrectByMisId({ misId:execLogId })
.then((res) => {
console.log('queryMistakesCorrectByMisId', maxRetries, res.body);
const isWait = res.body.isWait;
if (isWait === 'Y') {
// 如果是Y,延迟2秒后重试,重试次数减1
return new Promise((resolve) => {
setTimeout(() => {
resolve(queryWithRetry(execLogId, maxRetries - 1));
}, 2000);
});
} else {
// 不是Y,直接返回结果
return res;
}
})
.catch((error) => {
// 对接口调用错误进行重试
if (maxRetries > 0) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(queryWithRetry(execLogId, maxRetries - 1));
}, 2000);
});
}
return Promise.reject(error);
});
};
// 录音组件提交
const touchBtnSubmit = (type, submitObj) => {
touchBtnRef.value?.clearinputText(); // 清除发送框数据
if (type === 'voice') {
topic.value.es_status = '00'; // 00转圈
const voicePath = submitObj;
commonUploadVoiceFile(voicePath, '/traask/traAskchat/voiceTrans', {})
.then((res) => {
let _res = JSON.parse(res.data);
if (_res.rtnCode === '0000') {
const _content = _res.body.transContent.trim();
if (_content === '') {
topic.value.es_status = ''; // 取消转圈
return common.msg('未识别到文字');
} else {
topic.value.es_status = '02';
topic.value.my_answer = {
anserResult: _res.body.transContent,
ossKey: _res.body.fileId,
ossAddr: _res.body.ossFileAddr,
voicePath: voicePath
};
}
}
})
.catch((err) => {
topic.value.es_status = ''; // 取消转圈
common.msg('系统异常,请联系管理员');
});
} else if (type === 'text') {
topic.value.es_status = '01';
topic.value.my_answer = {
anserResult: submitObj // 正确答案文字内容
};
}
};
// 添加下一道题
const addProblem = async (index) => {
// index 要加载的题号
if (index < topicList.length) return true; // 说明此题已经加载
console.log('xxx', topicList.length, storageIdList.value.length);
if (topicList.length >= storageIdList.value.length) return false; // 说明已经加载所有题了
const id = storageIdList.value[index]; // 找到id
if (storageIdDict.value[id] === undefined) {
//尚未加载(几乎不可能)
return true;
} else if (storageIdDict.value[id].state === '00') {
//加载中
} else if (storageIdDict.value[id].state === '01') {
// 已经加载
// console.log('增加已经加载的数据', storageIdDict.value[id].topic);
topicList.push(storageIdDict.value[id].topic);
return true;
} else if (storageIdDict.value[id].state === '02') {
//加载失败
}
topicList.push(storageIdDict.value[id].state);
return true;
};
// 网络加载题,并且放在
const addRequestProblem = async (index, success = () => {}) => {
const id = storageIdList.value[index];
const preload = () => {
nextTick(() => {
// 最多只预加载当前题号的后两道题(questionNumber.value是当前题号)
// 修正索引判断:确保 index + 1 是有效的数组索引(< 数组长度)
if (index + 1 === storageIdList.value.length) return; // 防止下标越界
if (index + 1 > questionNumber.value + 2) return; // 防止加载太多
const last_id = storageIdList.value[index + 1];
// 判断是否加载
if (last_id in storageIdDict.value) {
return;
} else {
addRequestProblem(index + 1, success);
}
});
};
if (storageIdDict.value[id] && storageIdDict.value[id].state === '01') {
preload();
return;
}
storageIdDict.value[id] = { state: '00' };
queryMistakesAnswerByMisId({ misId: id })
.then(({ body }) => {
const topic_item = {
...body,
isPreview: false,
clearResult: true,
es_status: '', // 预设一下问答题的状态,'' 空为没有 00 转圈 01 播放,其他暂定
my_answer: []
};
storageIdDict.value[id] = {
state: '01',
topic: topic_item
};
success(topic_item);
preload();
})
.catch(() => {
storageIdDict.value[id] = { state: '02' };
})
.finally(() => {
// if (isLoading) common.hideLoading();
});
};
// 初始化时检查
onLoad(() => {
const correct = getPageCache('correct');
if (!correct) {
return common.navigateBack();
}
paperData.name = correct.name;
storageIdList.value = correct.list;
setupKeyboardHeightListener((height) => {
popup_input_bottom.value = `${height}px`;
});
setTimeout(() => {
addRequestProblem(0, () => {
addProblem(0);
});
}, 10);
});
const result_click = async ({ key }) => {
isTriggered.value = false; // 恢复
if (key === 'return') {
// 返回
dialogRef.value.close();
await nextTick();
common.navigateBack();
} else if (key === 'exit') {
// 中途退出
dialogRef.value.close();
await nextTick();
common.navigateBack();
} else if (key === 'revert') {
// 返回
dialogRef.value.close();
}
};
const style_button_1 = {
color: '#FFFFFF',
fontSize: '30rpx',
backgroundColor: '#0066FF',
borderRadius: '8rpx'
};
const style_button_2 = {
color: '#0066FF',
fontSize: '30rpx',
backgroundColor: '#E2EDFF',
borderRadius: '8rpx'
};
// 弹出框配置
const dialogConfiguration = reactive({});
// 交卷按钮点击,打开交卷确认框
const hand_in_paper_button_click = async () => {
console.log('交卷按钮');
const stat = 'P';
common.redirectTo(`/pages/wrongQuestionRecord/result?stat=${stat}`);
};
// 中途退出
const exit_practice = () => {
dialogRef.value.open({
title: '确认现在退出吗?',
buttonList: [
{
key: 'exit',
name: '退出',
style: style_button_1
},
{
key: 'revert',
name: '返回',
style: style_button_2
}
]
});
};
onShow(() => {});
onHide(() => {});
onUnload(() => {});
</script>
<style scoped lang="scss">
$bg-margin-left: 30rpx;
.popup_title_text {
color: red;
}
.popup {
//答题卡
z-index: 1800;
padding: 0 38rpx;
.sheet_popup_div {
width: 100%;
height: 100%;
background-color: #ffffff;
padding: 10rpx 34rpx;
display: flex;
flex-direction: column;
align-items: center;
border-radius: 16rpx 16rpx 0px 0px;
position: relative;
.title_div {
width: 100%;
text-align: center;
padding: 30rpx 0;
font-family: PingFangSC;
font-weight: 600;
font-size: 34rpx;
color: #000000;
text-align: center;
font-style: normal;
border-bottom: 2rpx solid #efefef;
}
.popup_back_div {
width: 60rpx;
height: 60rpx;
position: absolute;
top: 30rpx;
right: 30rpx;
display: flex;
align-items: center;
justify-content: center;
.img {
width: 30rpx;
height: 30rpx;
}
}
.sheet_table {
margin-top: 48rpx;
margin-bottom: 48rpx;
display: flex;
flex-wrap: wrap;
width: calc(100vw - 108rpx);
align-content: space-between;
.sheet_table_item {
width: calc((100vw - 108rpx) / 5 - 16rpx);
height: calc((100vw - 108rpx) / 5 - 16rpx);
margin: 8rpx;
border: 2rpx solid #e5e5e5;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: #999999;
}
.current {
background-color: rgba(0, 102, 255, 0.6);
border-color: #0066ff;
color: #ffffff;
}
.completed {
// 已完成
background-color: rgba(0, 102, 255, 0.1);
border-color: #0066ff;
color: #0066ff;
}
.mark,
.completed.mark {
background-color: rgba(232, 181, 49, 0.1);
color: #e8b531;
border-color: #e8b531;
}
.current.mark {
// 已完成
background-color: rgba(0, 102, 255, 0.6);
color: #ffffff;
border-color: #e8b531;
}
}
.sheet_button {
width: 100%;
}
}
}
.fixed-box {
position: fixed;
width: 100%;
left: 0;
bottom: 0;
border-radius: 16rpx 16rpx 0px 0px;
border: 2rpx solid #e5e5e5;
background: #ffffff;
}
.touch-btn-div {
height: 0;
overflow: hidden;
transition: height 0.2s ease-out;
}
.touch-btn-div.show {
height: 150rpx;
}
.fixed-btn-box {
height: 154rpx;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20rpx 20rpx 20rpx;
.sheet_button {
display: flex;
flex-direction: column;
align-items: center;
margin: 0 70rpx;
.img {
width: 38rpx;
height: 40rpx;
}
.sheet_text {
margin-top: 8rpx;
font-family: PingFangSC;
font-weight: 500;
font-size: 24rpx;
color: #000000;
}
}
.button_div {
flex: 1;
display: flex;
justify-content: space-between;
.button {
flex: 1;
}
}
}
.scroll-Y {
max-height: calc(100vh - var(--status-bar-height) - 70rpx - 70rpx - 40rpx - 64rpx - 68rpx - 60rpx);
min-height: calc(100vh - var(--status-bar-height) - 70rpx - 70rpx - 40rpx - 64rpx - 68rpx - 60rpx - 380rpx);
}
.swiper {
// 状态栏 var(--status-bar-height)
// 标题栏 70rpx
// 进度时间栏 70rpx
// 进度时间栏上下间距 40rpx = 20rpx + 20rpx
// 正文上下间距 64rpx
height: calc(100vh - var(--status-bar-height) - 70rpx - 70rpx - 40rpx - 64rpx);
// background-color: red;
}
.content {
position: relative;
margin: 32rpx $bg-margin-left;
border-radius: 16rpx;
padding: 34rpx 30rpx 34rpx 36rpx;
background-color: #fff;
.expose_shadow {
margin-left: 36rpx;
position: absolute;
bottom: -26rpx;
left: 0%;
width: calc(100% - 66rpx);
height: 28rpx;
background-color: #ffffff;
border-radius: 0 0 14rpx 14rpx;
opacity: 0.4;
z-index: 0;
// background-color: red;
}
}
.countdown-and-question-number-div {
margin: 20rpx $bg-margin-left;
height: 70rpx;
// background-color: red;
display: flex;
flex-direction: column;
justify-content: space-between;
z-index: 10;
.top {
display: flex;
align-items: center;
justify-content: space-between;
font-family: PingFangSC;
font-size: 26rpx;
color: #ffffff;
text-align: left;
font-style: normal;
}
.question {
display: flex;
align-items: center;
// background-color: red;
width: 242rpx;
.grey {
margin-right: 8rpx;
}
color: #ffffff;
font-weight: 600;
font-size: 26rpx;
}
.grey {
color: #b6e2ff;
}
.bold {
font-weight: 600;
}
}
.nav-div {
position: relative;
.title {
// padding-top:var(--status-bar-height);
// padding: 0 30rpx;
margin: 0 calc($bg-margin-left + 70rpx);
height: 70rpx;
font-family: PingFangSC;
font-weight: 500;
font-size: 34rpx;
color: #ffffff;
text-align: center;
font-style: normal;
line-height: 70rpx;
}
.back_div {
position: absolute;
right: $bg-margin-left;
top: 0;
width: 70rpx;
height: 70rpx;
display: flex;
align-items: center;
justify-content: flex-end;
.img {
width: 32rpx;
height: 32rpx;
}
// background-color: red;
}
}
.main_div {
display: flex;
flex-direction: column;
background: linear-gradient(to top, #3480ff, #0066ff);
/* 确保背景占满整个视口高度 */
min-height: 100vh;
margin: 0;
}
</style>