YingXingAI/utils/chat.js

126 lines
4.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();
});
}