|
|
@@ -5,8 +5,7 @@ interface ConsultMessage {
|
|
|
sendType: string; // 发送类型 1-患者 2-医生 3-系统 4-AI
|
|
|
messageType: "1" | "2"; // 1文本 2 图片
|
|
|
messageContent?: string; // 消息内容
|
|
|
- sendTime?: string; // 发送时间
|
|
|
- createdAt: number;
|
|
|
+ sendTime?: string; // 发送消息的时间
|
|
|
}
|
|
|
import { Post } from "../../../../lib/request/method";
|
|
|
import { upload } from "../../../../lib/request/upload";
|
|
|
@@ -23,25 +22,41 @@ const sendTypeMap: Record<string, "user" | "agent" | "human" | "system"> = {
|
|
|
// 计算底部安全区位置(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;
|
|
|
}
|
|
|
|
|
|
+// 判断消息是否为真正的咨询结束消息(排除30分钟提醒消息)
|
|
|
+function isConsultEndMessage(msg: ConsultMessage): boolean {
|
|
|
+ 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("咨询结束") && !isReminder;
|
|
|
+ return isRealEnd;
|
|
|
+}
|
|
|
+
|
|
|
// 获取的聊天消息为ConsultMessage格式 提取为一个公共的方法
|
|
|
function transformMessage(item: AnyObject): ConsultMessage {
|
|
|
- const sender = sendTypeMap[item.sendType] || "user";
|
|
|
- const sendTime = item.sendTime || "";
|
|
|
- const createdAt = sendTime ? dayjs(sendTime).valueOf() : Date.now();
|
|
|
+ const sender = sendTypeMap[item.sendType];
|
|
|
return {
|
|
|
- id: `msg-${item.id || item.consultRecordId}-${createdAt}`,
|
|
|
+ id: `msg-${item.id}`,
|
|
|
consultRecordId: item.consultRecordId,
|
|
|
sender,
|
|
|
+ sendTime: item.sendTime || "",
|
|
|
sendType: item.sendType,
|
|
|
messageType: item.messageType as "1" | "2",
|
|
|
messageContent: item.messageContent || "",
|
|
|
- createdAt,
|
|
|
};
|
|
|
}
|
|
|
|
|
|
@@ -59,6 +74,7 @@ Component({
|
|
|
isTransferredToHuman: false, // 是否已转人工
|
|
|
consultEnded: false, // 是否已结束咨询
|
|
|
_pollTimer: 0 as any, // 5秒轮询最新消息定时器
|
|
|
+ textareaHeight: 80, // textarea 高度(rpx),初始值与 min-height 一致
|
|
|
},
|
|
|
|
|
|
lifetimes: {
|
|
|
@@ -88,21 +104,9 @@ Component({
|
|
|
}
|
|
|
|
|
|
// 检查历史消息中是否有真正的咨询结束消息(排除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 hasEndMessage = messages.some((msg: ConsultMessage) =>
|
|
|
+ isConsultEndMessage(msg)
|
|
|
+ );
|
|
|
|
|
|
const consultEnded = hasEndMessage || wx.getStorageSync("consultEnded");
|
|
|
|
|
|
@@ -116,14 +120,22 @@ Component({
|
|
|
// 键盘高度监听作为位置同步的补充
|
|
|
const kbHandler = (res: any) => {
|
|
|
const height = res?.height ?? 0;
|
|
|
+ // 如果键盘高度没有实际变化,不更新位置(避免输入时的高度变化触发位置更新)
|
|
|
+ if (Math.abs(height - this.data._keyboardHeight) < 5) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
// 清除之前的定时器
|
|
|
if (this.data._kbTimer) {
|
|
|
clearTimeout(this.data._kbTimer);
|
|
|
}
|
|
|
|
|
|
+ // 键盘展开时立即更新位置,确保没有空隙
|
|
|
+ // 键盘收起时稍微延迟,避免与输入框高度变化冲突
|
|
|
+ const delay = height > 0 ? 0 : 100; // 键盘展开时立即更新,不延迟
|
|
|
const timer = setTimeout(() => {
|
|
|
this._updateInputPosition(height);
|
|
|
- }, 50) as unknown as number;
|
|
|
+ }, delay) as unknown as number;
|
|
|
|
|
|
this.setData({ _kbTimer: timer });
|
|
|
};
|
|
|
@@ -152,11 +164,9 @@ Component({
|
|
|
},
|
|
|
},
|
|
|
methods: {
|
|
|
- // 滚动到底部
|
|
|
- _scrollToBottom(delay: number = 100) {
|
|
|
- setTimeout(() => {
|
|
|
- this.triggerEvent("scroll", { id: "bottom" });
|
|
|
- }, delay);
|
|
|
+ // 滚动到底部-
|
|
|
+ _scrollToBottom() {
|
|
|
+ this.triggerEvent("scroll", { id: "bottom" });
|
|
|
},
|
|
|
// 收起键盘并更新位置
|
|
|
_hideKeyboardAndUpdatePosition() {
|
|
|
@@ -179,16 +189,20 @@ Component({
|
|
|
}
|
|
|
|
|
|
const systemInfo = wx.getSystemInfoSync();
|
|
|
- const keyboardHeightRpx = (750 / systemInfo.windowWidth) * keyboardHeight;
|
|
|
+ const rpx2px = systemInfo.windowWidth / 750;
|
|
|
+
|
|
|
+ // 键盘高度是 px,转换为 rpx
|
|
|
+ const keyboardHeightRpx =
|
|
|
+ keyboardHeight > 0 ? keyboardHeight / rpx2px : 0;
|
|
|
|
|
|
// 计算输入框底部位置
|
|
|
- // 键盘展开时:面板紧贴键盘,不加 tabbar 距离
|
|
|
+ // 键盘展开时:面板紧贴键盘(bottom = 键盘高度),确保没有空隙
|
|
|
// 键盘收起时:保留 tabbar 距离(baseInputBottom 已包含安全区 + 100rpx tabbar)
|
|
|
const nextBottom =
|
|
|
keyboardHeight > 0 ? keyboardHeightRpx : this.data.baseInputBottom;
|
|
|
|
|
|
- // 避免重复更新相同位置(5rpx 容差)
|
|
|
- if (Math.abs(nextBottom - this.data.inputBoxBottom) < 5) {
|
|
|
+ // 避免重复更新相同位置(1rpx 容差,更精确)
|
|
|
+ if (Math.abs(nextBottom - this.data.inputBoxBottom) < 1) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
@@ -203,9 +217,7 @@ Component({
|
|
|
|
|
|
// 键盘弹出时平滑滚动到底部
|
|
|
if (keyboardHeight > 0) {
|
|
|
- setTimeout(() => {
|
|
|
- this.triggerEvent("scroll", { id: "bottom" });
|
|
|
- }, 150);
|
|
|
+ this.triggerEvent("scroll", { id: "bottom" });
|
|
|
}
|
|
|
},
|
|
|
_ensureFocus() {
|
|
|
@@ -230,20 +242,16 @@ Component({
|
|
|
content: "确定要结束本次咨询?",
|
|
|
cancelText: "继续咨询",
|
|
|
confirmText: "结束",
|
|
|
- })
|
|
|
- .then((res) => {
|
|
|
- if (res.confirm) {
|
|
|
- // 确认结束
|
|
|
- this._endConsult();
|
|
|
- } else {
|
|
|
- // 继续咨询,恢复聚焦
|
|
|
- this.setData({ keepFocus: true });
|
|
|
- this._ensureFocus();
|
|
|
- }
|
|
|
- })
|
|
|
- .catch(() => {
|
|
|
- // 弹窗异常时不做处理
|
|
|
- });
|
|
|
+ }).then((res: any) => {
|
|
|
+ if (res.confirm) {
|
|
|
+ // 确认结束
|
|
|
+ this._endConsult();
|
|
|
+ } else {
|
|
|
+ // 继续咨询,恢复聚焦
|
|
|
+ this.setData({ keepFocus: true });
|
|
|
+ this._ensureFocus();
|
|
|
+ }
|
|
|
+ });
|
|
|
},
|
|
|
async _endConsult() {
|
|
|
// 格式化日期时间,格式:MM-DD HH:mm:ss(与系统消息格式一致)
|
|
|
@@ -259,7 +267,6 @@ Component({
|
|
|
messageType: "1",
|
|
|
messageContent: "咨询结束",
|
|
|
sendTime: endDate,
|
|
|
- createdAt: Date.now(),
|
|
|
});
|
|
|
|
|
|
// 调用结束咨询接口
|
|
|
@@ -279,7 +286,8 @@ Component({
|
|
|
|
|
|
// 更新本地存储:标记咨询已结束
|
|
|
wx.setStorageSync("consultEnded", true);
|
|
|
-
|
|
|
+ //咨询结束之后 需要清除咨询id
|
|
|
+ wx.removeStorageSync("consultId");
|
|
|
// 停止轮询最新消息
|
|
|
this._stopPolling();
|
|
|
|
|
|
@@ -298,10 +306,58 @@ Component({
|
|
|
this.triggerEvent("consultEvent", { type: "end" });
|
|
|
|
|
|
// 滚动到底部
|
|
|
- // this._scrollToBottom();
|
|
|
+ this._scrollToBottom();
|
|
|
},
|
|
|
handleInput(event: any) {
|
|
|
- this.setData({ inputText: event.detail.value });
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 多行时才动态计算高度
|
|
|
+ // 字体大小 28rpx,行高 1.9,所以每行实际高度约为 28 * 1.9 ≈ 53rpx
|
|
|
+ const lineHeight = 53; // 每行高度(rpx)
|
|
|
+ const padding = 24; // 上下 padding 总和(12rpx * 2 = 24rpx)
|
|
|
+
|
|
|
+ // 计算高度:行数 * 行高 + 上下 padding
|
|
|
+ const calculatedHeight = lineCount * lineHeight + padding;
|
|
|
+ // 限制在 minHeight 和 maxHeight 之间
|
|
|
+ const finalHeight = Math.max(
|
|
|
+ minHeight,
|
|
|
+ Math.min(maxHeight, calculatedHeight)
|
|
|
+ );
|
|
|
+
|
|
|
+ // 只在高度确实需要变化时更新(阈值设为 3rpx,减少频繁更新和跳动)
|
|
|
+ if (Math.abs(this.data.textareaHeight - finalHeight) > 3) {
|
|
|
+ this.setData({ textareaHeight: finalHeight });
|
|
|
+ }
|
|
|
},
|
|
|
onInputFocus(event: any) {
|
|
|
const keyboardHeight = event.detail.height ?? 0;
|
|
|
@@ -351,9 +407,7 @@ Component({
|
|
|
// 获取最新消息
|
|
|
async _getLatestMessages() {
|
|
|
const consultId = wx.getStorageSync("consultId");
|
|
|
- if (!consultId) {
|
|
|
- return;
|
|
|
- }
|
|
|
+ if (!consultId) return;
|
|
|
try {
|
|
|
// 获取最新消息
|
|
|
const res = await Post(`/consultManage/getLatestMsgs/${consultId}`);
|
|
|
@@ -366,24 +420,9 @@ Component({
|
|
|
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;
|
|
|
- });
|
|
|
+ const hasEndMessage = newMessages.some((msg: ConsultMessage) =>
|
|
|
+ isConsultEndMessage(msg)
|
|
|
+ );
|
|
|
|
|
|
// 如果收到真正的咨询结束消息,更新状态并停止轮询
|
|
|
// 注意:30分钟提醒消息不会触发此逻辑,会继续轮询等待真正的结束消息
|
|
|
@@ -408,7 +447,7 @@ Component({
|
|
|
}
|
|
|
|
|
|
//如果有最新消息要滚动到底部
|
|
|
- this._scrollToBottom(100);
|
|
|
+ this._scrollToBottom();
|
|
|
}
|
|
|
} catch (error: any) {
|
|
|
wx.showToast({
|
|
|
@@ -429,9 +468,10 @@ Component({
|
|
|
consultRecordId: consultId,
|
|
|
messageType,
|
|
|
messageContent,
|
|
|
+ }).then(() => {
|
|
|
+ // 发送成功后获取最新消息
|
|
|
+ this._getLatestMessages();
|
|
|
});
|
|
|
- // 发送成功后获取最新消息
|
|
|
- await this._getLatestMessages();
|
|
|
} catch (error: any) {
|
|
|
wx.showToast({
|
|
|
title: error?.errMsg || "发送失败,请重试",
|
|
|
@@ -456,19 +496,27 @@ Component({
|
|
|
sendType: "1",
|
|
|
messageType: "1",
|
|
|
messageContent: text,
|
|
|
- createdAt: Date.now(),
|
|
|
});
|
|
|
+ // 平滑滚动到底部,确保新消息可见
|
|
|
+ this._scrollToBottom();
|
|
|
// 发送信息
|
|
|
this._sendMessage("1", text);
|
|
|
|
|
|
// 清空输入框,保持键盘展开状态
|
|
|
- this.setData({ inputText: "" });
|
|
|
- // 平滑滚动到底部,确保新消息可见
|
|
|
- this._scrollToBottom();
|
|
|
+ // 同时重置高度为最小高度,确保立即恢复
|
|
|
+ this.setData({
|
|
|
+ inputText: "",
|
|
|
+ textareaHeight: 80, // 重置为最小高度
|
|
|
+ });
|
|
|
},
|
|
|
async chooseImage() {
|
|
|
- // 收起键盘并更新位置
|
|
|
+ // 保存当前焦点状态,选择图片后恢复
|
|
|
+ const wasFocused = this.data.inputFocus;
|
|
|
+ const currentKeyboardHeight = this.data._keyboardHeight;
|
|
|
+
|
|
|
+ // 临时收起键盘,选择图片需要系统界面
|
|
|
this._hideKeyboardAndUpdatePosition();
|
|
|
+
|
|
|
try {
|
|
|
const res = await wx.chooseMedia({
|
|
|
count: 1,
|
|
|
@@ -481,11 +529,43 @@ Component({
|
|
|
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);
|
|
|
+ });
|
|
|
+ }
|
|
|
}
|
|
|
},
|
|
|
// 发送图片
|
|
|
@@ -501,7 +581,6 @@ Component({
|
|
|
sendType: "1",
|
|
|
messageType: "2",
|
|
|
messageContent: imagePath, // 先用本地路径,上传后更新
|
|
|
- createdAt: Date.now(),
|
|
|
});
|
|
|
|
|
|
// 上传图片
|
|
|
@@ -542,7 +621,13 @@ Component({
|
|
|
(msg: ConsultMessage) => msg.messageType === "2" && msg.messageContent
|
|
|
)
|
|
|
.map((msg: ConsultMessage) => msg.messageContent!);
|
|
|
-
|
|
|
+ console.log(
|
|
|
+ urls,
|
|
|
+ "111",
|
|
|
+ this.data.messages.filter(
|
|
|
+ (msg: ConsultMessage) => msg.messageType === "2" && msg.messageContent
|
|
|
+ )
|
|
|
+ );
|
|
|
wx.previewImage({
|
|
|
current: currentUrl, // 当前显示图片的链接
|
|
|
urls: urls.length > 0 ? urls : [currentUrl], // 需要预览的图片链接列表
|
|
|
@@ -558,8 +643,6 @@ Component({
|
|
|
// 把获取的最新的消息追加到所有消息后面
|
|
|
const messages = [...this.data.messages, message];
|
|
|
this.setData({ messages });
|
|
|
- // 延迟滚动,确保消息渲染完成后再滚动
|
|
|
- this._scrollToBottom(100);
|
|
|
},
|
|
|
},
|
|
|
});
|