guoyu/fronted_uniapp/pages/voice-text-input.vue
2025-12-10 22:53:20 +08:00

315 lines
8.1 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="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>