海信医疗-远程超声管理平台-信创国产化
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

1028 lines
30 KiB

<template>
<div class="meeting-container">
<!-- 顶部导航栏 -->
<header class="top-bar">
<div class="logo">
<img src="@/assets/logo/logo.png" />
<span>信联</span>
</div>
<div class="title">{{ meetingTitle }}</div>
<div class="window-controls">
<i class="el-icon-minus" @click="minimizeWindow"></i>
<i class="el-icon-close" @click="closeWindow"></i>
</div>
</header>
<!-- 顶部状态信息条 -->
<div class="status-bar">
<span class="status-item">直播间口令:{{ meetingCode }}</span>
<span class="divider">|</span>
<span class="status-item">{{ participantCount }}人观看</span>
<span class="divider">|</span>
<span class="status-item">{{ timerDisplay }}</span>
<span class="divider">|</span>
<span class="status-item" :style="{ color: networkQuality.color }">
<i class="el-icon-caret-top" style="transform: rotate(-90deg); margin-right: 4px;"></i>
</span>
</div>
<!-- 主画面区域左右双分屏 -->
<main class="main-content">
<!-- 左侧画面 -->
<div class="video-stage left-stage" ref="videoContainerLeft">
<!-- 采集卡/主视频画面 -->
<video
ref="shareVideo"
class="share-preview"
autoplay
playsinline
muted
v-if="isSharing"
></video>
</div>
<!-- 右侧画面 -->
<div class="video-stage right-stage" ref="videoContainerRight">
<!-- 本地摄像头画面 -->
<video
ref="localVideo"
class="local-camera"
autoplay
muted
playsinline
v-if="camOn"
/>
</div>
</main>
<!-- 底部工具栏 -->
<footer class="bottom-bar">
<div class="tools-section">
<div class="tool-item" @click="toggleMic">
<div class="icon-wrapper">
<i class="el-icon-microphone"></i>
<span v-if="!micOn" class="slash-line"></span>
</div>
<span>麦克风</span>
</div>
<div class="tool-item" @click="toggleSpeaker">
<div class="icon-wrapper">
<i class="el-icon-phone-outline"></i>
<span v-if="!speakerOn" class="slash-line"></span>
</div>
<span>扬声器</span>
</div>
<div class="tool-item" @click="toggleRecord">
<div class="icon-wrapper">
<i class="el-icon-video-play" :class="{ 'is-recording': isRecording }"></i>
</div>
<span>{{ isRecording ? "录制中" : "录制" }}</span>
</div>
<div class="tool-item" @click="handleMessage">
<div class="icon-wrapper">
<i class="el-icon-chat-line-round"></i>
</div>
<span>消息</span>
</div>
<div class="tool-item" @click="toggleShare">
<div class="icon-wrapper">
<i class="el-icon-share"></i>
</div>
<span>共享</span>
</div>
<div class="tool-item" @click="handleInvite">
<div class="icon-wrapper">
<i class="el-icon-user-solid"></i>
</div>
<span>邀请</span>
</div>
<div class="tool-item" @click="openSettingDialog">
<div class="icon-wrapper">
<i class="el-icon-setting"></i>
</div>
<span>设置</span>
</div>
<div class="divider-line"></div>
<div class="tool-item end-btn" @click="endMeeting">
<div class="icon-wrapper">
<i class="el-icon-switch-button"></i>
</div>
<span>结束会议</span>
</div>
</div>
</footer>
<!-- 邀请弹窗 -->
<CreateGroupDialog
:visible.sync="inviteDialogVisible"
title="通讯录邀请"
@confirm="handleInviteConfirm"
@close="inviteDialogVisible = false"
/>
<!-- 设置弹窗 -->
<el-dialog
title="设置"
:visible.sync="settingDialogVisible"
width="31%"
@open="onSettingDialogOpen"
:show-close="false"
>
<el-tabs v-model="settingTab" tab-position="left">
<el-tab-pane label="通用" name="general">
<el-form :model="tempSetting" label-width="140px">
<el-form-item label="会诊名称">
<el-input
v-model="tempSetting.meetingTitle"
style="width: 240px"
></el-input>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="视频" name="video">
<el-form :model="tempSetting" label-width="140px">
<el-form-item label="摄像头/工作站">
<el-select v-model="tempSetting.cameraId" style="width: 100%">
<el-option
v-for="item in videoDeviceList"
:key="item.deviceId"
:label="item.label"
:value="item.deviceId"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="绿联采集卡">
<el-select
v-model="tempSetting.captureCard"
style="width: 100%"
@change="switchCaptureCard"
>
<el-option label="不使用采集卡" value=""></el-option>
<el-option
v-for="item in videoDeviceList"
:key="item.deviceId"
:label="item.label"
:value="item.deviceId"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="网络调控策略">
<el-select v-model="tempSetting.netStrategy" style="width: 100%">
<el-option label="弱网流畅优先" value="smooth"></el-option>
<el-option label="弱网清晰优先" value="quality"></el-option>
</el-select>
</el-form-item>
<el-form-item label="摄像头分辨率">
<el-select v-model="tempSetting.resolution" style="width: 100%">
<el-option label="240P" value="240P"></el-option>
<el-option label="480P" value="480P"></el-option>
<el-option label="720P" value="720P"></el-option>
<el-option label="1080P" value="1080P"></el-option>
</el-select>
</el-form-item>
<el-form-item label="采集卡分辨率">
<el-select v-model="tempSetting.captureRes" style="width: 100%">
<el-option label="1080P" value="1080P"></el-option>
<el-option label="720P" value="720P"></el-option>
<el-option label="480P" value="480P"></el-option>
</el-select>
</el-form-item>
<el-form-item label="采集卡帧率">
<el-select v-model="tempSetting.captureFps" style="width: 100%">
<el-option label="30" value="30"></el-option>
<el-option label="25" value="25"></el-option>
<el-option label="15" value="15"></el-option>
</el-select>
</el-form-item>
<el-form-item label="采集卡码率">
<el-input v-model="tempSetting.captureBitrate"></el-input>
</el-form-item>
<el-form-item label="视频镜像">
<el-radio-group v-model="tempSetting.videoMirror">
<el-radio :label="true">开启</el-radio>
<el-radio :label="false">关闭</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="音频" name="audio">
<el-form :model="tempSetting" label-width="140px">
<el-form-item label="麦克风">
<el-select v-model="tempSetting.micId" style="width: 100%">
<el-option
v-for="item in audioInList"
:key="item.deviceId"
:label="item.label"
:value="item.deviceId"
/>
</el-select>
</el-form-item>
<el-form-item label="扬声器">
<el-select v-model="tempSetting.speakerId" style="width: 100%">
<el-option
v-for="item in audioOutList"
:key="item.deviceId"
:label="item.label"
:value="item.deviceId"
/>
</el-select>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="标注" name="draw">
<el-form :model="tempSetting" label-width="140px">
<el-form-item label="画笔颜色">
<el-color-picker v-model="tempSetting.brushColor" />
</el-form-item>
<el-form-item label="">
<el-checkbox v-model="tempSetting.syncColor"
>同步标记颜色</el-checkbox
>
</el-form-item>
<el-form-item label="画笔类型">
<el-radio-group v-model="tempSetting.brushType">
<el-radio label="dashed">虚线</el-radio>
<el-radio label="arrow">箭头</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="快捷键" name="shortcut">
<el-form :model="tempSetting" label-width="140px">
<el-form-item label="截图快捷键">
<el-input v-model="tempSetting.screenshotKey"></el-input>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
<template slot="footer">
<el-button @click="settingDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveSetting">保存并应用</el-button>
</template>
</el-dialog>
</div>
</template>
<script>
import CreateGroupDialog from "@/views/message/components/CreateGroupDialog";
export default {
name: "MeetingRoom",
components: {
CreateGroupDialog,
},
data() {
return {
// ==================== 会议基础信息 ====================
meetingTitle: "超声事业部的云教学",
meetingCode: "9019100",
participantCount: 1,
resolution: "1080P",
frameRate: 25,
networkDelay: 0,
networkQuality: { text: "良好", color: "#00e676" },
// ==================== 计时器 ====================
timer: 0,
timerInterval: null,
timerRunning: false,
// ==================== 录制相关 ====================
isRecording: false,
recordedChunks: [],
mediaRecorder: null,
recordingStartTime: null,
recordingTime: "00:00:00",
recordingTimer: null,
// ==================== 音视频设备 ====================
inviteDialogVisible: false,
settingDialogVisible: false,
settingTab: "general",
micOn: true, // 麦克风(默认开启)
speakerOn: true, // 扬声器(默认开启,非静音)
camOn: true, // 摄像头(默认开启)
localStream: null,
// ==================== 屏幕共享 / 采集卡 ====================
isSharing: true,
shareStream: null,
captureCheckTimer: null,
// ==================== 设置模块 ====================
videoDeviceList: [],
audioInList: [],
audioOutList: [],
tempSetting: {},
globalSetting: {
meetingTitle: "超声事业部的云教学",
cameraId: "",
micId: "",
speakerId: "",
resolution: "480P",
netStrategy: "smooth",
brushColor: "#FF69B4",
brushWidth: 3,
screenshotKey: "Ctrl+1",
captureCard: "",
captureRes: "1080P",
captureFps: "30",
captureBitrate: "",
videoMirror: false,
brushType: "dashed",
syncColor: true,
},
};
},
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.initPage();
this.initSDK();
},
methods: {
// ==================== 【1】初始化 ====================
initPage() {
this.loadLocalSetting();
this.startTimer();
this.openCameraAndMic(); // 进入页面自动打开摄像头和麦克风
this.fetchMeetingInfo();
this.startNetworkMonitoring();
// 隐藏侧边栏、调整布局
this.$store.dispatch("app/toggleSideBarHide", true);
this.$store.dispatch("settings/changeSetting", {
key: "fixedHeader",
value: false,
});
this.$store.dispatch("settings/changeSetting", {
key: "tagsView",
value: false,
});
},
// SDK 初始化加入房间
initSDK() {
window.hirtcwebsdk.init({
serviceID: "90f4d5a954ab449e9b6ac92",
serviceKey: "2c17c6393771ee3048ae30d6b380c5ec",
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,
});
const roomId = "room_" + (this.meetingCode || "default");
const uid = String(this.$store.state.user.id || Date.now());
const uName = (this.$store.state.user.name || "用户").trim();
window.hirtcwebsdk.join(roomId, uid, uName);
window.hirtcwebsdk.addListener("joined", () => {
this.$message.success("加入会议成功");
this.updateParticipantCount();
});
window.hirtcwebsdk.addListener(
"user-joined",
this.updateParticipantCount
);
window.hirtcwebsdk.addListener("user-left", this.updateParticipantCount);
},
// ==================== 【2】设备:摄像头 + 麦克风 ====================
// 打开摄像头+麦克风并渲染(进入页面自动执行)
async openCameraAndMic() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
this.localStream = stream;
this.$refs.localVideo.srcObject = stream;
// 默认扬声器开启(不静音)
this.$refs.localVideo.muted = !this.speakerOn;
} catch (err) {
console.error("打开设备失败", err);
this.$message.error("请允许摄像头/麦克风权限");
}
},
// 麦克风开关(控制上传的声音)
toggleMic() {
if (!this.localStream) return;
this.micOn = !this.micOn;
this.localStream
.getAudioTracks()
.forEach((t) => (t.enabled = this.micOn));
this.$message[this.micOn ? "success" : "warning"](
this.micOn ? "麦克风已打开" : "麦克风已关闭"
);
},
// 扬声器开关(控制本地播放的声音,静音/取消静音)
toggleSpeaker() {
this.speakerOn = !this.speakerOn;
// 控制所有视频元素的静音状态
if (this.$refs.localVideo) {
this.$refs.localVideo.muted = !this.speakerOn;
}
if (this.$refs.shareVideo) {
this.$refs.shareVideo.muted = !this.speakerOn;
}
this.$message[this.speakerOn ? "success" : "warning"](
this.speakerOn ? "扬声器已打开" : "扬声器已静音"
);
},
// 摄像头开关(控制上传的视频画面)
toggleCam() {
if (!this.localStream) return;
this.camOn = !this.camOn;
this.localStream
.getVideoTracks()
.forEach((t) => (t.enabled = this.camOn));
this.$message[this.camOn ? "success" : "warning"](
this.camOn ? "摄像头已打开" : "摄像头已关闭"
);
},
// ==================== 【3】采集卡设备 ====================
async switchCaptureCard(deviceId) {
// 清理旧采集卡流
if (this.captureCheckTimer) clearTimeout(this.captureCheckTimer);
this.closeCaptureCard();
if (!deviceId) {
this.isSharing = false;
this.$message.success("已关闭采集卡");
return;
}
this.isSharing = true;
await this.$nextTick();
const constraints = {
video: {
deviceId: { exact: deviceId },
width: { ideal: 1920 },
height: { ideal: 1080 },
frameRate: { ideal: 30 },
},
audio: false,
};
let captureStream = null;
try {
captureStream = await navigator.mediaDevices.getUserMedia(constraints);
} catch (err) {
console.error("采集卡获取流失败:", err);
const lowConstraints = {
video: { deviceId: { exact: deviceId } },
audio: false,
};
try {
captureStream = await navigator.mediaDevices.getUserMedia(
lowConstraints
);
} catch (e2) {
this.$message.error(
"无法打开采集卡,请检查USB口、权限、是否被其他软件占用"
);
this.isSharing = false;
return;
}
}
const videoEl = this.$refs.shareVideo;
videoEl.srcObject = captureStream;
videoEl.muted = true;
videoEl.playsinline = true;
videoEl.autoplay = true;
let hasValidFrame = false;
videoEl.onloadeddata = () => (hasValidFrame = true);
// 3秒超时无信号判定失败
this.captureCheckTimer = setTimeout(() => {
if (!hasValidFrame) {
this.$message.error("采集卡无信号,请检查设备、HDMI线、USB3.0接口");
this.closeCaptureCard();
this.isSharing = false;
}
}, 3000);
// 监听采集卡断开
const videoTrack = captureStream.getVideoTracks()[0];
if (videoTrack)
videoTrack.onended = () => {
this.$message.warning("采集卡信号已断开");
this.closeCaptureCard();
this.isSharing = false;
};
},
// 关闭采集卡并释放资源
closeCaptureCard() {
try {
if (this.captureCheckTimer) clearTimeout(this.captureCheckTimer);
const videoEl = this.$refs.shareVideo;
if (!videoEl) return;
const stream = videoEl.srcObject;
if (stream)
stream.getTracks().forEach((t) => {
t.stop();
t.onended = null;
});
videoEl.srcObject = null;
videoEl.onloadeddata = null;
} catch (e) {
console.log("关闭采集卡异常:", e);
}
},
// ==================== 【4】屏幕共享 ====================
async toggleShare() {
if (this.isSharing) {
this.stopShare();
return;
}
try {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: { width: 1920, height: 1080, frameRate: 15 },
audio: false,
});
const track = stream.getVideoTracks()[0];
track.onended = () => this.stopShare();
this.shareStream = stream;
this.$refs.shareVideo.srcObject = stream;
this.isSharing = true;
this.$message.success("开始共享");
window.hirtcwebsdk?.publishScreen(stream);
} catch (e) {
this.$message.error("共享失败");
}
},
// 停止共享
stopShare() {
if (this.shareStream)
this.shareStream.getTracks().forEach((t) => {
t.stop();
t.onended = null;
});
this.shareStream = null;
this.isSharing = false;
this.$refs.shareVideo.srcObject = null;
this.$message.info("已停止共享");
window.hirtcwebsdk?.unPublishScreen();
},
// ==================== 【5】录制 ====================
async toggleRecord() {
this.isRecording ? this.stopRecording() : await this.startRecording();
},
// 开始录制
async startRecording() {
try {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: true,
});
this.mediaRecorder = new MediaRecorder(stream, {
mimeType: "video/webm;codecs=vp8,opus",
});
this.recordedChunks = [];
this.mediaRecorder.ondataavailable = (e) =>
e.data.size && this.recordedChunks.push(e.data);
this.mediaRecorder.onstop = () => {
const blob = new Blob(this.recordedChunks, { type: "video/webm" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${
this.meetingTitle
}录制_${new Date().toLocaleString()}.webm`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
this.$message.success("录制完成");
};
this.mediaRecorder.start();
this.isRecording = true;
this.recordingStartTime = new Date();
this.startRecordingTimer();
this.$message.warning("录制中");
} catch (e) {
this.$message.error("录制失败");
}
},
// 停止录制
stopRecording() {
if (this.mediaRecorder && this.isRecording) {
this.mediaRecorder.stop();
this.mediaRecorder.stream.getTracks().forEach((t) => t.stop());
this.isRecording = false;
this.stopRecordingTimer();
}
},
// 录制计时
startRecordingTimer() {
this.recordingTimer = setInterval(() => {
const sec = Math.floor((new Date() - this.recordingStartTime) / 1000);
const h = String(Math.floor(sec / 3600)).padStart(2, "0");
const m = String(Math.floor((sec % 3600) / 60)).padStart(2, "0");
const s = String(sec % 60).padStart(2, "0");
this.recordingTime = `${h}:${m}:${s}`;
}, 1000);
},
// 停止录制计时
stopRecordingTimer() {
clearInterval(this.recordingTimer);
this.recordingTime = "00:00:00";
},
// ==================== 【6】计时器 ====================
toggleTimer() {
this.timerRunning ? this.pauseTimer() : this.startTimer();
},
startTimer() {
if (this.timerInterval) return;
this.timerRunning = true;
this.timerInterval = setInterval(() => this.timer++, 1000);
},
pauseTimer() {
clearInterval(this.timerInterval);
this.timerInterval = null;
this.timerRunning = false;
},
// ==================== 【7】网络状态监控 ====================
startNetworkMonitoring() {
setInterval(() => {
if (!window.hirtcwebsdk) return;
const s = window.hirtcwebsdk.getNetworkStats();
if (s?.rtt) {
this.networkDelay = Math.round(s.rtt);
if (this.networkDelay < 100)
this.networkQuality = { text: "优秀", color: "#00e676" };
else if (this.networkDelay < 300)
this.networkQuality = { text: "良好", color: "#00e676" };
else if (this.networkDelay < 600)
this.networkQuality = { text: "一般", color: "#ffc107" };
else this.networkQuality = { text: "差", color: "#f56c6c" };
}
}, 2000);
},
// ==================== 【8】设置模块 ====================
openSettingDialog() {
this.settingDialogVisible = true;
},
async onSettingDialogOpen() {
this.tempSetting = { ...this.globalSetting };
await this.enumerateDevices();
},
// 枚举音视频设备
async enumerateDevices() {
try {
await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
const devices = await navigator.mediaDevices.enumerateDevices();
this.videoDeviceList = devices.filter((d) => d.kind === "videoinput");
this.audioInList = devices.filter((d) => d.kind === "audioinput");
this.audioOutList = devices.filter((d) => d.kind === "audiooutput");
} catch (e) {
this.$message.warning("请允许音视频权限以查看设备");
}
},
// 保存设置
saveSetting() {
this.globalSetting = { ...this.tempSetting };
this.meetingTitle = this.globalSetting.meetingTitle;
this.resolution = this.globalSetting.resolution;
localStorage.setItem(
"meetingGlobalSetting",
JSON.stringify(this.globalSetting)
);
this.settingDialogVisible = false;
this.$message.success("设置已保存");
},
// 加载本地缓存设置
loadLocalSetting() {
const cache = localStorage.getItem("meetingGlobalSetting");
if (cache) {
this.globalSetting = JSON.parse(cache);
this.meetingTitle = this.globalSetting.meetingTitle;
this.resolution = this.globalSetting.resolution;
}
},
// ==================== 【9】会议成员、邀请、消息 ====================
handleInvite() {
this.inviteDialogVisible = true;
},
handleInviteConfirm(selectedContacts) {
console.log('邀请的联系人:', selectedContacts);
this.$message.success(`已邀请 ${selectedContacts.length} 位联系人`);
this.inviteDialogVisible = false;
},
handleMessage() {
this.$message.info("消息功能待开发");
},
fetchMeetingInfo() {
const q = this.$route.query;
if (q.title) this.meetingTitle = q.title;
if (q.code) this.meetingCode = q.code;
if (q.resolution) this.resolution = q.resolution;
if (q.frameRate) this.frameRate = q.frameRate;
},
updateParticipantCount() {
if (window.hirtcwebsdk) {
const us = window.hirtcwebsdk.getUsers();
this.participantCount = us ? us.length : 1;
}
},
// ==================== 【10】窗口与会议控制 ====================
minimizeWindow() {
this.$router.push("/video/index");
},
closeWindow() {
this.$confirm("确定退出会诊?", "提示", { type: "warning" }).then(() => {
this.$store.dispatch("app/toggleSideBarHide", false);
this.$store.dispatch("settings/changeSetting", {
key: "fixedHeader",
value: true,
});
this.$store.dispatch("settings/changeSetting", {
key: "tagsView",
value: true,
});
this.$router.push("/");
});
},
endMeeting() {
this.$confirm("确定结束会议?", "提示", { type: "warning" }).then(() => {
this.stopShare();
this.localStream?.getTracks().forEach((t) => t.stop());
this.$store.dispatch("app/toggleSideBarHide", false);
this.$store.dispatch("settings/changeSetting", {
key: "fixedHeader",
value: true,
});
this.$store.dispatch("settings/changeSetting", {
key: "tagsView",
value: true,
});
window.name === "consultationWindow"
? window.close()
: this.$router.push("/");
});
},
},
beforeDestroy() {
// 页面销毁清理所有资源
clearInterval(this.timerInterval);
this.stopRecording();
this.stopShare();
this.closeCaptureCard();
this.localStream?.getTracks().forEach((t) => t.stop());
},
};
</script>
<style lang="scss">
.meeting-container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #004d40;
color: #ffffff;
}
.top-bar {
height: 40px;
background: linear-gradient(180deg, #00695c 0%, #004d40 100%);
display: flex;
align-items: center;
padding: 0 10px;
justify-content: space-between;
border-bottom: 1px solid #00796b;
.logo {
display: flex;
align-items: center;
font-weight: 600;
img {
width: 30px;
margin-right: 5px;
}
}
.title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 13px;
color: #b2dfdb;
}
.window-controls {
display: flex;
align-items: center;
i {
margin-left: 10px;
cursor: pointer;
font-size: 13px;
color: #b2dfdb;
}
}
}
.status-bar {
height: 40px;
background-color: #004d40;
display: flex;
align-items: center;
padding: 0 10px;
color: #b2dfdb;
.status-item {
font-family: Consolas, monospace;
}
.divider {
margin: 0 6px;
color: #00796b;
}
}
.main-content {
flex: 1;
display: flex;
overflow: hidden;
}
.video-stage {
flex: 1;
background-color: #000;
position: relative;
&.left-stage {
border-right: 2px solid #00796b;
}
.share-preview {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: contain;
z-index: 5;
}
.local-camera {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: contain;
z-index: 5;
}
}
.bottom-bar {
height: 90px;
background: linear-gradient(180deg, #004d40 0%, #00695c 100%);
border-top: 1px solid #00796b;
display: flex;
align-items: center;
justify-content: center;
padding: 0 12px;
.tools-section {
display: flex;
justify-content: center;
gap: 18px;
align-items: center;
.tool-item {
display: flex;
flex-direction: column;
align-items: center;
color: #fff;
cursor: pointer;
font-size: 11px;
width: 60px;
padding: 8px 4px;
border-radius: 4px;
position: relative;
transition: background-color 0.2s ease;
.icon-wrapper {
position: relative;
}
i {
font-size: 22px;
margin-bottom: 3px;
}
&:hover {
background-color: #00897b;
}
&.active {
background-color: #00897b;
}
.slash-line {
position: absolute;
top: 2px;
left: 2px;
width: 22px;
height: 22px;
border-top: 2px solid #f44336;
transform: rotate(45deg);
transform-origin: left top;
pointer-events: none;
}
}
.divider-line {
width: 1px;
height: 30px;
background-color: #00796b;
margin: 0 4px;
}
.end-btn {
color: #ff5252;
&:hover {
background-color: rgba(255, 82, 82, 0.2);
}
}
}
}
.is-recording {
color: #ef5350;
animation: blink 1s infinite;
}
@keyframes blink {
0% {
opacity: 1;
}
50% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
</style>