diff --git a/components/full-page.vue b/components/full-page.vue index 4453441..266b36f 100644 --- a/components/full-page.vue +++ b/components/full-page.vue @@ -22,7 +22,7 @@ - + diff --git a/hooks/useInfoCheck.js b/hooks/useInfoCheck.js index d741127..d5989ee 100644 --- a/hooks/useInfoCheck.js +++ b/hooks/useInfoCheck.js @@ -9,11 +9,15 @@ export default function useInfoCheck() { function withInfo(fn) { return async (...args) => { if (!doctorInfo.value || !doctorInfo.value.anotherName) { - await confirm('请先完善您的个人信息,方可使用该功能!', { cancelText: '再等等', confirmText: '去完善' }) + try { + await confirm('请先完善您的个人信息,方可使用该功能!', { cancelText: '再等等', confirmText: '去完善' }) + } catch { + return; + } return uni.navigateTo({ url: '/pages/work/profile' }); } return fn(...args); } } return { withInfo } -} \ No newline at end of file +} 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 6d84d42..9a098b2 100644 --- a/pages/case/components/archive-detail/follow-up-manage-tab.vue +++ b/pages/case/components/archive-detail/follow-up-manage-tab.vue @@ -75,6 +75,7 @@ normalizeGroupId(chatGroupId.value || "")); + +const PENDING_FOLLOWUP_SEND_STORAGE_KEY = "ykt_followup_pending_send"; + const page = ref(1); const pageSize = 10; const pages = ref(1); @@ -733,6 +753,7 @@ onMounted(() => { if (userId && name) userNameMap.value = { ...(userNameMap.value || {}), [userId]: name }; loadTeams(); + refreshChatRoom(); resetList(); uni.$on("archive-detail:followup-changed", resetList); }); @@ -926,12 +947,19 @@ watch( .file-list { margin-top: 6px; } + +.file-list.no-send-text { + margin-top: 0; +} .file-item { - margin-top: 4px; display: flex; align-items: center; gap: 6px; } + +.file-item + .file-item { + margin-top: 4px; +} .file-icon { font-size: 14px; flex-shrink: 0; diff --git a/pages/case/components/archive-detail/service-info-tab.vue b/pages/case/components/archive-detail/service-info-tab.vue index d351882..e11f9aa 100644 --- a/pages/case/components/archive-detail/service-info-tab.vue +++ b/pages/case/components/archive-detail/service-info-tab.vue @@ -34,7 +34,7 @@ {{ i.timeStr }} - + {{ i.fileType === 'article' ? '查看文章' : '查看问卷' }} @@ -137,6 +137,11 @@ const loading = ref(false); const list = ref([]); const expandMap = ref({}); const userNameMap = ref({}); +const teamNameMap = ref({}); +const loadedTeamNameIds = new Set(); + +// 先隐藏“查看问卷/文章”入口(保留相关代码,后续可随时打开) +const showFileEntry = ref(false); const accountStore = useAccountStore(); const { account, doctorInfo } = storeToRefs(accountStore); @@ -183,6 +188,7 @@ function getExecutorId(r) { const row = r && typeof r === 'object' ? r : {}; return String( row.executorUserId || + row.executorUserID || row.executorId || row.executor || row.creatorUserId || @@ -194,11 +200,11 @@ function getExecutorId(r) { function executorText(r) { const row = r && typeof r === 'object' ? r : {}; - const fromRow = normalizeName(row.executorName || row.executorUserName || row.creatorName || row.updateUserName || ''); - if (fromRow) return fromRow; const uid = getExecutorId(row); const mapped = normalizeName(resolveUserName(uid)); - return mapped || (uid ? uid : '--'); + const fromRow = normalizeName(row.executorName || row.executorUserName || row.creatorName || row.updateUserName || ''); + if (mapped && uid && mapped !== uid && (!fromRow || fromRow === uid)) return mapped; + return fromRow || mapped || (uid ? uid : '--'); } function getExecuteTeamId(r) { @@ -209,9 +215,86 @@ function getExecuteTeamId(r) { function resolveTeamName(teamId) { const tid = String(teamId || '') || ''; if (!tid) return ''; + const cached = teamNameMap.value?.[tid]; + if (cached) return String(cached); const list = teamList.value || []; const hit = list.find((i) => i && i.value === tid); - return hit?.label ? String(hit.label) : ''; + if (hit?.label) return String(hit.label); + // 不阻塞渲染:后台补齐团队名 + void batchLoadTeamNames([tid]); + return ''; +} + +let teamNameBatchInflight = null; // Promise | null +async function batchLoadTeamNames(teamIds) { + const ids = Array.isArray(teamIds) ? teamIds.map((v) => String(v || '').trim()).filter(Boolean) : []; + if (!ids.length) return; + const uniq = Array.from(new Set(ids)); + + const unknown = uniq.filter((tid) => { + if (loadedTeamNameIds.has(tid)) return false; + const cached = teamNameMap.value?.[tid]; + if (cached) return false; + const list = teamList.value || []; + const hit = list.find((i) => i && i.value === tid); + return !hit?.label; + }); + if (!unknown.length) return; + + if (teamNameBatchInflight) return teamNameBatchInflight; + + const corpId = getCorpId(); + if (!corpId) return; + unknown.forEach((tid) => loadedTeamNameIds.add(tid)); + + teamNameBatchInflight = (async () => { + // 现成接口:getTeamById 支持 teamIds 批量查询,返回 team 列表(含 teamId/name) + try { + const res = await api('getTeamById', { corpId, teamIds: unknown }, false); + if (res?.success) { + const rows = Array.isArray(res?.data) ? res.data : Array.isArray(res?.data?.data) ? res.data.data : []; + const patch = rows.reduce((acc, t) => { + const id = String(t?.teamId || t?.id || t?._id || '').trim(); + if (!id) return acc; + const name = String(t?.name || t?.teamName || t?.team || '').trim(); + if (!name) return acc; + // 只补缺,不覆盖已有映射 + const existing = teamNameMap.value?.[id]; + if (existing) return acc; + acc[id] = name; + return acc; + }, {}); + if (Object.keys(patch).length) teamNameMap.value = { ...(teamNameMap.value || {}), ...patch }; + return; + } + } catch { + // ignore + } + + // 兜底:逐个 getTeamData(并发不高,避免卡顿) + const limit = 4; + let idx = 0; + const workers = Array.from({ length: Math.min(limit, unknown.length) }, async () => { + while (idx < unknown.length) { + const tid = unknown[idx++]; + try { + const res = await api('getTeamData', { corpId, teamId: tid }, false); + if (!res?.success) continue; + const data = res?.data && typeof res.data === 'object' ? res.data : {}; + const name = String(data?.name || data?.teamName || data?.team || '').trim(); + if (!name) continue; + if (!teamNameMap.value?.[tid]) teamNameMap.value = { ...(teamNameMap.value || {}), [tid]: name }; + } catch { + // ignore + } + } + }); + await Promise.allSettled(workers); + })().finally(() => { + teamNameBatchInflight = null; + }); + + return teamNameBatchInflight; } function executeTeamText(r) { @@ -264,21 +347,110 @@ async function loadTeamMembers(teamId) { const tid = String(teamId || '') || ''; if (!tid) return; if (loadedTeamMemberIds.has(tid)) return; - loadedTeamMemberIds.add(tid); await ensureDoctor(); const corpId = getCorpId(); if (!corpId) return; - const res = await api('getTeamData', { corpId, teamId: tid }); - if (!res?.success) return; - const t = res?.data && typeof res.data === 'object' ? res.data : {}; + loadedTeamMemberIds.add(tid); + + // 以 getTeamData 为准(getTeamMemberAvatarsAndName 存在不全/不准的情况) + const fallback = await api('getTeamData', { corpId, teamId: tid }); + if (!fallback?.success) { + loadedTeamMemberIds.delete(tid); + return; + } + const t = fallback?.data && typeof fallback.data === 'object' ? fallback.data : {}; const members = Array.isArray(t.memberList) ? t.memberList : []; const map = members.reduce((acc, m) => { - const uid = String(m?.userid || ''); + if (typeof m === 'string') { + const id = String(m || ''); + if (id) acc[id] = id; + return acc; + } + const uid = String(m?.userid || m?.userId || m?.corpUserId || m?._id || m?.id || ''); if (!uid) return acc; - acc[uid] = String(m?.anotherName || m?.name || m?.userid || '') || uid; + acc[uid] = String(m?.anotherName || m?.name || m?.userid || m?.userId || '') || uid; return acc; }, {}); userNameMap.value = { ...(userNameMap.value || {}), ...map }; + + // 补缺:仅当当前没有映射时才用 avatars 接口补齐,避免覆盖正确姓名 + try { + const res = await api('getTeamMemberAvatarsAndName', { corpId, teamId: tid }); + if (res?.success && res?.data && typeof res.data === 'object') { + const raw = res.data; + const patch = Object.keys(raw).reduce((acc, uid) => { + const id = String(uid || ''); + if (!id) return acc; + const existing = userNameMap.value?.[id]; + if (existing && existing !== id) return acc; + const name = String(raw?.[uid]?.name || raw?.[uid]?.anotherName || '').trim(); + if (!name || name === id) return acc; + acc[id] = name; + return acc; + }, {}); + if (Object.keys(patch).length) userNameMap.value = { ...(userNameMap.value || {}), ...patch }; + } + } catch { + // ignore + } +} + +let corpMemberBatchInflight = null; // Promise | null +async function batchLoadCorpMembers(userIds) { + const ids = Array.isArray(userIds) ? userIds.map((v) => String(v || '').trim()).filter(Boolean) : []; + if (!ids.length) return; + const uniq = Array.from(new Set(ids)); + const unknown = uniq.filter((id) => { + const existing = userNameMap.value?.[id]; + return !existing || existing === id; + }); + if (!unknown.length) return; + + if (corpMemberBatchInflight) return corpMemberBatchInflight; + + await ensureDoctor(); + const corpId = getCorpId(); + if (!corpId) return; + + corpMemberBatchInflight = (async () => { + try { + const res = await api( + 'getCorpMember', + { + page: 1, + pageSize: Math.min(Math.max(unknown.length, 10), 500), + params: { + corpId, + memberList: unknown, + }, + }, + false + ); + if (!res?.success) return; + + const rows = Array.isArray(res?.data) ? res.data : Array.isArray(res?.data?.data) ? res.data.data : []; + if (!rows.length) return; + + const patch = rows.reduce((acc, m) => { + const id = String(m?.userid || m?.userId || m?.corpUserId || '').trim(); + if (!id) return acc; + const existing = userNameMap.value?.[id]; + if (existing && existing !== id) return acc; + const display = String(m?.anotherName || m?.name || '').trim(); + if (!display || display === id) return acc; + acc[id] = display; + return acc; + }, {}); + + if (Object.keys(patch).length) userNameMap.value = { ...(userNameMap.value || {}), ...patch }; + } catch { + // ignore + } + })().finally(() => { + corpMemberBatchInflight = null; + }); + + return corpMemberBatchInflight; } const moreStatus = computed(() => { @@ -375,6 +547,13 @@ async function getMore() { // 尽量加载记录所属团队成员,用于执行人展示 const teamIds = mapped.map((i) => i.executeTeamId).filter(Boolean); Array.from(new Set(teamIds)).forEach((tid) => loadTeamMembers(tid)); + + // 批量补齐团队名(与 userid 补齐策略一致:缓存 + 只补缺,不阻塞渲染) + void batchLoadTeamNames(teamIds); + + // 补齐非团队成员执行人姓名(例如其他团队创建/操作) + const executorIds = mapped.map((i) => i.executorUserId).filter(Boolean); + void batchLoadCorpMembers(executorIds); } finally { loading.value = false; } @@ -669,6 +848,7 @@ watch( display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; + line-clamp: 3; overflow: hidden; } .pen { diff --git a/pages/case/search.vue b/pages/case/search.vue index e4bad94..e8ffbe6 100644 --- a/pages/case/search.vue +++ b/pages/case/search.vue @@ -6,7 +6,7 @@ - 输入患者名称、手机号或院内ID号进行搜索 + 输入患者名称、手机号或病案号进行搜索 @@ -73,6 +73,7 @@ import { toast } from '@/utils/widget'; const searchQuery = ref(''); const searchResultsRaw = ref([]); const searching = ref(false); +const searchSeq = ref(0); const CURRENT_TEAM_STORAGE_KEY = 'ykt_case_current_team'; const DETAIL_STORAGE_KEY = 'ykt_case_archive_detail'; @@ -111,11 +112,29 @@ function formatPatient(raw) { const mobiles = asArray(raw?.mobiles).map(String).filter(Boolean); const mobile = raw?.mobile ? String(raw.mobile) : (mobiles[0] || ''); + const customerNumber = raw?.customerNumber || raw?.hospitalId || ''; + const customerProfileNo2 = raw?.customerProfileNo2 || ''; + const customerProfileNo3 = raw?.customerProfileNo3 || ''; + + function normalizeRecordTypeLabel(value) { + const s = String(value || '').trim(); + if (!s) return ''; + const lower = s.toLowerCase(); + const base = lower.endsWith('record') && s.length > 6 ? s.slice(0, -6) : s; + const baseLower = String(base).toLowerCase(); + if (baseLower === 'outpatient' || baseLower === 'out_patient' || baseLower === 'out-patient') return '门诊记录'; + if (baseLower === 'inhospital' || baseLower === 'in_hospital' || baseLower === 'in-hospital' || baseLower === 'inpatient') return '住院记录'; + if (baseLower === 'preconsultation' || baseLower === 'pre_consultation' || baseLower === 'pre-consultation' || baseLower === 'preconsultationtemplate') return '预问诊记录'; + if (baseLower === 'physicalexaminationtemplate' || baseLower === 'physicalexamination' || baseLower === 'physical_examination') return '体检记录'; + // 如果已经是中文(或后端已给展示名),直接返回 + return s; + } + // 解析病历信息 let record = null; if (raw?.latestRecord && typeof raw.latestRecord === 'object') { const lr = raw.latestRecord; - const type = lr.type || ''; + const type = normalizeRecordTypeLabel(lr.type || lr.medicalTypeName || lr.medicalType || ''); const date = lr.date || ''; const diagnosis = lr.diagnosis || ''; if (type || date || diagnosis) { @@ -139,10 +158,79 @@ function formatPatient(raw) { record, createTime: raw?.createTime || '', creator: raw?.creatorName || raw?.creator || '', - hospitalId: raw?.customerNumber || raw?.hospitalId || '', + hospitalId: customerNumber, + customerNumber, + customerProfileNo2, + customerProfileNo3, }; } +function normalizeText(value) { + return String(value || '').trim(); +} + +function normalizeDigits(value) { + const s = normalizeText(value); + return s.replace(/\D/g, ''); +} + +function isChinaMobile(value) { + const digits = normalizeDigits(value); + return /^1[3-9]\d{9}$/.test(digits); +} + +function isLikelyArchiveNo(value) { + const s = normalizeText(value); + if (!s) return false; + if (/[\u4e00-\u9fa5]/.test(s)) return false; // 中文一般是姓名 + if (isChinaMobile(s)) return false; + // 病案号一般为数字/字母组合(或纯数字) + return /^[0-9A-Za-z-]{3,}$/.test(s); +} + +function matchesAnyArchiveNo(patient, q) { + const needle = normalizeText(q).toLowerCase(); + if (!needle) return false; + const values = [ + patient?.customerNumber, + patient?.customerProfileNo2, + patient?.customerProfileNo3, + patient?.hospitalId, + ].map((v) => normalizeText(v).toLowerCase()).filter(Boolean); + + // 优先精确匹配 + if (values.some((v) => v === needle)) return true; + // 兜底:包含匹配(部分医院病案号前后可能带前缀/后缀) + return values.some((v) => v.includes(needle)); +} + +function matchesAnyMobile(patient, q) { + const needle = normalizeDigits(q); + if (!needle) return false; + const mobiles = [ + patient?.mobile, + ...(Array.isArray(patient?.mobiles) ? patient.mobiles : []), + patient?.phone, + patient?.phone1, + ].map(normalizeDigits).filter(Boolean); + return mobiles.some((m) => m === needle); +} + +async function fetchList(params) { + const res = await api('searchCorpCustomerForCaseList', params); + if (!res?.success) { + throw new Error(res?.message || '搜索失败'); + } + const payload = + res && typeof res === 'object' + ? res.data && typeof res.data === 'object' && !Array.isArray(res.data) + ? res.data + : res + : {}; + const list = Array.isArray(payload.list) ? payload.list : Array.isArray(payload.data) ? payload.data : []; + return list.map(formatPatient); +} + // Computed const searchResults = computed(() => { return searchResultsRaw.value || []; @@ -163,29 +251,58 @@ const doSearch = useDebounce(async () => { return; } + const seq = (searchSeq.value += 1); searching.value = true; - const res = await api('searchCorpCustomerForCaseList', { - corpId, - userId, - teamId, - name: q, - page: 1, - pageSize: 50, - }); - searching.value = false; + try { + if (isChinaMobile(q)) { + const digits = normalizeDigits(q); + const list = await fetchList({ + corpId, + userId, + teamId, + page: 1, + pageSize: 50, + mobile: digits, + }); + if (seq !== searchSeq.value) return; - if (!res?.success) { - toast(res?.message || '搜索失败'); - return; + // 后端为包含匹配,这里做一次精确过滤,避免“部分号段”带来误命中 + searchResultsRaw.value = list.filter((p) => matchesAnyMobile(p, digits)); + return; + } + + if (isLikelyArchiveNo(q)) { + const list = await fetchList({ + corpId, + userId, + teamId, + page: 1, + pageSize: 50, + archiveNo: q, + }); + if (seq !== searchSeq.value) return; + + // 兜底:后端字段不全时,仍然做一次本地匹配(包含 customerNumber/2/3) + searchResultsRaw.value = list.filter((p) => matchesAnyArchiveNo(p, q)); + return; + } + + const list = await fetchList({ + corpId, + userId, + teamId, + name: q, + page: 1, + pageSize: 50, + }); + if (seq !== searchSeq.value) return; + searchResultsRaw.value = list; + } catch (e) { + if (seq !== searchSeq.value) return; + toast(e?.message || '搜索失败'); + } finally { + if (seq === searchSeq.value) searching.value = false; } - const payload = - res && typeof res === 'object' - ? res.data && typeof res.data === 'object' && !Array.isArray(res.data) - ? res.data - : res - : {}; - const list = Array.isArray(payload.list) ? payload.list : Array.isArray(payload.data) ? payload.data : []; - searchResultsRaw.value = list.map(formatPatient); }, 600); const handleSearch = () => doSearch(); diff --git a/pages/home/case-home.vue b/pages/home/case-home.vue index bdb645a..6febbee 100644 --- a/pages/home/case-home.vue +++ b/pages/home/case-home.vue @@ -130,6 +130,7 @@ import dayjs from 'dayjs'; import api from '@/utils/api'; import useAccountStore from '@/store/account'; +import useInfoCheck from '@/hooks/useInfoCheck'; import { confirm as uniConfirm, hideLoading, loading as showLoading, toast } from '@/utils/widget'; // State @@ -163,6 +164,10 @@ const groupNameMap = computed(() => { // Team Members Map const userNameMap = ref({}); +const loadedMembersTeamId = ref(''); +const corpMemberNameInflight = new Map(); // userId -> Promise +const corpMemberNameTried = new Set(); // avoid retry storms on failures +let corpMemberBatchInflight = null; // Promise | null // 新增流程所需状态(认证相关) const managedArchiveCountAllTeams = ref(0); // 在管档案数(所有团队) @@ -189,6 +194,7 @@ const currentTab = computed(() => tabs.value.find((t) => t.key === currentTabKey const accountStore = useAccountStore(); const { account, doctorInfo } = storeToRefs(accountStore); const { getDoctorInfo } = accountStore; +const { withInfo } = useInfoCheck(); const teamDisplay = computed(() => `${currentTeam.value?.name || ''}`); @@ -196,6 +202,272 @@ function asArray(value) { return Array.isArray(value) ? value : []; } +function normalizeUserId(value) { + if (value === null || value === undefined) return ''; + if (typeof value === 'object') { + const obj = value; + const picked = + obj.userid || + obj.userId || + obj.userID || + obj.corpUserId || + obj.corpUserID || + obj._id || + obj.id || + ''; + return String(picked || '').trim(); + } + return String(value || '').trim(); +} + +function resolveUserName(userId) { + const id = normalizeUserId(userId); + if (!id) return ''; + // 优先使用当前登录人的信息(避免映射未命中时只显示 userid) + const d = doctorInfo.value || {}; + const doctorId = normalizeUserId(d.userid || d.userId || d.corpUserId || ''); + if (doctorId && doctorId === id) { + const display = String(d.anotherName || d.name || d.username || d.userid || '').trim(); + if (display) return display; + } + const mapped = userNameMap.value[id] || userNameMap.value[id.toLowerCase()]; + if (mapped) return mapped; + // 不阻塞渲染:后台补齐非团队成员姓名(例如其他团队成员创建) + if (isLikelyUserId(id)) void fetchCorpMemberDisplayName(id).catch(() => {}); + return id; +} + +function isLikelyUserId(value) { + const id = normalizeUserId(value); + if (!id) return false; + if (/[\u4e00-\u9fa5]/.test(id)) return false; // already looks like a name + if (/\s/.test(id)) return false; + return true; +} + +function extractDisplayNameFromAny(payload) { + if (!payload) return ''; + if (typeof payload === 'string') return payload.trim(); + if (Array.isArray(payload)) return extractDisplayNameFromAny(payload[0]); + if (typeof payload !== 'object') return ''; + const obj = payload; + const candidate = + obj.anotherName || + obj.name || + obj.realName || + obj.username || + obj.nickName || + obj.nickname || + obj.displayName || + obj.userName || + obj.userid || + obj.userId || + obj.corpUserId || + ''; + return String(candidate || '').trim(); +} + +function extractDisplayNameFromCorpMember(row) { + const m = row && typeof row === 'object' ? row : {}; + const name = String(m.anotherName || m.name || '').trim(); + return name; +} + +async function batchFetchCorpMemberDisplayNames(userIds) { + const ids = Array.isArray(userIds) ? userIds.map(normalizeUserId).filter(Boolean) : []; + if (!ids.length) return; + const uniq = Array.from(new Set(ids)); + + const unresolved = uniq.filter((id) => { + if (!isLikelyUserId(id)) return false; + const cached = userNameMap.value?.[id] || userNameMap.value?.[id.toLowerCase()]; + return !cached || cached === id; + }); + if (!unresolved.length) return; + + if (corpMemberBatchInflight) return corpMemberBatchInflight; + + const corpId = getCorpId() || String(account.value?.corpId || doctorInfo.value?.corpId || ''); + if (!corpId) return; + + corpMemberBatchInflight = (async () => { + try { + const res = await api( + 'getCorpMember', + { + page: 1, + pageSize: Math.min(Math.max(unresolved.length, 10), 500), + params: { + corpId, + memberList: unresolved, + }, + }, + false + ); + + if (!res?.success) return; + const rows = Array.isArray(res?.data) ? res.data : Array.isArray(res?.data?.data) ? res.data.data : []; + if (!rows.length) return; + + const next = { ...(userNameMap.value || {}) }; + rows.forEach((m) => { + const id = normalizeUserId(m?.userid || m?.userId || m?.corpUserId || ''); + if (!id) return; + const display = extractDisplayNameFromCorpMember(m) || id; + const existing = next[id] || next[id.toLowerCase()]; + // 仅补缺:不覆盖已有的非空/非同值映射 + if (existing && existing !== id) return; + if (display && display !== id) { + next[id] = display; + next[id.toLowerCase()] = display; + } + }); + userNameMap.value = next; + } catch { + // ignore + } + })().finally(() => { + corpMemberBatchInflight = null; + }); + + return corpMemberBatchInflight; +} + +async function fetchCorpMemberDisplayName(userId) { + const id = normalizeUserId(userId); + if (!id) return ''; + if (!isLikelyUserId(id)) return ''; + + const cached = userNameMap.value[id] || userNameMap.value[id.toLowerCase()]; + if (cached && cached !== id) return cached; + + if (corpMemberNameInflight.has(id)) return corpMemberNameInflight.get(id); + if (corpMemberNameTried.has(id)) return ''; + + const corpId = getCorpId() || String(account.value?.corpId || doctorInfo.value?.corpId || ''); + if (!corpId) return ''; + + const p = (async () => { + corpMemberNameTried.add(id); + + // 1) 首选:企业成员主页信息(更可能支持用 userid 查询) + try { + const res = await api('getCorpMemberHomepageInfo', { corpId, corpUserId: id }, false); + if (res?.success) { + const name = + extractDisplayNameFromAny(res?.data) || + extractDisplayNameFromAny(res?.data?.data) || + extractDisplayNameFromAny(res?.data?.member) || + ''; + if (name) return name; + } + } catch { + // ignore + } + + // 1.1) 有的后端参数名为 userId + try { + const res = await api('getCorpMemberHomepageInfo', { corpId, userId: id }, false); + if (res?.success) { + const name = + extractDisplayNameFromAny(res?.data) || + extractDisplayNameFromAny(res?.data?.data) || + extractDisplayNameFromAny(res?.data?.member) || + ''; + if (name) return name; + } + } catch { + // ignore + } + + // 2) 兜底:成员数据接口(部分环境可能支持 corpUserId) + try { + const res = await api('getCorpMemberData', { corpId, corpUserId: id }, false); + if (res?.success) { + const name = + extractDisplayNameFromAny(res?.data) || + extractDisplayNameFromAny(res?.data?.data) || + ''; + if (name) return name; + } + } catch { + // ignore + } + + // 2.1) 同样尝试 userId + try { + const res = await api('getCorpMemberData', { corpId, userId: id }, false); + if (res?.success) { + const name = + extractDisplayNameFromAny(res?.data) || + extractDisplayNameFromAny(res?.data?.data) || + ''; + if (name) return name; + } + } catch { + // ignore + } + + return ''; + })() + .then((name) => { + const display = String(name || '').trim(); + if (display) { + const next = { ...(userNameMap.value || {}) }; + next[id] = display; + next[id.toLowerCase()] = display; + userNameMap.value = next; + } + return display; + }) + .finally(() => { + corpMemberNameInflight.delete(id); + }); + + corpMemberNameInflight.set(id, p); + return p; +} + +async function prefetchUserNamesFromPatients(patients) { + const list = Array.isArray(patients) ? patients : []; + if (!list.length) return; + + const ids = new Set(); + list.forEach((p) => { + const u1 = normalizeUserId(p?.recentAddOperatorUserId); + const u2 = normalizeUserId(p?.creator); + if (u1) ids.add(u1); + if (u2) ids.add(u2); + }); + + const targets = Array.from(ids).filter((id) => { + if (!isLikelyUserId(id)) return false; + const cached = userNameMap.value[id] || userNameMap.value[id.toLowerCase()]; + return !cached || cached === id; + }); + if (!targets.length) return; + + // 优先批量补齐(现成后端接口 getCorpMember 支持 memberList) + await batchFetchCorpMemberDisplayNames(targets); + + const limit = 6; + let idx = 0; + const workers = Array.from({ length: Math.min(limit, targets.length) }, async () => { + while (idx < targets.length) { + const cur = targets[idx++]; + try { + const cached = userNameMap.value?.[cur] || userNameMap.value?.[cur.toLowerCase()]; + if (cached && cached !== cur) continue; + await fetchCorpMemberDisplayName(cur); + } catch { + // ignore + } + } + }); + + await Promise.allSettled(workers); +} + function normalizeTeam(raw) { if (!raw || typeof raw !== 'object') return null; const teamId = raw.teamId || raw.id || raw._id || ''; @@ -235,23 +507,76 @@ function ensureUserInfoForFeature() { return false; } +async function ensureDoctorForQuery() { + if (account.value?.openid) { + try { + const a = account.value || {}; + const accountId = normalizeUserId(a.userid || a.userId || a.corpUserId || ''); + const d = doctorInfo.value || {}; + const doctorId = normalizeUserId(d.userid || d.userId || d.corpUserId || ''); + // doctorInfo 可能是旧缓存:当与当前账号不一致时强制刷新 + if (!doctorId || (accountId && doctorId && accountId !== doctorId)) { + await getDoctorInfo(); + } + } catch { + // ignore + } + } + return Boolean(getUserId()); +} + async function loadTeamMembers() { const corpId = getCorpId(); const teamId = getTeamId(); if (!corpId || !teamId) return; + if (loadedMembersTeamId.value === teamId && Object.keys(userNameMap.value || {}).length > 0) return; try { + const nextMap = { ...(userNameMap.value || {}) }; + + // 以团队详情为准(getTeamMemberAvatarsAndName 存在不全/不准的情况) const res = await api('getTeamData', { corpId, teamId }); - if (!res?.success) return; - const t = res?.data && typeof res.data === 'object' ? res.data : {}; - const members = Array.isArray(t.memberList) ? t.memberList : []; - // Update map - members.forEach(m => { - const uid = String(m?.userid || ''); - if (uid) { - userNameMap.value[uid] = String(m?.anotherName || m?.name || m?.userid || '') || uid; - } - }); + if (res?.success) { + const t = res?.data && typeof res.data === 'object' ? res.data : {}; + const members = Array.isArray(t.memberList) ? t.memberList : []; + members.forEach((m) => { + if (typeof m === 'string') { + const k = normalizeUserId(m); + if (k) { + nextMap[k] = nextMap[k] || k; + nextMap[k.toLowerCase()] = nextMap[k.toLowerCase()] || nextMap[k] || k; + } + return; + } + const display = String(m?.anotherName || m?.name || m?.userid || m?.userId || m?.corpUserId || '').trim(); + const keys = [m?.userid, m?.userId, m?.corpUserId].map(normalizeUserId).filter(Boolean); + keys.forEach((k) => { + nextMap[k] = display || nextMap[k] || k; + nextMap[String(k).toLowerCase()] = display || nextMap[String(k).toLowerCase()] || k; + }); + }); + } + + // 补缺:仅当当前没有映射时才用 avatars 接口补齐,避免覆盖正确姓名 + try { + const avatarRes = await api('getTeamMemberAvatarsAndName', { corpId, teamId }); + const mapObj = avatarRes?.success && avatarRes?.data && typeof avatarRes.data === 'object' ? avatarRes.data : {}; + Object.entries(mapObj).forEach(([uid, info]) => { + const k = normalizeUserId(uid); + if (!k) return; + const existing = nextMap[k] || nextMap[k.toLowerCase()]; + if (existing && existing !== k) return; + const display = String(info?.name || info?.anotherName || info?.userid || '').trim(); + if (!display || display === k) return; + nextMap[k] = display; + nextMap[k.toLowerCase()] = display; + }); + } catch { + // ignore + } + + userNameMap.value = nextMap; + loadedMembersTeamId.value = teamId; } catch (e) { console.error('获取团队成员失败', e); } @@ -260,7 +585,7 @@ async function loadTeamMembers() { function resolveCreatorName(patient) { const val = patient.creator; if (!val) return ''; - return userNameMap.value[val] || val; + return resolveUserName(val); } function resolveRecentAddTime(patient) { @@ -268,9 +593,12 @@ function resolveRecentAddTime(patient) { } function resolveRecentAddOperatorName(patient) { - const uid = patient?.recentAddOperatorUserId || patient?.creator || ''; + const nameFromApi = String(patient?.recentAddOperatorName || '').trim(); + // 后端部分场景会把 userid 填到 name 字段里,此时仍需通过 userid 解析姓名 + if (nameFromApi && !isLikelyUserId(nameFromApi)) return nameFromApi; + const uid = patient?.recentAddOperatorUserId || nameFromApi || patient?.creator || ''; if (!uid) return ''; - return userNameMap.value[uid] || uid; + return resolveUserName(uid); } function resolveRecentAddAction(patient) { @@ -396,6 +724,205 @@ function parseCreateTime(value) { return d2.isValid() ? d2 : null; } +function normalizeMedicalType(raw) { + const s = String(raw || '').trim(); + if (!s) return ''; + const lower = s.toLowerCase(); + // 常见后缀:xxxRecord(后端/历史数据可能返回此形式) + if (lower.endsWith('record') && s.length > 6) { + return normalizeMedicalType(s.slice(0, -6)); + } + // 中文兜底(部分接口返回展示名) + if (s.includes('门诊')) return 'outpatient'; + if (s.includes('住院') || s.includes('入院')) return 'inhospital'; + if (s.includes('预问诊') || s.includes('问诊')) return 'preConsultation'; + if (s.includes('体检')) return 'physicalExaminationTemplate'; + if (lower.includes('preconsult') || (lower.includes('pre') && lower.includes('consult'))) return 'preConsultation'; + if (lower === 'outpatient' || lower === 'out_patient' || lower === 'out-patient') return 'outpatient'; + if (lower === 'inhospital' || lower === 'in_hospital' || lower === 'in-hospital' || lower === 'inpatient') return 'inhospital'; + if (lower === 'physicalexaminationtemplate' || lower === 'physicalexamination' || lower === 'physical_examination') return 'physicalExaminationTemplate'; + if (s === 'outPatient') return 'outpatient'; + if (s === 'inHospital') return 'inhospital'; + if (s === 'preConsultation') return 'preConsultation'; + if (s === 'physicalExaminationTemplate') return 'physicalExaminationTemplate'; + return s; +} + +function normalizeText(v) { + if (Array.isArray(v)) { + const parts = v + .map((i) => normalizeText(i)) + .filter((i) => i !== null && i !== undefined && String(i).trim()); + return parts.join(','); + } + if (v === 0) return '0'; + if (v && typeof v === 'object') { + const o = v; + const candidate = o.label ?? o.name ?? o.text ?? o.title ?? o.value ?? o.diseaseName ?? o.code ?? ''; + return candidate ? String(candidate) : ''; + } + return v ? String(v) : ''; +} + +function formatAnyDate(value, fmt = 'YYYY-MM-DD') { + const d = parseCreateTime(value); + return d ? d.format(fmt) : ''; +} + +function resolveLatestRecord(lr) { + if (!lr || typeof lr !== 'object') return null; + + const formData = lr?.formData && typeof lr.formData === 'object' ? lr.formData : null; + const recordData = lr?.recordData && typeof lr.recordData === 'object' ? lr.recordData : null; + const data = lr?.data && typeof lr.data === 'object' ? lr.data : null; + + const hasValue = (v) => { + if (v === 0) return true; + if (Array.isArray(v)) return v.length > 0; + if (v && typeof v === 'object') return Object.keys(v).length > 0; + return v !== null && v !== undefined && String(v).trim() !== ''; + }; + + const pick = (...keys) => { + for (const k of keys) { + const v0 = lr?.[k]; + if (hasValue(v0)) return v0; + const v1 = formData?.[k]; + if (hasValue(v1)) return v1; + const v2 = recordData?.[k]; + if (hasValue(v2)) return v2; + const v3 = data?.[k]; + if (hasValue(v3)) return v3; + } + return undefined; + }; + + const rawType = String(pick('medicalType', 'templateType', 'type') || '').trim(); + const uiType = normalizeMedicalType(rawType); + // 注意:不要用 type 字段兜底“名称”,很多场景 type 是类型码(如 preConsultation / physicalExaminationTemplate) + const typeLabelRaw = String( + pick( + 'tempName', + 'templateName', + 'templateTitle', + 'tempTitle', + 'recordName', + 'medicalName', + 'medicalRecordName', + 'consultName', + 'preConsultationName', + 'inspectName', + 'physicalName', + 'name' + ) || '' + ).trim(); + const typeLabel = + typeLabelRaw && normalizeMedicalType(typeLabelRaw) === uiType ? '' : typeLabelRaw; + + const rawDate = pick( + 'date', + 'visitTime', + 'inhosDate', + 'consultDate', + 'inspectDate', + 'sortTime', + 'createTime', + 'createdAt', + 'updateTime', + 'updatedAt', + 'time' + ); + const rawDateStr = String(rawDate ?? '').trim(); + const date = (/^\d{10,13}$/.test(rawDateStr) ? (formatAnyDate(rawDateStr, 'YYYY-MM-DD') || rawDateStr) : rawDateStr) + || formatAnyDate(pick('visitTime', 'inhosDate', 'consultDate', 'inspectDate', 'sortTime', 'createTime', 'updateTime'), 'YYYY-MM-DD') + || '-'; + + let third = ''; + // 后端若已计算出统一展示字段 diagnosis,则优先使用 + const directDiagnosis = normalizeText(pick('diagnosis', 'diagnosisName')); + if (String(directDiagnosis || '').trim()) { + third = directDiagnosis; + } else if (uiType === 'outpatient' || uiType === 'inhospital') { + third = normalizeText(pick( + 'diagnosisName', + 'diagnosis', + 'diagnosisList', + 'diagnosisNames', + 'mainDiagnosis', + 'admissionDiagnosis', + 'inDiagnosis', + 'outDiagnosis', + 'outPatientDiagnosis', + 'inHospitalDiagnosis' + )); + } else if (uiType === 'preConsultation') { + third = normalizeText( + pick( + 'diagnosis', + 'diagnosisName', + 'chiefComplaint', + 'chiefComplain', + 'chiefComplaintText', + 'chiefComplaintContent', + 'complaint', + 'complaintDesc', + 'complaintText', + 'mainComplaint', + 'mainSuit', + 'mainSuitText', + 'mainSuitContent', + 'chief', + 'zs', + 'zhuSu', + 'cc', + 'presentIllness', + 'historyOfPresentIllness', + 'currentIllness' + ) + ); + } else if (uiType === 'physicalExaminationTemplate') { + third = normalizeText( + pick( + 'diagnosis', + 'diagnosisName', + 'summary', + 'summaryText', + 'inspectSummary', + 'checkSummary', + 'examSummary', + 'physicalSummary', + 'briefSummary', + 'resultSummary', + 'conclusion', + 'conclusionText', + 'inspectConclusion', + 'inspectResult', + 'finalConclusion', + 'finalSummary', + 'reportConclusion', + 'reportSummary' + ) + ); + } else { + third = normalizeText(pick('diagnosis', 'diagnosisName', 'summary', 'chiefComplaint')); + } + third = String(third || '').replace(/\s+/g, ' ').trim(); + if (!third) { + // 最后的兜底:避免经常展示为 '-' + third = normalizeText(pick('summary', 'inspectSummary', 'chiefComplaint', 'presentIllness', 'treatmentPlan', 'abstract', 'brief')); + third = String(third || '').replace(/\s+/g, ' ').trim(); + } + if (!String(third || '').trim()) third = '-'; + + const type = typeLabel || (uiType === 'outpatient' ? '门诊记录' + : uiType === 'inhospital' ? '住院记录' + : uiType === 'preConsultation' ? '预问诊记录' + : uiType === 'physicalExaminationTemplate' ? '体检档案' + : '-'); + + return { type, date, diagnosis: third }; +} + function formatPatient(raw) { const name = raw?.name || raw?.customerName || ''; const sex = raw?.sex || raw?.gender || ''; @@ -427,20 +954,16 @@ function formatPatient(raw) { // 解析病历信息 let record = null; - if (raw?.latestRecord && typeof raw.latestRecord === 'object') { - const lr = raw.latestRecord; - const type = lr.type || ''; - const date = lr.date || ''; - const diagnosis = lr.diagnosis || ''; - // 只有存在有效信息时才设置 record - if (type || date || diagnosis) { - record = { - type: type || '-', - date: date || '-', - diagnosis: diagnosis || '-' - }; - } - } + const latestRaw = + raw?.latestRecord ?? + raw?.latestMedicalRecord ?? + raw?.latestMedicalCase ?? + raw?.lastRecord ?? + raw?.lastMedicalRecord ?? + raw?.recentRecord ?? + null; + const latest = Array.isArray(latestRaw) ? latestRaw[0] : latestRaw; + if (latest && typeof latest === 'object') record = resolveLatestRecord(latest); return { ...raw, @@ -457,6 +980,14 @@ function formatPatient(raw) { recentAddTimeTs, recentAddType, recentAddOperatorUserId, + recentAddOperatorName: String( + raw?.recentAddOperatorName + || raw?.recentAddOperatorAnotherName + || raw?.recentAddOperatorUserName + || raw?.recentAddOperatorRealName + || raw?.recentAddOperatorDisplayName + || '' + ).trim(), creator: raw?.creatorName || raw?.creator || '', hospitalId: raw?.customerNumber || raw?.hospitalId || '', record, @@ -518,6 +1049,7 @@ async function reload(reset = true) { if (!currentTeam.value) return; if (loading.value) return; + await ensureDoctorForQuery(); const userId = getUserId(); const corpId = getCorpId(); const teamId = getTeamId(); @@ -571,6 +1103,8 @@ async function reload(reset = true) { const list = Array.isArray(payload.list) ? payload.list : Array.isArray(payload.data) ? payload.data : []; const next = list.map(formatPatient); rawPatients.value = page.value === 1 ? next : [...rawPatients.value, ...next]; + // 补齐创建人/新增人姓名(部分创建人不在当前团队成员列表中) + void prefetchUserNamesFromPatients(next).catch(() => {}); pages.value = Number(payload.pages || 0) || 0; totalFromApi.value = Number(payload.total || 0) || rawPatients.value.length; managedArchiveCountAllTeams.value = @@ -665,13 +1199,13 @@ const toggleTeamPopup = () => { } uni.showActionSheet({ itemList: teams.value.map((i) => i.name), - success: function (res) { + success: async (res) => { currentTeam.value = teams.value[res.tapIndex] || teams.value[0] || null; if (currentTeam.value) uni.setStorageSync(CURRENT_TEAM_STORAGE_KEY, currentTeam.value); currentTabKey.value = 'all'; - loadGroups(); - loadTeamMembers(); - reload(true); + await loadGroups(); + await loadTeamMembers(); + await reload(true); } }); }; @@ -683,15 +1217,14 @@ const goToSearch = () => { }); }; -const goToGroupManage = () => { +const goToGroupManage = withInfo(() => { if (checkBatchMode()) return; - if (!ensureUserInfoForFeature()) return; uni.navigateTo({ url: '/pages/case/group-manage' }); -}; +}); -const toggleBatchMode = () => { +const toggleBatchMode = withInfo(() => { if (isBatchMode.value) { // Already in batch mode, do nothing or prompt? // Prompt says "click Other operations... prompt please finish". @@ -701,9 +1234,9 @@ const toggleBatchMode = () => { } isBatchMode.value = true; selectedItems.value = []; -}; +}); -const handleCreate = () => { +const handleCreate = withInfo(() => { if (checkBatchMode()) return; const rawMax = doctorInfo.value?.maxCustomerArchive; const hasMaxField = rawMax !== undefined && rawMax !== null && String(rawMax).trim() !== ''; @@ -775,7 +1308,7 @@ const handleCreate = () => { } } }); -}; +}); // 新增流程:认证分支 const startVerifyFlow = () => { @@ -1016,6 +1549,7 @@ watch(currentTeam, (t) => { watch(currentTabKey, () => { if (!currentTeam.value) return; + loadTeamMembers(); reload(true); }); @@ -1032,7 +1566,7 @@ onLoad(async () => { await loadTeams(); if (currentTeam.value) { await loadGroups(); - loadTeamMembers(); + await loadTeamMembers(); await reload(true); } await refreshVerifyStatus(); @@ -1056,7 +1590,7 @@ onShow(async () => { } else { await loadGroups(); } - loadTeamMembers(); + await loadTeamMembers(); await refreshVerifyStatus(); }); diff --git a/pages/message/index.vue b/pages/message/index.vue index 3f73eab..48ec97a 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 } 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 && diff --git a/utils/api.js b/utils/api.js index 18a53f9..7b43521 100644 --- a/utils/api.js +++ b/utils/api.js @@ -3,11 +3,14 @@ import request from "./http"; const urlsConfig = { corp: { getCorpMemberHomepageInfo: 'getCorpMemberHomepageInfo', + getCorpMember: 'getCorpMember', + getCorpMemberOptions: 'getCorpMemberOptions', // 企业信息/标签 getCorpInfo: 'getCorpInfo', getCorpTags: 'getCorpTags', getTeamBaseInfo: 'getTeamBaseInfo', getTeamData: 'getTeamData', + getTeamById: 'getTeamById', getTeamBymember: 'getTeamBymember', getCurrentTemplate: 'getCurrentTemplate', getTemplateGroup: 'getTemplateGroup', diff --git a/utils/send-message-helper.js b/utils/send-message-helper.js index 01bc69a..c35dd93 100644 --- a/utils/send-message-helper.js +++ b/utils/send-message-helper.js @@ -6,6 +6,38 @@ import { globalTimChatManager } from './tim-chat.js'; import api from './api.js'; import { toast } from './widget.js'; const env = __VITE_ENV__; + +function nowTs() { + return Date.now(); +} + +async function tryAddServiceRecord(payload) { + try { + const res = await api('addServiceRecord', payload); + if (!res?.success) { + console.warn('写入服务记录失败:', res?.message || res); + } + } catch (e) { + console.error('写入服务记录异常:', e); + } +} + +function canWriteServiceRecord(options = {}) { + return Boolean(options?.corpId && options?.userId && options?.customerId); +} + +function normalizeServiceRecordBase(options = {}) { + return { + corpId: options.corpId, + executorUserId: String(options.userId || ''), + creatorUserId: String(options.userId || ''), + customerId: String(options.customerId || ''), + customerName: String(options.customerName || ''), + executeTeamId: String(options.teamId || options.executeTeamId || ''), + executionTime: nowTs(), + externalUserId: String(options.externalUserId || options.customerUserId || ''), + }; +} /** * 发送文字消息 * @param {string} content - 文字内容 @@ -168,6 +200,24 @@ export async function sendArticleMessage(article, options = {}) { console.error('记录文章发送失败:', err); }); } + + // 写入服务记录留痕(异步,不阻塞) + if (canWriteServiceRecord(options)) { + const base = normalizeServiceRecordBase(options); + tryAddServiceRecord({ + ...base, + eventType: 'ContentReminder', + taskContent: `推送文章:${article.title || '宣教文章'}`, + pannedEventSendFile: { + type: 'article', + articleId: article._id || options.articleId || '', + title: article.title || '', + url: article.url || '', + }, + pannedEventName: '宣教发送', + }); + } + return true; } else { toast(result?.error || '发送文章消息失败'); @@ -243,6 +293,22 @@ export async function sendSurveyMessage(survey, options = {}) { const result = await globalTimChatManager.sendCustomMessage(customMessageData); if (result?.success) { + // 写入服务记录留痕(异步,不阻塞) + if (canWriteServiceRecord(options)) { + const base = normalizeServiceRecordBase(options); + tryAddServiceRecord({ + ...base, + eventType: 'questionnaire', + taskContent: `推送问卷:${survey.name || '问卷'}`, + pannedEventSendFile: { + type: 'questionnaire', + surveryId: survey._id || survey.surveryId || '', + name: survey.name || '', + url: surveyLink || '', + }, + pannedEventName: '问卷调查', + }); + } return true; } else { toast(result?.error || '发送问卷消息失败');