diff --git a/common/http.api.js b/common/http.api.js
index f54f97d..30a4823 100644
--- a/common/http.api.js
+++ b/common/http.api.js
@@ -236,18 +236,21 @@ const install = (Vue, vm) => {
// 获取消息接收方用户信息
let GetReceiverUserInfoApi = (params = {}) =>
vm.$u.get("api/Dialogue/GetReceiverUserInfo", params);
- // 置顶一个会话
- let OverheadOneDialogueApi = (params = {}) =>
- vm.$u.post("api/Dialogue/OverheadOneDialogue", params);
- // 将会话消息标记为已读
- let ReadMessageApi = (params = {}) =>
- vm.$u.post("api/Dialogue/ReadMessage", params);
+ // 置顶一个会话
+ let OverheadOneDialogueApi = (params = {}) =>
+ vm.$u.post("api/Dialogue/OverheadOneDialogue", params);
+ // 将会话消息标记为已读
+ let ReadMessageApi = (params = {}) =>
+ vm.$u.post("api/Dialogue/ReadMessage", params);
// 删除会话
let DeleteDialogueApi = (params = {}) =>
vm.$u.post("api/Dialogue/DeleteDialogue", params);
+ // 转人工服务
+ let TransferToALiveAgentApi = (params = {}) =>
+ vm.$u.post("api/Dialogue/TransferToALiveAgent", params);
// 将各个定义的接口名称,统一放进对象挂载到vm.$u.api(因为vm就是this,也即this.$u.api)下
- vm.$u.api = {
+ vm.$u.api = {
UploadSingleImage,
getTeacherInfo,
getData,
@@ -306,16 +309,17 @@ const install = (Vue, vm) => {
UpdateUserApi,
GetTeacherListApi,
GetDialogueListApi,
- GetDialogueList_UserApi,
- AddDialogueApi,
- SendMessage_PrivateApi,
- GetChatHistoryDataApi,
- GetReceiverUserInfoApi,
- OverheadOneDialogueApi,
- ReadMessageApi,
- DeleteDialogueApi,
- };
-};
+ GetDialogueList_UserApi,
+ AddDialogueApi,
+ SendMessage_PrivateApi,
+ GetChatHistoryDataApi,
+ GetReceiverUserInfoApi,
+ OverheadOneDialogueApi,
+ ReadMessageApi,
+ DeleteDialogueApi,
+ TransferToALiveAgentApi,
+ };
+};
export default {
install,
diff --git a/pages/chat/index.vue b/pages/chat/index.vue
index 1a4ce7c..f2ed716 100644
--- a/pages/chat/index.vue
+++ b/pages/chat/index.vue
@@ -374,13 +374,6 @@ export default {
});
},
- // 格式化时间
- 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);
diff --git a/pages/home/history/index.vue b/pages/home/history/index.vue
index bf888ea..457e1b1 100644
--- a/pages/home/history/index.vue
+++ b/pages/home/history/index.vue
@@ -52,7 +52,7 @@
-
+
-
+
✓
@@ -164,6 +167,7 @@ export default {
chatHistoryAI: [], // ai咨询历史记录
chatHistoryTeacher: [], // 人工咨询历史记录
openedItems: {}, // 记录每个项目的滑动状态
+ openedSwipeId: "", // 当前展开的侧滑项 id
};
},
computed: {
@@ -207,16 +211,9 @@ export default {
// 重置滑动状态
closeAllSwipeActions() {
- const swipeActionRefs = this.$refs.swipeActionRef || [];
- const refs = Array.isArray(swipeActionRefs)
- ? swipeActionRefs
- : [swipeActionRefs];
-
- refs.forEach((ref) => {
- if (ref && typeof ref.closeAll === "function") {
- ref.closeAll();
- }
- });
+ const ids = this.currentHistoryItemIds || [];
+ ids.forEach((id) => this.closeSwipeActionById(id));
+ this.openedSwipeId = "";
},
// 清空 openedItems 并调用 closeAllSwipeActions()
@@ -225,6 +222,16 @@ export default {
this.closeAllSwipeActions();
},
+ closeSwipeActionById(itemId) {
+ if (!itemId) return;
+ const swipeRef = this.$refs[`swipe-${itemId}`];
+ const ref = Array.isArray(swipeRef) ? swipeRef[0] : swipeRef;
+ if (ref && typeof ref.closeAll === "function") {
+ ref.closeAll();
+ }
+ this.$set(this.openedItems, itemId, "none");
+ },
+
// 获取ai历史记录
async getChatHistoryList() {
this.$u.api.GetConversationPage().then((res) => {
@@ -465,6 +472,16 @@ export default {
// 处理滑动操作变化
handleSwipeChange(e, itemId) {
this.$set(this.openedItems, itemId, e);
+ if (e === "right" || e === "left") {
+ if (this.openedSwipeId && this.openedSwipeId !== itemId) {
+ this.closeSwipeActionById(this.openedSwipeId);
+ }
+ this.openedSwipeId = itemId;
+ return;
+ }
+ if (e === "none" && this.openedSwipeId === itemId) {
+ this.openedSwipeId = "";
+ }
},
// 处理单个删除操作
diff --git a/pages/home/index/index.vue b/pages/home/index/index.vue
index 0556eab..44b0b90 100644
--- a/pages/home/index/index.vue
+++ b/pages/home/index/index.vue
@@ -128,7 +128,7 @@
@@ -136,11 +136,77 @@
{{ message.displayTime }}
+
+
+
+
+ 请选择您要咨询的类型,以便系统为您转接对应的客服
+
+
+
+
+ 招生咨询
+
+
+
+ 迎新咨询
+
+
+
+
+ {{ getTransferCardTip(message) }}
+
+
+
@@ -157,7 +223,9 @@
{
- // setTimeout(() => {
- const query = uni.createSelectorQuery().in(this);
- query
- .select(".chat-content")
- .boundingClientRect((data) => {
- if (data) {
- console.log("chat-content高度:", data.height);
-
- // 同时设置scrollTop和scrollToView
- this.scrollTop = data.height + 200;
- // if (this.messageGroups.length > 0) {
- // const lastMessage =
- // this.messageGroups[this.messageGroups.length - 1];
- // this.scrollToView = "msg-" + lastMessage.id;
- // console.log("设置scrollToView:", this.scrollToView);
- // }
- }
- })
- .exec();
- // }, 300);
+ scrollToBottomByContentHeight(this, {
+ selector: ".chat-content",
+ extraOffset: 200,
});
},
handleFeatureClick(item) {
@@ -580,6 +615,11 @@ export default {
return;
}
+ if (item.title === "转人工") {
+ this.handleTransferEntryClick();
+ return;
+ }
+
if (item.title === "电话咨询") {
this.advicePhoneShow = true;
return;
@@ -598,6 +638,153 @@ export default {
}
},
+ createLocalUserTextMessage(messageText) {
+ return {
+ id: Math.random().toString(36).substring(2, 15),
+ message: messageText,
+ sendDate: "",
+ isSend: true,
+ isRead: false,
+ interactMode: 0,
+ messageType: 0,
+ timeLabel: 0,
+ displayTime: "",
+ };
+ },
+
+ createTransferCardMessage() {
+ return {
+ id: "transfer_" + Math.random().toString(36).substring(2, 15),
+ message: "",
+ sendDate: "",
+ isSend: true,
+ isRead: false,
+ interactMode: 1,
+ messageType: 0,
+ timeLabel: 0,
+ displayTime: "",
+ customType: "transferCard",
+ transferStatus: "idle",
+ selectedConsultationType: null,
+ transferTipText: "",
+ };
+ },
+
+ handleTransferEntryClick() {
+ if (!this.isChat) {
+ this.resetChatState();
+ }
+
+ this.isLoadingMore = false;
+ this.isTransferSubmitting = false;
+
+ const nextList = this.aiMessageGroups.filter(
+ (m) => !(m && m.customType === "transferCard")
+ );
+
+ nextList.push(this.createLocalUserTextMessage("转人工"));
+ const card = this.createTransferCardMessage();
+ this.activeTransferCardId = card.id;
+ nextList.push(card);
+ this.$store.commit("push_AiMsgList", nextList);
+ },
+
+ getTransferCardTip(message) {
+ if (!message) return "";
+ if (this.isTransferSubmitting) return "正在为您转接人工客服,请稍等...";
+ if (message.transferTipText) return message.transferTipText;
+ return "";
+ },
+
+ handleTransferTypeSelect(consultationType) {
+ const cardId = this.activeTransferCardId;
+ const findCardIndex = (list) => {
+ if (cardId) {
+ const byId = list.findIndex(
+ (m) => m && m.id === cardId
+ );
+ if (byId >= 0) return byId;
+ }
+ return list.findIndex(
+ (m) => m && m.customType === "transferCard"
+ );
+ };
+ const list = this.aiMessageGroups;
+ const idx = findCardIndex(list);
+ if (idx < 0) return;
+
+ const current = list[idx] || {};
+ if (this.isTransferSubmitting || current.transferStatus === "done") return;
+
+ const next = {
+ ...current,
+ transferStatus: "idle",
+ selectedConsultationType: consultationType,
+ transferTipText: "",
+ };
+ const nextList = list.slice();
+ nextList.splice(idx, 1, next);
+ this.$store.commit("push_AiMsgList", nextList);
+ this.isTransferSubmitting = true;
+
+ const params = {
+ consultationType,
+ message: "转人工",
+ ip: "",
+ };
+
+ if (this.currentDMid) {
+ params.dialogueManagementId = this.currentDMid;
+ }
+
+ this.$u.api
+ .TransferToALiveAgentApi(params)
+ .then((res) => {
+ this.isTransferSubmitting = false;
+ const latestList = this.aiMessageGroups;
+ const nextIdx = findCardIndex(latestList);
+ if (nextIdx < 0) return;
+ const succeed = res && res.succeed !== false;
+ const serverTip =
+ (res && res.data && res.data.message) ||
+ (res && res.error) ||
+ "";
+ if (!succeed) {
+ const updated = {
+ ...latestList[nextIdx],
+ transferStatus: "idle",
+ transferTipText: serverTip,
+ };
+ const updatedList = latestList.slice();
+ updatedList.splice(nextIdx, 1, updated);
+ this.$store.commit("push_AiMsgList", updatedList);
+ return;
+ }
+ const updated = {
+ ...latestList[nextIdx],
+ transferStatus: "done",
+ transferTipText: serverTip,
+ };
+ const updatedList = latestList.slice();
+ updatedList.splice(nextIdx, 1, updated);
+ this.$store.commit("push_AiMsgList", updatedList);
+ })
+ .catch(() => {
+ this.isTransferSubmitting = false;
+ const latestList = this.aiMessageGroups;
+ const nextIdx = findCardIndex(latestList);
+ if (nextIdx < 0) return;
+ const updated = {
+ ...latestList[nextIdx],
+ transferStatus: "idle",
+ transferTipText: "转人工失败,请稍后重试",
+ };
+ const updatedList = latestList.slice();
+ updatedList.splice(nextIdx, 1, updated);
+ this.$store.commit("push_AiMsgList", updatedList);
+ });
+ },
+
// 修改发送消息的方法
sendMessageFn() {
if (!this.messageValue) {
@@ -624,7 +811,7 @@ export default {
};
// 添加到消息列表
- this.messageGroups.push(userMessage);
+ this.$store.commit("push_AiMsg", userMessage);
this.messageValue = "";
// 立即添加一个AI回复的加载状态消息
@@ -642,7 +829,7 @@ export default {
};
// 添加加载状态消息到列表
- this.messageGroups.push(loadingMessage);
+ this.$store.commit("push_AiMsg", loadingMessage);
const params = {
query: sendMessage,
@@ -663,7 +850,7 @@ export default {
this.currentDMid = data.dmid;
// 从消息列表中移除加载状态消息
- this.messageGroups = this.messageGroups.filter(
+ const baseList = this.aiMessageGroups.filter(
(msg) => !msg.isLoading
);
@@ -683,13 +870,13 @@ export default {
};
// 添加到消息列表
- this.messageGroups.push(aiMessage);
+ this.$store.commit("push_AiMsgList", baseList.concat([aiMessage]));
})
.catch((error) => {
console.error("API请求失败:", error);
// 从消息列表中移除加载状态消息
- this.messageGroups = this.messageGroups.filter(
+ const baseList = this.aiMessageGroups.filter(
(msg) => !msg.isLoading
);
@@ -706,7 +893,10 @@ export default {
displayTime: "",
};
- this.messageGroups.push(errorMessage);
+ this.$store.commit(
+ "push_AiMsgList",
+ baseList.concat([errorMessage])
+ );
});
},
@@ -737,7 +927,7 @@ export default {
};
// 添加到消息列表
- this.messageGroups.push(userMessage);
+ this.$store.commit("push_AiMsg", userMessage);
// 立即添加一个AI回复的加载状态消息
const loadingMessage = {
@@ -754,7 +944,7 @@ export default {
};
// 添加加载状态消息到列表
- this.messageGroups.push(loadingMessage);
+ this.$store.commit("push_AiMsg", loadingMessage);
const params = {
id: item.id,
@@ -772,7 +962,7 @@ export default {
const data = res.data.entityInfo;
// 从消息列表中移除加载状态消息
- this.messageGroups = this.messageGroups.filter(
+ const baseList = this.aiMessageGroups.filter(
(msg) => !msg.isLoading
);
@@ -792,11 +982,15 @@ export default {
};
// 添加到消息列表
- this.messageGroups.push(aiMessage);
+ this.$store.commit(
+ "push_AiMsgList",
+ baseList.concat([aiMessage])
+ );
} else {
// 从消息列表中移除加载状态消息
- this.messageGroups = this.messageGroups.filter(
- (msg) => !msg.isLoading
+ this.$store.commit(
+ "push_AiMsgList",
+ this.aiMessageGroups.filter((msg) => !msg.isLoading)
);
}
})
@@ -804,7 +998,7 @@ export default {
console.error("API请求失败:", error);
// 从消息列表中移除加载状态消息
- this.messageGroups = this.messageGroups.filter(
+ const baseList = this.aiMessageGroups.filter(
(msg) => !msg.isLoading
);
@@ -821,17 +1015,13 @@ export default {
displayTime: "",
};
- this.messageGroups.push(errorMessage);
+ this.$store.commit(
+ "push_AiMsgList",
+ baseList.concat([errorMessage])
+ );
});
},
- // 格式化时间的辅助方法
- formatTime(date) {
- const hours = date.getHours().toString().padStart(2, "0");
- const minutes = date.getMinutes().toString().padStart(2, "0");
- return `${hours}:${minutes}`;
- },
-
// 处理选中的对话
handleSelectConversation(data) {
// 关闭弹窗
@@ -848,7 +1038,7 @@ export default {
this.isSwitchingConversation = true;
// 重置消息列表和分页相关状态
- this.messageGroups = [];
+ this.$store.commit("push_AiMsgList", []);
this.pageQuery.PageIndex = 1;
this.isLoadingMore = false;
this.noMoreData = false;
@@ -859,52 +1049,12 @@ export default {
// 处理消息内容,如果是JSON格式则解析并格式化
processMessageContent(message) {
- try {
- // 尝试解析 JSON
- 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) {
- // 解析失败,说明不是JSON格式,直接返回原消息
- return message;
- }
-
- return message;
+ return processChatMessageContent(message);
},
// 公共排序:按时间升序;时间相同,用户消息(interactMode=0)在前
sortMessages(list = []) {
- const processedList = (list || []).map((item) => {
- // 对每条消息进行处理
- return {
- ...item,
- message: this.processMessageContent(item.message),
- };
- });
-
- // console.log("processedList", processedList);
-
- return processedList.sort((a, b) => {
- const timeA = new Date(a.sendDate).getTime();
- const timeB = new Date(b.sendDate).getTime();
- if (timeA === timeB) return a.interactMode - b.interactMode;
- return timeA - timeB;
- });
+ return sortChatMessages(list);
},
// 刷新当前对话的消息详情
@@ -922,7 +1072,10 @@ export default {
const currentList = res.item2 || [];
// 将消息按sendDate升序排列,时间相同时用户消息(interactMode=0)排在前面
- this.messageGroups = this.sortMessages(currentList);
+ this.$store.commit(
+ "push_AiMsgList",
+ this.sortMessages(currentList)
+ );
})
.finally(() => {
// 延迟重置切换对话标志位,确保滚动事件处理完成
@@ -957,11 +1110,17 @@ export default {
})
.then((res3) => {
const prevList = res3?.item2 || [];
- this.messageGroups = this.sortMessages(prevList);
+ this.$store.commit(
+ "push_AiMsgList",
+ this.sortMessages(prevList)
+ );
this.pageQuery.PageIndex = prevIndex;
});
} else {
- this.messageGroups = this.sortMessages(currentList);
+ this.$store.commit(
+ "push_AiMsgList",
+ this.sortMessages(currentList)
+ );
}
});
},
@@ -1020,23 +1179,12 @@ export default {
}
// 将消息按sendDate升序排列,时间相同时用户消息(interactMode=0)排在前面
- const nextPageMessageGroups = res.item2.sort((a, b) => {
- const timeA = new Date(a.sendDate).getTime();
- const timeB = new Date(b.sendDate).getTime();
+ const nextPageMessageGroups = sortChatMessages(res.item2 || []);
- // 如果时间相同,按interactMode排序(0排在前,1排在后)
- if (timeA === timeB) {
- return a.interactMode - b.interactMode;
- }
-
- // 否则按时间升序排列
- return timeA - timeB;
- });
-
- this.messageGroups = [
- ...nextPageMessageGroups,
- ...this.messageGroups,
- ];
+ this.$store.commit(
+ "prepend_AiMsgList",
+ nextPageMessageGroups
+ );
})
.finally(() => {
setTimeout(() => {
@@ -1521,6 +1669,79 @@ export default {
text-align: left;
}
}
+
+ .message-transfer {
+ width: 100%;
+ margin-bottom: 40rpx;
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .transfer-card {
+ width: 100%;
+ background-color: #ffffff;
+ border-radius: 16rpx;
+ padding: 40rpx 36rpx;
+ box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.06);
+ }
+
+ .transfer-card-title {
+ padding: 0 16rpx;
+ font-family: PingFang SC;
+ font-size: 32rpx;
+ color: #333333;
+ line-height: 1.6;
+ font-weight: 500;
+ }
+
+ .transfer-card-options {
+ margin-top: 32rpx;
+ // padding: 24rpx;
+ border-radius: 16rpx;
+ // background-color: #f6f8ff;
+ display: flex;
+ gap: 36rpx;
+ }
+
+ .transfer-option {
+ flex: 1;
+ height: 112rpx;
+ border-radius: 16rpx;
+ background-color: #f6f8fa;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 16rpx;
+ }
+
+ .transfer-option.selected {
+ background-color: #eef0fe;
+ border-color: #dbe3ff;
+ }
+
+ .transfer-option.disabled {
+ opacity: 0.7;
+ }
+
+ .transfer-option-text {
+ font-family: PingFang SC;
+ font-size: 30rpx;
+ color: #333333;
+ font-weight: 500;
+ }
+
+ .transfer-option.selected .transfer-option-text {
+ color: #4a6cf7;
+ }
+
+ .transfer-card-status {
+ margin-top: 24rpx;
+ text-align: center;
+ font-size: 28rpx;
+ color: #999;
+ }
}
.chat-footer {
diff --git a/store/index.js b/store/index.js
index 60ed024..03972e6 100644
--- a/store/index.js
+++ b/store/index.js
@@ -43,6 +43,8 @@ const store = new Vuex.Store({
vuex_token: lifeData.vuex_token ? lifeData.vuex_token : "",
// 如果vuex_version无需保存到本地永久存储,无需lifeData.vuex_version方式
vuex_version: "1.0.0",
+ // 智能客服消息组列表
+ vuex_aiMessageGroups: [],
// 当前聊天窗口消息列表(数组
vuex_msgList: [],
// 最近联系人/会话列表
@@ -179,6 +181,42 @@ const store = new Vuex.Store({
if (!msg) return;
state.vuex_msgList.unshift(msg);
},
+
+ // ===== 智能客服:消息组列表维护(参照 vuex_msgList 维护方式) =====
+ // 覆盖整个列表
+ push_AiMsgList(state, list) {
+ state.vuex_aiMessageGroups = Array.isArray(list) ? list : [];
+ },
+ // 追加历史消息到头部(分页加载)
+ prepend_AiMsgList(state, list) {
+ const prev = state.vuex_aiMessageGroups || [];
+ const next = Array.isArray(list) ? list : [];
+ const merged = [...next, ...prev];
+
+ const seen = new Set();
+ state.vuex_aiMessageGroups = merged.filter((item) => {
+ const id = item && item.id;
+ if (!id) return true;
+ if (seen.has(id)) return false;
+ seen.add(id);
+ return true;
+ });
+ },
+ // 推送一条新消息(去重)
+ push_AiMsg(state, msg) {
+ if (!msg || !msg.id) return;
+ const exists = (state.vuex_aiMessageGroups || []).some(
+ (item) => item && item.id === msg.id
+ );
+ if (!exists) {
+ state.vuex_aiMessageGroups.push(msg);
+ }
+ },
+ // 插入一条消息到头部
+ unshift_AiMsg(state, msg) {
+ if (!msg) return;
+ state.vuex_aiMessageGroups.unshift(msg);
+ },
// 更新置顶状态(本地)
update_TopState(state, { friendId, isTop }) {
state.vuex_userMsgList = (state.vuex_userMsgList || []).map((item) => {
diff --git a/utils/chat.js b/utils/chat.js
index 714a464..38c146d 100644
--- a/utils/chat.js
+++ b/utils/chat.js
@@ -123,3 +123,67 @@ export function scrollToBottomByContentHeight(
.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}`;
+}