409 lines
12 KiB
Vue
409 lines
12 KiB
Vue
<template>
|
||
<div class="assessment-container">
|
||
<!-- 顶部信息栏 -->
|
||
<div class="assessment-header">
|
||
<el-card shadow="never">
|
||
<div class="header-content">
|
||
<div class="title">正在测评:{{ scaleName }}</div>
|
||
<div class="progress">
|
||
<span>进度:{{ currentIndex + 1 }} / {{ totalItems }}</span>
|
||
<el-progress :percentage="progressPercent" :stroke-width="8"></el-progress>
|
||
</div>
|
||
</div>
|
||
<div class="action-buttons">
|
||
<el-button type="warning" @click="handlePause">暂停测评</el-button>
|
||
<el-button @click="handleExit">退出</el-button>
|
||
</div>
|
||
</el-card>
|
||
</div>
|
||
|
||
<!-- 题目区域 -->
|
||
<el-card shadow="never" class="question-card" v-if="currentItem">
|
||
<div class="question-number">第 {{ currentIndex + 1 }} 题</div>
|
||
<div class="question-content">{{ currentItem.itemContent }}</div>
|
||
|
||
<!-- 单选题 -->
|
||
<div class="options-container" v-if="currentItem.itemType === 'single'">
|
||
<el-radio-group v-model="selectedOption" @change="handleAnswerChange">
|
||
<div v-for="option in currentOptions" :key="option.optionId" class="option-item">
|
||
<el-radio :label="option.optionId">{{ option.optionCode }}. {{ option.optionContent }}</el-radio>
|
||
</div>
|
||
</el-radio-group>
|
||
</div>
|
||
|
||
<!-- 多选题 -->
|
||
<div class="options-container" v-if="currentItem.itemType === 'multiple'">
|
||
<el-checkbox-group v-model="selectedOptions" @change="handleAnswerChange">
|
||
<div v-for="option in currentOptions" :key="option.optionId" class="option-item">
|
||
<el-checkbox :label="option.optionId">{{ option.optionCode }}. {{ option.optionContent }}</el-checkbox>
|
||
</div>
|
||
</el-checkbox-group>
|
||
</div>
|
||
|
||
<!-- 矩阵题 -->
|
||
<div class="matrix-container" v-if="currentItem.itemType === 'matrix'">
|
||
<el-table :data="[{}]" border>
|
||
<el-table-column
|
||
v-for="option in currentOptions"
|
||
:key="option.optionId"
|
||
:label="option.optionContent"
|
||
align="center"
|
||
width="120">
|
||
<template slot-scope="scope">
|
||
<el-radio v-model="selectedOption" :label="option.optionId" @change="handleAnswerChange"></el-radio>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</el-card>
|
||
|
||
<!-- 底部导航 -->
|
||
<div class="navigation-buttons">
|
||
<el-button @click="handlePrev" :disabled="currentIndex === 0">上一题</el-button>
|
||
<el-button type="primary" @click="handleNext" :disabled="currentIndex === totalItems - 1">下一题</el-button>
|
||
<el-button type="success" @click="handleSubmit" :disabled="!isComplete || loading" style="margin-left: 20px;">
|
||
提交测评 ({{ answeredCount }}/{{ totalItems }})
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { getAssessment, pauseAssessment, getAssessmentItems, submitAssessment, getAssessmentAnswers } from "@/api/psychology/assessment";
|
||
import { listOption } from "@/api/psychology/option";
|
||
import { saveAnswer } from "@/api/psychology/assessment";
|
||
|
||
export default {
|
||
name: "AssessmentTaking",
|
||
data() {
|
||
return {
|
||
assessmentId: null,
|
||
scaleName: '',
|
||
itemList: [],
|
||
optionMap: {},
|
||
currentIndex: 0,
|
||
selectedOption: null,
|
||
selectedOptions: [],
|
||
answersMap: {},
|
||
loading: false
|
||
};
|
||
},
|
||
computed: {
|
||
currentItem() {
|
||
return this.itemList[this.currentIndex];
|
||
},
|
||
currentOptions() {
|
||
return this.optionMap[this.currentItem?.itemId] || [];
|
||
},
|
||
totalItems() {
|
||
return this.itemList.length;
|
||
},
|
||
progressPercent() {
|
||
return this.totalItems > 0 ? Math.round((this.currentIndex + 1) / this.totalItems * 100) : 0;
|
||
},
|
||
answeredCount() {
|
||
// 计算已答题数量
|
||
return this.itemList.filter(item => {
|
||
const answer = this.answersMap[item.itemId];
|
||
if (!answer) {
|
||
return false;
|
||
}
|
||
// 单选题需要 optionId,多选题需要 optionIds(字符串,用逗号分隔)
|
||
if (item.itemType === 'single') {
|
||
return answer.optionId != null;
|
||
} else if (item.itemType === 'multiple') {
|
||
return answer.optionIds != null && answer.optionIds.trim().length > 0;
|
||
}
|
||
return true;
|
||
}).length;
|
||
},
|
||
isComplete() {
|
||
if (this.itemList.length === 0) {
|
||
return false;
|
||
}
|
||
return this.answeredCount === this.itemList.length;
|
||
}
|
||
},
|
||
created() {
|
||
this.assessmentId = this.$route.query.assessmentId;
|
||
if (!this.assessmentId) {
|
||
this.$modal.msgError("测评ID不能为空");
|
||
this.$router.push('/psychology/assessment');
|
||
return;
|
||
}
|
||
this.loadAssessment();
|
||
},
|
||
methods: {
|
||
/** 加载测评信息 */
|
||
loadAssessment() {
|
||
this.loading = true;
|
||
Promise.all([
|
||
getAssessment(this.assessmentId),
|
||
getAssessmentItems(this.assessmentId),
|
||
getAssessmentAnswers(this.assessmentId)
|
||
]).then(([assessmentRes, itemsRes, answersRes]) => {
|
||
const assessment = assessmentRes.data;
|
||
if (!assessment) {
|
||
this.$modal.msgError("测评不存在");
|
||
this.$router.push('/psychology/assessment');
|
||
return;
|
||
}
|
||
|
||
this.scaleName = assessment.scaleName || '未知量表';
|
||
this.itemList = itemsRes.data || [];
|
||
|
||
// 检查题目列表是否为空
|
||
if (this.itemList.length === 0) {
|
||
this.$modal.msgWarning("该量表暂无题目,请联系管理员添加题目");
|
||
this.$router.push('/psychology/assessment');
|
||
return;
|
||
}
|
||
|
||
// 加载已保存的答案
|
||
const savedAnswers = answersRes.data || [];
|
||
savedAnswers.forEach(answer => {
|
||
this.answersMap[answer.itemId] = {
|
||
assessmentId: answer.assessmentId,
|
||
itemId: answer.itemId,
|
||
optionId: answer.optionId,
|
||
optionIds: answer.optionIds,
|
||
answerScore: answer.answerScore
|
||
};
|
||
});
|
||
|
||
// 加载所有题目的选项
|
||
this.loadAllOptions().then(() => {
|
||
// 选项加载完成后,加载当前题目的答案
|
||
this.loadCurrentAnswer();
|
||
}).catch(error => {
|
||
console.error('加载选项失败:', error);
|
||
this.$modal.msgError("加载题目选项失败,请刷新重试");
|
||
});
|
||
this.loading = false;
|
||
}).catch(error => {
|
||
console.error('加载测评信息失败:', error);
|
||
this.$modal.msgError("加载测评信息失败,请重试");
|
||
this.loading = false;
|
||
});
|
||
},
|
||
/** 加载所有题目的选项 */
|
||
loadAllOptions() {
|
||
const promises = this.itemList.map(item => {
|
||
return listOption(item.itemId).then(response => {
|
||
this.$set(this.optionMap, item.itemId, response.data || []);
|
||
});
|
||
});
|
||
return Promise.all(promises);
|
||
},
|
||
/** 答案改变事件 */
|
||
handleAnswerChange() {
|
||
const itemId = this.currentItem.itemId;
|
||
const answer = {
|
||
assessmentId: this.assessmentId,
|
||
itemId: itemId,
|
||
optionId: null,
|
||
optionIds: null,
|
||
answerScore: 0
|
||
};
|
||
|
||
if (this.currentItem.itemType === 'single') {
|
||
answer.optionId = this.selectedOption;
|
||
// 计算得分
|
||
const selectedOpt = this.currentOptions.find(opt => opt.optionId === this.selectedOption);
|
||
if (selectedOpt) {
|
||
answer.answerScore = selectedOpt.optionScore || 0;
|
||
}
|
||
// 使用 $set 确保响应式更新
|
||
this.$set(this.answersMap, itemId, answer);
|
||
} else if (this.currentItem.itemType === 'multiple') {
|
||
answer.optionIds = this.selectedOptions.length > 0 ? this.selectedOptions.join(',') : null;
|
||
// 计算多选题得分(所有选中选项的分数之和)
|
||
let totalScore = 0;
|
||
this.selectedOptions.forEach(optId => {
|
||
const selectedOpt = this.currentOptions.find(opt => opt.optionId === optId);
|
||
if (selectedOpt && selectedOpt.optionScore) {
|
||
totalScore += parseFloat(selectedOpt.optionScore) || 0;
|
||
}
|
||
});
|
||
answer.answerScore = totalScore;
|
||
// 使用 $set 确保响应式更新
|
||
this.$set(this.answersMap, itemId, answer);
|
||
}
|
||
|
||
// 自动保存答案
|
||
this.saveAnswerToServer(answer);
|
||
},
|
||
/** 保存答案到服务器 */
|
||
saveAnswerToServer(answer) {
|
||
saveAnswer(answer).then(() => {
|
||
// 静默保存成功
|
||
}).catch(error => {
|
||
console.error('保存答案失败:', error);
|
||
// 不显示错误提示,避免打断用户答题
|
||
});
|
||
},
|
||
/** 上一题 */
|
||
handlePrev() {
|
||
if (this.currentIndex > 0) {
|
||
this.currentIndex--;
|
||
this.loadCurrentAnswer();
|
||
}
|
||
},
|
||
/** 下一题 */
|
||
handleNext() {
|
||
if (this.currentIndex < this.totalItems - 1) {
|
||
this.currentIndex++;
|
||
this.loadCurrentAnswer();
|
||
}
|
||
},
|
||
/** 加载当前题目的答案 */
|
||
loadCurrentAnswer() {
|
||
if (!this.currentItem) {
|
||
return;
|
||
}
|
||
const itemId = this.currentItem.itemId;
|
||
const answer = this.answersMap[itemId];
|
||
|
||
if (this.currentItem.itemType === 'single') {
|
||
this.selectedOption = answer && answer.optionId ? answer.optionId : null;
|
||
this.selectedOptions = [];
|
||
} else if (this.currentItem.itemType === 'multiple') {
|
||
this.selectedOption = null;
|
||
if (answer && answer.optionIds) {
|
||
// 将字符串转换为数字数组
|
||
this.selectedOptions = answer.optionIds.split(',').map(id => parseInt(id)).filter(id => !isNaN(id));
|
||
} else {
|
||
this.selectedOptions = [];
|
||
}
|
||
} else {
|
||
this.selectedOption = null;
|
||
this.selectedOptions = [];
|
||
}
|
||
},
|
||
/** 暂停测评 */
|
||
handlePause() {
|
||
this.$modal.confirm('确定要暂停测评吗?您可以稍后继续完成。').then(() => {
|
||
pauseAssessment(this.assessmentId).then(() => {
|
||
this.$modal.msgSuccess("测评已暂停");
|
||
this.$router.push('/psychology/assessment');
|
||
});
|
||
});
|
||
},
|
||
/** 退出 */
|
||
handleExit() {
|
||
this.$modal.confirm('确定要退出测评吗?已答题目将会保存。').then(() => {
|
||
this.$router.push('/psychology/assessment');
|
||
});
|
||
},
|
||
/** 提交测评 */
|
||
handleSubmit() {
|
||
// 检查是否所有题目都已作答
|
||
if (!this.isComplete) {
|
||
const remaining = this.itemList.length - this.answeredCount;
|
||
this.$modal.msgWarning(`还有 ${remaining} 道题目未作答,请完成所有题目后再提交`);
|
||
return;
|
||
}
|
||
|
||
this.$modal.confirm('确定要提交测评吗?提交后将不能修改。').then(() => {
|
||
submitAssessment(this.assessmentId).then(response => {
|
||
this.$modal.msgSuccess(response.msg || "测评已提交,报告已生成");
|
||
this.$router.push('/psychology/assessment');
|
||
}).catch(error => {
|
||
this.$modal.msgError(error.msg || "提交失败,请重试");
|
||
});
|
||
});
|
||
}
|
||
},
|
||
watch: {
|
||
currentIndex() {
|
||
this.loadCurrentAnswer();
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.assessment-container {
|
||
min-height: 100vh;
|
||
background-color: #f5f5f5;
|
||
padding: 20px;
|
||
}
|
||
|
||
.assessment-header {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.header-content {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.title {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
|
||
.progress {
|
||
flex: 1;
|
||
margin-left: 20px;
|
||
max-width: 400px;
|
||
}
|
||
|
||
.action-buttons {
|
||
text-align: right;
|
||
}
|
||
|
||
.question-card {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.question-number {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
color: #409EFF;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.question-content {
|
||
font-size: 16px;
|
||
line-height: 1.8;
|
||
color: #333;
|
||
margin-bottom: 25px;
|
||
}
|
||
|
||
.options-container {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.option-item {
|
||
padding: 10px 0;
|
||
border-bottom: 1px solid #eee;
|
||
cursor: pointer;
|
||
transition: background-color 0.3s;
|
||
}
|
||
|
||
.option-item:hover {
|
||
background-color: #f5f7fa;
|
||
}
|
||
|
||
.option-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.matrix-container {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.navigation-buttons {
|
||
text-align: center;
|
||
padding: 20px 0;
|
||
background-color: white;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
</style>
|
||
|