guoyu/fronted_uniapp/pages/score/detail.vue
2025-12-06 14:53:35 +08:00

457 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>
</view>
<view class="answer-row">
<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>
</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
}
}
}
</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;
}
}
}
}
}
}
</style>