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(); }); } /** * 处理聊天消息内容:若内容为 JSON 字符串且符合预期结构,则提取并拼接为可读文本。 * * 说明: * - 首页对话存在部分消息内容为 JSON 字符串的情况(如包含 `answerTypeLabel/imageUrl/nearbyPaths`) * - 这里做“尽量解析、解析失败回退原文”的处理,避免误伤普通文本消息 * * @param {string} message - 原始消息文本 * @returns {string} - 处理后的文本 */ export function processChatMessageContent(message) { try { const parsed = JSON.parse(message); if (typeof parsed === "object" && parsed !== null) { let content = ""; const { answerTypeLabel, imageUrl, nearbyPaths } = parsed; if (answerTypeLabel) content += `${answerTypeLabel}\n`; if (imageUrl) content += `${imageUrl}\n`; if (nearbyPaths) content += `${nearbyPaths}\n`; return content ? content.trim() : message; } } catch (e) { return message; } return message; } /** * 聊天消息排序:按 `sendDate` 升序;时间相同时,用户消息(`interactMode=0`)排在前面。 * 同时会对每条消息的 `message` 字段进行 `processChatMessageContent` 处理。 * * @param {Array} list - 会话详情接口返回的消息列表 * @returns {Array} - 处理并排序后的新数组 */ export function sortChatMessages(list = []) { const processedList = (list || []).map((item) => ({ ...item, message: processChatMessageContent(item && item.message), })); return processedList.sort((a, b) => { const timeA = new Date(a && a.sendDate).getTime(); const timeB = new Date(b && b.sendDate).getTime(); if (timeA === timeB) return (a && a.interactMode) - (b && b.interactMode); return timeA - timeB; }); } /** * 将 Date 格式化为 `HH:mm`。 * * @param {Date} date - 时间对象 * @returns {string} - 格式化字符串 */ export function formatHHmm(date) { if (!(date instanceof Date) || Number.isNaN(date.getTime())) return ""; const hours = date.getHours().toString().padStart(2, "0"); const minutes = date.getMinutes().toString().padStart(2, "0"); return `${hours}:${minutes}`; }