You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

705 lines
15 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view>
<scroll-view class="scroll-view" scroll-y scroll-with-animation :scroll-top="top">
<view style="padding: 30rpx 30rpx 240rpx;">
<view class="message" :class="[item.userType]" v-for="(item, index) in list" :key="index"
@click="msgClick(item, index)">
<image :src="item.avatar" v-if="item.userType === 'friend'" class="avatar" mode="widthFix"></image>
<view class="content" v-if="item.messageType === 'image'">
<image :src="item.content" mode="widthFix"></image>
</view>
<!-- 加载状态消息 -->
<view class="content loading-content" v-else-if="item.isLoading">
<view class="loading-dots">
<view class="dot" v-for="i in 3" :key="i"></view>
</view>
</view>
<view class="content voice-content" v-else-if="item.messageType === 'voice'">
<a-trumpet :isPlay="item.isplay" style="margin-right: 4rpx;"></a-trumpet>
{{ item.content }}
</view>
<view class="content" v-else>
{{ item.content }}
</view>
<image :src="item.avatar" v-if="item.userType === 'self'" class="avatar" mode="widthFix"></image>
</view>
</view>
</scroll-view>
<view class="tool">
<block v-if="messageType === 'text'">
<image src="../../static/voice.png" mode="widthFix" class="left-icon" @click="messageType = 'voice'">
</image>
<input type="text" v-model="content" class="input" @confirm="send" />
<image src="../../static/thumb.png" mode="widthFix" class="thumb" @click="send"></image>
</block>
<block v-else-if="messageType === 'voice'">
<image src="../../static/text.png" mode="widthFix" class="left-icon" @click="messageType = 'text'">
</image>
<text class="voice-crl" @touchstart="touchstart" @touchend="touchend">{{ recordStart ? '松开 发送' : '按住 说话'
}}</text>
</block>
</view>
<view v-if="recordStart" class="audio-animation">
<view class="audio-wave">
<text class="audio-wave-text" v-for="item in 10" :style="{ 'animation-delay': `${item / 10}s` }"></text>
<view class="text"> </view>
</view>
</view>
</view>
</template>
<script>
const innerAudioContext = uni.createInnerAudioContext();
const plugin = requirePlugin("WechatSI")
const manager = plugin.getRecordRecognitionManager()
import permision from '../../common/permission.js';
import {
getAIReply,
getChatLog
} from '@/services/api.js';
export default {
data() {
return {
person_id: '',
permisionState: false,
content: '',
list: [],
top: 0,
messageType: 'voice', // text 发送文本voice 发送语音
recordStart: false,
isLoading: false, // 添加loading状态变量
playingIndex: -1,
};
},
onShow() {
// uni.hideHomeButton()
},
onLoad(options) {
this.person_id = uni.getStorageSync('person_id');
uni.setNavigationBarTitle({
title: '芽芽星语'
})
this._friendAvatar = 'https://ww1.sinaimg.cn/mw690/a1a9fb72gy1hl1cd1znplj20j60j6n0h.jpg'
this._selfAvatar = 'https://ask.dcloud.net.cn/uploads/avatar/001/43/07/62_avatar_max.jpg?=1705916918'
getChatLog({
page: '1',
page_size: '10',
person_id: this.person_id
}).then((res) => {
const messages = [];
res.data.data.forEach(item => {
// 添加用户消息
messages.push({
content: item.user_input,
userType: 'self',
avatar: this._selfAvatar
});
// 添加AI回复消息
messages.push({
content: `${Math.ceil(item.duration)}''`,
audioSrc: item.audio_url,
userType: 'friend',
avatar: this._friendAvatar,
messageType: 'voice',
isplay: false // 明确设置初始值
});
// 使用数组赋值而不是push确保响应式更新
this.list = messages;
});
}).catch((err) => {
console.log(err)
}).finally(() => {
this.scrollToBottom();
})
// this.list = [{
// content: '历史消息',
// userType: 'friend',
// messageType: 'voice',
// avatar: this._friendAvatar
// },
// {
// content: '历史消息',
// userType: 'self',
// avatar: this._selfAvatar
// }
// ]
manager.onStart = () => {
console.log('==onStart==')
this.recordStart = true
};
manager.onStop = (res) => {
console.log('==onStop==', res)
this.recordStart = false
this.voicePath = res.tempFilePath;
this.voiceText = res.result;
if (this.voiceText == '') {
this.voiceText = '周围太安静啦~再试试~';
}
// 创建新的消息数组
const newList = [...this.list];
// 添加用户消息
newList.push({
content: this.voiceText,
userType: 'self',
avatar: this._selfAvatar
});
// 添加loading消息
const loadingIndex = newList.length;
newList.push({
content: 'loading',
userType: 'friend',
avatar: this._friendAvatar,
isLoading: true,
isplay: false
});
// 更新整个列表
this.list = newList;
this.scrollToBottom();
// this.list.push({
// content: this.voiceText,
// userType: 'self',
// avatar: this._selfAvatar
// })
// this.scrollToBottom();
// // 先添加一个loading状态的消息
// this.isLoading = true;
// this.list.push({
// content: 'loading',
// userType: 'friend',
// avatar: this._friendAvatar,
// isLoading: true,
// isplay: false
// })
// this.loadingMessageIndex = this.list.length - 1;
// this.scrollToBottom();
getAIReply({
prompt: this.voiceText,
person_id: this.person_id
}).then((res) => {
// 再次创建新数组以更新loading消息
const updatedList = [...this.list];
updatedList[loadingIndex] = {
content: `${Math.ceil(res.data.duration)}''`,
audioSrc: res.data.url,
userType: 'friend',
avatar: this._friendAvatar,
messageType: 'voice',
isplay: false
};
this.list = updatedList;
innerAudioContext.src = res.data.url;
innerAudioContext.play();
// 监听播放结束事件
innerAudioContext.onEnded(() => {
this.$set(this.list[loadingIndex], 'isplay', false);
});
// 监听错误事件
innerAudioContext.onError(() => {
this.$set(this.list[loadingIndex], 'isplay', false);
});
}).catch((err) => {
// 错误处理将loading消息更新为错误提示
this.list[this.loadingMessageIndex] = {
content: '抱歉,获取回复失败,请重试',
userType: 'friend',
avatar: this._friendAvatar,
isLoading: false
};
}).finally(() => {
this.isLoading = false;
this.scrollToBottom();
})
};
// 识别错误事件
manager.onError = (err) => {
uni.showToast({
title: '错误(' + err.retcode + ')' + err.msg,
icon: 'none',
duration: 2000, //持续时间为 2秒
})
};
},
onHide() {
if (this._innerAudioContext) {
this._innerAudioContext.stop()
}
},
created() {
this.checkPermission();
},
methods: {
upx2px(upx) {
return uni.upx2px(upx) + 'px';
},
send() {
if (this.content.length == 0) {
return;
}
const _content = this.content;
// 创建新的消息数组
const newList = [...this.list];
// 添加用户消息
newList.push({
content: this.content,
userType: 'self',
avatar: this._selfAvatar
});
// 添加loading消息
const loadingIndex = newList.length;
newList.push({
content: 'loading',
userType: 'friend',
avatar: this._friendAvatar,
isLoading: true,
isplay: false
});
// 更新整个列表
this.list = newList;
this.scrollToBottom();
this.content = '';
getAIReply({
prompt: _content,
person_id: this.person_id
}).then((res) => {
// 再次创建新数组以更新loading消息
const updatedList = [...this.list];
updatedList[loadingIndex] = {
content: `${Math.ceil(res.data.duration)}''`,
audioSrc: res.data.url,
userType: 'friend',
avatar: this._friendAvatar,
messageType: 'voice',
isplay: true
};
this.list = updatedList;
innerAudioContext.src = res.data.url;
innerAudioContext.play();
// 监听播放结束事件
innerAudioContext.onEnded(() => {
this.$set(this.list[loadingIndex], 'isplay', false);
});
// 监听错误事件
innerAudioContext.onError(() => {
this.$set(this.list[loadingIndex], 'isplay', false);
});
}).catch((err) => {
console.log(err)
// 错误处理将loading消息更新为错误提示
this.list[this.loadingMessageIndex] = {
content: '抱歉,获取回复失败,请重试',
userType: 'friend',
avatar: this._friendAvatar,
isLoading: false
};
}).finally(() => {
this.isLoading = false;
this.scrollToBottom();
})
},
scrollToBottom() {
this.top = this.list.length * 1000
},
msgClick(data, index) {
if (data.messageType === 'voice') {
// 先停止之前可能在播放的音频
innerAudioContext.stop();
// 重置所有消息的播放状态
this.list.forEach((msg, i) => {
if (msg.messageType === 'voice') {
this.$set(this.list[i], 'isplay', false);
}
});
// 设置当前消息的播放状态
this.$set(this.list[index], 'isplay', true);
// 播放音频
innerAudioContext.src = data.audioSrc;
innerAudioContext.play();
// 监听播放结束事件
innerAudioContext.onEnded(() => {
this.$set(this.list[index], 'isplay', false);
});
// 监听错误事件
innerAudioContext.onError(() => {
this.$set(this.list[index], 'isplay', false);
});
}
},
authTips() {
uni.showModal({
title: '提示',
content: '您拒绝了麦克风权限,将导致功能不能正常使用,去设置权限?',
confirmText: '去设置',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
uni.openSetting({
success: (res) => {
if (res.authSetting['scope.record']) {
console.log("已授权麦克风");
this._recordAuth = true
} else {
// 未授权
wx.showModal({
title: '提示',
content: '您未授权麦克风,功能将无法使用',
showCancel: false,
confirmText: '知道了'
})
}
}
})
}
}
})
},
touchstart() {
if (!this.permisionState) {
this.checkPermission();
return;
}
manager.start({
duration: 60000,
lang: "zh_CN"
})
},
async checkPermission() {
var that = this;
// #ifdef APP-PLUS
// 先判断os
let os = uni.getSystemInfoSync().osName;
if (os == 'ios') {
this.permisionState = await permision.judgeIosPermission('record');
} else {
this.permisionState = await permision.requestAndroidPermission('android.permission.RECORD_AUDIO');
}
if (this.permisionState !== true && this.permisionState !== 1) {
uni.showToast({
title: '请先授权使用录音',
icon: 'none'
});
return;
}
// #endif
// #ifdef MP-WEIXIN
uni.authorize({
scope: 'scope.record',
success(e) {
that.permisionState = true;
},
fail() {
uni.showToast({
title: '请授权使用录音',
icon: 'none'
});
}
});
// #endif
},
touchend() {
if (!this.recordStart) return
manager.stop();
},
//播放声音
play(src) {
this._innerAudioContext = wx.createInnerAudioContext()
this._innerAudioContext.src = src
this._innerAudioContext.play()
this._innerAudioContext.onPlay(() => {
console.log('开始播放')
})
this._innerAudioContext.onEnded(() => {
// 播放完毕,清除音频链接
console.log('播放完毕');
})
this._innerAudioContext.onError((res) => {
console.log('audio play error', res)
})
},
}
}
</script>
<style lang="scss" scoped>
.scroll-view {
/* #ifdef H5 */
height: calc(100vh - 44px);
/* #endif */
/* #ifndef H5 */
height: 100vh;
/* #endif */
background: #eee;
box-sizing: border-box;
}
.message {
display: flex;
align-items: flex-start;
margin-bottom: 30rpx;
.avatar {
width: 80rpx;
height: 80rpx;
border-radius: 10rpx;
margin-right: 30rpx;
}
.content {
min-height: 86rpx;
max-width: 60vw;
box-sizing: border-box;
font-size: 28rpx;
line-height: 1.3;
padding: 20rpx;
border-radius: 10rpx;
background: #fff;
display: flex;
align-items: center;
image {
width: 200rpx;
}
}
.voice-content {
display: flex;
align-items: center;
width: 24vw;
}
&.self {
justify-content: flex-end;
.avatar {
margin: 0 0 0 30rpx;
}
.content {
position: relative;
&::after {
position: absolute;
content: '';
width: 0;
height: 0;
border: 16rpx solid transparent;
border-left: 16rpx solid #fff;
right: -28rpx;
top: 24rpx;
}
}
}
&.friend {
.content {
position: relative;
&::after {
position: absolute;
content: '';
width: 0;
height: 0;
border: 16rpx solid transparent;
border-right: 16rpx solid #fff;
left: -28rpx;
top: 24rpx;
}
}
}
}
.tool {
position: fixed;
width: 100%;
min-height: 120rpx;
left: 0;
bottom: 0;
background: #fff;
display: flex;
align-items: center;
box-sizing: border-box;
padding: 20rpx 24rpx 20rpx 40rpx;
padding-bottom: calc(20rpx + constant(safe-area-inset-bottom)/2) !important;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom)/2) !important;
.left-icon {
width: 56rpx;
height: 56rpx;
margin-right: 10rpx;
}
.input,
.voice-crl {
background: #eee;
border-radius: 10rpx;
height: 70rpx;
margin-left: 10rpx;
margin-right: 20rpx;
flex: 1;
padding: 0 20rpx;
box-sizing: border-box;
font-size: 28rpx;
}
.thumb {
width: 64rpx;
height: 64rpx;
}
.voice-crl {
text-align: center;
line-height: 70rpx;
font-weight: bold;
}
}
.audio-animation {
position: fixed;
// width: 100vw;
// height: 100vh;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
z-index: 202410;
display: flex;
justify-content: center;
align-items: center;
.text {
text-align: center;
font-size: 28rpx;
color: #333;
margin-top: 60rpx;
}
.audio-wave {
padding: 50rpx;
.audio-wave-text {
background-color: blue;
width: 7rpx;
height: 12rpx;
margin: 0 6rpx;
border-radius: 5rpx;
display: inline-block;
border: none;
animation: wave 0.25s ease-in-out;
animation-iteration-count: infinite;
animation-direction: alternate;
}
/* 声波动画 */
@keyframes wave {
from {
transform: scaleY(1);
}
to {
transform: scaleY(4);
}
}
}
}
.loading-content {
min-width: 100rpx;
display: flex;
justify-content: center;
align-items: center;
}
.loading-dots {
display: flex;
align-items: center;
}
.dot {
width: 12rpx;
height: 12rpx;
background-color: #999;
border-radius: 50%;
margin: 0 6rpx;
animation: dot-animation 1.4s infinite ease-in-out both;
}
.dot:nth-child(1) {
animation-delay: -0.32s;
}
.dot:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes dot-animation {
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
</style>