750 lines
17 KiB
Vue
750 lines
17 KiB
Vue
<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> |