feat: 语音输入交互完善
This commit is contained in:
@@ -1,107 +0,0 @@
|
||||
<template>
|
||||
<uni-popup position="center" :mask-click-able="false" ref="popupRef" type="center" :safe-area="false" :custom-style="{width: '100%', height: '100vh', borderRadius: 0, position: 'fixed', top: '0', left: '0', zIndex: 9999}">
|
||||
<view class="recording-popup">
|
||||
<view class="recording-wave">
|
||||
|
||||
<!-- 波形动画 -->
|
||||
<view class="wave-animation"></view>
|
||||
</view>
|
||||
<view class="recording-text">
|
||||
{{ isSlideToText ? '松开发送 转文字' : '松开发送' }}
|
||||
</view>
|
||||
<view class="recording-cancel" @click="handleCancel">
|
||||
取消
|
||||
</view>
|
||||
</view>
|
||||
</uni-popup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
isSlideToText: Boolean
|
||||
})
|
||||
|
||||
const emit = defineEmits(['cancel'])
|
||||
|
||||
const popupRef = ref(null)
|
||||
|
||||
// 打开弹窗
|
||||
const open = () => {
|
||||
if (popupRef.value) {
|
||||
popupRef.value.open()
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
const close = () => {
|
||||
if (popupRef.value) {
|
||||
popupRef.value.close()
|
||||
}
|
||||
}
|
||||
|
||||
// 处理取消
|
||||
const handleCancel = () => {
|
||||
emit('cancel')
|
||||
close()
|
||||
}
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
open,
|
||||
close
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 录音弹窗样式 */
|
||||
.recording-popup {
|
||||
width: 100% !important;
|
||||
height: 100vh !important;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
border-radius: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: white;
|
||||
padding: 40px 0;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.recording-wave {
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(76, 217, 100, 0.3);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.wave-animation {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
/* 这里可以添加波形动画 */
|
||||
background-image: url('/static/wave_icon.png');
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.recording-text {
|
||||
font-size: 20px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.recording-cancel {
|
||||
font-size: 18px;
|
||||
color: #CCCCCC;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
129
components/Speech/RecordingWaveBtn.vue
Normal file
129
components/Speech/RecordingWaveBtn.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<view class="recording-wave-btn">
|
||||
<view class="audio-visualizer">
|
||||
<view
|
||||
v-for="(bar, index) in audioBars"
|
||||
:key="index"
|
||||
class="audio-bar"
|
||||
:style="{
|
||||
height: bar.height + 'px',
|
||||
transition: 'height 0.1s ease-out',
|
||||
}"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
|
||||
const animationTimer = ref(null);
|
||||
const isAnimating = ref(false);
|
||||
|
||||
// 音频条数据
|
||||
const audioBars = ref([]);
|
||||
const BAR_COUNT = 30;
|
||||
|
||||
// 初始化音频条
|
||||
const initAudioBars = () => {
|
||||
audioBars.value = [];
|
||||
for (let i = 0; i < BAR_COUNT; i++) {
|
||||
audioBars.value.push({
|
||||
height: 4 + Math.random() * 8,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 更新音频条动画
|
||||
const updateAudioBars = () => {
|
||||
if (!isAnimating.value) return;
|
||||
|
||||
// 使用 for 循环随机修改每个音频条的高度
|
||||
for (let i = 0; i < audioBars.value.length; i++) {
|
||||
const bar = audioBars.value[i];
|
||||
|
||||
// 生成随机高度值
|
||||
const minHeight = 4;
|
||||
const maxHeight = 20;
|
||||
const randomHeight = minHeight + Math.random() * (maxHeight - minHeight);
|
||||
|
||||
// 添加一些变化幅度,让高度变化更自然
|
||||
const variation = (Math.random() - 0.5) * 10;
|
||||
bar.height = Math.max(
|
||||
minHeight,
|
||||
Math.min(maxHeight, randomHeight + variation)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// 开始动画
|
||||
const startAnimation = () => {
|
||||
if (!isAnimating.value) {
|
||||
isAnimating.value = true;
|
||||
|
||||
const animate = () => {
|
||||
if (isAnimating.value) {
|
||||
updateAudioBars();
|
||||
animationTimer.value = setTimeout(animate, 200); // 每200ms更新一次,模仿人类说话语速
|
||||
}
|
||||
};
|
||||
|
||||
animate();
|
||||
}
|
||||
};
|
||||
|
||||
// 停止动画
|
||||
const stopAnimation = () => {
|
||||
isAnimating.value = false;
|
||||
if (animationTimer.value) {
|
||||
clearTimeout(animationTimer.value);
|
||||
animationTimer.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载时初始化并自动开始动画
|
||||
onMounted(() => {
|
||||
initAudioBars();
|
||||
startAnimation();
|
||||
});
|
||||
|
||||
// 组件卸载时停止动画
|
||||
onUnmounted(() => {
|
||||
stopAnimation();
|
||||
});
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
startAnimation,
|
||||
stopAnimation,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.recording-wave-btn {
|
||||
box-shadow: 0px 0px 20px 0px rgba(52, 25, 204, 0.05);
|
||||
margin: 0 12px;
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #00a6ff;
|
||||
height: 44px;
|
||||
border-radius: 50px;
|
||||
}
|
||||
|
||||
.audio-visualizer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 20px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.audio-bar {
|
||||
width: 2px;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -1,140 +0,0 @@
|
||||
<template>
|
||||
<uni-popup position="center" :mask-click-able="false" ref="popupRef" type="center" :safe-area="false" :custom-style="{ width: '100%', maxWidth: '100vw', borderRadius: 0, position: 'fixed', top: '0', left: '0', zIndex: 9999 }">
|
||||
<view class="voice-result-popup">
|
||||
<view class="voice-result-bubble">
|
||||
<textarea v-model="editedVoiceText" class="editable-textarea" placeholder="语音转换结果"></textarea>
|
||||
</view>
|
||||
<view class="voice-result-actions">
|
||||
<view class="action-button cancel" @click="handleCancel">取消</view>
|
||||
<view class="action-button send" @click="handleSendText">发送</view>
|
||||
</view>
|
||||
</view>
|
||||
</uni-popup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
voiceText: String
|
||||
})
|
||||
|
||||
const emit = defineEmits(['cancel', 'sendText'])
|
||||
|
||||
const popupRef = ref(null)
|
||||
const editedVoiceText = ref(props.voiceText || '')
|
||||
|
||||
// 监听props变化,更新编辑框内容
|
||||
watch(() => props.voiceText,
|
||||
(newValue) => {
|
||||
editedVoiceText.value = newValue || ''
|
||||
}
|
||||
)
|
||||
|
||||
// 打开弹窗
|
||||
const open = () => {
|
||||
if (popupRef.value) {
|
||||
popupRef.value.open()
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
const close = () => {
|
||||
if (popupRef.value) {
|
||||
popupRef.value.close()
|
||||
}
|
||||
}
|
||||
|
||||
// 处理取消
|
||||
const handleCancel = () => {
|
||||
emit('cancel')
|
||||
close()
|
||||
}
|
||||
|
||||
// 处理发送文本
|
||||
const handleSendText = () => {
|
||||
emit('sendText', { text: editedVoiceText.value })
|
||||
close()
|
||||
}
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
open,
|
||||
close
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 语音结果弹窗样式 */
|
||||
.voice-result-popup {
|
||||
width: 100% !important;
|
||||
height: 100vh;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
padding: 40px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.voice-result-bubble {
|
||||
background-color: #00A6FF;
|
||||
color: white;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 40px;
|
||||
min-height: 120px;
|
||||
max-height: 400px;
|
||||
font-size: 16px;
|
||||
overflow-y: auto;
|
||||
|
||||
box-shadow: 2px 2px 6px 0px rgba(0,0,0,0.1);
|
||||
border-radius: 20px 4px 20px 20px;
|
||||
border: 1px solid;
|
||||
border-color: #FFFFFF;
|
||||
}
|
||||
|
||||
.editable-textarea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.editable-textarea:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.voice-result-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
margin-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
padding: 12px 24px;
|
||||
border-radius: 30px;
|
||||
font-size: 18px;
|
||||
min-width: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cancel {
|
||||
color: #F5F5F5;
|
||||
border: 1px solid;
|
||||
border-color: #F5F5F5;
|
||||
}
|
||||
|
||||
.send {
|
||||
color: #333;
|
||||
background-color: #F5F5F5;
|
||||
}
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user