公司演示版e鹿悦游
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.
 
 
 
 
 
CjyTravel/subPageC/Ai/index.vue

765 lines
19 KiB

<template>
<view class="v-pages Ai">
<!-- 对话框 -->
<scroll-view id="scrollview" :style="{height:style.contentViewHeight+'px'}" class="scroll-view" scroll-y="true"
:scroll-with-animation="true" :scroll-top="scrollTop">
<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"
src="https://eluyou.ailuquan.cn/upload/image/2024/mapIcon/daolan/ai-self.png"></image>
<text>E鹿小助手</text>
</view>
<zero-markdown-view :markdown="item.content" v-if="item.content"></zero-markdown-view>
<view class="loading" v-else><text class="loading-dot"></text><text
class="loading-dot"></text><text class="loading-dot"></text></view>
</view>
<view class="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"
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" :disabled='inputDisabled' 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" @click="clickSprink()" @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()
import ZeroMarkdownView from "@/subPageC/components/zero-markdown-view/components/zero-markdown-view/zero-markdown-view.vue"
import {
mapState,
mapMutations
} from "vuex";
import store from "@/store/index.js";
export default {
components: {
ZeroMarkdownView
},
computed: {
...mapState(["PrimaryColor", "userInfo"])
},
data() {
return {
//公共路径
rootPath: this.$config.ROOTPATH,
content: '',
list: [{
content: "嗨,我是旅行策划小能手,为您打造个性化行程,让旅行更精彩!",
userType: 'self',
}],
style: {
pageHeight: 0,
contentViewHeight: 0,
footViewHeight: 90,
mitemHeight: 0
},
scrollTop: 0,
//发送类型
messageType: 'voice', // text 发送文本;voice 发送语音
recordStart: false,
identity: "",
//websocket
Token: null,
inputDisabled: false,
querUserInfo: {},
wsInfo: {
ws: null, // ws对象
alive: false, // 是否连接
isLogin: false, // 是否登录
isJoin: false, // 是否加入
lock: false, // 锁住重连
reconnectTimer: null, // 重连计时
reconnectTime: 5000, // 重连计时间隔
clientTimer: null, // 客户端计时
clientTime: 10000, // 客户端计时间隔
serverTimer: null, // 服务端计时
serverTime: 30000, // 服务端计时间隔
},
//是否长按事件
islongPress: false,
timer: null, //长按计时器
param: {
pageSize: 10,
pageNo: 1,
noData: false,
loading: false,
finished: false,
openid: undefined,
appid: undefined,
},
nowUserId:null
}
},
onUnload() {
this.outWs()
},
onHide() {
this.inputDisabled=false
this.outWs()
},
onShow() {
const userInfo = uni.getStorageSync('userInfo');
const refreshTokenValue = userInfo.refreshToken;
if (!refreshTokenValue){
store.commit('setUserInfo', null)
uni.removeStorageSync('userInfo');
setTimeout(() => {
uni.navigateBack({ delta: 1 })
}, 1500)
uni.showToast({
title: '登录过期,请重新登录',
icon: 'none',
mask: true
})
}else{
this.$Request.post(this.$config.refreshToken, { refreshToken: refreshTokenValue }, 'application/json', null, false, false)
.then(response => {
console.log('response',response)
const newAccessToken = response.data.accessToken;
const newRefreshToken = response.data.refreshToken;
userInfo.accessToken = newAccessToken;
userInfo.refreshToken = newRefreshToken;
store.commit('setUserInfo', userInfo)
this.Token = newAccessToken
this.nowUserId = response.data.userId
this.getUserInfo(response.data.userId)
})
.catch(error => {
store.commit('setUserInfo', null)
uni.removeStorageSync('userInfo');
setTimeout(() => {
uni.navigateBack({ delta: 1 })
}, 1500)
uni.showToast({
title: '登录过期,请重新登录',
icon: 'none',
mask: true
})
});
}
},
created: function() {
const res = uni.getSystemInfoSync();
this.style.pageHeight = res.windowHeight;
this.style.contentViewHeight = res.windowHeight - uni.getSystemInfoSync().screenWidth / 350 * (100) +
50; //像素
},
methods: {
// 用户信息
getUserInfo(userId) {
this.querUserInfo = {
"message": "",
"dialogId": "",
"identity": this.generateRandomString(8),
"userId": userId
};
console.log('querUserInfo',this.querUserInfo)
this.connectWs()
},
// websocket
connectWs() {
const $this = this
this.wsInfo.ws = null;
console.log('wss://eluyou.ailuquan.cn/prod-api/infra/ws?token=' + this.Token)
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连接失败!");
},
});
// ws打开
this.wsInfo.ws.onOpen((res) => {
$this.wsInfo.alive = true;
// 开启心跳
$this.heartBeat();
console.log("ws开启成功!", $this.wsInfo.alive);
});
// ws消息
this.wsInfo.ws.onMessage((res) => {
$this.heartBeat();
// 处理消息
let data = JSON.parse(res.data);
$this.handlerMessage(JSON.parse(data.content));
});
// 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连接错误:", err);
});
},
// 心跳检测
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) {
let $this = this
if (Number(data.userId) == Number(this.querUserInfo.userId)) {
if (data.identity == this.querUserInfo.identity) {
if (data.context !== null) {
this.list[this.list.length - 1].content += data.context
setTimeout(() => {
$this.scrollToBottom()
}, 500)
}
if (data.aiStatus == 2) {
this.inputDisabled = false
}
}
}
},
//重新连接
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();
},
// 断开连接
outWs() {
if (this.wsInfo.ws) {
uni.closeSocket();
this.wsInfo.ws = null
this.list = []
this.list = [{
content: "嗨,我是旅行策划小能手,为您打造个性化行程,让旅行更精彩!",
userType: 'self',
}]
}
},
// 发送消息
sendMsg() {
const $this = this
console.log('$this.nowUserId',$this.nowUserId)
this.getUserInfo($this.nowUserId)
this.list.push({
content: this.content,
userType: 'friend',
})
if (this.querUserInfo.identity == "") {
this.querUserInfo.identity = this.generateRandomString(8);
}
this.inputDisabled = true
$this.list.push({
content: "",
userType: 'self',
})
$this.scrollToBottom()
this.querUserInfo.message = this.content
this.content = ''
this.$Request.post(this.$config.aiSendMsg, $this.querUserInfo, "json", null, false, true)
.then(res => {});
},
//屏幕滚动
/**
* @author gongliying
* @date 2019-07-26
* @information 跳转页面底部
*/
scrollToBottom: function() {
let that = this;
let query = uni.createSelectorQuery();
query.selectAll('.list_item').boundingClientRect();
query.select('#scrollview').boundingClientRect();
query.exec((res) => {
that.style.mitemHeight = 0;
res[0].forEach((rect) => that.style.mitemHeight = that.style.mitemHeight + rect.height +
200) //获取所有内部子元素的高度
// 因为vue的虚拟DOM 每次生成的新消息都是之前的,所以采用异步setTimeout    主要就是添加了这定时器
setTimeout(() => {
if (that.style.mitemHeight > (that.style.contentViewHeight -
100)) { //判断子元素高度是否大于显示高度
that.scrollTop = that.style.mitemHeight - that.style
.contentViewHeight //用子元素的高度减去显示的高度就获益获得序言滚动的高度
}
}, 100)
})
},
//开启麦克风权限
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: '知道了'
})
}
}
})
}
}
})
},
//长按事件
longpress() {
const $this = this
this.islongPress = true;
if (this.islongPress == true && this.inputDisabled == false) {
//开始录音
const _permission = 'scope.record'
uni.getSetting({
success: (res) => {
if (res.authSetting.hasOwnProperty('scope.record')) {
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 {
// 属性不存在,需要授权
console.log('属性不存在,需要授权')
uni.authorize({
scope: 'scope.record',
success: () => {
// 授权成功
$this._recordAuth = true
console.log('授权成功')
},
fail: (res) => {
console.log('// 未授权隐私协议', res.errno)
/**
* 104 未授权隐私协议
* 用户可能拒绝官方隐私授权弹窗,为了避免过度弹窗打扰用户,开发者再次调用隐私相关接口时,
* 若距上次用户拒绝不足10秒,将不再触发弹窗,直接给到开发者用户拒绝隐私授权弹窗的报错
*/
if (res.errno == 104) {
uni.showModal({
title: '温馨提示',
content: '您拒绝了隐私协议,请稍后再试',
confirmText: '知道了',
showCancel: false,
success: () => {}
})
} else {
// 用户拒绝授权
console.log('// 用户拒绝授权')
$this.authTips()
}
}
})
}
}
})
}
},
//开启麦克风权限
authTips() {
const $this = this
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 $this = this
this.timer = setTimeout(() => {
$this.longpress();
}, 200)
},
//停止录音
touchend() {
const $this = this
//延时执行为了防止 click() 还未判断 islongPress 的值就被置为 fasle
clearTimeout(this.timer);
setTimeout(() => {
this.islongPress = false
}, 200)
if (!this._recordAuth || !this.recordStart) return
//停止录音
recorderManager.stop();
recorderManager.onStop((res) => {
const {
duration,
tempFilePath
} = res
$this.recordStart = false
wx.uploadFile({
url: $this.rootPath + '/app-api/wechatshop/toolIdentify/identifySpeech',
filePath: tempFilePath,
name: 'file',
formData: {
'file': tempFilePath
},
success: function(res) {
let data = JSON.parse(res.data)
if (data.data) {
$this.content = data.data
$this.sendMsg()
} else {
wx.showModal({
title: '提示',
content: "没听清,请您再说一遍",
showCancel: false,
success: function(res) {}
});
}
},
fail: function(res) {
UTIL.log(res);
wx.showModal({
title: '提示',
content: "网络请求失败,请确保网络是否正常",
showCancel: false,
success: function(res) {}
});
wx.hideToast();
}
});
})
},
//点击事件
clickSprink() {
// 非长按
if (this.islongPress == false) {
wx.showModal({
title: '提示',
content: "说话时间太短",
showCancel: false,
success: function(res) {}
});
} else if (this.islongPress == true) {
console.log("长按事件");
}
},
//获取当前对话 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;
padding-bottom: 60rpx;
}
.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;
}
.v-ai-list {
padding: 24rpx 24rpx 60rpx 24rpx;
.list_item {
margin: 24rpx 0;
.text-name {
font-size: 28rpx;
color: #333333;
.img {
width: 80rpx;
height: 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: 400rpx;
height: 200rpx;
background-color: rgba(0, 0, 0, .45);
border-radius: 20rpx;
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: #fff;
margin-top: 20rpx;
}
.audio-wave {
padding: 50rpx;
.audio-wave-text {
background-color: #fff;
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 {
height: 40rpx;
}
.loading-dot {
display: inline-block;
width: 4px;
height: 4px;
border-radius: 4px;
background-color: #000;
animation: loading 1s infinite;
}
.loading-dot:nth-child(2) {
animation-delay: 0.2s;
}
.loading-dot:nth-child(3) {
animation-delay: 0.4s;
}
</style>