315 lines
8.1 KiB
Vue
315 lines
8.1 KiB
Vue
<template>
|
||
<view class="voice-input-container">
|
||
<custom-navbar title="语音评测(文字输入模式)"></custom-navbar>
|
||
|
||
<!-- 评测内容 -->
|
||
<view class="content-section">
|
||
<view class="section-title">📝 评测内容</view>
|
||
<view class="content-display">{{ content || '请先设置评测内容' }}</view>
|
||
<button v-if="!content" @click="selectContent" class="btn-secondary">
|
||
选择内容
|
||
</button>
|
||
</view>
|
||
|
||
<!-- 文字输入区 -->
|
||
<view class="input-section">
|
||
<view class="section-title">✍️ 输入您的朗读内容</view>
|
||
<view class="input-tip">💡 提示:请按照上方内容准确输入</view>
|
||
<textarea
|
||
v-model="userInput"
|
||
class="text-input"
|
||
placeholder="请在此输入您朗读的内容..."
|
||
:maxlength="500"
|
||
:auto-height="true"
|
||
:show-confirm-bar="false"
|
||
/>
|
||
<view class="input-counter">{{ userInput.length }}/500</view>
|
||
</view>
|
||
|
||
<!-- 操作按钮 -->
|
||
<view class="action-section">
|
||
<button @click="clearInput" class="btn-secondary">
|
||
清空
|
||
</button>
|
||
<button @click="submitEvaluation" :disabled="!canSubmit" class="btn-primary">
|
||
提交评测
|
||
</button>
|
||
</view>
|
||
|
||
<!-- 评测结果 -->
|
||
<view v-if="result" class="result-section">
|
||
<view class="result-header">
|
||
<text class="result-title">📊 评测结果</text>
|
||
<text class="result-score">{{ result.totalScore }}分</text>
|
||
</view>
|
||
<view class="result-detail">
|
||
<view class="detail-item">
|
||
<text class="detail-label">准确度:</text>
|
||
<text class="detail-value">{{ result.accuracy }}分</text>
|
||
</view>
|
||
<view class="detail-item">
|
||
<text class="detail-label">完整度:</text>
|
||
<text class="detail-value">{{ result.completeness }}分</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { saveVoiceScore } from '@/api/study/voiceEvaluation.js'
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
content: '',
|
||
userInput: '',
|
||
courseId: null,
|
||
result: null
|
||
}
|
||
},
|
||
computed: {
|
||
canSubmit() {
|
||
return this.content && this.userInput.trim().length > 0
|
||
}
|
||
},
|
||
onLoad(options) {
|
||
if (options.content) {
|
||
this.content = decodeURIComponent(options.content)
|
||
}
|
||
if (options.courseId) {
|
||
this.courseId = options.courseId
|
||
}
|
||
},
|
||
methods: {
|
||
selectContent() {
|
||
uni.navigateTo({
|
||
url: '/pages/voice/content-list'
|
||
})
|
||
},
|
||
|
||
clearInput() {
|
||
this.userInput = ''
|
||
},
|
||
|
||
async submitEvaluation() {
|
||
if (!this.canSubmit) {
|
||
return
|
||
}
|
||
|
||
uni.showLoading({ title: '评测中...' })
|
||
|
||
try {
|
||
// 简单的文字匹配评分
|
||
const score = this.calculateScore(this.content, this.userInput)
|
||
|
||
// 保存到后端
|
||
await saveVoiceScore(
|
||
this.content,
|
||
this.userInput,
|
||
score.totalScore,
|
||
score.accuracy,
|
||
score.completeness,
|
||
score.fluency,
|
||
score.pronunciation,
|
||
this.courseId,
|
||
JSON.stringify(score)
|
||
)
|
||
|
||
this.result = score
|
||
|
||
uni.hideLoading()
|
||
uni.showToast({
|
||
title: '评测完成',
|
||
icon: 'success'
|
||
})
|
||
|
||
} catch (error) {
|
||
console.error('评测失败:', error)
|
||
uni.hideLoading()
|
||
uni.showToast({
|
||
title: error.message || '评测失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
},
|
||
|
||
// 计算评分(简单的字符匹配)
|
||
calculateScore(original, input) {
|
||
// 去除标点和空格
|
||
const cleanOriginal = original.replace(/[,。!?、;:""''()\s]/g, '')
|
||
const cleanInput = input.replace(/[,。!?、;:""''()\s]/g, '')
|
||
|
||
// 计算相似度
|
||
const originalChars = cleanOriginal.split('')
|
||
const inputChars = cleanInput.split('')
|
||
|
||
let matchCount = 0
|
||
const minLength = Math.min(originalChars.length, inputChars.length)
|
||
|
||
for (let i = 0; i < minLength; i++) {
|
||
if (originalChars[i] === inputChars[i]) {
|
||
matchCount++
|
||
}
|
||
}
|
||
|
||
// 准确度:匹配字符数/原文长度
|
||
const accuracy = originalChars.length > 0
|
||
? (matchCount / originalChars.length) * 100
|
||
: 0
|
||
|
||
// 完整度:输入长度/原文长度
|
||
const completeness = originalChars.length > 0
|
||
? Math.min((inputChars.length / originalChars.length) * 100, 100)
|
||
: 0
|
||
|
||
// 综合得分
|
||
const totalScore = (accuracy * 0.6 + completeness * 0.4).toFixed(1)
|
||
|
||
return {
|
||
totalScore: parseFloat(totalScore),
|
||
accuracy: accuracy.toFixed(1),
|
||
completeness: completeness.toFixed(1),
|
||
fluency: 85.0, // 文字输入默认流利度
|
||
pronunciation: accuracy.toFixed(1)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.voice-input-container {
|
||
min-height: 100vh;
|
||
background: #f5f7fa;
|
||
padding: 20rpx;
|
||
}
|
||
|
||
.content-section,
|
||
.input-section,
|
||
.action-section,
|
||
.result-section {
|
||
background: #fff;
|
||
border-radius: 20rpx;
|
||
padding: 30rpx;
|
||
margin-bottom: 30rpx;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
margin-bottom: 20rpx;
|
||
color: #333;
|
||
}
|
||
|
||
.content-display {
|
||
padding: 30rpx;
|
||
background: #f5f5f5;
|
||
border-radius: 12rpx;
|
||
font-size: 32rpx;
|
||
line-height: 1.8;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.input-tip {
|
||
padding: 20rpx;
|
||
background: #fff9e6;
|
||
border-left: 4rpx solid #ffc107;
|
||
border-radius: 8rpx;
|
||
font-size: 28rpx;
|
||
color: #856404;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.text-input {
|
||
width: 100%;
|
||
min-height: 300rpx;
|
||
padding: 20rpx;
|
||
background: #f9f9f9;
|
||
border: 2rpx solid #e0e0e0;
|
||
border-radius: 12rpx;
|
||
font-size: 32rpx;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.input-counter {
|
||
text-align: right;
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
margin-top: 10rpx;
|
||
}
|
||
|
||
.action-section {
|
||
display: flex;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
button {
|
||
flex: 1;
|
||
padding: 30rpx;
|
||
border: none;
|
||
border-radius: 12rpx;
|
||
font-size: 32rpx;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: #409eff;
|
||
color: #fff;
|
||
}
|
||
|
||
.btn-primary:disabled {
|
||
background: #ccc;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: #fff;
|
||
color: #409eff;
|
||
border: 2rpx solid #409eff;
|
||
}
|
||
|
||
.result-section {
|
||
animation: fadeIn 0.3s;
|
||
}
|
||
|
||
.result-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 30rpx;
|
||
padding-bottom: 20rpx;
|
||
border-bottom: 2rpx solid #e0e0e0;
|
||
}
|
||
|
||
.result-title {
|
||
font-size: 36rpx;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.result-score {
|
||
font-size: 48rpx;
|
||
font-weight: bold;
|
||
color: #67c23a;
|
||
}
|
||
|
||
.detail-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 20rpx 0;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.detail-label {
|
||
color: #666;
|
||
}
|
||
|
||
.detail-value {
|
||
font-weight: bold;
|
||
color: #409eff;
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; transform: translateY(20rpx); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
</style>
|