|
|
|
@ -5,15 +5,34 @@
|
|
|
|
|
<h2>{{ examTitle }}</h2>
|
|
|
|
|
<p>总分:{{ totalScore }} 时长:{{ duration }}</p>
|
|
|
|
|
<p class="time-remaining">剩余时间<br /><span>{{ formattedTime }}</span></p>
|
|
|
|
|
<button class="submit-button" @click="submitExam">提交试卷</button>
|
|
|
|
|
<button class="submit-button" @click="submitExam" :disabled="loading">提交试卷</button>
|
|
|
|
|
<div class="question-navigation">
|
|
|
|
|
<!-- Question navigation with clickable boxes -->
|
|
|
|
|
<div
|
|
|
|
|
v-for="(question, index) in questions"
|
|
|
|
|
:key="question.id"
|
|
|
|
|
class="question-box"
|
|
|
|
|
:class="{ answered: answers[question.id] }"
|
|
|
|
|
@click="scrollToQuestion(index)"
|
|
|
|
|
>
|
|
|
|
|
{{ index + 1 }}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 试题内容 -->
|
|
|
|
|
<div class="main-content" v-if="questions.length > 0">
|
|
|
|
|
<div v-for="(question, index) in questions" :key="question.id" class="question">
|
|
|
|
|
<p>{{ index + 1 }}. {{ question.text }}</p>
|
|
|
|
|
<!-- 主内容 -->
|
|
|
|
|
<div class="main-content" v-if="!loading && questions.length > 0">
|
|
|
|
|
<div
|
|
|
|
|
class="question"
|
|
|
|
|
v-for="(question, index) in questions"
|
|
|
|
|
:key="question.id"
|
|
|
|
|
:ref="'question-' + question.id"
|
|
|
|
|
>
|
|
|
|
|
<p>{{ index + 1 }}. {{ question.text }}
|
|
|
|
|
<span class="score" v-if="question.score">({{ question.score }} 分)</span>
|
|
|
|
|
</p>
|
|
|
|
|
<div class="options">
|
|
|
|
|
<!-- 有选项时 -->
|
|
|
|
|
<!-- 有选项 -->
|
|
|
|
|
<template v-if="question.options.length > 0">
|
|
|
|
|
<label v-for="(option, optIndex) in question.options" :key="optIndex">
|
|
|
|
|
<input
|
|
|
|
@ -25,7 +44,7 @@
|
|
|
|
|
{{ option.label }}
|
|
|
|
|
</label>
|
|
|
|
|
</template>
|
|
|
|
|
<!-- 无选项时 -->
|
|
|
|
|
<!-- 无选项 -->
|
|
|
|
|
<template v-else>
|
|
|
|
|
<textarea
|
|
|
|
|
v-model="answers[question.id]"
|
|
|
|
@ -38,7 +57,7 @@
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 加载中提示 -->
|
|
|
|
|
<div class="loading" v-else>
|
|
|
|
|
<div class="loading" v-if="loading">
|
|
|
|
|
正在加载试题,请稍候...
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
@ -54,18 +73,30 @@ export default {
|
|
|
|
|
examTitle: "考试中...", // 考试标题
|
|
|
|
|
totalScore: 0, // 总分
|
|
|
|
|
duration: "未知", // 考试时长
|
|
|
|
|
remainingTime: 0, // 剩余时间(秒)
|
|
|
|
|
remainingTime: 5*60, // 剩余时间(秒)
|
|
|
|
|
questions: [], // 存储从后端获取的试题数据
|
|
|
|
|
answers: {}, // 存储用户选择的答案
|
|
|
|
|
loading: true, // 加载状态标志
|
|
|
|
|
examId: null, // 从路由获取的试卷 ID
|
|
|
|
|
timer: null, // 倒计时定时器
|
|
|
|
|
refreshTimer: null, // 页面刷新定时器
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
computed: {
|
|
|
|
|
// 格式化剩余时间
|
|
|
|
|
formattedTime() {
|
|
|
|
|
const minutes = Math.floor(this.remainingTime / 60);
|
|
|
|
|
const seconds = this.remainingTime % 60;
|
|
|
|
|
const minutes = Math.floor(this.remainingTime / 60); // 计算分钟
|
|
|
|
|
const seconds = this.remainingTime % 60; // 计算剩余秒数
|
|
|
|
|
return `${minutes}分${seconds}秒`;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
watch: {
|
|
|
|
|
questions(newVal) {
|
|
|
|
|
if (newVal.length > 0) {
|
|
|
|
|
// 确保只有在数据加载后启动倒计时
|
|
|
|
|
this.startCountdown();
|
|
|
|
|
if (this.refreshTimer) clearTimeout(this.refreshTimer); // 清除刷新计时器
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
methods: {
|
|
|
|
@ -80,72 +111,123 @@ export default {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const res = await axios.get('http://localhost:8080/student/examPaper/selectByUserId', {
|
|
|
|
|
headers: { Authorization: `Bearer ${token}`,
|
|
|
|
|
'Content-Type': 'application/json',}
|
|
|
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
|
|
|
});
|
|
|
|
|
// 打印返回的数据,确保 res.data 和 res.data.data 存在
|
|
|
|
|
console.log('请求返回的数据:', res.data);
|
|
|
|
|
|
|
|
|
|
// 确保 res.data 和 res.data.data 存在且有 grade 属性
|
|
|
|
|
if (res.data && res.data.data && res.data.data.grade) {
|
|
|
|
|
const grade = res.data.data.grade;
|
|
|
|
|
console.log('学生年级:', grade);
|
|
|
|
|
} else {
|
|
|
|
|
console.error("返回数据缺少 grade 字段!", res.data);
|
|
|
|
|
alert("返回数据缺少年级信息!");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const examRes = await axios.get(`http://localhost:8080/student/homepage/task_paper`, {
|
|
|
|
|
params: { name: this.name, subject: this.subject,grade: res.data.data.grade},
|
|
|
|
|
headers: { Authorization: `Bearer ${token}`,
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log('examRes', examRes);
|
|
|
|
|
|
|
|
|
|
if (examRes.data.code === 200) {
|
|
|
|
|
const { name, grade, subject, list } = examRes.data.data;
|
|
|
|
|
this.examTitle = name;
|
|
|
|
|
this.totalScore = list.length * 10; // 假设每题10分
|
|
|
|
|
this.duration = "60分钟"; // 假设固定时长
|
|
|
|
|
this.remainingTime = 3600; // 假设默认1小时
|
|
|
|
|
this.questions = list.map(item => ({
|
|
|
|
|
id: item.id,
|
|
|
|
|
text: item.content,
|
|
|
|
|
options: item.chance ? item.chance.map(opt => ({ value: opt.label, label: opt.text })) : [],
|
|
|
|
|
}));
|
|
|
|
|
console.log("试题数据:", this.questions);
|
|
|
|
|
const examRes = await axios.get(`http://localhost:8080/student/homepage/task_paper`, {
|
|
|
|
|
params: { name: this.name, subject: this.subject, grade },
|
|
|
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (examRes.data.code === 200) {
|
|
|
|
|
const { name, list } = examRes.data.data;
|
|
|
|
|
this.examTitle = name;
|
|
|
|
|
this.totalScore = examRes.data.data.sumscore;
|
|
|
|
|
this.duration = this.time;
|
|
|
|
|
this.remainingTime = this.time * 60;
|
|
|
|
|
this.questions = list.map((item) => ({
|
|
|
|
|
id: item.id,
|
|
|
|
|
text: item.content,
|
|
|
|
|
options: item.chance
|
|
|
|
|
? item.chance.map((opt) => ({ value: opt.label, label: opt.text }))
|
|
|
|
|
: [],
|
|
|
|
|
score: item.score,
|
|
|
|
|
}));
|
|
|
|
|
} else {
|
|
|
|
|
alert("加载试题失败:" + examRes.data.msg);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
alert("加载试题失败:" + examRes.data.msg);
|
|
|
|
|
alert("返回数据缺少年级信息!");
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("加载试题时发生错误:"+ error);
|
|
|
|
|
console.error("加载试题时发生错误:", error);
|
|
|
|
|
alert("加载试题失败,请稍后再试!");
|
|
|
|
|
} finally {
|
|
|
|
|
this.loading = false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
scrollToQuestion(index) {
|
|
|
|
|
const questionId = this.questions[index].id;
|
|
|
|
|
let questionElement = this.$refs[`question-${questionId}`];
|
|
|
|
|
|
|
|
|
|
// 检查 questionElement 的类型
|
|
|
|
|
if (Array.isArray(questionElement)) {
|
|
|
|
|
// 如果是数组,取第一个元素
|
|
|
|
|
console.log(`Element found as array for question ID: ${questionId}, Element:`, questionElement[0]);
|
|
|
|
|
questionElement = questionElement[0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 添加调试信息
|
|
|
|
|
console.log(`Trying to scroll to question ID: ${questionId}, Element found:`, questionElement);
|
|
|
|
|
|
|
|
|
|
if (questionElement && questionElement.scrollIntoView) {
|
|
|
|
|
questionElement.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
|
|
|
} else {
|
|
|
|
|
console.warn(`Element for question ${index + 1} not found or invalid.`);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 启动页面刷新计时器
|
|
|
|
|
startRefreshTimer() {
|
|
|
|
|
this.refreshTimer = setTimeout(() => {
|
|
|
|
|
if (!this.questions.length) {
|
|
|
|
|
alert("加载超时,页面将刷新以重试...");
|
|
|
|
|
location.reload(); // 刷新页面
|
|
|
|
|
}
|
|
|
|
|
}, 10000); // 10 秒超时
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 提交试卷
|
|
|
|
|
async submitExam() {
|
|
|
|
|
try {
|
|
|
|
|
// 检查未作答的题目
|
|
|
|
|
const unanswered = this.questions.filter(
|
|
|
|
|
(q) => !this.answers[q.id] || this.answers[q.id].trim() === ""
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (unanswered.length > 0) {
|
|
|
|
|
alert(`还有 ${unanswered.length} 道题未作答,请完成后再提交!`);
|
|
|
|
|
const confirmSubmit = confirm(`还有 ${unanswered.length} 道题未作答,是否仍然提交试卷?`);
|
|
|
|
|
if (!confirmSubmit) {
|
|
|
|
|
return; // 用户选择不提交
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('答案', this.answers);
|
|
|
|
|
|
|
|
|
|
// 计算时间
|
|
|
|
|
const remainingTime = Math.round(Math.max(this.duration*60 - this.remainingTime, 0) / 60);
|
|
|
|
|
|
|
|
|
|
const token = this.$store.state.token;
|
|
|
|
|
if (!token) {
|
|
|
|
|
alert("用户未登录,请重新登录!");
|
|
|
|
|
this.$router.push("/login");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 提交答案
|
|
|
|
|
const response = await axios.post("http://localhost:8080/api/exam/submit", {
|
|
|
|
|
examId: this.examId,
|
|
|
|
|
answers: this.answers,
|
|
|
|
|
// 将 this.answers 转换为符合 AnswerVo 格式的列表
|
|
|
|
|
const answerList = Object.keys(this.answers).map((questionId) => {
|
|
|
|
|
return {
|
|
|
|
|
id: Number(questionId), // 确保 id 是数字
|
|
|
|
|
answer: this.answers[questionId].trim() // 确保答案是字符串
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const response = await axios.post("http://localhost:8080/student/homepage/task_answer", {
|
|
|
|
|
testid: this.examId, // testid
|
|
|
|
|
list: answerList, // list 是 AnswerVo 对象的数组
|
|
|
|
|
time: remainingTime // time
|
|
|
|
|
}, {
|
|
|
|
|
headers: {
|
|
|
|
|
Authorization: `Bearer ${token}`
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (response.data.code === 200) {
|
|
|
|
|
alert("考试提交成功!");
|
|
|
|
|
this.$router.push("/examList");
|
|
|
|
|
this.$router.push("/student/exam");
|
|
|
|
|
} else {
|
|
|
|
|
alert("提交失败:" + response.data.msg);
|
|
|
|
|
}
|
|
|
|
@ -157,48 +239,48 @@ export default {
|
|
|
|
|
|
|
|
|
|
// 倒计时逻辑
|
|
|
|
|
startCountdown() {
|
|
|
|
|
if (this.timer) clearInterval(this.timer);
|
|
|
|
|
this.timer = setInterval(() => {
|
|
|
|
|
if (this.remainingTime > 0) {
|
|
|
|
|
this.remainingTime -= 1;
|
|
|
|
|
} else {
|
|
|
|
|
if (this.remainingTime <= 0) {
|
|
|
|
|
clearInterval(this.timer);
|
|
|
|
|
alert("时间到,试卷将自动提交!");
|
|
|
|
|
this.submitExam();
|
|
|
|
|
alert("时间到!");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}, 1000);
|
|
|
|
|
|
|
|
|
|
this.remainingTime--; // 每秒减少
|
|
|
|
|
}, 1000); // 每秒减少一次
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
created() {
|
|
|
|
|
this.subject = this.$route.query.subject;
|
|
|
|
|
this.name = this.$route.query.name;
|
|
|
|
|
this.examId = this.$route.query.id;
|
|
|
|
|
this.time = this.$route.query.time;
|
|
|
|
|
|
|
|
|
|
if (!this.subject && !this.name) {
|
|
|
|
|
alert("未获取到试卷,请重新选择试卷!");
|
|
|
|
|
if (!this.subject || !this.name) {
|
|
|
|
|
alert("未获取到试卷,请重新选择!");
|
|
|
|
|
this.$router.push("/exam");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取试题数据并启动倒计时
|
|
|
|
|
this.fetchQuestions();
|
|
|
|
|
this.startRefreshTimer();
|
|
|
|
|
},
|
|
|
|
|
mounted() {
|
|
|
|
|
this.startCountdown();
|
|
|
|
|
beforeDestroy() {
|
|
|
|
|
if (this.timer) clearInterval(this.timer);
|
|
|
|
|
if (this.refreshTimer) clearTimeout(this.refreshTimer);
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
/* 页面布局 */
|
|
|
|
|
.exam-container {
|
|
|
|
|
display: flex;
|
|
|
|
|
height: 100vh;
|
|
|
|
|
background-color: #f4f6f9;
|
|
|
|
|
padding: 0; /* 确保没有额外的内边距 */
|
|
|
|
|
padding: 0;
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 左侧栏样式 */
|
|
|
|
|
.sidebar {
|
|
|
|
|
width: 20%;
|
|
|
|
|
padding: 20px;
|
|
|
|
@ -206,11 +288,10 @@ export default {
|
|
|
|
|
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
|
|
|
|
|
text-align: center;
|
|
|
|
|
position: sticky;
|
|
|
|
|
top: 0; /* 固定位置 */
|
|
|
|
|
height: 100vh; /* 确保左侧栏一直显示 */
|
|
|
|
|
top: 0;
|
|
|
|
|
height: 100vh;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 使左侧栏文字不会被遮挡 */
|
|
|
|
|
.sidebar h2 {
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
@ -234,35 +315,19 @@ export default {
|
|
|
|
|
background-color: #0056b3;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 主内容区域 */
|
|
|
|
|
.main-content {
|
|
|
|
|
flex: 1;
|
|
|
|
|
padding: 30px;
|
|
|
|
|
background-color: #ffffff; /* 设置和左侧栏一致的背景 */
|
|
|
|
|
overflow-y: auto; /* 允许滚动 */
|
|
|
|
|
height: 100vh; /* 确保主内容区域可以填满视口高度 */
|
|
|
|
|
box-sizing: border-box; /* 包括内边距在内的大小计算 */
|
|
|
|
|
background-color: #ffffff;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
height: 100vh;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 题目区域 */
|
|
|
|
|
.question {
|
|
|
|
|
margin-bottom: 20px; /* 保证每道题目之间有间距 */
|
|
|
|
|
padding-bottom: 10px; /* 让横线不与题目文字紧贴 */
|
|
|
|
|
border-bottom: 1px solid #e0e0e0; /* 设置浅色横线 */
|
|
|
|
|
}
|
|
|
|
|
.question p {
|
|
|
|
|
font-size: 2rem; /* 默认字体大小 */
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* 选项样式 */
|
|
|
|
|
.options label {
|
|
|
|
|
font-size: 1.2rem; /* 默认字体大小 */
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
padding-bottom: 10px;
|
|
|
|
|
border-bottom: 1px solid #e0e0e0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.options input[type="radio"] {
|
|
|
|
@ -274,18 +339,65 @@ textarea {
|
|
|
|
|
border: 1px solid #ccc;
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
padding: 10px;
|
|
|
|
|
font-size: 1.2rem; /* 调整字体大小 */
|
|
|
|
|
line-height: 1.5; /* 调整行高 */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.question p {
|
|
|
|
|
font-size: 2.2rem; /* 调整题目字体大小 */
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 加载中样式 */
|
|
|
|
|
.options label {
|
|
|
|
|
font-size: 1.4rem; /* 调整选项字体大小 */
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.loading {
|
|
|
|
|
text-align: center;
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
color: #666;
|
|
|
|
|
margin-top: 50px;
|
|
|
|
|
}
|
|
|
|
|
/* 页面根元素字体大小 */
|
|
|
|
|
html {
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
|
|
|
|
.score {
|
|
|
|
|
font-size: 1.4rem;
|
|
|
|
|
color: #999;
|
|
|
|
|
margin-left: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Sidebar question navigation */
|
|
|
|
|
.question-navigation {
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.question-box {
|
|
|
|
|
width: 40px;
|
|
|
|
|
height: 40px;
|
|
|
|
|
margin: 5px;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
align-items: center;
|
|
|
|
|
background-color: #f0f0f0;
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-size: 1.2rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.question-box:hover {
|
|
|
|
|
background-color: #007bff;
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|
|
|
|
|
.question-box.answered {
|
|
|
|
|
background-color: red; /* 已做题目的背景色 */
|
|
|
|
|
color: white; /* 文字颜色 */
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|