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>
|