guoyu/fronted_uniapp/pages/score/detail.vue

457 lines
15 KiB
Vue
Raw Normal View History

2025-12-03 18:58:36 +08:00
<template>
<view class="score-detail-container">
<view class="detail-header">
<text class="header-title">成绩详情</text>
</view>
<!-- 成绩概览 -->
<view class="score-overview" v-if="scoreData">
<view class="main-score">
<text class="score-value">{{ scoreData.obtainedScore || 0 }}</text>
<text class="score-total">/ {{ scoreData.totalScore || 0 }}</text>
</view>
<view class="score-percent">
<text>得分率{{ getScorePercent() }}%</text>
</view>
</view>
<!-- 考试信息 -->
<view class="info-card" v-if="scoreData">
<view class="card-title">考试信息</view>
<view class="info-item">
<text class="info-label">考试名称</text>
<text class="info-value">{{ scoreData.examName || '未知' }}</text>
</view>
<view class="info-item">
<text class="info-label">提交时间</text>
<text class="info-value">{{ formatTime(scoreData.submitTime) }}</text>
</view>
<view class="info-item" v-if="scoreData.duration">
<text class="info-label">用时</text>
<text class="info-value">{{ formatDuration(scoreData.duration) }}</text>
</view>
</view>
<!-- 答题详情 -->
<view class="answer-detail" v-if="answerDetails && answerDetails.length > 0">
<view class="card-title">答题详情</view>
<view
v-for="(detail, index) in answerDetails"
:key="detail.id || index"
class="answer-item"
:class="{ 'correct': detail.isCorrect === '1', 'wrong': detail.isCorrect === '0' }"
>
<view class="answer-header">
<text class="answer-number"> {{ index + 1 }} </text>
<text class="answer-score" :class="detail.isCorrect === '1' ? 'score-correct' : 'score-wrong'">
{{ detail.score || 0 }}/{{ detail.questionScore || 0 }}
</text>
</view>
<view class="question-content">
<text class="question-text">{{ detail.questionContent }}</text>
</view>
<view class="answer-comparison">
<view class="answer-row">
<text class="answer-label">我的答案</text>
<view class="answer-value" :class="detail.isCorrect === '0' ? 'wrong-answer' : 'correct-answer-user'">
<text v-if="detail.questionType !== 'multiple'">{{ detail.studentAnswer || '未作答' }}</text>
<view v-else class="answer-options">
<text v-for="(item, idx) in formatAnswerWithOptions(detail.studentAnswer, detail.options)" :key="idx" class="answer-option-item">
{{ item }}
</text>
</view>
</view>
2025-12-03 18:58:36 +08:00
</view>
<view class="answer-row">
2025-12-03 18:58:36 +08:00
<text class="answer-label">正确答案</text>
<view class="answer-value correct-answer">
<text v-if="!detail.correctAnswer || detail.correctAnswer === ''" style="color: #ff9800;">
题目未设置正确答案
</text>
<text v-else-if="detail.questionType !== 'multiple'">{{ detail.correctAnswer }}</text>
<view v-else class="answer-options">
<text v-for="(item, idx) in formatAnswerWithOptions(detail.correctAnswer, detail.options)" :key="idx" class="answer-option-item">
{{ item }}
</text>
</view>
</view>
2025-12-03 18:58:36 +08:00
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { getScoreDetail, getMyScores } from '@/api/study/score.js'
export default {
data() {
return {
scoreId: null,
examId: null,
scoreData: null,
answerDetails: [],
loading: false
}
},
onLoad(options) {
if (options.id) {
this.scoreId = options.id
}
if (options.examId) {
this.examId = options.examId
}
if (this.scoreId) {
this.loadScoreDetail()
} else {
uni.showToast({
title: '缺少成绩ID',
icon: 'none'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
},
methods: {
async loadScoreDetail() {
this.loading = true
try {
const hasExamId = await this.ensureExamId()
if (!hasExamId) {
throw new Error('缺少考试ID无法加载详情')
}
// 如果后端需要 examId也一起传递
const response = await getScoreDetail(this.scoreId, this.examId)
if (response.code === 200 && response.data) {
this.scoreData = response.data
this.answerDetails = response.data.answerDetails || []
} else {
throw new Error(response.msg || '加载失败')
}
} catch (error) {
console.error('加载成绩详情失败', error)
uni.showToast({
title: error.message || '加载失败',
icon: 'none'
})
} finally {
this.loading = false
}
},
async ensureExamId() {
if (this.examId) {
return true
}
try {
const response = await getMyScores()
if (response.code === 200 && response.data) {
const target = response.data.find(item => item.id == this.scoreId)
if (target && target.examId) {
this.examId = target.examId
return true
}
}
} catch (error) {
console.error('获取考试ID失败', error)
}
return false
},
getScorePercent() {
if (!this.scoreData || !this.scoreData.totalScore) return 0
return Math.round((this.scoreData.obtainedScore || 0) / this.scoreData.totalScore * 100)
},
formatTime(timeStr) {
if (!timeStr) return ''
const date = new Date(timeStr)
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
const hour = date.getHours().toString().padStart(2, '0')
const minute = date.getMinutes().toString().padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}`
},
formatDuration(seconds) {
if (!seconds) return '0分钟'
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
if (hours > 0) {
return `${hours}小时${minutes}分钟`
}
return `${minutes}分钟`
},
// 解析选项JSON字符串转数组
parseOptions(optionsStr) {
if (!optionsStr) return []
try {
if (typeof optionsStr === 'string') {
return JSON.parse(optionsStr)
}
return optionsStr
} catch (e) {
console.error('解析选项失败:', e)
return []
}
},
// 格式化答案显示(将"A,B"转换为["A. 选项1", "B. 选项2"]
formatAnswerWithOptions(answer, optionsStr) {
if (!answer || answer === '暂无') return ['暂无']
const options = this.parseOptions(optionsStr)
// 将答案拆分(逗号或顿号分隔)
const answerParts = answer.split(/[,,、]/).map(item => item.trim()).filter(item => item)
// 如果没有选项列表,直接返回答案文本(每个换行显示)
if (!options || options.length === 0) {
console.log('没有选项列表,直接返回:', answerParts)
return answerParts
}
console.log('格式化答案:', { answer, options, answerParts })
// 判断答案格式:是字母标签(A,B)还是选项文本
const resultMap = new Map() // 使用Map按字母顺序存储
answerParts.forEach((part) => {
const partUpper = part.toUpperCase()
// 如果是单个字母(A-Z),转换为"A. 选项"格式
if (partUpper.length === 1 && partUpper >= 'A' && partUpper <= 'Z') {
const index = partUpper.charCodeAt(0) - 65 // A=0, B=1, C=2...
if (index >= 0 && index < options.length) {
const cleanOption = options[index].replace(/^[A-Z]\.\s*/, '').trim()
console.log(`字母标签 ${partUpper} -> ${cleanOption}`)
resultMap.set(partUpper, `${partUpper}. ${cleanOption}`)
}
} else {
// 如果答案本身就是选项文本,找到对应的字母标签
const optionIndex = options.findIndex(opt => {
const cleanOpt = opt.replace(/^[A-Z]\.\s*/, '').trim()
const cleanPart = part.replace(/^[A-Z]\.\s*/, '').trim()
return cleanOpt === cleanPart || opt === part
})
if (optionIndex >= 0) {
const label = String.fromCharCode(65 + optionIndex) // 0=A, 1=B...
const cleanOption = options[optionIndex].replace(/^[A-Z]\.\s*/, '').trim()
console.log(`选项文本 ${part} -> ${label}. ${cleanOption}`)
resultMap.set(label, `${label}. ${cleanOption}`)
} else {
// 都不匹配,直接返回原文本
console.log(`无法匹配: ${part}`)
resultMap.set(part, part)
}
}
})
// 按字母顺序排序A, B, C...
const sortedKeys = Array.from(resultMap.keys()).sort()
const result = sortedKeys.map(key => resultMap.get(key))
return result.length > 0 ? result : answerParts
2025-12-03 18:58:36 +08:00
}
}
}
</script>
<style lang="scss" scoped>
.score-detail-container {
padding: 20rpx;
background-color: #f5f5f5;
min-height: 100vh;
}
.detail-header {
text-align: center;
margin-bottom: 30rpx;
.header-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
}
.score-overview {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 20rpx;
padding: 60rpx 40rpx;
margin-bottom: 30rpx;
text-align: center;
.main-score {
margin-bottom: 20rpx;
.score-value {
font-size: 120rpx;
font-weight: bold;
color: #fff;
line-height: 1;
}
.score-total {
font-size: 48rpx;
color: rgba(255, 255, 255, 0.8);
margin-left: 10rpx;
}
}
.score-percent {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.9);
}
}
.info-card, .answer-detail {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
.card-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 30rpx;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.info-label {
font-size: 28rpx;
color: #999;
}
.info-value {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
}
}
.answer-item {
padding: 30rpx;
margin-bottom: 20rpx;
border-radius: 12rpx;
border: 2rpx solid #e0e0e0;
&.correct {
background: #e8f5e9;
border-color: #4caf50;
}
&.wrong {
background: #ffebee;
border-color: #f44336;
}
.answer-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.answer-number {
font-size: 28rpx;
font-weight: bold;
color: #333;
}
.answer-score {
font-size: 26rpx;
font-weight: bold;
padding: 4rpx 12rpx;
border-radius: 8rpx;
&.score-correct {
background: #4caf50;
color: #fff;
}
&.score-wrong {
background: #f44336;
color: #fff;
}
}
}
.question-content {
margin-bottom: 20rpx;
.question-text {
font-size: 28rpx;
line-height: 1.6;
color: #333;
}
}
.answer-comparison {
.answer-row {
display: flex;
align-items: flex-start;
margin-bottom: 12rpx;
.answer-label {
font-size: 26rpx;
color: #666;
width: 160rpx;
flex-shrink: 0;
}
.answer-value {
flex: 1;
font-size: 26rpx;
word-break: break-all;
&.wrong-answer {
color: #f44336;
font-weight: bold;
}
&.correct-answer {
color: #4caf50;
font-weight: bold;
}
&.correct-answer-user {
color: #4caf50;
font-weight: bold;
}
.answer-options {
display: flex;
flex-direction: column;
gap: 8rpx;
.answer-option-item {
display: block;
line-height: 1.6;
padding: 4rpx 0;
}
}
2025-12-03 18:58:36 +08:00
}
}
}
}
</style>