YingXingAI/pages/chat/index.vue

600 lines
15 KiB
Vue
Raw Normal View History

<template>
<view class="chat-page">
<!-- 顶部导航 -->
<header-bar
:title="vuex_msgUser.name"
leftIcon="arrow-left"
@leftClick="handleLeftClick"
></header-bar>
<!-- 消息列表 -->
<view class="chat-container">
<scroll-view
class="chat-scroll"
scroll-y
:scroll-into-view="scrollToView"
:scroll-top="scrollTop"
:scroll-with-animation="!isLoading"
:upper-threshold="20"
@scroll="handleScroll"
@scrolltoupper="handleScrollToUpper"
>
<view class="chat-content">
<!-- 教师信息 -->
<div class="teacher-info-card">
<image
class="teacher-avatar"
:src="receiverHeadSculptureUrl"
></image>
<div class="teacher-info">
<div class="teacher-name">{{ vuex_msgUser.name }}</div>
<div class="teacher-school">
<image
class="school-icon"
src="/static/common/images/icon_college.png"
></image>
<text class="school-text">{{ vuex_msgUser.collegeName }}</text>
</div>
<div class="teacher-college">
<image
class="college-icon"
src="/static/common/images/icon_major.png"
></image>
<text class="college-text">{{ vuex_msgUser.collegeName }}</text>
</div>
</div>
</div>
<!-- 上拉刷新loading -->
<view class="loading-more" v-if="isLoading">
<u-loading mode="circle" color="#4370fe"></u-loading>
</view>
<!-- 到顶部提示 -->
<view
class="no-more-data"
v-if="noMoreData && vuex_msgList && vuex_msgList.length"
>
<text>已经到顶了</text>
</view>
<view
v-for="(message, index) in vuex_msgList"
:key="message.id"
:id="'msg-' + message.id"
>
<!-- 时间 -->
<view class="message-time" v-if="isShowTime(index)">
{{ formatShowTime(message.sendDate) }}
</view>
<!-- 0 发送消息 -->
<view
class="message-right"
v-if="message.senderId === vuex_user.Id"
:id="'msg-' + message.id"
>
<view class="message-content">
<text>{{ message.message }}</text>
</view>
<image
class="user-avatar"
:src="headSculptureUrl"
mode="scaleToFill"
/>
</view>
<!-- 1 收到消息 -->
<view
class="message-left"
v-if="message.senderId !== vuex_user.Id"
:id="'msg-' + message.id"
>
<image
class="ai-avatar"
:src="receiverHeadSculptureUrl"
mode="scaleToFill"
/>
<view class="message-content">
<text>{{ message.message }}</text>
</view>
</view>
</view>
<!-- 底部间距 滚动锚点 -->
<view id="bottom-anchor" class="bottom-anchor"></view>
</view>
</scroll-view>
</view>
<!-- 输入栏 -->
<view class="chat-footer">
<view class="input-area">
<input
v-model="messageValue"
type="text"
class="chat-input"
:focus="true"
placeholder="请输入内容"
placeholder-style="color: #adadad;"
@confirm="handleSend"
/>
<view class="send-btn" @click="handleSend">
<image
class="send-icon"
src="/static/common/images/icon_send.png"
mode="scaleToFill"
/>
</view>
</view>
</view>
</view>
</template>
<script>
import HeaderBar from "@/components/HeaderBar.vue"; // 导入头部组件
import {
formatChatShowTime,
scrollToBottomByContentHeight,
shouldShowTime,
} from "@/utils/chat.js";
export default {
name: "ChatDetail",
components: {
HeaderBar, // 注册头部组件
},
data() {
return {
baseUrl: "",
// 头像
myAvatar: "/static/avatar/default-avatar.png",
otherAvatar: "/static/avatar/default-avatar.png",
// 消息列表
messageList: [
{
id: "9cac8661-bf09-4b63-ab15-1a0a36b91110",
message: "你知道今年的录取分数线吗",
sendDate: "2025-08-10T15:11:32.886075",
isSend: true,
isRead: false,
interactMode: 0,
messageType: 0,
},
{
id: "02306fc3-c821-4a23-ad66-0bd788854105",
message: "回答。",
sendDate: "2025-08-10T15:11:36.88644",
isSend: true,
isRead: false,
interactMode: 1,
messageType: 0,
},
],
// 输入框
messageValue: "",
// 滚动位置
scrollToView: "",
scrollTop: 0,
currentScrollTop: 0,
currentScrollHeight: 0,
PageIndex: 1,
PageSize: 20,
isLoading: false,
noMoreData: false,
};
},
onLoad(options) {
console.log(this.vuex_msgList);
console.log(this.vuex_msgUser, "this.vuex_msgUser");
this.baseUrl = this.$u.http.config.baseUrl;
// 加载历史消息
this.getMsgList();
},
computed: {
// 最后一条消息的ID
lastMsgId() {
const list = this.vuex_msgList || [];
if (!list.length) return "";
return list[list.length - 1]?.id || "";
},
receiverHeadSculptureUrl() {
if (this.vuex_msgUser.headSculptureUrl) {
return this.baseUrl + "/" + this.vuex_msgUser.headSculptureUrl;
}
return "/static/common/images/avatar_default2.png";
},
headSculptureUrl() {
if (this.vuex_user.HeadSculptureUrl) {
return this.baseUrl + "/" + this.vuex_user.HeadSculptureUrl;
}
return "/static/common/images/avatar_default2.png";
},
},
watch: {
// 监听最后一条消息的ID变化滚动到底部
lastMsgId(val) {
if (!val) return;
if (this.isLoading) return;
this.$nextTick(() => {
this.scrollToBottom();
});
},
},
methods: {
// 返回
handleLeftClick() {
uni.navigateBack();
},
// 点击发送
handleSend() {
if (!this.messageValue) {
return;
}
// 构建消息对象
const message = {
dialogueManagementId: this.vuex_msgUser.dialogueManagementId,
receiverId: this.vuex_msgUser.id,
message: this.messageValue,
messageType: 0,
filePath: "",
ip: "",
};
this.sendMsgFn(message);
},
// 发送消息
sendMsgFn(message) {
this.$u.api
.SendMessage_PrivateApi(message)
.then((res) => {
console.log(res, "发送消息成功");
if (res.succeed) {
// 添加到消息列表
const msgUserData = {
id: Math.random().toString(36).substring(2),
dialogueManagementId: this.vuex_msgUser.dialogueManagementId,
senderId: this.vuex_user.Id,
receiverId: this.vuex_msgUser.receiverId,
sendDate: new Date().toISOString(),
message: this.messageValue,
messageType: 0,
filePath: "",
// interactMode: 0,
};
this.$store.commit("push_Msg", msgUserData);
// 清空输入框
this.messageValue = "";
// // 滚动到底部
// this.$nextTick(() => {
// this.scrollToBottom();
// });
}
})
.catch((error) => {
return msg.warning("发送失败");
});
},
// 加载对话消息
getMsgList() {
return this.$store
.dispatch("fetchChatRecord", {
dialogueManagementId: this.vuex_msgUser.dialogueManagementId,
PageIndex: this.PageIndex,
PageSize: this.PageSize,
})
.then((list) => {
const len = Array.isArray(list) ? list.length : 0;
// 第一页无数据,设置为没有更多数据
if (len === 0 && this.PageIndex === 1) {
this.noMoreData = true;
}
return list;
});
},
handleScroll(e) {
const detail = (e && e.detail) || {};
this.currentScrollTop = Number(detail.scrollTop) || 0;
this.currentScrollHeight = Number(detail.scrollHeight) || 0;
},
// 滚动到顶部,加载下一页历史消息
handleScrollToUpper() {
if (this.isLoading || this.noMoreData) return;
this.isLoading = true;
const beforeTop = this.currentScrollTop || 0;
const beforeHeight = this.currentScrollHeight || 0;
const nextPageIndex = this.PageIndex + 1;
this.scrollToView = "";
this.$store
.dispatch("fetchChatRecordNextPage", {
dialogueManagementId: this.vuex_msgUser.dialogueManagementId,
PageIndex: nextPageIndex,
PageSize: this.PageSize,
})
.then((list) => {
if (!list || !list.length) {
this.noMoreData = true;
return;
}
this.PageIndex = nextPageIndex;
// this.$nextTick(() => {
// const query = uni.createSelectorQuery().in(this);
// query
// .select(".chat-content")
// .boundingClientRect((rect) => {
// const afterHeight = Number(rect && rect.height) || 0;
// const delta = afterHeight - beforeHeight;
// if (delta > 0) {
// this.scrollTop = beforeTop + delta;
// }
// })
// .exec();
// });
})
.finally(() => {
setTimeout(() => {
this.isLoading = false;
}, 100);
});
},
// 滚动到底部
scrollToBottom() {
if (this.isLoading) return;
scrollToBottomByContentHeight(this, {
selector: ".chat-content",
extraOffset: 200,
});
},
// 格式化时间
formatTime(date) {
const hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, "0");
return `${hours}:${minutes}`;
},
// 是否显示时间
isShowTime(index) {
return shouldShowTime(this.vuex_msgList, index);
},
// 格式化显示时间
formatShowTime(sendDate) {
return formatChatShowTime(sendDate);
},
},
};
</script>
<style lang="scss" scoped>
.chat-page {
height: 100vh;
padding-top: 88rpx;
background-image: url("@/static/common/images/images_bg.png");
width: 100%;
background-repeat: no-repeat;
background-size: 100% 100%;
background-position: 0 88rpx;
background-attachment: fixed;
.chat-container {
display: flex;
flex-direction: column;
padding: 0 30rpx;
box-sizing: border-box;
height: calc(100vh - 88rpx - 146rpx);
position: relative;
overflow: hidden;
.chat-scroll {
flex: 1;
height: 100%;
overflow-y: scroll;
.loading-more {
text-align: center;
margin-bottom: 32rpx;
}
.no-more-data {
text-align: center;
font-size: 24rpx;
color: #999;
margin-bottom: 32rpx;
padding: 10rpx 0;
}
.teacher-info-card {
background-color: #ffffff;
padding: 32rpx;
border-radius: 16rpx;
margin: 30rpx 0;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
margin-bottom: 36rpx;
.teacher-avatar {
width: 120rpx;
height: 120rpx;
border-radius: 10rpx;
margin-right: 36rpx;
object-fit: cover;
}
.teacher-info {
display: flex;
flex-direction: column;
justify-content: space-evenly;
gap: 16rpx;
flex: 1;
.teacher-name {
font-family: PingFang SC;
font-weight: bold;
font-size: 36rpx;
color: #333333;
}
.teacher-school,
.teacher-college {
display: flex;
align-items: center;
.school-icon,
.college-icon {
margin-right: 16rpx;
width: 24rpx;
height: 24rpx;
display: inline-block;
text-align: center;
}
.school-text,
.college-text {
font-size: 24rpx;
color: #666666;
}
}
}
}
.message-time {
text-align: center;
font-size: 24rpx;
color: #999999;
padding: 20rpx;
}
.message-left,
.message-right {
display: flex;
margin-bottom: 40rpx;
align-items: flex-start;
}
.message-left {
justify-content: flex-start;
flex-wrap: wrap; /* 允许反馈区换行到消息下方 */
.ai-avatar {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
margin-right: 16rpx;
background-color: #f0f0f0;
flex-shrink: 0;
}
.message-content {
background-color: #ffffff;
max-width: 70%;
padding: 20rpx 24rpx;
border-radius: 0 16rpx 16rpx 16rpx;
font-size: 28rpx;
line-height: 1.5;
}
}
.message-right {
justify-content: flex-end;
.user-avatar {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
margin-left: 16rpx;
background-color: #f0f0f0;
flex-shrink: 0;
}
.message-content {
background-color: #4370fe;
color: #ffffff;
max-width: 70%;
padding: 20rpx 24rpx;
border-radius: 16rpx 0 16rpx 16rpx;
font-size: 28rpx;
line-height: 1.5;
text-align: left;
}
}
.bottom-anchor {
height: 48rpx;
width: 100%;
}
}
}
.chat-footer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 32rpx;
box-sizing: border-box;
background-color: #ffffff;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
.input-area {
display: flex;
align-items: center;
background-color: #f2f4f9;
border-radius: 20rpx;
padding: 16rpx 24rpx;
box-sizing: border-box;
.chat-input {
flex: 1;
height: 40rpx;
font-size: 28rpx;
color: #333;
background-color: transparent;
}
.send-btn {
width: 50rpx;
height: 50rpx;
display: flex;
align-items: center;
justify-content: center;
.send-icon {
width: 40rpx;
height: 46rpx;
}
}
}
}
}
</style>