feat: realistic face avatar + voice input + ASR endpoint
This commit is contained in:
@@ -53,11 +53,14 @@
|
||||
</scroll-view>
|
||||
|
||||
<view class="input-bar" v-if="!isComplete">
|
||||
<view class="mic-btn" :class="{ recording: isRecording }" @touchstart="startRecord" @touchend="stopRecord" @touchcancel="stopRecord">
|
||||
<text class="mic-icon">🎤</text>
|
||||
</view>
|
||||
<view class="input-box">
|
||||
<textarea class="input-area" v-model="inputText" placeholder="输入你的回答..." :auto-height="true" :maxlength="2000" :disabled="aiLoading" @confirm="sendAnswer" />
|
||||
</view>
|
||||
<view class="send-btn" :class="{ disabled: !inputText.trim() || aiLoading }" @click="sendAnswer">
|
||||
<text class="send-icon">➤</text>
|
||||
<view class="send-btn" :class="{ disabled: (!inputText.trim() && !isRecording) || aiLoading }" @click="sendAnswer">
|
||||
<text class="send-icon">{{ isRecording ? '◉' : '➤' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -88,6 +91,8 @@ const aiSpeechText = ref('')
|
||||
const aiAudioUrl = ref('')
|
||||
const isSpeaking = ref(false)
|
||||
const dhRef = ref(null)
|
||||
const isRecording = ref(false)
|
||||
let recorder = null
|
||||
|
||||
let timerSeconds = 0
|
||||
let timerInterval = null
|
||||
@@ -227,6 +232,43 @@ const confirmExit = () => {
|
||||
success: (r) => { if (r.confirm) uni.navigateBack() },
|
||||
})
|
||||
}
|
||||
|
||||
function startRecord() {
|
||||
if (aiLoading.value || isComplete.value) return
|
||||
isRecording.value = true
|
||||
recorder = uni.getRecorderManager()
|
||||
recorder.onStart(() => {})
|
||||
recorder.onError(() => { isRecording.value = false })
|
||||
recorder.start({ format: 'mp3' })
|
||||
uni.vibrateShort({ type: 'medium' })
|
||||
}
|
||||
|
||||
function stopRecord() {
|
||||
if (!recorder || !isRecording.value) return
|
||||
isRecording.value = false
|
||||
recorder.stop()
|
||||
recorder.onStop(async (res) => {
|
||||
if (!res.tempFilePath) return
|
||||
const audioPath = res.tempFilePath
|
||||
try {
|
||||
const uploadRes = await uni.uploadFile({
|
||||
url: api('/asr/recognize'),
|
||||
filePath: audioPath,
|
||||
name: 'audio',
|
||||
header: { 'Authorization': `Bearer ${token.value}` },
|
||||
})
|
||||
if (uploadRes.statusCode === 200 && uploadRes.data) {
|
||||
const data = typeof uploadRes.data === 'string' ? JSON.parse(uploadRes.data) : uploadRes.data
|
||||
if (data.text) {
|
||||
inputText.value = data.text
|
||||
uni.vibrateShort({ type: 'light' })
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
uni.showToast({ title: '语音识别失败,请手动输入', icon: 'none' })
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -300,6 +342,18 @@ const confirmExit = () => {
|
||||
}
|
||||
.input-box { flex: 1; background: var(--color-bg); border-radius: var(--radius-md); padding: 12rpx 20rpx; }
|
||||
.input-area { width: 100%; font-size: 26rpx; color: var(--color-text); max-height: 160rpx; line-height: 1.5; }
|
||||
.mic-btn {
|
||||
width: 64rpx; height: 64rpx; border-radius: 50%; background: #F3F4F6;
|
||||
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.mic-btn:active { transform: scale(0.9); }
|
||||
.mic-btn.recording { background: #FEE2E2; animation: mic-pulse 1s infinite; }
|
||||
.mic-icon { font-size: 28rpx; }
|
||||
@keyframes mic-pulse {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); }
|
||||
50% { box-shadow: 0 0 0 16rpx rgba(239, 68, 68, 0); }
|
||||
}
|
||||
.send-btn {
|
||||
width: 80rpx; height: 80rpx; background: linear-gradient(135deg, var(--color-gradient-start), var(--color-gradient-mid));
|
||||
border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0;
|
||||
|
||||
Reference in New Issue
Block a user