refactor(chat): 提取聊天工具函数到独立模块
将时间格式化、滚动到底部和时间显示判断逻辑提取到 utils/chat.js 提升代码复用性和可维护性,保持与首页实现一致
This commit is contained in:
parent
31af22face
commit
9e10f6788d
|
|
@ -135,7 +135,11 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import HeaderBar from "@/components/HeaderBar.vue"; // 导入头部组件
|
import HeaderBar from "@/components/HeaderBar.vue"; // 导入头部组件
|
||||||
import dayjs from "dayjs"; // 导入 dayjs
|
import {
|
||||||
|
formatChatShowTime,
|
||||||
|
scrollToBottomByContentHeight,
|
||||||
|
shouldShowTime,
|
||||||
|
} from "@/utils/chat.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "ChatDetail",
|
name: "ChatDetail",
|
||||||
|
|
@ -304,6 +308,7 @@ export default {
|
||||||
})
|
})
|
||||||
.then((list) => {
|
.then((list) => {
|
||||||
const len = Array.isArray(list) ? list.length : 0;
|
const len = Array.isArray(list) ? list.length : 0;
|
||||||
|
// 第一页无数据,设置为没有更多数据
|
||||||
if (len === 0 && this.PageIndex === 1) {
|
if (len === 0 && this.PageIndex === 1) {
|
||||||
this.noMoreData = true;
|
this.noMoreData = true;
|
||||||
}
|
}
|
||||||
|
|
@ -363,17 +368,9 @@ export default {
|
||||||
// 滚动到底部
|
// 滚动到底部
|
||||||
scrollToBottom() {
|
scrollToBottom() {
|
||||||
if (this.isLoading) return;
|
if (this.isLoading) return;
|
||||||
|
scrollToBottomByContentHeight(this, {
|
||||||
this.$nextTick(() => {
|
selector: ".chat-content",
|
||||||
const query = uni.createSelectorQuery().in(this);
|
extraOffset: 200,
|
||||||
query
|
|
||||||
.select(".chat-content")
|
|
||||||
.boundingClientRect((data) => {
|
|
||||||
if (data) {
|
|
||||||
this.scrollTop = Number(data.height || 0) + 200;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.exec();
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -386,56 +383,11 @@ export default {
|
||||||
|
|
||||||
// 是否显示时间
|
// 是否显示时间
|
||||||
isShowTime(index) {
|
isShowTime(index) {
|
||||||
if (index == 0) {
|
return shouldShowTime(this.vuex_msgList, index);
|
||||||
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) {
|
formatShowTime(sendDate) {
|
||||||
// 《消息时间为今天发送》 1分钟内显示 ‘刚刚’ 超过一分钟 且小于60分钟 显示 ‘x分钟前’ 大于等于60分钟显示 ‘今天 hh:mm’
|
return formatChatShowTime(sendDate);
|
||||||
// 《消息时间为昨天发送》显示 ‘昨天 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")}`;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue