diff --git a/src/layout/components/ResetPwd/index.vue b/src/layout/components/ResetPwd/index.vue index 609f4b1..07a76f7 100644 --- a/src/layout/components/ResetPwd/index.vue +++ b/src/layout/components/ResetPwd/index.vue @@ -6,38 +6,40 @@ width="500" :show-close='false' :close-on-click-modal="false" - v-loading="loading" + > - - - - - - - - - - - - 保存 - 关闭 - - +
+ + + + + + + + + + + + 保存 + 关闭 + + +
@@ -112,6 +114,7 @@ const validatorPasswords = async (rule, value, callback) => { /** 提交按钮 */ async function submit() { + loading.value = true; try { const valid = await proxy.$refs.pwdRef.validate(); if (!valid) return; @@ -125,9 +128,7 @@ async function submit() { ElMessage.error(resPwd.data.suggestions.join(', ') || '密码强度校验失败'); return } - } - - loading.value = true; + } const res = await changePwd(user.oldPassword, user.newPassword); if(res.meta.code !== 200){ ElMessage.error(res.meta?.message || '密码修改失败'); diff --git a/src/layout/components/Sidebar/SidebarItem.vue b/src/layout/components/Sidebar/SidebarItem.vue index 89c0fda..8d6c50a 100644 --- a/src/layout/components/Sidebar/SidebarItem.vue +++ b/src/layout/components/Sidebar/SidebarItem.vue @@ -12,7 +12,7 @@ :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }" > - + @@ -25,7 +25,7 @@ diff --git a/src/layout/components/Sidebar/index.vue b/src/layout/components/Sidebar/index.vue index ed80fdd..c67b145 100644 --- a/src/layout/components/Sidebar/index.vue +++ b/src/layout/components/Sidebar/index.vue @@ -46,7 +46,7 @@ const settingsStore = useSettingsStore() const showLogo = computed(() => settingsStore.sidebarLogo) const sideTheme = computed(() => settingsStore.sideTheme) -// const theme = computed(() => settingsStore.theme) +const theme = computed(() => settingsStore.theme) const isCollapse = computed(() => !appStore.sidebar.opened) const activeMenu = computed(() => { diff --git a/src/stores/modules/settings.js b/src/stores/modules/settings.js index 2d3fec5..30c0766 100644 --- a/src/stores/modules/settings.js +++ b/src/stores/modules/settings.js @@ -11,7 +11,7 @@ export const useSettingsStore = defineStore( { state: () => ({ title: '', - theme: storageSetting.theme === undefined ? '#141414' : storageSetting.theme, + theme: storageSetting.theme === undefined ? '#434343' : storageSetting.theme, sideTheme: storageSetting.sideTheme === undefined ? sideTheme : storageSetting.sideTheme, showSettings: showSettings, topNav: storageSetting.topNav === undefined ? topNav : storageSetting.topNav, diff --git a/src/views/conferencingRoom/business/index.js b/src/views/conferencingRoom/business/index.js index 26669cb..5eb80db 100644 --- a/src/views/conferencingRoom/business/index.js +++ b/src/views/conferencingRoom/business/index.js @@ -1,3 +1,88 @@ -export function generateUUID() { - +import { ElMessage } from 'element-plus'; +export function errorHandling(error,type) { + switch (error.name) { + case 'NotAllowedError': + ElMessage.error('用户拒绝了权限请求,请允许此网站使用摄像头'); + break; + case 'NotFoundError': + ElMessage.error(`未检测到可用的${type}设备,请检查${type}是否已正确连接`); + break; + case 'NotSupportedError': + ElMessage.error(`当前浏览器不支持${type}功能,请使用现代浏览器如Chrome、Firefox或Edge`); + break; + case 'NotReadableError': + ElMessage.error(`${type}设备正被其他应用程序占用,请关闭其他使用${type}的应用后重试`); + break; + case 'OverconstrainedError': + ElMessage.error(`${type}配置不兼容,请尝试调整${type}设置`); + break; + default: + ElMessage.error('服务错误,请刷新重试'); + } +} + +// 处理数据接收事件 +export function handleDataReceived(payload, participant, kind) { + try { + const decoder = new TextDecoder(); + const strData = decoder.decode(payload); + ElMessage.info(`收到消息 from ${participant.identity}: ${strData}`); + } catch (error) { + console.error('处理接收消息失败:', error); + } +} + +export function handleReconnected() { + ElMessage.success('已重新连接到房间'); +} + +// 获取设备名称 +export function getDeviceName(devices, deviceId) { + const device = devices.find(d => d.deviceId === deviceId); + return device ? (device.label || '未知设备') : '未知设备'; +} + +export function handleVideoLoaded(identity, type) { + console.log(`视频加载完成: ${identity}的${type}视频`); +} + +export function handleConnectionStateChanged(state) { + console.log('连接状态改变:', state); +} + +// 平滑曲线绘制函数 +export function drawSmoothCurve(ctx, path) { + if (path.length < 3) { + // 如果点太少,直接绘制直线 + ctx.beginPath(); + ctx.moveTo(path[0].x, path[0].y); + for (let i = 1; i < path.length; i++) { + ctx.lineTo(path[i].x, path[i].y); + } + return; + } + + ctx.beginPath(); + ctx.moveTo(path[0].x, path[0].y); + + const threshold = 5; // 距离阈值,用于控制平滑度 + + for (let i = 1; i < path.length - 2;) { + let a = 1; + // 寻找下一个足够远的点 + while (i + a < path.length - 2 && + Math.sqrt(Math.pow(path[i].x - path[i + a].x, 2) + + Math.pow(path[i].y - path[i + a].y, 2)) < threshold) { + a++; + } + + const xc = (path[i].x + path[i + a].x) / 2; + const yc = (path[i].y + path[i + a].y) / 2; + + ctx.quadraticCurveTo(path[i].x, path[i].y, xc, yc); + i += a; + } + + // 连接最后两个点 + ctx.lineTo(path[path.length - 1].x, path[path.length - 1].y); } \ No newline at end of file diff --git a/src/views/conferencingRoom/components/fileUpload/index.vue b/src/views/conferencingRoom/components/fileUpload/index.vue new file mode 100644 index 0000000..95f5f94 --- /dev/null +++ b/src/views/conferencingRoom/components/fileUpload/index.vue @@ -0,0 +1,6 @@ + diff --git a/src/views/conferencingRoom/index.vue b/src/views/conferencingRoom/index.vue index fdf3f45..4d248e3 100644 --- a/src/views/conferencingRoom/index.vue +++ b/src/views/conferencingRoom/index.vue @@ -26,6 +26,16 @@
+ +
@@ -207,6 +221,12 @@ {{ isWhiteboardActive ? '退出白板' : '共享白板' }} + + {{ isLaserPointerActive ? '关闭激光笔' : '激光笔' }} + + + 文件上传 + 邀请人员 @@ -227,26 +247,23 @@ import { ElMessage ,ElMessageBox} from 'element-plus'; import { ArrowDown, Refresh, Check } from '@element-plus/icons-vue'; import { getRoomToken, getRoomList ,getInvite,getTokenApi,exitRoomApi,getRoomInfoApi} from "@/api/conferencingRoom.js" import { Room, RoomEvent, ParticipantEvent, Track } from "livekit-client"; +import { errorHandling ,handleDataReceived ,handleReconnected,getDeviceName,handleVideoLoaded,handleConnectionStateChanged,drawSmoothCurve} from './business/index.js' import { useRoute, useRouter } from 'vue-router' import { useRoomStore } from '@/stores/modules/room.js' import { useUserStore } from '@/stores/modules/user.js' import tabulaRase from '@/views/custom/tabulaRase/index.vue' import { mqttClient } from "@/utils/mqtt.js"; import InviterJoinRoom from "@/views/conferencingRoom/components/InviterJoinRoom/index.vue" - const userStore = useUserStore() const roomStore = useRoomStore() const route = useRoute(); const router = useRouter() - // LiveKit 服务器配置 const wsURL = "wss://meeting.cnsdt.com:443"; - const hostUid = ref('') const status = ref(true); const roomName = ref('') const roomId = ref('') - // 音视频相关引用和数据 const localVideo = ref(null); const localAudio = ref(null); @@ -254,41 +271,63 @@ const cameraEnabled = ref(false); const microphoneEnabled = ref(false); const isScreenSharing = ref(false); const isLocalSpeaking = ref(false); - // 设备列表 const cameraDevices = ref([]); const microphoneDevices = ref([]); const selectedCameraId = ref(''); const selectedMicrophoneId = ref(''); - // 远程参与者管理 const remoteParticipants = ref(new Map()); const videoElementsMap = ref(new Map()); const audioElementsMap = ref(new Map()); - // 屏幕共享相关 const screenSharingUser = ref(''); const activeScreenShareTrack = ref(null); -const screenShareVideo = ref(null); +const screenShareVideo = ref(null);//屏幕共享视频元素 const globalScreenSharingUser = ref(''); // 当前正在共享屏幕的用户 const isGlobalScreenSharing = ref(false); // 是否有用户正在共享屏幕 - //共享白板 const isWhiteboardActive = ref(false); const whiteboardRef = ref(null); const isGlobalWhiteboardSharing = ref(false); // 是否有用户正在共享白板 - //当前房间信息 -const roomInfo = ref('') - +const roomInfo = ref('') //邀请参会人员 const inviterJoinRoomRef = ref() +// 激光笔相关状态 +const isLaserPointerActive = ref(false); +const laserPointerCanvas = ref(null); +const laserPointerContext = ref(null); +const laserPointerElements = ref([]); +const laserPointerTimeout = ref(null); +// 鼠标状态跟踪 +const mouseState = reactive({ + isDrawing: false, + lastX: 0, + lastY: 0, + startX: 0, + startY: 0, + currentPath: [] // 添加当前路径数组 +}); +// 激光笔样式配置 +const laserPointerConfig = reactive({ + color: '#ff0000', // 红色激光 + thickness: 3, + duration: 2000, // 2秒后消失 + fadeDuration: 300 // 淡出动画时间 +}); + // 白板消息类型 const WHITEBOARD_MESSAGE_TYPES = { OPEN: 'open_whiteboard', CLOSE: 'close_whiteboard', SYNC: 'sync_whiteboard' +}; +// 在 script 中添加激光笔消息类型和同步功能 +const LASER_POINTER_MESSAGE_TYPES = { + DRAW: 'laser_draw', + CLEAR: 'laser_clear' }; // 计算属性 @@ -340,23 +379,390 @@ const room = new Room({ }, // 音频发布配置 audioEncoding: { - maxBitrate: 64000, // 64kbps for audio + maxBitrate: 64000, }, // dtx: true, // 不连续传输,节省带宽 red: true, // 冗余编码,提高抗丢包能力 } }); +// 激光笔功能 +function toggleLaserPointer() { + if (!hasActiveScreenShare.value || isWhiteboardActive.value) { + ElMessage.warning('请在屏幕共享模式下使用激光笔'); + return; + } + + isLaserPointerActive.value = !isLaserPointerActive.value; + + if (isLaserPointerActive.value) { + initLaserPointerCanvas(); + ElMessage.success('激光笔已开启,双击添加标记,拖拽绘制线条'); + } else { + cleanupLaserPointer(); + ElMessage.info('激光笔已关闭'); + } +} + +function handleScreenShareLoaded() { + console.log('屏幕共享视频加载完成'); + // 视频加载完成后初始化激光笔 Canvas + initLaserPointerCanvas(); +} + +// 初始化激光笔 Canvas +function initLaserPointerCanvas() { + if (!laserPointerCanvas.value) return; + const videoWrapper = document.querySelector('.screen-share-wrapper'); + if (!videoWrapper) return; + const rect = videoWrapper.getBoundingClientRect(); + // 设置 Canvas 尺寸 + laserPointerCanvas.value.width = rect.width; + laserPointerCanvas.value.height = rect.height; + // 获取上下文 + laserPointerContext.value = laserPointerCanvas.value.getContext('2d'); + // 设置 Canvas 样式 + laserPointerCanvas.value.style.position = 'absolute'; + laserPointerCanvas.value.style.top = '0'; + laserPointerCanvas.value.style.left = '0'; + laserPointerCanvas.value.style.zIndex = '10'; + laserPointerCanvas.value.style.cursor = 'crosshair'; + laserPointerCanvas.value.style.pointerEvents = isLaserPointerActive.value ? 'auto' : 'none'; +} + +// 清理激光笔 +function cleanupLaserPointer() { + if (laserPointerTimeout.value) { + clearTimeout(laserPointerTimeout.value); + laserPointerTimeout.value = null; + } + + if (laserPointerContext.value && laserPointerCanvas.value) { + laserPointerContext.value.clearRect(0, 0, laserPointerCanvas.value.width, laserPointerCanvas.value.height); + } + + laserPointerElements.value = []; + mouseState.isDrawing = false; + mouseState.currentPath = []; +} +// 获取鼠标坐标(转换为百分比坐标) +function getMouseCoordinates(e) { + if (!laserPointerCanvas.value) return { x: 0, y: 0 }; + + const rect = laserPointerCanvas.value.getBoundingClientRect(); + return { + x: ((e.clientX - rect.left) / laserPointerCanvas.value.width).toFixed(4), + y: ((e.clientY - rect.top) / laserPointerCanvas.value.height).toFixed(4) + }; +} +// 获取实际像素坐标 +function getPixelCoordinates(e) { + if (!laserPointerCanvas.value) return { x: 0, y: 0 }; + + const rect = laserPointerCanvas.value.getBoundingClientRect(); + return { + x: e.clientX - rect.left, + y: e.clientY - rect.top + }; +} + +// Canvas 事件处理 双击 +function handleCanvasDoubleClick(e) { + if (!isLaserPointerActive.value) return; + const coords = getMouseCoordinates(e); + const pixelCoords = getPixelCoordinates(e); + // 创建圆形标记 + const circleElement = { + type: 'circle', + data: { + color: laserPointerConfig.color, + start: coords, + thickness: laserPointerConfig.thickness, + pixelCoords: pixelCoords, + timestamp: Date.now() + } + }; + laserPointerElements.value.push(circleElement); + drawCircle(pixelCoords); + // 发送到其他用户(如果需要) + publishLaserPointerData(circleElement); + // 2秒后清除 + scheduleCleanup(); +} + +function handleCanvasMouseDown(e) { + if (!isLaserPointerActive.value) return; + + const pixelCoords = getPixelCoordinates(e); + const percentageCoords = getMouseCoordinates(e); + + mouseState.isDrawing = true; + mouseState.startX = pixelCoords.x; + mouseState.startY = pixelCoords.y; + mouseState.lastX = pixelCoords.x; + mouseState.lastY = pixelCoords.y; + + // 初始化当前路径,添加第一个点 + mouseState.currentPath = [{ + x: percentageCoords.x, + y: percentageCoords.y + }]; +} + +function handleCanvasMouseMove(e) { + if (!isLaserPointerActive.value || !mouseState.isDrawing) return; + + const pixelCoords = getPixelCoordinates(e); + const percentageCoords = getMouseCoordinates(e); + + // 添加当前点到路径 + mouseState.currentPath.push({ + x: percentageCoords.x, + y: percentageCoords.y + }); + + // 实时绘制临时线条 + drawLine(mouseState.lastX, mouseState.lastY, pixelCoords.x, pixelCoords.y); + + mouseState.lastX = pixelCoords.x; + mouseState.lastY = pixelCoords.y; +} + +function handleCanvasMouseUp(e) { + if (!isLaserPointerActive.value || !mouseState.isDrawing) return; + + const percentageCoords = getMouseCoordinates(e); + + // 添加最后一个点到路径 + if (mouseState.currentPath.length > 0) { + const lastPoint = mouseState.currentPath[mouseState.currentPath.length - 1]; + // 如果最后一个点不是当前点,则添加当前点 + if (lastPoint.x !== percentageCoords.x || lastPoint.y !== percentageCoords.y) { + mouseState.currentPath.push({ + x: percentageCoords.x, + y: percentageCoords.y + }); + } + } + + // 创建铅笔元素 + const pencilElement = { + type: 'pencil', + data: { + color: laserPointerConfig.color, + path: [...mouseState.currentPath], // 复制路径数组 + thickness: laserPointerConfig.thickness, + timestamp: Date.now() + } + }; + + // 只有当路径有足够多的点时才保存和发送 + if (pencilElement.data.path.length >= 2) { + laserPointerElements.value.push(pencilElement); + + // 重新绘制所有元素(包括新的铅笔路径) + redrawLaserElements(); + + // 发送到其他用户 + publishLaserPointerData(pencilElement); + + // 安排清理 + scheduleCleanup(); + } + + // 重置绘图状态 + mouseState.isDrawing = false; + mouseState.currentPath = []; +} + +function handleCanvasMouseLeave() { + if (mouseState.isDrawing) { + // 如果正在绘制,完成当前路径 + handleCanvasMouseUp(new MouseEvent('mouseup')); + } + mouseState.isDrawing = false; +} +// 绘制函数 +function drawCircle(coords) { + if (!laserPointerContext.value) return; + const ctx = laserPointerContext.value; + ctx.beginPath(); + ctx.arc(coords.x, coords.y, 8, 0, Math.PI * 2); + ctx.fillStyle = laserPointerConfig.color; + ctx.fill(); + ctx.strokeStyle = '#ffffff'; + ctx.lineWidth = 2; + ctx.stroke(); +} + +function drawLine(startX, startY, endX, endY) { + if (!laserPointerContext.value) return; + const ctx = laserPointerContext.value; + ctx.beginPath(); + ctx.moveTo(startX, startY); + ctx.lineTo(endX, endY); + ctx.strokeStyle = laserPointerConfig.color; + ctx.lineWidth = laserPointerConfig.thickness; + ctx.lineCap = 'round'; + ctx.stroke(); +} + +// 绘制铅笔路径函数 +function drawPencilPath(pencilData) { + if (!laserPointerContext.value || !pencilData.path || pencilData.path.length < 2) return; + + const ctx = laserPointerContext.value; + + // 将百分比坐标转换为像素坐标 + const pixelPath = pencilData.path.map(pt => ({ + x: parseFloat(pt.x) * laserPointerCanvas.value.width, + y: parseFloat(pt.y) * laserPointerCanvas.value.height + })); + + // 设置绘制样式 + ctx.strokeStyle = pencilData.color; + ctx.lineWidth = pencilData.thickness; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + // 绘制平滑曲线 + drawSmoothCurve(ctx, pixelPath); + ctx.stroke(); +} + +// 重绘所有元素 +function redrawLaserElements() { + if (!laserPointerContext.value || !laserPointerCanvas.value) return; + + const ctx = laserPointerContext.value; + ctx.clearRect(0, 0, laserPointerCanvas.value.width, laserPointerCanvas.value.height); + + const now = Date.now(); + laserPointerElements.value.forEach(element => { + if (now - element.data.timestamp < laserPointerConfig.duration) { + if (element.type === 'circle' && element.data.pixelCoords) { + drawCircle(element.data.pixelCoords); + } else if (element.type === 'pencil') { + drawPencilPath(element.data); + } + } + }); +} + +// 从百分比坐标转换为像素坐标 +function getPixelFromPercentage(percentageCoords) { + if (!laserPointerCanvas.value) return { x: 0, y: 0 }; + + return { + x: percentageCoords.x * laserPointerCanvas.value.width, + y: percentageCoords.y * laserPointerCanvas.value.height + }; +} + +// 安排清理 +function scheduleCleanup() { + if (laserPointerTimeout.value) { + clearTimeout(laserPointerTimeout.value); + } + + laserPointerTimeout.value = setTimeout(() => { + const now = Date.now(); + laserPointerElements.value = laserPointerElements.value.filter( + element => now - element.data.timestamp < laserPointerConfig.duration + ); + redrawLaserElements(); + + // 如果还有元素,继续清理 + if (laserPointerElements.value.length > 0) { + scheduleCleanup(); + } + }, laserPointerConfig.duration); +} + +// 响应式调整 Canvas 大小 +function resizeLaserPointerCanvas() { + if (!isLaserPointerActive.value || !laserPointerCanvas.value) return; + + const videoWrapper = document.querySelector('.screen-share-wrapper'); + if (!videoWrapper) return; + + const rect = videoWrapper.getBoundingClientRect(); + laserPointerCanvas.value.width = rect.width; + laserPointerCanvas.value.height = rect.height; + + // 重新绘制所有元素 + redrawLaserElements(); +} + +// 监听窗口大小变化 +window.addEventListener('resize', resizeLaserPointerCanvas); + +// 在初始化 MQTT 时订阅激光笔主题 +function subscribeToLaserPointerTopic() { + try { + mqttClient.subscribe(`xSynergy/laserPointer/${room.name}`, handleLaserPointerMessage); + } catch (error) { + console.error('订阅激光笔主题失败:', error); + } +} +// 发布激光笔数据 +function publishLaserPointerData(element) { + try { + const message = { + type: LASER_POINTER_MESSAGE_TYPES.DRAW, + roomId: roomId.value, + sender: hostUid.value, + timestamp: Date.now(), + element: element + }; + + mqttClient.publish(`xSynergy/laserPointer/${room.name}`, message); + } catch (error) { + console.error('发布激光笔数据失败:', error); + } +} +// 处理接收到的激光笔数据 +function handleLaserPointerMessage(payload, topic) { + try { + const messageStr = payload.toString(); + const data = JSON.parse(messageStr); + + // 只处理当前房间的消息 + if (data.roomId !== room.name) return; + + // 忽略自己发送的消息 + if (data.sender === hostUid.value) return; + + switch (data.type) { + case LASER_POINTER_MESSAGE_TYPES.DRAW: + // 添加远程用户的激光笔绘制 + laserPointerElements.value.push(data.element); + redrawLaserElements(); + scheduleCleanup(); + break; + + case LASER_POINTER_MESSAGE_TYPES.CLEAR: + cleanupLaserPointer(); + break; + } + } catch (error) { + console.error('处理激光笔消息失败:', error); + } +} + +//文件上传 +async function fileUploadHandle(){ + +} + //摄像头打开下拉框触发 async function handleCameraVisibleChange(e){ try { if(e){ // 请求摄像头权限 const stream = await navigator.mediaDevices.getUserMedia({ video: true }); - // 获取设备列表 const devices = await navigator.mediaDevices.enumerateDevices(); - // 过滤摄像头设备 cameraDevices.value = devices.filter(device => device.kind === 'videoinput'); // 重要:立即停止临时媒体流,避免占用摄像头 @@ -379,18 +785,15 @@ async function handleCameraCommand(deviceId) { await handleCameraVisibleChange(true); ElMessage.success('设备列表已刷新'); return; - } - - selectedCameraId.value = deviceId; - + } + selectedCameraId.value = deviceId; // 如果摄像头已经开启,重新开启以应用新设备 if (cameraEnabled.value) { await switchCameraDevice(deviceId); } else { // 如果摄像头未开启,直接开启选中的设备 await enableCameraWithDevice(deviceId); - } - + } ElMessage.success(`已切换到摄像头: ${getDeviceName(cameraDevices.value, deviceId)}`); } @@ -399,17 +802,13 @@ async function handleMicrophoneVisibleChange(e){ try { if(e){ // 请求麦克风权限 - const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); - + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); // 获取设备列表 - const devices = await navigator.mediaDevices.enumerateDevices(); - + const devices = await navigator.mediaDevices.enumerateDevices(); // 过滤麦克风设备 microphoneDevices.value = devices.filter(device => device.kind === 'audioinput'); - // 停止所有轨道来关闭临时的麦克风访问 stream.getTracks().forEach(track => track.stop()); - } } catch (error) { console.error('麦克风访问失败:', error); @@ -426,10 +825,8 @@ async function handleMicrophoneCommand(deviceId) { await handleMicrophoneVisibleChange(); ElMessage.success('设备列表已刷新'); return; - } - - selectedMicrophoneId.value = deviceId; - + } + selectedMicrophoneId.value = deviceId; // 如果麦克风已经开启,重新开启以应用新设备 if (microphoneEnabled.value) { await switchMicrophoneDevice(deviceId); @@ -444,17 +841,14 @@ async function handleMicrophoneCommand(deviceId) { async function enableCameraWithDevice(deviceId) { try { // 更新设备配置 - room.options.videoCaptureDefaults.deviceId = deviceId; - + room.options.videoCaptureDefaults.deviceId = deviceId; // 开启摄像头 await room.localParticipant.setCameraEnabled(true); - cameraEnabled.value = true; - + cameraEnabled.value = true; // 手动获取并附加视频轨道 setTimeout(() => { attachLocalCameraTrack(); - }, 200); - + }, 200); return true; } catch (error) { ElMessage.error(`使用指定设备开启摄像头失败`); @@ -474,12 +868,10 @@ async function enableCameraWithDevice(deviceId) { async function enableMicrophoneWithDevice(deviceId) { try { // 更新设备配置 - room.options.audioCaptureDefaults.deviceId = deviceId; - + room.options.audioCaptureDefaults.deviceId = deviceId; // 开启麦克风 await room.localParticipant.setMicrophoneEnabled(true); - microphoneEnabled.value = true; - + microphoneEnabled.value = true; return true; } catch (error) { console.error('使用指定设备开启麦克风失败:', error); @@ -492,19 +884,15 @@ async function enableMicrophoneWithDevice(deviceId) { async function switchCameraDevice(deviceId) { try { // 先关闭当前摄像头 - await room.localParticipant.setCameraEnabled(false); - + await room.localParticipant.setCameraEnabled(false); // 更新设备配置 - room.options.videoCaptureDefaults.deviceId = deviceId; - + room.options.videoCaptureDefaults.deviceId = deviceId; // 重新开启摄像头 - await room.localParticipant.setCameraEnabled(true); - + await room.localParticipant.setCameraEnabled(true); // 手动获取并附加视频轨道 setTimeout(() => { attachLocalCameraTrack(); - }, 200); - + }, 200); return true; } catch (error) { console.error('切换摄像头设备失败:', error); @@ -522,14 +910,11 @@ async function switchCameraDevice(deviceId) { async function switchMicrophoneDevice(deviceId) { try { // 先关闭当前麦克风 - await room.localParticipant.setMicrophoneEnabled(false); - + await room.localParticipant.setMicrophoneEnabled(false); // 更新设备配置 - room.options.audioCaptureDefaults.deviceId = deviceId; - + room.options.audioCaptureDefaults.deviceId = deviceId; // 重新开启麦克风 - await room.localParticipant.setMicrophoneEnabled(true); - + await room.localParticipant.setMicrophoneEnabled(true); return true; } catch (error) { console.error('切换麦克风设备失败:', error); @@ -543,13 +928,7 @@ async function switchMicrophoneDevice(deviceId) { } } -// 获取设备名称 -function getDeviceName(devices, deviceId) { - const device = devices.find(d => d.deviceId === deviceId); - return device ? (device.label || '未知设备') : '未知设备'; -} - -// 初始化MQTT连接 +// 初始化白板MQTT连接 async function initMqtt() { try { // 使用随机客户端ID连接 @@ -557,7 +936,7 @@ async function initMqtt() { await mqttClient.connect(clientId); console.log('MQTT连接成功,客户端ID:', clientId); // 订阅白板主题 - subscribeToWhiteboardTopic(); + subscribeToWhiteboardTopic(); } catch (error) { console.error('MQTT连接失败:', error); ElMessage.error('白板同步连接失败'); @@ -571,6 +950,20 @@ function subscribeToWhiteboardTopic() { } catch (error) { console.error('订阅白板主题失败:', error); } +} +// 初始化激光笔MQTT连接 +async function initToLaserPointerMqtt() { + try { + // 使用随机客户端ID连接 + const clientId = `toLaserPointer_${Date.now()}`; + await mqttClient.connect(clientId); + console.log('MQTT连接(toLaserPointer)成功,客户端ID:', clientId); + //订阅激光笔主题 + subscribeToLaserPointerTopic() + } catch (error) { + console.error('MQTT连接失败:', error); + ElMessage.error('激光笔同步连接失败'); + } } // 处理白板消息 @@ -585,16 +978,13 @@ function handleWhiteboardMessage(payload, topic) { switch (data.type) { case WHITEBOARD_MESSAGE_TYPES.OPEN: handleRemoteWhiteboardOpen(data); - break; - + break; case WHITEBOARD_MESSAGE_TYPES.CLOSE: handleRemoteWhiteboardClose(data); - break; - + break; case WHITEBOARD_MESSAGE_TYPES.SYNC: handleWhiteboardSync(data); - break; - + break; default: console.warn('未知的白板消息类型:', data.type); } @@ -606,8 +996,7 @@ function handleWhiteboardMessage(payload, topic) { // 处理远程打开白板 function handleRemoteWhiteboardOpen(data) { ElMessage.info(`${data.senderName || data.sender} 开启了白板`); - isWhiteboardActive.value = true; - + isWhiteboardActive.value = true; // 如果正在屏幕共享,自动停止 if (isScreenSharing.value) { room.localParticipant.setScreenShareEnabled(false); @@ -662,8 +1051,7 @@ function publishWhiteboardMessage(type, payload = {}) { senderName: hostUid.value, timestamp: Date.now(), payload - }; - + }; mqttClient.publish(`xSynergy/shareWhiteboard/${room.name}`, message); console.log('白板消息发布成功:', type); return true; @@ -696,17 +1084,14 @@ async function startWhiteboard() { await room.localParticipant.setScreenShareEnabled(false); isScreenSharing.value = false; ElMessage.info('已停止屏幕共享,开启白板'); - } - + } // 激活白板状态 - isWhiteboardActive.value = true; - + isWhiteboardActive.value = true; const success = publishWhiteboardMessage(WHITEBOARD_MESSAGE_TYPES.OPEN, { action: 'open', whiteboardId: roomId.value, roomName: roomName.value - }); - + }); if (success) { ElMessage.success('白板已开启,已通知其他参会者'); } else { @@ -721,33 +1106,26 @@ async function startWhiteboard() { async function exitWhiteboard() { try { // 确认退出 - await ElMessageBox.confirm( - '确定要退出白板吗?', - '提示', + await ElMessageBox.confirm('确定要退出白板吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' } - ); - + ); // 发布关闭白板消息 publishWhiteboardMessage(WHITEBOARD_MESSAGE_TYPES.CLOSE, { action: 'close', whiteboardId: roomId.value, roomName: roomName.value - }); - + }); // 执行退出白板逻辑 - isWhiteboardActive.value = false; - + isWhiteboardActive.value = false; // 可以在这里添加白板清理逻辑 if (whiteboardRef.value && whiteboardRef.value.cleanup) { whiteboardRef.value.cleanup(); - } - - ElMessage.success('已退出白板'); - + } + ElMessage.success('已退出白板'); } catch (error) { if (error !== 'cancel') { console.error('退出白板失败:', error); @@ -758,19 +1136,15 @@ async function exitWhiteboard() { // 设置视频元素引用 function setParticipantVideoRef(el, identity, type) { - if (!el) return; - + if (!el) return; if (!videoElementsMap.value.has(identity)) { videoElementsMap.value.set(identity, {}); - } - + } const elements = videoElementsMap.value.get(identity); elements[type] = el; - videoElementsMap.value.set(identity, elements); - + videoElementsMap.value.set(identity, elements); // 如果已经有轨道数据,立即附加 - const participantData = remoteParticipants.value.get(identity); - + const participantData = remoteParticipants.value.get(identity); if (participantData) { if (type === 'camera' && participantData.cameraTrack) { attachTrackToVideo(el, participantData.cameraTrack); @@ -782,10 +1156,8 @@ function setParticipantVideoRef(el, identity, type) { // 设置音频元素引用 function setParticipantAudioRef(el, identity) { - if (!el) return; - - audioElementsMap.value.set(identity, el); - + if (!el) return; + audioElementsMap.value.set(identity, el); // 如果已经有音频轨道数据,立即附加 const participantData = remoteParticipants.value.get(identity); if (participantData && participantData.audioTrack) { @@ -796,12 +1168,15 @@ function setParticipantAudioRef(el, identity) { // 设置屏幕共享视频引用 function setScreenShareVideoRef(el) { if (!el) return; - screenShareVideo.value = el; - + screenShareVideo.value = el; // 如果已经有屏幕共享轨道,立即附加 if (activeScreenShareTrack.value) { attachTrackToVideo(el, activeScreenShareTrack.value); - } + } + // 初始化激光笔 Canvas + nextTick(() => { + initLaserPointerCanvas(); + }); } // 设置事件监听器 @@ -827,9 +1202,9 @@ function setupRoomListeners() { // 事件处理函数 async function handleConnected() { await initMqtt(); + await initToLaserPointerMqtt() roomId.value = room.name - status.value = false; - + status.value = false; ElMessage.success('已成功连接到房间'); // 初始化现有远程参与者 room.remoteParticipants.forEach(participant => { @@ -841,7 +1216,6 @@ async function handleConnected() { } function handleDisconnected(reason) { - status.value = true; cameraEnabled.value = false; microphoneEnabled.value = false; @@ -851,30 +1225,22 @@ function handleDisconnected(reason) { audioElementsMap.value.clear(); screenSharingUser.value = ''; activeScreenShareTrack.value = null; - ElMessage.error('连接已断开'); } -function handleReconnected() { - ElMessage.success('已重新连接到房间'); -} - // 处理轨道订阅事件 -function handleTrackSubscribed(track, publication, participant) { - +function handleTrackSubscribed(track, publication, participant) { if (track) { if (track.kind === Track.Kind.Video) { // 更新参与者轨道信息 - updateParticipantTrack(participant, publication.source, track); - + updateParticipantTrack(participant, publication.source, track); // 如果是屏幕共享,更新屏幕共享状态 if (publication.source === Track.Source.ScreenShare) { updateScreenShareState(participant, track); // 设置全局屏幕共享用户 globalScreenSharingUser.value = participant.identity; isGlobalScreenSharing.value = true; - } - + } // 立即附加到视频元素(如果元素已存在) attachTrackToParticipantVideo(participant.identity, publication.source, track); } else if (track.kind === Track.Kind.Audio) { @@ -882,8 +1248,7 @@ function handleTrackSubscribed(track, publication, participant) { updateParticipantAudioTrack(participant, track); attachTrackToParticipantAudio(participant.identity, track); } - } - + } // 更新参与者的轨道状态 updateParticipantTracks(participant); } @@ -893,8 +1258,7 @@ function handleTrackUnsubscribed(track, publication, participant) { if (track.kind === Track.Kind.Video) { removeParticipantTrack(participant, publication.source); // 清理视频元素 - detachTrackFromParticipantVideo(participant.identity, publication.source); - + detachTrackFromParticipantVideo(participant.identity, publication.source); // 如果是屏幕共享,更新屏幕共享状态 if (publication.source === Track.Source.ScreenShare) { updateScreenShareState(participant, null); @@ -928,14 +1292,12 @@ function handleParticipantDisconnected(participant) { if (screenShareVideo.value && screenShareVideo.value.srcObject) { screenShareVideo.value.srcObject = null; } - } - + } // 如果离开的用户是全局屏幕共享者,清除全局状态 if (participant.identity === globalScreenSharingUser.value) { globalScreenSharingUser.value = ''; isGlobalScreenSharing.value = false; - } - + } removeRemoteParticipant(participant); ElMessage.info(`用户离开: ${participant.identity}`); } @@ -948,12 +1310,10 @@ function handleLocalTrackPublished(publication) { } else if (publication.source === Track.Source.ScreenShare) { // 本地用户开始屏幕共享 screenSharingUser.value = room.localParticipant.identity; - activeScreenShareTrack.value = publication.track; - + activeScreenShareTrack.value = publication.track; // 设置全局屏幕共享状态 globalScreenSharingUser.value = room.localParticipant.identity; - isGlobalScreenSharing.value = true; - + isGlobalScreenSharing.value = true; if (screenShareVideo.value) { attachTrackToVideo(screenShareVideo.value, publication.track); } @@ -1024,8 +1384,7 @@ function setupParticipantListeners(participant) { function handleActiveSpeakersChanged(speakers) { // 更新本地说话状态 const localIsSpeaking = speakers.some(speaker => speaker.identity === room.localParticipant.identity); - isLocalSpeaking.value = localIsSpeaking; - + isLocalSpeaking.value = localIsSpeaking; // 更新远程参与者说话状态 remoteParticipants.value.forEach((data, identity) => { const isSpeaking = speakers.some(speaker => speaker.identity === identity); @@ -1036,28 +1395,12 @@ function handleActiveSpeakersChanged(speakers) { }); } -// 处理数据接收事件 -function handleDataReceived(payload, participant, kind) { - try { - const decoder = new TextDecoder(); - const strData = decoder.decode(payload); - ElMessage.info(`收到消息 from ${participant.identity}: ${strData}`); - } catch (error) { - console.error('处理接收消息失败:', error); - } -} - -function handleConnectionStateChanged(state) { - console.log('连接状态改变:', state); -} - // 更新屏幕共享状态 function updateScreenShareState(participant, track) { if (track) { // 有新的屏幕共享轨道 screenSharingUser.value = participant.identity; - activeScreenShareTrack.value = track; - + activeScreenShareTrack.value = track; // 附加到屏幕共享视频元素 if (screenShareVideo.value) { attachTrackToVideo(screenShareVideo.value, track); @@ -1224,8 +1567,7 @@ function updateParticipantTracks(participant) { // 检查视频轨道状态 let hasCamera = false; let hasScreen = false; - let hasAudio = false; - + let hasAudio = false; // 检查已发布的轨道 participant.videoTrackPublications.forEach(publication => { if (publication.isSubscribed && publication.track) { @@ -1267,15 +1609,7 @@ function updateParticipantSpeaking(participant, speaking) { data.isSpeaking = speaking; remoteParticipants.value.set(participant.identity, { ...data }); } -} - -function handleVideoLoaded(identity, type) { - console.log(`视频加载完成: ${identity}的${type}视频`); -} - -function handleScreenShareLoaded() { - console.log('屏幕共享视频加载完成'); -} +} // 视频轨道处理函数 function attachLocalVideoTrack(track) { @@ -1335,8 +1669,7 @@ async function toggleCamera() { if (!localVideo.value) { console.warn('本地视频元素未找到,等待DOM更新'); await nextTick(); - } - + } // 开启摄像头 // 确保有视屏输入设备权限和设备列表 if (cameraDevices.value.length === 0) { @@ -1344,10 +1677,9 @@ async function toggleCamera() { await handleCameraVisibleChange(true); } if (cameraDevices.value.length === 0) { - ElMessage.error('未找到可用的摄像头设备'); + // ElMessage.error('未找到可用的摄像头设备'); return; - } - + } // 自动选择第一个可用设备(如果当前没有选中设备) let deviceToUse = selectedCameraId.value; if (!deviceToUse && cameraDevices.value.length > 0) { @@ -1367,9 +1699,7 @@ async function toggleCamera() { attachLocalCameraTrack(); }, 200); ElMessage.success('麦克风已开启'); - } - - + } ElMessage.success('摄像头已开启'); } } catch (error) { @@ -1428,21 +1758,17 @@ async function toggleMicrophone() { if (microphoneDevices.value.length === 0) { // 如果没有设备列表,先获取 await handleMicrophoneVisibleChange(true); - } - + } if (microphoneDevices.value.length === 0) { - ElMessage.error('未找到可用的麦克风设备'); + // ElMessage.error('未找到可用的麦克风设备'); return; - } - - + } // 自动选择第一个可用设备(如果当前没有选中设备) let deviceToUse = selectedMicrophoneId.value; if (!deviceToUse && microphoneDevices.value.length > 0) { deviceToUse = microphoneDevices.value[0].deviceId; selectedMicrophoneId.value = deviceToUse; - } - + } if (deviceToUse) { await enableMicrophoneWithDevice(deviceToUse); ElMessage.success(`麦克风已开启 - ${getDeviceName(microphoneDevices.value, deviceToUse)}`); @@ -1451,8 +1777,7 @@ async function toggleMicrophone() { await room.localParticipant.setMicrophoneEnabled(true); microphoneEnabled.value = true; ElMessage.success('麦克风已开启'); - } - + } ElMessage.success('麦克风已开启'); } } catch (error) { @@ -1464,41 +1789,17 @@ async function toggleMicrophone() { } } -function errorHandling(error,type) { - switch (error.name) { - case 'NotAllowedError': - ElMessage.error('用户拒绝了权限请求,请允许此网站使用摄像头'); - break; - case 'NotFoundError': - ElMessage.error(`未检测到可用的${type}设备,请检查${type}是否已正确连接`); - break; - case 'NotSupportedError': - ElMessage.error(`当前浏览器不支持${type}功能,请使用现代浏览器如Chrome、Firefox或Edge`); - break; - case 'NotReadableError': - ElMessage.error(`${type}设备正被其他应用程序占用,请关闭其他使用${type}的应用后重试`); - break; - case 'OverconstrainedError': - ElMessage.error(`${type}配置不兼容,请尝试调整${type}设置`); - break; - default: - ElMessage.error('服务错误,请刷新重试'); - } -} - async function toggleScreenShare() { try { if(isWhiteboardActive.value){ ElMessage.error('请先关闭白板'); return; - } - + } // 检查是否已经有其他用户在共享屏幕 if (!isScreenSharing.value && isGlobalScreenSharing.value && globalScreenSharingUser.value !== hostUid.value) { ElMessage.error(`当前 ${globalScreenSharingUser.value} 正在共享屏幕,请等待其结束后再共享`); return; - } - + } if (isScreenSharing.value) { await room.localParticipant.setScreenShareEnabled(false); isScreenSharing.value = false; @@ -1513,7 +1814,7 @@ async function toggleScreenShare() { isScreenSharing.value = true; // 设置全局屏幕共享状态 globalScreenSharingUser.value = hostUid.value; - isGlobalScreenSharing.value = true; + isGlobalScreenSharing.value = true; ElMessage.success('屏幕共享已开始'); } } catch (error) { @@ -1524,8 +1825,7 @@ async function toggleScreenShare() { function handleScreenShareEnded() { console.log('用户通过浏览器控件停止了屏幕共享'); isScreenSharing.value = false; - ElMessage.info('屏幕共享已停止'); - + ElMessage.info('屏幕共享已停止'); // 移除事件监听器 room.localParticipant.off('screenShareEnded', handleScreenShareEnded); } @@ -1566,17 +1866,14 @@ async function leaveRoom() { if (whiteboardRef.value && whiteboardRef.value.cleanup) { whiteboardRef.value.cleanup(); } - } - + } // 断开MQTT连接 - mqttClient.disconnect(); - + mqttClient.disconnect(); // const res = await exitRoomApi(room.name) // 停止屏幕共享(如果正在共享) if (isScreenSharing.value) { await room.localParticipant.setScreenShareEnabled(false); - } - + } // 关闭摄像头和麦克风 await room.localParticipant.setCameraEnabled(false); await room.localParticipant.setMicrophoneEnabled(false); @@ -1585,11 +1882,9 @@ async function leaveRoom() { selectedMicrophoneId.value = ''; selectedCameraId.value = '' // 断开与房间的连接 - await room.disconnect(); - + await room.disconnect(); // 重置所有状态 - resetRoomState(); - + resetRoomState(); ElMessage.success('已离开会议'); router.push({ path: '/coordinate', @@ -1667,8 +1962,36 @@ function resetRoomState() { }); } +watch([() => hasActiveScreenShare.value, () => isScreenSharing.value], ([newHasActiveScreenShare, newIsScreenSharing]) => { + // 如果屏幕共享被关闭(包括本地停止共享或远程共享结束) + if (!newHasActiveScreenShare && !newIsScreenSharing) { + // 检查激光笔是否处于开启状态 + if (isLaserPointerActive.value) { + console.log('屏幕共享已结束,自动关闭激光笔'); + // 自动关闭激光笔 + closeLaserPointer(); + } + } +}, { immediate: true }); + +// 添加专门的关闭激光笔函数 +function closeLaserPointer() { + isLaserPointerActive.value = false; + cleanupLaserPointer(); + + // 重置Canvas样式 + if (laserPointerCanvas.value) { + laserPointerCanvas.value.style.pointerEvents = 'none'; + laserPointerCanvas.value.style.cursor = 'default'; + } + + ElMessage.info('屏幕共享已结束,激光笔已自动关闭'); +} + // 在组件卸载时也清理资源 onUnmounted(() => { + window.removeEventListener('resize', resizeLaserPointerCanvas); + cleanupLaserPointer(); if (isWhiteboardActive.value && whiteboardRef.value && whiteboardRef.value.cleanup) { whiteboardRef.value.cleanup(); } @@ -1701,10 +2024,40 @@ onMounted(async () => { } } }); - \ No newline at end of file