flinfo/dc-App/pages/Chat/chatApp.vue

550 lines
12 KiB
Vue
Raw Normal View History

2025-03-01 10:26:49 +08:00
<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标签随意不影响页面展示即可
propprop随便取名options是变量
:change:[name]name和prop保持一致就行
renderScript和新加的script标签的module名字一样
onChangerenderScript标签中的方法
思路是通过修改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>