From cfa15a445ba70da726a1c88f14bc23021f25e5bc Mon Sep 17 00:00:00 2001 From: Jafeng <2998840497@qq.com> Date: Mon, 9 Feb 2026 17:02:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=9B=9E=E8=AE=BF?= =?UTF-8?q?=E5=86=85=E5=AE=B9=E8=B7=B3=E8=BD=AC=E5=88=B0=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E9=97=B4=E5=8F=91=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/case/archive-detail.vue | 45 ++- .../archive-detail/follow-up-manage-tab.vue | 278 +++++++++++++++--- pages/message/index.vue | 153 ++++++++-- 3 files changed, 385 insertions(+), 91 deletions(-) diff --git a/pages/case/archive-detail.vue b/pages/case/archive-detail.vue index e8a8a0f..e449530 100644 --- a/pages/case/archive-detail.vue +++ b/pages/case/archive-detail.vue @@ -334,18 +334,26 @@ async function fetchArchive() { await ensureDoctor(); loading('加载中...'); try { + const prevLocalChatGroupId = normalizeGroupId(chatGroupId.value || archive.value.chatGroupId || ''); const res = await api('getCustomerByCustomerId', { customerId: archiveId.value }); if (!res?.success) { toast(res?.message || '获取档案失败'); return; } - archive.value = { ...archive.value, ...normalizeArchiveFromApi(res.data) }; + + const normalized = normalizeArchiveFromApi(res.data); + // 后端经常不返回 chatGroupId(或为空),但本地已探测出可用群聊时不能被覆盖掉,否则返回页面会导致“去聊天”按钮短暂/持续消失 + if (!normalized.chatGroupId && prevLocalChatGroupId) { + normalized.chatGroupId = prevLocalChatGroupId; + } + + archive.value = { ...archive.value, ...normalized }; saveToStorage(); loadTeamMembers(); await fetchTeamGroups(true); chatGroupId.value = normalizeGroupId(archive.value.chatGroupId || ''); if (!chatGroupId.value) { - refreshChatRoom(); + await refreshChatRoom({ force: true }); } else { const meta = await getChatRoomMeta(chatGroupId.value); const ok = isChatRoomForArchive(meta, archiveId.value); @@ -354,7 +362,7 @@ async function fetchArchive() { if (!ok || (currentTeamId && metaTeamId && metaTeamId !== currentTeamId)) { chatGroupId.value = ''; archive.value.chatGroupId = ''; - refreshChatRoom(); + await refreshChatRoom({ force: true }); } else { archive.value.chatGroupId = chatGroupId.value; } @@ -618,7 +626,7 @@ const goEdit = () => { const goChat = async () => { let gid = normalizeGroupId(currentChatGroupId.value || ''); if (!gid) { - await refreshChatRoom(); + await refreshChatRoom({ force: true }); gid = normalizeGroupId(currentChatGroupId.value || ''); } if (!gid) { @@ -634,7 +642,7 @@ const goChat = async () => { chatGroupId.value = ''; archive.value.chatGroupId = ''; saveToStorage(); - await refreshChatRoom(); + await refreshChatRoom({ force: true }); gid = normalizeGroupId(currentChatGroupId.value || ''); } if (!gid) { @@ -650,6 +658,7 @@ const goChat = async () => { const isRefreshingChatRoom = ref(false); let lastRefreshChatRoomAt = 0; +let refreshChatRoomPromise = null; function isChatRoomForArchive(meta, customerId) { const cid = String(meta?.customerId || ''); @@ -681,16 +690,24 @@ function parseAnyTimeMs(v) { return d.isValid() ? d.valueOf() : 0; } -async function refreshChatRoom() { +async function refreshChatRoom(options = {}) { + const force = Boolean(options?.force); const customerId = String(archiveId.value || ''); if (!customerId) return; - if (isRefreshingChatRoom.value) return; + + // 如果正在刷新,复用同一个 promise,确保调用方可以 await 到结果 + if (isRefreshingChatRoom.value && refreshChatRoomPromise) { + await refreshChatRoomPromise; + return; + } + const now = Date.now(); - if (now - lastRefreshChatRoomAt < 5000) return; + if (!force && now - lastRefreshChatRoomAt < 5000) return; lastRefreshChatRoomAt = now; isRefreshingChatRoom.value = true; - try { + refreshChatRoomPromise = (async () => { + try { const corpId = getCorpId(); const teamId = getCurrentTeamId(); @@ -742,11 +759,15 @@ async function refreshChatRoom() { chatGroupId.value = ''; archive.value.chatGroupId = ''; } - } catch (e) { + } catch (e) { // ignore - } finally { + } finally { isRefreshingChatRoom.value = false; - } + refreshChatRoomPromise = null; + } + })(); + + await refreshChatRoomPromise; } const makeCall = () => { diff --git a/pages/case/components/archive-detail/follow-up-manage-tab.vue b/pages/case/components/archive-detail/follow-up-manage-tab.vue index be97c5c..894dfb7 100644 --- a/pages/case/components/archive-detail/follow-up-manage-tab.vue +++ b/pages/case/components/archive-detail/follow-up-manage-tab.vue @@ -90,9 +90,9 @@ @@ -216,7 +216,6 @@ import dayjs from "dayjs"; import api from "@/utils/api"; import useAccountStore from "@/store/account"; import { toast } from "@/utils/widget"; -import { handleFollowUpMessages } from "@/utils/send-message-helper"; import { getTodoEventTypeLabel, getTodoEventTypeOptions, @@ -235,12 +234,27 @@ const { account, doctorInfo } = storeToRefs(accountStore); const { getDoctorInfo } = accountStore; function getUserId() { - return doctorInfo.value?.userid || ""; + const d = doctorInfo.value || {}; + const a = account.value || {}; + return String(d.userid || d.userId || d.corpUserId || a.userid || a.userId || "") || ""; } function getCorpId() { const team = uni.getStorageSync("ykt_case_current_team") || {}; - return team.corpId || doctorInfo.value?.corpId || ""; + const d = doctorInfo.value || {}; + const a = account.value || {}; + return String(d.corpId || a.corpId || team.corpId || "") || ""; +} + +function getCurrentTeamId() { + const team = uni.getStorageSync("ykt_case_current_team") || {}; + return String(team?.teamId || team?._id || team?.id || "") || ""; +} + +function normalizeGroupId(v) { + const s = String(v || "").trim(); + if (!s) return ""; + return s.startsWith("GROUP") ? s.slice(5) : s; } const statusTabs = [ @@ -270,6 +284,11 @@ const query = reactive({ const list = ref([]); const total = ref(0); +const chatGroupId = ref(""); +const currentChatGroupId = computed(() => normalizeGroupId(chatGroupId.value || "")); + +const PENDING_FOLLOWUP_SEND_STORAGE_KEY = "ykt_followup_pending_send"; + const page = ref(1); const pageSize = 10; const pages = ref(1); @@ -511,75 +530,163 @@ function toDetail(todo) { }); } -async function sendFollowUp(todo) { - if (!todo.sendContent && (!todo.fileList || todo.fileList.length === 0)) { - toast("没有发送内容"); - return; - } +function hasSendContent(todo) { + return Boolean(todo?.sendContent) || (Array.isArray(todo?.fileList) && todo.fileList.length > 0); +} +function isExecutorMe(todo) { + const me = String(getUserId() || ""); + const executor = String(todo?.executorUserId || ""); + if (!me || !executor) return false; + return me === executor; +} + +function canShowSendButton(todo) { + if (!hasSendContent(todo)) return false; + if (!isExecutorMe(todo)) return false; + // 当前患者无会话则不展示 + return Boolean(currentChatGroupId.value); +} + +function buildFollowUpMessages(todo) { const messages = []; - - // 1. 发送文字内容 - if (todo.sendContent) { - messages.push({ - type: "text", - content: todo.sendContent, - }); + if (todo?.sendContent) { + messages.push({ type: "text", content: String(todo.sendContent) }); } - console.log("==============>fileList", todo.fileList); - // 2. 处理文件列表(图片、宣教文章、问卷) - if (Array.isArray(todo.fileList)) { + if (Array.isArray(todo?.fileList)) { for (const file of todo.fileList) { - if (file.type === "image" && file.URL) { - // 发送图片 + const outerType = String(file?.type || ""); + + let innerFile = file?.file; + if (typeof innerFile === "string") { + try { + innerFile = JSON.parse(innerFile); + } catch { + // ignore + } + } + innerFile = innerFile && typeof innerFile === "object" ? innerFile : null; + + const innerType = String(innerFile?.type || ""); + const outerUrl = String(file?.URL || file?.url || ""); + const innerUrl = String(innerFile?.url || ""); + + // 兼容 followup-detail.vue 的判定方式 + let fileType = ""; + if (outerType === "image" || innerType.includes("image")) fileType = "image"; + else if (innerType === "article") fileType = "article"; + else if (innerType === "questionnaire") fileType = "questionnaire"; + else fileType = outerType; + + const url = fileType === "article" || fileType === "questionnaire" ? (innerUrl || outerUrl) : (outerUrl || innerUrl); + + if (fileType === "image" && url) { messages.push({ type: "image", - content: file.URL, - name: file.file?.name || file.name || "图片", + content: url, + name: innerFile?.name || file?.name || "图片", }); - } else if (file.file.type === "article" && file.file?.url) { - // 发送宣教文章 - 从 URL 中解析 id - const articleId = extractIdFromUrl(file.file.url); + continue; + } + + if (fileType === "article") { + const fallbackArticleId = String(innerFile?._id || file?._id || innerFile?.articleId || file?.articleId || "") || ""; + const extractedId = extractIdFromUrl(url); + const articleId = String(extractedId || fallbackArticleId || ""); + + // url 兜底:如果后端没给文章跳转链接,则用 articleId+corpId 拼接 + let articleUrl = String(url || ""); + if (!articleUrl && articleId) { + const corpId = getCorpId(); + articleUrl = `${__VITE_ENV__?.MP_PATIENT_PAGE_BASE_URL || ""}pages/article/index?id=${encodeURIComponent( + articleId + )}&corpId=${encodeURIComponent(corpId || "")}`; + } + + // 没有 id 和 url 时,跳过 + if (!articleId && !articleUrl) continue; + messages.push({ type: "article", content: { _id: articleId, - title: file.file?.name || "宣教文章", - url: file.file?.url || file.URL, - subtitle: file.file?.subtitle || "", - cover: file.file?.cover || "", + title: innerFile?.name || file?.name || "宣教文章", + url: articleUrl, + subtitle: innerFile?.subtitle || "", + cover: innerFile?.cover || file?.URL || "", articleId: articleId, }, }); - } else if (file.file.type === "questionnaire" && file.file?.surveryId) { - // 发送问卷 + continue; + } + + if (fileType === "questionnaire") { + const surveryId = innerFile?.surveryId || file?.surveryId; + if (!surveryId) continue; + const surveyId = String(innerFile?._id || file?._id || surveryId || ""); messages.push({ type: "questionnaire", content: { - _id: file.file?._id || file._id, - name: file.file?.name || file.name || "问卷", - surveryId: file.file?.surveryId || file.surveryId, - url: file.file?.url || file.URL, + _id: surveyId, + name: innerFile?.name || file?.name || "问卷", + surveryId, + url: String(url || ""), + createBy: innerFile?.createBy, }, }); } } } - // 调用统一的消息发送处理函数 - const success = await handleFollowUpMessages(messages, { - userId: getUserId(), - customerId: props.archiveId, - customerName: props.data?.name || "", - corpId: getCorpId(), - env: __VITE_ENV__, + return messages; +} + +async function goChatAndSend(todo) { + if (!canShowSendButton(todo)) return; + if (!props.archiveId) return; + + let gid = normalizeGroupId(currentChatGroupId.value || ""); + if (!gid) { + await refreshChatRoom(); + gid = normalizeGroupId(currentChatGroupId.value || ""); + } + if (!gid) { + toast("暂无可进入的会话"); + return; + } + + const messages = buildFollowUpMessages(todo); + if (!messages.length) { + console.warn("[followup] buildFollowUpMessages empty:", { + sendContent: todo?.sendContent, + fileList: todo?.fileList, + }); + toast("发送内容解析失败"); + return; + } + + const conversationID = `GROUP${gid}`; + + uni.setStorageSync(PENDING_FOLLOWUP_SEND_STORAGE_KEY, { + createdAt: Date.now(), + groupId: gid, + conversationID, + messages, + context: { + userId: getUserId(), + customerId: props.archiveId, + customerName: props.data?.name || "", + corpId: getCorpId(), + env: __VITE_ENV__, + }, }); - if (success) { - toast("消息已发送"); - uni.navigateBack(); - } + uni.navigateTo({ + url: `/pages/message/index?conversationID=${encodeURIComponent( + conversationID + )}&groupID=${encodeURIComponent(gid)}&fromCase=true&pendingFollowUpSend=1`, + }); } /** @@ -606,6 +713,80 @@ function extractIdFromUrl(url) { } } +const isRefreshingChatRoom = ref(false); +let lastRefreshChatRoomAt = 0; + +function parseAnyTimeMs(v) { + if (v === null || v === undefined) return 0; + if (typeof v === "number") return v; + const s = String(v).trim(); + if (!s) return 0; + if (/^\d{10,13}$/.test(s)) return Number(s.length === 10 ? `${s}000` : s); + const d = dayjs(s); + return d.isValid() ? d.valueOf() : 0; +} + +async function refreshChatRoom() { + const customerId = String(props.archiveId || ""); + if (!customerId) return; + if (isRefreshingChatRoom.value) return; + const now = Date.now(); + if (now - lastRefreshChatRoomAt < 5000) return; + lastRefreshChatRoomAt = now; + + isRefreshingChatRoom.value = true; + try { + await ensureDoctor(); + const corpId = getCorpId(); + const teamId = getCurrentTeamId(); + + const baseQuery = { + corpId, + customerId, + page: 1, + pageSize: 50, + }; + + const queryWithTeam = teamId ? { ...baseQuery, teamId } : baseQuery; + let detailRes = await api("getGroupList", queryWithTeam, false); + let details = Array.isArray(detailRes?.data?.list) ? detailRes.data.list : []; + + if (!details.length && teamId) { + detailRes = await api("getGroupList", baseQuery, false); + details = Array.isArray(detailRes?.data?.list) ? detailRes.data.list : []; + } + + if (!detailRes?.success || !details.length) { + chatGroupId.value = ""; + return; + } + + const currentTeamId = getCurrentTeamId(); + const detailsForCurrentTeam = currentTeamId + ? details.filter((g) => String(g?.teamId || g?.team?._id || g?.team?.teamId || "") === currentTeamId) + : []; + const candidates = detailsForCurrentTeam.length ? detailsForCurrentTeam : details; + + const statusRank = (s) => (s === "processing" ? 3 : s === "pending" ? 2 : 1); + candidates.sort((a, b) => { + const ra = statusRank(String(a?.orderStatus || "")); + const rb = statusRank(String(b?.orderStatus || "")); + if (rb !== ra) return rb - ra; + const ta = parseAnyTimeMs(a?.updatedAt) || parseAnyTimeMs(a?.createdAt); + const tb = parseAnyTimeMs(b?.updatedAt) || parseAnyTimeMs(b?.createdAt); + return tb - ta; + }); + + const best = candidates[0] || {}; + const gid = normalizeGroupId(best.groupId || best.groupID || best.group_id || ""); + chatGroupId.value = gid ? String(gid) : ""; + } catch (e) { + // ignore + } finally { + isRefreshingChatRoom.value = false; + } +} + // ---- filter popup ---- const filterPopupRef = ref(null); const state = ref(null); @@ -692,6 +873,7 @@ onMounted(() => { if (userId && name) userNameMap.value = { ...(userNameMap.value || {}), [userId]: name }; loadTeams(); + refreshChatRoom(); resetList(); uni.$on("archive-detail:followup-changed", resetList); }); diff --git a/pages/message/index.vue b/pages/message/index.vue index 93e8de5..61681c5 100644 --- a/pages/message/index.vue +++ b/pages/message/index.vue @@ -182,6 +182,7 @@ import { onLoad, onShow, onHide } from "@dcloudio/uni-app"; import { storeToRefs } from "pinia"; import useAccountStore from "@/store/account.js"; import { globalTimChatManager, TIM } from "@/utils/tim-chat.js"; +import { handleFollowUpMessages } from "@/utils/send-message-helper"; import { startIMMonitoring, stopIMMonitoring, @@ -210,6 +211,63 @@ 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); + +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 || ""; @@ -367,35 +425,35 @@ function getBubbleClass(message) { 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); - } - - checkLoginAndInitTIM(); - updateNavigationTitle(); -}); +// 页面加载 +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); + } + + checkLoginAndInitTIM(); + updateNavigationTitle(); +}); // 检查登录状态并初始化IM const checkLoginAndInitTIM = async () => { @@ -527,15 +585,45 @@ const initTIMCallbacks = async () => { } }); - messageList.value = uniqueMessages; + // 合并现有 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, "条,过滤后", - uniqueMessages.length, + messageList.value.length, "条消息" ); + if (!data.isPullUp && !data.isRefresh) { + initialMessageListLoaded.value = true; + // 首屏加载完成后再尝试消费待发送 payload + tryConsumePendingFollowUpSend(); + } + isCompleted.value = data.isCompleted || false; isLoadingMore.value = false; @@ -620,6 +708,9 @@ const loadMessageList = async () => { timChatManager.enterConversation(chatInfo.value.conversationID || "test1"); + // 若从病历回访记录带入待发送内容,则进入会话后自动发送 + tryConsumePendingFollowUpSend(); + // 标记会话为已读 - 确保清空未读数 if ( timChatManager.tim &&