【vue】仿对话(包含下拉刷新、滚动条到底部、流式数据)
vue
vuejs/vue: 是一个用于构建用户界面的 JavaScript 框架,具有简洁的语法和丰富的组件库,可以用于开发单页面应用程序和多页面应用程序。
项目地址:https://gitcode.com/gh_mirrors/vu/vue
免费下载资源
·
第一部分:下拉刷新
我这里就把主要的下拉刷新的写一下,上拉是一样的道理,就不写了
<div class="talk_top" ref="listWrapper" id="listWrapper">
<div class="loadingpic" v-loading="loading"></div>
<div v-for="message in messages" :key="message.id" class="message">
<div class="minetext" v-html="message.text"></div>
</div>
</div>
<script>
export default {
data() {
return {
loading: false, // 加载中
messages:[],
}
},
mounted() {
this.$nextTick(() => {
this.getAiLogsByTopicId(); // 初始化数据
this.re();
});
},
methods: {
//这是放在mounted中获取初始数据
getAiLogsByTopicId() {
this.$axios
.get("/userApp/ai/getAiLogsByTopicId", {
params: {
TopicId: this.topic.topicId,
pageNum: 1,
pageSize: this.page.pageSize
}
})
.then(res => {
if (res.code === 200) {
this.messages = res.rows.reverse();
// console.log(res.rows);
this.total = Number(res.total);
}
});
},
//放在re()里面的数据接口
getlist() {
if (this.total >= this.messages.length) {
this.page.pageNum++;
this.loading = true;
this.$axios
.get("/userApp/ai/getAiLogsByTopicId", {
params: {
TopicId: this.topic.topicId,
pageNum: this.page.pageNum,
pageSize: this.page.pageSize
}
})
.then(res => {
if (res.code === 200) {
if (this.page.pageNum >= Number(res.allPage)) {
this.$message.success("数据到底啦");
this.loading = false;
return;
} else {
this.messages = [...res.rows.reverse(), ...this.messages];
this.total = Number(res.total);
// console.log(this.messages);
this.loading = false;
}
}
})
.catch(error => {
this.$message.error(error.msg);
this.loading = false;
});
} else {
this.$message.success("数据到底啦");
// this.showdown = true;
this.loading = false;
}
},
// 下拉、上拉刷新
re() {
var flag = false;
var PstartX;
var PstartY;
var PMoveX;
var PMoveY;
var PendX;
var PendY;
let that = this;
document.onmousedown = function(ev) {
flag = true;
PstartX = ev.pageX;
PstartY = ev.pageY;
// console.log("start:" + PstartX, PstartY);
document.onmousemove = function(ev) {
PMoveX = ev.pageX;
PMoveY = ev.pageY;
if (flag) {
// console.log("move:" + PMoveX, PMoveY);
var resutl = getpostion(PMoveY, PstartY);
switch (resutl) {
case 0:
// console.log("无操作");
break;
case 1:
// console.log("向上");
break;
case 2:
// console.log("向下");
if (PMoveY - PstartY > 0) {
if (PMoveY - PstartY >= 50) {
document.getElementById("listWrapper").style.marginTop =
PMoveY - PstartY + "px";
}
that.loading = true;
// document.getElementById("loadingpic").style.display = "block";
}
break;
}
}
};
document.onmouseup = function(ev) {
flag = false;
PendX = ev.pageX;
PendY = ev.pageY;
// console.log("end:" + PendX, PendY);
var resutl = getpostion(PMoveY, PstartY);
switch (resutl) {
case 0:
// console.log("无操作");
break;
case 1:
// console.log("向上");
break;
case 2:
// console.log("向下");
// location.reload();
setTimeout(() => {
that.getlist(); //调用接口
//回弹到初始位置
document.getElementById("listWrapper").style.marginTop = "0px";
}, 500);
break;
}
};
// 判断是上拉还是下拉
function getpostion(PMoveY, PstartY) {
if (PMoveY - PstartY == 0) {
return 0; //无操作
}
if (PMoveY - PstartY < 0) {
return 1; //向上
}
if (PMoveY - PstartY > 0) {
return 2; //向下
}
}
};
},
}
}
</script>
在他的基础上修改了一下,他的上面有点小问题https://www.cnblogs.com/zmcxsf/p/10443189.html
第二部分:滚动条到底部
方法一
1、调用接口数据,然后调用re()方法,如果不调用re方法,页面没反应
2、在 mounted中获取滚动区域的document,然后让调用方法,让盒子滚动到底部,这样页面一打开,滚动条就在底部了
3、监听滚动事件,判断用户滚动状态
<template>
<div class="chat">
<div class="center">
<div v-if="pasId === 0">
<div class="talk_history" id="chat">
<div class="talk_top" ref="listWrapper" id="listWrapper">
<div class="loadingpic" v-loading="loading"></div>
<div v-for="message in messages" :key="message.id" class="message">
。。。。内容
</div>
</div>
</div>
<div class="talk_huifu">
<div style="border-bottom:1px solid #f8f8f8;display:flex">
<el-upload
ref="upload"
class="upload-demo"
action="https://jsonplaceholder.typicode.com/posts/"
:show-file-list="false"
:file-list="fileList"
accept=".pdf"
:on-change="talkchange"
:http-request="http"
>
<img
style="margin:0 15px 0 10px"
src="../../assets/img/wenjianjia.png"
title="发送文件"
/>
</el-upload>
</div>
<div align="right">
<el-input
type="textarea"
v-model="talk"
:maxlength="4000"
:rows="3"
resize="none"
placeholder="有什么我可以帮您?"
></el-input>
<el-button size="medium" :disabled="disabled" @click="sub(0)"
>发送</el-button
>
</div>
</div>
</div>
</div>
</template>
<script>
let source = null;
var AIurl = "http://192.168.4.172:8081/userApp/ai/sse";
var resultUrl = "http://192.168.4.172:8081/userApp/ai/aiHelp";
import { EventSourcePolyfill } from "event-source-polyfill";
export default {
layout: "AI2",
name: "",
data() {
return {
disabled: false, //ai回答结束,按钮取消禁用
showdown: false, //数据到底啦
chatContent: null,
isScrolling: true,
loading: false, // 加载中
page: 1,
total: 0, //总条数
talk: "", //发送的聊天信息
len: true, //发送信息的次数
fileList: [], //发送文件
topic: {}, //新建对话的信息
word: {}, //文件的信息
messages: [], //聊天历史
page: {
pageNum: 0,
pageSize: 10
},
total: 0,
pasId: 0, //上一页的id
};
},
mounted() {
this.getAiLogsByTopicId(); // 初始化数据
this.re();
setTimeout(() => {
this.chatContent = document.getElementsByClassName("talk_top")[0];
this.scrollToBottom();
// 监听滚动事件,判断用户滚动状态
this.chatContent.addEventListener("scroll", this.handleScroll);
}, 1000);
},
watch: {
disabled: {
handler(newval, oldval) {
this.disabled = newval;
},
deep: true,
immediate: true
}
},
methods: {
// 定义将滚动条定位在底部的方法
scrollToBottom() {
var that = this;
this.$nextTick(() => {
if (that.isScrolling) {
that.chatContent.scrollTop =
that.chatContent.scrollHeight - that.chatContent.offsetHeight;
// console.log(that.chatContent.scrollTop);
}
});
},
handleScroll() {
const scrollContainer = this.chatContent;
const scrollTop = scrollContainer.scrollTop;
const scrollHeight = scrollContainer.scrollHeight;
const offsetHeight = scrollContainer.offsetHeight;
if (scrollTop + offsetHeight < scrollHeight) {
// 用户开始滚动并在最底部之上,取消保持在最底部的效果
this.isScrolling = true;
} else {
// 用户停止滚动并滚动到最底部,开启保持到最底部的效果
this.isScrolling = false;
}
},
// 发送对话---------------------------------------------------
// 加载数据对话
getAiLogsByTopicId() {
this.$axios
.get("/userApp/ai/getAiLogsByTopicId", {
params: {
TopicId: this.topic.topicId,
pageNum: 1,
pageSize: this.page.pageSize
}
})
.then(res => {
if (res.code === 200) {
this.messages = res.rows.reverse();
// console.log(res.rows);
this.total = Number(res.total);
}
});
},
getlist() {
if (this.total >= this.messages.length) {
this.page.pageNum++;
this.loading = true;
this.$axios
.get("/userApp/ai/getAiLogsByTopicId", {
params: {
TopicId: this.topic.topicId,
pageNum: this.page.pageNum,
pageSize: this.page.pageSize
}
})
.then(res => {
if (res.code === 200) {
if (this.page.pageNum >= Number(res.allPage)) {
this.$message.success("数据到底啦");
this.loading = false;
return;
} else {
this.messages = [...res.rows.reverse(), ...this.messages];
this.total = Number(res.total);
// console.log(this.messages);
this.loading = false;
}
}
})
.catch(error => {
this.$message.error(error.msg);
this.loading = false;
});
} else {
this.$message.success("数据到底啦");
// this.showdown = true;
this.loading = false;
}
},
// 下拉、上拉刷新
re() {
var flag = false;
var PstartX;
var PstartY;
var PMoveX;
var PMoveY;
var PendX;
var PendY;
let that = this;
document.onmousedown = function(ev) {
flag = true;
PstartX = ev.pageX;
PstartY = ev.pageY;
// console.log("start:" + PstartX, PstartY);
document.onmousemove = function(ev) {
PMoveX = ev.pageX;
PMoveY = ev.pageY;
if (flag) {
// console.log("move:" + PMoveX, PMoveY);
var resutl = getpostion(PMoveY, PstartY);
switch (resutl) {
case 0:
// console.log("无操作");
break;
case 1:
// console.log("向上");
break;
case 2:
// console.log("向下");
if (PMoveY - PstartY > 0) {
if (PMoveY - PstartY >= 50) {
document.getElementById("listWrapper").style.marginTop =
PMoveY - PstartY + "px";
}
that.loading = true;
// document.getElementById("loadingpic").style.display = "block";
}
break;
}
}
};
document.onmouseup = function(ev) {
flag = false;
PendX = ev.pageX;
PendY = ev.pageY;
// console.log("end:" + PendX, PendY);
var resutl = getpostion(PMoveY, PstartY);
switch (resutl) {
case 0:
// console.log("无操作");
break;
case 1:
// console.log("向上");
break;
case 2:
// console.log("向下");
// location.reload();
setTimeout(() => {
that.getlist(); //调用接口
//回弹到初始位置
document.getElementById("listWrapper").style.marginTop = "0px";
}, 500);
break;
}
};
// 判断是上拉还是下拉
function getpostion(PMoveY, PstartY) {
if (PMoveY - PstartY == 0) {
return 0; //无操作
}
if (PMoveY - PstartY < 0) {
return 1; //向上
}
if (PMoveY - PstartY > 0) {
return 2; //向下
}
}
};
},
sub(type) {
let that = this;
// 判断次数,在发送
if (type === 1) {
this.messages.push({
aiId: "", //编号
userId: "", //用户id
userAsk: "", //用户询问
aiReply: "", //ai回复
askTime: "", //询问时间
isPdf: true, //是否是pdf
fileName: this.word.fileName, //文件名称
fileSize: this.word.fileSize, //文件大小
aiTopicId: this.topic.topicId //话题编号
});
this.$message.success("请稍等,正在响应中");
this.load(this.messages[this.messages.length - 1], 1, that.chatContent);//调用接口
setTimeout(() => {
that.chatContent.scrollTop = that.chatContent.scrollHeight + 150;
}, 500);
this.word = {};
} else {
if (this.talk === "") {
this.$message.error("请填写内容");
} else {
this.messages.push({
aiId: "", //编号
userId: "", //用户id
userAsk: this.talk, //用户询问
aiReply: "", //ai回复
askTime: "", //询问时间
isPdf: false, //是否是pdf
fileName: null, //文件名称
fileSize: null, //文件大小
aiTopicId: this.topic.topicId //话题编号
});
this.$message.success("请稍等,正在响应中");
this.load(
this.messages[this.messages.length - 1],
0,
that.chatContent
);
setTimeout(() => {
that.chatContent.scrollTop = that.chatContent.scrollHeight + 150;
}, 500);
}
this.talk = "";
}
},
// 获取ai结果
load(item, type, chatContent) {
this.disabled = true;
if (type === 1) {
source = new EventSourcePolyfill(
Url +
"userApp/ai/sse?question=" +
this.word.fileDownloadPath +
"&fileName=" +
this.word.fileName +
"&fileSize=" +
this.word.fileSize +
"&isPdf=true&aiTopicId=" +
this.topic.topicId,
{
headers: {
token: this.$cookies.get("userToken")
}
}
);
} else {
source = new EventSourcePolyfill(
Url +
"userApp/ai/sse?question=" +
this.talk +
"&isPdf=false&aiTopicId=" +
this.topic.topicId,
{
headers: {
token: this.$cookies.get("userToken")
}
}
);
}
// open:订阅成功(和后端连接成功)
source.addEventListener("open", function(e) {});
source.onmessage = e => {
// console.log(JSON.parse(e.data));
if (JSON.parse(e.data).end) {
// 结束了
item.aiId = JSON.parse(e.data).end;
this.disabled = false;
return;
} else if (JSON.parse(e.data).error) {
this.$message.error(JSON.parse(e.data).error);
this.disabled = false;
return;
} else {
item.aiReply += JSON.parse(e.data).content.replace(/\\n/g, "\n");
if (this.isScrolling) {
chatContent.scrollTop = chatContent.scrollHeight;
}
}
};
source.onerror = event => {
if (event.error !== undefined) {
this.$message.error("出错了,请联系客服人员");
}
this.disabled = false;
event.target.close();
};
},
// 上传文件
talkchange(file, fileList) {
this.$refs.upload.uploadFiles = [];
this.fileList = [];
this.fileList = fileList;
},
async http(file, fileList) {
var formData = new FormData();
formData.append("files", file.file);
this.$axios.post("/dev/file/uploadFile", formData).then(res => {
if (res.code === 200) {
this.word = {
fileName: res.data[0].fileName,
fileSize: res.data[0].fileSize,
fileDownloadPath: res.data[0].fileDownloadPath
};
// this.$message.success("上传成功");
this.sub(1);
}
});
},
}
};
</script>
方法二
如果在流式数据中使用,需要添加判断条件,如果滚动条在底部就滚动,否则不滚动
if (that.isScrolling) {
that.scrollToBottom()
}
<template>
<div id="Mindopt" class="Mindopt">
<div id="right_AI" class="right_AI">
<div
style="text-align: left; margin-bottom: 20px"
v-for="(ele, index) in messages"
:key="index"
>
<div class="box1" v-if="ele.content"></div>
<div class="box2" v-if="ele.reply_content"></div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Mindopt',
data () {
return {
isScrolling: true,
oldScrollTop: 0,
}
},
watch: {
messages: {
handler (val) {
this.scrollToBottom()
},
deep: true
}
},
mounted () {
document.querySelector('#right_AI').addEventListener('scroll', this.handleScroll)
},
created () {
this.chatMemory();
},
methods: {
// 定义将滚动条定位在底部的方法
scrollToBottom () {
this.$nextTick(() => {
if (this.isScrolling) {
var container = this.$el.querySelector('#right_AI')
container.scrollTop = container.scrollHeight
}
})
},
handleScroll () {
let scrollTop = document.querySelector('#right_AI').scrollTop
// 更新——滚动前,滚动条距文档顶部的距离
let scrollStep = scrollTop - this.oldScrollTop
this.oldScrollTop = scrollTop
//判断当前是向上or向下滚动
if (scrollStep < 0) {
//向上
this.isScrolling = false
} else {
this.isScrolling = true
}
},
// 获取当前对话历史
async chatMemory () {
axios
.get(this.$globalPath + "agent/info/1", {
headers: {
Authorization: getToken(),
},
})
.then((response) => {
this.messages = response.data.data.messages;
// 为了防止图片在加载中,然后不能及时获取到高度,以至于滚动条不到底部
setTimeout(() => {
this.scrollToBottom()
}, 1000)
})
.catch((error) => {
// 处理错误情况
console.error(error);
});
},
}
}
</script>
三、流式数据
没有下拉刷新,就只是流式数据对话,
发送对话,滚动条到底部,ai回答的时候出现省略号闪烁。
上传多个文件并显示文件列表
第一种方法:使用new XMLHttpRequest()
<template>
<div id="BotsLook" class="BotsLook">
<!-- 左侧对话内容 -->
<div style="display: flex; width: 100vw; height: calc(100% - 80px)">
<div class="left">
<div
class="content"
id="AIleft"
:style="{
height:
uploadList.length > 0
? `calc(100% - 200px)`
: `calc(100% - 110px)`,
}"
>
<div
style="text-align: left; margin-bottom: 20px"
v-for="(ele, index) in ansterList"
:key="index"
>
<div v-if="ele.files">
<div style="margin-bottom: 20px">
<div
style="
display: flex;
margin-bottom: 5px;
line-height: 32px;
font-size: 14px;
font-weight: 600;
"
>
<img :src="avatar" class="left_box_content_lizi_avaterImg" />
<span>{{ info.nick_name }}</span>
</div>
<div v-if="Array.isArray(ele.files)">
<div
class="wenjian_box_show"
style="margin-left: 32px"
v-for="(obj, ind) in ele.files"
:key="ind"
>
<div style="display: flex">
<img
:src="imgs(obj.file_name)"
style="width: 30px; height: 36px"
/>
<div style="width: 82%">
<div class="wenjian_box_name">
{{ obj.file_name }}
</div>
<div style="font-size: 12px">
{{ obj.file_size | sizeTostr }}
</div>
</div>
</div>
</div>
</div>
<div v-else class="wenjian_box_show" style="margin-left: 32px">
<div style="display: flex">
<img
:src="imgs(ele.files.file_name)"
style="width: 30px; height: 40px"
/>
<div style="width: 82%">
<div class="wenjian_box_name">
{{ ele.files.file_name }}
</div>
<div style="font-size: 12px">
{{ ele.files.file_size | sizeTostr }}
</div>
</div>
</div>
</div>
<div>
<div
v-if="ele.content"
class="left_box_content_anster"
:style="{
'margin-left': '32px',
}"
v-html="ele.content"
></div>
</div>
</div>
</div>
<div v-else>
<div v-if="ele.content" style="margin-bottom: 20px">
<div
style="
display: flex;
margin-bottom: 5px;
line-height: 32px;
font-size: 14px;
font-weight: 600;
"
>
<img :src="avatar" class="left_box_content_lizi_avaterImg" />
<span>{{ info.nick_name }}</span>
</div>
<div
class="left_box_content_anster"
style="margin-left: 32px"
v-html="ele.content"
></div>
</div>
</div>
<div style="margin-bottom: 20px">
<div
style="
display: flex;
margin-bottom: 5px;
line-height: 32px;
font-size: 14px;
font-weight: 600;
"
>
<img
:src="BotsInfo.ico"
class="left_box_content_lizi_avaterImg"
/>
<span>{{ BotsInfo.name }}</span>
</div>
<div
v-if="ele.reply_content"
style="display: inline-block; max-width: 100%"
class="imghover"
>
<div
v-highlight
class="left_box_content_lizi_AI markdown-body"
style="margin-left: 32px"
v-html="ele.reply_content"
></div>
</div>
</div>
</div>
<div
v-if="overdot"
class="left_box_content_lizi_AI"
style="margin-left: 32px; padding: 12px"
>
<span class="dot">•••</span>
</div>
</div>
<!-- 停止响应 -->
<div v-if="over" style="margin: auto; width: 100px">
<el-button
:style="{
position: 'absolute',
bottom: uploadList.length > 0 ? '190px' : '100px',
}"
@click="overBtn"
>{{ $t('Stop') }}</el-button
>
</div>
<!-- 输入框+文件 -->
<div class="left_box_inpt">
<div class="right_box_inpt_wenjian" v-if="uploadList.length > 0">
<div class="wenjian_far_box">
<div
class="wenjian_box"
style="position: relative"
v-for="(ele, index) in uploadList"
:key="index"
>
<div v-if="imgs(ele.file.name) === 'image'">
<img :src="ele.location" style="width: 30px; height: 36px" />
</div>
<div v-else style="display: flex">
<img
:src="imgs(ele.file.name)"
style="width: 30px; height: 36px"
/>
<div style="width: 65%">
<div class="wenjian_box_name">
{{ ele.file.name }}
</div>
<div style="font-size: 12px" v-if="!ele.progressFlag">
{{ ele.file.size | sizeTostr }}
</div>
<div style="font-size: 12px" v-if="ele.progressFlag">
{{ ele.progress }}%
</div>
</div>
</div>
<el-button
@click="deleteUpload(ele, index)"
type="text"
class="subbtn wenjian_btn"
icon="el-icon-error"
>
</el-button>
<div v-if="ele.progressFlag" class="mask"></div>
</div>
</div>
<div style="display: flex">
<el-input
v-model="memory"
@input="oninput"
@keyup.enter.native="sub()"
>
</el-input>
<div style="display: flex; line-height: 50px; padding: 0 10px">
<el-tooltip
class="item"
effect="dark"
:content="$t('upload')"
placement="top"
>
<el-upload
ref="upload"
action="#"
multiple
:http-request="httpRequest"
:before-upload="beforeUpload"
:show-file-list="false"
accept=".pdf,docx,.xls,.xlsx"
>
<el-button
class="subbtn"
type="text"
style="padding: 17px 0"
icon="el-icon-circle-plus-outline"
>
</el-button>
</el-upload>
</el-tooltip>
<span
style="
width: 1px;
height: 15px;
background-color: #dcdfe6;
margin: auto 10px;
"
></span>
<el-tooltip
class="item"
effect="dark"
:content="$t('send')"
placement="top"
>
<el-button
:disabled="BtnDisabled"
class="subbtn"
type="text"
@click="sub()"
>
<img
v-if="BtnDisabled"
src="@/assets/right1.png"
style="width: 15px"
/>
<img v-else src="@/assets/right3.png" style="width: 15px" />
</el-button>
</el-tooltip>
</div>
</div>
</div>
<div class="right_box_inpt" v-else>
<el-input
v-model="memory"
@input="oninput"
@keyup.enter.native="sub()"
>
</el-input>
<div style="display: flex; line-height: 50px; padding: 0 10px">
<el-tooltip
class="item"
effect="dark"
:content="$t('upload')"
placement="top"
>
<el-upload
ref="upload"
action="#"
multiple
:http-request="httpRequest"
:before-upload="beforeUpload"
:show-file-list="false"
accept=".pdf,docx,.xls,.xlsx"
>
<el-button
class="subbtn"
type="text"
style="padding: 17px 0"
icon="el-icon-circle-plus-outline"
>
</el-button>
</el-upload>
</el-tooltip>
<span
style="
width: 1px;
height: 15px;
background-color: #dcdfe6;
margin: auto 10px;
"
></span>
<el-tooltip
class="item"
effect="dark"
:content="$t('send')"
placement="top"
>
<el-button
:disabled="BtnDisabled"
class="subbtn"
type="text"
@click="sub()"
>
<img
v-if="BtnDisabled"
src="@/assets/right1.png"
style="width: 15px"
/>
<img v-else src="@/assets/right3.png" style="width: 15px" />
</el-button>
</el-tooltip>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import axios from "axios";
import { getToken } from "@/utils/auth";
import { matchType } from "@/utils/index";
import { mapGetters } from "vuex";
import { marked } from "marked";
// import hljs from "highlight.js";
// 两个地方
var xhr = new XMLHttpRequest();
export default {
computed: {
...mapGetters(["sidebar", "avatar", "name", "info"]),
},
data () {
return {
active: null,
BotsInfo: {
plugins: [],
private: [],
},
memory: "", //发送信息
ansterList: [],
over: false, //停止响应
overdot: false, //省略号是否显示
BtnDisabled: true, //发送按钮
isScrolling: true,
starttoolTime: 0, // 工具开始时间
startTime: 0, // 模型开始时间
endTime: 0, // 结束时间
uploadList: [],
loading: false, //文件上传中
dicshow: true,
dicshowBtn: false,
tooltipIsShow: false,
uniqueArr: [],
shareData: {
qr_code: "",
url: ""
},
loading: true,
oldScrollTop: 0
};
},
filters: {
sizeTostr: function (val) {
if (val === 0) return "0 B";
var k = 1024;
var sizes = ["B", "KB", "MB", "GB", "PB", "TB", "EB", "ZB", "YB"],
i = Math.floor(Math.log(val) / Math.log(k));
return (val / Math.pow(k, i)).toPrecision(3) + "" + sizes[i];
},
},
watch: {
ansterList: {
handler (val) {
this.scrollToBottom()
},
deep: true
}
},
created () {
this.agentBotInfo();
},
mounted () {
document.querySelector('#AIleft').addEventListener('scroll', this.handleScroll)
setTimeout(() => {
const dom = this.$el.querySelector("#rightdicshow")
const dicFather = this.$el.querySelector("#dicFather")
const domScrollWidth = dom && dom.scrollWidth
const domClientWidth = dicFather && dicFather.clientWidth
this.tooltipIsShow = (domScrollWidth + 62) === domClientWidth
}, 100);
},
beforeDestroy () {
xhr.abort();
this.overdot = false;
this.over = false;
this.BtnDisabled = false;
this.memory = "";
},
methods: {
// 定义将滚动条定位在底部的方法
scrollToBottom () {
this.$nextTick(() => {
if (this.isScrolling) {
var container = this.$el.querySelector('#AIleft')
container.scrollTop = container.scrollHeight
}
})
},
handleScroll () {
let scrollTop = document.querySelector('#AIleft').scrollTop
// 更新——滚动前,滚动条距文档顶部的距离
let scrollStep = scrollTop - this.oldScrollTop
this.oldScrollTop = scrollTop
//判断当前是向上or向下滚动
if (scrollStep < 0) {
//向上
this.isScrolling = false
} else {
this.isScrolling = true
}
},
// 获取详情
agentBotInfo () {
axios
.get(
this.$globalPath + "agent/info/" + JSON.parse(this.$route_query.id),
{
headers: {
Authorization: getToken(),
},
}
)
.then((response) => {
this.BotsInfo = response.data.data;
this.ansterList = this.BotsInfo.messages;
if (this.ansterList.length > 0) {
this.ansterList.forEach((i) => {
i.text = i.reply_content;
if (i.reply_content) {
// 创建自定义渲染器
class CustomRenderer extends marked.Renderer {
heading (text, level) {
// 将一级标题转换为h1标签
if (level === 1) {
return `<div class="hClass"># ${text}</div>`;
} else if (level === 2) {
return `<div class="hClass">## ${text}</div>`;
} else if (level === 3) {
return `<div class="hClass">### ${text}</div>`;
} else if (level === 4) {
return `<div class="hClass">#### ${text}</div>`;
} else if (level === 5) {
return `<div class="hClass">##### ${text}</div>`;
} else if (level === 6) {
return `<div class="hClass">###### ${text}</div>`;
}
}
}
// 使用自定义渲染器
const renderer = new CustomRenderer();
i.reply_content = marked(i.reply_content, {
renderer: renderer,
breaks: true,
});
// i.reply_content = marked(i.reply_content);
}
});
}
})
.catch((error) => {
// 处理错误情况
console.error(error);
});
},
// 对话-----------------------------------------------------------------------------------
// 根据文件名,判断后缀名格式,然后返回对应的图片
imgs (val) {
let text = matchType(val);
if (text === "pdf") {
return require("@/assets/pdf.svg");
} else if (text === "image") {
return text;
} else if (text === "excel") {
return require("@/assets/excel.svg");
} else if (text === "word") {
return require("@/assets/word.svg");
}
},
// 上传文件前
beforeUpload (file) {
// 限制文件大小和pdf不能超过250页
const fileLi = {};
for (const key in file) {
fileLi[key] = file[key];
}
const isLt2M = file.size > 10 * 1024 * 1024;
let fileType = file.name.substring(file.name.lastIndexOf(".") + 1);
const istype = ["pdf", "docx", "xls", "xlsx"].includes(fileType);
if (!istype) {
this.$message.error(this.$t("uploadType"));
return false;
}
if (isLt2M) {
this.$message.error(this.$t("uploadLt2M"));
return false;
}
if (istype && !isLt2M) {
this.uploadList.push({
file: fileLi,
progress: 0,
status: "ready",
location: "",
progressFlag: false,
local_file_url: "",
});
// this.loading = true;
}
return istype && !isLt2M;
},
async httpRequest (file, callback) {
this.BtnDisabled = true;
let formdata = new FormData();
formdata.append("file", file.file);
this.uploadList.forEach((i) => {
if (i.file.uid === file.file.uid) {
i.progressFlag = true;
}
});
axios({
url: this.$globalPath + "upload/kimi_upload",
method: "post",
data: formdata,
headers: {
"Content-Type": "multipart/form-data",
Authorization: getToken(),
},
onUploadProgress: (progressEvent) => {
// progressEvent.loaded:已上传文件大小
// progressEvent.total:被上传文件的总大小
this.uploadList.forEach((i) => {
if (i.file.uid === file.file.uid) {
i.progress = (progressEvent.loaded / progressEvent.total) * 100;
}
});
// this.uploadList[0].progress =
// (progressEvent.loaded / progressEvent.total) * 100;
},
}).then((response) => {
if (response.status === 200) {
this.uploadList.forEach((i) => {
if (i.file.uid === file.file.uid) {
i.status = "success";
i.file_id = response.data.file_id;
i.location = response.data.file_location;
i.local_file_url = response.data.local_file_url;
if (i.progress === 100) {
i.progressFlag = false;
}
}
});
} else {
this.uploadList.forEach((i) => {
if (i.file.uid === file.file.uid) {
i.status = "error";
this.uploadList.splice(index, 1);
}
});
}
let a = this.uploadList.every((item) => !item.progressFlag);
if (a) {
this.BtnDisabled = false;
}
});
},
// 删除文件
deleteUpload (ele, index) {
this.uploadList.splice(index, 1);
if (this.memory === "" && this.uploadList.length === 0) {
this.BtnDisabled = true;
}
},
// 监听输入框
oninput (e) {
if (e !== "") {
this.BtnDisabled = false;
} else {
this.BtnDisabled = true;
}
},
// 停止回响
overBtn () {
xhr.abort();
this.overdot = false;
this.over = false;
this.BtnDisabled = false;
this.memory = "";
this.ansterList[this.ansterList.length - 1].reply_content = "";
},
// 发送对话
sub (ele) {
let that = this;
if (ele) {
// 输入的内容
this.ansterList.push({
reply_content: "", //ai回复内容
// id: null,
// appid: 1, //应用编号 首页对话为1
// role: "user", //user=正常对话 system=预设内容
content: ele, //对话内容
userid: this.info.id, //当前用户
toolshow: false, //是否调用工具
tool: 0, //0是正在调用,1是已调用,2是运行完毕,3是展示运行结果
modelTime: 0, //模型时间
toolTime: 0, //工具时间
Time: 0, //总时间
toolName: "", //工具名称
toolText: false,
});
} else {
if (this.uploadList.length === 0) {
// 输入的内容
this.ansterList.push({
reply_content: "", //ai回复内容
content: this.memory, //对话内容
userid: this.info.id, //当前用户
toolshow: false,
tool: 0,
modelTime: 0, //模型时间
toolTime: 0, //工具时间
Time: 0,
toolName: "",
toolText: false,
});
} else {
const files = [];
this.uploadList.forEach((i) => {
files.push({
file_name: i.file.name, //文件名
file_size: i.file.size, //文件大小
file_url: i.location,
file_id: i.file_id,
local_file_url: i.local_file_url,
});
});
// 上传文件
this.ansterList.push({
reply_content: "", //ai回复内容
content: this.memory, //对话内容
userid: this.info.id, //当前用户
toolshow: false,
tool: 0,
modelTime: 0, //模型时间
toolTime: 0, //工具时间
Time: 0,
toolName: "",
toolText: false,
files: files,
});
}
}
const tijiao = {
id: this.BotsInfo.id,
memory: ele ? ele : this.memory, //对话内容
};
if (this.uploadList.length > 0) {
tijiao.files = [];
this.uploadList.forEach((i) => {
tijiao.files.push({
file_name: i.file.name, //文件名
file_size: i.file.size, //文件大小
file_url: i.location,
file_id: i.file_id,
local_file_url: i.local_file_url,
});
});
}
// console.log(tijiao);
setTimeout(() => {
this.load(
this.ansterList[this.ansterList.length - 1],
that.chatContent,
tijiao
);
}, 500);
},
// 获取ai结果
async load (item, chatContent, tijiao) {
this.startTime = 0;
this.startTime = new Date().getTime();
this.overdot = true;
this.over = true;
this.BtnDisabled = true;
this.memory = "";
this.uploadList = [];
const that = this;
let toolList = [];
if (that.isScrolling) {
that.scrollToBottom()
}
xhr.open("POST", this.$globalPath + "agent/public_chat", true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.setRequestHeader("Authorization", getToken());
xhr.onreadystatechange = function () {
// console.log(xhr);
if (xhr.readyState === 4 && xhr.status === 200) {
console.log("读取完毕");
// var result = xhr.responseText;
// console.log(result);
that.over = false;
that.overdot = false;
} else if (xhr.status !== 200 && xhr.status !== 0) {
console.log("HTTP 错误:" + xhr.status);
that.over = false;
that.overdot = false;
} else if (xhr.status === 0 && xhr.readyState !== 4) {
this.overdot = true;
this.over = true;
} else if (xhr.status === 0 && xhr.readyState === 4) {
that.over = false;
that.overdot = false;
that.$message({
message: this.$t("errorsub"),
type: "error",
});
}
};
xhr.onprogress = function (e) {
// console.log("onprogress", e);
//每次数据到达都会触发
var response = e.currentTarget.response;
if (response.slice(-6) == "[done]") {
// 这里做当收到'[done]'时的处理
console.log("成功完成!");
that.over = false;
that.overdot = false;
// item.reply_content = 'load1' + response
} else if (response.substr(0, 6) === "开始工具调用") {
item.toolshow = true;
item.tool = 0;
that.$forceUpdate();
that.starttoolTime = 0;
that.starttoolTime = new Date().getTime();
let coli = response.substring(response.lastIndexOf("正在调用"));
item.toolName = coli.substr(0, coli.indexOf(",")).substr(4);
let str = response.substring(6, response.indexOf("正在调用"));
that.toolRun(item, chatContent, str);
} else if (response.startsWith("多工具调用")) {
// console.log(response);
let aa = response.slice(6);
// 获取工具,too->开头,s\n结尾的
const toolRegex = /tool->([\s\S]+?)s\n/g;
if (aa.match(toolRegex) && aa.match(toolRegex).length > 0) {
// 去重
toolList = aa.match(toolRegex).filter((item, index, self) => {
return index === self.findIndex((obj) => obj === item);
});
that.uniqueArr = [];
// 去除开头tool->,结尾s\n
toolList.forEach((i) => {
i = i.slice(6, -2);
that.uniqueArr.push(i.split("|"));
});
if (that.uniqueArr.length > 0) {
item.toolName = that.uniqueArr[0][0];
// that.uniqueArr.forEach((j) => {
// item.toolName = j[0];
// item.toolshow = true;
// item.tool = 0;
// });
if (item.toolName) {
item.toolshow = true;
item.tool = 0;
}
}
// 去除插件的部分,保留回答的内容
let abc = null;
abc = toolList.join(" ");
// 创建自定义渲染器
class CustomRenderer extends marked.Renderer {
heading (text, level) {
// 将一级标题转换为h1标签
if (level === 1) {
return `<div class="hClass"># ${text}</div>`;
} else if (level === 2) {
return `<div class="hClass">## ${text}</div>`;
} else if (level === 3) {
return `<div class="hClass">### ${text}</div>`;
} else if (level === 4) {
return `<div class="hClass">#### ${text}</div>`;
} else if (level === 5) {
return `<div class="hClass">##### ${text}</div>`;
} else if (level === 6) {
return `<div class="hClass">###### ${text}</div>`;
}
}
}
// 使用自定义渲染器
const renderer = new CustomRenderer();
const lastcont = aa.slice(abc.length - 1);
if (lastcont.trim().length > 0) {
item.tool = 2;
item.Time = 0;
item.modelTime = 0;
item.toolTime = 0;
that.uniqueArr.forEach((i) => {
item.Time += Number(i[1].slice(0, -1));
item.modelTime += Number(i[2].slice(2, -1));
item.toolTime += Number(i[3].slice(2));
});
item.Time = item.Time.toFixed(2);
item.modelTime = item.modelTime.toFixed(2);
item.toolTime = item.toolTime.toFixed(2);
item.reply_content = marked(lastcont, {
renderer: renderer,
breaks: true,
});
item.text = lastcont;
} else {
item.tool = 1;
}
if (that.isScrolling) {
chatContent.scrollTop = chatContent.scrollHeight;
}
}
} else {
item.toolshow = false;
that.over = true;
that.overdot = false;
// 创建自定义渲染器
class CustomRenderer extends marked.Renderer {
heading (text, level) {
// 将一级标题转换为h1标签
if (level === 1) {
return `<div class="hClass"># ${text}</div>`;
} else if (level === 2) {
return `<div class="hClass">## ${text}</div>`;
} else if (level === 3) {
return `<div class="hClass">### ${text}</div>`;
} else if (level === 4) {
return `<div class="hClass">#### ${text}</div>`;
} else if (level === 5) {
return `<div class="hClass">##### ${text}</div>`;
} else if (level === 6) {
return `<div class="hClass">###### ${text}</div>`;
}
}
}
// 使用自定义渲染器
const renderer = new CustomRenderer();
item.reply_content = marked(response, {
renderer: renderer,
breaks: true,
});
item.text = response;
// item.reply_content = item.reply_content.replace(/\s+$/m, "");
}
if (that.isScrolling) {
that.scrollToBottom()
}
};
xhr.send(JSON.stringify(tijiao));
},
//调用工具接口
toolRun (item, chatContent, str) {
this.endTime = 0;
this.over = true;
this.BtnDisabled = true;
const that = this;
item.tool = 1;
if (that.isScrolling) {
that.scrollToBottom()
}
xhr.open("POST", this.$globalPath + "agent/tool_run", true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.setRequestHeader("Authorization", getToken());
xhr.onreadystatechange = function () {
// console.log('toolRun', xhr);
if (xhr.readyState === 4 && xhr.status === 200) {
console.log("读取完毕");
// var result = xhr.responseText;
// console.log(result);
if (item.tool === 3) {
localStorage.setItem("tool", 2);
item.toolText = true;
that.$forceUpdate();
} else {
item.tool = 2;
item.toolText = true;
}
that.over = false;
that.overdot = false;
// that.agentBotInfo();
} else if (xhr.status !== 200 && xhr.status !== 0) {
console.log("HTTP 错误:" + xhr.status);
that.over = false;
that.overdot = false;
} else if (xhr.status === 0 && xhr.readyState !== 4) {
this.overdot = true;
this.over = true;
} else if (xhr.status === 0 && xhr.readyState === 4) {
that.over = false;
that.overdot = false;
that.$message({
message: this.$t("errorsub"),
type: "error",
});
}
};
xhr.onprogress = function (e) {
if (that.endTime === 0) {
that.endTime = new Date().getTime();
item.toolTime = (that.endTime - that.starttoolTime) / 1000; // 转换为秒
item.modelTime = (that.endTime - that.startTime) / 1000; // 转换为秒
item.Time = (item.toolTime + item.modelTime).toFixed(2);
}
// console.log("onprogress", e);
//每次数据到达都会触发
var response = e.currentTarget.response;
// console.log('toolRun', response);
if (response.slice(-6) == "[done]") {
// 这里做当收到'[done]'时的处理
console.log("成功完成!");
that.over = false;
that.overdot = false;
} else {
that.over = true;
that.overdot = false;
// 创建自定义渲染器
class CustomRenderer extends marked.Renderer {
heading (text, level) {
// 将一级标题转换为h1标签
if (level === 1) {
return `<div class="hClass"># ${text}</div>`;
} else if (level === 2) {
return `<div class="hClass">## ${text}</div>`;
} else if (level === 3) {
return `<div class="hClass">### ${text}</div>`;
} else if (level === 4) {
return `<div class="hClass">#### ${text}</div>`;
} else if (level === 5) {
return `<div class="hClass">##### ${text}</div>`;
} else if (level === 6) {
return `<div class="hClass">###### ${text}</div>`;
}
}
}
// 使用自定义渲染器
const renderer = new CustomRenderer();
item.reply_content = marked(response, {
renderer: renderer,
breaks: true,
});
item.text = response;
// item.reply_content = item.reply_content.replace(/\s+$/m, "");
}
if (that.isScrolling) {
that.scrollToBottom()
}
};
xhr.send(str);
},
},
};
</script>
<style lang="scss" scoped>
.dot {
font-family: simsun; /*固定字体避免设置的宽度无效*/
animation: dot 1s infinite step-start;
display: inline-block;
width: 1.5em;
vertical-align: bottom; /*始终让省略号在文字的下面*/
overflow: hidden;
}
@keyframes dot {
/*动态改变显示宽度, 但始终让总占据空间不变, 避免抖动*/
0% {
width: 0;
margin-right: 1.5em;
}
33% {
width: 0.5em;
margin-right: 1em;
}
66% {
width: 1em;
margin-right: 0.5em;
}
100% {
width: 1.5em;
margin-right: 0;
}
}
</style>
第二种方法:post方法使用插件@microsoft/fetch-event-source
这里就只写方法了
其中new AbortController()是为了可以断开连接
<script>
let ctrlAbout2 = null;
import { fetchEventSource } from '@microsoft/fetch-event-source';
export default {
data () {
return {
};
},
beforeDestroy () {
if (ctrlAbout2) {
ctrlAbout2.abort()
}
},
methods: {
toolRun (item, chatContent, str) {
ctrlAbout2 = new AbortController();
let source2 = fetchEventSource(this.$globalPath + "agent/tool_run", {
method: 'POST',
headers: {
"Content-Type": 'application/json',
"Accept": 'text/event-stream; charset=utf-8',
'Connection': 'keep-alive',
'Authorization': getToken()
},
body: JSON.stringify(str),
openWhenHidden: true,
onopen (response) {
// // console.log(response)
// if (response.ok) {
// return; // everything's good
// } else if (response.status >= 400 && response.status < 500 && response.status !== 429) {
// // client-side errors are usually non-retriable:
// throw new FatalError();
// } else {
// throw new RetriableError();
// }
},
onmessage (event) {
//每次数据到达都会触发
// 返回的JSON格式,是单引号不能转换,必须双引号才能JSON.parse
var res2 = event.data.replaceAll(/"/g, '\\"')
var res = res2.replaceAll(/'/g, '"')
var response = JSON.parse(res).message
// var response = event.data
if (response === "[done]") {
// 这里做当收到'[done]'时的处理
console.log("成功完成!");
that.over = false;
that.overdot = false;
that.BtnDisabled = true;
that.inputdisabled = false
} else {
that.over = true;
that.overdot = false;
messag1 += response.replaceAll("\\n", "\n")
item.reply_content = marked(messag1, markedOptions);
item.text = messag1;
}
if (that.isScrolling) {
that.scrollToBottom()
}
},
onclose (msg) {
// console.log(msg);
ctrlAbout2.abort()//出错后不要重试
that.over = false;
that.BtnDisabled = true;
that.inputdisabled = false
},
onerror (error) {
// console.log(error);
ctrlAbout2.abort()
that.over = false;
that.BtnDisabled = true;
that.inputdisabled = false
throw error// 必须抛出错误,否则停止不了
}
})
},
// 停止回响
overBtn () {
if (ctrlAbout) {
ctrlAbout.abort()
}
if (ctrlAbout2) {
ctrlAbout2.abort()
}
// xhr.abort();
this.overdot = false;
this.over = false;
this.BtnDisabled = false;
this.inputdisabled = false
this.messinput = "";
// this.messages[this.messages.length - 1].reply_content = "";
},
},
};
</script>
第三种方法:get使用插件"event-source-polyfill"
<script>
let eventSource = null
import { EventSourcePolyfill } from "event-source-polyfill";
export default {
data () {
return {
};
},
beforeDestroy () {
if (eventSource) {
eventSource.close();
}
},
methods: {
getanalysis (uuid, ele) {
this.itemanalysis = ele
ele.isanalysis = true
this.$forceUpdate();
if (eventSource) {
eventSource.close();
}
ele.drawerAnalysis = ''
let url = this.$globalPath + 'agent/get_logs?uuid=' + uuid
let that = this
this.drawerdot = true
if (that.isScrolling) {
that.scrollToBottom()
}
// 如果需要加参数的话是在url 后面拼接参数 sysm/user/1
eventSource = new EventSourcePolyfill(
url,
{
// heartbeatTimeout: 3 * 60 * 1000,//这是自定义配置请求超时时间 默认是4500ms(印象中是)
headers: {
'Authorization': getToken(),
},
}
);
let message = ''
// open:订阅成功(和后端连接成功)
eventSource.onopen = function (e) { };
eventSource.onmessage = function (e) {
if (e.data === '[done]') {
eventSource.close();
that.drawerdot = false
} else {
// 返回的JSON格式,是单引号不能转换,必须双引号才能JSON.parse
var res2 = e.data.replaceAll(/"/g, '\\"')
var res = res2.replaceAll(/'/g, '"')
var cont = JSON.parse(res).message
message += cont.replaceAll("\\n", "\n") + ``
ele.drawerAnalysis = marked(message, {
breaks: true,
});
if (that.isScrolling) {
that.scrollToBottom()
}
}
};
//error:错误(可能是断开,可能是后端返回的信息)
eventSource.onerror = function (e) {
that.drawerdot = false
e.target.close(); // 关闭连接
};
},
},
};
</script>
GitHub 加速计划 / vu / vue
207.53 K
33.66 K
下载
vuejs/vue: 是一个用于构建用户界面的 JavaScript 框架,具有简洁的语法和丰富的组件库,可以用于开发单页面应用程序和多页面应用程序。
最近提交(Master分支:1 个月前 )
73486cb5
* chore: fix link broken
Signed-off-by: snoppy <michaleli@foxmail.com>
* Update packages/template-compiler/README.md [skip ci]
---------
Signed-off-by: snoppy <michaleli@foxmail.com>
Co-authored-by: Eduardo San Martin Morote <posva@users.noreply.github.com> 3 个月前
e428d891
Updated Browser Compatibility reference. The previous currently returns HTTP 404. 4 个月前
更多推荐
已为社区贡献10条内容
所有评论(0)