From cae3876d8b7b4ec39a60300e0b18d3b2cbc4665f Mon Sep 17 00:00:00 2001 From: duoaohui <928970622@qq.com> Date: Thu, 4 Jun 2026 15:52:22 +0800 Subject: [PATCH] =?UTF-8?q?=E7=94=B5=E8=AF=9D=E5=92=A8=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/cloud-law/video-call.vue | 24 +--- frontend/src/pages/meeting/preview.vue | 24 ++-- frontend/src/pages/meeting/room.vue | 131 ++++++++++++++------ frontend/src/utils/cloudLawMiniProgram.js | 80 ++++++++++++ 4 files changed, 188 insertions(+), 71 deletions(-) create mode 100644 frontend/src/utils/cloudLawMiniProgram.js diff --git a/frontend/src/pages/cloud-law/video-call.vue b/frontend/src/pages/cloud-law/video-call.vue index bb81ebc..d6a407b 100644 --- a/frontend/src/pages/cloud-law/video-call.vue +++ b/frontend/src/pages/cloud-law/video-call.vue @@ -162,6 +162,7 @@ import { MEETING_ENDED_REASON_LABEL } from '@/constants/meeting' import VideoTile from '@/components/meeting/VideoTile.vue' +import { minimizeCloudLawMeeting } from '@/utils/cloudLawMiniProgram' import bgImage from '@/static/bg.png' import hangupIcon from '@/static/cacel.png' import voiceOnIcon from '@/static/vocie_calling_1.png' @@ -1003,27 +1004,8 @@ const minimizeToMiniProgram = () => { minimizingToMiniProgram.value = true stopCommandPolling() meetingStore.leave().catch(() => {}) - const mp = getMiniProgram() - try { - if (mp && typeof mp.postMessage === 'function') { - mp.postMessage({ - data: { - type: 'cloudLawMinimized', - action: 'cloudLawMinimized', - callId: callId.value - } - }) - } - } catch (e) { - console.warn('[CloudLawVideoCall] minimize postMessage failed', e) - } - try { - if (mp && typeof mp.navigateBack === 'function') { - mp.navigateBack({ delta: 1 }) - return - } - } catch (e) { - console.warn('[CloudLawVideoCall] minimize navigateBack failed', e) + if (minimizeCloudLawMeeting(callId.value)) { + return } minimizingToMiniProgram.value = false uni.showToast({ title: '请使用左上角返回', icon: 'none' }) diff --git a/frontend/src/pages/meeting/preview.vue b/frontend/src/pages/meeting/preview.vue index f49d5bb..e565f9b 100644 --- a/frontend/src/pages/meeting/preview.vue +++ b/frontend/src/pages/meeting/preview.vue @@ -115,6 +115,7 @@ import { ref, reactive, computed, onBeforeUnmount, nextTick } from 'vue' import { onLoad } from '@dcloudio/uni-app' import { useMeetingStore } from '@/store/meeting' import { useUserStore } from '@/store/user' +import { isCloudLawEmbed, isCloudLawLawRole, minimizeCloudLawMeeting } from '@/utils/cloudLawMiniProgram' const meetingStore = useMeetingStore() const userStore = useUserStore() @@ -124,6 +125,7 @@ const joinCode = ref('') const joinPassword = ref('') const autoJoin = ref(false) const cloudLawRole = ref('') +const cloudLawCallId = ref('') const initializing = ref(true) // autoJoin 失败/超时的可见错误(替代无限"正在进入会议…"转圈) const autoJoinError = ref('') @@ -372,6 +374,10 @@ const onToggleVideoDefault = (e) => { mediaPrefs.startVideo = e.detail.value } const onToggleAudioDefault = (e) => { mediaPrefs.startAudio = e.detail.value } const onCancel = () => { + if (isCloudLawEmbed() && isCloudLawLawRole(cloudLawRole.value)) { + minimizeCloudLawMeeting(cloudLawCallId.value) + return + } uni.navigateBack() } @@ -398,16 +404,10 @@ const retryAutoJoin = () => { const exitAutoJoin = () => { clearAutoJoinWatchdog() - // 云律小程序 web-view 内:返回到小程序;否则回上一步 - try { - const embedded = typeof window !== 'undefined' && window.sessionStorage - && window.sessionStorage.getItem('echo_cloud_law_meeting_embed') === '1' - const mp = typeof window !== 'undefined' && window.wx && window.wx.miniProgram ? window.wx.miniProgram : null - if (embedded && mp && typeof mp.navigateBack === 'function') { - mp.navigateBack({ delta: 1 }) - return - } - } catch (e) {} + if (isCloudLawEmbed() && isCloudLawLawRole(cloudLawRole.value)) { + minimizeCloudLawMeeting(cloudLawCallId.value) + return + } onCancel() } @@ -469,7 +469,8 @@ const onJoin = async (options = {}) => { } const roomCode = meetingStore.currentRoom?.room_code const roleQ = cloudLawRole.value ? `&cloudLawRole=${encodeURIComponent(cloudLawRole.value)}` : '' - uni.redirectTo({ url: `/pages/meeting/room${roomCode ? `?code=${roomCode}${roleQ}` : (roleQ ? `?${roleQ.slice(1)}` : '')}` }) + const callQ = cloudLawCallId.value ? `&callId=${encodeURIComponent(cloudLawCallId.value)}` : '' + uni.redirectTo({ url: `/pages/meeting/room${roomCode ? `?code=${roomCode}${roleQ}${callQ}` : `${roleQ || callQ ? `?${(roleQ + callQ).replace(/^&/, '')}` : ''}`}` }) } catch (err) { const msg = err?.message || JSON.stringify(err) // autoJoin 模式下把错误显式呈现在页面(toast 一闪而过,真机很难看清) @@ -494,6 +495,7 @@ onLoad(async (query) => { mode.value = query?.mode === 'join' ? 'join' : 'create' joinCode.value = query?.code || '' cloudLawRole.value = query?.cloudLawRole || query?.cloud_law_role || '' + cloudLawCallId.value = query?.callId || query?.call_id || '' autoJoin.value = query?.autoJoin === '1' || query?.autoJoin === 'true' || query?.autoJoin === true // #ifdef H5 if (String(cloudLawRole.value).toLowerCase() === 'law') { diff --git a/frontend/src/pages/meeting/room.vue b/frontend/src/pages/meeting/room.vue index 908ac16..b75775a 100644 --- a/frontend/src/pages/meeting/room.vue +++ b/frontend/src/pages/meeting/room.vue @@ -15,6 +15,10 @@ + + + + {{ meetingTitle }} {{ formattedCode }} @@ -23,6 +27,7 @@ + @@ -227,13 +232,22 @@ import InviteDialog from '@/components/meeting/InviteDialog.vue' import NetworkBadge from '@/components/meeting/NetworkBadge.vue' import ChatPanel from '@/components/meeting/ChatPanel.vue' import SelfVideoFloat from '@/components/meeting/SelfVideoFloat.vue' +import { + isCloudLawEmbed, + isCloudLawLawRole, + minimizeCloudLawMeeting, + notifyCloudLawCallEnded +} from '@/utils/cloudLawMiniProgram' const meetingStore = useMeetingStore() const userStore = useUserStore() /** 云律律师端:工具栏用共享屏幕替换聊天 */ const cloudLawRole = ref('') +const cloudLawCallId = ref('') +const minimizingToMiniProgram = ref(false) const replaceChatWithScreenShare = computed(() => String(cloudLawRole.value).toLowerCase() === 'law') +const showCloudLawBack = computed(() => isCloudLawEmbed() && isCloudLawLawRole(cloudLawRole.value)) const memberVisible = ref(false) const inviteVisible = ref(false) @@ -740,7 +754,20 @@ const onTransferHost = async (uid) => { } } -const onLeaveClick = () => { leaveDialogVisible.value = true } +const onCloudLawBack = () => { + if (minimizingToMiniProgram.value) return + minimizingToMiniProgram.value = true + meetingStore.leave().catch(() => {}) + minimizeCloudLawMeeting(cloudLawCallId.value) +} + +const onLeaveClick = () => { + if (showCloudLawBack.value) { + onCloudLawBack() + return + } + leaveDialogVisible.value = true +} const confirmLeaveSelf = async () => { leaveDialogVisible.value = false @@ -775,53 +802,27 @@ const confirmEndOrLeave = async () => { } } -// 是否运行在云律小程序 web-view 内(SSO 进入时打的标记) -const isCloudLawEmbed = () => { - try { - return typeof window !== 'undefined' - && !!window.sessionStorage - && window.sessionStorage.getItem('echo_cloud_law_meeting_embed') === '1' - } catch (e) { - return false - } -} - -const getMiniProgram = () => { - return typeof window !== 'undefined' && window.wx && window.wx.miniProgram ? window.wx.miniProgram : null -} - // 退出会议视频界面:先给提示,再退出。 -// 在云律小程序 web-view 内 → 通知并 navigateBack 回到小程序(关闭会议 web-view 页); -// 普通浏览器/PC → 回 EchoChat 首页。 -const leaveAndExit = (tip) => { +// 云律律师端「返回」→ 最小化并弹出「返回会议」;真正挂断/结束 → cloudLawCallEnded。 +const leaveAndExit = (tip, ended = true) => { if (tip) { uni.showToast({ title: tip, icon: 'none', duration: 1200 }) } - // 留一点时间让提示可见,再退出视频界面 - setTimeout(redirectHome, tip ? 600 : 800) + setTimeout(() => redirectHome(ended), tip ? 600 : 800) } let meetingExited = false -const redirectHome = () => { +const redirectHome = (ended = true) => { if (meetingExited) return - const miniProgram = getMiniProgram() - if (isCloudLawEmbed() && miniProgram) { + if (isCloudLawEmbed() && isCloudLawLawRole(cloudLawRole.value)) { meetingExited = true - try { - miniProgram.postMessage({ data: { type: 'cloudLawCallEnded', action: 'cloudLawCallEnded' } }) - } catch (e) {} - try { - if (typeof miniProgram.navigateBack === 'function') { - miniProgram.navigateBack({ delta: 1 }) - return - } - if (typeof miniProgram.redirectTo === 'function') { - miniProgram.redirectTo({ url: '/pages/index/home_page/home_page' }) - return - } - } catch (e) { - meetingExited = false + const minimize = minimizingToMiniProgram.value || !ended + if (minimize) { + if (minimizeCloudLawMeeting(cloudLawCallId.value)) return + } else if (notifyCloudLawCallEnded(cloudLawCallId.value)) { + return } + meetingExited = false } uni.reLaunch({ url: '/pages/index/index' }) } @@ -838,6 +839,7 @@ const syncViewportHeight = () => { // ============ 生命周期 ============ let stateStopWatch = null +let popstateHandler = null // P2-3 修复:redirectTo 后必须 return,避免 onMounted 继续执行闪默认 UI // 使用 module-scoped 标记,onMounted 读取后判断是否提前退出 @@ -845,6 +847,7 @@ let redirectingToJoin = false onLoad((query) => { cloudLawRole.value = query?.cloudLawRole || query?.cloud_law_role || '' + cloudLawCallId.value = query?.callId || query?.call_id || '' if (!cloudLawRole.value) { // #ifdef H5 try { @@ -888,12 +891,31 @@ onMounted(() => { // 会议被主持人/后端结束(ENDED 状态)时,弹提示并回首页 stateStopWatch = watch(() => meetingStore.localState, (s) => { if (s === MEETING_LOCAL_STATE_ENDED) { + if (minimizingToMiniProgram.value) { + minimizingToMiniProgram.value = false + redirectHome(false) + return + } const reason = meetingStore.lastEndedReason const label = MEETING_ENDED_REASON_LABEL?.[reason] || '会议已结束' uni.showToast({ title: label, icon: 'none' }) - setTimeout(redirectHome, 1200) + setTimeout(() => redirectHome(true), 1200) } }) + + // #ifdef H5 + if (showCloudLawBack.value) { + try { + history.pushState({ cloudLawMeeting: 1 }, '') + popstateHandler = () => { + if (!minimizingToMiniProgram.value) { + onCloudLawBack() + } + } + window.addEventListener('popstate', popstateHandler) + } catch (e) {} + } + // #endif }) onBeforeUnmount(() => { @@ -903,6 +925,12 @@ onBeforeUnmount(() => { } if (viewportCleanup) { viewportCleanup(); viewportCleanup = null } if (stateStopWatch) { stateStopWatch(); stateStopWatch = null } + // #ifdef H5 + if (popstateHandler) { + window.removeEventListener('popstate', popstateHandler) + popstateHandler = null + } + // #endif }) onUnload(() => { @@ -949,6 +977,23 @@ onUnload(() => { z-index: 1000; } +.cloud-law-back { + width: 64rpx; + height: 64rpx; + margin-right: 12rpx; + border-radius: 50%; + background: rgba(255, 255, 255, 0.12); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} +.cloud-law-back-icon { + font-size: 44rpx; + line-height: 1; + color: #FFFFFF; + font-weight: 300; +} .top-bar { display: flex; align-items: center; @@ -962,6 +1007,14 @@ onUnload(() => { } .top-left { + display: flex; + flex-direction: row; + align-items: center; + gap: 6rpx; + min-width: 0; + flex: 1; +} +.top-left-info { display: flex; flex-direction: column; gap: 6rpx; diff --git a/frontend/src/utils/cloudLawMiniProgram.js b/frontend/src/utils/cloudLawMiniProgram.js new file mode 100644 index 0000000..cd11612 --- /dev/null +++ b/frontend/src/utils/cloudLawMiniProgram.js @@ -0,0 +1,80 @@ +/** 云律小程序 web-view 内与原生壳通信(返回会议浮窗 / 挂断) */ + +export function getMiniProgram() { + return typeof window !== 'undefined' && window.wx && window.wx.miniProgram ? window.wx.miniProgram : null +} + +export function isCloudLawEmbed() { + try { + return typeof window !== 'undefined' + && !!window.sessionStorage + && window.sessionStorage.getItem('echo_cloud_law_meeting_embed') === '1' + } catch (e) { + return false + } +} + +export function isCloudLawLawRole(role) { + if (String(role || '').toLowerCase() === 'law') return true + try { + return window.sessionStorage.getItem('echo_cloud_law_meeting_role') === 'law' + } catch (e) { + return false + } +} + +/** 律师端返回上一页:会议仍在进行,通知小程序弹出「返回会议」浮窗 */ +export function minimizeCloudLawMeeting(callId = '') { + const mp = getMiniProgram() + if (!isCloudLawEmbed() || !mp) return false + try { + if (typeof mp.postMessage === 'function') { + mp.postMessage({ + data: { + type: 'cloudLawMinimized', + action: 'cloudLawMinimized', + callId: String(callId || '') + } + }) + } + } catch (e) { + console.warn('[cloudLawMiniProgram] minimize postMessage failed', e) + } + try { + if (typeof mp.navigateBack === 'function') { + mp.navigateBack({ delta: 1 }) + return true + } + } catch (e) { + console.warn('[cloudLawMiniProgram] minimize navigateBack failed', e) + } + return false +} + +/** 会议真正结束:通知小程序释放占用 */ +export function notifyCloudLawCallEnded(callId = '') { + const mp = getMiniProgram() + if (!isCloudLawEmbed() || !mp) return false + try { + if (typeof mp.postMessage === 'function') { + mp.postMessage({ + data: { + type: 'cloudLawCallEnded', + action: 'cloudLawCallEnded', + callId: String(callId || '') + } + }) + } + } catch (e) { + console.warn('[cloudLawMiniProgram] ended postMessage failed', e) + } + try { + if (typeof mp.navigateBack === 'function') { + mp.navigateBack({ delta: 1 }) + return true + } + } catch (e) { + console.warn('[cloudLawMiniProgram] ended navigateBack failed', e) + } + return false +}