240 lines
5.8 KiB
Vue
240 lines
5.8 KiB
Vue
<template>
|
|
<view class="voice-popup" v-if="visible">
|
|
<view class="voice-mask" @click="hideVoicePanel"></view>
|
|
<view class="voice-input-panel">
|
|
<view class="voice-header">
|
|
<text>{{ $lang('ai-chat.index.voiceInputTitle') }}</text>
|
|
<button class="close-btn" @click="hideVoicePanel">
|
|
<text class="iconfont icon-close"></text>
|
|
</button>
|
|
</view>
|
|
<view class="voice-content">
|
|
<view class="voice-wave" :class="{ recording: voiceInputing }">
|
|
<view
|
|
v-for="i in 8"
|
|
:key="i"
|
|
class="wave-bar"
|
|
:style="{ animationDelay: (i * 0.1) + 's' }"></view>
|
|
</view>
|
|
<text class="voice-tip">{{ voiceInputing ? $lang('ai-chat.index.recordingTip') : $lang('ai-chat.index.startRecordTip') }}</text>
|
|
</view>
|
|
<view class="voice-actions">
|
|
<button class="voice-btn" @click="toggleVoiceInput">
|
|
<text class="iconfont" :class="voiceInputing ? 'icon-stop' : 'icon-voice'"></text>
|
|
</button>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script>
|
|
/**
|
|
* ai-chat-voice-panel - 语音输入面板组件
|
|
*
|
|
* Props:
|
|
* visible: Boolean - 控制显示/隐藏
|
|
*
|
|
* Events:
|
|
* update:visible - 同步关闭状态
|
|
* close - 请求关闭面板
|
|
*/
|
|
export default {
|
|
name: 'AiChatVoicePanel',
|
|
|
|
props: {
|
|
visible: {
|
|
type: Boolean,
|
|
default: false
|
|
}
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
voiceInputing: false
|
|
}
|
|
},
|
|
|
|
watch: {
|
|
visible(val) {
|
|
if (!val) {
|
|
this.voiceInputing = false
|
|
}
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
hideVoicePanel() {
|
|
this.voiceInputing = false
|
|
this.$emit('update:visible', false)
|
|
this.$emit('close')
|
|
},
|
|
|
|
toggleVoiceInput() {
|
|
this.voiceInputing = !this.voiceInputing
|
|
if (this.voiceInputing) {
|
|
this.startVoiceInput()
|
|
} else {
|
|
this.stopVoiceInput()
|
|
}
|
|
},
|
|
|
|
startVoiceInput() {
|
|
// #ifdef H5
|
|
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
|
navigator.mediaDevices.getUserMedia({ audio: true })
|
|
.then(stream => {
|
|
// 录音中,可扩展实时转文字
|
|
})
|
|
.catch(error => {
|
|
uni.showToast({ title: this.$lang('ai-chat.index.voiceInputFailed'), icon: 'none' })
|
|
this.voiceInputing = false
|
|
})
|
|
}
|
|
// #endif
|
|
},
|
|
|
|
stopVoiceInput() {
|
|
this.voiceInputing = false
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
/* 引入图标字体: 由 main.wxss 全局提供, 不再重复 import */
|
|
@import '../styles/ai-chat-global.scss';
|
|
|
|
.voice-popup {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
z-index: 999;
|
|
|
|
.voice-mask {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: rgba(0, 0, 0, 0.4);
|
|
}
|
|
|
|
.voice-input-panel {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
background-color: $bg-card;
|
|
border-radius: $radius-lg;
|
|
padding: 48rpx;
|
|
width: 60%;
|
|
max-width: 520rpx;
|
|
box-shadow: 0 8rpx 36rpx rgba(0, 0, 0, 0.15);
|
|
|
|
.voice-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 32rpx;
|
|
border-bottom: 2rpx solid #f0f4f8;
|
|
padding-bottom: 16rpx;
|
|
|
|
text {
|
|
font-size: 34rpx;
|
|
font-weight: 600;
|
|
color: $color-text;
|
|
}
|
|
|
|
.close-btn {
|
|
width: 56rpx;
|
|
height: 56rpx;
|
|
border-radius: 50%;
|
|
background-color: $bg-main;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: none;
|
|
|
|
.iconfont {
|
|
font-size: 28rpx;
|
|
color: $color-text-light;
|
|
}
|
|
}
|
|
}
|
|
|
|
.voice-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
margin-bottom: 48rpx;
|
|
|
|
.voice-wave {
|
|
width: 280rpx;
|
|
height: 160rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-bottom: 24rpx;
|
|
|
|
&.recording .wave-bar {
|
|
animation: wave 1.2s infinite ease-in-out;
|
|
}
|
|
|
|
.wave-bar {
|
|
width: 8rpx;
|
|
height: 40rpx;
|
|
margin: 0 4rpx;
|
|
background-color: $color-primary;
|
|
border-radius: 4rpx;
|
|
animation: none;
|
|
|
|
&:nth-child(1) { height: 60rpx; animation-delay: 0s; }
|
|
&:nth-child(2) { height: 90rpx; animation-delay: 0.1s; }
|
|
&:nth-child(3) { height: 50rpx; animation-delay: 0.2s; }
|
|
&:nth-child(4) { height: 120rpx; animation-delay: 0.3s; }
|
|
&:nth-child(5) { height: 70rpx; animation-delay: 0.4s; }
|
|
&:nth-child(6) { height: 100rpx; animation-delay: 0.5s; }
|
|
&:nth-child(7) { height: 80rpx; animation-delay: 0.6s; }
|
|
&:nth-child(8) { height: 40rpx; animation-delay: 0.7s; }
|
|
}
|
|
}
|
|
|
|
.voice-tip {
|
|
font-size: 28rpx;
|
|
color: $color-text-light;
|
|
}
|
|
}
|
|
|
|
.voice-actions {
|
|
display: flex;
|
|
justify-content: center;
|
|
|
|
.voice-btn {
|
|
width: 120rpx;
|
|
height: 120rpx;
|
|
border-radius: 50%;
|
|
background-color: $color-primary;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-shadow: 0 8rpx 24rpx rgba(108, 142, 191, 0.3);
|
|
border: none;
|
|
|
|
.iconfont {
|
|
font-size: 56rpx;
|
|
color: white;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@keyframes wave {
|
|
0%, 100% { transform: scaleY(0.5); }
|
|
50% { transform: scaleY(1.2); }
|
|
}
|
|
</style>
|