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>
|