feat:更新激光笔功能

This commit is contained in:
leilei
2025-10-23 10:22:56 +08:00
parent f19ab86ada
commit 528170fe2f
8 changed files with 1432 additions and 409 deletions

View File

@@ -6,8 +6,9 @@
width="500"
:show-close='false'
:close-on-click-modal="false"
v-loading="loading"
>
<div v-loading="loading">
<el-form ref="pwdRef" :model="user" :rules="rules" label-width="100px">
<el-form-item label="旧密码" prop="oldPassword">
<el-input
@@ -38,6 +39,7 @@
<el-button type="danger" @click="close">关闭</el-button>
</el-form-item>
</el-form>
</div>
</el-dialog>
</div>
@@ -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;
@@ -126,8 +129,6 @@ async function submit() {
return
}
}
loading.value = true;
const res = await changePwd(user.oldPassword, user.newPassword);
if(res.meta.code !== 200){
ElMessage.error(res.meta?.message || '密码修改失败');

View File

@@ -12,7 +12,7 @@
:index="resolvePath(onlyOneChild.path)"
: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 }} -->
<!-- <template #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>
<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>
</template>

View File

@@ -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(() => {

View File

@@ -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,

View File

@@ -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);
}

View File

@@ -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

View File

@@ -45,6 +45,10 @@
<div class="video-overlay">
<span class="participant-name">{{ screenSharingUser }}</span>
</div>
<!-- 添加canvas容器 -->
<div class="canvas-container" ref="canvasContainerRef">
<canvas ref="screenShareCanvasRef" class="screen-share-canvas"></canvas>
</div>
</div>
</div>
</div>
@@ -207,6 +211,12 @@
<el-button @click="toggleWhiteboard" :type="isWhiteboardActive ? 'danger' : 'info'" class="control-btn" size="large">
{{ isWhiteboardActive ? '退出白板' : '共享白板' }}
</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>
@@ -216,9 +226,9 @@
</div>
</div>
</div>
</div>
<!-- 邀请人员组件 -->
<InviterJoinRoom ref="inviterJoinRoomRef" @confirmSelection="handleConfirmSelection" />
</div>
</template>
<script setup>
@@ -233,20 +243,16 @@ 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,35 +260,38 @@ 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 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 = {
@@ -325,37 +334,33 @@ const room = new Room({
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
channelCount: 1, // 单声道减少带宽
},
videoCaptureDefaults: {
// resolution: { width: 1280, height: 720 },
resolution: { width: 640, height: 480 }, // 降低分辨率
frameRate: 15, // 降低帧率
resolution: { width: 1280, height: 720 },
},
publishDefaults: {
screenShareEncoding: {
maxBitrate: 3_000_000,
maxFramerate: 30,
},
videoEncoding: {
// maxBitrate: 2_500_000,
// maxFramerate: 30,
maxBitrate: 800000, // 降低视频比特率
maxFramerate: 15,
maxBitrate: 2_500_000,
maxFramerate: 30,
},
// 音频发布配置
audioEncoding: {
// maxBitrate: 64000, // 64kbps
maxBitrate: 32000, // 降低音频比特率
maxBitrate: 64000,
},
dtx: true, // 不连续传输,节省带宽
// dtx: true, // 不连续传输,节省带宽
red: true, // 冗余编码,提高抗丢包能力
},
// 添加视频编解码器偏好
videoCodec: 'vp8', // VP8 通常更稳定
}
});
//文件上传
async function fileUploadHandle(){
}
//摄像头打开下拉框触发
async function handleCameraVisibleChange(e){
try {
@@ -376,7 +381,6 @@ async function handleCameraVisibleChange(e){
}
} catch (error) {
console.error('摄像头访问失败:', error);
ElMessage.error('摄像头访问失败,请检查权限设置');
// 使用更友好的方式提示用户
errorHandling(error,'摄像头');
// 清空设备列表
@@ -423,7 +427,6 @@ async function handleMicrophoneVisibleChange(e){
}
} catch (error) {
console.error('麦克风访问失败:', error);
ElMessage.error('麦克风访问失败,请检查权限设置');
// 使用更友好的方式提示用户
errorHandling(error,'麦克风');
// 清空设备列表
@@ -494,7 +497,6 @@ async function enableMicrophoneWithDevice(deviceId) {
return true;
} catch (error) {
console.error('使用指定设备开启麦克风失败:', error);
ElMessage.error('使用指定设备开启麦克风失败,请检查权限设置');
microphoneEnabled.value = false;
throw error;
}
@@ -520,7 +522,6 @@ async function switchCameraDevice(deviceId) {
return true;
} catch (error) {
console.error('切换摄像头设备失败:', error);
ElMessage.error('切换摄像头设备失败');
// 如果切换失败,尝试重新开启之前的设备
try {
await room.localParticipant.setCameraEnabled(true);
@@ -546,7 +547,6 @@ async function switchMicrophoneDevice(deviceId) {
return true;
} catch (error) {
console.error('切换麦克风设备失败:', error);
ElMessage.error('切换麦克风设备失败');
// 如果切换失败,尝试重新开启之前的设备
try {
await room.localParticipant.setMicrophoneEnabled(true);
@@ -584,7 +584,6 @@ function subscribeToWhiteboardTopic() {
mqttClient.subscribe(`xSynergy/shareWhiteboard/${room.name}`, handleWhiteboardMessage);
} catch (error) {
console.error('订阅白板主题失败:', error);
ElMessage.error('订阅白板主题失败');
}
}
@@ -615,7 +614,6 @@ function handleWhiteboardMessage(payload, topic) {
}
} catch (error) {
console.error('处理白板消息失败:', error);
ElMessage.error('处理白板消息失败');
}
}
@@ -813,7 +811,6 @@ function setParticipantAudioRef(el, identity) {
function setScreenShareVideoRef(el) {
if (!el) return;
screenShareVideo.value = el;
// 如果已经有屏幕共享轨道,立即附加
if (activeScreenShareTrack.value) {
attachTrackToVideo(el, activeScreenShareTrack.value);
@@ -1583,16 +1580,13 @@ async function leaveRoom() {
whiteboardRef.value.cleanup();
}
}
// 断开MQTT连接
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);
@@ -1602,10 +1596,8 @@ async function leaveRoom() {
selectedCameraId.value = ''
// 断开与房间的连接
await room.disconnect();
// 重置所有状态
resetRoomState();
ElMessage.success('已离开会议');
router.push({
path: '/coordinate',
@@ -1719,3 +1711,683 @@ onMounted(async () => {
});
</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>