541 lines
13 KiB
Vue
541 lines
13 KiB
Vue
|
|
<template>
|
|||
|
|
<view class="container" :style="pageHeight">
|
|||
|
|
<!-- <view class="top_">
|
|||
|
|
<u-icon @click="goBack()" name="arrow-left" color="#fff" size="20"></u-icon>
|
|||
|
|
<view class="">{{info.title || "details"}}</view>
|
|||
|
|
<view class="AirWall"></view>
|
|||
|
|
</view> -->
|
|||
|
|
<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 " v-if="item.answer" >
|
|||
|
|
<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>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
import { getHistoryMsg } from "@/request/template-talk/history-msg.js";
|
|||
|
|
import requestChat from '../../utils/requestChat'
|
|||
|
|
import config from '@/config'
|
|||
|
|
export default {
|
|||
|
|
data() {
|
|||
|
|
return {
|
|||
|
|
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:20,
|
|||
|
|
scrollId:'bottomId'
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
computed:{
|
|||
|
|
// 页面高度
|
|||
|
|
pageHeight(){
|
|||
|
|
const safeAreaHeight = this.scrollView.safeAreaHeight;
|
|||
|
|
if(safeAreaHeight > 0){
|
|||
|
|
return `height: calc(${safeAreaHeight}px - var(--window-top));`
|
|||
|
|
}
|
|||
|
|
return "";
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
created() {
|
|||
|
|
|
|||
|
|
let tempInfo={
|
|||
|
|
icon:this.$route.query.icon,
|
|||
|
|
token:this.$route.query.token,
|
|||
|
|
conversation:this.$route.query.conversation,
|
|||
|
|
userId:this.$route.query.userId,
|
|||
|
|
userAvatar:this.$route.query.userAvatar
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.info = tempInfo
|
|||
|
|
this.userId = this.$route.query.userId
|
|||
|
|
this.userAvatar = this.$route.query.userAvatar
|
|||
|
|
this.getMessage()
|
|||
|
|
|
|||
|
|
|
|||
|
|
console.log(tempInfo,138);
|
|||
|
|
},
|
|||
|
|
onShow() {
|
|||
|
|
|
|||
|
|
|
|||
|
|
},
|
|||
|
|
methods: {
|
|||
|
|
|
|||
|
|
goBack(){
|
|||
|
|
uni.navigateBack()
|
|||
|
|
},
|
|||
|
|
// 下拉刷新
|
|||
|
|
refresherrefresh(e){
|
|||
|
|
console.log(e,151);
|
|||
|
|
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;
|
|||
|
|
console.log(that.info.token,158);
|
|||
|
|
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])
|
|||
|
|
console.log(that.messagesList.length,JSON.stringify(that.messagesList[0]),185);
|
|||
|
|
// 数据挂载后执行,不懂的请自行阅读 Vue.js 文档对 Vue.nextTick 函数说明。
|
|||
|
|
that.$nextTick(()=>{
|
|||
|
|
that.$forceUpdate()
|
|||
|
|
// 设置当前滚动的位置
|
|||
|
|
that.scrollView.intoView = this.scrollId;
|
|||
|
|
})
|
|||
|
|
that.ajax.flag = true;
|
|||
|
|
}
|
|||
|
|
get();
|
|||
|
|
},
|
|||
|
|
// 发送信息
|
|||
|
|
handleSendClick(){
|
|||
|
|
if(!this.content){
|
|||
|
|
uni.showToast({
|
|||
|
|
title:'Please enter valid content',
|
|||
|
|
icon:'none'
|
|||
|
|
})
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
this.sendMessage(this.content);
|
|||
|
|
},
|
|||
|
|
async sendMessage(query) {
|
|||
|
|
console.log('Starting sendMessage...');
|
|||
|
|
|
|||
|
|
if(this.ajax.sendFlag) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: 'Please wait',
|
|||
|
|
icon: 'none'
|
|||
|
|
})
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 添加到消息列表
|
|||
|
|
this.messagesList.unshift(msgItem);
|
|||
|
|
|
|||
|
|
// #ifdef APP-PLUS
|
|||
|
|
try {
|
|||
|
|
const xhr = new plus.net.XMLHttpRequest();
|
|||
|
|
const url = `${config.publicUrl}v1/chat-messages`;
|
|||
|
|
|
|||
|
|
xhr.open('POST', url, true);
|
|||
|
|
|
|||
|
|
// 设置请求头
|
|||
|
|
xhr.setRequestHeader('Authorization', this.info.token);
|
|||
|
|
xhr.setRequestHeader('Content-Type', 'application/json');
|
|||
|
|
xhr.setRequestHeader('Accept', 'text/event-stream');
|
|||
|
|
|
|||
|
|
let tempList = [];
|
|||
|
|
let that = this;
|
|||
|
|
|
|||
|
|
// 监听数据流
|
|||
|
|
xhr.onprogress = function(event) {
|
|||
|
|
console.log('Receiving data...');
|
|||
|
|
const receivedText = event.target.responseText;
|
|||
|
|
console.log('Received text length:', receivedText.length);
|
|||
|
|
|
|||
|
|
// 按数据块分割
|
|||
|
|
const lines = receivedText.split('\ndata: ');
|
|||
|
|
console.log('Processing lines:', lines.length);
|
|||
|
|
|
|||
|
|
// 处理每一行数据
|
|||
|
|
lines.forEach((line, index) => {
|
|||
|
|
try {
|
|||
|
|
if (!line.trim()) return;
|
|||
|
|
|
|||
|
|
// 清理并解析数据
|
|||
|
|
const cleanLine = line.replace('data: ', '');
|
|||
|
|
const data = JSON.parse(cleanLine);
|
|||
|
|
console.log('Parsed data:', data);
|
|||
|
|
|
|||
|
|
// 处理会话ID
|
|||
|
|
if (data.conversation_id) {
|
|||
|
|
uni.setStorageSync(that.info.conversation, data.conversation_id);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理回答内容
|
|||
|
|
if (data.answer) {
|
|||
|
|
const uniqueKey = data.answer + '' + index;
|
|||
|
|
if (!tempList.includes(uniqueKey)) {
|
|||
|
|
console.log('New answer chunk:', data.answer);
|
|||
|
|
tempList.push(uniqueKey);
|
|||
|
|
msgItem.answer += data.answer;
|
|||
|
|
that.$set(that.messagesList, 0, msgItem);
|
|||
|
|
that.scrollView.intoView = 'bottomId';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.log('Parse error for line:', error);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 请求完成
|
|||
|
|
xhr.onload = function() {
|
|||
|
|
console.log('Request completed');
|
|||
|
|
that.ajax.sendFlag = false;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 请求错误
|
|||
|
|
xhr.onerror = function(error) {
|
|||
|
|
console.error('Request failed:', error);
|
|||
|
|
that.ajax.sendFlag = false;
|
|||
|
|
uni.showToast({
|
|||
|
|
title: 'Request failed',
|
|||
|
|
icon: 'none'
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 设置超时
|
|||
|
|
xhr.timeout = 900000; // 15分钟
|
|||
|
|
xhr.ontimeout = function() {
|
|||
|
|
console.error('Request timeout');
|
|||
|
|
that.ajax.sendFlag = false;
|
|||
|
|
uni.showToast({
|
|||
|
|
title: 'Request timeout',
|
|||
|
|
icon: 'none'
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 发送请求
|
|||
|
|
console.log('Sending request...');
|
|||
|
|
xhr.send(JSON.stringify(requestData));
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Error in sendMessage:', error);
|
|||
|
|
this.ajax.sendFlag = false;
|
|||
|
|
uni.showToast({
|
|||
|
|
title: 'Request failed',
|
|||
|
|
icon: 'none'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
// #endif
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</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>
|