|
|
|
@ -0,0 +1,481 @@
|
|
|
|
|
<template>
|
|
|
|
|
<div class="exam-container">
|
|
|
|
|
<!-- 左侧栏 -->
|
|
|
|
|
<div class="sidebar">
|
|
|
|
|
<!-- 返回按钮 -->
|
|
|
|
|
<div class="back-button">
|
|
|
|
|
<button @click="goBack">
|
|
|
|
|
<span>返回</span>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<h2>{{ examTitle }}</h2>
|
|
|
|
|
<p>答题人:{{ sender }}</p>
|
|
|
|
|
<p>得分:{{ currentScore }} 总分:{{ totalScore }}</p>
|
|
|
|
|
<p>已答对:{{ correctQuestions }} 总题数:{{ totalQuestions }}</p>
|
|
|
|
|
<div class="question-navigation">
|
|
|
|
|
<div
|
|
|
|
|
v-for="(question, index) in questions"
|
|
|
|
|
:key="question.id"
|
|
|
|
|
class="question-box"
|
|
|
|
|
:class="{
|
|
|
|
|
answered: answers[question.id]?.length > 0,
|
|
|
|
|
active: currentQuestionIndex === index,
|
|
|
|
|
correct: question.tf === 'true',
|
|
|
|
|
incorrect: question.tf === 'false',
|
|
|
|
|
// completed: question.tf !== null
|
|
|
|
|
}"
|
|
|
|
|
@click="scrollToQuestion(index)"
|
|
|
|
|
>
|
|
|
|
|
{{ index + 1 }}
|
|
|
|
|
<span v-if="question.tf !== null" class="status-label">批改完毕</span>
|
|
|
|
|
<span v-else class="status-label">待批改</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<button @click="submitGrading(answers)" class="grading-complete">批改完成</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 主内容 -->
|
|
|
|
|
<div class="main-content" v-if="!loading && questions.length">
|
|
|
|
|
<div
|
|
|
|
|
v-for="(question, index) in questions"
|
|
|
|
|
:key="question.id"
|
|
|
|
|
class="question"
|
|
|
|
|
:ref="'question-' + question.id"
|
|
|
|
|
:class="{ correct: question.tf === 'true', incorrect: question.tf === 'false', active: currentQuestionIndex === index }"
|
|
|
|
|
>
|
|
|
|
|
<p>{{ index + 1 }}. {{ question.content }}</p>
|
|
|
|
|
<div class="options">
|
|
|
|
|
<!-- 单选题 (Single Choice) -->
|
|
|
|
|
<template v-if="question.type === '单选题'">
|
|
|
|
|
<div v-for="(option, optIndex) in question.chance" :key="optIndex"
|
|
|
|
|
class="option"
|
|
|
|
|
:class="{ selected: answers[question.id] === option.label }">
|
|
|
|
|
{{ option.label }}: {{ option.text }}
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<!-- 多选题 (Multiple Choice) -->
|
|
|
|
|
<template v-else-if="question.type === '多选题'">
|
|
|
|
|
<div v-for="(option, optIndex) in question.chance" :key="optIndex" class="option">
|
|
|
|
|
{{ option.label }}: {{ option.text }}
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<!-- 判断题 (True/False) -->
|
|
|
|
|
<template v-else-if="question.type === '判断题'">
|
|
|
|
|
<div>
|
|
|
|
|
<input
|
|
|
|
|
type="radio"
|
|
|
|
|
:name="'question-' + question.id"
|
|
|
|
|
value="true"
|
|
|
|
|
:checked="answers[question.id] === 'true'"
|
|
|
|
|
disabled
|
|
|
|
|
/>
|
|
|
|
|
T
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<input
|
|
|
|
|
type="radio"
|
|
|
|
|
:name="'question-' + question.id"
|
|
|
|
|
value="false"
|
|
|
|
|
:checked="answers[question.id] === 'false'"
|
|
|
|
|
disabled
|
|
|
|
|
/>
|
|
|
|
|
F
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<p>你的答案:<span>{{ question.idanswer }}</span></p>
|
|
|
|
|
<p>正确答案:<span>{{ question.answer }}</span></p>
|
|
|
|
|
<p>分数:{{ question.score }} 分</p>
|
|
|
|
|
<p v-if="question.analysis">解析:{{ question.analysis }}</p>
|
|
|
|
|
<!-- 批改未批改题目 -->
|
|
|
|
|
<div class="grade-buttons">
|
|
|
|
|
<button @click="gradeQuestion(question.id, 'true')" class="grade-button correct">批改为正确</button>
|
|
|
|
|
<button @click="gradeQuestion(question.id, 'false')" class="grade-button incorrect">批改为错误</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 加载中 -->
|
|
|
|
|
<div v-if="loading" class="loading">
|
|
|
|
|
正在加载试题,请稍候...
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 空状态 -->
|
|
|
|
|
<div v-if="!loading && !questions.length" class="empty">
|
|
|
|
|
未找到试题,请联系管理员!
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
import axios from "axios";
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
name: "ViewExamPaper",
|
|
|
|
|
data() {
|
|
|
|
|
return {
|
|
|
|
|
examTitle: this.examname, // 试卷标题
|
|
|
|
|
sender: "", // 发送人
|
|
|
|
|
totalScore: 0, // 总分
|
|
|
|
|
questions: [], // 试题数据
|
|
|
|
|
answers: {}, // 用户答案
|
|
|
|
|
correctQuestions: 0, // 答对的题目数
|
|
|
|
|
currentScore: 0, // 当前得分
|
|
|
|
|
totalQuestions: 0, // 总题数
|
|
|
|
|
loading: true, // 加载状态
|
|
|
|
|
submitted: false, // 是否已提交
|
|
|
|
|
currentQuestionIndex: 0, // 当前题目索引
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
methods: {
|
|
|
|
|
async fetchExamData() {
|
|
|
|
|
try {
|
|
|
|
|
const token = this.$store.state.tokens[this.userId];
|
|
|
|
|
if (!token) {
|
|
|
|
|
alert("用户未登录,请重新登录!");
|
|
|
|
|
this.$router.push("/login");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const response = await axios.get(
|
|
|
|
|
"http://localhost:8080/teacher/answerSheet/selectQuestionanswer",
|
|
|
|
|
{
|
|
|
|
|
params: {senderId: this.studentId, testid: this.testid},
|
|
|
|
|
headers: {Authorization: `Bearer ${token}`},
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
const {sender, list} = response.data.data;
|
|
|
|
|
this.examTitle = this.examname;
|
|
|
|
|
this.sender = sender;
|
|
|
|
|
this.questions = list;
|
|
|
|
|
this.totalQuestions = this.questions.length;
|
|
|
|
|
this.totalScore = list.reduce((sum, q) => sum + q.score, 0);
|
|
|
|
|
this.correctQuestions = list.filter((q) => q.tf === "true").length;
|
|
|
|
|
this.loading = false;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("获取试题失败", error);
|
|
|
|
|
this.loading = false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
goBack() {
|
|
|
|
|
this.$router.go(-1); // 返回上一页
|
|
|
|
|
},
|
|
|
|
|
gradeQuestion(questionId, result) {
|
|
|
|
|
const question = this.questions.find(q => q.id === questionId);
|
|
|
|
|
|
|
|
|
|
// 如果批改了,更新 `tf` 状态和答案
|
|
|
|
|
if (question.tf !== result) {
|
|
|
|
|
// 更新批改状态
|
|
|
|
|
question.tf = result; // 更新批改状态
|
|
|
|
|
this.answers[questionId] = result; // 更新用户答案
|
|
|
|
|
|
|
|
|
|
// 更新答对题目数量和当前得分
|
|
|
|
|
this.correctQuestions = this.questions.filter(q => q.tf === "true").length;
|
|
|
|
|
this.currentScore = this.questions.reduce((sum, q) => sum + (q.tf === "true" ? q.score : 0), 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(`题目 ID: ${questionId} | 批改结果: ${result} | 正确答案: ${question.answer}`);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
scrollToQuestion(index) {
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
const questionRef = this.$refs[`question-${this.questions[index].id}`];
|
|
|
|
|
if (questionRef && questionRef[0]) {
|
|
|
|
|
questionRef[0].scrollIntoView({behavior: "smooth", block: "start"});
|
|
|
|
|
this.currentQuestionIndex = index;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
async submitGrading() {
|
|
|
|
|
// 将数据准备成后台需要的格式
|
|
|
|
|
const updatedQuestions = this.questions.map(question => {
|
|
|
|
|
return {
|
|
|
|
|
...question,
|
|
|
|
|
// 确保更新了答案的批改状态,不改变原始数据结构
|
|
|
|
|
tf: this.answers[question.id] || question.tf, // 使用最新的批改状态
|
|
|
|
|
userAnswer: this.answers[question.id] || question.idanswer // 确保用户的答案被发送回去
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const token = this.$store.state.tokens[this.userId];
|
|
|
|
|
if (!token) {
|
|
|
|
|
alert("用户未登录,请重新登录!");
|
|
|
|
|
this.$router.push("/login");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
//批改数据提交
|
|
|
|
|
const response = await axios.post(
|
|
|
|
|
"http://localhost:8080/teacher/answerSheet/markObjectiveQuestions", // 提交批改的接口
|
|
|
|
|
{
|
|
|
|
|
senderId: this.studentId,
|
|
|
|
|
testid: this.testid,
|
|
|
|
|
list: updatedQuestions, // 发送更新后的试题数据
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
headers: {Authorization: `Bearer ${token}`},
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
if(response.data.code === 200){
|
|
|
|
|
const res = await axios.post(
|
|
|
|
|
`http://localhost:8080/teacher/answerSheet/addmarkedtest`, // 提交批改的接口
|
|
|
|
|
null, // 如果不传递请求体数据,可以将请求体置为 null
|
|
|
|
|
{
|
|
|
|
|
params: {
|
|
|
|
|
senderId: this.studentId, // 请求参数
|
|
|
|
|
testid: this.testid, // 请求参数
|
|
|
|
|
},
|
|
|
|
|
headers: {
|
|
|
|
|
Authorization: `Bearer ${token}`, // 请求头 Authorization
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
if(res.data.code === 200){
|
|
|
|
|
this.$message.success("批改成功!");
|
|
|
|
|
this.$router.go(-1);
|
|
|
|
|
}else{
|
|
|
|
|
this.$message.error("批改失败!");
|
|
|
|
|
}
|
|
|
|
|
}else{
|
|
|
|
|
this.$message.error("数据提交异常!");
|
|
|
|
|
this.fetchExamData();
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("提交批改失败", error);
|
|
|
|
|
alert("提交批改失败,请稍后再试!");
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
mounted() {
|
|
|
|
|
this.fetchExamData();
|
|
|
|
|
},
|
|
|
|
|
created() {
|
|
|
|
|
this.userId = this.$route.query.userId;
|
|
|
|
|
this.examname = this.$route.query.name;
|
|
|
|
|
this.testid = this.$route.query.testid;
|
|
|
|
|
this.studentId = this.$route.query.studentId;
|
|
|
|
|
console.log("222222222",this.studentId);
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.grading-complete {
|
|
|
|
|
border: none; /* 去掉边框 */
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center; /* 居中对齐按钮 */
|
|
|
|
|
background-color: #388e3c;
|
|
|
|
|
color: white;
|
|
|
|
|
padding: 10px 15px;
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
text-align: center;
|
|
|
|
|
margin-top: 15px; /* 控制按钮距离题目的间距 */
|
|
|
|
|
width: 100%; /* 让按钮占满容器宽度 */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.grade-completed-button {
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.grade-completed-button button {
|
|
|
|
|
background-color: #388e3c;
|
|
|
|
|
color: white;
|
|
|
|
|
padding: 8px 15px;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
transition: background-color 0.3s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.grade-completed-button button:hover {
|
|
|
|
|
background-color: #2c6b32;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.grade-completed-button button:disabled {
|
|
|
|
|
background-color: #bdbdbd;
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.grade-buttons {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 15px; /* 按钮之间的间距 */
|
|
|
|
|
margin-top: 15px; /* 控制按钮距离题目的间距 */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.grade-button {
|
|
|
|
|
padding: 10px 20px;
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 30px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
min-width: 120px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.grade-button:hover {
|
|
|
|
|
transform: scale(1.05); /* 悬停时稍微放大按钮 */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.grade-button.correct {
|
|
|
|
|
background: linear-gradient(145deg, #81c784, #66bb6a); /* 绿色渐变背景 */
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.grade-button.correct:hover {
|
|
|
|
|
background: linear-gradient(145deg, #66bb6a, #81c784); /* 悬停时反转颜色 */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.grade-button.incorrect {
|
|
|
|
|
background: linear-gradient(145deg, #e57373, #f44336); /* 红色渐变背景 */
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.grade-button.incorrect:hover {
|
|
|
|
|
background: linear-gradient(145deg, #f44336, #e57373); /* 悬停时反转颜色 */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.back-button {
|
|
|
|
|
padding-bottom: 10px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.back-button button {
|
|
|
|
|
background-color: #1976d2;
|
|
|
|
|
color: white;
|
|
|
|
|
padding: 8px 15px;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.back-button button:hover {
|
|
|
|
|
background-color: #1565c0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.exam-container {
|
|
|
|
|
display: flex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sidebar {
|
|
|
|
|
position: fixed; /* 固定左侧栏 */
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
width: 20%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
background-color: #f4f4f4;
|
|
|
|
|
overflow-y: auto; /* 允许左侧栏滚动 */
|
|
|
|
|
border-right: 1px solid #ddd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sidebar h2,
|
|
|
|
|
.sidebar p {
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.question-navigation {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(5, 1fr); /* Adjust number of columns */
|
|
|
|
|
gap: 10px;
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.question-box {
|
|
|
|
|
padding: 15px;
|
|
|
|
|
background-color: #f1f1f1;
|
|
|
|
|
text-align: center;
|
|
|
|
|
border: 1px solid #ddd;
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: background-color 0.3s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.question-box:hover {
|
|
|
|
|
background-color: #e0e0e0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.question-box.active {
|
|
|
|
|
background-color: #bbdefb; /* Highlight active question */
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.question-box.answered {
|
|
|
|
|
background-color: #c8e6c9; /* Highlight answered question */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.question-box.correct {
|
|
|
|
|
background-color: #e0f7fa;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.question-box.incorrect {
|
|
|
|
|
background-color: #ffebee;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.question-box.completed {
|
|
|
|
|
background-color: #fce4ec;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-label {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #555;
|
|
|
|
|
margin-left: 5px;
|
|
|
|
|
font-style: italic;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.main-content {
|
|
|
|
|
margin-left: 20%; /* 右侧主内容的宽度根据左侧栏的宽度调整 */
|
|
|
|
|
padding: 20px;
|
|
|
|
|
width: 80%;
|
|
|
|
|
overflow-y: auto; /* 使右侧内容可滚动 */
|
|
|
|
|
height: 100vh;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.question {
|
|
|
|
|
padding: 10px;
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
border: 1px solid #ddd;
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.correct {
|
|
|
|
|
background-color: #e0f7fa;
|
|
|
|
|
color: #00796b;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.incorrect {
|
|
|
|
|
background-color: #ffebee;
|
|
|
|
|
color: #d32f2f;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.loading,
|
|
|
|
|
.empty {
|
|
|
|
|
text-align: center;
|
|
|
|
|
margin-top: 50px;
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.empty {
|
|
|
|
|
color: #888;
|
|
|
|
|
}
|
|
|
|
|
</style>
|