550 lines
12 KiB
Vue
550 lines
12 KiB
Vue
|
|
<template>
|
|||
|
|
<view class="container" :style="pageHeight">
|
|||
|
|
<view class="box-1">
|
|||
|
|
<scroll-view
|
|||
|
|
scroll-y
|
|||
|
|
refresher-background="transparent"
|
|||
|
|
style="height: 100%;"
|
|||
|
|
@scrolltoupper="refresherrefresh"
|
|||
|
|
|
|||
|
|
:refresher-enabled="false"
|
|||
|
|
:scroll-with-animation="false"
|
|||
|
|
:refresher-triggered="scrollView.refresherTriggered"
|
|||
|
|
:scroll-into-view="scrollView.intoView"
|
|||
|
|
>
|
|||
|
|
<!-- <view id="topId"></view> -->
|
|||
|
|
<view class="talk-list" >
|
|||
|
|
<view class=""
|
|||
|
|
v-for="(item,index) in messagesList"
|
|||
|
|
:key="item.id"
|
|||
|
|
>
|
|||
|
|
<view class="item flex-col push" v-if="item.query" >
|
|||
|
|
<image :src="imagesUrl+userAvatar" mode="aspectFill" class="pic"></image>
|
|||
|
|
<view class="content">
|
|||
|
|
<template v-if="item.contentType === 'image'">
|
|||
|
|
<image :src="item.content" mode="widthFix" style="width: 400rpx;"></image>
|
|||
|
|
</template>
|
|||
|
|
<template v-else>
|
|||
|
|
{{item.query}}
|
|||
|
|
</template>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<view class="item flex-col pull " id="po_" v-if="item.answer" >
|
|||
|
|
<view class="po_z"></view>
|
|||
|
|
<image :src="info.icon" mode="aspectFill" class="pic"></image>
|
|||
|
|
<view class="content" >
|
|||
|
|
<template v-if="item.contentType === 'image'">
|
|||
|
|
<image :src="item.content" mode="widthFix" style="width: 400rpx;"></image>
|
|||
|
|
</template>
|
|||
|
|
<template v-else>
|
|||
|
|
|
|||
|
|
<text class="" >
|
|||
|
|
{{item.answer}}
|
|||
|
|
</text>
|
|||
|
|
|
|||
|
|
</template>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
<view id="bottomId"></view>
|
|||
|
|
</scroll-view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<view class="box-2">
|
|||
|
|
<view class="flex-col">
|
|||
|
|
|
|||
|
|
<view class="flex-grow">
|
|||
|
|
<input type="text" class="content" v-model="content" placeholder="Please enter what you want to know" placeholder-style="color:#DDD;" :cursor-spacing="6">
|
|||
|
|
</view>
|
|||
|
|
<!-- <view style="margin-right: 20rpx;">
|
|||
|
|
<uni-icons type="image" size="24" color="#32714F" @tap="handleImageClick"></uni-icons>
|
|||
|
|
|
|||
|
|
</view> -->
|
|||
|
|
<button class="send" @tap="handleSendClick">send</button>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!--
|
|||
|
|
text标签随意,不影响页面展示即可
|
|||
|
|
prop:prop随便取名,options是变量
|
|||
|
|
:change:[name]:name和prop保持一致就行
|
|||
|
|
renderScript:和新加的script标签的module名字一样
|
|||
|
|
onChange:renderScript标签中的方法,
|
|||
|
|
思路是,通过修改options,触发onChange方法
|
|||
|
|
-->
|
|||
|
|
<!-- <text :prop="options" :change:prop="renderScript.onChange"></text> -->
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
// @ts-nocheck - Multiple exports allowed for Vue component and renderjs
|
|||
|
|
import { getHistoryMsg } from "@/request/template-talk/history-msg.js";
|
|||
|
|
import requestChat from '../../utils/requestChat'
|
|||
|
|
import config from '@/config'
|
|||
|
|
import {startMsgSocket,msgSocketConnect,sendMsg,closeMsgSocket} from './msgSocket'
|
|||
|
|
|
|||
|
|
export default {
|
|||
|
|
data() {
|
|||
|
|
return {
|
|||
|
|
//是否长按事件
|
|||
|
|
islongPress:false,
|
|||
|
|
timer:null,//长按计时器
|
|||
|
|
options: {},
|
|||
|
|
imagesUrl:config.imagesUrl,
|
|||
|
|
info:{},
|
|||
|
|
title:'',
|
|||
|
|
// 聊天列表数据
|
|||
|
|
messagesList:[],
|
|||
|
|
// 滚动容器
|
|||
|
|
scrollView:{
|
|||
|
|
refresherTriggered:false,
|
|||
|
|
intoView:"",
|
|||
|
|
safeAreaHeight:0
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 请求参数
|
|||
|
|
ajax:{
|
|||
|
|
rows:20, //每页数量
|
|||
|
|
page:1, //页码
|
|||
|
|
flag:true, // 请求开关
|
|||
|
|
sendFlag:false
|
|||
|
|
},
|
|||
|
|
// 发送内容
|
|||
|
|
content:'',
|
|||
|
|
userId:null,
|
|||
|
|
userAvatar:null,
|
|||
|
|
taskChat:null,
|
|||
|
|
firstId:null,
|
|||
|
|
limit:100,
|
|||
|
|
scrollId:'bottomId',
|
|||
|
|
tempList :[],
|
|||
|
|
requestTask: null,
|
|||
|
|
tempBuffer: '',
|
|||
|
|
msgSocket:null,
|
|||
|
|
msgTimer:null
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
computed:{
|
|||
|
|
// 页面高度
|
|||
|
|
pageHeight(){
|
|||
|
|
const safeAreaHeight = this.scrollView.safeAreaHeight;
|
|||
|
|
if(safeAreaHeight > 0){
|
|||
|
|
return `height: calc(${safeAreaHeight}px - var(--window-top));`
|
|||
|
|
}
|
|||
|
|
return "";
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
onLoad(option) {
|
|||
|
|
if(option){
|
|||
|
|
let infoData = JSON.parse(option.data)
|
|||
|
|
|
|||
|
|
let tempInfo={
|
|||
|
|
icon:infoData.icon,
|
|||
|
|
token:infoData.token,
|
|||
|
|
conversation:infoData.conversation,
|
|||
|
|
userId:infoData.userId,
|
|||
|
|
userAvatar:infoData.userAvatar
|
|||
|
|
}
|
|||
|
|
uni.setStorageSync('userId',infoData.userId)
|
|||
|
|
this.info = tempInfo
|
|||
|
|
this.userId = infoData.userId
|
|||
|
|
this.userAvatar = infoData.userAvatar
|
|||
|
|
this.getMessage()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
onShow() {
|
|||
|
|
|
|||
|
|
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
methods: {
|
|||
|
|
longpress(){
|
|||
|
|
this.islongPress = true;
|
|||
|
|
console.log("长按事件");
|
|||
|
|
},
|
|||
|
|
//点击事件
|
|||
|
|
clickSprink(){
|
|||
|
|
// 非长按
|
|||
|
|
if(this.islongPress == false){
|
|||
|
|
console.log("点击事件");
|
|||
|
|
}else if(this.islongPress == true){
|
|||
|
|
console.log("长按事件");
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
//手指触摸动作开始
|
|||
|
|
touchstart(){
|
|||
|
|
this.timer = setTimeout(()=>{
|
|||
|
|
this.longpress();
|
|||
|
|
},2000)
|
|||
|
|
},
|
|||
|
|
//手指触摸动作结束
|
|||
|
|
touchend(){
|
|||
|
|
//延时执行为了防止 click() 还未判断 islongPress 的值就被置为 fasle
|
|||
|
|
clearTimeout(this.timer);
|
|||
|
|
setTimeout(() => {
|
|||
|
|
this.islongPress = false
|
|||
|
|
}, 200)
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
startSocket(){
|
|||
|
|
this.msgSocket = startMsgSocket(this.userId)
|
|||
|
|
this.msgInfo()
|
|||
|
|
//追加心跳机制
|
|||
|
|
},
|
|||
|
|
msgInfo() {
|
|||
|
|
if (this.msgSocket) {
|
|||
|
|
this.msgSocket.onMessage(res => {
|
|||
|
|
console.log('触发首页的消息回调',res);
|
|||
|
|
if(res.data.indexOf("conversation_id")>-1){
|
|||
|
|
uni.setStorageSync(this.info.conversation, JSON.parse(res.data).conversation_id);
|
|||
|
|
}else{
|
|||
|
|
this.messagesList[0].answer = this.messagesList[0].answer+res.data
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
async sendMessage(query) {
|
|||
|
|
if(this.msgSocket){
|
|||
|
|
closeMsgSocket(this.msgSocket);
|
|||
|
|
this.msgSocket= null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.startSocket()
|
|||
|
|
this.content = '';
|
|||
|
|
this.ajax.sendFlag = true;
|
|||
|
|
|
|||
|
|
const msgItem = {
|
|||
|
|
query: query,
|
|||
|
|
answer: "",
|
|||
|
|
contentType: 'text'
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const requestData = {
|
|||
|
|
"inputs": { "type": "text" },
|
|||
|
|
"query": query,
|
|||
|
|
"response_mode": "streaming",
|
|||
|
|
"conversation_id": uni.getStorageSync(this.info.conversation) || null,
|
|||
|
|
"user": this.userId,
|
|||
|
|
'token': this.info.token,
|
|||
|
|
'answer':''
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
this.messagesList.unshift(msgItem);
|
|||
|
|
let that = this;
|
|||
|
|
setTimeout(()=>{
|
|||
|
|
sendMsg(that.msgSocket,JSON.stringify(requestData))
|
|||
|
|
},500)
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
|
|||
|
|
// 在renderScript中触发的方法
|
|||
|
|
acceptData(data) {
|
|||
|
|
|
|||
|
|
let that = this
|
|||
|
|
let msgItem = this.messagesList[0]
|
|||
|
|
if(data.line){
|
|||
|
|
if(data.sendStatus&&data.sendStatus == 'ok'){
|
|||
|
|
const lastIndex = data.line.lastIndexOf('}');
|
|||
|
|
const validJsonString = data.line.slice(0, lastIndex + 1);
|
|||
|
|
let item = JSON.parse(validJsonString.replace('data: ',''))
|
|||
|
|
uni.setStorageSync(that.info.conversation,item.conversation_id)
|
|||
|
|
that.ajax.sendFlag = false
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if(data.line.indexOf('answer')>-1&&data.line.indexOf('{')>-1&&data.line.indexOf('}')>-1){
|
|||
|
|
try{
|
|||
|
|
// 查找最后一个出现的'}'字符的索引
|
|||
|
|
const lastIndex = data.line.lastIndexOf('}');
|
|||
|
|
|
|||
|
|
// 截取从开头到这个'}'字符(包括它)的所有内容
|
|||
|
|
const validJsonString = data.line.slice(0, lastIndex + 1);
|
|||
|
|
let item = JSON.parse(validJsonString.replace('data: ',''))
|
|||
|
|
if(item.answer&&!this.tempList.includes(item.answer+'')){
|
|||
|
|
this.tempList.push(item.answer+'')
|
|||
|
|
msgItem.answer = msgItem.answer+(item.answer)
|
|||
|
|
}
|
|||
|
|
that.$set(that.messagesList,0,msgItem)
|
|||
|
|
that.scrollView.intoView = 'bottomId';
|
|||
|
|
}catch (error) {
|
|||
|
|
console.log(177,data.line,177);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
goBack(){
|
|||
|
|
uni.navigateBack()
|
|||
|
|
},
|
|||
|
|
// 下拉刷新
|
|||
|
|
refresherrefresh(e){
|
|||
|
|
this.getMessage();
|
|||
|
|
this.scrollView.refresherTriggered = true;
|
|||
|
|
},
|
|||
|
|
// 获取历史消息
|
|||
|
|
getMessage(){
|
|||
|
|
|
|||
|
|
if(!this.ajax.flag||!uni.getStorageSync(this.info.conversation)){
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
let url = 'v1/messages?user='+this.userId+'&conversation_id='+uni.getStorageSync(this.info.conversation)+'&limit='+this.limit
|
|||
|
|
if(this.firstId){
|
|||
|
|
url = url+"&first_id="+this.firstId
|
|||
|
|
}
|
|||
|
|
let that = this
|
|||
|
|
let get = async ()=>{
|
|||
|
|
that.ajax.flag = false;
|
|||
|
|
let res = await requestChat({
|
|||
|
|
url: url ,
|
|||
|
|
method: 'get',
|
|||
|
|
token:that.info.token
|
|||
|
|
})
|
|||
|
|
let data = res.data
|
|||
|
|
that.scrollView.refresherTriggered = false;
|
|||
|
|
|
|||
|
|
// 获取待滚动元素选择器,解决插入数据后,滚动条定位时使用。取当前消息数据的第一条信息元素
|
|||
|
|
|
|||
|
|
for (var i = 0; i < data.length; i++) {
|
|||
|
|
if(i==0){
|
|||
|
|
that.firstId = data[i].id
|
|||
|
|
}
|
|||
|
|
that.messagesList.unshift(data[i])
|
|||
|
|
}
|
|||
|
|
that.$set(that.messagesList,0,that.messagesList[0])
|
|||
|
|
// 数据挂载后执行,不懂的请自行阅读 Vue.js 文档对 Vue.nextTick 函数说明。
|
|||
|
|
that.$nextTick(()=>{
|
|||
|
|
that.$forceUpdate()
|
|||
|
|
// 设置当前滚动的位置
|
|||
|
|
that.scrollView.intoView = this.scrollId;
|
|||
|
|
})
|
|||
|
|
that.ajax.flag = true;
|
|||
|
|
}
|
|||
|
|
get();
|
|||
|
|
},
|
|||
|
|
// 发送信息
|
|||
|
|
handleSendClick() {
|
|||
|
|
console.log('Send button clicked'); // 调试日志
|
|||
|
|
if(!this.content) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: 'Please enter valid content',
|
|||
|
|
icon: 'none'
|
|||
|
|
})
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
this.sendMessage(this.content);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss">
|
|||
|
|
@import "../../lib/global.scss";
|
|||
|
|
|
|||
|
|
.AirWall{
|
|||
|
|
width: 20px;
|
|||
|
|
height: 20px;
|
|||
|
|
}
|
|||
|
|
.top_{
|
|||
|
|
width: 100%;
|
|||
|
|
position: fixed;
|
|||
|
|
left: 0px;
|
|||
|
|
top: 0px;
|
|||
|
|
height: 180rpx;
|
|||
|
|
background: #32714F;
|
|||
|
|
z-index: 99999;
|
|||
|
|
box-sizing: border-box;
|
|||
|
|
|
|||
|
|
padding: 15px;
|
|||
|
|
padding-top: 100rpx;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
color: #fff;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
}
|
|||
|
|
page{
|
|||
|
|
background-color: #F3F3F3;
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
}
|
|||
|
|
.container{
|
|||
|
|
height: calc(100vh - var(--window-top));
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
flex-wrap: nowrap;
|
|||
|
|
align-content: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: stretch;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 加载数据提示 */
|
|||
|
|
.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: 0;
|
|||
|
|
flex: 1 0 auto;
|
|||
|
|
box-sizing: content-box;
|
|||
|
|
|
|||
|
|
}
|
|||
|
|
.box-2{
|
|||
|
|
height: auto;
|
|||
|
|
z-index: 2;
|
|||
|
|
border-top: #e5e5e5 solid 1px;
|
|||
|
|
box-sizing: content-box;
|
|||
|
|
background-color: #F3F3F3;
|
|||
|
|
|
|||
|
|
/* 兼容iPhoneX */
|
|||
|
|
padding-bottom: 0;
|
|||
|
|
padding-bottom: constant(safe-area-inset-bottom);
|
|||
|
|
padding-bottom: env(safe-area-inset-bottom);
|
|||
|
|
|
|||
|
|
>view{
|
|||
|
|
padding: 0 20rpx;
|
|||
|
|
height: 100rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.content{
|
|||
|
|
background-color: #fff;
|
|||
|
|
height: 64rpx;
|
|||
|
|
padding: 0 20rpx;
|
|||
|
|
border-radius: 32rpx;
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.send{
|
|||
|
|
background-color: #32714F;
|
|||
|
|
color: #fff;
|
|||
|
|
height: 64rpx;
|
|||
|
|
margin-left: 20rpx;
|
|||
|
|
border-radius: 32rpx;
|
|||
|
|
padding: 0;
|
|||
|
|
width: 120rpx;
|
|||
|
|
line-height: 62rpx;
|
|||
|
|
|
|||
|
|
&:active{
|
|||
|
|
background-color: #32714F;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.talk-list{
|
|||
|
|
padding-bottom: 20rpx;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column-reverse;
|
|||
|
|
flex-wrap: nowrap;
|
|||
|
|
align-content: flex-start;
|
|||
|
|
justify-content: flex-end;
|
|||
|
|
align-items: stretch;
|
|||
|
|
|
|||
|
|
// 添加弹性容器,让内容自动在顶部
|
|||
|
|
&::before{
|
|||
|
|
content: '.';
|
|||
|
|
display: inline;
|
|||
|
|
visibility: hidden;
|
|||
|
|
line-height: 0;
|
|||
|
|
font-size: 0;
|
|||
|
|
flex: 1 0 auto;
|
|||
|
|
height: 1px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
/* 消息项,基础类 */
|
|||
|
|
.item{
|
|||
|
|
padding: 20rpx 20rpx 0 20rpx;
|
|||
|
|
align-items:flex-start;
|
|||
|
|
align-content:flex-start;
|
|||
|
|
color: #333;
|
|||
|
|
|
|||
|
|
.pic{
|
|||
|
|
width: 92rpx;
|
|||
|
|
height: 92rpx;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
border: #fff solid 1px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.content{
|
|||
|
|
padding: 20rpx;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
max-width: 500rpx;
|
|||
|
|
word-break: break-all;
|
|||
|
|
line-height: 52rpx;
|
|||
|
|
position: relative;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 收到的消息 */
|
|||
|
|
&.pull{
|
|||
|
|
.content{
|
|||
|
|
margin-left: 32rpx;
|
|||
|
|
background-color: #fff;
|
|||
|
|
|
|||
|
|
&::after{
|
|||
|
|
content: '';
|
|||
|
|
display: block;
|
|||
|
|
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{
|
|||
|
|
margin-right: 32rpx;
|
|||
|
|
background-color: #32714F;
|
|||
|
|
color: #fff;
|
|||
|
|
|
|||
|
|
&::after{
|
|||
|
|
content: '';
|
|||
|
|
display: block;
|
|||
|
|
width: 0;
|
|||
|
|
height: 0;
|
|||
|
|
border-top: 16rpx solid transparent;
|
|||
|
|
border-bottom: 16rpx solid transparent;
|
|||
|
|
border-left: 20rpx solid #32714F;
|
|||
|
|
position: absolute;
|
|||
|
|
top: 30rpx;
|
|||
|
|
right: -18rpx;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|