1216 lines
34 KiB
Vue
1216 lines
34 KiB
Vue
<template>
|
||
<page-meta :page-style="'overflow:' + (keyboardHeight > 0 ? 'hidden' : 'visible')"></page-meta>
|
||
<view class="chat-page" :style="{ paddingBottom: keyboardHeight + 'px' }">
|
||
<!-- 患者信息栏 -->
|
||
<view class="patient-info-bar" v-if="patientInfo.name">
|
||
<view class="patient-info-content">
|
||
<view class="patient-basic-info">
|
||
<text class="patient-name">{{ patientInfo.name }}</text>
|
||
<text class="patient-detail"
|
||
>{{ patientInfo.sex }} · {{ patientInfo.age }}岁</text
|
||
>
|
||
</view>
|
||
<view class="patient-detail-btn" @click="handleViewPatientDetail">
|
||
<text class="detail-btn-text">查看档案</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 聊天消息区域 -->
|
||
<scroll-view
|
||
class="chat-content"
|
||
:style="{ bottom: (keyboardHeight > 0 ? keyboardHeight + 60 : 60) + 'px' }"
|
||
scroll-y="true"
|
||
enhanced="true"
|
||
bounces="false"
|
||
:scroll-into-view="scrollIntoView"
|
||
@scroll="onScroll"
|
||
@scrolltoupper="handleScrollToUpper"
|
||
ref="chatScrollView"
|
||
>
|
||
<!-- 加载更多提示 -->
|
||
<view class="load-more-tip" v-if="messageList.length >= 15">
|
||
<view class="loading" v-if="isLoadingMore">
|
||
<text class="loading-text">加载中...</text>
|
||
</view>
|
||
<view class="load-tip" v-else-if="!isCompleted">
|
||
<text class="tip-text">↑ 上滑加载更多</text>
|
||
</view>
|
||
<view class="load-tip" v-else>
|
||
<text class="completed-text">已加载全部消息</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 聊天消息列表 -->
|
||
<view class="message-list" @click="closeMorePanel">
|
||
<view
|
||
v-for="(message, index) in messageList"
|
||
:key="message.ID"
|
||
:id="`msg-${message.ID}`"
|
||
class="message-item"
|
||
:class="{
|
||
'message-right': message.flow === 'out',
|
||
'message-left': message.flow === 'in',
|
||
}"
|
||
>
|
||
<!-- 时间分割线 -->
|
||
<view
|
||
v-if="shouldShowTime(message, index, messageList)"
|
||
class="time-divider"
|
||
>
|
||
<text class="time-text">{{ formatTime(message.lastTime) }}</text>
|
||
</view>
|
||
|
||
<view v-if="isSystemMessage(message)">
|
||
<SystemMessage :message="message" />
|
||
</view>
|
||
|
||
<!-- 消息内容 -->
|
||
<view v-else class="message-content">
|
||
<image
|
||
v-if="message.flow === 'in'"
|
||
class="doctor-msg-avatar"
|
||
:src="getUserAvatar(message.from)"
|
||
mode="aspectFill"
|
||
/>
|
||
<image
|
||
v-if="message.flow === 'out'"
|
||
class="user-msg-avatar"
|
||
:src="getUserAvatar(message.from)"
|
||
mode="aspectFill"
|
||
/>
|
||
|
||
<!-- 消息内容区域 -->
|
||
<view class="message-bubble-container">
|
||
<!-- 用户名显示 -->
|
||
<view
|
||
class="username-label"
|
||
:class="{
|
||
left: message.flow === 'in',
|
||
right: message.flow === 'out',
|
||
}"
|
||
>
|
||
<text class="username-text">{{
|
||
chatMember[message.from]?.name
|
||
}}</text>
|
||
</view>
|
||
|
||
<view class="message-bubble" :class="getBubbleClass(message)">
|
||
<!-- 消息内容 -->
|
||
<MessageTypes
|
||
:message="message"
|
||
:formatTime="formatTime"
|
||
:playingVoiceId="playingVoiceId"
|
||
@playVoice="playVoice"
|
||
@previewImage="previewImage"
|
||
@viewDetail="(message) => handleViewDetail(message)"
|
||
/>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 发送状态 -->
|
||
<view v-if="message.flow === 'out'" class="message-status">
|
||
<text
|
||
v-if="message.status === 'failed'"
|
||
class="status-text failed"
|
||
>发送失败</text
|
||
>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 接受问诊组件 -->
|
||
<ConsultAccept
|
||
v-if="showConsultAccept"
|
||
@accept="handleAcceptConsult"
|
||
@reject="handleRejectConsult"
|
||
/>
|
||
|
||
<!-- 拒绝原因对话框 -->
|
||
<RejectReasonModal
|
||
:visible="showRejectReasonModal"
|
||
@confirm="handleRejectReasonConfirm"
|
||
@cancel="handleRejectReasonCancel"
|
||
/>
|
||
|
||
<!-- AI助手按钮组 -->
|
||
<AIAssistantButtons
|
||
v-if="
|
||
!isEvaluationPopupOpen &&
|
||
!showConsultAccept &&
|
||
orderStatus === 'processing'
|
||
"
|
||
ref="aiAssistantRef"
|
||
:groupId="groupId"
|
||
:patientAccountId="chatInfo.userID || ''"
|
||
:patientId="patientId"
|
||
:corpId="corpId"
|
||
@streamText="handleStreamText"
|
||
@clearInput="handleClearInput"
|
||
@generatingStateChange="handleGeneratingStateChange"
|
||
/>
|
||
|
||
<!-- 聊天输入组件 -->
|
||
<ChatInput
|
||
v-if="!isEvaluationPopupOpen && !showConsultAccept"
|
||
ref="chatInputRef"
|
||
:timChatManager="timChatManager"
|
||
:formatTime="formatTime"
|
||
:groupId="
|
||
chatInfo.conversationID
|
||
? chatInfo.conversationID.replace('GROUP', '')
|
||
: ''
|
||
"
|
||
:userId="openid"
|
||
:teamId="teamId"
|
||
:patientId="patientId"
|
||
:corpId="corpId"
|
||
:patientInfo="patientInfo"
|
||
:orderStatus="orderStatus"
|
||
:isGenerating="isGenerating"
|
||
:keyboardHeight="keyboardHeight"
|
||
@scrollToBottom="() => scrollToBottom(true)"
|
||
@messageSent="() => scrollToBottom(true)"
|
||
@endConsult="handleEndConsult"
|
||
@openConsult="handleOpenConsult"
|
||
/>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onUnmounted, nextTick, watch, computed } from "vue";
|
||
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
|
||
import { storeToRefs } from "pinia";
|
||
import useAccountStore from "@/store/account.js";
|
||
import { globalTimChatManager } from "@/utils/tim-chat.js";
|
||
import { handleFollowUpMessages } from "@/utils/send-message-helper";
|
||
import {
|
||
startIMMonitoring,
|
||
stopIMMonitoring,
|
||
} from "@/utils/im-status-manager.js";
|
||
import {
|
||
getVoiceUrl,
|
||
validateVoiceUrl,
|
||
createAudioContext,
|
||
showMessage,
|
||
formatTime,
|
||
shouldShowTime,
|
||
previewImage,
|
||
throttle,
|
||
clearMessageCache,
|
||
handleViewDetail,
|
||
checkIMConnectionStatus,
|
||
} from "@/utils/chat-utils.js";
|
||
import api from "@/utils/api.js";
|
||
import useGroupChat from "./hooks/use-group-chat";
|
||
import MessageTypes from "./components/message-types.vue";
|
||
import ChatInput from "./components/chat-input.vue";
|
||
import SystemMessage from "./components/system-message.vue";
|
||
import ConsultAccept from "./components/consult-accept.vue";
|
||
import RejectReasonModal from "./components/reject-reason-modal.vue";
|
||
import AIAssistantButtons from "./components/ai-assistant-buttons.vue";
|
||
|
||
const timChatManager = globalTimChatManager;
|
||
|
||
const PENDING_FOLLOWUP_SEND_STORAGE_KEY = "ykt_followup_pending_send";
|
||
const pendingFollowUpSendConsumed = ref(false);
|
||
const initialMessageListLoaded = ref(false);
|
||
const isCallbacksInitialized = ref(false); // 标记回调是否已初始化
|
||
|
||
function normalizeGroupId(v) {
|
||
const s = String(v || "").trim();
|
||
if (!s) return "";
|
||
return s.startsWith("GROUP") ? s.slice(5) : s;
|
||
}
|
||
|
||
async function tryConsumePendingFollowUpSend() {
|
||
if (pendingFollowUpSendConsumed.value) return;
|
||
|
||
// 等待 IM 就绪与首屏消息加载完成,避免发送的本地消息被列表回调覆盖
|
||
if (!timChatManager?.isLoggedIn) return;
|
||
if (!initialMessageListLoaded.value) return;
|
||
|
||
const raw = uni.getStorageSync(PENDING_FOLLOWUP_SEND_STORAGE_KEY);
|
||
const payload = raw && typeof raw === "object" ? raw : null;
|
||
if (!payload) return;
|
||
|
||
const createdAt = Number(payload.createdAt || 0) || 0;
|
||
// 过期就清理,避免误发送
|
||
if (createdAt && Date.now() - createdAt > 5 * 60 * 1000) {
|
||
uni.removeStorageSync(PENDING_FOLLOWUP_SEND_STORAGE_KEY);
|
||
return;
|
||
}
|
||
|
||
const payloadConversationID = String(payload.conversationID || "");
|
||
const payloadGroupId = normalizeGroupId(payload.groupId || "");
|
||
const currentConversationID = String(chatInfo.value.conversationID || "");
|
||
const currentGroupId = normalizeGroupId(groupId.value || "");
|
||
|
||
// 必须匹配当前会话,才允许发送
|
||
if (!payloadConversationID || payloadConversationID !== currentConversationID)
|
||
return;
|
||
if (payloadGroupId && currentGroupId && payloadGroupId !== currentGroupId)
|
||
return;
|
||
|
||
const messages = Array.isArray(payload.messages) ? payload.messages : [];
|
||
const context =
|
||
payload.context && typeof payload.context === "object"
|
||
? payload.context
|
||
: {};
|
||
if (!messages.length) {
|
||
uni.removeStorageSync(PENDING_FOLLOWUP_SEND_STORAGE_KEY);
|
||
return;
|
||
}
|
||
|
||
pendingFollowUpSendConsumed.value = true;
|
||
// 先清理再发,避免页面重复初始化导致二次发送
|
||
uni.removeStorageSync(PENDING_FOLLOWUP_SEND_STORAGE_KEY);
|
||
|
||
await nextTick();
|
||
const ok = await handleFollowUpMessages(messages, context);
|
||
if (ok) {
|
||
uni.showToast({ title: "消息已发送", icon: "success" });
|
||
} else {
|
||
uni.showToast({ title: "部分发送失败", icon: "none" });
|
||
}
|
||
}
|
||
|
||
// 获取环境变量
|
||
const env = __VITE_ENV__;
|
||
const corpId = env.MP_CORP_ID || "";
|
||
|
||
// 获取登录状态
|
||
const { account, openid, isIMInitialized } = storeToRefs(useAccountStore());
|
||
const { initIMAfterLogin } = useAccountStore();
|
||
|
||
// 聊天输入组件引用
|
||
const chatInputRef = ref(null);
|
||
const aiAssistantRef = ref(null);
|
||
const isGenerating = ref(false);
|
||
|
||
const groupId = ref("");
|
||
const { chatMember, getGroupInfo, getUserAvatar } = useGroupChat(groupId);
|
||
|
||
// 动态设置导航栏标题
|
||
const updateNavigationTitle = (title = "群聊") => {
|
||
uni.setNavigationBarTitle({
|
||
title: title,
|
||
});
|
||
};
|
||
|
||
// 聊天信息
|
||
const chatInfo = ref({
|
||
conversationID: "",
|
||
userID: "",
|
||
avatar: "/static/home/avatar.svg",
|
||
});
|
||
|
||
// 键盘高度
|
||
const keyboardHeight = ref(0);
|
||
|
||
// 评价弹窗状态
|
||
const isEvaluationPopupOpen = ref(false);
|
||
|
||
// 订单状态
|
||
const orderStatus = ref("");
|
||
|
||
// 患者信息
|
||
const patientInfo = ref({
|
||
name: "",
|
||
sex: "",
|
||
age: "",
|
||
mobile: "",
|
||
});
|
||
|
||
// 患者ID
|
||
const patientId = ref("");
|
||
const teamId = ref("");
|
||
|
||
// 计算弹框显示状态 - 只有 pending 状态才显示接受问诊组件
|
||
const showConsultAccept = computed(() => orderStatus.value === "pending");
|
||
|
||
// 拒绝原因对话框状态
|
||
const showRejectReasonModal = ref(false);
|
||
// 消息列表相关状态
|
||
const messageList = ref([]);
|
||
const isLoading = ref(false);
|
||
const scrollIntoView = ref("");
|
||
// 分页加载相关状态
|
||
const isLoadingMore = ref(false);
|
||
const isCompleted = ref(false);
|
||
const lastFirstMessageId = ref("");
|
||
|
||
// 判断是否为系统消息
|
||
function isSystemMessage(message) {
|
||
if (message.type !== "TIMCustomElem") {
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
// 检查 payload.data 是否包含系统消息标记
|
||
if (message.payload?.data) {
|
||
const data =
|
||
typeof message.payload.data === "string"
|
||
? JSON.parse(message.payload.data)
|
||
: message.payload.data;
|
||
|
||
// 检查是否为系统消息类型
|
||
if (data.type === "system_message") {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// 检查 description 是否为系统消息标记
|
||
if (message.payload?.description === "系统消息标记") {
|
||
return true;
|
||
}
|
||
|
||
// 兼容旧的系统消息格式
|
||
if (message.payload?.description === "SYSTEM_NOTIFICATION") {
|
||
return true;
|
||
}
|
||
} catch (error) {
|
||
console.error("判断系统消息失败:", error);
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
// 获取群组订单状态
|
||
const fetchGroupOrderStatus = async () => {
|
||
try {
|
||
const result = await api("getGroupListByGroupId", {
|
||
groupId: groupId.value,
|
||
});
|
||
|
||
if (result.success && result.data) {
|
||
orderStatus.value = result.data.orderStatus || "";
|
||
|
||
// 更新导航栏标题为团队名称
|
||
const teamName = result.data.team?.name || "群聊";
|
||
updateNavigationTitle(teamName);
|
||
|
||
teamId.value =
|
||
result.data.teamId ||
|
||
result.data.team?.teamId ||
|
||
result.data.team?._id ||
|
||
"";
|
||
|
||
// 更新患者信息
|
||
if (result.data.patient) {
|
||
patientInfo.value = {
|
||
name: result.data.patient.name || "",
|
||
sex: result.data.patient.sex || "",
|
||
age: result.data.patient.age || "",
|
||
mobile: result.data.patient.mobile || "",
|
||
};
|
||
}
|
||
// 更新患者ID
|
||
if (result.data.patientId) {
|
||
patientId.value = result.data.patientId.toString();
|
||
}
|
||
|
||
console.log("获取群组订单状态:", {
|
||
orderStatus: orderStatus.value,
|
||
teamName: teamName,
|
||
patientInfo: patientInfo.value,
|
||
groupId: groupId.value,
|
||
});
|
||
} else {
|
||
console.error("获取群组订单状态失败:", result.message);
|
||
}
|
||
} catch (error) {
|
||
console.error("获取群组订单状态异常:", error);
|
||
}
|
||
};
|
||
|
||
// 获取消息气泡样式类
|
||
function getBubbleClass(message) {
|
||
// 图片消息不需要气泡背景
|
||
if (message.type === "TIMImageElem") {
|
||
return "image-bubble";
|
||
}
|
||
if (message.type === "TIMCustomElem") {
|
||
return message.flow === "out" ? "" : "";
|
||
}
|
||
return message.flow === "out" ? "user-bubble" : "doctor-bubble";
|
||
}
|
||
|
||
// 页面加载
|
||
onLoad((options) => {
|
||
const decodeQueryValue = (v) => {
|
||
const s = typeof v === "string" ? v : String(v || "");
|
||
if (!s) return "";
|
||
try {
|
||
return decodeURIComponent(s);
|
||
} catch (e) {
|
||
return s;
|
||
}
|
||
};
|
||
|
||
const rawGroupId = decodeQueryValue(options.groupID || "");
|
||
groupId.value = rawGroupId.startsWith("GROUP")
|
||
? rawGroupId.replace(/^GROUP/, "")
|
||
: rawGroupId;
|
||
messageList.value = [];
|
||
isLoading.value = false;
|
||
if (options.conversationID) {
|
||
const cid = decodeQueryValue(options.conversationID);
|
||
chatInfo.value.conversationID = cid;
|
||
timChatManager.setConversationID(cid);
|
||
console.log("设置当前会话ID:", cid);
|
||
}
|
||
if (options.userID) {
|
||
chatInfo.value.userID = decodeQueryValue(options.userID);
|
||
}
|
||
|
||
// 监听键盘高度变化
|
||
uni.onKeyboardHeightChange((res) => {
|
||
console.log("键盘高度变化:", res.height);
|
||
const oldHeight = keyboardHeight.value;
|
||
keyboardHeight.value = res.height;
|
||
|
||
// 键盘弹出时(从0变为非0),自动滚动到底部
|
||
if (oldHeight === 0 && res.height > 0) {
|
||
nextTick(() => {
|
||
setTimeout(() => {
|
||
scrollToBottom(true);
|
||
}, 100);
|
||
});
|
||
}
|
||
});
|
||
|
||
checkLoginAndInitTIM();
|
||
updateNavigationTitle();
|
||
});
|
||
|
||
// 检查登录状态并初始化IM
|
||
const checkLoginAndInitTIM = async () => {
|
||
if (!isIMInitialized.value) {
|
||
uni.showLoading({
|
||
title: "连接中...",
|
||
});
|
||
const success = await initIMAfterLogin();
|
||
uni.hideLoading();
|
||
// if (!success) {
|
||
// uni.showToast({
|
||
// title: "IM连接失败,请重试",
|
||
// icon: "none",
|
||
// });
|
||
// return;
|
||
// }
|
||
} else if (!timChatManager.isLoggedIn) {
|
||
uni.showLoading({
|
||
title: "重连中...",
|
||
});
|
||
const reconnected = await timChatManager.ensureIMConnection();
|
||
uni.hideLoading();
|
||
if (!reconnected) {
|
||
return;
|
||
}
|
||
}
|
||
initTIMCallbacks();
|
||
};
|
||
|
||
// 初始化IM回调函数
|
||
const initTIMCallbacks = async () => {
|
||
// 标记回调已初始化
|
||
isCallbacksInitialized.value = true;
|
||
|
||
timChatManager.setCallback("onSDKReady", () => {
|
||
if (messageList.value.length === 0 && !isLoading.value) {
|
||
loadMessageList();
|
||
}
|
||
});
|
||
|
||
timChatManager.setCallback("onSDKNotReady", () => {});
|
||
|
||
timChatManager.setCallback("onMessageReceived", (message) => {
|
||
console.log("页面收到消息:", {
|
||
messageID: message.ID,
|
||
conversationID: message.conversationID,
|
||
currentConversationID: chatInfo.value.conversationID,
|
||
type: message.type,
|
||
flow: message.flow,
|
||
});
|
||
|
||
// 验证消息属于当前群聊
|
||
if (message.conversationID !== chatInfo.value.conversationID) {
|
||
console.log("⚠️ 消息不属于当前群聊,已过滤");
|
||
return;
|
||
}
|
||
|
||
// 检查消息是否已存在
|
||
const existingMessage = messageList.value.find(
|
||
(msg) => msg.ID === message.ID
|
||
);
|
||
if (!existingMessage) {
|
||
messageList.value.push(message);
|
||
console.log("✓ 添加消息到列表,当前消息数量:", messageList.value.length);
|
||
|
||
// 如果是系统消息,重新获取订单状态
|
||
if (isSystemMessage(message)) {
|
||
fetchGroupOrderStatus();
|
||
}
|
||
|
||
// 立即滚动到底部,不使用延迟
|
||
nextTick(() => {
|
||
scrollToBottom(true);
|
||
});
|
||
|
||
// 立即标记会话为已读,确保未读数为0
|
||
if (
|
||
timChatManager.tim &&
|
||
timChatManager.isLoggedIn &&
|
||
chatInfo.value.conversationID
|
||
) {
|
||
timChatManager.tim
|
||
.setMessageRead({
|
||
conversationID: chatInfo.value.conversationID,
|
||
})
|
||
.then(() => {
|
||
console.log("✓ 收到新消息后已标记为已读");
|
||
})
|
||
.catch((error) => {
|
||
console.error("✗ 标记已读失败:", error);
|
||
});
|
||
}
|
||
}
|
||
});
|
||
|
||
timChatManager.setCallback("onMessageSent", (data) => {
|
||
const { messageID, status } = data;
|
||
const message = messageList.value.find((msg) => msg.ID === messageID);
|
||
if (message) {
|
||
message.status = status;
|
||
console.log("更新消息状态:", messageID, status);
|
||
}
|
||
});
|
||
|
||
timChatManager.setCallback("onMessageListLoaded", (data) => {
|
||
console.log("【onMessageListLoaded】收到消息列表回调");
|
||
uni.hideLoading();
|
||
|
||
let messages = [];
|
||
if (typeof data === "object" && data.messages) {
|
||
messages = data.messages;
|
||
} else {
|
||
messages = data;
|
||
}
|
||
isLoading.value = false;
|
||
|
||
// 去重处理 + 严格过滤当前群聊的消息
|
||
const uniqueMessages = [];
|
||
const seenIds = new Set();
|
||
|
||
messages.forEach((message) => {
|
||
const belongsToCurrentConversation =
|
||
message.conversationID === chatInfo.value.conversationID;
|
||
|
||
if (
|
||
message.ID &&
|
||
!seenIds.has(message.ID) &&
|
||
belongsToCurrentConversation
|
||
) {
|
||
seenIds.add(message.ID);
|
||
uniqueMessages.push(message);
|
||
}
|
||
});
|
||
|
||
// 合并现有 messageList,避免首屏加载覆盖刚发送的本地消息
|
||
const merged = [];
|
||
const mergedSeen = new Set();
|
||
const existing = Array.isArray(messageList.value) ? messageList.value : [];
|
||
|
||
for (const m of existing) {
|
||
if (!m || !m.ID) continue;
|
||
if (m.conversationID !== chatInfo.value.conversationID) continue;
|
||
if (mergedSeen.has(m.ID)) continue;
|
||
mergedSeen.add(m.ID);
|
||
merged.push(m);
|
||
}
|
||
for (const m of uniqueMessages) {
|
||
if (!m || !m.ID) continue;
|
||
if (mergedSeen.has(m.ID)) continue;
|
||
mergedSeen.add(m.ID);
|
||
merged.push(m);
|
||
}
|
||
merged.sort((a, b) => {
|
||
const ta = Number(a?.lastTime || a?.time || 0) || 0;
|
||
const tb = Number(b?.lastTime || b?.time || 0) || 0;
|
||
return ta - tb;
|
||
});
|
||
|
||
messageList.value = merged;
|
||
console.log(
|
||
"消息列表已更新,原始",
|
||
messages.length,
|
||
"条,过滤后",
|
||
messageList.value.length,
|
||
"条消息"
|
||
);
|
||
|
||
if (!data.isPullUp && !data.isRefresh) {
|
||
initialMessageListLoaded.value = true;
|
||
// 首屏加载完成后再尝试消费待发送 payload
|
||
tryConsumePendingFollowUpSend();
|
||
}
|
||
|
||
isCompleted.value = data.isCompleted || false;
|
||
isLoadingMore.value = false;
|
||
|
||
nextTick(() => {
|
||
if (data.isRefresh) {
|
||
console.log("后台刷新完成,保持当前滚动位置");
|
||
return;
|
||
}
|
||
|
||
if (data.isPullUp && lastFirstMessageId.value) {
|
||
console.log(
|
||
"上拉加载完成,定位到加载前的第一条消息:",
|
||
lastFirstMessageId.value
|
||
);
|
||
setTimeout(() => {
|
||
scrollIntoView.value = `msg-${lastFirstMessageId.value}`;
|
||
lastFirstMessageId.value = "";
|
||
}, 100);
|
||
return;
|
||
}
|
||
|
||
if (!data.isPullUp) {
|
||
setTimeout(() => {
|
||
scrollToBottom();
|
||
setTimeout(() => {
|
||
scrollToBottom();
|
||
}, 100);
|
||
}, 200);
|
||
}
|
||
});
|
||
});
|
||
|
||
timChatManager.setCallback("onError", (error) => {
|
||
console.error("TIM错误:", error);
|
||
uni.showToast({
|
||
title: error,
|
||
icon: "none",
|
||
});
|
||
});
|
||
|
||
nextTick(() => {
|
||
if (timChatManager.tim && timChatManager.isLoggedIn) {
|
||
setTimeout(() => {
|
||
loadMessageList();
|
||
}, 50);
|
||
} else if (timChatManager.tim) {
|
||
let checkCount = 0;
|
||
const checkInterval = setInterval(() => {
|
||
checkCount++;
|
||
if (timChatManager.isLoggedIn) {
|
||
clearInterval(checkInterval);
|
||
loadMessageList();
|
||
} else if (checkCount > 10) {
|
||
clearInterval(checkInterval);
|
||
showMessage("IM登录超时,请重新进入");
|
||
}
|
||
}, 1000);
|
||
}
|
||
});
|
||
};
|
||
|
||
// 加载消息列表
|
||
const loadMessageList = async () => {
|
||
if (isLoading.value) {
|
||
console.log("正在加载中,跳过重复加载");
|
||
return;
|
||
}
|
||
|
||
console.log(
|
||
"【loadMessageList】开始加载消息,会话ID:",
|
||
chatInfo.value.conversationID
|
||
);
|
||
isLoading.value = true;
|
||
|
||
uni.showLoading({
|
||
title: "加载中...",
|
||
mask: false,
|
||
});
|
||
|
||
// 获取群组订单状态
|
||
await fetchGroupOrderStatus();
|
||
|
||
timChatManager.enterConversation(chatInfo.value.conversationID || "test1");
|
||
|
||
// 若从病历回访记录带入待发送内容,则进入会话后自动发送
|
||
tryConsumePendingFollowUpSend();
|
||
|
||
// 标记会话为已读 - 确保清空未读数
|
||
if (
|
||
timChatManager.tim &&
|
||
timChatManager.isLoggedIn &&
|
||
chatInfo.value.conversationID
|
||
) {
|
||
console.log("标记会话为已读:", chatInfo.value.conversationID);
|
||
timChatManager.tim
|
||
.setMessageRead({
|
||
conversationID: chatInfo.value.conversationID,
|
||
})
|
||
.then(() => {
|
||
console.log("✓ 会话已标记为已读:", chatInfo.value.conversationID);
|
||
})
|
||
.catch((error) => {
|
||
console.error("✗ 标记会话已读失败:", error);
|
||
});
|
||
}
|
||
};
|
||
|
||
// 语音播放相关
|
||
let currentAudioContext = null;
|
||
const playingVoiceId = ref(null);
|
||
|
||
// 播放语音
|
||
const playVoice = (message) => {
|
||
if (playingVoiceId.value === message.ID && currentAudioContext) {
|
||
currentAudioContext.stop();
|
||
currentAudioContext.destroy();
|
||
currentAudioContext = null;
|
||
playingVoiceId.value = null;
|
||
return;
|
||
}
|
||
|
||
if (currentAudioContext) {
|
||
currentAudioContext.stop();
|
||
currentAudioContext.destroy();
|
||
currentAudioContext = null;
|
||
}
|
||
|
||
const voiceUrl = getVoiceUrl(message);
|
||
if (!validateVoiceUrl(voiceUrl)) return;
|
||
|
||
playingVoiceId.value = message.ID;
|
||
|
||
currentAudioContext = createAudioContext(voiceUrl);
|
||
currentAudioContext.onEnded(() => {
|
||
currentAudioContext = null;
|
||
playingVoiceId.value = null;
|
||
});
|
||
currentAudioContext.onError(() => {
|
||
currentAudioContext = null;
|
||
playingVoiceId.value = null;
|
||
});
|
||
currentAudioContext.play();
|
||
};
|
||
|
||
// 滚动到底部
|
||
const scrollToBottom = (immediate = false) => {
|
||
if (messageList.value.length > 0) {
|
||
const lastMessage = messageList.value[messageList.value.length - 1];
|
||
const targetId = `msg-${lastMessage.ID}`;
|
||
|
||
if (immediate) {
|
||
// 立即滚动:先清空再设置,触发滚动
|
||
scrollIntoView.value = "";
|
||
nextTick(() => {
|
||
scrollIntoView.value = targetId;
|
||
});
|
||
} else {
|
||
// 正常滚动,使用短延迟确保DOM更新
|
||
scrollIntoView.value = "";
|
||
setTimeout(() => {
|
||
scrollIntoView.value = targetId;
|
||
}, 50);
|
||
}
|
||
}
|
||
};
|
||
|
||
// 关闭功能栏
|
||
const closeMorePanel = () => {
|
||
uni.$emit("closeMorePanel");
|
||
};
|
||
|
||
// 滚动事件
|
||
const onScroll = throttle((e) => {
|
||
// 滚动处理
|
||
}, 100);
|
||
|
||
// 处理上滑加载更多
|
||
const handleScrollToUpper = async () => {
|
||
console.log("【handleScrollToUpper】触发上滑事件,准备加载更多");
|
||
console.log(
|
||
" 当前状态: isLoadingMore=",
|
||
isLoadingMore.value,
|
||
"isCompleted=",
|
||
isCompleted.value
|
||
);
|
||
|
||
if (isLoadingMore.value || isCompleted.value) {
|
||
console.log(
|
||
" ⏭️ 跳过加载:isLoadingMore=",
|
||
isLoadingMore.value,
|
||
"isCompleted=",
|
||
isCompleted.value
|
||
);
|
||
return;
|
||
}
|
||
|
||
if (messageList.value.length < 15) {
|
||
console.log(" ⏭️ 消息数量不足15条,跳过加载更多");
|
||
return;
|
||
}
|
||
|
||
if (messageList.value.length > 0) {
|
||
lastFirstMessageId.value = messageList.value[0].ID;
|
||
console.log(" 📍 记录当前第一条消息ID:", lastFirstMessageId.value);
|
||
}
|
||
|
||
isLoadingMore.value = true;
|
||
|
||
try {
|
||
console.log(" 📡 调用 timChatManager.loadMoreMessages()");
|
||
const result = await timChatManager.loadMoreMessages();
|
||
|
||
console.log(" 📥 加载结果:", result);
|
||
|
||
if (result.success) {
|
||
console.log(` ✅ 加载更多成功,新增 ${result.count} 条消息`);
|
||
} else {
|
||
console.log(" ⚠️ 加载失败:", result.message || result.error);
|
||
if (result.message === "已加载全部消息") {
|
||
console.log(" ✅ 已加载全部消息");
|
||
isCompleted.value = true;
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error(" ❌ 加载更多失败:", error);
|
||
uni.showToast({
|
||
title: "加载失败,请重试",
|
||
icon: "none",
|
||
});
|
||
} finally {
|
||
isLoadingMore.value = false;
|
||
console.log(" 🏁 加载完成,isLoadingMore 设置为 false");
|
||
}
|
||
};
|
||
|
||
// 页面显示
|
||
onShow(() => {
|
||
if (!account.value || !openid.value) {
|
||
uni.redirectTo({
|
||
url: "/pages-center/login/login",
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (!isIMInitialized.value) {
|
||
checkLoginAndInitTIM();
|
||
} else if (timChatManager.tim && !timChatManager.isLoggedIn) {
|
||
// IM未登录,尝试重连
|
||
timChatManager.ensureIMConnection().then(() => {
|
||
// 重连成功后重新注册回调
|
||
if (timChatManager.isLoggedIn && chatInfo.value.conversationID) {
|
||
console.log("✓ 重连成功,重新注册消息监听回调");
|
||
initTIMCallbacks();
|
||
timChatManager.setConversationID(chatInfo.value.conversationID);
|
||
}
|
||
});
|
||
} else if (
|
||
timChatManager.tim &&
|
||
timChatManager.isLoggedIn &&
|
||
chatInfo.value.conversationID
|
||
) {
|
||
// IM已登录,只需要重新设置会话ID,不重新注册回调(避免影响正在发送的消息)
|
||
console.log("✓ 页面显示,重新设置当前会话ID");
|
||
timChatManager.setConversationID(chatInfo.value.conversationID);
|
||
|
||
// 如果回调未初始化,才进行初始化
|
||
if (!isCallbacksInitialized.value) {
|
||
console.log("✓ 回调未初始化,进行初始化");
|
||
initTIMCallbacks();
|
||
}
|
||
|
||
// 标记会话为已读
|
||
if (timChatManager.tim && timChatManager.isLoggedIn) {
|
||
timChatManager.tim
|
||
.setMessageRead({
|
||
conversationID: chatInfo.value.conversationID,
|
||
})
|
||
.then(() => {
|
||
console.log("✓ 页面显示时已标记会话为已读");
|
||
})
|
||
.catch((error) => {
|
||
console.error("✗ 标记会话已读失败:", error);
|
||
});
|
||
}
|
||
|
||
startIMMonitoring(30000);
|
||
}
|
||
|
||
// 监听回访任务发送事件
|
||
uni.$on("send-followup-message", handleSendFollowUpMessage);
|
||
});
|
||
|
||
// 处理发送回访任务消息
|
||
const handleSendFollowUpMessage = async (data) => {
|
||
try {
|
||
if (chatInputRef.value) {
|
||
// 将回访计划内容设置到输入框
|
||
chatInputRef.value.setInputText(data.content);
|
||
|
||
// 延迟后自动发送
|
||
setTimeout(() => {
|
||
if (chatInputRef.value) {
|
||
chatInputRef.value.sendTextMessageFromPhrase(data.content);
|
||
}
|
||
}, 100);
|
||
}
|
||
} catch (error) {
|
||
console.error("发送回访任务消息失败:", error);
|
||
uni.showToast({
|
||
title: "发送失败,请重试",
|
||
icon: "none",
|
||
});
|
||
}
|
||
};
|
||
|
||
// 页面隐藏
|
||
onHide(() => {
|
||
stopIMMonitoring();
|
||
// 清空当前会话ID,避免离开页面后收到的消息被错误标记为已读
|
||
timChatManager.currentConversationID = null;
|
||
console.log("✓ 页面隐藏,已清空当前会话ID");
|
||
});
|
||
|
||
const sendCommonPhrase = (content) => {
|
||
if (chatInputRef.value) {
|
||
// 覆盖输入框内容,而不是直接发送
|
||
chatInputRef.value.setInputText(content);
|
||
}
|
||
};
|
||
|
||
// 处理流式文本输入
|
||
const handleStreamText = (char) => {
|
||
if (chatInputRef.value) {
|
||
chatInputRef.value.appendStreamText(char);
|
||
}
|
||
};
|
||
|
||
// 处理清空输入框
|
||
const handleClearInput = () => {
|
||
if (chatInputRef.value) {
|
||
chatInputRef.value.clearInputText();
|
||
}
|
||
};
|
||
|
||
// 处理生成状态变化
|
||
const handleGeneratingStateChange = (generating) => {
|
||
isGenerating.value = generating;
|
||
};
|
||
|
||
// 暴露方法给常用语页面调用
|
||
defineExpose({
|
||
sendCommonPhrase,
|
||
});
|
||
|
||
// 处理接受问诊
|
||
const handleAcceptConsult = async () => {
|
||
try {
|
||
uni.showLoading({
|
||
title: "处理中...",
|
||
});
|
||
|
||
// 调用后端接口接受问诊
|
||
const result = await api("acceptConsultation", {
|
||
groupId: groupId.value,
|
||
adminAccount: account.value?.userId || "",
|
||
extraData: {
|
||
acceptedBy: account.value?.userId || "",
|
||
acceptedByName: account.value?.name || "医生",
|
||
},
|
||
});
|
||
|
||
uni.hideLoading();
|
||
|
||
if (result.success) {
|
||
// 重新获取订单状态
|
||
await fetchGroupOrderStatus();
|
||
uni.showToast({
|
||
title: "已接受问诊",
|
||
icon: "success",
|
||
});
|
||
} else {
|
||
throw new Error(result.message || "操作失败");
|
||
}
|
||
} catch (error) {
|
||
console.error("接受问诊失败:", error);
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: error.message || "操作失败",
|
||
icon: "none",
|
||
});
|
||
}
|
||
};
|
||
|
||
// 处理拒绝问诊
|
||
const handleRejectConsult = () => {
|
||
// 显示拒绝原因选择对话框
|
||
showRejectReasonModal.value = true;
|
||
};
|
||
|
||
// 处理拒绝原因确认
|
||
const handleRejectReasonConfirm = async (reason) => {
|
||
try {
|
||
showRejectReasonModal.value = false;
|
||
|
||
uni.showLoading({
|
||
title: "处理中...",
|
||
});
|
||
|
||
// 获取医生信息
|
||
const memberName = account.value?.name || "医生";
|
||
|
||
// 获取群组ID
|
||
const currentGroupId = chatInfo.value.conversationID.replace("GROUP", "");
|
||
|
||
// 调用后端接口发送拒绝消息
|
||
const result = await api("rejectConsultation", {
|
||
groupId: currentGroupId,
|
||
memberName,
|
||
reason,
|
||
});
|
||
|
||
uni.hideLoading();
|
||
|
||
if (result.success) {
|
||
// 重新获取订单状态
|
||
await fetchGroupOrderStatus();
|
||
uni.showToast({
|
||
title: "已拒绝问诊",
|
||
icon: "success",
|
||
});
|
||
} else {
|
||
throw new Error(result.message || "发送失败");
|
||
}
|
||
} catch (error) {
|
||
console.error("拒绝问诊失败:", error);
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: error.message || "操作失败",
|
||
icon: "none",
|
||
});
|
||
}
|
||
};
|
||
|
||
// 处理拒绝原因取消
|
||
const handleRejectReasonCancel = () => {
|
||
showRejectReasonModal.value = false;
|
||
};
|
||
|
||
// 处理查看患者详情
|
||
const handleViewPatientDetail = () => {
|
||
if (!patientId.value) {
|
||
uni.showToast({
|
||
title: "患者信息不完整",
|
||
icon: "none",
|
||
});
|
||
return;
|
||
}
|
||
|
||
uni.navigateTo({
|
||
url: `/pages/case/archive-detail?id=${patientId.value}`,
|
||
});
|
||
};
|
||
// 处理结束问诊
|
||
const handleEndConsult = async () => {
|
||
try {
|
||
uni.showLoading({
|
||
title: "处理中...",
|
||
});
|
||
// 调用结束问诊接口,直接传入 groupId
|
||
const result = await api("endConsultation", {
|
||
groupId: groupId.value,
|
||
adminAccount: account.value?.userId || "",
|
||
extraData: {
|
||
endBy: account.value?.userId || "",
|
||
endByName: account.value?.name || "医生",
|
||
endReason: "问诊完成",
|
||
},
|
||
});
|
||
uni.hideLoading();
|
||
if (result.success) {
|
||
uni.showToast({
|
||
title: "问诊已结束",
|
||
icon: "success",
|
||
});
|
||
} else {
|
||
throw new Error(result.message || "操作失败");
|
||
}
|
||
} catch (error) {
|
||
console.error("结束问诊失败:", error);
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: error.message || "操作失败",
|
||
icon: "none",
|
||
});
|
||
}
|
||
};
|
||
|
||
// 处理开启会话
|
||
const handleOpenConsult = async () => {
|
||
try {
|
||
uni.showLoading({
|
||
title: "处理中...",
|
||
});
|
||
// 调用开启会话接口
|
||
const result = await api("openConsultation", {
|
||
groupId: groupId.value,
|
||
adminAccount: account.value?.userId || "",
|
||
extraData: {
|
||
openedBy: account.value?.userId || "",
|
||
openedByName: account.value?.name || "医生",
|
||
openReason: "重新开启会话",
|
||
},
|
||
});
|
||
uni.hideLoading();
|
||
if (result.success) {
|
||
// 重新获取订单状态
|
||
await fetchGroupOrderStatus();
|
||
uni.showToast({
|
||
title: "会话已开启",
|
||
icon: "success",
|
||
});
|
||
} else {
|
||
throw new Error(result.message || "操作失败");
|
||
}
|
||
} catch (error) {
|
||
console.error("开启会话失败:", error);
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: error.message || "操作失败",
|
||
icon: "none",
|
||
});
|
||
}
|
||
};
|
||
|
||
// 页面卸载
|
||
onUnmounted(() => {
|
||
clearMessageCache();
|
||
|
||
// 移除键盘监听
|
||
uni.offKeyboardHeightChange();
|
||
|
||
timChatManager.setCallback("onSDKReady", null);
|
||
timChatManager.setCallback("onSDKNotReady", null);
|
||
timChatManager.setCallback("onMessageReceived", null);
|
||
timChatManager.setCallback("onMessageListLoaded", null);
|
||
timChatManager.setCallback("onError", null);
|
||
|
||
// 移除回访任务发送事件监听
|
||
uni.$off("send-followup-message", handleSendFollowUpMessage);
|
||
});
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
@import "./chat.scss";
|
||
</style>
|