interface ConsultMessage { id: string; // 咨询记录详情ID consultRecordId: number; //咨询记录ID sender: "user" | "agent" | "human" | "system"; sendType: string; // 发送类型 1-患者 2-医生 3-系统 4-AI messageType: "1" | "2"; // 1文本 2 图片 messageContent?: string; // 消息内容 sendTime?: string; // 发送消息的时间 } import I18nBehavior from "../../../../i18n/behavior"; import { Post } from "../../../../lib/request/method"; import { upload } from "../../../../lib/request/upload"; import dayjs from "dayjs"; const sendTypeMap: Record = { "1": "user", // 患者 "2": "human", // 医生 "3": "system", // 系统 "4": "agent", // AI }; // 计算底部安全区位置(rpx) function calculateSafeBottomRpx(): number { const systemInfo = wx.getSystemInfoSync(); // 获取窗口的高度 const windowHeight = systemInfo.windowHeight; // 获取安全区底部的高度 const safeAreaBottom = systemInfo.safeArea?.bottom ?? windowHeight; const safeBottom = windowHeight - safeAreaBottom; //将px转为rpx return (750 / systemInfo.windowWidth) * safeBottom; } function isConsultEndMessage(msg: ConsultMessage): boolean { if (msg.sendType !== "3" || !msg.messageContent) { return false; } const content = msg.messageContent; const isReminder = content.includes("30分钟") && (content.includes("5分钟后") || content.includes("将在")); const isRealEnd = content.includes("咨询结束") && !isReminder; return isRealEnd; } // 获取的聊天消息为ConsultMessage格式 function transformMessage(item: AnyObject): ConsultMessage { const sender = sendTypeMap[item.sendType]; return { id: `msg-${item.id}`, consultRecordId: item.consultRecordId, sender, sendTime: item.sendTime || "", sendType: item.sendType, messageType: item.messageType as "1" | "2", messageContent: item.messageContent || "", }; } Component({ behaviors: [I18nBehavior], properties: {}, data: { title: '', i18n: { consultChat: { _: '聊天' } }, messages: [] as ConsultMessage[], inputText: "", inputFocus: true, inputBoxBottom: 0, baseInputBottom: 0, keepFocus: true, _keyboardHeight: 0, // 当前键盘高度 isTransferredToHuman: false, // 是否已转人工 consultEnded: false, // 是否已结束咨询 _pollTimer: 0 as any, // 5秒轮询最新消息定时器 textareaHeight: 80, // textarea 高度(rpx),初始值与 min-height 一致 }, observers: { 'i18n.consultChat._'(this: any, title: string) { this.setData({ title }); }, }, lifetimes: { async attached() { const safeBottomRpx = calculateSafeBottomRpx(); const tabBarHeight = 100; // rpx const baseBottom = safeBottomRpx + tabBarHeight; // 获取咨询中的id const consultId = wx.getStorageSync("consultId"); let messages: ConsultMessage[] = []; if (consultId) { try { // 获取所有消息的数据 const res = await Post(`/consultManage/getAllMsgs/${consultId}`); if (res.data && res.data.length > 0) { messages = res.data.map((item: AnyObject) => { const msg = transformMessage(item); return msg; }); } } catch (error: any) { wx.showToast({ title: error?.errMsg || "获取历史消息失败", icon: "none", }); } } const hasEndMessage = messages.some((msg: ConsultMessage) => isConsultEndMessage(msg) ); const consultEnded = hasEndMessage || wx.getStorageSync("consultEnded"); this.setData({ baseInputBottom: baseBottom, inputBoxBottom: baseBottom, messages, consultEnded: !!consultEnded, }); this.triggerEvent("boxBottom", { inputBoxBottom: baseBottom }); const kbHandler = (res: any) => { const height = res?.height ?? 0; // 键盘收起时,直接更新位置 if (height === 0) { this._updateInputPosition(0); return; } if (this.data.inputFocus || this.data.keepFocus) { this._updateInputPosition(height); } }; wx.onKeyboardHeightChange?.(kbHandler); (this as any)._kbHandler = kbHandler; this._ensureFocus(); const systemInfo = wx.getSystemInfoSync(); const isHarmonyOS = systemInfo.system && systemInfo.system.toLowerCase().includes("harmony"); const delayTime = isHarmonyOS ? 300 : 0; if (delayTime > 0) { setTimeout(() => { if (this.data._keyboardHeight > 0 && this.data.keepFocus) { this._updateInputPosition(this.data._keyboardHeight); } }, delayTime); } // 如果咨询未结束,启动轮询最新消息 if (!consultEnded) { this._startPolling(); } }, detached() { // 清理监听 if ((this as any)._kbHandler) { wx.offKeyboardHeightChange?.((this as any)._kbHandler); } this._stopPolling(); }, }, methods: { _scrollToBottom() { this.triggerEvent("scroll", { id: "bottom" }); }, _hideKeyboardAndUpdatePosition() { wx.hideKeyboard?.(); this.setData({ inputFocus: false, keepFocus: false, }); this._updateInputPosition(0); }, _updateInputPosition(keyboardHeight: number) { const systemInfo = wx.getSystemInfoSync(); const rpx2px = systemInfo.windowWidth / 750; const keyboardHeightRpx = keyboardHeight > 0 ? keyboardHeight / rpx2px : 0; const nextBottom = keyboardHeight > 0 ? keyboardHeightRpx : this.data.baseInputBottom; // 避免重复更新相同位置(容差1rpx) if (Math.abs(nextBottom - this.data.inputBoxBottom) < 1) { if (keyboardHeight !== this.data._keyboardHeight) { this.setData({ _keyboardHeight: keyboardHeight }); } return; } this.setData({ inputBoxBottom: nextBottom, _keyboardHeight: keyboardHeight, }); this.triggerEvent("boxBottom", { inputBoxBottom: nextBottom }); if (keyboardHeight > 0) { this.triggerEvent("scroll", { id: "bottom" }); } }, _ensureFocus() { if (!this.data.keepFocus) return; this.setData({ inputFocus: false }); wx.nextTick?.(() => { setTimeout(() => { if (this.data.keepFocus) { this.setData({ inputFocus: true }); setTimeout(() => { if (this.data._keyboardHeight > 0 && this.data.inputFocus) { this._updateInputPosition(this.data._keyboardHeight); } }, 150); } }, 120); }); }, tapPanel() { if (!this.data.inputFocus && this.data.keepFocus) { this._ensureFocus(); } }, endConsult() { // 收起键盘并更新位置 this._hideKeyboardAndUpdatePosition(); wx.showModal({ title: "", content: `确定要结束本次?${this.data.title}`, cancelText: `继续${this.data.title}`, confirmText: "结束", }).then((res: any) => { if (res.confirm) { // 确认结束 this._endConsult(); } else { // 继续咨询,恢复聚焦 this.setData({ keepFocus: true }); this._ensureFocus(); } }); }, async _endConsult() { const endDate = dayjs().format("MM-DD HH:mm:ss"); const consultId = wx.getStorageSync("consultId"); this._appendMessage({ id: `end-time-${Date.now()}`, consultRecordId: consultId || 0, sender: "system", sendType: "3", messageType: "1", messageContent: `${this.data.title}结束`, sendTime: endDate, }); // 调用结束咨询接口 if (consultId) { try { await Post(`/consultManage/end/${consultId}`); } catch (error: any) { wx.showToast({ title: error?.errMsg || `结束${this.data.title}失败`, icon: "none", }); } } // 设置结束状态 this.setData({ consultEnded: true }); wx.setStorageSync("consultEnded", true); wx.removeStorageSync("consultId"); this._stopPolling(); // 收起键盘 wx.hideKeyboard?.(); // 重置底部位置为正常值(tabbar 高度 + 安全区高度) const safeBottomRpx = calculateSafeBottomRpx(); const tabBarHeight = 100; // rpx const normalBottom = tabBarHeight + safeBottomRpx; this.triggerEvent("boxBottom", { inputBoxBottom: normalBottom }); // 通知父组件显示guide菜单组件 this.triggerEvent("consultEvent", { type: "end" }); // 滚动到底部 this._scrollToBottom(); }, handleInput(event: any) { const value = event.detail.value; this.setData({ inputText: value }); // 内容为空时,立即重置为最小高度 if (!value || value.trim() === "") { if (this.data.textareaHeight !== 80) { this.setData({ textareaHeight: 80 }); } } }, onLineChange(event: any) { const minHeight = 80; // 最小高度(rpx) const maxHeight = 200; // 最大高度(rpx) const lineCount = event.detail.lineCount || 1; // 如果输入框为空,直接设置为最小高度 if (!this.data.inputText || this.data.inputText.trim() === "") { if (this.data.textareaHeight !== minHeight) { this.setData({ textareaHeight: minHeight }); } return; } if (lineCount === 1) { if (this.data.textareaHeight !== minHeight) { this.setData({ textareaHeight: minHeight }); } return; } const lineHeight = 53; // 每行高度(rpx) const padding = 24; const calculatedHeight = lineCount * lineHeight + padding; const finalHeight = Math.max( minHeight, Math.min(maxHeight, calculatedHeight) ); if (Math.abs(this.data.textareaHeight - finalHeight) > 3) { this.setData({ textareaHeight: finalHeight }); } }, onInputFocus(event: any) { const keyboardHeight = event.detail.height ?? 0; // 设置 focus 状态 this.setData({ inputFocus: true, keepFocus: true, }); if (keyboardHeight > 0) { this._updateInputPosition(keyboardHeight); } else { if (this.data._keyboardHeight > 0) { // 延迟一点时间,等待键盘完全弹起后再更新 setTimeout(() => { if (this.data.inputFocus && this.data._keyboardHeight > 0) { this._updateInputPosition(this.data._keyboardHeight); } }, 100); } } }, onInputBlur() { // 设置 focus 状态 this.setData({ inputFocus: false, }); setTimeout(() => { if (!this.data.inputFocus) { this._updateInputPosition(0); } }, 100); }, // 启动轮询最新消息 _startPolling() { if (this.data.consultEnded) return; this._stopPolling(); const timer = setInterval(() => { if (this.data.consultEnded) { this._stopPolling(); return; } this._getLatestMessages(); }, 5000); this.setData({ _pollTimer: timer }); }, _stopPolling() { if (this.data._pollTimer) { clearInterval(this.data._pollTimer); this.setData({ _pollTimer: 0 }); } }, // 获取最新消息 async _getLatestMessages() { const consultId = wx.getStorageSync("consultId"); if (!consultId) return; try { // 获取最新消息 const res = await Post(`/consultManage/getLatestMsgs/${consultId}`); if (res.data && Array.isArray(res.data) && res.data.length > 0) { const newMessages = res.data.map((item: AnyObject) => { const msg = transformMessage(item); return msg; }); const allMessages = [...this.data.messages, ...newMessages]; this.setData({ messages: allMessages }); const hasEndMessage = newMessages.some((msg: ConsultMessage) => isConsultEndMessage(msg) ); if (hasEndMessage && !this.data.consultEnded) { this.setData({ consultEnded: true }); wx.setStorageSync("consultEnded", true); this._stopPolling(); // 收起键盘 wx.hideKeyboard?.(); const safeBottomRpx = calculateSafeBottomRpx(); const tabBarHeight = 100; // rpx const normalBottom = tabBarHeight + safeBottomRpx; this.triggerEvent("boxBottom", { inputBoxBottom: normalBottom }); // 通知父组件显示guide菜单组件 this.triggerEvent("consultEvent", { type: "end" }); } this._scrollToBottom(); } } catch (error: any) { wx.showToast({ title: error?.errMsg || "获取最新消息失败", icon: "none", }); } }, // 发送消息到后端 async _sendMessage(messageType: "1" | "2", messageContent: string) { const consultId = wx.getStorageSync("consultId"); if (!consultId) { wx.showToast({ title: `${this.data.title}ID不存在`, icon: "none" }); return; } try { await Post(`/consultManage/sendConsultMsg`, { consultRecordId: consultId, messageType, messageContent, }).then(() => { this._stopPolling(); this._getLatestMessages(); setTimeout(() => { if (!this.data.consultEnded) { this._startPolling(); } }, 5000); }); } catch (error: any) { wx.showToast({ title: error?.errMsg || "发送失败,请重试", icon: "none", }); } }, async sendText() { const text = this.data.inputText.trim(); if (!text) { wx.showToast({ title: "发送内容不能为空", icon: "none" }); return; } const consultId = wx.getStorageSync("consultId"); // 先添加用户消息到界面 const messageId = `user-text-${Date.now()}`; this._appendMessage({ id: messageId, consultRecordId: consultId || 0, sender: "user", sendType: "1", messageType: "1", messageContent: text, }); this._scrollToBottom(); // 发送信息 this._sendMessage("1", text); // 保存当前键盘状态 const wasFocused = this.data.inputFocus; const currentKeyboardHeight = this.data._keyboardHeight; this.setData({ inputText: "", textareaHeight: 80, }); if (!wasFocused) { this.setData({ inputFocus: false, keepFocus: false, }); } else { if (currentKeyboardHeight > 0) { this._updateInputPosition(currentKeyboardHeight); } } }, async chooseImage() { const wasFocused = this.data.inputFocus; const currentKeyboardHeight = this.data._keyboardHeight; this._hideKeyboardAndUpdatePosition(); try { const res = await wx.chooseMedia({ count: 1, mediaType: ["image"], sourceType: ["album", "camera"], }); const files = res.tempFiles ?? []; const file = files[0]; if (!file?.tempFilePath) return; const imagePath = file.tempFilePath; // 直接发送图片 await this._sendImageMessage(imagePath); if (wasFocused) { wx.nextTick?.(() => { setTimeout(() => { this.setData({ inputFocus: true, keepFocus: true, }); // 恢复键盘位置 if (currentKeyboardHeight > 0) { this._updateInputPosition(currentKeyboardHeight); } }, 200); }); } } catch (error: any) { if (error.errMsg && !error.errMsg.includes("cancel")) { console.error("选择图片失败", error); } if (wasFocused) { wx.nextTick?.(() => { setTimeout(() => { this.setData({ inputFocus: true, keepFocus: true, }); // 恢复键盘位置 if (currentKeyboardHeight > 0) { this._updateInputPosition(currentKeyboardHeight); } }, 200); }); } } }, // 发送图片 async _sendImageMessage(imagePath: string) { const consultId = wx.getStorageSync("consultId"); try { const messageId = `user-image-${Date.now()}`; this._appendMessage({ id: messageId, consultRecordId: consultId || 0, sender: "user", sendType: "1", messageType: "2", messageContent: imagePath, }); // 上传图片 const imageUrl = await upload({ params: { name: "file", file: imagePath }, transform({ data }: any) { return data?.url || data; }, }); // 发送图片消息 await this._sendMessage("2", imageUrl); // 更新消息中的图片URL const messages = this.data.messages; const messageIndex = messages.findIndex( (msg: ConsultMessage) => msg.id === messageId ); if (messageIndex !== -1) { messages[messageIndex].messageContent = imageUrl; this.setData({ messages }); } this._scrollToBottom(); } catch (error: any) { wx.showToast({ title: error?.errMsg || "图片上传失败", icon: "none", }); } }, // 预览图片 previewImage(e: any) { const currentUrl = e.currentTarget.dataset.url; const urls = this.data.messages .filter( (msg: ConsultMessage) => msg.messageType === "2" && msg.messageContent ) .map((msg: ConsultMessage) => msg.messageContent!); wx.previewImage({ current: currentUrl, urls: urls.length > 0 ? urls : [currentUrl], fail: (err) => { wx.showToast({ title: err?.errMsg || "预览图片失败", icon: "none", }); }, }); }, _appendMessage(message: ConsultMessage) { // 把获取的最新的消息追加到所有消息后面 const messages = [...this.data.messages, message]; this.setData({ messages }); }, }, });