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

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 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>