feat:更新激光笔功能
This commit is contained in:
@@ -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 || '密码修改失败');
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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">
|
||||
<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>
|
||||
Reference in New Issue
Block a user