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.
536 lines
13 KiB
536 lines
13 KiB
<template>
|
|
<view class="v-pages Ai">
|
|
<!-- 对话框 -->
|
|
<scroll-view class="scroll-view" scroll-y scroll-with-animation :scroll-top="top">
|
|
<view class="v-ai-list">
|
|
<view class="list_item" :class="item.userType" v-for="(item,index) in list" :key="index">
|
|
<view v-if="item.userType=='self'">
|
|
<view class="text-name row flex-align-center">
|
|
<image class="img" mode="widthFix" src="https://eluyou.ailuquan.cn/upload/image/2024/mapIcon/daolan/ai-self.png"></image>
|
|
<text>E鹿小助手</text>
|
|
</view>
|
|
<view class="text-box">{{item.content}}</view>
|
|
</view>
|
|
<view class="row row flex-align-center" style="justify-content: flex-end;"
|
|
v-if="item.userType=='friend'">
|
|
<view class="text-box">{{item.content}}</view>
|
|
<view class="text-name row flex-align-center">
|
|
<image class="img" mode="widthFix" src="https://eluyou.ailuquan.cn/upload/image/2024/mapIcon/daolan/ai-friend.png"></image>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</scroll-view>
|
|
<!-- 语音/文字发送 -->
|
|
<view class="tool flex-align-center">
|
|
<block v-if="messageType === 'text'">
|
|
<image src="https://eluyou.ailuquan.cn/upload/image/2024/mapIcon/daolan/icon-voice.png" mode="widthFix" class="left-icon"
|
|
@click="messageType='voice'">
|
|
</image>
|
|
<input type="text" v-model="content" class="input" @confirm="sendMsg" />
|
|
</block>
|
|
<block v-if="messageType === 'voice'">
|
|
<image src="https://eluyou.ailuquan.cn/upload/image/2024/mapIcon/daolan/icon-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 recorderManager = wx.getRecorderManager()
|
|
export default {
|
|
data() {
|
|
return {
|
|
content: '',
|
|
list: [{
|
|
content: "Hi,你好,E鹿小助手很高兴为您提供咨询服务。",
|
|
userType: 'self',
|
|
}],
|
|
top: '-90rpx',
|
|
//发送类型
|
|
messageType: 'voice', // text 发送文本;voice 发送语音
|
|
recordStart: false,
|
|
identity: "",
|
|
//websocket
|
|
Token:null ,
|
|
userInfo: {},
|
|
wsInfo: {
|
|
ws: null, // ws对象
|
|
alive: false, // 是否连接
|
|
isLogin: false, // 是否登录
|
|
isJoin: false, // 是否加入
|
|
lock: false, // 锁住重连
|
|
reconnectTimer: null, // 重连计时
|
|
reconnectTime: 5000, // 重连计时间隔
|
|
clientTimer: null, // 客户端计时
|
|
clientTime: 10000, // 客户端计时间隔
|
|
serverTimer: null, // 服务端计时
|
|
serverTime: 30000, // 服务端计时间隔
|
|
}
|
|
}
|
|
},
|
|
|
|
onShow() {
|
|
const userInfo = uni.getStorageSync("userInfo");
|
|
this.userInfo = {
|
|
"message": "",
|
|
"dialogId": "",
|
|
"identity": "",
|
|
"userId": userInfo.userId
|
|
};
|
|
this.Token = userInfo.accessToken;
|
|
this.connectWs()
|
|
},
|
|
methods: {
|
|
// websocket
|
|
connectWs() {
|
|
const $this = this
|
|
this.wsInfo.ws = null;
|
|
this.wsInfo.ws = uni.connectSocket({
|
|
url: 'wss://eluyou.ailuquan.cn/prod-api/infra/ws?token='+this.Token,
|
|
success() {
|
|
$this.wsInfo.alive = true;
|
|
console.log("ws连接成功!");
|
|
},
|
|
fail() {
|
|
$this.wsInfo.alive = false;
|
|
console.log("ws连接失败!");
|
|
},
|
|
});
|
|
|
|
console.log("ws信息:", this.wsInfo.ws);
|
|
// ws打开
|
|
this.wsInfo.ws.onOpen((res) => {
|
|
$this.wsInfo.alive = true;
|
|
// 开启心跳
|
|
$this.heartBeat();
|
|
console.log("ws开启成功!",$this.wsInfo.alive);
|
|
});
|
|
// ws消息
|
|
this.wsInfo.ws.onMessage((res) => {
|
|
console.log("ws接收消息:", res);
|
|
$this.heartBeat();
|
|
// 处理消息
|
|
let data = JSON.parse(res.data);
|
|
$this.handlerMessage(data);
|
|
console.log("ws接收消息:", data);
|
|
});
|
|
// ws关闭
|
|
this.wsInfo.ws.onClose((res) => {
|
|
$this.wsInfo.alive = false;
|
|
$this.reConnect();
|
|
console.log("ws连接关闭:", res);
|
|
});
|
|
// ws错误
|
|
this.wsInfo.ws.onError((err) => {
|
|
$this.wsInfo.alive = false;
|
|
$this.reConnect();
|
|
console.log("ws连接错误:", res);
|
|
});
|
|
},
|
|
// 心跳检测
|
|
heartBeat() {
|
|
const $this = this
|
|
clearTimeout(this.wsInfo.clientTimer);
|
|
clearTimeout(this.wsInfo.serverTimer);
|
|
this.wsInfo.clientTimer = setTimeout(() => {
|
|
if ($this.wsInfo.ws) {
|
|
let pong = {
|
|
type: "ping",
|
|
};
|
|
$this.wsInfo.ws.send({
|
|
data: JSON.stringify(pong),
|
|
fail() {
|
|
$this.wsInfo.serverTimer = setTimeout(() => {
|
|
$this.closeWs();
|
|
}, $this.wsInfo.serverTime);
|
|
},
|
|
});
|
|
}
|
|
}, $this.wsInfo.clientTime);
|
|
},
|
|
//发送消息
|
|
handlerMessage(data) {
|
|
console.log("adat", data)
|
|
},
|
|
//重新连接
|
|
reConnect() {
|
|
if (this.wsInfo.lock) return;
|
|
this.wsInfo.lock = true;
|
|
this.wsInfo.reconnectTimer = setTimeout(() => {
|
|
this.connectWs();
|
|
this.wsInfo.lock = false;
|
|
}, this.wsInfo.reconnectTime);
|
|
},
|
|
// 断开连接
|
|
closeWs() {
|
|
if (!this.wsInfo.alive) {
|
|
uni.showToast({
|
|
title: "请先连接!",
|
|
icon: "error",
|
|
});
|
|
return;
|
|
}
|
|
// wsLogout();
|
|
this.wsInfo.ws.close();
|
|
},
|
|
|
|
// 发送消息
|
|
sendMsg() {
|
|
const $this = this
|
|
this.list.push({
|
|
content: this.content,
|
|
userType: 'self',
|
|
})
|
|
|
|
if (this.userInfo.identity == "") {
|
|
this.userInfo.identity = this.generateRandomString(8);
|
|
}
|
|
this.userInfo.message = this.content
|
|
|
|
this.$Request.post(this.$config.aiSendMsg, $this.userInfo, "json", null, false, true)
|
|
.then(res => {
|
|
console.log("---", res)
|
|
});
|
|
this.content = ''
|
|
this.scrollToBottom()
|
|
// // 模拟对方回复
|
|
// setTimeout(() => {
|
|
// this.list.push({
|
|
// content: '好的',
|
|
// userType: 'friend',
|
|
// })
|
|
// this.scrollToBottom()
|
|
// }, 1500)
|
|
},
|
|
//屏幕滚动
|
|
scrollToBottom() {
|
|
this.top = this.list.length * 1000
|
|
},
|
|
//开启麦克风权限
|
|
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() {
|
|
//开始录音
|
|
const _permission = 'scope.record'
|
|
uni.getSetting({
|
|
success: (res) => {
|
|
// 判断是否有相关权限属性
|
|
if (res.authSetting.hasOwnProperty(_permission)) {
|
|
// 属性存在,且为false,用户拒绝过权限
|
|
if (!res.authSetting[_permission]) {
|
|
this.authTips()
|
|
} else {
|
|
// 已授权
|
|
this._recordAuth = true
|
|
// 开始录音
|
|
recorderManager.start({
|
|
format: 'pcm',
|
|
})
|
|
recorderManager.onStart(() => {
|
|
this.recordStart = true
|
|
})
|
|
|
|
// 错误回调
|
|
recorderManager.onError((res) => {
|
|
console.log('recorder error', res)
|
|
uni.showToast({
|
|
icon: 'none',
|
|
title: '系统出错,请重试'
|
|
})
|
|
this.recordStart = false
|
|
})
|
|
}
|
|
} 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()
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
})
|
|
},
|
|
//停止录音
|
|
touchend() {
|
|
const $this = this
|
|
if (!this._recordAuth || !this.recordStart) return
|
|
//停止录音
|
|
recorderManager.stop();
|
|
recorderManager.onStop((res) => {
|
|
|
|
const {
|
|
duration,
|
|
tempFilePath
|
|
} = res
|
|
this.recordStart = false
|
|
wx.uploadFile({
|
|
url: 'http://192.168.130.157:48080/app-api/wechatshop/toolIdentify/identifySpeech',
|
|
filePath: tempFilePath,
|
|
name: 'file',
|
|
formData: {
|
|
'file': tempFilePath
|
|
},
|
|
success: function(res) {
|
|
let data = JSON.parse(res.data)
|
|
$this.list.push({
|
|
content: data.data,
|
|
audioSrc: tempFilePath,
|
|
userType: 'self',
|
|
messageType: 'voice'
|
|
})
|
|
$this.scrollToBottom()
|
|
},
|
|
fail: function(res) {
|
|
UTIL.log(res);
|
|
wx.showModal({
|
|
title: '提示',
|
|
content: "网络请求失败,请确保网络是否正常",
|
|
showCancel: false,
|
|
success: function(res) {}
|
|
});
|
|
wx.hideToast();
|
|
}
|
|
});
|
|
})
|
|
},
|
|
//获取当前对话 key
|
|
generateRandomString(length) {
|
|
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
let result = '';
|
|
for (let i = 0; i < length; i++) {
|
|
const randomIndex = Math.floor(Math.random() * characters.length);
|
|
result += characters.charAt(randomIndex);
|
|
}
|
|
return result;
|
|
},
|
|
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
.scroll-view {
|
|
/* #ifdef H5 */
|
|
height: calc(100vh - 44px);
|
|
/* #endif */
|
|
/* #ifndef H5 */
|
|
height: 100vh;
|
|
/* #endif */
|
|
box-sizing: border-box;
|
|
top: 0;
|
|
}
|
|
|
|
.v-pages.Ai {
|
|
// background: url('https://eluyou.ailuquan.cn/upload/image/2024/mapIcon/daolan/ai-bg.png');
|
|
// background-repeat: no-repeat;
|
|
// background-position: top;
|
|
// background-size: cover;
|
|
min-height: 50vh;
|
|
}
|
|
|
|
.v-ai-list {
|
|
padding: 24rpx 24rpx 60rpx 24rpx;
|
|
|
|
.list_item {
|
|
margin: 24rpx 0;
|
|
|
|
.text-name {
|
|
font-size: 28rpx;
|
|
color: #333333;
|
|
|
|
.img {
|
|
width: 80rpx;
|
|
}
|
|
}
|
|
|
|
.text-box {
|
|
margin-top: 16rpx;
|
|
padding: 24rpx;
|
|
font-size: 28rpx;
|
|
color: #1B1B1B;
|
|
border-radius: 8rpx 16rpx 16rpx 16rpx;
|
|
}
|
|
|
|
&.self {
|
|
padding-right: 60rpx;
|
|
|
|
.text-name {
|
|
.img {
|
|
margin-right: 10rpx;
|
|
}
|
|
}
|
|
|
|
.text-box {
|
|
background-color: #fff;
|
|
}
|
|
}
|
|
|
|
&.friend {
|
|
padding-left: 60rpx;
|
|
|
|
.text-name {
|
|
margin-left: 10rpx;
|
|
}
|
|
|
|
.text-box {
|
|
color: #fff;
|
|
background: linear-gradient(to right, #0983FF, #57ABFF);
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
}
|
|
|
|
.tool {
|
|
position: fixed;
|
|
width: 100%;
|
|
min-height: 120rpx;
|
|
left: 0;
|
|
bottom: 0;
|
|
z-index: 99;
|
|
background: #fff;
|
|
display: flex;
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</style>
|
|
|