xunfeiAI/pages/index/index.vue

2078 lines
48 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="wrapper" @touchmove="touchmove">
<view class="conten-top">
<view class="" style="position: absolute;left: 30rpx;"><i class="iconfont icon-xiangzuo"
style="font-size: 40rpx;text-align: left;"></i></view>
<view class="" style="font-size: 30rpx;">
农业咨询
</view>
</view>
<view style="padding-top:var(--status-bar-height);padding-bottom: 30rpx; margin-bottom: 30rpx;"></view>
<view class="tips color_fff size_12 align_c" :class="{ 'show':ajax.loading }" @tap="getHistoryMsg">
{{ajax.loadText}}
</view>
<view class="placeholder"></view>
<view class="box-1" id="list-box" ref="box">
<view class="talk-list">
<view class="talk-list-con">
<view class="talk-list-con-title">
您好
</view>
<view class="talk-list-con-title1">
您可以和我说任何有关农业的问题<br />
例如下面的问题赶快试试吧
</view>
<view class="talk-list-con-title2">
<text style="margin-top: 15rpx;"> 下方输入</text>
<view class="" @click="sendDetail('冬季怎么保存蔬菜')">
冬季怎么保存蔬菜
<image src="../../static/icon/airight.png" mode=""></image>
</view>
</view>
</view>
<view v-for="(item,index) in talkList" :key="index" :id="`msg-${item.id}`">
<view class="item flex_col" :class="item.type == 1 ? 'push':'pull' ">
<image :src="item.type==1?avatar:item.pic" mode="aspectFill" class="pic" v-if="item.pic">
</image>
<view class="content" v-if="item.id=='@'">
<!-- 加载中 -->
<view class="blinking-box">
<image src="@/static/icon/ioc.gif" mode="aspectFit"></image>
<view class="">正在快速生成答案</view>
</view>
</view>
<view class="" v-else>
<view v-if="talkList.length-1==index" class="multiline-text">
<view class="content">
<view class="" v-if="item.type==3" style="border: 1px solid #2573fb;margin-left: 20rpx;">
<image :src="item.Image" mode="" style="width: 200rpx;height: 200rpx;" @click="previewImage(item.Image)"></image>
<view class="">{{c_content}}</view>
</view>
<view class="" v-else>
<bing-math v-if="c_content!=''" :key="`math-${item.id}`" class="bing-math1"
:latex="c_content"></bing-math>
<view v-else class="blinking-box">
<image src="@/static/icon/ioc.gif" mode="aspectFit"></image>
<view class="">正在快速生成答案</view>
</view>
</view>
</view>
<view class="contenta" v-if="c_content!=''&&index==nums">
<view class="contenta-img" v-if='item.playStatus' @click="palyaudio(item,index)">
<image src="@/static/icon/ai7.png" mode="aspectFit"></image>
</view>
<view class="contenta-img" v-else @click="palyaudio(item,index)">
<image src="@/static/icon/ai8.png" mode="aspectFit"></image>
</view>
<view class="contenta-ai" v-show="item.playStatus==false">
<image src="@/static/icon/ai.gif" mode="aspectFit"></image>
</view>
<view class="contenta-imga" @tap="copyContent(c_content)"
:style="{'margin-left':item.playStatus==false?'377rpx' :'427rpx','margin-right': '20rpx'}">
<image src="@/static/icon/ai10.png" mode="aspectFit"></image>
</view>
<view class="contenta-imga" @click="Share">
<image src="@/static/icon/ai9.png" mode="aspectFit"></image>
</view>
</view>
</view>
<view v-else>
<!-- <rich-text :nodes="item.content"></rich-text> -->
<view class="" v-if="item.type==3" style="border: 1px solid #2573fb;margin-left: 20rpx;">
<image :src="item.Image" mode="" style="width: 200rpx;height: 200rpx;" @click="previewImage(item.Image)"></image>
</view>
<view class="" v-else>
<view class="content multiline-text">
<bing-math :key="`math-${item.id}`" class="bing-math"
:latex="item.content"></bing-math>
</view>
</view>
<view class="contenta" v-if="!item.pic">
<view class="contenta-img" v-if='item.playStatus' @click="palyaudio(item,index)">
<image src="@/static/icon/ai7.png" mode="aspectFit"></image>
</view>
<view class="contenta-img" v-else @click="palyaudio(item,index)">
<image src="@/static/icon/ai8.png" mode="aspectFit"></image>
</view>
<view class="contenta-ai" v-show="item.playStatus==false">
<image src="@/static/icon/ai.gif" mode="aspectFit"></image>
</view>
<view class="contenta-imga" @tap="copyContent(c_content)"
:style="{'margin-left':item.playStatuss==false?'377rpx' :'427rpx','margin-right': '20rpx'}">
<image src="@/static/icon/ai10.png" mode="aspectFit"></image>
</view>
<view class="contenta-imga" @click="Share">
<image src="@/static/icon/ai9.png" mode="aspectFit"></image>
</view>
</view>
</view>
</view>
</view>
</view>
<view id="bottom-box"></view>
</view>
</view>
<uni-transition custom-class="box-2" mode-class="slide-left" :show="showStop">
<view class="flex_col">
<view class="flex_grow content downsocket" @click="closeSocketTask">中断连接</view>
</view>
</uni-transition>
<uni-transition custom-class="box-2" mode-class="slide-right" :show="!showStop">
<view class="flex_c">
<view class="flex_c_img" v-if="isshow" @click="hidesend">
<image src="@/static/icon/hua.png" mode="aspectFit"></image>
</view>
<view class="flex_c_img" v-else @click="hidesend">
<image src="@/static/icon/den.png" mode="aspectFit"></image>
</view>
<view class="flex_c_txt" v-if="isshow">
<input type="text" class="content" v-model="content" placeholder="输入您要咨询的内容" @focus="focus"
@confirm="send" placeholder-style="color:#DDD;" :cursor-spacing="6">
</view>
<view class="flex_c_txt" v-else>
<view class="con" @touchend="end" @touchmove="move" @longpress="start">
{{btnStatus==0? "按住说话":btnStatus==1? "说话中...": btnStatus==2? "松开手指发送录音": "上划取消"}}
</view>
<!-- <view :class=" btnStatus==0? 'footerBtn': btnStatus==1?'footerBtn_star':'footerBtn_move'">
<view class="footerBtn_chr" @touchend="end" @touchmove="move" @touchstart="start">
{{btnStatus==0? "按住说话":btnStatus==1? "说话中...": btnStatus==2? "松开手指发送录音": "上划取消"}}
</view>
</view> -->
</view>
<view class="flex_c_img" @tap="send" v-if='content.length>0'>
<image src="@/static/icon/fa1.png" mode="aspectFit"></image>
</view>
<view class="flex_c_img" @tap="send" v-else>
<image src="@/static/icon/fa.png" mode="aspectFit"></image>
</view>
<view class="flex_c_img" @click="addgn">
<image src="@/static/icon/jia.png" mode="aspectFit"></image>
</view>
<!-- <button class="send" @tap="send">发送</button> -->
</view>
<view class="flex_d" v-if="gnishow">
<view class="" @click="album">
<image src="../../static/icon/ai2.png" mode=""></image>
</view>
<view class="" @click="camera">
<image src="../../static/icon/ai3.png" mode=""></image>
</view>
</view>
</uni-transition>
<uni-popup ref="popup" :mask-click="false">
<view class="popup_box" @touchend="end" @touchmove="move" @longpress="start">
<view class="move">
<view v-if="btnStatus!==0" :class="btnStatus==3? 'move_center_clear': 'move_center'">
<view class="move_center_top">
<view class="move_center_item" v-for="(item,index) in 15" :key="index"
:style="{'background':btnStatus==3?'rgba(250, 83, 83, .5)':'rgba(149, 234, 108, .5)'}">
</view>
</view>
<view class="move_center_footer">
{{btnStatus==3? '松开手指,取消发送':"正在录音..."}}
</view>
</view>
</view>
<view class="popup_box-con">
<view class="popup_box-cona">
<view :class="risshow?'bg1':''">
X
</view>
<!-- <view class="" @tap="$u.throttle(end, 1000)">
</view> -->
</view>
<view class="popup_box-conb">
<image src="@/static/icon/ai1.png" mode=""></image>
</view>
</view>
</view>
</uni-popup>
<qiqbshare ref="child"></qiqbshare>
</view>
</template>
<script>
import {
xuiat,
iatWss,
debounce,
throttle,
fileToBase64,
dataURLtoBlob,
ttWss,
ttocr
} from "@/api/api.js"
import {
HTTP_REQUEST_URL
} from '@/config/app';
import {
pathToBase64,
base64ToPath
} from '@/js_sdk/mmmm-image-tools/index.js'
import * as base64 from "base-64"
import CryptoJS from '../../static/crypto-js/crypto-js.js'
import parser from '../../static/fast-xml-parser/src/parser'
import * as utf8 from "utf8"
import BingMath from "@/components/bing-math/bing-math.vue"
import qiqbshare from "@/components/qiqb-share/qiqb-share.vue"
const recorderManager = uni.getRecorderManager()
const innerAudioContext = uni.createInnerAudioContext()
export default {
components: {
'bing-math': BingMath,
'qiqbshare': qiqbshare,
},
data() {
return {
wssType: 'nongye', // 网络连接类型
avatarType: "/static/icon/ny.png", // AI头像类型
talkList: [],
ajax: {
rows: 15, //每页数量
page: 1, //页码
flag: false, // 请求开关
loading: false, // 加载中
loadText: '正在获取消息'
},
keyboardHeight: 0,
showplc: true,
content: '',
c_content: '',
n_content: '',
avatar: '/static/avatar.png', // 用户头像
params: [], // 发送的消息内容
timer: '',
timer1: '',
socketTask: {},
TEXT: '',
historyTextList: [], // 历史会话信息由于最大token12000,可以结合实际使用,进行移出
tempRes: '', // 临时答复保存
socketing: false, // 是否正在接收
showStop: false, // 是否显示中断按钮
scrollTop: 0,
isshow: true,
shouldScrollToBottom: true,
nums: 0,
recordFlg: true, //录音锁
btnStatus: 0, //按钮状态
audioDuration: "",
voicePath: "", //音频本地路径
timer: null, //定时器
num: 0, //录音时长
gnishow: false,
audioUrl: '',
audioTxt: '',
handlerInterval: null,
huashow: false,
Fstatus: false,
touchStartX: 0, //触摸时的原点
touchStartY: 0, //触摸时的原点
touchMoveX: 0, // x轴方向移动的距离
touchMoveY: 0, // y轴方向移动的距离
risshow: false,
islong: false,
// 是否开始绘制
isDraw: false,
imageSize: {
},
audio_file: [],
currentIndex: 0,
kk: 1,
wv: null,
Imageurl: '',
content1: '',
audioContext: null, // 存储当前的innerAudioContext实例
}
},
mounted() {
this.$nextTick(() => {
this.getHistoryMsg();
});
uni.onKeyboardHeightChange(e => {
let h = this.keyboardHeight;
this.keyboardHeight = e.height;
this.$nextTick(() => {
setTimeout(() => {
uni.pageScrollTo({
scrollTop: 9999999, // 当前位置向下滚动
duration: 300 // 滚动过渡时间为300ms默认值为300ms
});
}, 0)
})
})
},
onLoad() {
let avatar = uni.getStorageSync('avatar');
avatar ? this.avatar = avatar : null;
uni.$on('MPinfo', (e) => {
this.avatar = e.avatar;
})
},
beforeDestroy() {
// #ifdef APP-PLUS
uni.offKeyboardHeightChange();
// #endif
},
onPageScroll(e) {
if (e.scrollTop < 5) {
this.getHistoryMsg();
}
},
created() {
// 为了防止苹果手机静音无法播放
// uni.setInnerAudioOption({
// obeyMuteSwitch: false
// })
let that = this;
recorderManager.onStop(function(res) {
// 解决第一次录音,挂实例挂载问题
that.$nextTick(() => {
// console.log('recorder stop' + JSON.stringify(res));
that.audioDuration = res.duration;
that.voicePath = res.tempFilePath;
})
});
},
watch: {
n_content(n, o) {
// this.c_content = n;
if (this.timer) clearInterval(this.timer);
let cl = this.c_content.length;
let nc = this.n_content.split('');
this.timer = setInterval(() => {
if (cl < nc.length) {
this.c_content += nc[cl];
cl++;
if (cl % 6 == 0) this.$nextTick(() => {
uni.pageScrollTo({
scrollTop: 999999,
})
})
} else {
// console.log(this.socketing==false, cl == nc.length);
if (this.socketing == false) this.showStop = false;
clearInterval(this.timer);
this.$nextTick(() => {
uni.pageScrollTo({
scrollTop: 9999999,
})
})
}
}, 60)
},
},
methods: {
//海报计算
cavshare(data) {
let arry = []
let arry1 = []
let a;
let b;
if (data.length > 2) {
a = data.length - 2
b = data.length - 1
} else {
a = 0
b = 1
}
for (let i in data[a].content) {
if (data[a].content.slice(i * 22, (i + 1) * 22)) {
arry.push(data[a].content.slice(i * 22, (i + 1) * 22))
}
}
for (let i in data[b].content) {
if (data[b].content.slice(i * 22, (i + 1) * 22)) {
arry1.push(data[b].content.slice(i * 22, (i + 1) * 22))
}
}
// console.log(arry1.length,arry.length)
this.imageSize.height = (arry1.length * 20) + (arry.length * 20) + 100 + 150
this.$refs.child.refresh(this.talkList, this.imageSize.height, (arry.length * 20) + 100);
},
//滑动到底部
TabItemTap(e) {
uni.pageScrollTo({
selector: ".box-1",
scrollTop: 0
})
},
//分享
Share() {
// this.isDraw = !this.isDraw;
this.cavshare(this.talkList)
this.$refs.child.init()
},
//打开相册
album() {
let that = this
uni.chooseImage({
count: 1,
sizeType: ['original', 'compressed'],
sourceType: ['album'], //这要注意camera掉拍照album是打开手机相册
success: (res) => {
// const tempFilePaths = res.tempFilePaths;
console.log(res.tempFilePaths,'2222222222')
uni.uploadFile({
url: HTTP_REQUEST_URL + '/api/upload/image', // 上传地址
filePath: res.tempFilePaths[0], // 要上传的文件路径
name: 'file', // 上传文件对应的 key 值
header: {
'content-type': 'multipart/form-data'
},
success: function(res) {
let data = JSON.parse(res.data)
console.log(data,'222222222222')
if (data.code == 1) {
// this.File()
let data1 = {
"id": '@',
"content": '',
"type": 2,
"pic": this.avatar
}
that.talkList.push(data1);
that.Imageurl = data.data.uri
that.imgfile(data.data.uri)
}
},
fail: function(error) {
// 上传失败处理逻辑
that.talkList = that.talkList.filter(item => item.id !== '@');
console.log(error, 11);
}
})
}
});
},
//拍照
camera() {
let that = this
uni.chooseImage({
count: 1,
sizeType: ['original', 'compressed'],
sourceType: ['camera'], //这要注意camera掉拍照album是打开手机相册
success: (res) => {
console.log(res);
// const tempFilePaths = res.tempFilePaths;
console.log(res.tempFilePaths)
uni.uploadFile({
url: HTTP_REQUEST_URL + '/api/upload/image', // 上传地址
filePath: res.tempFilePaths[0], // 要上传的文件路径
name: 'file', // 上传文件对应的 key 值
header: {
'content-type': 'multipart/form-data'
},
success: function(res) {
let data = JSON.parse(res.data)
console.log(data, data.code)
if (data.code == 1) {
// this.File()
let data1 = {
"id": '@',
"content": '',
"type": 2,
"pic": this.avatar
}
that.talkList.push(data1);
that.Imageurl = data.data.uri
that.imgfile(data.data.uri)
}
},
fail: function(error) {
// 上传失败处理逻辑
that.talkList = that.talkList.filter(item => item.id !== '@');
console.log(error, 11);
}
})
// this.imgfile(res.tempFilePaths)
}
});
},
//复制
copyContent(data) {
uni.setClipboardData({
data,
success: function() {
uni.getClipboardData({
success: function(res) {
uni.showToast({
title: "复制成功",
});
}
});
}
});
},
//图片预览
previewImage(url){
uni.previewImage({
urls: [url], // 图片地址数组
current: url, // 当前预览的图片地址
success: function() {
console.log('预览图片成功');
},
fail: function(error) {
console.error('预览图片失败', error);
}
});
},
//图片转文字
imgfile(tempFilePath) {
let that = this
that.content1=''
// console.log(tempFilePath, '2')
ttocr({
image: tempFilePath
}).then(res => {
if (res.code == 1) {
uni.showToast({
title: '图片识别成功'
});
console.log(res.data.words)
that.gnishow = false
that.talkList = that.talkList.filter(item => item.id !== '@');
if (res.data.words.length > 0) {
for (let i in res.data.words) {
that.content1 += res.data.words[i]
if (res.data.words.length - 1 == i) {
that.send1()
}
}
}
}
}).catch((err) => {
that.talkList = that.talkList.filter(item => item.id !== '@');
console.log(err)
})
// uni.uploadFile({
// url: HTTP_REQUEST_URL + '/api/xun_fei/ocr', // 上传地址
// filePath: tempFilePath, // 要上传的文件路径
// name: 'image', // 上传文件对应的 key 值
// header: {
// 'content-type': 'multipart/form-data'
// },
// success: function(res) {
// let data = JSON.parse(res.data)
// console.log(data)
// if (data.code == 1) {
// uni.showToast({
// title: '图片识别成功'
// });
// that.talkList = that.talkList.filter(item => item.id !== '@');
// if (data.data.words.length > 0) {
// that.content = ''
// for (let i in data.data.words) {
// that.content = that.content + data.data.words[i]
// }
// that.send1()
// }
// }
// },
// fail: function(error) {
// // 上传失败处理逻辑
// that.talkList = that.talkList.filter(item => item.id !== '@');
// console.log(error, 11);
// }
// })
},
//功能展开
addgn() {
this.gnishow = !this.gnishow
},
// 点击关闭遮罩层
closePop() {
this.$refs.popup.close()
},
//振动
onFeedTap() {
let platform = uni.getSystemInfoSync().platform
// #ifdef APP-PLUS
if (platform == "ios") {
let UIImpactFeedbackGenerator = plus.ios.importClass('UIImpactFeedbackGenerator');
let impact = new UIImpactFeedbackGenerator();
impact.prepare();
impact.init(1);
impact.impactOccurred();
}
if (platform == "android") {
// uni.vibrateShort();
uni.vibrateShort({
success: () => {
console.log('success');
},
fail: () => {
console.log('失败');
},
});
}
// #endif
},
//文字转语音
txtspeech(val, i) {
xuiat({
text: val
}).then(res => {
if (res.code == 1) {
this.talkList[i].audio_file = res.data.audio_file
this.nums = this.talkList.length - 1
// this.palyaudio(res.data)
}
}).catch((err) => {
console.log(err)
})
},
//语音转文字
uploadFile(tempFilePath) {
let that = this
// console.log(tempFilePath)
uni.uploadFile({
url: HTTP_REQUEST_URL + '/api/xun_fei/iat', // 上传地址
filePath: tempFilePath, // 要上传的文件路径
name: 'audio', // 上传文件对应的 key 值
header: {
'content-type': 'multipart/form-data'
},
success: function(res) {
let data = JSON.parse(res.data)
if (data.code == 1) {
uni.showToast({
title: '语音识别成功'
});
that.talkList = that.talkList.filter(item => item.id !== '@');
if (data.data.words.length > 0) {
that.content = data.data.words
that.send()
}
} else {
that.talkList = that.talkList.filter(item => item.id !== '@');
uni.showToast({
title: '语音识别失败'
});
}
},
fail: function(error) {
// 上传失败处理逻辑
that.deletespeech()
console.log(error, 11);
uni.hideLoading();
}
})
},
//语音分段 数组 条数 延时
ScanAudio(urls, currentIndex, delayInSeconds) {
if (currentIndex >= urls.length) {
this.palystatus = true;
return false;
}
const music = uni.createInnerAudioContext();
music.src = urls[currentIndex];
console.log(urls[currentIndex])
console.log('播放成功', this.palystatus)
if (this.palystatus) {
this.palystatus = false
music.play();
} else {
music.pause();
}
music.onEnded(() => {
music.destroy();
setTimeout(() => {
this.ScanAudio(urls, currentIndex + 1, delayInSeconds);
}, delayInSeconds * 1000);
});
},
start(e) {
this.onFeedTap()
this.touchT = new Date().getTime();
this.islong = true
this.risshow = false
this.$refs.popup.open('center')
// 限制时长
this.timer = setInterval(() => {
this.num++
}, 1000)
this.btnStatus = 1
recorderManager.start({
//时长5分钟,单位毫秒
duration: 300000,
// format: "pcm",
});
this.touchStartX = e.touches[0].pageX; // 获取触摸时的原点
this.touchStartY = e.touches[0].pageY; // 获取触摸时的原点
},
move(e) {
let that = this;
let moveX = this.touchMoveX - e.touches[0].pageX;
let moveY = this.touchMoveY - e.touches[0].pageY;
// console.log(-115 > moveX && -240 < moveX,moveY)
let platform = uni.getSystemInfoSync().platform;
if (platform === 'ios') {
if (-115 > moveX && -240 < moveX && moveY < -620 && moveY > -730) {
this.risshow = true
this.deletespeech()
} else {
this.risshow = false
}
} else {
if (-115 > moveX && -240 < moveX && moveY < -359 && moveY > -480) {
this.risshow = true
this.deletespeech()
} else {
this.risshow = false
}
}
},
end() {
this.touchE = new Date().getTime();
if (this.islong) {
this.closePop()
this.islong = false
clearInterval(this.timer)
this.btnStatus = 0
recorderManager.stop();
// this.closePop()
if (this.risshow == false && this.touchE - this.touchT > 3000) {
let data = {
"id": '@',
"content": '加载完毕',
"type": 2,
}
this.talkList.push(data);
// 异步,解决录音结束后,路径没赋值就传值,导致的路径为空
setTimeout(() => {
let obj = {
status: false,
path: [{
src: this.voicePath,
switchStatus: false,
soundTime: this.num
}]
}
if (this.voicePath.length > 0 && this.risshow == false) {
// this.huashow=true
this.TabItemTap()
throttle(this.uploadFile(this.voicePath), 1000)
}
this.num = 0
}, 500)
} else {
if (!this.risshow) {
uni.showModal({
title: "录音时长不超过3s"
})
}
}
}
},
//删除本地录音文件
deletespeech() {
this.btnStatus = 0
let that = this
this.closePop()
uni.getSavedFileList({
success: function(res) {
if (res.fileList.length > 0) {
uni.removeSavedFile({
filePath: res.fileList[0].filePath,
complete: function(res) {
console.log(res);
}
});
}
}
});
},
//发送示列
sendDetail(data) {
this.content = data
this.send()
},
//语音隐藏
hidesend() {
this.isshow = !this.isshow
},
//录音
Dialogue() {
// this.maskShow = !this.maskShow
this.$refs.popup.open('center')
},
footerCheck(e) {
console.log(e)
this.maskShow = false
this.isshow = true
},
//分段音频播放
palyaudio1(item, i) {
console.log(this.audio_file, '111')
if (!this.palystatus) {
this.ScanAudio(this.audio_file, this.currentIndex, 0)
} else {
this.ScanAudio(this.audio_file, this.currentIndex, 0)
// this.txtspeech(item.content, i)
}
// innerAudioContext.onError((res) => {
// console.log(res.errMsg);
// console.log(res.errCode);
// this.palystatus = true
// });
innerAudioContext.onEnded((res) => {
this.palystatus = true
});
},
//音频播放
palyaudio(items, i) {
// console.log(this.talkList,items.audio_file,'000000')
if (this.audioContext) {
// 停止上一段对话内容的播放
this.audioContext.stop();
this.audioContext = null;
}
this.talkList.forEach((item, index) => {
if (index != i) {
item.playStatus = true
}
// item.playStatus = (index == i);
});
this.$forceUpdate()
this.talkList[i].playStatus = !this.talkList[i].playStatus
if (!this.talkList[i].playStatus) {
if(items.audio_file.length>0){
innerAudioContext.src = items.audio_file;
innerAudioContext.play()
this.talkList[i].playStatus = false
}else{
this.talkList[i].playStatus = true
}
} else {
innerAudioContext.pause()
this.talkList[i].playStatus = true
this.$forceUpdate()
}
innerAudioContext.onError((res) => {
console.log(res.errMsg);
console.log(res.errCode);
this.talkList[i].playStatus = true
this.$forceUpdate()
innerAudioContext.destroy()
});
innerAudioContext.onEnded((res) => {
this.talkList[i].playStatus = true
this.$forceUpdate()
});
this.audioContext = innerAudioContext;
this.$forceUpdate()
},
copyText(str) {
uni.setClipboardData({
data: str,
success: function() {
uni.showToast({
icon: 'none',
title: '复制成功'
});
}
});
},
// 获取历史消息
getHistoryMsg() {
return;
if (this.ajax.flag) {
return; //
}
let get = async () => {
this.hideLoadTips();
this.ajax.flag = false;
let data = await this.joinHistoryMsg();
console.log('----- 模拟数据格式,供参考 -----');
// console.log(data); // 查看请求返回的数据结构
// 获取待滚动元素选择器,解决插入数据后,滚动条定位时使用
let selector = '';
if (this.ajax.page > 1) {
// 非第一页,则取历史消息数据的第一条信息元素
selector = `#msg-${this.talkList[0].id}`;
} else {
// 第一页,则取当前消息数据的最后一条信息元素
selector = `#msg-${data[data.length-1].id}`;
}
// 将获取到的消息数据合并到消息数组中
this.talkList = [...data, ...this.talkList];
// 数据挂载后执行,不懂的请自行阅读 Vue.js 文档对 Vue.nextTick 函数说明。
this.$nextTick(() => {
// 设置当前滚动的位置
this.setPageScrollTo(selector);
this.hideLoadTips(true);
if (data.length < this.ajax.rows) {
// 当前消息数据条数小于请求要求条数时,则无更多消息,不再允许请求。
// 可在此处编写无更多消息数据时的逻辑
} else {
this.ajax.page++;
// 延迟 200ms ,以保证设置窗口滚动已完成
setTimeout(() => {
this.ajax.flag = true;
}, 200)
}
})
}
get();
},
// 拼接历史记录消息
joinHistoryMsg() {
let join = () => {
let arr = [];
//通过当前页码及页数,模拟数据内容
let startIndex = (this.ajax.page - 1) * this.ajax.rows;
let endIndex = startIndex + this.ajax.rows;
for (let i = startIndex; i < endIndex; i++) {
arr.push({
"id": i, // 消息的ID
"content": `这是历史记录的第${i+1}条消息`, // 消息内容
"type": Math.random() > 0.5 ? 1 : 0, // 此为消息类别,设 1 为发出去的消息0 为收到对方的消息,
"pic": "/static/avatar.png" // 头像
})
}
/*
颠倒数组中元素的顺序。将最新的数据排在本次接口返回数据的最后面。
后端接口按 消息的时间降序查找出当前页的数据后,再将本页数据按消息时间降序排序返回。
这是数据的重点,因为页面滚动条和上拉加载历史的问题。
*/
arr.reverse();
return arr;
}
// 此处用到 ES6 的 Promise 知识,不懂的请自行学习。
return new Promise((done, fail) => {
// 无数据请求接口,由 setTimeout 模拟,正式项目替换为 ajax 即可。
setTimeout(() => {
let data = join();
done(data);
}, 1500);
})
},
focus() {
// this.$nextTick(()=>{
// setTimeout(()=>{
// uni.pageScrollTo({
// scrollTop: 9999999, // 当前位置向下滚动
// duration: 300 // 滚动过渡时间为300ms默认值为300ms
// });
// }, 0)
// })
},
// 设置页面滚动位置
setPageScrollTo(selector) {
let view = uni.createSelectorQuery().in(this).select(selector);
view.boundingClientRect((res) => {
uni.pageScrollTo({
scrollTop: res.top - 30, // -30 为多显示出大半个消息的高度,示意上面还有信息。
duration: 0
});
}).exec();
},
// 隐藏加载提示
hideLoadTips(flag) {
if (flag) {
this.ajax.loadText = '消息获取成功';
setTimeout(() => {
this.ajax.loading = false;
}, 300);
} else {
this.ajax.loading = true;
this.ajax.loadText = '正在获取消息';
}
},
// 关闭连接
closeSocketTask() {
try {
this.talkList = this.talkList.filter(item => item.id !== '@');
clearInterval(this.timer);
this.talkList[this.talkList.length - 1].audio_file=''
this.talkList[this.talkList.length - 1].content = this.c_content + '';
// console.log(this.talkList[this.talkList.length - 1].content);
// this.c_content = '';
// this.n_content = '';
this.socketTask.close({
code: 500, // APP端存在BUG,正常关闭的code为1000,无法正常关闭,需要将code换为其他值
complete: (res) => {
this.showStop = false;
console.log('主动断开', res);
this.wsLiveFlag = false;
}
})
} catch (e) {
//TODO handle the exception
}
},
// 发送信息
send() {
if (!this.content) {
uni.showToast({
title: '请输入有效的内容',
icon: 'none'
})
return;
}
// 将当前发送信息 添加到消息列表。
let data = {
"id": new Date().getTime(),
"content": this.content,
'Image': this.Imageurl,
"type": 1,
// "pic": this.avatar
"pic": this.avatar
}
// console.log(data);
this.TEXT = this.content;
this.talkList.push(data);
this.talkList.push({
"id": new Date().getTime(),
"content": '',
'Image': '',
"type": 2,
// "pic": this.avatarType
});
this.n_content = '';
this.c_content = '';
this.socketing = true;
// return ;
this.$nextTick(() => {
// 清空内容框中的内容
this.content = '';
// uni.pageScrollTo({
// scrollTop: 999999, // 设置一个超大值,以保证滚动条滚动到底部
// duration: 0
// });
})
this.sendToSpark();
},
send1() {
if (!this.content1) {
uni.showToast({
title: '请重新上传图片',
icon: 'none'
})
return;
}
this.showStop = true;
// 将当前发送信息 添加到消息列表。
let data = {
"id": new Date().getTime(),
"content": this.content1,
'Image': this.Imageurl,
"type": 3,
// "pic": this.avatar
"pic": this.avatar
}
// console.log(data);
this.TEXT = this.content1;
this.talkList.push(data);
this.talkList.push({
"id": new Date().getTime(),
"content": '',
'Image': '',
"type": 2,
// "pic": this.avatarType
});
this.n_content = '';
this.c_content = '';
this.socketing = true;
// return ;
this.$nextTick(() => {
// 清空内容框中的内容
this.content = '';
// uni.pageScrollTo({
// scrollTop: 999999, // 设置一个超大值,以保证滚动条滚动到底部
// duration: 0
// });
})
this.sendToSpark();
},
async sendToSpark() {
this.tempRes = "";
let realThis = this;
this.socketTask = uni.connectSocket({
//url: encodeURI(encodeURI(myUrl).replace(/\+/g, '%2B')),
url: `wss://chat.lihaink.cn/chat?type=${this.wssType}&timestamp=${Date.now()}`,
method: 'GET',
token: '',
success: res => {
console.log(res, "ws成功连接...")
realThis.wsLiveFlag = true;
}
})
realThis.socketTask.onError((res) => {
console.log("连接发生错误请检查appid是否填写", res)
})
realThis.socketTask.onOpen((res) => {
this.historyTextList.push({
"role": "user",
"content": this.TEXT
})
// 第一帧..........................................
console.log('连接成功...')
// let params = {
// "header": {
// "app_id": this.APPID,
// "uid": "aef9f963-7"
// },
// "parameter": {
// "chat": {
// "domain": "generalv2",
// "temperature": 0.5,
// "max_tokens": 1024
// }
// },
// "payload": {
// "message": {
// "text": this.historyTextList
// }
// }
// };
if (this.historyTextList.length > 9) this.params = JSON.parse(JSON.stringify(this
.historyTextList
.splice(-9)));
else this.params = JSON.parse(JSON.stringify(this.historyTextList));
this.isSurpass();
realThis.socketTask.send({ // 发送消息都用uni的官方版本
data: JSON.stringify(this.params),
success() {
console.log('第一帧发送成功');
}
});
});
// 接受到消息时
realThis.socketTask.onMessage((res) => {
// console.log('收到API返回的内容', res.data);
let obj = JSON.parse(res.data)
// console.log("我打印的"+obj.payload);
if (!realThis.wsLiveFlag) return;
let dataArray = obj.payload.choices.text;
for (let i = 0; i < dataArray.length; i++) {
this.talkList[this.talkList.length - 1].content += dataArray[i].content;
this.talkList[this.talkList.length - 1].playStatus = true;
this.n_content = this.talkList[this.talkList.length - 1].content;
realThis.tempRes = realThis.tempRes + dataArray[i].content
}
if(this.n_content.length>0){
this.nums = this.talkList.length - 1
this.txtspeech(this.n_content, this.talkList.length -
1)
this.showStop = true;
}
this.cavshare(this.talkList)
let temp = JSON.parse(res.data)
// console.log("0726",temp.header.code)
if (temp.header.code !== 0) {
this.socketing = false;
console.log(`${temp.header.code}:${temp.message}`);
realThis.socketTask.close({
success(res) {
console.log('关闭成功', res)
realThis.wsLiveFlag = false;
},
fail(err) {
console.log('关闭失败', err)
}
})
}
if (temp.header.code === 0) {
if (res.data && temp.header.status === 2) {
this.socketing = false;
this.historyTextList.push({
"role": "assistant",
"content": this.tempRes
})
setTimeout(() => {
let that = this
realThis.socketTask.close({
success(res) {
console.log('关闭成功', res)
realThis.wsLiveFlag = false;
},
fail(err) {
// console.log('关闭失败', err)
}
})
}, 1000)
}
}
})
},
// 判断文字是否超过五千字
isSurpass() {
let sum = this.params.reduce((accumulator, currentValue) => {
return accumulator + currentValue.content;
}, '');
if (sum.length > 5000) {
this.params.shift();
return this.isSurpass();
} else {
console.log(`本次发送${sum.length}字`);
return sum;
}
},
// 鉴权
getWebSocketUrl() {
return new Promise((resolve, reject) => {
// https://spark-api.xf-yun.com/v1.1/chat V1.5 domain general
// https://spark-api.xf-yun.com/v2.1/chat V2.0 domain generalv2
var url = "wss://spark-api.xf-yun.com/v2.1/chat";
var host = "spark-api.xf-yun.com";
var apiKeyName = "api_key";
var date = new Date().toGMTString();
var algorithm = "hmac-sha256";
var headers = "host date request-line";
var signatureOrigin = `host: ${host}\ndate: ${date}\nGET /v2.1/chat HTTP/1.1`;
var signatureSha = CryptoJS.HmacSHA256(signatureOrigin, this.APISecret);
var signature = CryptoJS.enc.Base64.stringify(signatureSha);
var authorizationOrigin =
`${apiKeyName}="${this.APIKey}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"`;
var authorization = base64.encode(authorizationOrigin);
url = `${url}?authorization=${authorization}&date=${encodeURI(date)}&host=${host}`;
// console.log(url)
resolve(url);
});
},
// 滚动到头部
bindScroll() {
if (this.userId == 0) {
this.getHistory();
this.getproductInfo();
this.getOrderInfo();
this.getRefundDetail();
this.getStoreDetail();
} else {
this.getMerHistory();
}
},
// 当滑动页面时,收起键盘,与微信聊天效果保持一致
touchmove(e) {
uni.hideKeyboard()
}
}
}
</script>
<style lang="scss">
@import "../../lib/global.scss";
page {
// background-color: #f5f5f5;
height: 100vh;
background: url('@/static/icon/bg1.png') no-repeat;
background-size: 100% 100%;
background-attachment: fixed;
background-repeat: no-repeat;
font-size: 28rpx;
}
@keyframes load {
0% {
height: 10%;
}
50% {
height: 100%;
}
100% {
height: 10%;
}
}
// 取消发送
.move_center_footer {
margin-top: 20rpx;
font-size: 24rpx;
color: #666;
}
.bg1 {
background-color: #fff !important;
color: #000 !important;
}
.move_center_top {
height: 40rpx;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
.move_center_item {
display: block;
// background: #333333;
width: 6rpx;
height: 10%;
margin: 0 6rpx;
float: left;
}
.move_center_item:nth-child(1) {
animation: load 2.5s 1.4s infinite linear;
}
.move_center_item:nth-child(2) {
animation: load 2.5s 1.2s infinite linear;
}
.move_center_item:nth-child(3) {
animation: load 2.5s 1s infinite linear;
}
.move_center_item:nth-child(4) {
animation: load 2.5s 0.8s infinite linear;
}
.move_center_item:nth-child(5) {
animation: load 2.5s 0.6s infinite linear;
}
.move_center_item:nth-child(6) {
animation: load 2.5s 0.4s infinite linear;
}
.move_center_item:nth-child(7) {
animation: load 2.5s 0.2s infinite linear;
}
.move_center_item:nth-child(8) {
animation: load 2.5s 0s infinite linear;
}
.move_center_item:nth-child(9) {
animation: load 2.5s 0.2s infinite linear;
}
.move_center_item:nth-child(10) {
animation: load 2.5s 0.4s infinite linear;
}
.move_center_item:nth-child(11) {
animation: load 2.5s 0.6s infinite linear;
}
.move_center_item:nth-child(12) {
animation: load 2.5s 0.8s infinite linear;
}
.move_center_item:nth-child(13) {
animation: load 2.5s 1s infinite linear;
}
.move_center_item:nth-child(14) {
animation: load 2.5s 1.2s infinite linear;
}
.move_center_item:nth-child(15) {
animation: load 2.5s 1.4s infinite linear;
}
}
.conten-top {
width: 100%;
padding-top: var(--status-bar-height);
padding-bottom: 30rpx;
background: url('@/static/icon/bg1.png');
background-size: cover;
text-align: center;
background-color: transparent;
position: absolute;
top: 0;
position: fixed;
z-index: 10;
}
.popup_box {
height: 100vh;
width: 100vw;
position: relative;
// 取消录音区域
.move {
width: 100%;
position: absolute;
left: 0;
bottom: 550rpx;
display: flex;
justify-content: center;
align-items: center;
.move_center_clear {
width: 40%;
height: 200rpx;
border-radius: 50rpx;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
// background: rgba(250, 83, 83, .5);
background: url('@/static/icon/ai6.png') no-repeat;
background-size: 100% 100%;
}
.move_center {
width: 40%;
height: 200rpx;
// background: rgba(149, 234, 108, .5);
background: url('@/static/icon/ai6.png') no-repeat;
background-size: 100% 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
}
.popup_box-con {
.popup_box-cona {
width: 100%;
position: absolute;
bottom: 288rpx;
display: flex;
justify-content: space-between;
padding: 0 40rpx;
view {
width: 140rpx;
height: 140rpx;
line-height: 140rpx;
border-radius: 50%;
text-align: center;
font-size: 37rpx;
color: #fff;
background: rgb(57, 57, 57);
margin: 0 auto;
}
view:hover {
background: #fff !important;
color: #333;
}
}
.popup_box-conb {
width: 100%;
height: 244rpx;
position: absolute;
bottom: 0;
image {
width: 100%;
height: 100%;
}
}
}
}
@keyframes blink {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.flex_c {
display: flex;
justify-content: space-between;
.flex_c_img {
width: 62rpx;
height: 62rpx;
margin-top: 20rpx;
image {
width: 100%;
height: 100%;
}
}
.flex_c_txt {
width: 450rpx;
margin-top: 15rpx;
.con {
width: 450rpx;
height: 70rpx;
line-height: 70rpx;
text-align: center;
background: #F5F5F5;
border-radius: 10rpx;
}
}
}
.flex_d {
height: 300rpx;
padding: 49rpx 50rpx;
display: flex;
view {
width: 136rpx;
height: 177rpx;
margin-right: 60rpx;
image {
width: 100%;
height: 100%;
}
}
}
.blinking-box {
display: flex;
font-size: 30rpx;
font-family: Microsoft YaHei;
font-weight: 400;
color: #182534;
image {
width: 40rpx;
height: 40rpx;
margin: auto 0;
margin-right: 30rpx;
}
}
.wrapper {
height: auto !important;
}
.contenta {
width: 710rpx;
height: 70rpx;
background-color: #fff;
display: flex;
margin-top: 10rpx;
padding-top: 12rpx;
.contenta-img {
width: 46rpx;
height: 46rpx;
image {
width: 100%;
height: 100%;
}
margin-left:34rpx;
}
.contenta-imga {
width: 37rpx;
height: 39rpx;
margin-top: 5rpx;
image {
width: 100%;
height: 100%;
}
margin-left:34rpx;
}
.contenta-ai {
width: 48rpx;
height: 48rpx;
image {
width: 100%;
height: 100%;
}
}
}
/* 加载数据提示 */
.tips {
position: fixed;
left: 0;
top: var(--window-top);
width: 100%;
z-index: 9;
background-color: rgba(0, 0, 0, 0.15);
height: 72rpx;
line-height: 72rpx;
transform: translateY(-80rpx);
transition: transform 0.3s ease-in-out 0s;
&.show {
transform: translateY(0);
}
}
.box-1 {
width: 100%;
height: 100%;
// min-height: calc(100vh - 100rpx);
padding-bottom: 200rpx;
box-sizing: content-box;
display: flex;
flex-direction: column;
justify-content: flex-end;
// /* 兼容iPhoneX */
// margin-bottom: 0;
// margin-bottom: constant(safe-area-inset-bottom);
// margin-bottom: env(safe-area-inset-bottom);
}
.multiline-text {
white-space: pre-line;
/* white-space: pre-wrap; */
}
.box-2 {
position: fixed;
left: 0;
width: 100%;
bottom: 0;
height: auto;
z-index: 2;
border-top: #e5e5e5 solid 1px;
box-sizing: content-box;
background-color: #fff;
transform: translateY(0);
/* 初始化 transform 属性 */
transition: transform 0.3s ease;
/* 添加过渡效果 */
/* 兼容iPhoneX */
padding-bottom: 0;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
>view {
padding: 20rpx 20rpx;
}
.content {
background: #F5F5F5;
height: 70rpx;
padding: 0 20rpx;
border-radius: 6rpx;
font-size: 28rpx;
}
.send {
background-color: #2573fb;
color: #fff;
height: 64rpx;
margin-left: 20rpx;
border-radius: 6rpx;
padding: 0;
width: 120rpx;
line-height: 62rpx;
&:active {
background-color: #1573fb;
}
}
}
.talk-list {
padding-bottom: 20rpx;
.talk-list-con {
width: 714rpx;
height: 320rpx;
background: #FFFFFF;
box-shadow: 5rpx 6rpx 0px 0px rgba(0, 0, 0, 0.02);
border-radius: 8rpx;
margin: 0 auto;
margin-top: 50rpx;
padding: 36rpx 32rpx;
.talk-list-con-title {
font-size: 32rpx;
font-family: Microsoft YaHei;
font-weight: 400;
color: #006CFF;
margin-bottom: 33rpx;
}
.talk-list-con-title1 {
font-size: 28rpx;
font-family: Microsoft YaHei;
font-weight: 400;
color: #182534;
line-height: 48rpx;
margin-bottom: 20rpx;
}
.talk-list-con-title2 {
display: flex;
font-size: 30rpx;
font-family: Microsoft YaHei;
font-weight: 400;
color: #182534;
view {
display: flex;
justify-content: space-between;
padding: 0 20rpx;
width: 468rpx;
height: 70rpx;
line-height: 70rpx;
text-align: center;
background: #E2EDF9;
border-radius: 12rpx;
image {
width: 55rpx;
height: 55rpx;
margin-top: 10rpx;
}
}
}
}
/* 消息项,基础类 */
.item {
padding: 20rpx 20rpx 0 20rpx;
align-items: flex-start;
align-content: flex-start;
color: #333;
.pic {
width: 92rpx;
height: 92rpx;
border-radius: 50%;
}
.content {
padding: 20rpx;
border-radius: 4px;
max-width: 580rpx;
word-break: break-all;
line-height: 52rpx;
position: relative;
}
/* 收到的消息 */
&.pull {
.content {
min-width: 20rpx;
min-height: 52rpx;
margin-left: 32rpx;
background-color: #fff;
&::after {
content: '';
display: none;
width: 0;
height: 0;
border-top: 16rpx solid transparent;
border-bottom: 16rpx solid transparent;
border-right: 20rpx solid #fff;
position: absolute;
top: 30rpx;
left: -18rpx;
}
}
}
/* 发出的消息 */
&.push {
/* 主轴为水平方向起点在右端。使不修改DOM结构也能改变元素排列顺序 */
// flex-direction: row-reverse;
.content {
min-width: 20rpx;
min-height: 52rpx;
margin-left: 32rpx;
background-color: #2573fb;
color: #fff;
&::after {
content: '';
display: block;
width: 0;
height: 0;
border-top: 16rpx solid transparent;
border-bottom: 16rpx solid transparent;
border-left: 20rpx solid #2573fb;
position: absolute;
top: 30rpx;
left: -20rpx;
transform: rotate(180deg);
}
}
}
}
}
.bing-math {
margin: 0 !important;
padding: 0 !important;
}
.talk-list .item.pull .content {
max-width: 590px;
margin-left: 0px;
}
.placeholder {
width: 100vw;
background-color: #1573fb;
// background-color: transparent;
// transform: translateY(0); /* 初始化 transform 属性 */
// transition: transform 0.3s ease; /* 添加过渡效果 */
}
.downsocket {
display: flex;
justify-content: center;
align-items: center;
background-color: #2573fb !important;
color: #fff !important;
}
</style>