YingXingAI/pages/chat/index.vue

648 lines
17 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 dayjs from "dayjs"; // 导入 dayjs
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;
this.$nextTick(() => {
const query = uni.createSelectorQuery().in(this);
query
.select(".chat-content")
.boundingClientRect((data) => {
if (data) {
this.scrollTop = Number(data.height || 0) + 200;
}
})
.exec();
});
},
// 格式化时间
formatTime(date) {
const hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, "0");
return `${hours}:${minutes}`;
},
// 是否显示时间
isShowTime(index) {
if (index == 0) {
return true;
}
let isTime = new Date(this.vuex_msgList[index].sendDate).getTime(); //当前时间
const time = new Date(this.vuex_msgList[index - 1].sendDate).getTime(); //上条消息的时间
// 30 分钟内不显示时间提示
return isTime - time > 30 * 60 * 1000;
},
// 格式化显示时间
formatShowTime(sendDate) {
// 《消息时间为今天发送》 1分钟内显示 ‘刚刚’ 超过一分钟 且小于60分钟 显示 x分钟前 大于等于60分钟显示 ‘今天 hh:mm
// 《消息时间为昨天发送》显示 ‘昨天 hh:mm
// 《消息时间为昨天以前,并且是今年发起》 显示 MM月DD日 hh:mm
// 《消息时间为往年发送》 显示 YYYY年MM月DD日
var isTime = dayjs(); //当前时间
var msgTime = dayjs(sendDate); //消息发送时间
if (isTime.diff(msgTime, "second") < 60) {
return "刚刚";
}
if (isTime.diff(msgTime, "hour") < 1) {
return `${isTime.diff(msgTime, "minute")}分钟前`;
}
// 使用 startOf('day') 获取当天 00:00:00
var today = dayjs().startOf("day");
var msgDay = dayjs(sendDate).startOf("day");
var dayDiff = today.diff(msgDay, "day");
if (dayDiff === 0) {
return `今天 ${msgTime.format("HH:mm")}`;
}
if (dayDiff === 1) {
return `昨天 ${msgTime.format("HH:mm")}`;
}
// 使用 startOf('year') 获取当年第一天
var thisYear = dayjs().startOf("year");
var msgYear = dayjs(sendDate).startOf("year");
var yearDiff = thisYear.diff(msgYear, "year");
if (yearDiff === 0) {
// 今年
return `${msgTime.format("MM月DD日 HH:mm")}`; // 原代码这里有一处是 HH:mm:ss但注释和逻辑看来 HH:mm 更一致,根据需求调整
}
// 往年
return `${msgTime.format("YYYY年MM月DD日 HH:mm")}`;
},
},
};
</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>