| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634 |
- 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<string, "user" | "agent" | "human" | "system"> = {
- "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 });
- },
- },
- });
|