视频会议

This commit is contained in:
duoaohui
2026-05-23 14:15:19 +08:00
parent 2689301721
commit 2e66717932
5 changed files with 130 additions and 49 deletions

View File

@@ -95,6 +95,29 @@ import { useChatStore } from '@/store/chat'
import { useContactStore } from '@/store/contact' import { useContactStore } from '@/store/contact'
import { useNotifyStore } from '@/store/notify' import { useNotifyStore } from '@/store/notify'
function readSsoQueryFromLocation() {
try {
if (typeof window === 'undefined') return ''
const hash = String(window.location.hash || '')
if (hash.indexOf('/pages/auth/sso') >= 0 && hash.indexOf('?') >= 0) {
return hash.slice(hash.indexOf('?') + 1)
}
const search = String(window.location.search || '').replace(/^\?/, '')
if (search.indexOf('payload=') >= 0 || search.indexOf('token=') >= 0 || search.indexOf('cloudLawSso=') >= 0) {
return search
}
} catch (e) {}
return ''
}
function redirectToSsoIfPresent() {
const query = readSsoQueryFromLocation()
if (!query) return false
console.warn('[Login] detected SSO params on login page, redirect to SSO')
uni.redirectTo({ url: `/pages/auth/sso?${query}` })
return true
}
export default { export default {
name: 'LoginPage', name: 'LoginPage',
data() { data() {
@@ -106,6 +129,9 @@ export default {
errors: { account: '', password: '' } errors: { account: '', password: '' }
} }
}, },
onLoad() {
redirectToSsoIfPresent()
},
methods: { methods: {
/** 失焦时触发对应字段校验 */ /** 失焦时触发对应字段校验 */
onBlur(field) { onBlur(field) {

View File

@@ -146,6 +146,13 @@ function bootstrapAfterSso(token) {
onLoad((query) => { onLoad((query) => {
try { try {
try {
console.log('[SSO] location:', {
search: typeof window !== 'undefined' ? window.location.search : '',
hash: typeof window !== 'undefined' ? window.location.hash : '',
query
})
} catch (logErr) {}
const payload = resolveSsoPayload(query || {}) const payload = resolveSsoPayload(query || {})
const ssoUser = resolveSsoUser(payload) const ssoUser = resolveSsoUser(payload)
const redirect = resolveRedirect(query || {}) const redirect = resolveRedirect(query || {})

View File

@@ -1,4 +1,4 @@
<!-- <!--
启动页 / 路由分发页 启动页 / 路由分发页
功能 功能
@@ -23,9 +23,30 @@
*/ */
import { useUserStore } from '@/store/user' import { useUserStore } from '@/store/user'
function readSsoQueryFromLocation() {
try {
if (typeof window === 'undefined') return ''
const hash = String(window.location.hash || '')
if (hash.indexOf('/pages/auth/sso') >= 0 && hash.indexOf('?') >= 0) {
return hash.slice(hash.indexOf('?') + 1)
}
const search = String(window.location.search || '').replace(/^\?/, '')
if (search.indexOf('payload=') >= 0 || search.indexOf('token=') >= 0 || search.indexOf('cloudLawSso=') >= 0) {
return search
}
} catch (e) {}
return ''
}
export default { export default {
name: 'LaunchPage', name: 'LaunchPage',
onLoad() { onLoad() {
const ssoQuery = readSsoQueryFromLocation()
if (ssoQuery) {
console.warn('[Index] detected SSO params on launch page, redirect to SSO')
uni.redirectTo({ url: `/pages/auth/sso?${ssoQuery}` })
return
}
const store = useUserStore() const store = useUserStore()
if (store.isLoggedIn) { if (store.isLoggedIn) {
uni.switchTab({ url: '/pages/chat/index' }) uni.switchTab({ url: '/pages/chat/index' })
@@ -50,3 +71,4 @@ export default {
color: #94A3B8; color: #94A3B8;
} }
</style> </style>

View File

@@ -15,7 +15,10 @@
入会成功后uni.redirectTo('/pages/meeting/room?code=xxx') 入会成功后uni.redirectTo('/pages/meeting/room?code=xxx')
--> -->
<template> <template>
<view class="page"> <view v-if="initializing || autoJoin" class="auto-join-page">
<text class="auto-join-title">正在进入会议</text>
</view>
<view v-else class="page">
<view class="container"> <view class="container">
<text class="title">设备预览</text> <text class="title">设备预览</text>
<text class="subtitle">{{ modeLabel }}确认设备和显示名称后加入会议</text> <text class="subtitle">{{ modeLabel }}确认设备和显示名称后加入会议</text>
@@ -112,6 +115,7 @@ const mode = ref('create')
const joinCode = ref('') const joinCode = ref('')
const joinPassword = ref('') const joinPassword = ref('')
const autoJoin = ref(false) const autoJoin = ref(false)
const initializing = ref(true)
const modeLabel = computed(() => mode.value === 'create' ? '即将创建新会议' : `即将加入会议 ${joinCode.value}`) const modeLabel = computed(() => mode.value === 'create' ? '即将创建新会议' : `即将加入会议 ${joinCode.value}`)
@@ -359,17 +363,20 @@ const onCancel = () => {
uni.navigateBack() uni.navigateBack()
} }
const onJoin = async () => { const onJoin = async (options = {}) => {
if (joining.value || !canJoin.value) return if (joining.value || !canJoin.value) return
joining.value = true joining.value = true
const skipMedia = options.skipMedia === true
const shouldStartAudio = !skipMedia && mediaPrefs.startAudio
const shouldStartVideo = !skipMedia && mediaPrefs.startVideo
meetingStore.devicePreview = { meetingStore.devicePreview = {
audioDeviceId: selectedAudioId.value, audioDeviceId: selectedAudioId.value,
videoDeviceId: selectedVideoId.value, videoDeviceId: selectedVideoId.value,
speakerDeviceId: selectedSpeakerId.value, speakerDeviceId: selectedSpeakerId.value,
displayName: displayName.value, displayName: displayName.value,
startAudio: mediaPrefs.startAudio, startAudio: shouldStartAudio,
startVideo: mediaPrefs.startVideo startVideo: shouldStartVideo
} }
// iOS Safari/Chrome 上 preview 释放摄像头后 room 再次 getUserMedia 常触发 NotReadableError // iOS Safari/Chrome 上 preview 释放摄像头后 room 再次 getUserMedia 常触发 NotReadableError
@@ -380,21 +387,21 @@ const onJoin = async () => {
const rawAudioTrack = previewStream ? previewStream.getAudioTracks()[0] || null : null const rawAudioTrack = previewStream ? previewStream.getAudioTracks()[0] || null : null
const rawVideoTrack = previewStream ? previewStream.getVideoTracks()[0] || null : null const rawVideoTrack = previewStream ? previewStream.getVideoTracks()[0] || null : null
// 若用户关掉了"默认开麦/默认开摄像头",对应 track 不会被 mediasoup 接手,立即 stop 避免硬件泄漏 // 若用户关掉了"默认开麦/默认开摄像头",对应 track 不会被 mediasoup 接手,立即 stop 避免硬件泄漏
if (rawAudioTrack && !mediaPrefs.startAudio) { if (rawAudioTrack && !shouldStartAudio) {
try { rawAudioTrack.stop() } catch {} try { rawAudioTrack.stop() } catch {}
} }
if (rawVideoTrack && !mediaPrefs.startVideo) { if (rawVideoTrack && !shouldStartVideo) {
try { rawVideoTrack.stop() } catch {} try { rawVideoTrack.stop() } catch {}
} }
const audioTrack = mediaPrefs.startAudio ? rawAudioTrack : null const audioTrack = shouldStartAudio ? rawAudioTrack : null
const videoTrack = mediaPrefs.startVideo ? rawVideoTrack : null const videoTrack = shouldStartVideo ? rawVideoTrack : null
// previewStream 对象本身不再需要持有tracks 已取出交给 MediaEngine // previewStream 对象本身不再需要持有tracks 已取出交给 MediaEngine
// 避免在后续 onUnload 中误调 stopPreviewStream 把 track 给 stop 掉。 // 避免在后续 onUnload 中误调 stopPreviewStream 把 track 给 stop 掉。
previewStream = null previewStream = null
const prefs = { const prefs = {
startAudio: mediaPrefs.startAudio, startAudio: shouldStartAudio,
startVideo: mediaPrefs.startVideo, startVideo: shouldStartVideo,
audioDeviceId: selectedAudioId.value, audioDeviceId: selectedAudioId.value,
videoDeviceId: selectedVideoId.value, videoDeviceId: selectedVideoId.value,
audioTrack, audioTrack,
@@ -429,44 +436,51 @@ const onJoin = async () => {
// ==================== 生命周期 ==================== // ==================== 生命周期 ====================
onLoad(async (query) => { onLoad(async (query) => {
mode.value = query?.mode === 'join' ? 'join' : 'create' try {
joinCode.value = query?.code || '' mode.value = query?.mode === 'join' ? 'join' : 'create'
autoJoin.value = query?.autoJoin === '1' || query?.autoJoin === 'true' || query?.autoJoin === true joinCode.value = query?.code || ''
console.log('[Preview] onLoad params:', { autoJoin.value = query?.autoJoin === '1' || query?.autoJoin === 'true' || query?.autoJoin === true
query, console.log('[Preview] onLoad params:', {
mode: mode.value, query,
joinCode: joinCode.value, mode: mode.value,
autoJoin: autoJoin.value,
userId: userStore.userInfo?.id || userStore.userInfo?.user_id || ''
})
// P0-4密码不再从 URL query 读取,改为从 meetingStore.draftJoinPayload 一次性消费
// 防止 URL 历史 / DevTools 留痕;读取后立即清空草稿避免二次写回
if (mode.value === 'join') {
const draft = meetingStore.draftJoinPayload
if (draft && (!joinCode.value || draft.code === joinCode.value)) {
joinPassword.value = draft.password || ''
meetingStore.draftJoinPayload = null
} else {
joinPassword.value = ''
}
}
// 先请求权限并预览,默认拿到第一个设备
await startPreview()
// 权限到手后枚举设备拿到 label
await loadDevices()
// 若此时已选中某个设备,重新启动预览确保走明确 deviceId避免浏览器默认设备切换
if (hasPermission.value && (selectedVideoId.value || selectedAudioId.value)) {
await startPreview()
}
if (autoJoin.value && canJoin.value) {
await nextTick()
console.log('[Preview] autoJoin start:', {
joinCode: joinCode.value, joinCode: joinCode.value,
hasPermission: hasPermission.value, autoJoin: autoJoin.value,
permissionError: permissionError.value userId: userStore.userInfo?.id || userStore.userInfo?.user_id || ''
}) })
onJoin() // P0-4密码不再从 URL query 读取,改为从 meetingStore.draftJoinPayload 一次性消费
// 防止 URL 历史 / DevTools 留痕;读取后立即清空草稿避免二次写回
if (mode.value === 'join') {
const draft = meetingStore.draftJoinPayload
if (draft && (!joinCode.value || draft.code === joinCode.value)) {
joinPassword.value = draft.password || ''
meetingStore.draftJoinPayload = null
} else {
joinPassword.value = ''
}
}
if (autoJoin.value && canJoin.value) {
await nextTick()
console.log('[Preview] autoJoin start:', {
joinCode: joinCode.value,
hasPermission: hasPermission.value,
permissionError: permissionError.value
})
onJoin({ skipMedia: true })
return
}
initializing.value = false
// 先请求权限并预览,默认拿到第一个设备
await startPreview()
// 权限到手后枚举设备拿到 label
await loadDevices()
// 若此时已选中某个设备,重新启动预览确保走明确 deviceId避免浏览器默认设备切换
if (hasPermission.value && (selectedVideoId.value || selectedAudioId.value)) {
await startPreview()
}
} catch (err) {
initializing.value = false
throw err
} }
}) })
@@ -481,6 +495,17 @@ onBeforeUnmount(() => {
</script> </script>
<style scoped> <style scoped>
.auto-join-page {
min-height: 100vh;
background: #F8FAFC;
display: flex;
align-items: center;
justify-content: center;
}
.auto-join-title {
font-size: 30rpx;
color: #475569;
}
.page { min-height: 100vh; background: #F8FAFC; padding: 24rpx; } .page { min-height: 100vh; background: #F8FAFC; padding: 24rpx; }
.container { max-width: 960rpx; margin: 0 auto; } .container { max-width: 960rpx; margin: 0 auto; }
.title { font-size: 40rpx; font-weight: 600; color: #0F172A; display: block; margin: 24rpx 0 8rpx; } .title { font-size: 40rpx; font-weight: 600; color: #0F172A; display: block; margin: 24rpx 0 8rpx; }

View File

@@ -38,6 +38,7 @@ function isOnSsoOrEmbeddedMeeting() {
if (hash.indexOf('/pages/auth/sso') >= 0) return true if (hash.indexOf('/pages/auth/sso') >= 0) return true
if (search.indexOf('pages/auth/sso') >= 0 || search.indexOf('payload=') >= 0 || search.indexOf('cloudLawSso=') >= 0) return true if (search.indexOf('pages/auth/sso') >= 0 || search.indexOf('payload=') >= 0 || search.indexOf('cloudLawSso=') >= 0) return true
if (window.sessionStorage && window.sessionStorage.getItem('echo_cloud_law_meeting_embed') === '1') return true if (window.sessionStorage && window.sessionStorage.getItem('echo_cloud_law_meeting_embed') === '1') return true
if (hash.indexOf('/pages/meeting/') >= 0 && hash.indexOf('autoJoin=') >= 0) return true
// 仅当作为 iframe 子页运行时,才把 meeting 路径视为嵌入 // 仅当作为 iframe 子页运行时,才把 meeting 路径视为嵌入
if (typeof window !== 'undefined' && window.parent && window.parent !== window) { if (typeof window !== 'undefined' && window.parent && window.parent !== window) {
if (hash.indexOf('/pages/meeting/') >= 0) return true if (hash.indexOf('/pages/meeting/') >= 0) return true