feat:更新激光笔功能
This commit is contained in:
@@ -6,38 +6,40 @@
|
|||||||
width="500"
|
width="500"
|
||||||
:show-close='false'
|
:show-close='false'
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
v-loading="loading"
|
|
||||||
>
|
>
|
||||||
<el-form ref="pwdRef" :model="user" :rules="rules" label-width="100px">
|
<div v-loading="loading">
|
||||||
<el-form-item label="旧密码" prop="oldPassword">
|
<el-form ref="pwdRef" :model="user" :rules="rules" label-width="100px">
|
||||||
<el-input
|
<el-form-item label="旧密码" prop="oldPassword">
|
||||||
v-model="user.oldPassword"
|
<el-input
|
||||||
placeholder="请输入旧密码"
|
v-model="user.oldPassword"
|
||||||
type="password"
|
placeholder="请输入旧密码"
|
||||||
show-password
|
type="password"
|
||||||
/>
|
show-password
|
||||||
</el-form-item>
|
/>
|
||||||
<el-form-item label="新密码" prop="newPassword">
|
</el-form-item>
|
||||||
<el-input
|
<el-form-item label="新密码" prop="newPassword">
|
||||||
v-model="user.newPassword"
|
<el-input
|
||||||
placeholder="请输入新密码"
|
v-model="user.newPassword"
|
||||||
type="password"
|
placeholder="请输入新密码"
|
||||||
show-password
|
type="password"
|
||||||
/>
|
show-password
|
||||||
</el-form-item>
|
/>
|
||||||
<el-form-item label="确认密码" prop="confirmPassword">
|
</el-form-item>
|
||||||
<el-input
|
<el-form-item label="确认密码" prop="confirmPassword">
|
||||||
v-model="user.confirmPassword"
|
<el-input
|
||||||
placeholder="请确认新密码"
|
v-model="user.confirmPassword"
|
||||||
type="password"
|
placeholder="请确认新密码"
|
||||||
show-password
|
type="password"
|
||||||
/>
|
show-password
|
||||||
</el-form-item>
|
/>
|
||||||
<el-form-item>
|
</el-form-item>
|
||||||
<el-button type="primary" @click="submit">保存</el-button>
|
<el-form-item>
|
||||||
<el-button type="danger" @click="close">关闭</el-button>
|
<el-button type="primary" @click="submit">保存</el-button>
|
||||||
</el-form-item>
|
<el-button type="danger" @click="close">关闭</el-button>
|
||||||
</el-form>
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -112,6 +114,7 @@ const validatorPasswords = async (rule, value, callback) => {
|
|||||||
|
|
||||||
/** 提交按钮 */
|
/** 提交按钮 */
|
||||||
async function submit() {
|
async function submit() {
|
||||||
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const valid = await proxy.$refs.pwdRef.validate();
|
const valid = await proxy.$refs.pwdRef.validate();
|
||||||
if (!valid) return;
|
if (!valid) return;
|
||||||
@@ -125,9 +128,7 @@ async function submit() {
|
|||||||
ElMessage.error(resPwd.data.suggestions.join(', ') || '密码强度校验失败');
|
ElMessage.error(resPwd.data.suggestions.join(', ') || '密码强度校验失败');
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = true;
|
|
||||||
const res = await changePwd(user.oldPassword, user.newPassword);
|
const res = await changePwd(user.oldPassword, user.newPassword);
|
||||||
if(res.meta.code !== 200){
|
if(res.meta.code !== 200){
|
||||||
ElMessage.error(res.meta?.message || '密码修改失败');
|
ElMessage.error(res.meta?.message || '密码修改失败');
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
:index="resolvePath(onlyOneChild.path)"
|
:index="resolvePath(onlyOneChild.path)"
|
||||||
:class="{ 'submenu-title-noDropdown': !isNest }"
|
:class="{ 'submenu-title-noDropdown': !isNest }"
|
||||||
>
|
>
|
||||||
<svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" />
|
<!-- <svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" /> -->
|
||||||
<!-- {{ onlyOneChild.meta.title }} -->
|
<!-- {{ onlyOneChild.meta.title }} -->
|
||||||
<!-- <template #title> -->
|
<!-- <template #title> -->
|
||||||
<span class="menu-title" :title="hasTitle(onlyOneChild.meta.title)">
|
<span class="menu-title" :title="hasTitle(onlyOneChild.meta.title)">
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
<el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
|
<el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
|
||||||
<template v-if="item.meta" #title>
|
<template v-if="item.meta" #title>
|
||||||
<svg-icon :icon-class="item.meta && item.meta.icon" />
|
<!-- <svg-icon :icon-class="item.meta && item.meta.icon" /> -->
|
||||||
<span class="menu-title" :title="hasTitle(item.meta.title)">{{ item.meta.title }}</span>
|
<span class="menu-title" :title="hasTitle(item.meta.title)">{{ item.meta.title }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const settingsStore = useSettingsStore()
|
|||||||
|
|
||||||
const showLogo = computed(() => settingsStore.sidebarLogo)
|
const showLogo = computed(() => settingsStore.sidebarLogo)
|
||||||
const sideTheme = computed(() => settingsStore.sideTheme)
|
const sideTheme = computed(() => settingsStore.sideTheme)
|
||||||
// const theme = computed(() => settingsStore.theme)
|
const theme = computed(() => settingsStore.theme)
|
||||||
const isCollapse = computed(() => !appStore.sidebar.opened)
|
const isCollapse = computed(() => !appStore.sidebar.opened)
|
||||||
|
|
||||||
const activeMenu = computed(() => {
|
const activeMenu = computed(() => {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const useSettingsStore = defineStore(
|
|||||||
{
|
{
|
||||||
state: () => ({
|
state: () => ({
|
||||||
title: '',
|
title: '',
|
||||||
theme: storageSetting.theme === undefined ? '#141414' : storageSetting.theme,
|
theme: storageSetting.theme === undefined ? '#434343' : storageSetting.theme,
|
||||||
sideTheme: storageSetting.sideTheme === undefined ? sideTheme : storageSetting.sideTheme,
|
sideTheme: storageSetting.sideTheme === undefined ? sideTheme : storageSetting.sideTheme,
|
||||||
showSettings: showSettings,
|
showSettings: showSettings,
|
||||||
topNav: storageSetting.topNav === undefined ? topNav : storageSetting.topNav,
|
topNav: storageSetting.topNav === undefined ? topNav : storageSetting.topNav,
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<div class="file-upload-container">
|
||||||
|
<input type="file" ref="fileInput" @change="handleFileChange" />
|
||||||
|
<button @click="uploadFile">上传文件</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -45,6 +45,10 @@
|
|||||||
<div class="video-overlay">
|
<div class="video-overlay">
|
||||||
<span class="participant-name">{{ screenSharingUser }}</span>
|
<span class="participant-name">{{ screenSharingUser }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 添加canvas容器 -->
|
||||||
|
<div class="canvas-container" ref="canvasContainerRef">
|
||||||
|
<canvas ref="screenShareCanvasRef" class="screen-share-canvas"></canvas>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -207,6 +211,12 @@
|
|||||||
<el-button @click="toggleWhiteboard" :type="isWhiteboardActive ? 'danger' : 'info'" class="control-btn" size="large">
|
<el-button @click="toggleWhiteboard" :type="isWhiteboardActive ? 'danger' : 'info'" class="control-btn" size="large">
|
||||||
{{ isWhiteboardActive ? '退出白板' : '共享白板' }}
|
{{ isWhiteboardActive ? '退出白板' : '共享白板' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button @click="laserPointer" type="info" class="control-btn" size="large">
|
||||||
|
激光笔
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="fileUploadHandle" type="info" class="control-btn" size="large">
|
||||||
|
文件上传
|
||||||
|
</el-button>
|
||||||
<el-button @click="inviterJoinRoom" type="info" class="control-btn" size="large">
|
<el-button @click="inviterJoinRoom" type="info" class="control-btn" size="large">
|
||||||
邀请人员
|
邀请人员
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -216,9 +226,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<!-- 邀请人员组件 -->
|
||||||
<!-- 邀请人员组件 -->
|
<InviterJoinRoom ref="inviterJoinRoomRef" @confirmSelection="handleConfirmSelection" />
|
||||||
<InviterJoinRoom ref="inviterJoinRoomRef" @confirmSelection="handleConfirmSelection" />
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -233,20 +243,16 @@ import { useUserStore } from '@/stores/modules/user.js'
|
|||||||
import tabulaRase from '@/views/custom/tabulaRase/index.vue'
|
import tabulaRase from '@/views/custom/tabulaRase/index.vue'
|
||||||
import { mqttClient } from "@/utils/mqtt.js";
|
import { mqttClient } from "@/utils/mqtt.js";
|
||||||
import InviterJoinRoom from "@/views/conferencingRoom/components/InviterJoinRoom/index.vue"
|
import InviterJoinRoom from "@/views/conferencingRoom/components/InviterJoinRoom/index.vue"
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const roomStore = useRoomStore()
|
const roomStore = useRoomStore()
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
// LiveKit 服务器配置
|
// LiveKit 服务器配置
|
||||||
const wsURL = "wss://meeting.cnsdt.com:443";
|
const wsURL = "wss://meeting.cnsdt.com:443";
|
||||||
|
|
||||||
const hostUid = ref('')
|
const hostUid = ref('')
|
||||||
const status = ref(true);
|
const status = ref(true);
|
||||||
const roomName = ref('')
|
const roomName = ref('')
|
||||||
const roomId = ref('')
|
const roomId = ref('')
|
||||||
|
|
||||||
// 音视频相关引用和数据
|
// 音视频相关引用和数据
|
||||||
const localVideo = ref(null);
|
const localVideo = ref(null);
|
||||||
const localAudio = ref(null);
|
const localAudio = ref(null);
|
||||||
@@ -254,35 +260,38 @@ const cameraEnabled = ref(false);
|
|||||||
const microphoneEnabled = ref(false);
|
const microphoneEnabled = ref(false);
|
||||||
const isScreenSharing = ref(false);
|
const isScreenSharing = ref(false);
|
||||||
const isLocalSpeaking = ref(false);
|
const isLocalSpeaking = ref(false);
|
||||||
|
|
||||||
// 设备列表
|
// 设备列表
|
||||||
const cameraDevices = ref([]);
|
const cameraDevices = ref([]);
|
||||||
const microphoneDevices = ref([]);
|
const microphoneDevices = ref([]);
|
||||||
const selectedCameraId = ref('');
|
const selectedCameraId = ref('');
|
||||||
const selectedMicrophoneId = ref('');
|
const selectedMicrophoneId = ref('');
|
||||||
|
|
||||||
// 远程参与者管理
|
// 远程参与者管理
|
||||||
const remoteParticipants = ref(new Map());
|
const remoteParticipants = ref(new Map());
|
||||||
const videoElementsMap = ref(new Map());
|
const videoElementsMap = ref(new Map());
|
||||||
const audioElementsMap = ref(new Map());
|
const audioElementsMap = ref(new Map());
|
||||||
|
|
||||||
// 屏幕共享相关
|
// 屏幕共享相关
|
||||||
const screenSharingUser = ref('');
|
const screenSharingUser = ref('');
|
||||||
const activeScreenShareTrack = ref(null);
|
const activeScreenShareTrack = ref(null);
|
||||||
const screenShareVideo = ref(null);
|
const screenShareVideo = ref(null);//屏幕共享视频元素
|
||||||
const globalScreenSharingUser = ref(''); // 当前正在共享屏幕的用户
|
const globalScreenSharingUser = ref(''); // 当前正在共享屏幕的用户
|
||||||
const isGlobalScreenSharing = ref(false); // 是否有用户正在共享屏幕
|
const isGlobalScreenSharing = ref(false); // 是否有用户正在共享屏幕
|
||||||
|
|
||||||
//共享白板
|
//共享白板
|
||||||
const isWhiteboardActive = ref(false);
|
const isWhiteboardActive = ref(false);
|
||||||
const whiteboardRef = ref(null);
|
const whiteboardRef = ref(null);
|
||||||
const isGlobalWhiteboardSharing = ref(false); // 是否有用户正在共享白板
|
const isGlobalWhiteboardSharing = ref(false); // 是否有用户正在共享白板
|
||||||
|
|
||||||
//当前房间信息
|
//当前房间信息
|
||||||
const roomInfo = ref('')
|
const roomInfo = ref('')
|
||||||
|
|
||||||
//邀请参会人员
|
//邀请参会人员
|
||||||
const inviterJoinRoomRef = ref()
|
const inviterJoinRoomRef = ref()
|
||||||
|
// 添加canvas相关引用
|
||||||
|
const screenShareCanvasRef = ref(null);
|
||||||
|
const canvasContainerRef = ref(null);
|
||||||
|
const canvasContext = ref(null);
|
||||||
|
const isCanvasActive = ref(false);
|
||||||
|
const canvas = ref(null)
|
||||||
|
const ctx = ref(null)
|
||||||
|
const isDrawing = ref(false)
|
||||||
|
const startCoords = ref(null)
|
||||||
|
|
||||||
// 白板消息类型
|
// 白板消息类型
|
||||||
const WHITEBOARD_MESSAGE_TYPES = {
|
const WHITEBOARD_MESSAGE_TYPES = {
|
||||||
@@ -325,37 +334,33 @@ const room = new Room({
|
|||||||
echoCancellation: true,
|
echoCancellation: true,
|
||||||
noiseSuppression: true,
|
noiseSuppression: true,
|
||||||
autoGainControl: true,
|
autoGainControl: true,
|
||||||
channelCount: 1, // 单声道减少带宽
|
|
||||||
},
|
},
|
||||||
videoCaptureDefaults: {
|
videoCaptureDefaults: {
|
||||||
// resolution: { width: 1280, height: 720 },
|
resolution: { width: 1280, height: 720 },
|
||||||
resolution: { width: 640, height: 480 }, // 降低分辨率
|
|
||||||
frameRate: 15, // 降低帧率
|
|
||||||
},
|
},
|
||||||
publishDefaults: {
|
publishDefaults: {
|
||||||
screenShareEncoding: {
|
screenShareEncoding: {
|
||||||
maxBitrate: 3_000_000,
|
maxBitrate: 3_000_000,
|
||||||
maxFramerate: 30,
|
maxFramerate: 30,
|
||||||
|
},
|
||||||
},
|
|
||||||
videoEncoding: {
|
videoEncoding: {
|
||||||
// maxBitrate: 2_500_000,
|
maxBitrate: 2_500_000,
|
||||||
// maxFramerate: 30,
|
maxFramerate: 30,
|
||||||
maxBitrate: 800000, // 降低视频比特率
|
|
||||||
maxFramerate: 15,
|
|
||||||
},
|
},
|
||||||
// 音频发布配置
|
// 音频发布配置
|
||||||
audioEncoding: {
|
audioEncoding: {
|
||||||
// maxBitrate: 64000, // 64kbps
|
maxBitrate: 64000,
|
||||||
maxBitrate: 32000, // 降低音频比特率
|
|
||||||
},
|
},
|
||||||
dtx: true, // 不连续传输,节省带宽
|
// dtx: true, // 不连续传输,节省带宽
|
||||||
red: true, // 冗余编码,提高抗丢包能力
|
red: true, // 冗余编码,提高抗丢包能力
|
||||||
},
|
}
|
||||||
// 添加视频编解码器偏好
|
|
||||||
videoCodec: 'vp8', // VP8 通常更稳定
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//文件上传
|
||||||
|
async function fileUploadHandle(){
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
//摄像头打开下拉框触发
|
//摄像头打开下拉框触发
|
||||||
async function handleCameraVisibleChange(e){
|
async function handleCameraVisibleChange(e){
|
||||||
try {
|
try {
|
||||||
@@ -376,7 +381,6 @@ async function handleCameraVisibleChange(e){
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('摄像头访问失败:', error);
|
console.error('摄像头访问失败:', error);
|
||||||
ElMessage.error('摄像头访问失败,请检查权限设置');
|
|
||||||
// 使用更友好的方式提示用户
|
// 使用更友好的方式提示用户
|
||||||
errorHandling(error,'摄像头');
|
errorHandling(error,'摄像头');
|
||||||
// 清空设备列表
|
// 清空设备列表
|
||||||
@@ -423,7 +427,6 @@ async function handleMicrophoneVisibleChange(e){
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('麦克风访问失败:', error);
|
console.error('麦克风访问失败:', error);
|
||||||
ElMessage.error('麦克风访问失败,请检查权限设置');
|
|
||||||
// 使用更友好的方式提示用户
|
// 使用更友好的方式提示用户
|
||||||
errorHandling(error,'麦克风');
|
errorHandling(error,'麦克风');
|
||||||
// 清空设备列表
|
// 清空设备列表
|
||||||
@@ -494,7 +497,6 @@ async function enableMicrophoneWithDevice(deviceId) {
|
|||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('使用指定设备开启麦克风失败:', error);
|
console.error('使用指定设备开启麦克风失败:', error);
|
||||||
ElMessage.error('使用指定设备开启麦克风失败,请检查权限设置');
|
|
||||||
microphoneEnabled.value = false;
|
microphoneEnabled.value = false;
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -520,7 +522,6 @@ async function switchCameraDevice(deviceId) {
|
|||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('切换摄像头设备失败:', error);
|
console.error('切换摄像头设备失败:', error);
|
||||||
ElMessage.error('切换摄像头设备失败');
|
|
||||||
// 如果切换失败,尝试重新开启之前的设备
|
// 如果切换失败,尝试重新开启之前的设备
|
||||||
try {
|
try {
|
||||||
await room.localParticipant.setCameraEnabled(true);
|
await room.localParticipant.setCameraEnabled(true);
|
||||||
@@ -546,7 +547,6 @@ async function switchMicrophoneDevice(deviceId) {
|
|||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('切换麦克风设备失败:', error);
|
console.error('切换麦克风设备失败:', error);
|
||||||
ElMessage.error('切换麦克风设备失败');
|
|
||||||
// 如果切换失败,尝试重新开启之前的设备
|
// 如果切换失败,尝试重新开启之前的设备
|
||||||
try {
|
try {
|
||||||
await room.localParticipant.setMicrophoneEnabled(true);
|
await room.localParticipant.setMicrophoneEnabled(true);
|
||||||
@@ -584,7 +584,6 @@ function subscribeToWhiteboardTopic() {
|
|||||||
mqttClient.subscribe(`xSynergy/shareWhiteboard/${room.name}`, handleWhiteboardMessage);
|
mqttClient.subscribe(`xSynergy/shareWhiteboard/${room.name}`, handleWhiteboardMessage);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('订阅白板主题失败:', error);
|
console.error('订阅白板主题失败:', error);
|
||||||
ElMessage.error('订阅白板主题失败');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -615,7 +614,6 @@ function handleWhiteboardMessage(payload, topic) {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('处理白板消息失败:', error);
|
console.error('处理白板消息失败:', error);
|
||||||
ElMessage.error('处理白板消息失败');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -812,12 +810,11 @@ function setParticipantAudioRef(el, identity) {
|
|||||||
// 设置屏幕共享视频引用
|
// 设置屏幕共享视频引用
|
||||||
function setScreenShareVideoRef(el) {
|
function setScreenShareVideoRef(el) {
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
screenShareVideo.value = el;
|
screenShareVideo.value = el;
|
||||||
|
|
||||||
// 如果已经有屏幕共享轨道,立即附加
|
// 如果已经有屏幕共享轨道,立即附加
|
||||||
if (activeScreenShareTrack.value) {
|
if (activeScreenShareTrack.value) {
|
||||||
attachTrackToVideo(el, activeScreenShareTrack.value);
|
attachTrackToVideo(el, activeScreenShareTrack.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置事件监听器
|
// 设置事件监听器
|
||||||
@@ -1582,17 +1579,14 @@ async function leaveRoom() {
|
|||||||
if (whiteboardRef.value && whiteboardRef.value.cleanup) {
|
if (whiteboardRef.value && whiteboardRef.value.cleanup) {
|
||||||
whiteboardRef.value.cleanup();
|
whiteboardRef.value.cleanup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 断开MQTT连接
|
// 断开MQTT连接
|
||||||
mqttClient.disconnect();
|
mqttClient.disconnect();
|
||||||
|
|
||||||
// const res = await exitRoomApi(room.name)
|
// const res = await exitRoomApi(room.name)
|
||||||
// 停止屏幕共享(如果正在共享)
|
// 停止屏幕共享(如果正在共享)
|
||||||
if (isScreenSharing.value) {
|
if (isScreenSharing.value) {
|
||||||
await room.localParticipant.setScreenShareEnabled(false);
|
await room.localParticipant.setScreenShareEnabled(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭摄像头和麦克风
|
// 关闭摄像头和麦克风
|
||||||
await room.localParticipant.setCameraEnabled(false);
|
await room.localParticipant.setCameraEnabled(false);
|
||||||
await room.localParticipant.setMicrophoneEnabled(false);
|
await room.localParticipant.setMicrophoneEnabled(false);
|
||||||
@@ -1601,11 +1595,9 @@ async function leaveRoom() {
|
|||||||
selectedMicrophoneId.value = '';
|
selectedMicrophoneId.value = '';
|
||||||
selectedCameraId.value = ''
|
selectedCameraId.value = ''
|
||||||
// 断开与房间的连接
|
// 断开与房间的连接
|
||||||
await room.disconnect();
|
await room.disconnect();
|
||||||
|
|
||||||
// 重置所有状态
|
// 重置所有状态
|
||||||
resetRoomState();
|
resetRoomState();
|
||||||
|
|
||||||
ElMessage.success('已离开会议');
|
ElMessage.success('已离开会议');
|
||||||
router.push({
|
router.push({
|
||||||
path: '/coordinate',
|
path: '/coordinate',
|
||||||
@@ -1719,3 +1711,683 @@ onMounted(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
/* 白板容器样式 */
|
||||||
|
.whiteboard-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.whiteboard-component {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 退出白板按钮样式 */
|
||||||
|
.exit-whiteboard-btn {
|
||||||
|
margin-left: 10px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 添加音频状态指示器样式 */
|
||||||
|
.audio-indicator {
|
||||||
|
margin-left: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-indicator.muted {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-indicator:not(.muted) {
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.speaking {
|
||||||
|
background-color: #67c23a;
|
||||||
|
animation: pulse 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 隐藏音频元素,不显示在页面上 */
|
||||||
|
.audio-element, .participant-audio {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 控制下拉菜单样式 */
|
||||||
|
.control-dropdown {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-dropdown .el-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 选中的设备样式 */
|
||||||
|
.selected-device {
|
||||||
|
background-color: #f0f9ff;
|
||||||
|
color: #409eff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-icon {
|
||||||
|
margin-left: auto;
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 设备列表项样式 */
|
||||||
|
.el-dropdown-menu__item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 全局样式 */
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #0f0f1a;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.livekit-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(135deg, #0f0f1a 0%, #1a1a2e 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单样式 */
|
||||||
|
.form-container {
|
||||||
|
max-width: 480px;
|
||||||
|
margin: 60px auto;
|
||||||
|
padding: 40px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-header h2 {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-header p {
|
||||||
|
margin: 0;
|
||||||
|
color: #a0a0b0;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.livekit-form {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.livekit-form .el-form-item {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.livekit-form .el-form-item__label {
|
||||||
|
color: #c0c0d0;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.livekit-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.livekit-input .el-input__inner {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 10px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.livekit-input .el-input__inner:focus {
|
||||||
|
border-color: #4a6cf7;
|
||||||
|
box-shadow: 0 0 0 2px rgba(74, 108, 247, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.livekit-input .el-input__inner::placeholder {
|
||||||
|
color: #6a6a7a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-radio {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-radio .el-radio__input.is-checked + .el-radio__label {
|
||||||
|
color: #4a6cf7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-radio .el-radio__inner {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-radio .el-radio__inner::after {
|
||||||
|
background-color: #4a6cf7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-button {
|
||||||
|
width: 100%;
|
||||||
|
background: linear-gradient(135deg, #4a6cf7 0%, #6a4af7 100%);
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 14px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 8px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 12px rgba(74, 108, 247, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(74, 108, 247, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 会议界面样式 */
|
||||||
|
.meeting-container {
|
||||||
|
width: calc(100vw - 40px) ;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 视频布局 */
|
||||||
|
.video-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
height: calc(100vh - 80px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 当有屏幕共享时的布局 */
|
||||||
|
.video-layout.screen-sharing-active {
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 屏幕共享区域 */
|
||||||
|
.screen-share-area {
|
||||||
|
flex: 3;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screen-share-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.screen-share-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sharing-user {
|
||||||
|
color: #a0a0b0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screen-share-video {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screen-share-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - 36px) ;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screen-share-element {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
background: #000;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 参会者区域 */
|
||||||
|
.participants-area {
|
||||||
|
flex: 1;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participants-area.with-screen-share {
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participants-header {
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.participants-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-grid {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: calc(100vh - 200px); /* 为底部控制栏留出空间 */
|
||||||
|
margin-bottom: 80px; /* 确保内容不被底部控制栏遮挡 */
|
||||||
|
}
|
||||||
|
.video-grid.grid-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participant-card {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 280px; /* 统一最小高度 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-grid.grid-layout .video-wrapper {
|
||||||
|
height: 200px; /* 在网格布局中也保持相同高度 */
|
||||||
|
}
|
||||||
|
.participant-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.local-participant {
|
||||||
|
border: 2px solid rgba(74, 108, 247, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.participant-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.participant-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #a0a0b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #4ade80;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height:calc(100% - 36px) ; /* 固定高度 */
|
||||||
|
overflow: hidden;
|
||||||
|
background: #000; /* 统一背景色 */
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-element {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-overlay {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participant-name {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screen-sharing-indicator {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #4a6cf7;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(135deg, #2a2a3a 0%, #1a1a2a 100%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #a0a0b0;
|
||||||
|
font-size: 14px;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-placeholder i {
|
||||||
|
font-size: 32px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #4a4a5a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fixed-controls {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: rgba(15, 15, 26, 0.95);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 16px 20px;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: auto; /* 将控制按钮推到卡片底部 */
|
||||||
|
}
|
||||||
|
.control-btn {
|
||||||
|
border-radius: 12px;
|
||||||
|
border: none;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
padding: 12px 24px;
|
||||||
|
font-size: 14px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn i {
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leave-btn {
|
||||||
|
background: linear-gradient(135deg, #f56c6c 0%, #e64a4a 100%);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leave-btn:hover {
|
||||||
|
background: linear-gradient(135deg, #e64a4a 0%, #d63030 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 开启摄像头和麦克风下拉框 */
|
||||||
|
.control-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.dropdown-btn {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.selected-device {
|
||||||
|
background-color: #f0f7ff;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
.check-icon {
|
||||||
|
margin-left: 8px;
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microphone-control-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.microphone-control-group .microphone-btn {
|
||||||
|
border-radius: 12px 0 0 12px;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microphone-control-group .microphone-dropdown {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microphone-control-group .dropdown-btn {
|
||||||
|
border-radius: 0 12px 12px 0;
|
||||||
|
border: none;
|
||||||
|
border-left: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
box-shadow: none;
|
||||||
|
width: 44px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 悬停效果 */
|
||||||
|
.microphone-control-group:hover {
|
||||||
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.microphone-control-group .control-btn:hover {
|
||||||
|
transform: none; /* 取消单独的悬停位移 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.microphone-control-group {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microphone-control-group .microphone-btn {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microphone-control-group .dropdown-btn {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
.whiteboard-container {
|
||||||
|
height: 300px; /* 在移动端限制白板高度 */
|
||||||
|
}
|
||||||
|
.form-container {
|
||||||
|
margin: 20px auto;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-layout.screen-sharing-active {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participants-area.with-screen-share {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 在小屏幕上使用单列布局 */
|
||||||
|
.video-grid.grid-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端控制栏调整 */
|
||||||
|
.fixed-controls {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-container {
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn {
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn i {
|
||||||
|
margin-right: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 中等屏幕上的网格布局 */
|
||||||
|
@media (min-width: 769px) and (max-width: 1200px) {
|
||||||
|
.video-grid.grid-layout {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 大屏幕上的网格布局 */
|
||||||
|
@media (min-width: 1201px) {
|
||||||
|
.video-grid.grid-layout {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 超小屏幕调整 */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.microphone-control-group {
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microphone-control-group .microphone-btn {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microphone-control-group .dropdown-btn {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
.controls-container {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn i {
|
||||||
|
margin-right: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user