|
|
|
|
<template>
|
|
|
|
|
<div class="meeting-container">
|
|
|
|
|
<!-- 1. 顶部信息栏 -->
|
|
|
|
|
<header class="top-bar">
|
|
|
|
|
<div class="logo">信联</div>
|
|
|
|
|
<div class="title">超声事业部的云诊室</div>
|
|
|
|
|
<div class="window-controls">
|
|
|
|
|
<i class="el-icon-minus" @click="minimizeWindow"></i>
|
|
|
|
|
<i class="el-icon-close" @click="closeWindow"></i>
|
|
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<!-- 2. 会议状态栏 -->
|
|
|
|
|
<div class="status-bar">
|
|
|
|
|
<span>会议口令: 1019100</span>
|
|
|
|
|
<span class="divider">|</span>
|
|
|
|
|
<span>{{ timerDisplay }}</span>
|
|
|
|
|
<!-- 计时器显示 -->
|
|
|
|
|
<span class="divider">|</span>
|
|
|
|
|
<span>1人在会议中</span>
|
|
|
|
|
<span class="divider">|</span>
|
|
|
|
|
<span>1080P</span>
|
|
|
|
|
<span class="divider">|</span>
|
|
|
|
|
<span>25fps</span>
|
|
|
|
|
<span class="divider">|</span>
|
|
|
|
|
<span>网络延迟 0ms</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 3. 主视频区域 -->
|
|
|
|
|
<main class="main-content">
|
|
|
|
|
<!-- 视频流容器 (模拟黑屏) -->
|
|
|
|
|
<div class="video-stage" ref="videoContainer">
|
|
|
|
|
<!-- 这里放置远程视频流 -->
|
|
|
|
|
<div class="placeholder-text">视频流区域</div>
|
|
|
|
|
|
|
|
|
|
<!-- 绘图层 (Canvas) - 覆盖在视频之上 -->
|
|
|
|
|
<canvas
|
|
|
|
|
v-show="isDrawingMode"
|
|
|
|
|
ref="drawingCanvas"
|
|
|
|
|
class="drawing-canvas"
|
|
|
|
|
@mousedown="startDrawing"
|
|
|
|
|
@mousemove="draw"
|
|
|
|
|
@mouseup="stopDrawing"
|
|
|
|
|
@mouseleave="stopDrawing"
|
|
|
|
|
></canvas>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 右侧侧边栏 (参会人/基层医生) -->
|
|
|
|
|
<aside class="sidebar">
|
|
|
|
|
<div class="remote-user">
|
|
|
|
|
<div class="video-box">
|
|
|
|
|
<img
|
|
|
|
|
src="https://via.placeholder.com/300x200?text=Remote+Video"
|
|
|
|
|
alt="Remote"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="user-info">
|
|
|
|
|
<span>基层医生</span>
|
|
|
|
|
<i class="el-icon-video-camera"></i>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</aside>
|
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
<!-- 4. 底部工具栏 -->
|
|
|
|
|
<footer class="bottom-bar">
|
|
|
|
|
<el-button type="primary" class="btn-create">创建病例</el-button>
|
|
|
|
|
|
|
|
|
|
<div class="tools">
|
|
|
|
|
<!-- 麦克风 -->
|
|
|
|
|
<div class="tool-item" @click="toggleMic">
|
|
|
|
|
<i
|
|
|
|
|
:class="micOn ? 'el-icon-microphone' : 'el-icon-microphone-off'"
|
|
|
|
|
></i>
|
|
|
|
|
<span>麦克风</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 摄像头 -->
|
|
|
|
|
<div class="tool-item" @click="toggleCam">
|
|
|
|
|
<i
|
|
|
|
|
:class="camOn ? 'el-icon-video-camera' : 'el-icon-video-camera-off'"
|
|
|
|
|
></i>
|
|
|
|
|
<span>摄像头</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 共享 -->
|
|
|
|
|
<div class="tool-item">
|
|
|
|
|
<i class="el-icon-share"></i>
|
|
|
|
|
<span>共享</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 画笔 (核心功能) -->
|
|
|
|
|
<div
|
|
|
|
|
class="tool-item"
|
|
|
|
|
:class="{ active: isDrawingMode }"
|
|
|
|
|
@click="toggleDraw"
|
|
|
|
|
>
|
|
|
|
|
<i class="el-icon-edit"></i>
|
|
|
|
|
<span>画笔</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 激光笔 (模拟) -->
|
|
|
|
|
<div class="tool-item">
|
|
|
|
|
<i class="el-icon-aim"></i>
|
|
|
|
|
<span>激光笔</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 邀请 (核心功能) -->
|
|
|
|
|
<div class="tool-item" @click="handleInvite">
|
|
|
|
|
<i class="el-icon-user-add"></i>
|
|
|
|
|
<span>邀请</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 录制 (核心功能) -->
|
|
|
|
|
<div class="tool-item" @click="toggleRecord">
|
|
|
|
|
<i
|
|
|
|
|
class="el-icon-video-play"
|
|
|
|
|
:class="{ 'is-recording': isRecording }"
|
|
|
|
|
></i>
|
|
|
|
|
<span>{{ isRecording ? "停止录制" : "录制" }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 截图 -->
|
|
|
|
|
<div class="tool-item" @click="takeScreenshot">
|
|
|
|
|
<i class="el-icon-camera"></i>
|
|
|
|
|
<span>截图</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 计时器 (核心功能 - 点击重置) -->
|
|
|
|
|
<div class="tool-item" @click="resetTimer">
|
|
|
|
|
<i class="el-icon-timer"></i>
|
|
|
|
|
<span>计时器</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 设置 -->
|
|
|
|
|
<div class="tool-item">
|
|
|
|
|
<i class="el-icon-setting"></i>
|
|
|
|
|
<span>设置</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="right-actions">
|
|
|
|
|
<el-button
|
|
|
|
|
type="danger"
|
|
|
|
|
icon="el-icon-switch-button"
|
|
|
|
|
@click="endMeeting"
|
|
|
|
|
>结束会议</el-button
|
|
|
|
|
>
|
|
|
|
|
<span class="clock">{{ currentTime }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</footer>
|
|
|
|
|
|
|
|
|
|
<!-- 邀请弹窗 -->
|
|
|
|
|
<el-dialog
|
|
|
|
|
title="通讯录邀请"
|
|
|
|
|
:visible.sync="inviteDialogVisible"
|
|
|
|
|
width="30%"
|
|
|
|
|
>
|
|
|
|
|
<p>这里是通讯录列表,勾选用户进行邀请。</p>
|
|
|
|
|
<span slot="footer" class="dialog-footer">
|
|
|
|
|
<el-button @click="inviteDialogVisible = false">取 消</el-button>
|
|
|
|
|
<el-button type="primary" @click="confirmInvite">确 定</el-button>
|
|
|
|
|
</span>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
export default {
|
|
|
|
|
name: "MeetingRoom",
|
|
|
|
|
data() {
|
|
|
|
|
return {
|
|
|
|
|
currentTime: "",
|
|
|
|
|
timer: 0,
|
|
|
|
|
timerInterval: null,
|
|
|
|
|
isRecording: false,
|
|
|
|
|
recordedChunks: [],
|
|
|
|
|
mediaRecorder: null,
|
|
|
|
|
inviteDialogVisible: false,
|
|
|
|
|
micOn: true,
|
|
|
|
|
camOn: true,
|
|
|
|
|
isDrawingMode: false,
|
|
|
|
|
drawing: false,
|
|
|
|
|
lastX: 0,
|
|
|
|
|
lastY: 0,
|
|
|
|
|
ctx: null,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
computed: {
|
|
|
|
|
timerDisplay() {
|
|
|
|
|
const h = Math.floor(this.timer / 3600)
|
|
|
|
|
.toString()
|
|
|
|
|
.padStart(2, "0");
|
|
|
|
|
const m = Math.floor((this.timer % 3600) / 60)
|
|
|
|
|
.toString()
|
|
|
|
|
.padStart(2, "0");
|
|
|
|
|
const s = (this.timer % 60).toString().padStart(2, "0");
|
|
|
|
|
return `${h}:${m}:${s}`;
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
mounted() {
|
|
|
|
|
this.$store.dispatch("app/toggleSideBarHide", true);
|
|
|
|
|
this.$store.dispatch("settings/changeSetting", {
|
|
|
|
|
key: "fixedHeader",
|
|
|
|
|
value: false,
|
|
|
|
|
});
|
|
|
|
|
this.$store.dispatch("settings/changeSetting", {
|
|
|
|
|
key: "tagsView",
|
|
|
|
|
value: false,
|
|
|
|
|
});
|
|
|
|
|
this.updateClock();
|
|
|
|
|
setInterval(this.updateClock, 1000);
|
|
|
|
|
this.startTimer();
|
|
|
|
|
|
|
|
|
|
// 初始化 Canvas
|
|
|
|
|
this.initCanvas();
|
|
|
|
|
window.addEventListener("resize", this.initCanvas);
|
|
|
|
|
// 初始化SDK, 用来设置全局配置项,
|
|
|
|
|
// 一般的调用一次即可。也可以多次调用,每次调用都回立即更新配置项
|
|
|
|
|
// 初始化后才能进行进房行为
|
|
|
|
|
window.hirtcwebsdk.init({
|
|
|
|
|
// 服务id
|
|
|
|
|
serviceID: "90f4d5a954ab449e9b6aac92",
|
|
|
|
|
// 服务key
|
|
|
|
|
serviceKey: "2c17c6393771ee3048ae34d6b380c5ec",
|
|
|
|
|
// 摄像头视频分层配置
|
|
|
|
|
cameraLayers: [
|
|
|
|
|
{
|
|
|
|
|
width: 320,
|
|
|
|
|
height: 180,
|
|
|
|
|
// 帧率
|
|
|
|
|
frameRate: 20,
|
|
|
|
|
targetBw: 160000,
|
|
|
|
|
layerIndex: 0,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
width: 640,
|
|
|
|
|
height: 360,
|
|
|
|
|
frameRate: 20,
|
|
|
|
|
targetBw: 500000,
|
|
|
|
|
layerIndex: 1,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
width: 1280,
|
|
|
|
|
height: 720,
|
|
|
|
|
frameRate: 20,
|
|
|
|
|
targetBw: 1100000,
|
|
|
|
|
layerIndex: 2,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
leaveBeforeUnload: false,
|
|
|
|
|
debug: false,
|
|
|
|
|
forceArea: true,
|
|
|
|
|
screenEnableAudio: true,
|
|
|
|
|
// Services: {
|
|
|
|
|
// BasicRoomServiceToken: "https://121.36.105.19:7080/v1/auth/token",
|
|
|
|
|
// },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const roomId = "room_" + (this.meetingCode || "default");
|
|
|
|
|
const uid = String(this.$store.state.user.id || Date.now());
|
|
|
|
|
const uName = (this.$store.state.user.nickName || "用户").trim();
|
|
|
|
|
|
|
|
|
|
window.hirtcwebsdk.join(roomId, uid, uName);
|
|
|
|
|
window.hirtcwebsdk.addListener("joined", () => {
|
|
|
|
|
this.$message.success("加入会议成功");
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
methods: {
|
|
|
|
|
minimizeWindow() {
|
|
|
|
|
// 返回父页面(视频通信视讯)
|
|
|
|
|
this.$router.push("/video/index");
|
|
|
|
|
},
|
|
|
|
|
closeWindow() {
|
|
|
|
|
this.$store.dispatch("app/toggleSideBarHide", false);
|
|
|
|
|
},
|
|
|
|
|
updateClock() {
|
|
|
|
|
const now = new Date();
|
|
|
|
|
this.currentTime = now.toLocaleTimeString("zh-CN", { hour12: false });
|
|
|
|
|
},
|
|
|
|
|
startTimer() {
|
|
|
|
|
this.timerInterval = setInterval(() => {
|
|
|
|
|
this.timer++;
|
|
|
|
|
}, 1000);
|
|
|
|
|
},
|
|
|
|
|
resetTimer() {
|
|
|
|
|
this.timer = 0;
|
|
|
|
|
this.$message.info("计时器已重置");
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// --- 功能:邀请 ---
|
|
|
|
|
handleInvite() {
|
|
|
|
|
this.inviteDialogVisible = true;
|
|
|
|
|
},
|
|
|
|
|
confirmInvite() {
|
|
|
|
|
this.$message.success("邀请已发送");
|
|
|
|
|
this.inviteDialogVisible = false;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// --- 功能:录制 ---
|
|
|
|
|
async toggleRecord() {
|
|
|
|
|
if (this.isRecording) {
|
|
|
|
|
this.stopRecording();
|
|
|
|
|
} else {
|
|
|
|
|
this.startRecording();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
async startRecording() {
|
|
|
|
|
try {
|
|
|
|
|
// 实际场景中,你需要获取视频流 (Stream)
|
|
|
|
|
// const stream = this.localStream 或 this.remoteStream
|
|
|
|
|
// 这里为了演示,我们假设获取屏幕流或者创建一个空流会报错,所以主要演示逻辑
|
|
|
|
|
const stream = await navigator.mediaDevices.getDisplayMedia({
|
|
|
|
|
video: true,
|
|
|
|
|
audio: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.mediaRecorder = new MediaRecorder(stream);
|
|
|
|
|
this.recordedChunks = [];
|
|
|
|
|
|
|
|
|
|
this.mediaRecorder.ondataavailable = (event) => {
|
|
|
|
|
if (event.data.size > 0) {
|
|
|
|
|
this.recordedChunks.push(event.data);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.mediaRecorder.onstop = () => {
|
|
|
|
|
const blob = new Blob(this.recordedChunks, { type: "video/webm" });
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
const a = document.createElement("a");
|
|
|
|
|
a.style.display = "none";
|
|
|
|
|
a.href = url;
|
|
|
|
|
a.download = `meeting-record-${Date.now()}.webm`;
|
|
|
|
|
document.body.appendChild(a);
|
|
|
|
|
a.click();
|
|
|
|
|
window.URL.revokeObjectURL(url);
|
|
|
|
|
this.$message.success("视频已保存到本地");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.mediaRecorder.start();
|
|
|
|
|
this.isRecording = true;
|
|
|
|
|
this.$message.warning("会议录制已开始");
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("录制错误:", err);
|
|
|
|
|
this.$message.error("无法启动录制: " + err.message);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
stopRecording() {
|
|
|
|
|
if (this.mediaRecorder && this.isRecording) {
|
|
|
|
|
this.mediaRecorder.stop();
|
|
|
|
|
this.isRecording = false;
|
|
|
|
|
// 停止所有轨道以释放摄像头/屏幕占用
|
|
|
|
|
this.mediaRecorder.stream.getTracks().forEach((track) => track.stop());
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// --- 功能:画笔 ---
|
|
|
|
|
initCanvas() {
|
|
|
|
|
const canvas = this.$refs.drawingCanvas;
|
|
|
|
|
const container = this.$refs.videoContainer;
|
|
|
|
|
canvas.width = container.clientWidth;
|
|
|
|
|
canvas.height = container.clientHeight;
|
|
|
|
|
this.ctx = canvas.getContext("2d");
|
|
|
|
|
this.ctx.lineWidth = 3;
|
|
|
|
|
this.ctx.lineCap = "round";
|
|
|
|
|
this.ctx.strokeStyle = "#FF0000"; // 红色画笔
|
|
|
|
|
},
|
|
|
|
|
toggleDraw() {
|
|
|
|
|
this.isDrawingMode = !this.isDrawingMode;
|
|
|
|
|
if (!this.isDrawingMode) {
|
|
|
|
|
this.clearCanvas(); // 退出时清空,或者保留看需求
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
clearCanvas() {
|
|
|
|
|
const canvas = this.$refs.drawingCanvas;
|
|
|
|
|
this.ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
|
},
|
|
|
|
|
getCoordinates(e) {
|
|
|
|
|
const canvas = this.$refs.drawingCanvas;
|
|
|
|
|
const rect = canvas.getBoundingClientRect();
|
|
|
|
|
return {
|
|
|
|
|
x: e.clientX - rect.left,
|
|
|
|
|
y: e.clientY - rect.top,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
startDrawing(e) {
|
|
|
|
|
this.drawing = true;
|
|
|
|
|
const coords = this.getCoordinates(e);
|
|
|
|
|
this.lastX = coords.x;
|
|
|
|
|
this.lastY = coords.y;
|
|
|
|
|
},
|
|
|
|
|
draw(e) {
|
|
|
|
|
if (!this.drawing) return;
|
|
|
|
|
const coords = this.getCoordinates(e);
|
|
|
|
|
|
|
|
|
|
this.ctx.beginPath();
|
|
|
|
|
this.ctx.moveTo(this.lastX, this.lastY);
|
|
|
|
|
this.ctx.lineTo(coords.x, coords.y);
|
|
|
|
|
this.ctx.stroke();
|
|
|
|
|
|
|
|
|
|
this.lastX = coords.x;
|
|
|
|
|
this.lastY = coords.y;
|
|
|
|
|
},
|
|
|
|
|
stopDrawing() {
|
|
|
|
|
this.drawing = false;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// --- 其他功能 ---
|
|
|
|
|
takeScreenshot() {
|
|
|
|
|
this.$message.success("截图已保存");
|
|
|
|
|
// 实际截图逻辑需要使用 html2canvas 或 canvas.drawImage
|
|
|
|
|
},
|
|
|
|
|
toggleMic() {
|
|
|
|
|
this.micOn = !this.micOn;
|
|
|
|
|
},
|
|
|
|
|
toggleCam() {
|
|
|
|
|
this.camOn = !this.camOn;
|
|
|
|
|
},
|
|
|
|
|
endMeeting() {
|
|
|
|
|
this.$confirm("确定要结束会议吗?", "提示", { type: "warning" }).then(
|
|
|
|
|
() => {
|
|
|
|
|
this.$message.success("会议已结束");
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
beforeDestroy() {
|
|
|
|
|
clearInterval(this.timerInterval);
|
|
|
|
|
if (this.isRecording) this.stopRecording();
|
|
|
|
|
window.removeEventListener("resize", this.initCanvas);
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
/* 全局重置 */
|
|
|
|
|
* {
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 0;
|
|
|
|
|
}
|
|
|
|
|
body,
|
|
|
|
|
html {
|
|
|
|
|
height: 100%;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
|
|
|
|
|
"Microsoft YaHei", "微软雅黑", Arial, sans-serif;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.meeting-container {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
height: 100vh;
|
|
|
|
|
background-color: #004d40; /* 深绿色背景,类似截图 */
|
|
|
|
|
color: #fff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 顶部 */
|
|
|
|
|
.top-bar {
|
|
|
|
|
height: 40px;
|
|
|
|
|
background-color: #00695c;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 0 15px;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
}
|
|
|
|
|
.top-bar .logo {
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
}
|
|
|
|
|
.top-bar .title {
|
|
|
|
|
position: absolute;
|
|
|
|
|
left: 50%;
|
|
|
|
|
transform: translateX(-50%);
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
}
|
|
|
|
|
.window-controls i {
|
|
|
|
|
margin-left: 10px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 状态栏 */
|
|
|
|
|
.status-bar {
|
|
|
|
|
height: 30px;
|
|
|
|
|
background-color: #004d40;
|
|
|
|
|
border-bottom: 1px solid #00796b;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 0 15px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #b2dfdb;
|
|
|
|
|
}
|
|
|
|
|
.divider {
|
|
|
|
|
margin: 0 8px;
|
|
|
|
|
color: #00796b;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 主体内容 */
|
|
|
|
|
.main-content {
|
|
|
|
|
flex: 1;
|
|
|
|
|
display: flex;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.video-stage {
|
|
|
|
|
flex: 1;
|
|
|
|
|
background-color: #000;
|
|
|
|
|
position: relative;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
.placeholder-text {
|
|
|
|
|
color: #333;
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
z-index: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Canvas 覆盖层 */
|
|
|
|
|
.drawing-canvas {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
cursor: crosshair;
|
|
|
|
|
z-index: 10; /* 确保在视频上方 */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sidebar {
|
|
|
|
|
width: 300px;
|
|
|
|
|
background-color: #004d40;
|
|
|
|
|
border-left: 1px solid #00796b;
|
|
|
|
|
padding: 10px;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
.remote-user {
|
|
|
|
|
width: 100%;
|
|
|
|
|
background: #000;
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
.video-box img {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: auto;
|
|
|
|
|
display: block;
|
|
|
|
|
}
|
|
|
|
|
.user-info {
|
|
|
|
|
position: absolute;
|
|
|
|
|
bottom: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
width: 100%;
|
|
|
|
|
background: rgba(0, 0, 0, 0.5);
|
|
|
|
|
padding: 5px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 底部工具栏 */
|
|
|
|
|
.bottom-bar {
|
|
|
|
|
height: 80px;
|
|
|
|
|
background-color: #004d40;
|
|
|
|
|
border-top: 1px solid #00796b;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 0 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-create {
|
|
|
|
|
margin-right: 30px;
|
|
|
|
|
height: 60px;
|
|
|
|
|
background-color: #00acc1;
|
|
|
|
|
border: none;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tools {
|
|
|
|
|
flex: 1;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
gap: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tool-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
color: #fff;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
width: 60px;
|
|
|
|
|
}
|
|
|
|
|
.tool-item i {
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
margin-bottom: 5px;
|
|
|
|
|
}
|
|
|
|
|
.tool-item:hover {
|
|
|
|
|
color: #64ffda;
|
|
|
|
|
}
|
|
|
|
|
.tool-item.active {
|
|
|
|
|
color: #ffeb3b;
|
|
|
|
|
} /* 画笔激活状态 */
|
|
|
|
|
.is-recording {
|
|
|
|
|
color: red;
|
|
|
|
|
animation: blink 1s infinite;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes blink {
|
|
|
|
|
50% {
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.right-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: flex-end;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
height: 60px;
|
|
|
|
|
}
|
|
|
|
|
.clock {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
margin-top: 5px;
|
|
|
|
|
color: #b2dfdb;
|
|
|
|
|
}
|
|
|
|
|
</style>
|