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>
|
|
||||||
184
manifest.json
184
manifest.json
@@ -1,94 +1,100 @@
|
|||||||
{
|
{
|
||||||
"name": "YGTianmuCS",
|
"name": "YGTianmuCS",
|
||||||
"appid": "__UNI__BB03E8A",
|
"appid": "__UNI__BB03E8A",
|
||||||
"description": "",
|
"description": "",
|
||||||
"versionName": "1.0.0",
|
"versionName": "1.0.0",
|
||||||
"versionCode": "100",
|
"versionCode": "100",
|
||||||
"transformPx": false,
|
"transformPx": false,
|
||||||
/* 5+App特有相关 */
|
/* 5+App特有相关 */
|
||||||
"app-plus": {
|
"app-plus": {
|
||||||
"usingComponents": true,
|
"usingComponents": true,
|
||||||
"nvueStyleCompiler": "uni-app",
|
"nvueStyleCompiler": "uni-app",
|
||||||
"compilerVersion": 3,
|
"compilerVersion": 3,
|
||||||
"splashscreen": {
|
"splashscreen": {
|
||||||
"alwaysShowBeforeRender": true,
|
"alwaysShowBeforeRender": true,
|
||||||
"waiting": true,
|
"waiting": true,
|
||||||
"autoclose": true,
|
"autoclose": true,
|
||||||
"delay": 0
|
"delay": 0
|
||||||
|
},
|
||||||
|
"safearea": {
|
||||||
|
"bottom": {
|
||||||
|
"offset": "auto" // 自动适配安全区域
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/* 模块配置 */
|
||||||
|
"modules": {},
|
||||||
|
/* 应用发布信息 */
|
||||||
|
"distribute": {
|
||||||
|
/* android打包配置 */
|
||||||
|
"android": {
|
||||||
|
"permissions": [
|
||||||
|
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||||
|
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||||
|
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
/* ios打包配置 */
|
||||||
|
"ios": {},
|
||||||
|
/* SDK配置 */
|
||||||
|
"sdkConfigs": {
|
||||||
|
"oauth": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"safearea": {
|
/* 快应用特有相关 */
|
||||||
"bottom": {
|
"quickapp": {},
|
||||||
"offset": "auto" // 自动适配安全区域
|
/* 小程序特有相关 */
|
||||||
}
|
"mp-weixin": {
|
||||||
|
"appid": "wx5e79df5996572539",
|
||||||
|
"setting": {
|
||||||
|
"urlCheck": false
|
||||||
|
},
|
||||||
|
"usingComponents": true,
|
||||||
|
"requiredPrivateInfos": ["getLocation"],
|
||||||
|
"permission": {
|
||||||
|
"scope.userLocation": {
|
||||||
|
"desc": "用于获取当前所在城市信息"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"WechatSI": {
|
||||||
|
"version": "0.3.6",
|
||||||
|
"provider": "wx069ba97219f66d99"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
/* 模块配置 */
|
"mp-alipay": {
|
||||||
"modules": {},
|
"usingComponents": true
|
||||||
/* 应用发布信息 */
|
},
|
||||||
"distribute": {
|
"mp-baidu": {
|
||||||
/* android打包配置 */
|
"usingComponents": true
|
||||||
"android": {
|
},
|
||||||
"permissions": [
|
"mp-toutiao": {
|
||||||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
"usingComponents": true
|
||||||
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
},
|
||||||
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
"uniStatistics": {
|
||||||
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
"enable": false
|
||||||
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
},
|
||||||
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
"vueVersion": "3",
|
||||||
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
"h5": {
|
||||||
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
"router": {
|
||||||
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
"base": "./",
|
||||||
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
"mode": "hash"
|
||||||
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
},
|
||||||
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
"devServer": {
|
||||||
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
"https": false
|
||||||
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
}
|
||||||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
/* ios打包配置 */
|
|
||||||
"ios": {},
|
|
||||||
/* SDK配置 */
|
|
||||||
"sdkConfigs": {
|
|
||||||
"oauth": {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
/* 快应用特有相关 */
|
|
||||||
"quickapp": {},
|
|
||||||
/* 小程序特有相关 */
|
|
||||||
"mp-weixin": {
|
|
||||||
"appid": "wx5e79df5996572539",
|
|
||||||
"setting": {
|
|
||||||
"urlCheck": false
|
|
||||||
},
|
|
||||||
"usingComponents": true,
|
|
||||||
"requiredPrivateInfos": ["getLocation"],
|
|
||||||
"permission": {
|
|
||||||
"scope.userLocation": {
|
|
||||||
"desc": "用于获取当前所在城市信息"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mp-alipay": {
|
|
||||||
"usingComponents": true
|
|
||||||
},
|
|
||||||
"mp-baidu": {
|
|
||||||
"usingComponents": true
|
|
||||||
},
|
|
||||||
"mp-toutiao": {
|
|
||||||
"usingComponents": true
|
|
||||||
},
|
|
||||||
"uniStatistics": {
|
|
||||||
"enable": false
|
|
||||||
},
|
|
||||||
"vueVersion": "3",
|
|
||||||
"h5": {
|
|
||||||
"router": {
|
|
||||||
"base": "./",
|
|
||||||
"mode": "hash"
|
|
||||||
},
|
|
||||||
"devServer": {
|
|
||||||
"https": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="input-area-wrapper">
|
<view class="input-area-wrapper">
|
||||||
<view class="area-input">
|
<view v-if="!visibleWaveBtn" class="area-input">
|
||||||
<!-- 语音/键盘切换 -->
|
<!-- 语音/键盘切换 -->
|
||||||
<view class="input-container-voice" @click="toggleVoiceMode">
|
<view class="input-container-voice" @click="toggleVoiceMode">
|
||||||
<image v-if="!isVoiceMode" src="/static/input_voice_icon.png"></image>
|
<image v-if="!isVoiceMode" src="/static/input_voice_icon.png"></image>
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
<view class="input-button-container">
|
<view class="input-button-container">
|
||||||
<textarea
|
<textarea
|
||||||
ref="textareaRef"
|
ref="textareaRef"
|
||||||
|
v-if="!isVoiceMode"
|
||||||
:class="['textarea', ios ? 'ios' : 'android']"
|
:class="['textarea', ios ? 'ios' : 'android']"
|
||||||
type="text"
|
type="text"
|
||||||
cursor-spacing="65"
|
cursor-spacing="65"
|
||||||
@@ -33,9 +34,11 @@
|
|||||||
<view
|
<view
|
||||||
v-if="isVoiceMode"
|
v-if="isVoiceMode"
|
||||||
class="hold-to-talk-button"
|
class="hold-to-talk-button"
|
||||||
@click.stop="startRecording"
|
@longpress="handleVoiceTouchStart"
|
||||||
|
@touchend="handleVoiceTouchEnd"
|
||||||
|
@touchcancel="handleVoiceTouchEnd"
|
||||||
>
|
>
|
||||||
按住说话
|
按住 说话
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -50,27 +53,17 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 使用封装的弹窗组件 -->
|
<!-- 录音按钮 -->
|
||||||
<RecordingPopup
|
<RecordingWaveBtn v-if="visibleWaveBtn" ref="recordingWaveBtnRef" />
|
||||||
ref="recordingPopupRef"
|
|
||||||
:is-slide-to-text="isSlideToText"
|
|
||||||
@cancel="handleRecordingCancel"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<VoiceResultPopup
|
|
||||||
ref="voiceResultPopupRef"
|
|
||||||
:voice-text="voiceText"
|
|
||||||
@cancel="cancelVoice"
|
|
||||||
@sendVoice="handleSendVoice"
|
|
||||||
@sendText="handleSendText"
|
|
||||||
/>
|
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, nextTick, onMounted, computed } from "vue";
|
import { ref, watch, nextTick, onMounted, computed, defineExpose } from "vue";
|
||||||
import RecordingPopup from "@/components/Speech/RecordingPopup.vue";
|
import RecordingWaveBtn from "@/components/Speech/RecordingWaveBtn.vue";
|
||||||
import VoiceResultPopup from "@/components/Speech/VoiceResultPopup.vue";
|
|
||||||
|
const plugin = requirePlugin("WechatSI");
|
||||||
|
const manager = plugin.getRecordRecognitionManager();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: String,
|
modelValue: String,
|
||||||
@@ -88,19 +81,13 @@ const emit = defineEmits([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const textareaRef = ref(null);
|
const textareaRef = ref(null);
|
||||||
|
const recordingWaveBtnRef = ref(null);
|
||||||
const placeholder = ref("快告诉朵朵您在想什么~");
|
const placeholder = ref("快告诉朵朵您在想什么~");
|
||||||
const inputMessage = ref(props.modelValue || "");
|
const inputMessage = ref(props.modelValue || "");
|
||||||
const isFocused = ref(false);
|
const isFocused = ref(false);
|
||||||
const keyboardHeight = ref(0);
|
const keyboardHeight = ref(0);
|
||||||
const isVoiceMode = ref(false);
|
const isVoiceMode = ref(false);
|
||||||
const isRecording = ref(false);
|
const visibleWaveBtn = ref(false);
|
||||||
const recordingTime = ref(0);
|
|
||||||
const recordingTimer = ref(null);
|
|
||||||
const voiceText = ref("");
|
|
||||||
const showVoiceResult = ref(false);
|
|
||||||
const isSlideToText = ref(false);
|
|
||||||
const recordingPopupRef = ref(null);
|
|
||||||
const voiceResultPopupRef = ref(null);
|
|
||||||
|
|
||||||
// 判断当前平台是否为iOS
|
// 判断当前平台是否为iOS
|
||||||
const ios = computed(() => {
|
const ios = computed(() => {
|
||||||
@@ -128,106 +115,52 @@ const toggleVoiceMode = () => {
|
|||||||
isVoiceMode.value = !isVoiceMode.value;
|
isVoiceMode.value = !isVoiceMode.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 开始录音
|
// 处理语音按钮长按开始
|
||||||
const startRecording = () => {
|
const handleVoiceTouchStart = () => {
|
||||||
console.log("startRecording");
|
manager.start({ lang: "zh_CN" });
|
||||||
isRecording.value = true;
|
|
||||||
recordingTime.value = 0;
|
|
||||||
// 启动录音计时器
|
|
||||||
recordingTimer.value = setInterval(() => {
|
|
||||||
recordingTime.value += 1;
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
// 打开录音弹窗
|
visibleWaveBtn.value = true;
|
||||||
if (recordingPopupRef.value) {
|
|
||||||
recordingPopupRef.value.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用uni-app录音API
|
// 启动音频条动画
|
||||||
uni.startRecord({
|
nextTick(() => {
|
||||||
success: (res) => {
|
if (recordingWaveBtnRef.value) {
|
||||||
// 录音成功,处理录音文件
|
recordingWaveBtnRef.value.startAnimation();
|
||||||
const tempFilePath = res.tempFilePath;
|
}
|
||||||
// 这里可以添加语音转文字的逻辑
|
|
||||||
// 模拟语音转文字
|
|
||||||
setTimeout(() => {
|
|
||||||
voiceText.value = "这是语音转文字的结果";
|
|
||||||
showVoiceResult.value = true;
|
|
||||||
// 打开语音结果弹窗
|
|
||||||
if (voiceResultPopupRef.value) {
|
|
||||||
voiceResultPopupRef.value.open();
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
},
|
|
||||||
fail: (err) => {
|
|
||||||
console.error("录音失败:", err);
|
|
||||||
isRecording.value = false;
|
|
||||||
clearInterval(recordingTimer.value);
|
|
||||||
if (recordingPopupRef.value) {
|
|
||||||
recordingPopupRef.value.close();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理录音弹窗取消
|
// 处理语音按钮长按结束
|
||||||
const handleRecordingCancel = () => {
|
const handleVoiceTouchEnd = () => {
|
||||||
isRecording.value = false;
|
manager.stop();
|
||||||
clearInterval(recordingTimer.value);
|
|
||||||
uni.stopRecord();
|
// 停止音频条动画
|
||||||
|
if (recordingWaveBtnRef.value) {
|
||||||
|
recordingWaveBtnRef.value.stopAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
visibleWaveBtn.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理发送原语音
|
// 处理发送原语音
|
||||||
const handleSendVoice = (data) => {
|
const initRecord = () => {
|
||||||
// 发送语音逻辑
|
manager.onRecognize = (res) => {
|
||||||
emit("sendVoice", {
|
let text = res.result;
|
||||||
text: data.text,
|
inputMessage.value = text;
|
||||||
// 可以添加语音文件路径等信息
|
};
|
||||||
});
|
// 识别结束事件
|
||||||
showVoiceResult.value = false;
|
manager.onStop = (res) => {
|
||||||
isVoiceMode.value = false;
|
console.log(res, 37);
|
||||||
};
|
let text = res.result;
|
||||||
|
|
||||||
// 处理发送文本
|
if (text == "") {
|
||||||
const handleSendText = (data) => {
|
console.log("没有说话");
|
||||||
// 发送文本逻辑
|
return;
|
||||||
emit("sendVoice", {
|
|
||||||
text: data.text,
|
|
||||||
// 可以添加语音文件路径等信息
|
|
||||||
});
|
|
||||||
showVoiceResult.value = false;
|
|
||||||
isVoiceMode.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 取消语音
|
|
||||||
const cancelVoice = () => {
|
|
||||||
showVoiceResult.value = false;
|
|
||||||
isVoiceMode.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 测试弹窗方法
|
|
||||||
const testPopup = () => {
|
|
||||||
// 模拟开始录音,打开录音弹窗
|
|
||||||
isRecording.value = true;
|
|
||||||
console.log("===========1");
|
|
||||||
|
|
||||||
if (recordingPopupRef.value) {
|
|
||||||
console.log("===========2");
|
|
||||||
|
|
||||||
recordingPopupRef.value.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2秒后关闭录音弹窗,打开语音结果弹窗
|
|
||||||
setTimeout(() => {
|
|
||||||
if (recordingPopupRef.value) {
|
|
||||||
recordingPopupRef.value.close();
|
|
||||||
}
|
}
|
||||||
voiceText.value = "测试语音转文字结果";
|
|
||||||
showVoiceResult.value = true;
|
inputMessage.value = text;
|
||||||
if (voiceResultPopupRef.value) {
|
// 在语音识别完成后发送消息
|
||||||
voiceResultPopupRef.value.open();
|
emit("send", text);
|
||||||
}
|
};
|
||||||
}, 3000);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 监听键盘高度变化
|
// 监听键盘高度变化
|
||||||
@@ -235,20 +168,17 @@ onMounted(() => {
|
|||||||
// 监听键盘弹起
|
// 监听键盘弹起
|
||||||
uni.onKeyboardHeightChange((res) => {
|
uni.onKeyboardHeightChange((res) => {
|
||||||
keyboardHeight.value = res.height;
|
keyboardHeight.value = res.height;
|
||||||
if (res.height > 0) {
|
if (res.height) {
|
||||||
emit("keyboardShow", res.height);
|
emit("keyboardShow", res.height);
|
||||||
} else {
|
} else {
|
||||||
emit("keyboardHide");
|
emit("keyboardHide");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
initRecord();
|
||||||
});
|
});
|
||||||
|
|
||||||
const sendMessage = () => {
|
const sendMessage = () => {
|
||||||
if (isVoiceMode.value) {
|
|
||||||
testPopup();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.isSessionActive) {
|
if (props.isSessionActive) {
|
||||||
// 如果会话进行中,调用停止请求函数
|
// 如果会话进行中,调用停止请求函数
|
||||||
if (props.stopRequest) {
|
if (props.stopRequest) {
|
||||||
@@ -300,18 +230,13 @@ const blurInput = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 暴露方法给父组件
|
// 暴露方法给父组件
|
||||||
defineExpose({
|
defineExpose({ focusInput });
|
||||||
focusInput,
|
|
||||||
blurInput,
|
|
||||||
isFocused,
|
|
||||||
toggleVoiceMode,
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.area-input {
|
.area-input {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-end;
|
||||||
border-radius: 22px;
|
border-radius: 22px;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
box-shadow: 0px 0px 20px 0px rgba(52, 25, 204, 0.05);
|
box-shadow: 0px 0px 20px 0px rgba(52, 25, 204, 0.05);
|
||||||
@@ -338,13 +263,16 @@ defineExpose({
|
|||||||
|
|
||||||
.hold-to-talk-button {
|
.hold-to-talk-button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 44px;
|
||||||
color: #333333;
|
color: #333333;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-container-send {
|
.input-container-send {
|
||||||
@@ -378,11 +306,11 @@ defineExpose({
|
|||||||
line-height: normal;
|
line-height: normal;
|
||||||
|
|
||||||
&.android {
|
&.android {
|
||||||
padding: 11px 0;
|
padding: 12px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.ios {
|
&.ios {
|
||||||
padding: 4px 0;
|
padding: 6px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
|
|
||||||
<view class="container">
|
<view class="container">
|
||||||
<ChatMainList/>
|
<ChatMainList />
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import ChatMainList from "../chat/ChatMainList.vue";
|
import ChatMainList from "../chat/ChatMainList.vue";
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
Reference in New Issue
Block a user