guoyu/fronted_uniapp/pages/voice/detail.vue
2025-12-03 18:58:36 +08:00

433 lines
12 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="detail-container">
<!-- 顶部导航栏 -->
<view class="header">
<view class="header-content">
<text class="title">评测详情</text>
</view>
</view>
<template v-if="loading">
<view class="loading-container">
<text>加载中...</text>
</view>
</template>
<template v-else-if="detail">
<view class="detail-content">
<!-- 基本信息 -->
<view class="info-section">
<view class="section-title">📋 基本信息</view>
<view class="info-item">
<text class="info-label">评测内容:</text>
<text class="info-value">{{ detail.content }}</text>
</view>
<view class="info-item" v-if="detail.courseName">
<text class="info-label">所属课程:</text>
<text class="info-value">{{ detail.courseName }}</text>
</view>
<view class="info-item">
<text class="info-label">评测时间:</text>
<text class="info-value">{{ formatDateTime(detail.evaluationTime) }}</text>
</view>
</view>
<!-- 评分结果 -->
<view class="score-section">
<view class="section-title">📊 评分结果</view>
<view class="main-score">
<text class="main-score-value">{{ detail.score || 0 }}</text>
<text class="main-score-label">总分</text>
</view>
<view class="score-list">
<view class="score-row">
<text class="score-name">准确度</text>
<view class="score-bar-container">
<view class="score-bar" :style="{ width: (detail.accuracy || 0) + '%' }"></view>
</view>
<text class="score-num">{{ detail.accuracy || 0 }}分</text>
</view>
<view class="score-row">
<text class="score-name">流畅度</text>
<view class="score-bar-container">
<view class="score-bar" :style="{ width: (detail.fluency || 0) + '%' }"></view>
</view>
<text class="score-num">{{ detail.fluency || 0 }}分</text>
</view>
<view class="score-row">
<text class="score-name">完整度</text>
<view class="score-bar-container">
<view class="score-bar" :style="{ width: (detail.completeness || 0) + '%' }"></view>
</view>
<text class="score-num">{{ detail.completeness || 0 }}分</text>
</view>
<view class="score-row">
<text class="score-name">发音</text>
<view class="score-bar-container">
<view class="score-bar" :style="{ width: (detail.pronunciation || 0) + '%' }"></view>
</view>
<text class="score-num">{{ detail.pronunciation || 0 }}分</text>
</view>
</view>
</view>
<!-- 音频播放 -->
<view class="audio-section" v-if="detail.audioPath">
<view class="section-title">🎵 录音回放</view>
<view class="audio-player">
<button class="play-btn" @click="playAudio" :disabled="isPlaying">
<text class="play-icon">{{ isPlaying ? '⏸' : '▶' }}</text>
<text>{{ isPlaying ? '播放中' : '播放录音' }}</text>
</button>
</view>
</view>
<!-- 详细结果 -->
<view class="detail-result-section" v-if="detail.resultDetail">
<view class="section-title">📝 详细结果</view>
<view class="detail-result-content">
<text>{{ formatDetailResult(detail.resultDetail) }}</text>
</view>
</view>
</view>
</template>
<template v-else>
<view class="error-container">
<text class="error-text">加载失败请重试</text>
<button class="retry-btn" @click="loadDetail">重试</button>
</view>
</template>
</view>
</template>
<script>
import { getVoiceEvaluation } from '@/api/study/voiceEvaluation.js'
import request from '@/utils/request.js'
import config from '@/utils/config.js'
export default {
data() {
return {
id: null,
loading: true,
detail: null,
audioContext: null,
isPlaying: false
}
},
onLoad(options) {
if (options.id) {
this.id = parseInt(options.id)
this.loadDetail()
} else {
uni.showToast({
title: '参数错误',
icon: 'none'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
},
onUnload() {
if (this.audioContext) {
this.audioContext.destroy()
}
},
methods: {
// 加载详情
async loadDetail() {
this.loading = true
try {
const result = await getVoiceEvaluation(this.id)
if (result.code === 200 && result.data) {
this.detail = result.data
} else {
throw new Error(result.msg || '加载失败')
}
} catch (error) {
console.error('加载详情失败', error)
uni.showToast({
title: error.message || '加载失败',
icon: 'none'
})
} finally {
this.loading = false
}
},
// 播放音频
playAudio() {
if (!this.detail || !this.detail.audioPath) {
uni.showToast({
title: '音频文件不存在',
icon: 'none'
})
return
}
if (this.audioContext) {
this.audioContext.destroy()
}
// 使用request的baseURL来构建完整URL如果没有则使用config中的配置
const baseURL = request.baseURL || config.API_BASE_URL || 'http://localhost:8080'
let audioPath = this.detail.audioPath
// 确保路径以/开头
if (!audioPath.startsWith('/')) {
audioPath = '/' + audioPath
}
const audioUrl = baseURL + audioPath
console.log('播放音频URL:', audioUrl)
this.audioContext = uni.createInnerAudioContext()
this.audioContext.src = audioUrl
this.isPlaying = true
uni.showToast({
title: '开始播放',
icon: 'none',
duration: 1000
})
this.audioContext.play()
this.audioContext.onEnded(() => {
this.isPlaying = false
this.audioContext.destroy()
this.audioContext = null
uni.showToast({
title: '播放完成',
icon: 'success',
duration: 1500
})
})
this.audioContext.onError((err) => {
console.error('播放失败', err)
this.isPlaying = false
uni.showToast({
title: '播放失败:' + (err.errMsg || '未知错误'),
icon: 'none',
duration: 3000
})
if (this.audioContext) {
this.audioContext.destroy()
this.audioContext = null
}
})
},
// 格式化详细结果
formatDetailResult(resultDetail) {
if (!resultDetail) return ''
try {
const obj = JSON.parse(resultDetail)
return JSON.stringify(obj, null, 2)
} catch (e) {
return resultDetail
}
},
// 格式化日期时间
formatDateTime(dateTime) {
if (!dateTime) return ''
const date = new Date(dateTime)
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')
const second = date.getSeconds().toString().padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
}
}
}
</script>
<style scoped>
.detail-container {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 40rpx;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 40rpx 30rpx;
margin-bottom: 20rpx;
}
.header-content {
display: flex;
align-items: center;
}
.title {
font-size: 40rpx;
font-weight: bold;
color: #fff;
}
.loading-container, .error-container {
text-align: center;
padding: 100rpx 40rpx;
color: #999;
font-size: 28rpx;
}
.error-text {
display: block;
margin-bottom: 40rpx;
}
.retry-btn {
background-color: #409eff;
color: #fff;
border: none;
padding: 20rpx 60rpx;
border-radius: 50rpx;
font-size: 28rpx;
}
.detail-content {
padding: 0 20rpx;
}
.info-section, .score-section, .audio-section, .detail-result-section {
background-color: #fff;
padding: 30rpx;
margin-bottom: 20rpx;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 24rpx;
display: block;
}
.info-item {
display: flex;
margin-bottom: 20rpx;
line-height: 1.6;
}
.info-label {
font-size: 28rpx;
color: #666;
min-width: 160rpx;
}
.info-value {
font-size: 28rpx;
color: #333;
flex: 1;
}
.main-score {
text-align: center;
padding: 40rpx 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12rpx;
margin-bottom: 30rpx;
}
.main-score-value {
display: block;
font-size: 80rpx;
font-weight: bold;
color: #fff;
line-height: 1;
}
.main-score-label {
display: block;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.9);
margin-top: 10rpx;
}
.score-list {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.score-row {
display: flex;
align-items: center;
gap: 20rpx;
}
.score-name {
font-size: 28rpx;
color: #333;
min-width: 120rpx;
}
.score-bar-container {
flex: 1;
height: 12rpx;
background-color: #e4e7ed;
border-radius: 6rpx;
overflow: hidden;
}
.score-bar {
height: 100%;
background: linear-gradient(90deg, #67c23a 0%, #85ce61 100%);
border-radius: 6rpx;
transition: width 0.3s;
}
.score-num {
font-size: 28rpx;
font-weight: bold;
color: #409eff;
min-width: 80rpx;
text-align: right;
}
.audio-player {
display: flex;
justify-content: center;
}
.play-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
border: none;
padding: 24rpx 60rpx;
border-radius: 50rpx;
font-size: 28rpx;
display: flex;
align-items: center;
gap: 10rpx;
box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.3);
}
.play-icon {
font-size: 32rpx;
}
.detail-result-content {
background-color: #f9f9f9;
padding: 24rpx;
border-radius: 8rpx;
max-height: 400rpx;
overflow-y: auto;
}
.detail-result-content text {
font-size: 24rpx;
color: #666;
line-height: 1.8;
font-family: monospace;
white-space: pre-wrap;
word-break: break-all;
}
</style>