| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565 |
- 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; // 发送时间
- createdAt: number;
- }
- import { Post } from "../../../../lib/request/method";
- import { upload } from "../../../../lib/request/upload";
- import dayjs from "dayjs";
- // sendType映射: 1-患者 2-医生 3-系统 4-AI
- 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;
- return (750 / systemInfo.windowWidth) * safeBottom;
- }
- // 获取的聊天消息为ConsultMessage格式 提取为一个公共的方法
- function transformMessage(item: AnyObject): ConsultMessage {
- const sender = sendTypeMap[item.sendType] || "user";
- const sendTime = item.sendTime || "";
- const createdAt = sendTime ? dayjs(sendTime).valueOf() : Date.now();
- return {
- id: `msg-${item.id || item.consultRecordId}-${createdAt}`,
- consultRecordId: item.consultRecordId,
- sender,
- sendType: item.sendType,
- messageType: item.messageType as "1" | "2",
- messageContent: item.messageContent || "",
- createdAt,
- };
- }
- Component({
- properties: {},
- data: {
- messages: [] as ConsultMessage[],
- inputText: "",
- inputFocus: true,
- inputBoxBottom: 0,
- baseInputBottom: 0,
- keepFocus: true,
- _kbTimer: 0 as any,
- _keyboardHeight: 0, // 当前键盘高度
- isTransferredToHuman: false, // 是否已转人工
- consultEnded: false, // 是否已结束咨询
- _pollTimer: 0 as any, // 5秒轮询最新消息定时器
- },
- 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) =>
- transformMessage(item)
- );
- }
- } catch (error: any) {
- wx.showToast({
- title: error?.errMsg || "获取历史消息失败",
- icon: "none",
- });
- }
- }
- // 检查历史消息中是否有真正的咨询结束消息(排除30分钟提醒消息)
- const hasEndMessage = messages.some((msg: ConsultMessage) => {
- if (msg.sendType !== "3" || !msg.messageContent) {
- return false;
- }
- const content = msg.messageContent;
- // 排除30分钟提醒消息(包含"30分钟"和"5分钟后")
- const isReminder =
- content.includes("30分钟") &&
- (content.includes("5分钟后") || content.includes("将在"));
- // 判断是否为真正的咨询结束消息
- const isRealEnd =
- (content.includes("咨询结束") || content.includes("咨询已结束")) &&
- !isReminder;
- return isRealEnd;
- });
- 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 (this.data._kbTimer) {
- clearTimeout(this.data._kbTimer);
- }
- const timer = setTimeout(() => {
- this._updateInputPosition(height);
- }, 50) as unknown as number;
- this.setData({ _kbTimer: timer });
- };
- wx.onKeyboardHeightChange?.(kbHandler);
- (this as any)._kbHandler = kbHandler;
- // 渲染完成后再触发一次聚焦,确保键盘弹起
- this._ensureFocus();
- // 如果咨询未结束,启动轮询最新消息
- if (!consultEnded) {
- this._startPolling();
- }
- },
- detached() {
- // 清理监听和定时器
- if ((this as any)._kbHandler) {
- wx.offKeyboardHeightChange?.((this as any)._kbHandler);
- }
- if (this.data._kbTimer) {
- clearTimeout(this.data._kbTimer);
- }
- // 清理轮询定时器
- this._stopPolling();
- },
- },
- methods: {
- // 滚动到底部
- _scrollToBottom(delay: number = 100) {
- setTimeout(() => {
- this.triggerEvent("scroll", { id: "bottom" });
- }, delay);
- },
- // 收起键盘并更新位置
- _hideKeyboardAndUpdatePosition() {
- wx.hideKeyboard?.();
- this.setData({
- inputFocus: false,
- keepFocus: false,
- });
- this._updateInputPosition(0);
- },
- // 统一的位置更新方法
- _updateInputPosition(keyboardHeight: number) {
- // 避免重复更新相同高度(10px 容差)
- if (
- Math.abs(keyboardHeight - this.data._keyboardHeight) < 10 &&
- Math.abs(this.data.inputBoxBottom - this.data.baseInputBottom) < 10 &&
- keyboardHeight === 0
- ) {
- return;
- }
- const systemInfo = wx.getSystemInfoSync();
- const keyboardHeightRpx = (750 / systemInfo.windowWidth) * keyboardHeight;
- // 计算输入框底部位置
- // 键盘展开时:面板紧贴键盘,不加 tabbar 距离
- // 键盘收起时:保留 tabbar 距离(baseInputBottom 已包含安全区 + 100rpx tabbar)
- const nextBottom =
- keyboardHeight > 0 ? keyboardHeightRpx : this.data.baseInputBottom;
- // 避免重复更新相同位置(5rpx 容差)
- if (Math.abs(nextBottom - this.data.inputBoxBottom) < 5) {
- return;
- }
- // 更新位置
- this.setData({
- inputBoxBottom: nextBottom,
- _keyboardHeight: keyboardHeight,
- });
- // 通知父组件更新底部 padding
- this.triggerEvent("boxBottom", { inputBoxBottom: nextBottom });
- // 键盘弹出时平滑滚动到底部
- if (keyboardHeight > 0) {
- setTimeout(() => {
- this.triggerEvent("scroll", { id: "bottom" });
- }, 150);
- }
- },
- _ensureFocus() {
- if (!this.data.keepFocus) return;
- this.setData({ inputFocus: false });
- wx.nextTick?.(() => {
- setTimeout(() => {
- if (this.data.keepFocus) this.setData({ inputFocus: true });
- }, 120);
- });
- },
- tapPanel() {
- if (!this.data.inputFocus && this.data.keepFocus) {
- this._ensureFocus();
- }
- },
- endConsult() {
- // 收起键盘并更新位置
- this._hideKeyboardAndUpdatePosition();
- wx.showModal({
- title: "",
- content: "确定要结束本次咨询?",
- cancelText: "继续咨询",
- confirmText: "结束",
- })
- .then((res) => {
- if (res.confirm) {
- // 确认结束
- this._endConsult();
- } else {
- // 继续咨询,恢复聚焦
- this.setData({ keepFocus: true });
- this._ensureFocus();
- }
- })
- .catch(() => {
- // 弹窗异常时不做处理
- });
- },
- async _endConsult() {
- // 格式化日期时间,格式:MM-DD HH:mm:ss(与系统消息格式一致)
- 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: "咨询结束",
- sendTime: endDate,
- createdAt: Date.now(),
- });
- // 调用结束咨询接口
- if (consultId) {
- try {
- await Post(`/consultManage/end/${consultId}`);
- } catch (error: any) {
- wx.showToast({
- title: error?.errMsg || "结束咨询失败",
- icon: "none",
- });
- }
- }
- // 设置结束状态
- this.setData({ consultEnded: true });
- // 更新本地存储:标记咨询已结束
- wx.setStorageSync("consultEnded", true);
- // 停止轮询最新消息
- this._stopPolling();
- // 收起键盘
- wx.hideKeyboard?.();
- // 重置底部位置为正常值(tabbar 高度 + 安全区高度)
- const safeBottomRpx = calculateSafeBottomRpx();
- const tabBarHeight = 100; // rpx
- const normalBottom = tabBarHeight + safeBottomRpx;
- // 通知父组件重置 paddingBottom,避免菜单下方有大的距离
- this.triggerEvent("boxBottom", { inputBoxBottom: normalBottom });
- // 通知父组件显示guide菜单组件
- this.triggerEvent("consultEvent", { type: "end" });
- // 滚动到底部
- // this._scrollToBottom();
- },
- handleInput(event: any) {
- this.setData({ inputText: event.detail.value });
- },
- onInputFocus(event: any) {
- const keyboardHeight = event.detail.height ?? 0;
- // 设置 focus 状态
- this.setData({
- inputFocus: true,
- keepFocus: true,
- });
- // 立即更新位置
- this._updateInputPosition(keyboardHeight);
- },
- onInputBlur() {
- // 设置 focus 状态
- this.setData({
- inputFocus: false,
- });
- // 立即恢复位置(键盘高度为 0)
- this._updateInputPosition(0);
- },
- // 启动轮询最新消息
- _startPolling() {
- // 如果咨询已结束,不启动轮询
- if (this.data.consultEnded) return;
- // 清除之前的轮询定时器
- this._stopPolling();
- // 每5秒轮询一次
- 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) =>
- transformMessage(item)
- );
- const allMessages = [...this.data.messages, ...newMessages];
- this.setData({ messages: allMessages });
- // 检查是否有真正的咨询结束消息(排除30分钟提醒消息)
- // 30分钟提醒消息特征:包含"30分钟"和"5分钟后"等关键词
- // 真正的结束消息:包含"咨询结束"但不包含"5分钟后"、"将"等表示未来的词
- const hasEndMessage = newMessages.some((msg: ConsultMessage) => {
- if (msg.sendType !== "3" || !msg.messageContent) {
- return false;
- }
- const content = msg.messageContent;
- // 排除30分钟提醒消息(包含"30分钟"和"5分钟后")
- const isReminder =
- content.includes("30分钟") &&
- (content.includes("5分钟后") || content.includes("将在"));
- // 判断是否为真正的咨询结束消息
- const isRealEnd =
- (content.includes("咨询结束") ||
- content.includes("咨询已结束")) &&
- !isReminder;
- return isRealEnd;
- });
- // 如果收到真正的咨询结束消息,更新状态并停止轮询
- // 注意:30分钟提醒消息不会触发此逻辑,会继续轮询等待真正的结束消息
- if (hasEndMessage && !this.data.consultEnded) {
- this.setData({ consultEnded: true });
- wx.setStorageSync("consultEnded", true);
- this._stopPolling();
- // 收起键盘
- wx.hideKeyboard?.();
- // 重置底部位置为正常值(tabbar 高度 + 安全区高度)
- const safeBottomRpx = calculateSafeBottomRpx();
- const tabBarHeight = 100; // rpx
- const normalBottom = tabBarHeight + safeBottomRpx;
- // 通知父组件重置 paddingBottom
- this.triggerEvent("boxBottom", { inputBoxBottom: normalBottom });
- // 通知父组件显示guide菜单组件
- this.triggerEvent("consultEvent", { type: "end" });
- }
- //如果有最新消息要滚动到底部
- this._scrollToBottom(100);
- }
- } 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: "咨询ID不存在", icon: "none" });
- return;
- }
- try {
- await Post(`/consultManage/sendConsultMsg`, {
- consultRecordId: consultId,
- messageType,
- messageContent,
- });
- // 发送成功后获取最新消息
- await this._getLatestMessages();
- } 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,
- createdAt: Date.now(),
- });
- // 发送信息
- this._sendMessage("1", text);
- // 清空输入框,保持键盘展开状态
- this.setData({ inputText: "" });
- // 平滑滚动到底部,确保新消息可见
- this._scrollToBottom();
- },
- async chooseImage() {
- // 收起键盘并更新位置
- 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);
- } catch (error: any) {
- // 用户取消选择图片时不提示错误
- if (error.errMsg && !error.errMsg.includes("cancel")) {
- console.error("选择图片失败", error);
- }
- }
- },
- // 发送图片
- 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, // 先用本地路径,上传后更新
- createdAt: Date.now(),
- });
- // 上传图片
- const imageUrl = await upload({
- params: { name: "file", file: imagePath },
- transform({ data }: any) {
- return data?.url || data;
- },
- });
- // 发送图片消息
- await this._sendMessage("2", imageUrl);
- // 更新消息中的图片URL(从本地路径更新为服务器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;
- // 获取所有图片消息的 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 });
- // 延迟滚动,确保消息渲染完成后再滚动
- this._scrollToBottom(100);
- },
- },
- });
|