视频会议
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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 || {})
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<!--
|
<!--
|
||||||
启动页 / 路由分发页
|
启动页 / 路由分发页
|
||||||
|
|
||||||
功能:
|
功能:
|
||||||
- 判断用户登录状态(检查本地 Token)
|
- 判断用户登录状态(检查本地 Token)
|
||||||
- 已登录 → 跳转消息列表页(TabBar 首页)
|
- 已登录 → 跳转消息列表页(TabBar 首页)
|
||||||
- 未登录 → 跳转登录页
|
- 未登录 → 跳转登录页
|
||||||
|
|
||||||
此页面不展示 UI,仅作为路由分发入口
|
此页面不展示 UI,仅作为路由分发入口
|
||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user