2025-12-26 10:37:52 +08:00
|
|
|
|
<template>
|
2025-12-27 13:23:07 +08:00
|
|
|
|
<!-- 半透明遮罩 + 底部弹窗 -->
|
|
|
|
|
|
<view class="meeting-modal">
|
|
|
|
|
|
<view class="modal-mask" @tap="handleMaskClick"></view>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- #ifdef MP-WEIXIN -->
|
|
|
|
|
|
<!-- 微信小程序环境:底部弹窗 -->
|
|
|
|
|
|
<view class="modal-content">
|
|
|
|
|
|
<view class="modal-header">
|
|
|
|
|
|
<text class="modal-title">{{ meetingName || '视频会议' }}</text>
|
|
|
|
|
|
<view class="close-btn" @tap="confirmExit">
|
|
|
|
|
|
<text class="close-icon">×</text>
|
|
|
|
|
|
</view>
|
2025-12-26 10:37:52 +08:00
|
|
|
|
</view>
|
2025-12-27 13:23:07 +08:00
|
|
|
|
|
|
|
|
|
|
<view class="modal-body">
|
|
|
|
|
|
<view class="meeting-info">
|
|
|
|
|
|
<view class="info-item">
|
|
|
|
|
|
<text class="info-label">会议名称:</text>
|
|
|
|
|
|
<text class="info-value">{{ meetingName || '未命名会议' }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="info-item">
|
|
|
|
|
|
<text class="info-label">会议ID:</text>
|
|
|
|
|
|
<text class="info-value">{{ meetingId || '-' }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view class="tips-box">
|
|
|
|
|
|
<text class="tips-icon">⚠️</text>
|
|
|
|
|
|
<text class="tips-text">微信小程序暂不支持视频会议,请在浏览器中打开</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view class="url-preview">
|
|
|
|
|
|
<text class="url-label">会议链接:</text>
|
|
|
|
|
|
<text class="url-text">{{ meetingUrl }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view class="modal-footer">
|
|
|
|
|
|
<button class="action-btn copy-btn" @tap="copyUrl">
|
|
|
|
|
|
<text>复制会议链接</text>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="action-btn browser-btn" @tap="openInBrowser">
|
|
|
|
|
|
<text>在浏览器中打开</text>
|
|
|
|
|
|
</button>
|
2025-12-26 10:37:52 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2025-12-27 13:23:07 +08:00
|
|
|
|
<!-- #endif -->
|
2025-12-26 10:37:52 +08:00
|
|
|
|
|
2025-12-27 13:23:07 +08:00
|
|
|
|
<!-- #ifndef MP-WEIXIN -->
|
|
|
|
|
|
<!-- 非微信小程序环境:全屏web-view -->
|
|
|
|
|
|
<view class="meeting-page-full">
|
|
|
|
|
|
<view class="meeting-nav" :style="{ paddingTop: statusBarHeight + 'px', height: navBarHeight + 'px' }">
|
|
|
|
|
|
<view class="nav-back" @tap="handleNavBack">
|
|
|
|
|
|
<text class="back-icon">←</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<text class="nav-title">{{ meetingName || '视频会议' }}</text>
|
|
|
|
|
|
<view class="nav-right" @tap="endMeeting">
|
|
|
|
|
|
<text class="end-btn">结束会议</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<web-view
|
|
|
|
|
|
ref="jitsiWebView"
|
|
|
|
|
|
:src="meetingUrl"
|
|
|
|
|
|
:webview-styles="webviewStyles"
|
|
|
|
|
|
@message="handleWebViewMessage"
|
|
|
|
|
|
@error="handleWebViewError"
|
|
|
|
|
|
></web-view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<!-- #endif -->
|
2025-12-26 10:37:52 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { ref, onMounted } from 'vue'
|
|
|
|
|
|
import { workcaseChatAPI } from '@/api/workcase'
|
2026-01-20 16:17:39 +08:00
|
|
|
|
import { MEET_URL } from '@/config'
|
2025-12-26 10:37:52 +08:00
|
|
|
|
|
|
|
|
|
|
const statusBarHeight = ref(44)
|
|
|
|
|
|
const navBarHeight = ref(88)
|
|
|
|
|
|
const meetingUrl = ref('')
|
|
|
|
|
|
const meetingId = ref('')
|
2025-12-27 13:23:07 +08:00
|
|
|
|
const meetingName = ref('')
|
|
|
|
|
|
const meetingEnded = ref(false) // 会议是否已结束
|
2025-12-26 10:37:52 +08:00
|
|
|
|
|
|
|
|
|
|
const webviewStyles = ref({
|
|
|
|
|
|
progress: {
|
|
|
|
|
|
color: '#667eea'
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
// 获取状态栏高度
|
|
|
|
|
|
const windowInfo = uni.getWindowInfo()
|
|
|
|
|
|
statusBarHeight.value = windowInfo.statusBarHeight || 44
|
|
|
|
|
|
navBarHeight.value = statusBarHeight.value + 44
|
|
|
|
|
|
|
|
|
|
|
|
// 获取页面参数
|
|
|
|
|
|
const pages = getCurrentPages()
|
|
|
|
|
|
const currentPage = pages[pages.length - 1] as any
|
2025-12-27 13:23:07 +08:00
|
|
|
|
console.log('[MeetingView] currentPage.options:', currentPage?.options)
|
|
|
|
|
|
|
2025-12-26 10:37:52 +08:00
|
|
|
|
if (currentPage && currentPage.options) {
|
2025-12-27 13:23:07 +08:00
|
|
|
|
const originalMeetingUrl = decodeURIComponent(currentPage.options.meetingUrl || '')
|
2025-12-26 10:37:52 +08:00
|
|
|
|
meetingId.value = currentPage.options.meetingId || ''
|
2025-12-27 13:23:07 +08:00
|
|
|
|
meetingName.value = decodeURIComponent(currentPage.options.meetingName || '')
|
|
|
|
|
|
|
|
|
|
|
|
// 检测是否为微信小程序环境
|
|
|
|
|
|
// #ifdef MP-WEIXIN
|
|
|
|
|
|
// 小程序环境:直接使用原始URL(显示提示弹窗)
|
|
|
|
|
|
meetingUrl.value = originalMeetingUrl
|
|
|
|
|
|
console.log('[MeetingView] 微信小程序环境,显示提示弹窗')
|
|
|
|
|
|
// #endif
|
|
|
|
|
|
|
|
|
|
|
|
// #ifndef MP-WEIXIN
|
|
|
|
|
|
// 非小程序环境:使用HTML包装页面加载Jitsi(支持事件监听)
|
|
|
|
|
|
const wrapperPath = '/static/jitsi-wrapper.html'
|
|
|
|
|
|
meetingUrl.value = `${wrapperPath}?url=${encodeURIComponent(originalMeetingUrl)}`
|
|
|
|
|
|
console.log('[MeetingView] 使用包装页面:', meetingUrl.value)
|
|
|
|
|
|
// #endif
|
2025-12-26 10:37:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log('[MeetingView] 会议页面加载:', {
|
|
|
|
|
|
meetingId: meetingId.value,
|
2025-12-27 13:23:07 +08:00
|
|
|
|
meetingName: meetingName.value,
|
|
|
|
|
|
meetingUrl: meetingUrl.value,
|
|
|
|
|
|
urlLength: meetingUrl.value.length
|
2025-12-26 10:37:52 +08:00
|
|
|
|
})
|
2025-12-27 13:23:07 +08:00
|
|
|
|
|
|
|
|
|
|
// 检查URL是否有效
|
|
|
|
|
|
if (!meetingUrl.value) {
|
|
|
|
|
|
console.error('[MeetingView] 会议URL为空!')
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: '会议URL为空',
|
|
|
|
|
|
icon: 'none'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2025-12-26 10:37:52 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2025-12-27 13:23:07 +08:00
|
|
|
|
// 确认退出(小程序环境)
|
2025-12-26 10:37:52 +08:00
|
|
|
|
function confirmExit() {
|
2025-12-27 13:23:07 +08:00
|
|
|
|
uni.navigateBack()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 导航栏返回按钮(Web环境)
|
|
|
|
|
|
function handleNavBack() {
|
|
|
|
|
|
if (meetingEnded.value) {
|
|
|
|
|
|
// 会议已结束,直接返回
|
|
|
|
|
|
uni.navigateBack()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 会议进行中,提示用户
|
|
|
|
|
|
uni.showModal({
|
|
|
|
|
|
title: '提示',
|
|
|
|
|
|
content: '会议正在进行中,确定要离开吗?',
|
|
|
|
|
|
success: (res) => {
|
|
|
|
|
|
if (res.confirm) {
|
|
|
|
|
|
uni.navigateBack()
|
|
|
|
|
|
}
|
2025-12-26 10:37:52 +08:00
|
|
|
|
}
|
2025-12-27 13:23:07 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 点击遮罩关闭
|
|
|
|
|
|
function handleMaskClick() {
|
|
|
|
|
|
uni.navigateBack()
|
2025-12-26 10:37:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 结束会议
|
|
|
|
|
|
async function endMeeting() {
|
|
|
|
|
|
if (!meetingId.value) {
|
|
|
|
|
|
uni.navigateBack()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
uni.showModal({
|
|
|
|
|
|
title: '提示',
|
|
|
|
|
|
content: '确定要结束会议吗?这将关闭所有参与者的会议。',
|
|
|
|
|
|
success: async (res) => {
|
|
|
|
|
|
if (res.confirm) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
uni.showLoading({ title: '结束会议中...' })
|
|
|
|
|
|
await workcaseChatAPI.endVideoMeeting(meetingId.value)
|
|
|
|
|
|
uni.hideLoading()
|
2025-12-27 13:23:07 +08:00
|
|
|
|
|
|
|
|
|
|
meetingEnded.value = true
|
|
|
|
|
|
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: '会议已结束',
|
|
|
|
|
|
icon: 'success',
|
|
|
|
|
|
duration: 2000
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 2秒后自动返回
|
|
|
|
|
|
setTimeout(() => uni.navigateBack(), 2000)
|
2025-12-26 10:37:52 +08:00
|
|
|
|
} catch (e) {
|
|
|
|
|
|
uni.hideLoading()
|
|
|
|
|
|
console.error('[MeetingView] 结束会议失败:', e)
|
|
|
|
|
|
uni.showToast({ title: '结束会议失败', icon: 'none' })
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-27 13:23:07 +08:00
|
|
|
|
// 处理webview消息(监听Jitsi事件)
|
2025-12-26 10:37:52 +08:00
|
|
|
|
function handleWebViewMessage(e: any) {
|
|
|
|
|
|
console.log('[MeetingView] webview消息:', e)
|
2025-12-27 13:23:07 +08:00
|
|
|
|
|
|
|
|
|
|
const { data } = e.detail
|
|
|
|
|
|
if (!data || !Array.isArray(data) || data.length === 0) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 解析消息
|
|
|
|
|
|
const message = data[0]
|
|
|
|
|
|
console.log('[MeetingView] 解析消息:', message)
|
|
|
|
|
|
|
|
|
|
|
|
// 处理不同的Jitsi事件
|
|
|
|
|
|
if (message.event) {
|
|
|
|
|
|
switch (message.event) {
|
|
|
|
|
|
case 'videoConferenceLeft':
|
|
|
|
|
|
// 用户离开会议
|
|
|
|
|
|
console.log('[MeetingView] 用户离开会议')
|
|
|
|
|
|
handleUserLeftMeeting()
|
|
|
|
|
|
break
|
|
|
|
|
|
case 'videoConferenceJoined':
|
|
|
|
|
|
// 用户加入会议
|
|
|
|
|
|
console.log('[MeetingView] 用户加入会议')
|
|
|
|
|
|
break
|
|
|
|
|
|
case 'participantLeft':
|
|
|
|
|
|
// 参与者离开
|
|
|
|
|
|
console.log('[MeetingView] 参与者离开:', message.data)
|
|
|
|
|
|
break
|
|
|
|
|
|
case 'readyToClose':
|
|
|
|
|
|
// 会议准备关闭
|
|
|
|
|
|
console.log('[MeetingView] 会议准备关闭')
|
|
|
|
|
|
handleMeetingEnded()
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理用户离开会议
|
|
|
|
|
|
function handleUserLeftMeeting() {
|
|
|
|
|
|
// 用户主动离开会议,直接返回
|
|
|
|
|
|
uni.navigateBack()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理会议结束
|
|
|
|
|
|
async function handleMeetingEnded() {
|
|
|
|
|
|
if (meetingEnded.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
meetingEnded.value = true
|
|
|
|
|
|
console.log('[MeetingView] 会议已结束,同步到数据库')
|
|
|
|
|
|
|
|
|
|
|
|
// 调用后端API标记会议结束
|
|
|
|
|
|
if (meetingId.value) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await workcaseChatAPI.endVideoMeeting(meetingId.value)
|
|
|
|
|
|
console.log('[MeetingView] 会议结束状态已同步到数据库')
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('[MeetingView] 同步会议结束状态失败:', e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 提示用户并返回
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: '会议已结束',
|
|
|
|
|
|
icon: 'success',
|
|
|
|
|
|
duration: 2000
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => uni.navigateBack(), 2000)
|
2025-12-26 10:37:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理webview错误
|
|
|
|
|
|
function handleWebViewError(e: any) {
|
|
|
|
|
|
console.error('[MeetingView] webview错误:', e)
|
|
|
|
|
|
uni.showToast({ title: '会议加载失败', icon: 'none' })
|
|
|
|
|
|
}
|
2025-12-27 13:23:07 +08:00
|
|
|
|
|
|
|
|
|
|
// 复制会议链接
|
|
|
|
|
|
function copyUrl() {
|
2026-01-22 11:36:26 +08:00
|
|
|
|
// 获取页面参数
|
2026-01-20 16:17:39 +08:00
|
|
|
|
const pages = getCurrentPages()
|
|
|
|
|
|
const currentPage = pages[pages.length - 1] as any
|
|
|
|
|
|
|
2026-01-22 11:36:26 +08:00
|
|
|
|
// 优先检查页面参数中是否有jitsiIframeUrl
|
|
|
|
|
|
let fullMeetingUrl = ''
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否直接传递了jitsiIframeUrl参数
|
|
|
|
|
|
const jitsiIframeUrl = currentPage?.options?.jitsiIframeUrl || ''
|
|
|
|
|
|
if (jitsiIframeUrl) {
|
|
|
|
|
|
fullMeetingUrl = jitsiIframeUrl
|
|
|
|
|
|
} else if (meetingUrl.value) {
|
|
|
|
|
|
// 兼容老版本,使用meetingUrl
|
|
|
|
|
|
fullMeetingUrl = meetingUrl.value
|
|
|
|
|
|
} else {
|
|
|
|
|
|
uni.showToast({ title: '会议链接为空', icon: 'none' })
|
|
|
|
|
|
return
|
2026-01-20 16:17:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-27 13:23:07 +08:00
|
|
|
|
uni.setClipboardData({
|
2026-01-20 16:17:39 +08:00
|
|
|
|
data: fullMeetingUrl,
|
2025-12-27 13:23:07 +08:00
|
|
|
|
success: () => {
|
|
|
|
|
|
uni.showToast({ title: '链接已复制', icon: 'success' })
|
|
|
|
|
|
},
|
|
|
|
|
|
fail: () => {
|
|
|
|
|
|
uni.showToast({ title: '复制失败', icon: 'none' })
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 在浏览器中打开
|
|
|
|
|
|
function openInBrowser() {
|
2026-01-22 11:36:26 +08:00
|
|
|
|
// 获取页面参数
|
2026-01-20 16:17:39 +08:00
|
|
|
|
const pages = getCurrentPages()
|
|
|
|
|
|
const currentPage = pages[pages.length - 1] as any
|
|
|
|
|
|
|
2026-01-22 11:36:26 +08:00
|
|
|
|
// 优先检查页面参数中是否有jitsiIframeUrl
|
|
|
|
|
|
let fullMeetingUrl = ''
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否直接传递了jitsiIframeUrl参数
|
|
|
|
|
|
const jitsiIframeUrl = currentPage?.options?.jitsiIframeUrl || ''
|
|
|
|
|
|
if (jitsiIframeUrl) {
|
|
|
|
|
|
fullMeetingUrl = jitsiIframeUrl
|
|
|
|
|
|
} else if (meetingUrl.value) {
|
|
|
|
|
|
// 兼容老版本,使用meetingUrl
|
|
|
|
|
|
fullMeetingUrl = meetingUrl.value
|
|
|
|
|
|
} else {
|
|
|
|
|
|
uni.showToast({ title: '会议链接为空', icon: 'none' })
|
|
|
|
|
|
return
|
2026-01-20 16:17:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-27 13:23:07 +08:00
|
|
|
|
// #ifdef MP-WEIXIN
|
|
|
|
|
|
// 微信小程序:先复制链接,然后提示用户通过右上角菜单在浏览器中打开
|
|
|
|
|
|
uni.setClipboardData({
|
2026-01-20 16:17:39 +08:00
|
|
|
|
data: fullMeetingUrl,
|
2025-12-27 13:23:07 +08:00
|
|
|
|
success: () => {
|
|
|
|
|
|
uni.showModal({
|
|
|
|
|
|
title: '链接已复制',
|
|
|
|
|
|
content: '请按以下步骤在浏览器中打开会议:\n\n1. 点击右上角【···】菜单\n2. 选择【在浏览器中打开】\n3. 在浏览器中粘贴会议链接\n\n或直接在任意浏览器中粘贴打开',
|
|
|
|
|
|
showCancel: false,
|
|
|
|
|
|
confirmText: '知道了'
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
fail: () => {
|
|
|
|
|
|
uni.showToast({ title: '复制失败', icon: 'none' })
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
// #endif
|
|
|
|
|
|
|
|
|
|
|
|
// #ifndef MP-WEIXIN
|
|
|
|
|
|
// 非微信小程序:直接打开链接
|
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
|
if (typeof plus !== 'undefined') {
|
|
|
|
|
|
// App环境:使用plus打开系统浏览器
|
2026-01-20 16:17:39 +08:00
|
|
|
|
plus.runtime.openURL(fullMeetingUrl)
|
2025-12-27 13:23:07 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
// H5环境:新窗口打开
|
2026-01-20 16:17:39 +08:00
|
|
|
|
window.open(fullMeetingUrl, '_blank')
|
2025-12-27 13:23:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
// #endif
|
|
|
|
|
|
}
|
2025-12-26 10:37:52 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
2026-01-08 13:20:40 +08:00
|
|
|
|
@import "./Meeting.scss"
|
2025-12-26 10:37:52 +08:00
|
|
|
|
</style>
|