guoyu/fronted_uniapp/pages/voice-text-input.vue

315 lines
8.1 KiB
Vue
Raw Normal View History

2025-12-10 22:53:20 +08:00
<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>