聊天室修改视频会议

This commit is contained in:
2025-12-27 13:23:07 +08:00
parent 0f985ae8e8
commit 7c6fbc5ebe
14 changed files with 685 additions and 43 deletions

View File

@@ -40,4 +40,186 @@
color: #ff4444;
font-size: 14px;
font-weight: 500;
}
/* 弹窗模式样式 */
.meeting-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100vw;
height: 100vh;
z-index: 9999;
}
.modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1;
}
.modal-content {
position: fixed;
left: 0;
right: 0;
bottom: 0;
width: 100%;
background: #fff;
border-radius: 16px 16px 0 0;
animation: slideUp 0.3s ease-out;
max-height: 70vh;
overflow-y: auto;
z-index: 2;
}
@keyframes slideUp {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
.modal-header {
position: relative;
padding: 20px 20px 16px;
border-bottom: 1px solid #f0f0f0;
}
.modal-title {
font-size: 18px;
font-weight: 600;
color: #333;
padding-right: 40px;
}
.close-btn {
position: absolute;
right: 16px;
top: 16px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
.close-icon {
font-size: 28px;
color: #999;
line-height: 1;
}
.modal-body {
padding: 20px;
}
.meeting-info {
margin-bottom: 20px;
}
.info-item {
display: flex;
align-items: flex-start;
margin-bottom: 12px;
}
.info-label {
font-size: 14px;
color: #666;
flex-shrink: 0;
}
.info-value {
font-size: 14px;
color: #333;
flex: 1;
}
.tips-box {
background: #fff7e6;
border: 1px solid #ffd591;
border-radius: 8px;
padding: 12px;
display: flex;
align-items: center;
margin-bottom: 20px;
}
.tips-icon {
font-size: 20px;
margin-right: 8px;
}
.tips-text {
font-size: 13px;
color: #d48806;
flex: 1;
line-height: 1.5;
}
.url-preview {
background: #f5f5f5;
border-radius: 8px;
padding: 12px;
margin-bottom: 20px;
}
.url-label {
font-size: 12px;
color: #999;
display: block;
margin-bottom: 8px;
}
.url-text {
font-size: 12px;
color: #666;
word-break: break-all;
line-height: 1.6;
}
.modal-footer {
padding: 0 20px 32px;
display: flex;
flex-direction: column;
gap: 12px;
}
.action-btn {
width: 100%;
height: 48px;
border-radius: 8px;
font-size: 16px;
border: none;
display: flex;
align-items: center;
justify-content: center;
}
.copy-btn {
background: #667eea;
color: #fff;
}
.browser-btn {
background: #fff;
color: #667eea;
border: 1px solid #667eea;
}
/* 全屏页面样式(非小程序环境) */
.meeting-page-full {
width: 100%;
height: 100vh;
background: #000;
}

View File

@@ -1,23 +1,73 @@
<template>
<view class="meeting-page">
<!-- 自定义导航栏 -->
<view class="meeting-nav" :style="{ paddingTop: statusBarHeight + 'px', height: navBarHeight + 'px' }">
<view class="nav-back" @tap="confirmExit">
<text class="back-icon">←</text>
<!-- 半透明遮罩 + 底部弹窗 -->
<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>
</view>
<text class="nav-title">视频会议</text>
<view class="nav-right" @tap="endMeeting">
<text class="end-btn">结束会议</text>
<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>
</view>
</view>
<!-- #endif -->
<!-- Web-view加载Jitsi Meet -->
<web-view
:src="meetingUrl"
:webview-styles="webviewStyles"
@message="handleWebViewMessage"
@error="handleWebViewError"
></web-view>
<!-- #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 -->
</view>
</template>
@@ -29,6 +79,8 @@ const statusBarHeight = ref(44)
const navBarHeight = ref(88)
const meetingUrl = ref('')
const meetingId = ref('')
const meetingName = ref('')
const meetingEnded = ref(false) // 会议是否已结束
const webviewStyles = ref({
progress: {
@@ -45,28 +97,72 @@ onMounted(() => {
// 获取页面参数
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1] as any
console.log('[MeetingView] currentPage.options:', currentPage?.options)
if (currentPage && currentPage.options) {
meetingUrl.value = decodeURIComponent(currentPage.options.meetingUrl || '')
const originalMeetingUrl = decodeURIComponent(currentPage.options.meetingUrl || '')
meetingId.value = currentPage.options.meetingId || ''
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
}
console.log('[MeetingView] 会议页面加载:', {
meetingId: meetingId.value,
meetingUrl: meetingUrl.value
meetingName: meetingName.value,
meetingUrl: meetingUrl.value,
urlLength: meetingUrl.value.length
})
// 检查URL是否有效
if (!meetingUrl.value) {
console.error('[MeetingView] 会议URL为空')
uni.showToast({
title: '会议URL为空',
icon: 'none'
})
}
})
// 确认退出
// 确认退出(小程序环境)
function confirmExit() {
uni.showModal({
title: '提示',
content: '确定要退出会议吗?',
success: (res) => {
if (res.confirm) {
uni.navigateBack()
uni.navigateBack()
}
// 导航栏返回按钮Web环境
function handleNavBack() {
if (meetingEnded.value) {
// 会议已结束,直接返回
uni.navigateBack()
} else {
// 会议进行中,提示用户
uni.showModal({
title: '提示',
content: '会议正在进行中,确定要离开吗?',
success: (res) => {
if (res.confirm) {
uni.navigateBack()
}
}
}
})
})
}
}
// 点击遮罩关闭
function handleMaskClick() {
uni.navigateBack()
}
// 结束会议
@@ -85,24 +181,98 @@ async function endMeeting() {
uni.showLoading({ title: '结束会议中...' })
await workcaseChatAPI.endVideoMeeting(meetingId.value)
uni.hideLoading()
uni.showToast({ title: '会议已结束', icon: 'success' })
setTimeout(() => uni.navigateBack(), 1500)
meetingEnded.value = true
uni.showToast({
title: '会议已结束',
icon: 'success',
duration: 2000
})
// 2秒后自动返回
setTimeout(() => uni.navigateBack(), 2000)
} catch (e) {
uni.hideLoading()
console.error('[MeetingView] 结束会议失败:', e)
uni.showToast({ title: '结束会议失败', icon: 'none' })
uni.navigateBack()
}
}
}
})
}
// 处理webview消息
// 处理webview消息监听Jitsi事件
function handleWebViewMessage(e: any) {
console.log('[MeetingView] webview消息:', e)
// 可以在这里处理Jitsi Meet发送的消息
// 例如:会议结束、参与者加入/离开等事件
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)
}
// 处理webview错误
@@ -110,6 +280,62 @@ function handleWebViewError(e: any) {
console.error('[MeetingView] webview错误:', e)
uni.showToast({ title: '会议加载失败', icon: 'none' })
}
// 复制会议链接
function copyUrl() {
if (!meetingUrl.value) {
uni.showToast({ title: '会议链接为空', icon: 'none' })
return
}
uni.setClipboardData({
data: meetingUrl.value,
success: () => {
uni.showToast({ title: '链接已复制', icon: 'success' })
},
fail: () => {
uni.showToast({ title: '复制失败', icon: 'none' })
}
})
}
// 在浏览器中打开
function openInBrowser() {
if (!meetingUrl.value) {
uni.showToast({ title: '会议链接为空', icon: 'none' })
return
}
// #ifdef MP-WEIXIN
// 微信小程序:先复制链接,然后提示用户通过右上角菜单在浏览器中打开
uni.setClipboardData({
data: meetingUrl.value,
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打开系统浏览器
plus.runtime.openURL(meetingUrl.value)
} else {
// H5环境新窗口打开
window.open(meetingUrl.value, '_blank')
}
// #endif
}
</script>
<style lang="scss" scoped>

View File

@@ -22,7 +22,7 @@
<button
type="primary"
:disabled="!canJoinMeeting"
@click="handleJoinMeeting"
@tap="handleJoinMeeting"
size="mini"
>
<text>{{ buttonText }}</text>
@@ -194,7 +194,14 @@ const buttonText = computed((): string => {
* 加入会议
*/
function handleJoinMeeting() {
console.log('[MeetingCard] handleJoinMeeting 被点击', {
meetingId: props.meeting.meetingId,
canJoin: canJoinMeeting.value,
meeting: props.meeting
})
if (!props.meeting.meetingId) {
console.error('[MeetingCard] 会议ID不存在')
uni.showToast({
title: '会议ID不存在',
icon: 'none'
@@ -203,9 +210,14 @@ function handleJoinMeeting() {
}
if (!canJoinMeeting.value) {
console.warn('[MeetingCard] 不允许加入会议', {
status: props.meeting.status,
canJoin: canJoinMeeting.value
})
return
}
console.log('[MeetingCard] 触发join事件, meetingId:', props.meeting.meetingId)
emit('join', props.meeting.meetingId)
}
</script>