600 lines
15 KiB
Vue
600 lines
15 KiB
Vue
<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>
|