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.

750 lines
17 KiB

4 months ago
<template>
<view class="page">
<tabbar :index="1"></tabbar>
<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"></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="chooseImage"></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 './permission.js';
import {
getAIReply,
getChatLog
} from '@/config/api.js';
export default {
data() {
return {
permisionState: false,
content: '',
list: [],
top: 0,
messageType: 'voice', // text 发送文本voice 发送语音
recordStart: false,
isLoading: false, // 添加loading状态变量
playingIndex: -1,
};
},
onLoad(options) {
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',
session_id: '1'
}).then((res) => {
console.log(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
// }, {
// content: '历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,',
// userType: 'friend',
// avatar: this._friendAvatar
// },
// {
// content: '历史消息',
// userType: 'self',
// avatar: this._selfAvatar
// }, {
// content: '历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,历史消息,',
// userType: 'friend',
// 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,
session_id: '1'
}).then((res) => {
// this.list[this.loadingMessageIndex] = {
// content: `${Math.ceil(res.data.duration)}''`,
// audioSrc: res.data.url,
// userType: 'friend',
// avatar: this._friendAvatar,
// messageType: 'voice',
// isplay: false
// }
// 再次创建新数组以更新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;
}).catch((err) => {
console.log(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() {
this.list.push({
content: this.content,
userType: 'self',
avatar: this._selfAvatar
})
this.content = ''
this.scrollToBottom()
// 模拟对方回复
setTimeout(() => {
this.list.push({
content: '好的',
userType: 'friend',
avatar: this._friendAvatar
})
this.scrollToBottom()
}, 1500)
},
chooseImage() {
uni.chooseImage({
// sourceType: 'album',
success: (res) => {
this.list.push({
content: res.tempFilePaths[0],
userType: 'self',
messageType: 'image',
avatar: this._selfAvatar
})
this.scrollToBottom()
// 模拟对方回复
setTimeout(() => {
this.list.push({
content: '风景好漂亮啊~',
userType: 'friend',
avatar: this._friendAvatar
})
this.scrollToBottom()
}, 1500)
}
})
},
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);
});
}
/*
if (data.messageType === 'voice') {
// 设置当前播放的消息索引
// data.isplay = true;
this.list[index].isplay = true;
innerAudioContext.src = data.audioSrc;
innerAudioContext.play();
// 监听播放结束事件
innerAudioContext.onEnded(() => {
// 播放完毕,重置播放状态
// data.isplay = false;
this.list[index].isplay = false;
});
// 监听错误事件
innerAudioContext.onError(() => {
// data.isplay = false;
this.list[index].isplay = false;
});
}
*/
arguments
},
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"
})
/*
//开始录音
const _permission = 'scope.record'
uni.getSetting({
success: (res) => {
// 判断是否有相关权限属性
if (res.authSetting.hasOwnProperty(_permission)) {
// 属性存在且为false用户拒绝过权限
if (!res.authSetting[_permission]) {
this.authTips()
} else {
// 已授权
this._recordAuth = true
manager.start({
duration: 60000,
lang: "zh_CN"
})
}
} else {
// 属性不存在,需要授权
uni.authorize({
scope: _permission,
success: () => {
// 授权成功
this._recordAuth = true
},
fail: (res) => {
// 104 未授权隐私协议
// 用户可能拒绝官方隐私授权弹窗,为了避免过度弹窗打扰用户,开发者再次调用隐私相关接口时,
// * 若距上次用户拒绝不足10秒将不再触发弹窗直接给到开发者用户拒绝隐私授权弹窗的报错
if (res.errno == 104) {
uni.showModal({
title: '温馨提示',
content: '您拒绝了隐私协议,请稍后再试',
confirmText: '知道了',
showCancel: false,
success: () => {}
})
} else {
// 用户拒绝授权
this.authTips()
}
}
})
}
}
})
*/
},
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: 80rpx;
max-width: 60vw;
box-sizing: border-box;
font-size: 28rpx;
line-height: 1.3;
padding: 20rpx;
border-radius: 10rpx;
background: #fff;
image {
width: 200rpx;
}
}
.voice-content {
display: flex;
align-items: center;
}
&.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-right: 30rpx;
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>