366 lines
12 KiB
Vue
366 lines
12 KiB
Vue
<template>
|
||
<div class="jitsi-meeting-view">
|
||
<!-- 加载中 -->
|
||
<div v-if="loading" class="loading-container">
|
||
<div class="loading-spinner"></div>
|
||
<div class="loading-text">正在加载会议...</div>
|
||
</div>
|
||
|
||
<!-- 错误提示 -->
|
||
<div v-else-if="error" class="error-container">
|
||
<div class="error-icon">⚠️</div>
|
||
<div class="error-title">无法加入会议</div>
|
||
<div class="error-message">{{ error }}</div>
|
||
<button class="error-btn" @click="retryJoin">重新尝试</button>
|
||
</div>
|
||
|
||
<!-- Jitsi 会议容器 -->
|
||
<div v-else ref="jitsiContainer" id="jitsi-meet-container" class="meeting-container"></div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { workcaseChatAPI } from '@/api/workcase'
|
||
// @ts-ignore
|
||
import { TokenManager } from 'shared/api'
|
||
import axios from 'axios'
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
|
||
const loading = ref(true)
|
||
const error = ref('')
|
||
const meetingId = ref('')
|
||
const roomId = ref('')
|
||
const jitsiContainer = ref<HTMLDivElement | null>(null)
|
||
let jitsiApi: any = null
|
||
|
||
// 从URL获取参数
|
||
const getMeetingParams = () => {
|
||
// 优先从query参数获取
|
||
meetingId.value = route.query.meetingId as string || ''
|
||
roomId.value = route.query.roomId as string || ''
|
||
const tokenParam = route.query.token as string || ''
|
||
|
||
console.log('[JitsiMeetingView] URL参数:', {
|
||
meetingId: meetingId.value,
|
||
roomId: roomId.value,
|
||
hasToken: !!tokenParam
|
||
})
|
||
|
||
return { meetingId: meetingId.value, roomId: roomId.value, token: tokenParam }
|
||
}
|
||
|
||
// 使用token刷新登录状态
|
||
const refreshLoginWithToken = async (token: string) => {
|
||
try {
|
||
console.log('[JitsiMeetingView] 使用token刷新登录状态...')
|
||
|
||
const response = await axios.post(
|
||
'/api/urban-lifeline/auth/refresh',
|
||
{},
|
||
{
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
}
|
||
)
|
||
|
||
if (response.data?.success && response.data?.data) {
|
||
const loginDomain = response.data.data
|
||
const newToken = loginDomain.token
|
||
|
||
// 保存到localStorage
|
||
localStorage.setItem('token', newToken)
|
||
localStorage.setItem('loginDomain', JSON.stringify(loginDomain))
|
||
|
||
// 使用TokenManager设置token
|
||
TokenManager.setToken(newToken)
|
||
|
||
console.log('[JitsiMeetingView] 登录状态刷新成功')
|
||
return true
|
||
} else {
|
||
console.error('[JitsiMeetingView] 刷新登录失败:', response.data?.message)
|
||
return false
|
||
}
|
||
} catch (err: any) {
|
||
console.error('[JitsiMeetingView] 刷新登录异常:', err)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 加载 Jitsi External API 脚本
|
||
const loadJitsiScript = (): Promise<void> => {
|
||
return new Promise((resolve, reject) => {
|
||
// 检查是否已经加载
|
||
if ((window as any).JitsiMeetExternalAPI) {
|
||
resolve()
|
||
return
|
||
}
|
||
|
||
const script = document.createElement('script')
|
||
script.src = 'http://localhost:8280/external_api.js'
|
||
script.async = true
|
||
script.onload = () => {
|
||
console.log('[JitsiMeetingView] Jitsi External API 脚本加载成功')
|
||
resolve()
|
||
}
|
||
script.onerror = () => {
|
||
reject(new Error('加载 Jitsi External API 失败'))
|
||
}
|
||
document.head.appendChild(script)
|
||
})
|
||
}
|
||
|
||
// 初始化 Jitsi Meet
|
||
const initJitsiMeet = async (jitsiServerUrl: string, roomName: string, jwt: string, displayName: string) => {
|
||
try {
|
||
console.log('[JitsiMeetingView] 初始化 Jitsi Meet:', {
|
||
server: jitsiServerUrl,
|
||
room: roomName,
|
||
name: displayName
|
||
})
|
||
|
||
// 加载 External API 脚本
|
||
await loadJitsiScript()
|
||
|
||
const JitsiMeetExternalAPI = (window as any).JitsiMeetExternalAPI
|
||
|
||
// 解析 URL 获取协议和域名
|
||
const urlObj = new URL(jitsiServerUrl)
|
||
const domain = urlObj.host // 获取 host (localhost:8280)
|
||
const useHttps = urlObj.protocol === 'https:'
|
||
|
||
console.log('[JitsiMeetingView] 解析服务器配置:', {
|
||
domain,
|
||
protocol: urlObj.protocol,
|
||
useHttps
|
||
})
|
||
|
||
// 配置选项 - 关键!指定是否使用 HTTPS
|
||
// 👇 替换你原有的 options 全部代码,保留原有逻辑,仅修改配置项
|
||
const options: any = {
|
||
roomName: roomName,
|
||
width: '100%',
|
||
height: '100%',
|
||
parentNode: jitsiContainer.value,
|
||
jwt: jwt,
|
||
// ✅ 修复1:核心!正确的顶层配置项是 useHTTPS(不是 https),强制关闭HTTPS
|
||
useHTTPS: false,
|
||
// ✅ 修复2:禁用WebSocket的HTTPS,彻底阻止wss://请求(局域网必加)
|
||
useWebSocket: false,
|
||
configOverwrite: {
|
||
startWithAudioMuted: false,
|
||
startWithVideoMuted: false,
|
||
enableWelcomePage: false,
|
||
prejoinPageEnabled: false,
|
||
disableDeepLinking: true,
|
||
enableChat: true,
|
||
enableScreenSharing: true,
|
||
// ✅ 修复3:叠加禁用,彻底阻断API内部的HTTPS强制逻辑(局域网核心)
|
||
useHTTPS: false,
|
||
// ✅ 修复4:禁用第三方HTTPS资源请求,避免混合内容报错
|
||
disableThirdPartyRequests: false,
|
||
// ✅ 修复5:关闭服务端的HTTPS重定向检测
|
||
disableHttpsRedirect: true
|
||
},
|
||
interfaceConfigOverwrite: {
|
||
SHOW_JITSI_WATERMARK: false,
|
||
SHOW_WATERMARK_FOR_GUESTS: false,
|
||
DISABLE_JOIN_LEAVE_NOTIFICATIONS: false
|
||
},
|
||
userInfo: {
|
||
displayName: displayName
|
||
}
|
||
}
|
||
|
||
console.log('[JitsiMeetingView] 创建 JitsiMeetExternalAPI 实例,https=' + useHttps)
|
||
jitsiApi = new JitsiMeetExternalAPI(domain, options)
|
||
|
||
// 监听会议准备就绪事件
|
||
jitsiApi.addEventListener('videoConferenceJoined', (event: any) => {
|
||
console.log('[JitsiMeetingView] 用户已加入会议:', event)
|
||
})
|
||
|
||
// 监听用户离开会议事件
|
||
jitsiApi.addEventListener('videoConferenceLeft', (event: any) => {
|
||
console.log('[JitsiMeetingView] 用户离开会议:', event)
|
||
handleLeaveMeeting()
|
||
})
|
||
|
||
// 监听准备关闭事件
|
||
jitsiApi.addEventListener('readyToClose', () => {
|
||
console.log('[JitsiMeetingView] Jitsi 准备关闭')
|
||
handleLeaveMeeting()
|
||
})
|
||
|
||
console.log('[JitsiMeetingView] Jitsi Meet 初始化完成')
|
||
} catch (err: any) {
|
||
console.error('[JitsiMeetingView] 初始化 Jitsi Meet 失败:', err)
|
||
error.value = err.message || '初始化会议失败'
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 加入会议
|
||
const joinMeeting = async () => {
|
||
try {
|
||
loading.value = true
|
||
error.value = ''
|
||
|
||
const { meetingId: mid, roomId: rid, token } = getMeetingParams()
|
||
|
||
if (!mid) {
|
||
error.value = '缺少会议ID参数'
|
||
loading.value = false
|
||
return
|
||
}
|
||
|
||
// 检查是否有loginDomain
|
||
const hasLoginDomain = !!localStorage.getItem('loginDomain')
|
||
const hasToken = !!localStorage.getItem('token') || !!token
|
||
|
||
console.log('[JitsiMeetingView] 登录状态检查:', {
|
||
hasLoginDomain,
|
||
hasToken
|
||
})
|
||
|
||
// 如果没有loginDomain但有token(小程序或外部链接访问),先刷新登录
|
||
if (!hasLoginDomain && token) {
|
||
const refreshed = await refreshLoginWithToken(token)
|
||
if (!refreshed) {
|
||
error.value = '登录验证失败,请重新获取会议链接'
|
||
loading.value = false
|
||
return
|
||
}
|
||
}
|
||
|
||
// 检查登录状态
|
||
if (!TokenManager.hasToken()) {
|
||
error.value = '未登录,请先登录'
|
||
loading.value = false
|
||
// 重定向到登录页
|
||
const currentUrl = window.location.href
|
||
window.location.href = `/login?redirect=${encodeURIComponent(currentUrl)}`
|
||
return
|
||
}
|
||
|
||
// 调用后端接口加入会议
|
||
console.log('[JitsiMeetingView] 正在加入会议:', mid)
|
||
const result = await workcaseChatAPI.joinVideoMeeting(mid)
|
||
|
||
if (result.success && result.data) {
|
||
const meetingData = result.data
|
||
|
||
// 检查会议数据
|
||
if (!meetingData.jitsiServerUrl || !meetingData.jitsiRoomName || !meetingData.jwtToken) {
|
||
error.value = '会议数据不完整,无法加入'
|
||
loading.value = false
|
||
return
|
||
}
|
||
|
||
// 获取用户名
|
||
const loginDomain = JSON.parse(localStorage.getItem('loginDomain') || '{}')
|
||
const displayName = loginDomain.user?.userName || '访客'
|
||
|
||
// 先设置 loading 为 false,让容器渲染出来
|
||
loading.value = false
|
||
|
||
// 等待下一个 tick,确保 DOM 已渲染
|
||
await nextTick()
|
||
|
||
// 检查容器是否已经渲染
|
||
if (!jitsiContainer.value) {
|
||
error.value = '会议容器未准备好'
|
||
return
|
||
}
|
||
|
||
// 使用 Jitsi External API 初始化会议
|
||
await initJitsiMeet(
|
||
meetingData.jitsiServerUrl,
|
||
meetingData.jitsiRoomName,
|
||
meetingData.jwtToken,
|
||
displayName
|
||
)
|
||
} else {
|
||
error.value = result.message || '加入会议失败'
|
||
loading.value = false
|
||
}
|
||
} catch (err: any) {
|
||
console.error('[JitsiMeetingView] 加入会议异常:', err)
|
||
error.value = err.message || '加入会议失败'
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 处理离开会议(不结束)
|
||
const handleLeaveMeeting = () => {
|
||
console.log('[JitsiMeetingView] 处理离开会议,返回聊天室:', roomId.value)
|
||
|
||
// 清理 Jitsi API
|
||
if (jitsiApi) {
|
||
jitsiApi.dispose()
|
||
jitsiApi = null
|
||
}
|
||
|
||
// 如果有roomId,返回到对应的聊天室
|
||
if (roomId.value) {
|
||
router.push(`/chatRoom?roomId=${roomId.value}`)
|
||
} else {
|
||
// 没有roomId,尝试返回上一页或关闭窗口
|
||
if (window.history.length > 1) {
|
||
router.back()
|
||
} else {
|
||
window.close()
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理结束会议
|
||
const handleEndMeeting = async () => {
|
||
try {
|
||
console.log('[JitsiMeetingView] 处理结束会议:', meetingId.value)
|
||
|
||
if (meetingId.value) {
|
||
// 调用后端接口结束会议
|
||
const result = await workcaseChatAPI.endVideoMeeting(meetingId.value)
|
||
console.log('[JitsiMeetingView] 结束会议结果:', result)
|
||
}
|
||
|
||
// 返回上一页或关闭窗口
|
||
handleLeaveMeeting()
|
||
} catch (err) {
|
||
console.error('[JitsiMeetingView] 结束会议失败:', err)
|
||
// 即使失败也返回上一页
|
||
handleLeaveMeeting()
|
||
}
|
||
}
|
||
|
||
// 重新尝试加入
|
||
const retryJoin = () => {
|
||
joinMeeting()
|
||
}
|
||
|
||
// 生命周期
|
||
onMounted(() => {
|
||
console.log('[JitsiMeetingView] 组件挂载')
|
||
joinMeeting()
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
console.log('[JitsiMeetingView] 组件卸载')
|
||
// 清理 Jitsi API
|
||
if (jitsiApi) {
|
||
jitsiApi.dispose()
|
||
jitsiApi = null
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
@import url('./JitsiMeetingView.scss')
|
||
</style>
|