This commit is contained in:
2025-12-27 15:36:40 +08:00
parent 7c6fbc5ebe
commit 55801fa0ec
17 changed files with 1728 additions and 229 deletions

View File

@@ -0,0 +1,365 @@
<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>