diff --git a/pages/chat/index.vue b/pages/chat/index.vue
index 68235d6..1a4ce7c 100644
--- a/pages/chat/index.vue
+++ b/pages/chat/index.vue
@@ -14,7 +14,7 @@
scroll-y
:scroll-into-view="scrollToView"
:scroll-top="scrollTop"
- scroll-with-animation
+ :scroll-with-animation="!isLoading"
:upper-threshold="20"
@scroll="handleScroll"
@scrolltoupper="handleScrollToUpper"
@@ -47,6 +47,19 @@
+
+
+
+
+
+
+
+ 已经到顶了
+
+
import HeaderBar from "@/components/HeaderBar.vue"; // 导入头部组件
-import dayjs from "dayjs"; // 导入 dayjs
+import {
+ formatChatShowTime,
+ scrollToBottomByContentHeight,
+ shouldShowTime,
+} from "@/utils/chat.js";
export default {
name: "ChatDetail",
@@ -171,8 +188,8 @@ export default {
PageIndex: 1,
PageSize: 20,
- isLoadingHistory: false,
- noMoreHistory: false,
+ isLoading: false,
+ noMoreData: false,
};
},
@@ -215,7 +232,7 @@ export default {
// 监听最后一条消息的ID变化,滚动到底部
lastMsgId(val) {
if (!val) return;
- if (this.isLoadingHistory) return;
+ if (this.isLoading) return;
this.$nextTick(() => {
this.scrollToBottom();
});
@@ -283,11 +300,20 @@ export default {
// 加载对话消息
getMsgList() {
- return this.$store.dispatch("fetchChatRecord", {
- dialogueManagementId: this.vuex_msgUser.dialogueManagementId,
- PageIndex: this.PageIndex,
- PageSize: this.PageSize,
- });
+ 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) {
@@ -298,65 +324,54 @@ export default {
// 滚动到顶部,加载下一页历史消息
handleScrollToUpper() {
- if (this.isLoadingHistory || this.noMoreHistory) return;
+ if (this.isLoading || this.noMoreData) return;
- this.isLoadingHistory = true;
+ this.isLoading = true;
const beforeTop = this.currentScrollTop || 0;
const beforeHeight = this.currentScrollHeight || 0;
- this.PageIndex += 1;
+ const nextPageIndex = this.PageIndex + 1;
this.scrollToView = "";
this.$store
.dispatch("fetchChatRecordNextPage", {
dialogueManagementId: this.vuex_msgUser.dialogueManagementId,
- PageIndex: this.PageIndex,
+ PageIndex: nextPageIndex,
PageSize: this.PageSize,
})
.then((list) => {
if (!list || !list.length) {
- this.noMoreHistory = true;
+ this.noMoreData = true;
return;
}
- 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();
- });
- })
- .catch(() => {
- this.PageIndex = Math.max(1, this.PageIndex - 1);
+ 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.isLoadingHistory = false;
- }, 50);
+ this.isLoading = false;
+ }, 100);
});
},
// 滚动到底部
scrollToBottom() {
- // if (this.vuex_msglist.length > 0) {
- // const lastMsg = this.vuex_msglist[this.vuex_msglist.length - 1];
- // this.scrollToView = "msg-" + lastMsg.id;
- // }
-
- // 滚动到底部锚点
- if (this.scrollToView === "bottom-anchor") {
- this.scrollToView = "";
- this.$nextTick(() => {
- this.scrollToView = "bottom-anchor";
- });
- return;
- }
- this.scrollToView = "bottom-anchor";
+ if (this.isLoading) return;
+ scrollToBottomByContentHeight(this, {
+ selector: ".chat-content",
+ extraOffset: 200,
+ });
},
// 格式化时间
@@ -368,56 +383,11 @@ export default {
// 是否显示时间
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;
+ return shouldShowTime(this.vuex_msgList, index);
},
// 格式化显示时间
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")}`;
+ return formatChatShowTime(sendDate);
},
},
};
@@ -448,18 +418,18 @@ export default {
height: 100%;
overflow-y: scroll;
- // .loading-more {
- // text-align: center;
- // margin-bottom: 32rpx;
- // }
+ .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;
- // }
+ .no-more-data {
+ text-align: center;
+ font-size: 24rpx;
+ color: #999;
+ margin-bottom: 32rpx;
+ padding: 10rpx 0;
+ }
.teacher-info-card {
background-color: #ffffff;
diff --git a/pages/home/index/index.vue b/pages/home/index/index.vue
index 783d8c6..0556eab 100644
--- a/pages/home/index/index.vue
+++ b/pages/home/index/index.vue
@@ -909,6 +909,8 @@ export default {
// 刷新当前对话的消息详情
handleGetConversationDetail() {
+ if (!this.currentDMid) return;
+
this.$u.api
.GetConversationDetail({
"Item1.Id": this.currentDMid,
@@ -932,6 +934,8 @@ export default {
// 刷新当前页数据;若当前页为空且页码>1,则自动回退上一页
refreshPageWithFallback() {
+ if (!this.currentDMid) return;
+
const currentIndex = this.pageQuery.PageIndex || 1;
const pageSize = this.pageQuery.PageSize;
@@ -983,6 +987,8 @@ export default {
onScrollToUpper() {
console.log("触发上拉刷新");
+ if (!this.currentDMid) return;
+
// 如果已经没有更多数据或正在切换对话或当前对话为空(新建对话),不再触发上拉刷新
if (this.noMoreData || this.isSwitchingConversation) {
return;
@@ -1035,7 +1041,7 @@ export default {
.finally(() => {
setTimeout(() => {
this.isLoading = false;
- }, 300);
+ }, 100);
});
},
diff --git a/utils/chat.js b/utils/chat.js
new file mode 100644
index 0000000..714a464
--- /dev/null
+++ b/utils/chat.js
@@ -0,0 +1,125 @@
+import dayjs from "dayjs";
+
+/**
+ * 判断聊天列表中某条消息上方是否需要展示时间分隔。
+ *
+ * 典型用法:在 `v-for` 渲染消息时,根据相邻两条消息的时间差决定是否插入时间条。
+ *
+ * 规则:
+ * - 第一条消息始终展示时间
+ * - 其他消息:与上一条消息时间差大于 `gapMs` 时展示
+ *
+ * 边界与兼容:
+ * - `messageList` 不是数组 / 为空:返回 `false`
+ * - `sendDate` 为空或无法解析为有效时间戳:返回 `false`
+ *
+ * @param {Array<{ sendDate?: string }>} messageList - 消息数组(按时间正序)
+ * @param {number} index - 当前消息在数组中的下标
+ * @param {number} gapMs - 时间间隔阈值(毫秒),默认 30 分钟
+ * @returns {boolean} - 是否需要展示时间
+ */
+export function shouldShowTime(messageList, index, gapMs = 30 * 60 * 1000) {
+ if (!Array.isArray(messageList) || !messageList.length) return false;
+ if (index === 0) return true;
+
+ const current = messageList[index];
+ const prev = messageList[index - 1];
+ // `sendDate` 期望为 ISO 字符串或能被 Date 解析的字符串
+ const currentTime = new Date(current && current.sendDate).getTime();
+ const prevTime = new Date(prev && prev.sendDate).getTime();
+
+ if (Number.isNaN(currentTime) || Number.isNaN(prevTime)) return false;
+ return currentTime - prevTime > gapMs;
+}
+
+/**
+ * 将消息发送时间格式化为聊天页可展示的文本。
+ *
+ * 规则(与页面原逻辑一致):
+ * - 60 秒内:`刚刚`
+ * - 1 小时内:`x分钟前`
+ * - 今天:`今天 HH:mm`
+ * - 昨天:`昨天 HH:mm`
+ * - 更早但在今年:`MM月DD日 HH:mm`
+ * - 往年:`YYYY年MM月DD日 HH:mm`
+ *
+ * 说明:
+ * - 使用 `dayjs` 进行日期差计算与格式化,避免手写边界处理。
+ * - 若传入的 `sendDate` 不可解析,dayjs 会返回 Invalid Date,
+ * 此时 diff 结果可能不符合预期;上层建议确保 sendDate 为后端返回的时间字符串。
+ *
+ * @param {string} sendDate - 消息发送时间(后端返回的时间字符串)
+ * @returns {string} - 格式化后的展示文本
+ */
+export function formatChatShowTime(sendDate) {
+ const now = dayjs();
+ const msgTime = dayjs(sendDate);
+
+ if (now.diff(msgTime, "second") < 60) {
+ return "刚刚";
+ }
+ if (now.diff(msgTime, "hour") < 1) {
+ return `${now.diff(msgTime, "minute")}分钟前`;
+ }
+
+ // 使用 startOf('day') 抹平时分秒,方便按“天”比较
+ const today = dayjs().startOf("day");
+ const msgDay = dayjs(sendDate).startOf("day");
+ const dayDiff = today.diff(msgDay, "day");
+
+ if (dayDiff === 0) {
+ return `今天 ${msgTime.format("HH:mm")}`;
+ }
+ if (dayDiff === 1) {
+ return `昨天 ${msgTime.format("HH:mm")}`;
+ }
+
+ // 使用 startOf('year') 抹平年月日,方便按“年”比较
+ const thisYear = dayjs().startOf("year");
+ const msgYear = dayjs(sendDate).startOf("year");
+ const yearDiff = thisYear.diff(msgYear, "year");
+
+ if (yearDiff === 0) {
+ return `${msgTime.format("MM月DD日 HH:mm")}`;
+ }
+
+ return `${msgTime.format("YYYY年MM月DD日 HH:mm")}`;
+}
+
+/**
+ * 通过计算内容容器高度的方式,滚动到底部(与首页实现保持一致)。
+ *
+ * 为什么不用 `scroll-into-view`:
+ * - 私聊场景经常需要“追加/插入消息”并保持当前视口稳定
+ * - `scroll-into-view` 在某些端会触发额外的自动滚动/动画,造成跳动体验
+ *
+ * 工作方式:
+ * - 使用 `uni.createSelectorQuery().in(vm)` 取到组件内 `selector` 对应节点高度
+ * - 以节点高度作为 `scrollTop` 的目标值,并加上 `extraOffset` 作为底部缓冲
+ *
+ * 使用要求:
+ * - `vm` 必须是 Vue 组件实例(需要 `vm.$nextTick` 与 `vm.scrollTop`)
+ * - `selector` 对应的节点需要存在且能计算出 `boundingClientRect`
+ *
+ * @param {any} vm - Vue 组件实例(一般直接传 `this`)
+ * @param {{ selector?: string; extraOffset?: number }} options - 选择器与额外偏移
+ */
+export function scrollToBottomByContentHeight(
+ vm,
+ { selector = ".chat-content", extraOffset = 200 } = {}
+) {
+ if (!vm) return;
+
+ vm.$nextTick(() => {
+ const query = uni.createSelectorQuery().in(vm);
+ query
+ .select(selector)
+ .boundingClientRect((data) => {
+ if (data) {
+ // 这里用高度作为 scrollTop 目标值即可“贴底”,额外加一点缓冲避免极端情况没到最底
+ vm.scrollTop = Number(data.height || 0) + Number(extraOffset || 0);
+ }
+ })
+ .exec();
+ });
+}