2026-01-28 16:20:19 +08:00

865 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="chat-page">
<!-- 聊天消息区域 -->
<scroll-view
class="chat-content"
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="
chatMember[message.from]?.avatar || '/static/default-avatar.png'
"
mode="aspectFill"
/>
<!-- 患者头像右侧 -->
<image
v-if="message.flow === 'out'"
class="user-msg-avatar"
:src="
chatMember[message.from]?.avatar || '/static/home/avatar.svg'
"
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>
<!-- 取消申请组件 -->
<ConsultCancel v-if="showConsultCancel" @cancel="handleCancelConsult" />
<!-- 咨询申请组件 -->
<ConsultApply v-if="showConsultApply" @apply="handleApplyConsult" />
<!-- 聊天输入组件 -->
<ChatInput
v-if="!isEvaluationPopupOpen && !showConsultCancel && !showConsultApply"
ref="chatInputRef"
:timChatManager="timChatManager"
:formatTime="formatTime"
:groupId="
chatInfo.conversationID
? chatInfo.conversationID.replace('GROUP', '')
: ''
"
:userId="openid"
:corpId="corpId.value"
@scrollToBottom="() => scrollToBottom(true)"
@messageSent="() => scrollToBottom(true)"
/>
</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 {
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 ConsultCancel from "./components/consult-cancel.vue";
import ConsultApply from "./components/consult-apply.vue";
const timChatManager = globalTimChatManager;
// corpId 从群组信息中获取
const corpId = ref("");
// 获取登录状态
const { account, openid, isIMInitialized } = storeToRefs(useAccountStore());
const { initIMAfterLogin } = useAccountStore();
// 聊天输入组件引用
const chatInputRef = ref(null);
const groupId = ref("");
const { chatMember, getGroupInfo } = useGroupChat(groupId);
// 动态设置导航栏标题
const updateNavigationTitle = () => {
uni.setNavigationBarTitle({
title: "群聊",
});
};
// 聊天信息
const chatInfo = ref({
conversationID: "",
userID: "",
avatar: "/static/home/avatar.svg",
});
// 评价弹窗状态
const isEvaluationPopupOpen = ref(false);
// 订单状态
const orderStatus = ref("");
// 计算弹框显示状态
const showConsultCancel = computed(() => orderStatus.value === "pending");
const showConsultApply = computed(
() =>
orderStatus.value === "finished" ||
orderStatus.value === "cancelled" ||
orderStatus.value === "rejected"
);
// 消息列表相关状态
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 {
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;
}
}
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 || "";
corpId.value = result.data.corpId || "";
console.log("获取群组订单状态:", {
orderStatus: orderStatus.value,
corpId: corpId.value,
});
} else {
console.error("获取群组订单状态失败:", result.message);
}
} catch (error) {
console.error("获取群组订单状态异常:", error);
}
};
// 处理接收到的系统消息
function handleSystemMessageReceived(message) {
try {
if (!message.payload?.data) return;
const data =
typeof message.payload.data === "string"
? JSON.parse(message.payload.data)
: message.payload.data;
if (data.type !== "system_message") return;
console.log("收到系统消息,类型:", data.messageType);
// 系统消息触发时,重新获取订单状态
fetchGroupOrderStatus();
} catch (error) {
console.error("处理系统消息失败:", error);
}
}
// 检查是否有待接诊的系统消息
function checkConsultPendingStatus() {
// 直接获取最新的订单状态
fetchGroupOrderStatus();
}
// 获取消息气泡样式类
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) => {
groupId.value = options.groupID || "";
messageList.value = [];
isLoading.value = false;
if (options.conversationID) {
chatInfo.value.conversationID = options.conversationID;
timChatManager.setConversationID(options.conversationID);
console.log("设置当前会话ID:", options.conversationID);
}
if (options.userID) {
chatInfo.value.userID = options.userID;
}
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 () => {
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)) {
handleSystemMessageReceived(message);
}
// 检查是否有待接诊的系统消息
checkConsultPendingStatus();
// 立即滚动到底部,不使用延迟
nextTick(() => {
scrollToBottom(true);
});
}
});
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.value = uniqueMessages;
console.log(
"消息列表已更新,原始",
messages.length,
"条,过滤后",
uniqueMessages.length,
"条消息"
);
isCompleted.value = data.isCompleted || false;
isLoadingMore.value = false;
// 检查是否有待接诊的系统消息
checkConsultPendingStatus();
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");
// 标记会话为已读
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);
// 播放语音
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) {
timChatManager.ensureIMConnection();
}
startIMMonitoring(30000);
});
// 页面隐藏
onHide(() => {
stopIMMonitoring();
});
// 处理取消申请
const handleCancelConsult = async () => {
try {
uni.showModal({
title: "提示",
content: "确定要取消咨询申请吗?",
success: async (res) => {
if (res.confirm) {
uni.showLoading({
title: "处理中...",
});
try {
// 调用取消咨询申请接口
const result = await api("cancelConsultApplication", {
groupId: groupId.value,
corpId: corpId.value,
});
uni.hideLoading();
if (result.success) {
// 重新获取订单状态
await fetchGroupOrderStatus();
uni.showToast({
title: "已取消申请",
icon: "success",
});
// 延迟返回首页
setTimeout(() => {
uni.switchTab({
url: "/pages/home/home",
});
}, 1500);
} else {
throw new Error(result.message || "取消申请失败");
}
} catch (error) {
console.error("取消申请失败:", error);
uni.hideLoading();
uni.showToast({
title: error.message || "操作失败",
icon: "none",
});
}
}
},
});
} catch (error) {
console.error("取消申请失败:", error);
uni.showToast({
title: error.message || "操作失败",
icon: "none",
});
}
};
// 处理咨询申请
const handleApplyConsult = async () => {
try {
uni.showModal({
title: "提示",
content: "确定要重新申请咨询吗?",
success: async (res) => {
if (res.confirm) {
uni.showLoading({
title: "申请中...",
});
try {
// 获取当前群组信息提取客户ID和corpId
const groupInfo = await api("getGroupListByGroupId", {
groupId: groupId.value,
});
if (!groupInfo.success || !groupInfo.data) {
throw new Error("获取群组信息失败");
}
const customerId = groupInfo.data.customerId;
const groupCorpId = groupInfo.data.corpId;
if (!customerId) {
throw new Error("无法获取客户信息");
}
// 调用创建咨询群组接口(重新申请)
const result = await api("createConsultGroup", {
teamId: groupInfo.data.teamId || "",
corpId: groupCorpId || corpId.value,
customerId: customerId,
customerImUserId: openid.value,
});
uni.hideLoading();
if (result.success) {
const { groupId: newGroupId, isExisting } = result.data;
// 重新获取订单状态
await fetchGroupOrderStatus();
uni.showToast({
title: isExisting ? "已重新发起申请" : "申请已发送",
icon: "success",
});
// 如果是新群组,跳转到新的聊天页面
if (!isExisting && newGroupId !== groupId.value) {
setTimeout(() => {
uni.redirectTo({
url: `/pages/message/index?conversationID=GROUP${newGroupId}&groupID=${newGroupId}`,
});
}, 1500);
} else {
// 刷新当前页面消息
setTimeout(() => {
loadMessageList();
}, 1000);
}
} else {
throw new Error(result.message || "申请失败");
}
} catch (error) {
console.error("申请失败:", error);
uni.hideLoading();
uni.showToast({
title: error.message || "操作失败",
icon: "none",
});
}
}
},
});
} catch (error) {
console.error("申请失败:", error);
uni.showToast({
title: error.message || "操作失败",
icon: "none",
});
}
};
// 页面卸载
onUnmounted(() => {
clearMessageCache();
timChatManager.setCallback("onSDKReady", null);
timChatManager.setCallback("onSDKNotReady", null);
timChatManager.setCallback("onMessageReceived", null);
timChatManager.setCallback("onMessageListLoaded", null);
timChatManager.setCallback("onError", null);
});
</script>
<style scoped lang="scss">
@import "./chat.scss";
</style>