Files
tra-app/src/pages/examination/index.vue
T
2025-08-28 16:54:52 +08:00

751 lines
19 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" @click="hand_in_paper_button_click()">
{{ paperData.papersName }}
</view>
<view class="back_div"></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">/{{ topicList.length }}</text>
</view>
<view class="question">
<view class="grey">剩余时间</view>
<uv-count-down :time="paperData.limitTm * 1000" format="HH:mm:ss"></uv-count-down>
</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"
>
<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">
<Subject
:item="item"
:mode="mode"
@subject_click="subject_click"
:clearResult="true"
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="fixed-btn-box">
<view class="sheet_button" @click="openSheet(true)">
<image :src="examinationSheetIcon()" class="img"></image>
<view class="sheet_text">答题卡</view>
</view>
<view class="button_div">
<uv-button
class="button"
type="primary"
:throttleTime="100"
text="上一题"
color="#E2EDFF"
custom-style="color:#0066FF;borderRadius:16rpx;height:92rpx;"
@click="button_tab(-1)"
></uv-button>
<view style="width: 46rpx"></view>
<uv-button
class="button"
type="primary"
:throttleTime="100"
:text="next_question"
color="#0066FF"
custom-style="color:#FFFFFF;borderRadius:16rpx;height:92rpx;"
@click="button_tab(1)"
></uv-button>
</view>
</view>
<view class="touch-btn-div" :class="{ show: showTouchBtn }">
<TouchBtn
class="talk-btns"
ref="touchBtnRef"
@submit="touchBtnSubmit"
loadingText="问题回答中,请在问题回答结束后重试!"
:isLoading="!answerIsOver"
:isDisabled="touchIsDisabled"
/>
</view>
</view>
<uni-popup ref="popupRef" class="popup">
<view class="sheet_popup_div">
<view class="title_div">
<view class="text">答题卡</view>
<view class="popup_back_div" @click="openSheet(false)">
<image :src="optionErrorIcon('#000000')" class="img"></image>
</view>
</view>
<view class="sheet_table">
<view
class="sheet_table_item"
v-for="(item, index) in topicList"
:class="{
current: index === questionNumber,
mark: markList.includes(item.qnsId),
completed: completedListStatus[index]
}"
@click="clikc_sheet_item(index)"
>
<view class="circle">
{{ index + 1 }}
</view>
</view>
</view>
<view class="sheet_button">
<uv-button
class="button"
type="primary"
:throttleTime="100"
text="交卷"
color="#0066FF"
custom-style="color:#FFFFFF;borderRadius:16rpx;height:92rpx;"
@click="hand_in_paper_button_click()"
></uv-button>
</view>
</view>
</uni-popup>
<!-- 反馈组件 -->
<Feedback ref="feedbackRef"></Feedback>
<!-- 结尾弹出层 -->
<Dialog ref="dialogRef" :dialogConfiguration="dialogConfiguration" @button_click="result_click"></Dialog>
</view>
</template>
<script setup>
import Feedback from './components/feedback.vue';
import Dialog from './components/dialog.vue';
import Subject from '@/components/examination/subject.vue';
import CharactersSubject from '@/components/examination/characters-subject.vue';
import TouchBtn from '@/components/examination/touchBtn.vue';
import { ref, reactive, computed, onMounted, nextTick } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import common from '@/common/common';
import { getPageCache } from '@/common/common';
import { commonUploadVoiceFile } from '@/api/common.js';
import { optionErrorIcon, examinationSheetIcon } from '@/common/imgSvg';
import { commitPaperExam, queryPaperExamScore, startExamByCrs, commitCrsExam } from '@/api/examination.js';
const answerIsOver = ref(true);
const feedbackRef = ref(null);
const dialogRef = ref(null);
const popupRef = ref(null);
const touchBtnRef = ref(null);
let examinationStartTime = 0; // 考试开始时间
const mode = ref('examination'); // 考试 examination 练习practice
const paperData = reactive({
crsId: '', // 课程ID,通过课程id是否为空字符串判断这个试卷是课程考试还是考试中心考试
execId: '', // 执行ID
examId: '', // 考试ID
papersId: '', // 试卷ID
limitTm: 0, // 考试限制时间
papersName: '' // 试卷名称
}); // 试卷信息
const topicList = reactive([]); // 问题列表
const popup_input_bottom = ref('0px');
const questionNumber = ref(0); // 题号
const systemInfo = uni.getSystemInfoSync();
const topic = computed({
get: () => topicList[questionNumber.value],
set: (val) => (topicList[questionNumber.value] = val)
});
const showTouchBtn = computed(() => {
return topicList[questionNumber.value]['qnsTyp'] === 'ES';
});
// 答题卡判断每道题的状态
const completedListStatus = computed(() => {
return topicList.map((res) => {
// 问答题
if (res.qnsTyp === 'ES') {
return !!res.my_answer.anserResult;
} else if (typeof res.my_answer === 'string') {
return res.my_answer !== '';
} else if (Array.isArray(res.my_answer)) {
return res.my_answer.length !== 0;
}
});
});
// 下面语音组件的禁用状态判断
const touchIsDisabled = computed(() => {
return topic.value?.es_status !== '' && topic.value?.es_status !== undefined;
});
const markList = computed(() => topicList.filter((res) => !!res.mark).map((res) => res.qnsId)); // 标记列表
// 下一题按钮状态
const next_question = computed(() => (topicList.length === questionNumber.value + 1 ? '交卷' : '下一题'));
const progress = computed(() => {
// 计算进度百分比(保留两位小数)
const total = topicList.length;
if (total === 0) return 0;
const completed = questionNumber.value + 1;
return Math.min(Math.round((completed / total) * 10000) / 100, 100);
});
// 弹出框配置
const dialogConfiguration = reactive({});
const activeVoiceId = ref('');
const swiperChange = (item) => {
questionNumber.value = item.detail.current;
};
// 按上一题下一题
const button_tab = (num) => {
let newNumber = questionNumber.value + num;
if (newNumber < 0) {
newNumber = 0;
} else if (newNumber >= topicList.length) {
newNumber = topicList.length - 1;
hand_in_paper_button_click(); // 调用交卷
}
questionNumber.value = newNumber;
};
// 打开或者关闭弹出层
const openSheet = (status) => (status ? popupRef.value.open('bottom') : popupRef.value.close());
// 点击答题卡按钮
const clikc_sheet_item = (index) => {
questionNumber.value = index;
};
const subject_click = (type, params) => {
if (type === 'mark') {
topicList[questionNumber.value]['mark'] = !topicList[questionNumber.value]['mark'];
} else if (type === 'feedback') {
console.log('feedbackRef.value', feedbackRef.value);
feedbackRef.value.open(params.qnsId);
}
};
const voicePathValue = ref('');
// 录音按钮提交
const touchBtnSubmit = (type, submitObj) => {
const old_text = 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') {
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 = ''; // 取消转圈
// 失败了再把发送框数据还原回去
touchBtnRef.value?.clearinputText(old_text); // 清除发送框数据
common.msg('系统异常,请联系管理员');
});
} else if (type === 'text') {
topic.value.es_status = '01';
topic.value.anserResult = submitObj; // 正确答案文字内容
}
};
// 初始化时检查
onLoad(() => {
const examination = getPageCache('examination');
if (!examination) {
common.navigateBack();
return;
}
// 记录起始时间
examinationStartTime = new Date().getTime();
paperData.crsId = examination.crsId;
paperData.execId = examination.execId;
paperData.examId = examination.examId;
paperData.papersId = examination.papersId;
paperData.limitTm = examination.limitTm;
paperData.papersName = examination.papersName;
topicList.push(
...examination.qnsInfoVos.map((res) => ({
...res,
mark: false,
my_answer: []
}))
);
// #ifdef APP-PLUS
// 安全区域底部到屏幕底部的高度(即底部安全区域高度)
const safeAreaBottomHeight = systemInfo.screenHeight - systemInfo.safeArea.bottom;
uni.onKeyboardHeightChange((res) => {
let keyboardHeight = res.height;
// 仅在iOS端补偿安全区域高度
if (systemInfo.platform === 'ios') {
keyboardHeight = Math.max(0, keyboardHeight - safeAreaBottomHeight); // 避免负数
}
popup_input_bottom.value = `${keyboardHeight}px`;
console.log('端补偿安全区域高度', popup_input_bottom.value);
});
// #endif
});
const result_click = ({ key }) => {
console.log('res', key);
if (key === 'hand_in_paper') {
// 现在交卷
hand_in_paper();
} else if (key === 'continue') {
// 继续考试
dialogRef.value.close();
}
};
// 交卷按钮点击,打开交卷确认框
const hand_in_paper_button_click = () => {
console.log('点击交卷', paperData);
// 判断是否完成所有题目
const completeList = topicList
.filter((res) => !!(Array.isArray(res.my_answer) ? res.my_answer.length : res.my_answer))
.map((res) => ({
a: res.qnsId,
b: res.my_answer,
c: !!res.my_answer
}));
dialogConfiguration.title = '确认现在交卷吗?';
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 incompleteNumber = topicList.length - completeList.length;
if (incompleteNumber === 0) {
// 完成
dialogConfiguration.type = 'complete';
dialogConfiguration.title = '确认现在交卷吗?';
dialogConfiguration.buttonList = [
{
key: 'hand_in_paper',
name: '现在交卷',
style: style_button_1
},
{
key: 'continue',
name: '继续考试',
style: style_button_2
}
];
} else {
dialogConfiguration.type = 'incomplete'; // 未完成
dialogConfiguration.html = `当前考试进度:<text style="color:#0066FF;font-weight:bold;">${completeList.length}/${topicList.length}</text> 题 还有<text style="color:#0066FF;font-weight:bold;">${incompleteNumber}</text>道题未作答,确认交卷吗?`;
dialogConfiguration.buttonList = [
{
key: 'continue',
name: '继续考试',
style: style_button_1
},
{
key: 'hand_in_paper',
name: '现在交卷',
style: style_button_2
}
];
}
dialogRef.value.open();
};
// 执行交卷
const hand_in_paper = () => {
const answerDtoList = topicList.map((res) => {
let answerItem = {
examId: paperData.examId,
papersId: paperData.papersId,
qnsId: res.qnsId,
ossKey: '',
anserResult: ''
};
if (res.qnsTyp === 'ES') {
// 问答题特殊处理
return {
...answerItem,
anserResult: ''
};
} else {
//其他题
return {
...answerItem,
anserResult: res.my_answer.join(',')
};
}
});
console.log('answerDtoList', answerDtoList);
dialogRef.value.close();
common.loading('正在提交');
// 这里通过是否存在examId来判断是考试中心的考试还是课程的考试
let func;
if (paperData.examId) {
// 这里是考试中心考试
func = commitPaperExam({
papersId: paperData.papersId,
examId: paperData.examId,
execId: paperData.execId,
examLenTm: 1200,
answerDtoStr: JSON.stringify(answerDtoList)
});
} else {
func = commitCrsExam({
papersId: paperData.papersId,
execId: paperData.execId,
examLenTm: 1200,
answerDtoStr: JSON.stringify(answerDtoList)
});
}
func.then((res) => {
console.log('提交成功', res);
// 判断是否需要查分
if (res.body.flagQuery === 'Y') {
// 需要查分
const examLenTm = 678;
common.redirectTo(
`/pages/examination/result?execId=${res.body.execId}&examLenTm=${examLenTm}&papersName=${paperData.papersName}&papersId=${paperData.papersId}&mode=${mode.value}&crsId=${paperData.crsId}`
);
} else {
// todo 此处应该有个弹窗交代下结果
common.navigateBack();
}
})
.catch((res) => {
console.log('error', res);
})
.finally(() => {
common.hideLoading().body;
});
};
</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);
// background-color: yellow;
}
.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;
color: red;
.grey {
margin-right: 8rpx;
}
:deep(.uv-count-down__text) {
color: #ffffff !important;
font-weight: 600;
font-size: 26rpx !important;
}
}
.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;
// background-color: red;
}
}
.main_div {
display: flex;
flex-direction: column;
background: linear-gradient(to top, #3480ff, #0066ff);
/* 确保背景占满整个视口高度 */
min-height: 100vh;
margin: 0;
}
</style>