994 lines
28 KiB
Vue
994 lines
28 KiB
Vue
|
|
<template>
|
|||
|
|
<view class="chat-page">
|
|||
|
|
<head-card
|
|||
|
|
v-if="chatRoomStatus.isWaiting"
|
|||
|
|
:doctorInfo="doctorInfo"
|
|||
|
|
:order="currentOrder"
|
|||
|
|
@addSymptomDescription="addSymptomDescription()"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<!-- 倒计时显示 -->
|
|||
|
|
<view
|
|||
|
|
v-if="chatRoomCountDown && showCountdown"
|
|||
|
|
class="consult-countdown-bar"
|
|||
|
|
>
|
|||
|
|
<text class="countdown-text">问诊剩余时间:{{ chatRoomCountDown }}</text>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 聊天消息区域 -->
|
|||
|
|
<scroll-view
|
|||
|
|
class="chat-content"
|
|||
|
|
:class="{ 'chat-content-compressed': waitingForDoctor }"
|
|||
|
|
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-icon">⏳</text>
|
|||
|
|
<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-if="isSpecialMessage(message)">
|
|||
|
|
<special-message
|
|||
|
|
:doctorInfo="doctorInfo"
|
|||
|
|
:message="message"
|
|||
|
|
@popupStatusChange="handleEvaluationPopupStatusChange"
|
|||
|
|
/>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 消息内容 -->
|
|||
|
|
<view v-else class="message-content">
|
|||
|
|
<!-- 头像 -->
|
|||
|
|
<image
|
|||
|
|
v-if="message.flow === 'in'"
|
|||
|
|
class="doctor-msg-avatar"
|
|||
|
|
:src="chatMember[message.from]?.avatar"
|
|||
|
|
mode="aspectFill"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<!-- 用户头像(右侧) -->
|
|||
|
|
<image
|
|||
|
|
v-if="message.flow === 'out'"
|
|||
|
|
class="user-msg-avatar"
|
|||
|
|
:src="chatMember[message.from]?.avatar"
|
|||
|
|
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>
|
|||
|
|
<!-- 评价组件 -->
|
|||
|
|
|
|||
|
|
<!-- 聊天输入组件 -->
|
|||
|
|
<ChatInput
|
|||
|
|
v-if="!isClosed && !isEvaluationPopupOpen && !showDescriptionPopup"
|
|||
|
|
:timChatManager="timChatManager"
|
|||
|
|
:chatRoomBusiness="chatRoomBusiness"
|
|||
|
|
:formatTime="formatTime"
|
|||
|
|
@scrollToBottom="scrollToBottom"
|
|||
|
|
@messageSent="scrollToBottom"
|
|||
|
|
/>
|
|||
|
|
<!-- 问诊功能栏组件 -->
|
|||
|
|
<ConsultationBar
|
|||
|
|
v-if="isClosed && doctorInfo && !isEvaluationPopupOpen"
|
|||
|
|
:doctor="doctorInfo"
|
|||
|
|
/>
|
|||
|
|
<!-- 病情描述弹窗 -->
|
|||
|
|
<DescriptionPopup
|
|||
|
|
:orderId="currentOrder ? currentOrder.orderId : ''"
|
|||
|
|
:accountId="account ? account.id : ''"
|
|||
|
|
:visible="showDescriptionPopup"
|
|||
|
|
:isIMMode="true"
|
|||
|
|
@close="showDescriptionPopup = false"
|
|||
|
|
@submit="handleSubmitDescription"
|
|||
|
|
/>
|
|||
|
|
</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 {
|
|||
|
|
startIMMonitoring,
|
|||
|
|
stopIMMonitoring,
|
|||
|
|
} from "@/utils/im-status-manager.js";
|
|||
|
|
import { updateLastMessageId } from "@/api/consult-order.js";
|
|||
|
|
import {
|
|||
|
|
getVoiceUrl,
|
|||
|
|
validateVoiceUrl,
|
|||
|
|
createAudioContext,
|
|||
|
|
showMessage,
|
|||
|
|
formatTime,
|
|||
|
|
shouldShowTime,
|
|||
|
|
previewImage,
|
|||
|
|
throttle,
|
|||
|
|
clearMessageCache,
|
|||
|
|
handleViewDetail,
|
|||
|
|
checkIMConnectionStatus,
|
|||
|
|
} from "@/utils/chat-utils.js";
|
|||
|
|
import useChatBusiness from "./hooks/use-chat-business";
|
|||
|
|
import DescriptionPopup from "./components/description-popup.vue";
|
|||
|
|
import HeadCard from "./components/head-card.vue";
|
|||
|
|
import MessageTypes from "./components/message-types.vue";
|
|||
|
|
import ChatInput from "./components/chat-input.vue";
|
|||
|
|
import ConsultationBar from "./components/consultation-bar.vue";
|
|||
|
|
import SystemMessage from "./components/system-message.vue";
|
|||
|
|
import SpecialMessage from "./components/special-message/index.vue";
|
|||
|
|
|
|||
|
|
const timChatManager = globalTimChatManager;
|
|||
|
|
|
|||
|
|
// 获取登录状态
|
|||
|
|
const { account, openid, isIMInitialized } = storeToRefs(useAccountStore());
|
|||
|
|
const { initIMAfterLogin } = useAccountStore();
|
|||
|
|
|
|||
|
|
const groupId = ref("");
|
|||
|
|
const {
|
|||
|
|
chatRoomBusiness,
|
|||
|
|
getChatBusiness,
|
|||
|
|
currentOrder,
|
|||
|
|
chatRoomStatus,
|
|||
|
|
getCurrentOrder,
|
|||
|
|
chatRoomCountDown,
|
|||
|
|
showCountdown,
|
|||
|
|
doctorInfo,
|
|||
|
|
isClosed,
|
|||
|
|
chatMember,
|
|||
|
|
} = useChatBusiness(groupId);
|
|||
|
|
|
|||
|
|
// 动态设置导航栏标题
|
|||
|
|
const updateNavigationTitle = () => {
|
|||
|
|
const doctorName =
|
|||
|
|
chatRoomBusiness.value && chatRoomBusiness.value.doctorName
|
|||
|
|
? chatRoomBusiness.value.doctorName
|
|||
|
|
: "";
|
|||
|
|
const title = doctorName ? `${doctorName}医生云工作室` : "问诊咨询";
|
|||
|
|
uni.setNavigationBarTitle({
|
|||
|
|
title: title,
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 聊天信息
|
|||
|
|
const chatInfo = ref({
|
|||
|
|
conversationID: "",
|
|||
|
|
userID: "",
|
|||
|
|
avatar: "/static/home/doctor.png",
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 评价弹窗状态
|
|||
|
|
const isEvaluationPopupOpen = ref(false);
|
|||
|
|
|
|||
|
|
// 医生名字
|
|||
|
|
const doctorName = ref("");
|
|||
|
|
const showDescriptionPopup = ref(false);
|
|||
|
|
// 用户头像
|
|||
|
|
const userAvatar = ref("/static/center/user-avatar.png");
|
|||
|
|
|
|||
|
|
// 消息列表相关状态
|
|||
|
|
const messageList = ref([]);
|
|||
|
|
const isLoading = ref(false);
|
|||
|
|
const scrollIntoView = ref("");
|
|||
|
|
|
|||
|
|
// 分页加载相关状态
|
|||
|
|
const isLoadingMore = ref(false);
|
|||
|
|
const isCompleted = ref(false);
|
|||
|
|
const lastFirstMessageId = ref(""); // 记录加载前的第一条消息ID,用于加载后定位
|
|||
|
|
|
|||
|
|
// 问诊相关状态
|
|||
|
|
const consultationEnded = ref(false);
|
|||
|
|
const waitingForDoctor = ref(false); // 是否等待接诊
|
|||
|
|
const consultationEndTime = ref(null); // 问诊结束时间戳
|
|||
|
|
|
|||
|
|
// 判断是否为系统消息
|
|||
|
|
function isSystemMessage(message) {
|
|||
|
|
const description = message.payload?.description;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
message.type === "TIMCustomElem" && description === "SYSTEM_NOTIFICATION"
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function isSpecialMessage(message) {
|
|||
|
|
const description = message.payload?.description;
|
|||
|
|
return (
|
|||
|
|
message.type === "TIMCustomElem" &&
|
|||
|
|
["PATIENT_RATE_MESSAGE"].includes(description)
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取消息气泡样式类
|
|||
|
|
function getBubbleClass(message) {
|
|||
|
|
// 如果是自定义消息(包含消息卡片),根据flow决定气泡样式
|
|||
|
|
if (message.type === "TIMCustomElem") {
|
|||
|
|
return message.flow === "out" ? "user-bubble" : "doctor-bubble-blue";
|
|||
|
|
}
|
|||
|
|
// 其他消息根据flow判断
|
|||
|
|
return message.flow === "out" ? "user-bubble" : "doctor-bubble";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 保存问诊状态到本地存储
|
|||
|
|
function saveConsultationStatus() {
|
|||
|
|
const orderId = uni.getStorageSync("currentOrderId");
|
|||
|
|
const doctorId = uni.getStorageSync("currentDoctorId");
|
|||
|
|
const patientId = uni.getStorageSync("currentPatientId");
|
|||
|
|
|
|||
|
|
if (orderId && doctorId && patientId) {
|
|||
|
|
const consultationKey = `consultation_${orderId}_${doctorId}_${patientId}`;
|
|||
|
|
const status = {
|
|||
|
|
waitingForDoctor: waitingForDoctor.value,
|
|||
|
|
consultationEndTime: consultationEndTime.value,
|
|||
|
|
consultationEnded: consultationEnded.value,
|
|||
|
|
orderId,
|
|||
|
|
doctorId,
|
|||
|
|
patientId,
|
|||
|
|
};
|
|||
|
|
uni.setStorageSync(consultationKey, status);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 从本地存储恢复问诊状态
|
|||
|
|
function restoreConsultationStatus() {
|
|||
|
|
const orderId = uni.getStorageSync("currentOrderId");
|
|||
|
|
const doctorId = uni.getStorageSync("currentDoctorId");
|
|||
|
|
const patientId = uni.getStorageSync("currentPatientId");
|
|||
|
|
|
|||
|
|
if (orderId && doctorId && patientId) {
|
|||
|
|
const consultationKey = `consultation_${orderId}_${doctorId}_${patientId}`;
|
|||
|
|
const savedStatus = uni.getStorageSync(consultationKey);
|
|||
|
|
|
|||
|
|
if (savedStatus) {
|
|||
|
|
const now = Date.now();
|
|||
|
|
|
|||
|
|
if (
|
|||
|
|
savedStatus.consultationEnded ||
|
|||
|
|
(savedStatus.consultationEndTime &&
|
|||
|
|
now >= savedStatus.consultationEndTime)
|
|||
|
|
) {
|
|||
|
|
consultationEnded.value = true;
|
|||
|
|
} else if (
|
|||
|
|
!savedStatus.waitingForDoctor &&
|
|||
|
|
savedStatus.consultationEndTime
|
|||
|
|
) {
|
|||
|
|
consultationEndTime.value = savedStatus.consultationEndTime;
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
consultationEndTime.value = null;
|
|||
|
|
consultationEnded.value = false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理问诊结束
|
|||
|
|
async function handleConsultationEnd(message) {
|
|||
|
|
// 从聊天业务信息中获取订单ID
|
|||
|
|
const orderId = chatRoomBusiness.value.businessId;
|
|||
|
|
|
|||
|
|
const lastMessageId = message.ID;
|
|||
|
|
|
|||
|
|
console.log("更新最后一条消息ID:", {
|
|||
|
|
orderId,
|
|||
|
|
lastMessageId,
|
|||
|
|
messageType: message.type,
|
|||
|
|
status: message.payload?.data,
|
|||
|
|
chatRoomBusiness: chatRoomBusiness.value,
|
|||
|
|
currentOrder: currentOrder.value,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (orderId && lastMessageId) {
|
|||
|
|
// 调用API更新订单的最后一条消息ID
|
|||
|
|
const result = await updateLastMessageId({
|
|||
|
|
orderId,
|
|||
|
|
lastMessageId,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (result && result.success) {
|
|||
|
|
console.log("最后一条消息ID更新成功:", result);
|
|||
|
|
} else {
|
|||
|
|
console.error("最后一条消息ID更新失败:", result);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 页面加载
|
|||
|
|
onLoad((options) => {
|
|||
|
|
groupId.value = options.groupID || "";
|
|||
|
|
// 清空消息列表,准备加载新群聊的消息
|
|||
|
|
messageList.value = [];
|
|||
|
|
// 重置加载状态,确保新聊天室可以加载
|
|||
|
|
isLoading.value = false;
|
|||
|
|
if (options.conversationID) {
|
|||
|
|
chatInfo.value.conversationID = options.conversationID;
|
|||
|
|
// 立即设置当前会话ID,防止消息混淆
|
|||
|
|
timChatManager.setConversationID(options.conversationID);
|
|||
|
|
console.log("设置当前会话ID:", options.conversationID);
|
|||
|
|
}
|
|||
|
|
if (options.userID) {
|
|||
|
|
chatInfo.value.userID = options.userID;
|
|||
|
|
}
|
|||
|
|
if (options.doctorName) {
|
|||
|
|
doctorName.value = decodeURIComponent(options.doctorName);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 保存问诊参数
|
|||
|
|
if (options.orderId) uni.setStorageSync("currentOrderId", options.orderId);
|
|||
|
|
if (options.doctorId) uni.setStorageSync("currentDoctorId", options.doctorId);
|
|||
|
|
if (options.patientId)
|
|||
|
|
uni.setStorageSync("currentPatientId", options.patientId);
|
|||
|
|
// 保存lastMessageId用于消息定位
|
|||
|
|
if (options.lastMessageId) {
|
|||
|
|
uni.setStorageSync("targetMessageId", options.lastMessageId);
|
|||
|
|
}
|
|||
|
|
// 从群聊ID提取医生ID
|
|||
|
|
if (!chatInfo.value.userID && options.conversationID) {
|
|||
|
|
const groupId = options.conversationID;
|
|||
|
|
if (groupId.startsWith("GROUP")) {
|
|||
|
|
const actualGroupId = groupId.replace("GROUP", "");
|
|||
|
|
const parts = actualGroupId.split("_");
|
|||
|
|
if (parts.length >= 1) {
|
|||
|
|
chatInfo.value.userID = parts[0];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
checkLoginAndInitTIM();
|
|||
|
|
restoreConsultationStatus();
|
|||
|
|
updateNavigationTitle();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 检查登录状态并初始化IM
|
|||
|
|
const checkLoginAndInitTIM = async () => {
|
|||
|
|
if (!account.value || !openid.value) {
|
|||
|
|
uni.showModal({
|
|||
|
|
title: "提示",
|
|||
|
|
content: "请先登录后再进入聊天",
|
|||
|
|
showCancel: false,
|
|||
|
|
confirmText: "去登录",
|
|||
|
|
success: () => {
|
|||
|
|
uni.redirectTo({
|
|||
|
|
url: "/pages/login/login",
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!isIMInitialized.value) {
|
|||
|
|
uni.showLoading({
|
|||
|
|
title: "连接中...",
|
|||
|
|
});
|
|||
|
|
const success = await initIMAfterLogin(openid.value);
|
|||
|
|
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回调函数(使用全局已初始化的IM管理器)
|
|||
|
|
const initTIMCallbacks = async () => {
|
|||
|
|
// 设置当前页面的回调函数
|
|||
|
|
timChatManager.setCallback("onSDKReady", () => {
|
|||
|
|
// SDK准备就绪后,只有在消息列表为空且未在加载中时才加载消息列表
|
|||
|
|
if (messageList.value.length === 0 && !isLoading.value) {
|
|||
|
|
loadMessageList();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
timChatManager.setCallback("onSDKNotReady", () => {});
|
|||
|
|
|
|||
|
|
timChatManager.setCallback("onMessageReceived", (message) => {
|
|||
|
|
// tim-chat.js层已经做了会话过滤,这里再次验证消息归属
|
|||
|
|
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("⚠️ 消息不属于当前群聊,已过滤:", {
|
|||
|
|
messageConversationID: message.conversationID,
|
|||
|
|
currentConversationID: chatInfo.value.conversationID,
|
|||
|
|
});
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (isSystemMessage(message)) {
|
|||
|
|
judgeReloadOrder(message);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查消息是否已存在,避免重复添加
|
|||
|
|
const existingMessage = messageList.value.find(
|
|||
|
|
(msg) => msg.ID === message.ID
|
|||
|
|
);
|
|||
|
|
if (!existingMessage) {
|
|||
|
|
messageList.value.push(message);
|
|||
|
|
console.log("✓ 添加消息到列表,当前消息数量:", messageList.value.length);
|
|||
|
|
nextTick(() => {
|
|||
|
|
// 延迟滚动,确保DOM完全渲染
|
|||
|
|
setTimeout(() => {
|
|||
|
|
scrollToBottom();
|
|||
|
|
}, 100);
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
console.log("消息已存在,跳过添加:", message.ID);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 监听系统消息
|
|||
|
|
if (message.type === "TIMCustomElem") {
|
|||
|
|
const status = message.payload?.data;
|
|||
|
|
|
|||
|
|
if (status === "pending") {
|
|||
|
|
consultationEnded.value = false;
|
|||
|
|
saveConsultationStatus();
|
|||
|
|
} else if (status === "active") {
|
|||
|
|
consultationEnded.value = false;
|
|||
|
|
saveConsultationStatus();
|
|||
|
|
} else if (
|
|||
|
|
["rejected", "completed", "cancelled", "问诊已结束"].includes(status)
|
|||
|
|
) {
|
|||
|
|
consultationEnded.value = true;
|
|||
|
|
saveConsultationStatus();
|
|||
|
|
console.log("问诊结束,准备更新最后一条消息ID:", message);
|
|||
|
|
handleConsultationEnd(message);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
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);
|
|||
|
|
} else if (!belongsToCurrentConversation) {
|
|||
|
|
console.log("⚠️ 过滤掉不属于当前群聊的消息:", {
|
|||
|
|
messageID: message.ID,
|
|||
|
|
messageConversationID: message.conversationID,
|
|||
|
|
currentConversationID: chatInfo.value.conversationID,
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
messageList.value = uniqueMessages;
|
|||
|
|
console.log(
|
|||
|
|
"消息列表已更新,原始",
|
|||
|
|
messages.length,
|
|||
|
|
"条,过滤后",
|
|||
|
|
uniqueMessages.length,
|
|||
|
|
"条消息",
|
|||
|
|
data.isRefresh ? "(后台刷新)" : ""
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 同步分页状态
|
|||
|
|
isCompleted.value = data.isCompleted || false;
|
|||
|
|
isLoadingMore.value = false;
|
|||
|
|
console.log("【onMessageListLoaded】同步状态完成");
|
|||
|
|
console.log(" isCompleted =", isCompleted.value);
|
|||
|
|
console.log(" isPullUp =", data.isPullUp);
|
|||
|
|
|
|||
|
|
nextTick(() => {
|
|||
|
|
// 如果是后台刷新,不进行滚动操作,保持用户当前位置
|
|||
|
|
if (data.isRefresh) {
|
|||
|
|
console.log("后台刷新完成,保持当前滚动位置");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果是上拉加载,需要定位到加载前的第一条消息(保持用户阅读位置)
|
|||
|
|
if (data.isPullUp && lastFirstMessageId.value) {
|
|||
|
|
console.log(
|
|||
|
|
" 上拉加载完成,定位到加载前的第一条消息:",
|
|||
|
|
lastFirstMessageId.value
|
|||
|
|
);
|
|||
|
|
setTimeout(() => {
|
|||
|
|
scrollIntoView.value = `msg-${lastFirstMessageId.value}`;
|
|||
|
|
console.log(" ✅ 已定位到消息:", lastFirstMessageId.value);
|
|||
|
|
// 清除记录
|
|||
|
|
lastFirstMessageId.value = "";
|
|||
|
|
}, 100);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查是否有目标消息需要定位
|
|||
|
|
const targetMessageId = uni.getStorageSync("targetMessageId");
|
|||
|
|
if (targetMessageId && !data.isPullUp) {
|
|||
|
|
// 延迟执行滚动,确保DOM完全渲染
|
|||
|
|
setTimeout(() => {
|
|||
|
|
scrollToMessage(targetMessageId);
|
|||
|
|
// 清除存储的目标消息ID
|
|||
|
|
uni.removeStorageSync("targetMessageId");
|
|||
|
|
}, 300);
|
|||
|
|
} else if (!data.isPullUp) {
|
|||
|
|
// 延迟执行滚动到底部,增加延迟时间并多次尝试确保滚动成功
|
|||
|
|
setTimeout(() => {
|
|||
|
|
scrollToBottom();
|
|||
|
|
// 再次尝试确保滚动到底部
|
|||
|
|
setTimeout(() => {
|
|||
|
|
scrollToBottom();
|
|||
|
|
}, 100);
|
|||
|
|
}, 200);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
timChatManager.setCallback("onError", (error) => {
|
|||
|
|
console.error("TIM错误:", error);
|
|||
|
|
uni.showToast({
|
|||
|
|
title: error,
|
|||
|
|
icon: "none",
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 检查IM是否已经初始化,如果已初始化则直接加载消息列表
|
|||
|
|
// 使用 nextTick 确保会话ID已经设置完成
|
|||
|
|
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) {
|
|||
|
|
// 10秒超时
|
|||
|
|
clearInterval(checkInterval);
|
|||
|
|
showMessage("IM登录超时,请重新进入");
|
|||
|
|
}
|
|||
|
|
}, 1000);
|
|||
|
|
} else {
|
|||
|
|
// showMessage("IM连接异常,请重新进入");
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 加载消息列表
|
|||
|
|
const loadMessageList = () => {
|
|||
|
|
if (isLoading.value) {
|
|||
|
|
console.log("正在加载中,跳过重复加载");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log(
|
|||
|
|
"【loadMessageList】开始加载消息,会话ID:",
|
|||
|
|
chatInfo.value.conversationID
|
|||
|
|
);
|
|||
|
|
isLoading.value = true;
|
|||
|
|
|
|||
|
|
// 显示加载提示
|
|||
|
|
uni.showLoading({
|
|||
|
|
title: "加载中...",
|
|||
|
|
mask: false,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
timChatManager.enterConversation(chatInfo.value.conversationID || "test1");
|
|||
|
|
|
|||
|
|
// 标记会话为已读
|
|||
|
|
if (
|
|||
|
|
timChatManager.tim &&
|
|||
|
|
timChatManager.isLoggedIn &&
|
|||
|
|
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); // 当前正在播放的语音消息ID
|
|||
|
|
|
|||
|
|
// 播放语音
|
|||
|
|
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;
|
|||
|
|
|
|||
|
|
// 设置当前播放的语音ID
|
|||
|
|
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 = () => {
|
|||
|
|
if (messageList.value.length > 0) {
|
|||
|
|
const lastMessage = messageList.value[messageList.value.length - 1];
|
|||
|
|
console.log(lastMessage);
|
|||
|
|
scrollIntoView.value = ``;
|
|||
|
|
setTimeout(() => {
|
|||
|
|
scrollIntoView.value = `msg-${lastMessage.ID}`;
|
|||
|
|
}, 300);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 滚动到指定消息
|
|||
|
|
const scrollToMessage = (messageId, retryCount = 0) => {
|
|||
|
|
if (!messageId) {
|
|||
|
|
console.warn("scrollToMessage: messageId is empty");
|
|||
|
|
scrollToBottom();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
// 检查消息是否存在于当前消息列表中
|
|||
|
|
const targetMessageIndex = messageList.value.findIndex(
|
|||
|
|
(msg) => msg.ID === messageId
|
|||
|
|
);
|
|||
|
|
if (targetMessageIndex !== -1) {
|
|||
|
|
// 计算目标滚动位置:目标消息前5条消息
|
|||
|
|
const scrollToIndex = Math.max(0, targetMessageIndex - 5);
|
|||
|
|
const targetScrollMessage = messageList.value[scrollToIndex];
|
|||
|
|
|
|||
|
|
if (targetScrollMessage) {
|
|||
|
|
scrollIntoView.value = `msg-${targetScrollMessage.ID}`;
|
|||
|
|
|
|||
|
|
// 延迟检查是否滚动成功,如果没有成功则重试
|
|||
|
|
setTimeout(() => {
|
|||
|
|
if (retryCount < 3) {
|
|||
|
|
scrollIntoView.value = `msg-${targetScrollMessage.ID}`;
|
|||
|
|
retryCount++;
|
|||
|
|
setTimeout(() => scrollToMessage(messageId, retryCount), 200);
|
|||
|
|
}
|
|||
|
|
}, 100);
|
|||
|
|
} else {
|
|||
|
|
// 如果计算出的位置没有消息,滚动到底部
|
|||
|
|
scrollToBottom();
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
scrollToBottom();
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 关闭功能栏
|
|||
|
|
const closeMorePanel = () => {
|
|||
|
|
uni.$emit("closeMorePanel");
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 处理评价弹窗状态变化
|
|||
|
|
const handleEvaluationPopupStatusChange = (isOpen) => {
|
|||
|
|
console.log("评价弹窗状态变化:", isOpen);
|
|||
|
|
isEvaluationPopupOpen.value = isOpen;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 滚动事件
|
|||
|
|
const onScroll = throttle((e) => {
|
|||
|
|
// 滚动处理
|
|||
|
|
}, 100);
|
|||
|
|
|
|||
|
|
// 处理上滑加载更多
|
|||
|
|
const handleScrollToUpper = async () => {
|
|||
|
|
console.log("【handleScrollToUpper】触发上滑事件,准备加载更多");
|
|||
|
|
console.log(
|
|||
|
|
" 当前状态: isLoadingMore=",
|
|||
|
|
isLoadingMore.value,
|
|||
|
|
"isCompleted=",
|
|||
|
|
isCompleted.value
|
|||
|
|
);
|
|||
|
|
console.log(" 消息数量:", messageList.value.length);
|
|||
|
|
|
|||
|
|
// 如果正在加载或已加载全部,不处理
|
|||
|
|
if (isLoadingMore.value || isCompleted.value) {
|
|||
|
|
console.log(
|
|||
|
|
" ⏭️ 跳过加载:isLoadingMore=",
|
|||
|
|
isLoadingMore.value,
|
|||
|
|
"isCompleted=",
|
|||
|
|
isCompleted.value
|
|||
|
|
);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果消息数量少于15条,说明第一页还没加载完,不加载更多
|
|||
|
|
if (messageList.value.length < 15) {
|
|||
|
|
console.log(" ⏭️ 消息数量不足15条,跳过加载更多");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 记录加载前的第一条消息ID,用于加载后定位
|
|||
|
|
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;
|
|||
|
|
} else if (result.message !== "正在加载中") {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: result.message || "加载失败",
|
|||
|
|
icon: "none",
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error(" ❌ 加载更多失败:", error);
|
|||
|
|
uni.showToast({
|
|||
|
|
title: "加载失败,请重试",
|
|||
|
|
icon: "none",
|
|||
|
|
});
|
|||
|
|
} finally {
|
|||
|
|
isLoadingMore.value = false;
|
|||
|
|
console.log(" 🏁 加载完成,isLoadingMore 设置为 false");
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 补充病情描述
|
|||
|
|
const addSymptomDescription = () => {
|
|||
|
|
console.log("添加病情描述: addSymptomDescription");
|
|||
|
|
if (checkIMConnectionStatus(timChatManager)) {
|
|||
|
|
console.log("添加病情描述: checkIMConnectionStatus");
|
|||
|
|
showDescriptionPopup.value = true;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 处理病情描述提交
|
|||
|
|
const handleSubmitDescription = async (descriptionData) => {
|
|||
|
|
showDescriptionPopup.value = false;
|
|||
|
|
// 更新订单信息
|
|||
|
|
currentOrder.value = {
|
|||
|
|
...currentOrder.value,
|
|||
|
|
...descriptionData,
|
|||
|
|
};
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
async function judgeReloadOrder(message) {
|
|||
|
|
try {
|
|||
|
|
const extension = JSON.parse(message.payload.extension);
|
|||
|
|
const orderId = extension.orderId || "";
|
|||
|
|
const orderStatus = extension.orderStatus || "";
|
|||
|
|
const isSameOrder =
|
|||
|
|
orderId && currentOrder.value && orderId === currentOrder.value.orderId;
|
|||
|
|
const isSameStatus =
|
|||
|
|
orderStatus &&
|
|||
|
|
currentOrder.value &&
|
|||
|
|
orderStatus === currentOrder.value.orderStatus;
|
|||
|
|
const isExpired = extension.notifyType === "CHAT_GROUP_EXPIRE"; // 会话到达结束时间
|
|||
|
|
if (
|
|||
|
|
isExpired ||
|
|||
|
|
orderId ||
|
|||
|
|
!isSameOrder ||
|
|||
|
|
(isSameOrder && !isSameStatus)
|
|||
|
|
) {
|
|||
|
|
await getChatBusiness();
|
|||
|
|
getCurrentOrder();
|
|||
|
|
}
|
|||
|
|
} catch (e) {}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 页面显示
|
|||
|
|
onShow(() => {
|
|||
|
|
if (!account.value || !openid.value) {
|
|||
|
|
uni.redirectTo({
|
|||
|
|
url: "/pages/login/login",
|
|||
|
|
});
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!isIMInitialized.value) {
|
|||
|
|
checkLoginAndInitTIM();
|
|||
|
|
} else if (timChatManager.tim && !timChatManager.isLoggedIn) {
|
|||
|
|
timChatManager.ensureIMConnection();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 启动IM状态监控
|
|||
|
|
startIMMonitoring(30000);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 页面隐藏
|
|||
|
|
onHide(() => {
|
|||
|
|
// 停止IM状态监控
|
|||
|
|
stopIMMonitoring();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 页面卸载
|
|||
|
|
onUnmounted(() => {
|
|||
|
|
clearMessageCache();
|
|||
|
|
|
|||
|
|
timChatManager.setCallback("onSDKReady", null);
|
|||
|
|
timChatManager.setCallback("onSDKNotReady", null);
|
|||
|
|
timChatManager.setCallback("onMessageReceived", null);
|
|||
|
|
timChatManager.setCallback("onMessageListLoaded", null);
|
|||
|
|
timChatManager.setCallback("onError", null);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 监听 chatRoomBusiness 变化,更新导航栏标题
|
|||
|
|
watch(
|
|||
|
|
chatRoomBusiness,
|
|||
|
|
() => {
|
|||
|
|
updateNavigationTitle();
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
deep: true,
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
watch(
|
|||
|
|
showDescriptionPopup,
|
|||
|
|
(val) => {
|
|||
|
|
console.log("病情描述弹窗状态变化:", val, Date.now());
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
immediate: true,
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped lang="scss">
|
|||
|
|
@import "./chat.scss";
|
|||
|
|
</style>
|
|||
|
|
|