xunfeiAI/pages/index/index1.vue

2239 lines
52 KiB
Vue
Raw Permalink Normal View History

2023-11-01 15:08:17 +08:00
<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">
<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 class="contenta" v-if="c_content!=''&&index==nums">
<view class="contenta-img" v-if='palystatus' @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="palystatus==false">
<image src="@/static/icon/ai.gif" mode="aspectFit"></image>
</view>
<view class="contenta-imga" @tap="copyContent(c_content)"
:style="{'margin-left':palystatus==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 class="content multiline-text">
<!-- <rich-text :nodes="item.content"></rich-text> -->
<bing-math :key="`math-${item.id}`" class="bing-math" :latex="item.content"></bing-math>
</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" @touchstart="start" @longpress="longpress"
@click="longclick">
{{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" @touchstart="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,
} 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,
palystatus: true, //播放状态
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,
touchT: null,
touchE: null,
islong: false,
// 是否开始绘制
isDraw: false,
imageSize: {
},
audio_file: [],
currentIndex: 0,
wv: null
}
},
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;
})
});
this.list()
},
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() {
uni.chooseImage({
count: 1,
sizeType: ['original', 'compressed'],
sourceType: ['album'], //这要注意camera掉拍照album是打开手机相册
success: (res) => {
// const tempFilePaths = res.tempFilePaths;
this.imgfile(res.tempFilePaths)
let data = {
"id": '@',
"content": '',
"type": 2,
"pic": this.avatarType
}
this.talkList.push(data);
}
});
},
//拍照
camera() {
uni.chooseImage({
count: 1,
sizeType: ['original', 'compressed'],
sourceType: ['camera'], //这要注意camera掉拍照album是打开手机相册
success: (res) => {
console.log(res);
// const tempFilePaths = res.tempFilePaths;
this.imgfile(res.tempFilePaths)
let data = {
"id": '@',
"content": '',
"type": 2,
"pic": this.avatarType
}
this.talkList.push(data);
}
});
},
//复制
copyContent(data) {
uni.setClipboardData({
data,
success: function() {
uni.getClipboardData({
success: function(res) {
uni.showToast({
title: "复制成功",
});
}
});
}
});
},
//图片转文字
imgfile(tempFilePath) {
let that = this
console.log(tempFilePath, '2')
uni.uploadFile({
url: HTTP_REQUEST_URL + '/api/xun_fei/ocr', // 上传地址
filePath: tempFilePath[0], // 要上传的文件路径
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);
}
})
},
//获取讯飞地址
list() {
iatWss().then((res) => {
this.audioUrl = res.data.iat_wss
})
},
//功能展开
addgn() {
this.gnishow = !this.gnishow
},
// 点击关闭遮罩层
closePop() {
this.$refs.popup.close()
},
//语音转文字
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) {
// console.log(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.send1()
}
// console.log( that.talkList)
// if (that.talkList.length > 0) {
// that.talkList.splice(that.talkList.length - 1, 1)
// } else {
// that.talkList.splice(0, 1)
// }
that.voicePath = ''
// if (that.content.length > 0) {
// 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();
}
})
},
//振动
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)
})
// 暂时不用
// let arry1;
// if (val.indexOf(',') == -1) {
// arry1 = val.split(',')
// } else if (val.indexOf('') == -1) {
// arry1 = val.split('')
// } else {
// arry1.push(val)
// }
// const temp = []
// for (let i = 0; i < arry1.length; i++) {
// temp.push(
// // 使用Promise用于异步计算
// new Promise((resolve, reject) => {
// ttWss({
// data: arry1[i]
// }).then(res => {
// return resolve(res.data.mp3)
// }).catch(err => {
// return reject(err)
// })
// })
// )
// }
// Promise.all(temp).then(res => {
// // console.log(res,'2222222')
// this.audio_file = res
// this.ScanAudio(res, this.currentIndex, 0)
// })
},
//语音分段 数组 条数 延时
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);
});
},
//点击事件
longclick() {
if (this.touchE - this.touchT < 1000) {
console.log('点击');
}
},
longpress(e) {
// console.log(e.touches[0].pageX)
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,
sampleRate: 16000, //采样率,有效值 8000/16000/44100
numberOfChannels: 1, //录音通道数,有效值 1/2
encodeBitRate: 96000, //编码码率
format: "MP3", //音频格式,有效值 aac/mp3
frameSize: 8, //指定帧大小
// format: "pcm",
});
recorderManager.onFrameRecorded((res) => {
console.log(res, '111111111111')
let arr1 = res
uni.sendHostEvent('log', option, (ret) => {
//发送消息成功回调
console.log('语音分片消息成功' + JSON.stringify(arr1));
});
})
this.touchStartX = e.touches[0].pageX; // 获取触摸时的原点
this.touchStartY = e.touches[0].pageY; // 获取触摸时的原点
},
start(e) {
// console.log(e.touches[0].pageX)
this.touchStartX = e.touches[0].pageX; // 获取触摸时的原点
this.touchStartY = e.touches[0].pageY; // 获取触摸时的原点
},
end() {
this.touchE = new Date().getTime();
if (this.islong) {
this.closePop()
// clearInterval(this.timer1)
// this.btnStatus = 0
// // 暂停播放
// innerAudioContext.stop()
// //异步,解决录音结束后,路径没赋值就传值,导致的路径为空
// setTimeout(() => {
// let obj = {
// status: false,
// path: [{
// src: this.voicePath,
// switchStatus: false,
// soundTime: this.num
// }]
// }
// console.log(1111, this.voicePath)
// // if (this.voicePath.length > 0) {
// // uni.$u.throttle(this.uploadFile(this.voicePath), 500)
// // }
// this.num = 0
// // pathToBase64(this.voicePath)
// // .then(base64 => {
// // const arrayBuffer = new Uint8Array(base64)
// // const base1 = uni.arrayBufferToBase64(arrayBuffer)
// // let params = {
// // common: {
// // app_id: '2eda6c2e',
// // },
// // business: {
// // language: "zh_cn",
// // domain: "iat",
// // accent: "mandarin",
// // vad_eos: 5000,
// // dwa: "wpgs",
// // },
// // data: {
// // status: 2,
// // format: "audio/L16;rate=16000",
// // encoding: "raw",
// // audio: base64.split(',')[1]
// // },
// // };
// // this.audioTxt = JSON.stringify(params)
// // this.audiosckt()
// // })
// // .catch(error => {
// // console.error(error)
// // })
// // this.socketaudio.send(JSON.stringify(params))
// }, 500)
this.islong = false
clearInterval(this.timer)
this.btnStatus = 0
recorderManager.stop();
// console.log(this.voicePath, "888");
// 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"
})
}
}
}
},
toBase64() {
pathToBase64(this.voicePath)
.then(base64 => {
// this.$scope.$getAppWebview().children()[0].evalJS('uniEvent(base64)');
// const arrayBuffer = new Uint8Array(base64)
// const base1 = uni.arrayBufferToBase64(arrayBuffer.split(0, 1280))
// let params = {
// common: {
// app_id: '2eda6c2e',
// },
// business: {
// language: "zh_cn",
// domain: "iat",
// accent: "mandarin",
// vad_eos: 5000,
// dwa: "wpgs",
// },
// data: {
// status: 2,
// format: "audio/L16;rate=16000",
// encoding: "raw",
// audio: base1
// },
// };
// this.audioTxt = JSON.stringify(params)
})
.catch(error => {
console.error(error)
})
},
//删除本地录音文件
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) {
this.palystatus = !this.palystatus
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(item, i) {
this.palystatus = !this.palystatus
if (!this.palystatus) {
innerAudioContext.src = item.audio_file;
innerAudioContext.play()
} else {
innerAudioContext.pause()
}
innerAudioContext.onError((res) => {
console.log(res.errMsg);
console.log(res.errCode);
this.palystatus = true
});
innerAudioContext.onEnded((res) => {
this.palystatus = true
});
},
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 {
clearInterval(this.timer);
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
}
},
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
}
}
},
audiosckt() {
this.socketaudio = uni.connectSocket({
//url: encodeURI(encodeURI(myUrl).replace(/\+/g, '%2B')),
url: this.audioUrl,
method: 'GET',
token: '',
success: res => {
console.log(res, "ws成功连接...")
}
})
// 消息的发送和接收必须在正常连接打开中,才能发送或接收【否则会失败】
this.socketaudio.onOpen((res) => {
console.log("WebSocket连接正常打开中...");
//第一帧
let params = {
common: {
app_id: '2eda6c2e',
},
business: {
language: "zh_cn",
domain: "iat",
accent: "mandarin",
vad_eos: 5000,
dwa: "wpgs",
},
data: {
status: 0,
format: "audio/L16;rate=16000",
encoding: "raw",
},
};
this.socketaudio.send({
data: JSON.stringify(params),
async success(res) {
console.log("消息发送成功", '1');
that.audioTxt = ''
},
});
//第二帧
this.handlerInterval = setInterval(() => {
// 中间帧
console.log(this.audioTxt)
let that = this
this.socketaudio.send({
data: JSON.stringify({
data: {
status: 1,
format: 'audio/L16;rate=16000',
encoding: 'raw',
audio: this.audioTxt
}
}),
async success(res) {
console.log("消息发送成功", that.audioTxt, '2');
that.audioTxt = ''
},
});
if (this.audioTxt.length === 0) {
this.socketaudio.send({
data: JSON.stringify({
data: {
status: 2,
format: 'audio/L16;rate=16000',
encoding: 'raw',
audio: ''
}
}),
async success(res) {
console.log("消息发送成功", '3');
clearInterval(that.handlerInterval);
return false;
},
});
};
}, 40);
// 注:只有连接正常打开中 ,才能正常成功发送消息
// this.socketaudio.send({
// data: JSON.stringify(params),
// async success(res) {
// console.log("消息发送成功", res);
// },
// });
// 注:只有连接正常打开中 ,才能正常收到消息
})
// 这里仅是事件监听【如果socket关闭了会执行】
// this.socketaudio.onClose(() => {
// console.log("已经被关闭了")
// })
this.socketaudio.onMessage((res) => {
console.log("收到服务器内容:" + res.data);
});
},
send1() {
this.showStop = true;
// 将当前发送信息 添加到消息列表。
let data = {
"id": new Date().getTime(),
"content": this.content,
"type": 1,
"pic": this.avatarType
}
this.TEXT = this.content,
this.talkList.push(data);
this.talkList.push({
"id": new Date().getTime(),
"content": '',
"type": 2,
// "pic": this.avatarType
});
this.n_content = '';
this.c_content = '';
this.socketing = true;
// return ;
this.$nextTick(() => {
// 清空内容框中的内容
this.content = '';
})
this.sendToSpark1();
},
async sendToSpark1() {
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('连接成功...')
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.n_content = this.talkList[this.talkList.length - 1].content;
realThis.tempRes = realThis.tempRes + dataArray[i].content
}
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
})
this.nums = this.talkList.length - 1
this.txtspeech(this.n_content, this.talkList.length -
1)
setTimeout(() => {
let that = this
realThis.socketTask.close({
success(res) {
console.log('关闭成功', res)
realThis.wsLiveFlag = false;
},
fail(err) {
// console.log('关闭失败', err)
}
})
}, 1000)
}
}
})
},
// 发送信息
send() {
if (!this.content) {
uni.showToast({
title: '请输入有效的内容',
icon: 'none'
})
return;
}
this.showStop = true;
// 将当前发送信息 添加到消息列表。
let data = {
"id": new Date().getTime(),
"content": this.content,
"type": 1,
// "pic": this.avatar
"pic": this.avatarType
}
// console.log(data);
this.TEXT = this.content;
this.talkList.push(data);
this.talkList.push({
"id": new Date().getTime(),
"content": '',
"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.n_content = this.talkList[this.talkList.length - 1].content;
realThis.tempRes = realThis.tempRes + dataArray[i].content
}
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
})
this.nums = this.talkList.length - 1
this.txtspeech(this.n_content, this.talkList.length -
1)
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>