From 2410f5e2e9bea17a0a5af6df8609bad8463dafde Mon Sep 17 00:00:00 2001 From: duoaohui <928970622@qq.com> Date: Sat, 6 Jun 2026 11:18:31 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BF=90=E8=90=A5=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/cloud-law/video-call.vue | 25 +++---- frontend/src/pages/meeting/preview.vue | 5 +- frontend/src/pages/meeting/room.vue | 16 +++++ frontend/src/utils/cloudLawVoiceCall.js | 72 +++++++++++++++++++++ 4 files changed, 105 insertions(+), 13 deletions(-) create mode 100644 frontend/src/utils/cloudLawVoiceCall.js diff --git a/frontend/src/pages/cloud-law/video-call.vue b/frontend/src/pages/cloud-law/video-call.vue index d6a407b..9127037 100644 --- a/frontend/src/pages/cloud-law/video-call.vue +++ b/frontend/src/pages/cloud-law/video-call.vue @@ -163,6 +163,11 @@ import { } from '@/constants/meeting' import VideoTile from '@/components/meeting/VideoTile.vue' import { minimizeCloudLawMeeting } from '@/utils/cloudLawMiniProgram' +import { + notifyCloudLawBackendEnd, + notifyCloudLawParentCallEnded, + isCloudLawPcIframe +} from '@/utils/cloudLawVoiceCall' import bgImage from '@/static/bg.png' import hangupIcon from '@/static/cacel.png' import voiceOnIcon from '@/static/vocie_calling_1.png' @@ -473,17 +478,9 @@ const requestCloudLawCommands = (url) => { } const notifyCloudLawEnd = () => { - if (!callId.value || !cloudLawApiBase.value) return Promise.resolve() - return new Promise(resolve => { - uni.request({ - url: resolveCloudLawApiUrl(`${voiceEndBase.value}/${encodeURIComponent(callId.value)}/end`), - method: 'POST', - data: {}, - header: cloudLawToken.value ? { 'Content-Type': 'application/json', token: `Bearer ${cloudLawToken.value}` } : { 'Content-Type': 'application/json' }, - timeout: 5000, - complete: resolve - }) - }) + if (!callId.value) return Promise.resolve() + const role = isLawRole.value ? 'law' : 'caller' + return notifyCloudLawBackendEnd(callId.value, role, cloudLawApiBase.value, cloudLawToken.value) } const buildMiniProgramReturnUrl = () => { @@ -1113,8 +1110,12 @@ onMounted(() => { const label = MEETING_ENDED_REASON_LABEL?.[reason] || '视频咨询已结束' uni.showToast({ title: label, icon: 'none' }) ended.value = true - // 对方挂断或会议结束时,通知小程序跳转到「我的咨询详情」 + // 对方挂断或会议结束时,通知云律后端 + 父页面(PC 律师浮窗)+ 小程序 notifyCloudLawEnd().finally(() => { + if (isLawRole.value) { + notifyCloudLawParentCallEnded(callId.value) + if (isCloudLawPcIframe()) return + } notifyMiniProgramCallEndedOnce() scheduleEndedRedirect() }) diff --git a/frontend/src/pages/meeting/preview.vue b/frontend/src/pages/meeting/preview.vue index 3bf8545..f27ef8e 100644 --- a/frontend/src/pages/meeting/preview.vue +++ b/frontend/src/pages/meeting/preview.vue @@ -126,6 +126,7 @@ const joinPassword = ref('') const autoJoin = ref(false) const cloudLawRole = ref('') const cloudLawCallId = ref('') +const cloudLawApiBase = ref('') const initializing = ref(true) // autoJoin 失败/超时的可见错误(替代无限"正在进入会议…"转圈) const autoJoinError = ref('') @@ -472,7 +473,8 @@ const onJoin = async (options = {}) => { const roomCode = meetingStore.currentRoom?.room_code const roleQ = cloudLawRole.value ? `&cloudLawRole=${encodeURIComponent(cloudLawRole.value)}` : '' const callQ = cloudLawCallId.value ? `&callId=${encodeURIComponent(cloudLawCallId.value)}` : '' - uni.redirectTo({ url: `/pages/meeting/room${roomCode ? `?code=${roomCode}${roleQ}${callQ}` : `${roleQ || callQ ? `?${(roleQ + callQ).replace(/^&/, '')}` : ''}`}` }) + const apiBaseQ = cloudLawApiBase.value ? `&cloudLawApiBase=${encodeURIComponent(cloudLawApiBase.value)}` : '' + uni.redirectTo({ url: `/pages/meeting/room${roomCode ? `?code=${roomCode}${roleQ}${callQ}${apiBaseQ}` : `${roleQ || callQ || apiBaseQ ? `?${(roleQ + callQ + apiBaseQ).replace(/^&/, '')}` : ''}`}` }) } catch (err) { const msg = err?.message || JSON.stringify(err) // autoJoin 模式下把错误显式呈现在页面(toast 一闪而过,真机很难看清) @@ -498,6 +500,7 @@ onLoad(async (query) => { joinCode.value = query?.code || '' cloudLawRole.value = query?.cloudLawRole || query?.cloud_law_role || '' cloudLawCallId.value = query?.callId || query?.call_id || '' + cloudLawApiBase.value = query?.cloudLawApiBase || query?.cloud_law_api_base || '' if (isCloudLaw.value) { try { uni.setNavigationBarTitle({ title: '用户视频咨询' }) diff --git a/frontend/src/pages/meeting/room.vue b/frontend/src/pages/meeting/room.vue index b75775a..82e7071 100644 --- a/frontend/src/pages/meeting/room.vue +++ b/frontend/src/pages/meeting/room.vue @@ -238,6 +238,12 @@ import { minimizeCloudLawMeeting, notifyCloudLawCallEnded } from '@/utils/cloudLawMiniProgram' +import { + isCloudLawLawRoleInMeeting, + isCloudLawPcIframe, + notifyCloudLawBackendEnd, + notifyCloudLawParentCallEnded +} from '@/utils/cloudLawVoiceCall' const meetingStore = useMeetingStore() const userStore = useUserStore() @@ -245,6 +251,7 @@ const userStore = useUserStore() /** 云律律师端:工具栏用共享屏幕替换聊天 */ const cloudLawRole = ref('') const cloudLawCallId = ref('') +const cloudLawApiBase = ref('') const minimizingToMiniProgram = ref(false) const replaceChatWithScreenShare = computed(() => String(cloudLawRole.value).toLowerCase() === 'law') const showCloudLawBack = computed(() => isCloudLawEmbed() && isCloudLawLawRole(cloudLawRole.value)) @@ -848,6 +855,7 @@ let redirectingToJoin = false onLoad((query) => { cloudLawRole.value = query?.cloudLawRole || query?.cloud_law_role || '' cloudLawCallId.value = query?.callId || query?.call_id || '' + cloudLawApiBase.value = query?.cloudLawApiBase || query?.cloud_law_api_base || '' if (!cloudLawRole.value) { // #ifdef H5 try { @@ -899,6 +907,14 @@ onMounted(() => { const reason = meetingStore.lastEndedReason const label = MEETING_ENDED_REASON_LABEL?.[reason] || '会议已结束' uni.showToast({ title: label, icon: 'none' }) + // 云律律师端(含 PC iframe):用户挂断后同步云律后端 + 通知父页面释放浮窗 + if (isCloudLawLawRoleInMeeting(cloudLawRole.value) && cloudLawCallId.value) { + notifyCloudLawBackendEnd(cloudLawCallId.value, 'law', cloudLawApiBase.value) + notifyCloudLawParentCallEnded(cloudLawCallId.value) + if (isCloudLawPcIframe()) { + return + } + } setTimeout(() => redirectHome(true), 1200) } }) diff --git a/frontend/src/utils/cloudLawVoiceCall.js b/frontend/src/utils/cloudLawVoiceCall.js new file mode 100644 index 0000000..01e03e3 --- /dev/null +++ b/frontend/src/utils/cloudLawVoiceCall.js @@ -0,0 +1,72 @@ +/** 云律视频咨询:iframe / web-view 内通知云律后端结束通话,并通知父页面(律师 PC 浮窗)释放状态 */ + +export function getCloudLawApiBase(explicitBase) { + const base = String(explicitBase || '').trim().replace(/\/+$/, '') + if (base) return base + if (typeof window !== 'undefined' && window.location && window.location.origin) { + return window.location.origin + } + return '' +} + +export function resolveCloudLawApiUrl(path, apiBase) { + const base = getCloudLawApiBase(apiBase) + const normalizedPath = path.startsWith('/') ? path : `/${path}` + return `${base}${normalizedPath}` +} + +/** + * 通知云律后端结束通话(律师端 /api/law/voiceCall,用户端 /api/applet/voiceConnection) + */ +export function notifyCloudLawBackendEnd(callId, role = 'law', apiBase = '', token = '') { + if (!callId) return Promise.resolve(false) + const endBase = String(role).toLowerCase() === 'law' ? '/api/law/voiceCall' : '/api/applet/voiceConnection' + const url = resolveCloudLawApiUrl(`${endBase}/${encodeURIComponent(callId)}/end`, apiBase) + const header = { 'Content-Type': 'application/json' } + if (token) { + header.token = token.startsWith('Bearer') ? token : `Bearer ${token}` + } + return new Promise(resolve => { + uni.request({ + url, + method: 'POST', + data: {}, + header, + timeout: 8000, + complete: () => resolve(true) + }) + }) +} + +/** PC 律师端 iframe 会议结束时,通知父页面(outbound 浮窗)清除「会议进行中」状态 */ +export function notifyCloudLawParentCallEnded(callId = '') { + if (typeof window === 'undefined') return false + try { + if (window.parent && window.parent !== window) { + window.parent.postMessage({ + type: 'cloudLawCallEnded', + action: 'cloudLawCallEnded', + callId: String(callId || '') + }, '*') + return true + } + } catch (e) { + console.warn('[cloudLawVoiceCall] parent postMessage failed', e) + } + return false +} + +export function isCloudLawLawRoleInMeeting(role) { + if (String(role || '').toLowerCase() === 'law') return true + try { + return typeof window !== 'undefined' + && window.sessionStorage + && window.sessionStorage.getItem('echo_cloud_law_meeting_role') === 'law' + } catch (e) { + return false + } +} + +export function isCloudLawPcIframe() { + return typeof window !== 'undefined' && !!(window.parent && window.parent !== window) +}