海信医疗-远程超声管理平台-信创国产化
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.

1128 lines
31 KiB

1 month ago
<template>
<div class="meeting-container">
1 month ago
<!-- 顶部导航栏 -->
<header class="top-bar">
<div class="logo">
<img src="@/assets/logo/logo.png" />
<span>信联</span>
1 month ago
</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>
1 month ago
</div>
</header>
<!-- 主画面区域2x2 四宫格 -->
<main class="main-content">
<div class="grid-container">
<div
v-for="(slot, index) in gridSlots"
:key="index"
class="grid-slot"
>
<!-- 有成员时的状态 -->
<template v-if="slot.user">
<!-- 左上角信号强度 -->
<div class="signal-icon">
<i
v-for="n in 5"
:key="n"
class="bar"
:class="{ active: n <= slot.signalLevel }"
></i>
1 month ago
</div>
<!-- 顶部用户名称 -->
<div class="slot-name">{{ slot.user.name }}</div>
<!-- 取消质控按钮 -->
<el-button
class="cancel-btn"
type="info"
size="mini"
@click="cancelQualityControl(index)"
>
取消质控
</el-button>
<!-- 视频区域 -->
<video
v-if="slot.stream"
:ref="`video${index}`"
class="slot-video"
autoplay
playsinline
></video>
<!-- 无视频流时显示用户信息 -->
<div v-else class="user-btn" @click="showUserDetail(slot.user)">
<el-avatar :size="56" :src="slot.user.avatar" class="user-avatar">
<span class="avatar-text">{{ slot.user.name.charAt(0) }}</span>
</el-avatar>
<div class="user-info-text">
<span class="user-name">{{ slot.user.name }}</span>
<span :class="['status-dot', slot.user.status === '在线' ? 'online' : 'offline']"></span>
</div>
</div>
</template>
1 month ago
<!-- 无成员时的添加按钮 -->
<div v-else class="add-btn" @click="openAddUserDialog(index)">
<i class="el-icon-plus"></i>
</div>
1 month ago
</div>
</div>
</main>
<!-- 邀请弹窗 -->
<CreateGroupDialog
:visible.sync="inviteDialogVisible"
title="通讯录邀请"
@confirm="handleInviteConfirm"
@close="inviteDialogVisible = false"
/>
1 month ago
<!-- 添加质控成员弹窗 -->
<CreateGroupDialog
title="邀请参加人员"
:visible.sync="showAddDialog"
:recent-contacts="recentContacts"
:recent-groups="recentGroups"
:min-select-count="1"
confirm-text="确定"
@create-success="handleAddMembers"
/>
<!-- 设置弹窗 -->
<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.captureCardId" style="width: 100%">
<el-option
v-for="item in captureCardList"
:key="item.deviceId"
:label="item.label"
:value="item.deviceId"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="网络策略">
<el-radio-group v-model="tempSetting.networkStrategy">
<el-radio label="smooth">流畅优先</el-radio>
<el-radio label="quality">画质优先</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="分辨率">
<el-select v-model="tempSetting.resolution" style="width: 100%">
<el-option label="720P" value="720P"></el-option>
<el-option label="1080P" value="1080P"></el-option>
<el-option label="2K" value="2K"></el-option>
</el-select>
</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 audioDeviceList"
: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.speakerId" style="width: 100%">
<el-option
v-for="item in speakerDeviceList"
:key="item.deviceId"
:label="item.label"
:value="item.deviceId"
></el-option>
</el-select>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="画笔" name="brush">
<el-form :model="tempSetting" label-width="140px">
<el-form-item label="画笔颜色">
<el-color-picker v-model="tempSetting.brushColor"></el-color-picker>
</el-form-item>
<el-form-item label="画笔粗细">
<el-slider
v-model="tempSetting.brushWidth"
:min="1"
:max="10"
></el-slider>
</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>
<!-- 创建病例弹窗 -->
<CaseFormDialog :visible.sync="showCaseDialog" :is-edit="false" />
1 month ago
</div>
</template>
<script>
import CreateGroupDialog from "@/views/message/components/CreateGroupDialog";
import CaseFormDialog from "@/views/cases/components/CaseFormDialog";
1 month ago
export default {
name: "qualityControl",
1 month ago
components: {
CreateGroupDialog,
CaseFormDialog,
1 month ago
},
data() {
return {
// ==================== 会议基础信息 ====================
meetingTitle: "超声事业部的云质控",
meetingCode: "1687110",
participantCount: 1,
resolution: "1080P",
frameRate: 25,
networkDelay: 0,
currentTime: "",
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,
showCaseDialog: false,
micOn: true,
camOn: true,
localStream: null,
// ==================== 音频检测(说话波形) ====================
audioContext: null,
analyser: null,
voiceLevels: Array(12).fill(1),
voiceMonitorTimer: null,
// ==================== 画笔 ====================
isDrawingMode: false,
drawing: false,
lastX: 0,
lastY: 0,
brushColor: "#FF69B4",
brushLineWidth: 3,
brushLineDash: [5, 5],
ctx: null,
// ==================== 激光笔 ====================
isLaserMode: false,
laserX: 0,
laserY: 0,
laserAnimationId: null,
laserAlpha: 1,
laserAlphaDir: 1,
laserCtx: null,
// ==================== 屏幕共享 ====================
isSharing: false,
shareStream: null,
// ==================== 设置弹窗 ====================
settingDialogVisible: false,
settingTab: "general",
tempSetting: {
meetingTitle: "",
cameraId: "",
captureCardId: "",
networkStrategy: "smooth",
resolution: "1080P",
micId: "",
speakerId: "",
brushColor: "#FF69B4",
brushWidth: 3,
},
globalSetting: {
meetingTitle: "超声事业部的云质控",
cameraId: "",
captureCardId: "",
networkStrategy: "smooth",
resolution: "1080P",
micId: "",
speakerId: "",
brushColor: "#FF69B4",
brushWidth: 3,
},
videoDeviceList: [],
audioDeviceList: [],
speakerDeviceList: [],
captureCardList: [],
// ==================== 分屏数据(4个位置) ====================
1 month ago
gridSlots: [
{
index: 0,
signalLevel: 5,
user: {
name: "基层医生",
status: "在线",
avatar: "https://img.icons8.com/fluency/96/doctor-male.png",
},
stream: null,
1 month ago
},
{
index: 1,
signalLevel: 0,
user: null,
stream: null,
1 month ago
},
{
index: 2,
signalLevel: 5,
user: {
name: "于春晓",
status: "离线",
avatar: "https://img.icons8.com/fluency/96/doctor-male.png",
},
stream: null,
1 month ago
},
{
index: 3,
signalLevel: 0,
user: null,
stream: null,
1 month ago
},
],
// ==================== 添加弹窗状态 ====================
1 month ago
showAddDialog: false,
currentAddIndex: null,
// ==================== 联系人数据 ====================
1 month ago
recentContacts: [
{ id: "1", name: "基层医生(在线)", avatar: "" },
{ id: "2", name: "于春晓", avatar: "" },
{ id: "3", name: "郭君", avatar: "" },
{ id: "4", name: "张医生", avatar: "" },
{ id: "5", name: "李护士", avatar: "" },
{ id: "6", name: "王主任", avatar: "" },
],
recentGroups: [
{
id: "group1",
name: "医疗专家组",
avatar: "",
expanded: false,
members: [
{ id: "m1", name: "张医生", avatar: "", selected: false },
{ id: "m2", name: "李护士", avatar: "", selected: false },
{ id: "m3", name: "王主任", avatar: "", selected: false },
],
},
{
id: "group2",
name: "超声科团队",
avatar: "",
expanded: false,
members: [
{ id: "m4", name: "陈医生", avatar: "", selected: false },
{ id: "m5", name: "刘医生", avatar: "", selected: false },
],
},
],
};
},
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}`;
},
},
async mounted() {
1 month ago
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.startNetworkMonitoring();
this.initCanvas();
window.addEventListener("resize", this.initCanvas);
await this.openCameraAndMic();
await this.getDevices();
1 month ago
},
beforeDestroy() {
clearInterval(this.timerInterval);
if (this.isRecording) this.stopRecording();
window.removeEventListener("resize", this.initCanvas);
if (this.localStream) this.localStream.getTracks().forEach((t) => t.stop());
if (this.shareStream) this.shareStream.getTracks().forEach((t) => t.stop());
if (this.mediaRecorder) this.mediaRecorder.stop();
},
1 month ago
methods: {
// ==================== 【1】窗口控制 ====================
minimizeWindow() {
this.$router.push("/video/index");
},
closeWindow() {
this.$confirm("确定要退出本次质控吗?", "提示", {
type: "warning",
confirmButtonText: "确定",
cancelButtonText: "取消",
})
.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("/");
})
.catch(() => {});
},
// ==================== 【2】音视频控制 ====================
async openCameraAndMic() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
this.localStream = stream;
if (this.$refs.localVideo) {
this.$refs.localVideo.srcObject = stream;
}
this.startVoiceMonitor();
} catch (err) {
console.error("获取摄像头/麦克风失败:", err);
this.$message.error("无法访问摄像头或麦克风");
}
},
async getDevices() {
try {
const devices = await navigator.mediaDevices.enumerateDevices();
this.videoDeviceList = devices.filter((d) => d.kind === "videoinput");
this.audioDeviceList = devices.filter((d) => d.kind === "audioinput");
this.speakerDeviceList = devices.filter((d) => d.kind === "audiooutput");
this.captureCardList = this.videoDeviceList.filter((d) =>
d.label.toLowerCase().includes("usb")
);
} catch (err) {
console.error("获取设备列表失败:", err);
}
},
toggleMic() {
if (!this.localStream) return;
this.micOn = !this.micOn;
this.localStream.getAudioTracks().forEach((t) => (t.enabled = this.micOn));
if (this.micOn) {
this.startVoiceMonitor();
} else {
this.stopVoiceMonitor();
}
},
toggleCam() {
if (!this.localStream) return;
this.camOn = !this.camOn;
this.localStream.getVideoTracks().forEach((t) => (t.enabled = this.camOn));
},
// ==================== 【3】音频检测(说话波形) ====================
startVoiceMonitor() {
if (!this.localStream || !this.micOn) return;
try {
this.audioContext = new (window.AudioContext ||
window.webkitAudioContext)();
this.analyser = this.audioContext.createAnalyser();
const source = this.audioContext.createMediaStreamSource(
this.localStream
);
source.connect(this.analyser);
this.analyser.fftSize = 64;
const dataArray = new Uint8Array(this.analyser.frequencyBinCount);
const updateVoice = () => {
if (!this.analyser) return;
this.analyser.getByteFrequencyData(dataArray);
const levels = [];
for (let i = 0; i < 12; i++) {
const value = dataArray[i] || 0;
levels.push(Math.max(2, Math.min(20, value / 10)));
}
this.voiceLevels = levels;
this.voiceMonitorTimer = requestAnimationFrame(updateVoice);
};
updateVoice();
} catch (e) {
console.error("音频检测初始化失败:", e);
}
},
stopVoiceMonitor() {
if (this.voiceMonitorTimer) {
cancelAnimationFrame(this.voiceMonitorTimer);
this.voiceMonitorTimer = null;
}
if (this.audioContext) {
this.audioContext.close();
this.audioContext = null;
}
this.voiceLevels = Array(12).fill(1);
},
// ==================== 【4】屏幕共享 ====================
async toggleShare() {
if (!this.isSharing) {
try {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: true,
});
this.shareStream = stream;
if (this.$refs.shareVideo) {
this.$refs.shareVideo.srcObject = stream;
}
this.isSharing = true;
this.$message.info("开始共享");
} catch (e) {
console.error("共享失败:", e);
this.$message.error("共享失败");
}
} else {
this.stopShare();
}
},
stopShare() {
if (this.shareStream) {
this.shareStream.getTracks().forEach((t) => t.stop());
this.shareStream = null;
}
this.isSharing = false;
},
// ==================== 【5】录制 ====================
async toggleRecord() {
if (!this.isRecording) {
try {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: true,
});
this.recordedChunks = [];
this.recordingTime = "00:00:00";
this.mediaRecorder = new MediaRecorder(stream, {
mimeType: "video/webm;codecs=vp9,opus",
});
this.mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) 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 = `质控录制_${Date.now()}.webm`;
a.click();
URL.revokeObjectURL(url);
};
this.mediaRecorder.start();
this.isRecording = true;
this.startRecordingTimer();
this.$message.success("开始录制");
} catch (e) {
console.error("录制失败:", e);
this.$message.error("录制失败");
}
} else {
this.stopRecording();
}
},
stopRecording() {
if (this.mediaRecorder && this.isRecording) {
this.mediaRecorder.stop();
this.isRecording = false;
this.mediaRecorder.stream.getTracks().forEach((t) => t.stop());
clearInterval(this.recordingTimer);
this.$message.success("录制已保存");
}
},
startRecordingTimer() {
this.recordingTimer = setInterval(() => {
let [h, m, s] = this.recordingTime.split(":").map(Number);
s++;
if (s >= 60) {
s = 0;
m++;
}
if (m >= 60) {
m = 0;
h++;
}
this.recordingTime = `${String(h).padStart(2, "0")}:${String(
m
).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
}, 1000);
},
// ==================== 【6】计时器 ====================
toggleTimer() {
if (this.timerRunning) {
clearInterval(this.timerInterval);
this.timerRunning = false;
} else {
this.timerRunning = true;
this.timerInterval = setInterval(() => {
this.timer++;
}, 1000);
}
},
// ==================== 【7】截图 ====================
takeScreenshot() {
this.$message.success("截图已保存");
},
// ==================== 【8】画笔功能 ====================
initCanvas() {
const canvas = this.$refs.drawingCanvas;
const laserCanvas = this.$refs.laserCanvas;
const container = this.$refs.videoContainer;
if (canvas && container) {
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
this.ctx = canvas.getContext("2d");
this.ctx.lineWidth = this.brushLineWidth;
this.ctx.lineCap = "round";
this.ctx.strokeStyle = this.brushColor;
}
if (laserCanvas && container) {
laserCanvas.width = container.clientWidth;
laserCanvas.height = container.clientHeight;
this.laserCtx = laserCanvas.getContext("2d");
}
},
toggleDraw() {
this.isDrawingMode = !this.isDrawingMode;
this.isLaserMode = false;
if (!this.isDrawingMode) this.clearCanvas();
},
clearCanvas() {
const canvas = this.$refs.drawingCanvas;
if (this.ctx && canvas)
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;
},
// ==================== 【9】激光笔功能 ====================
toggleLaser() {
this.isLaserMode = !this.isLaserMode;
this.isDrawingMode = false;
if (!this.isLaserMode) this.clearLaser();
},
clearLaser() {
const canvas = this.$refs.laserCanvas;
if (this.laserCtx && canvas)
this.laserCtx.clearRect(0, 0, canvas.width, canvas.height);
if (this.laserAnimationId) {
cancelAnimationFrame(this.laserAnimationId);
this.laserAnimationId = null;
}
},
handleMouseMove(e) {
if (!this.isLaserMode) return;
const canvas = this.$refs.laserCanvas;
const rect = canvas.getBoundingClientRect();
this.laserX = e.clientX - rect.left;
this.laserY = e.clientY - rect.top;
this.drawLaser();
},
drawLaser() {
if (!this.isLaserMode) return;
const ctx = this.laserCtx;
const canvas = this.$refs.laserCanvas;
ctx.clearRect(0, 0, canvas.width, canvas.height);
this.laserAlpha += this.laserAlphaDir * 0.05;
if (this.laserAlpha >= 1 || this.laserAlpha <= 0.3) {
this.laserAlphaDir *= -1;
}
ctx.beginPath();
ctx.arc(this.laserX, this.laserY, 15, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255, 0, 0, ${this.laserAlpha * 0.3})`;
ctx.fill();
ctx.beginPath();
ctx.arc(this.laserX, this.laserY, 5, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255, 0, 0, ${this.laserAlpha})`;
ctx.fill();
this.laserAnimationId = requestAnimationFrame(() => this.drawLaser());
},
// ==================== 【10】设置弹窗 ====================
openSettingDialog() {
this.tempSetting = { ...this.globalSetting };
this.settingDialogVisible = true;
},
onSettingDialogOpen() {
this.getDevices();
},
saveSetting() {
this.globalSetting = { ...this.tempSetting };
this.meetingTitle = this.globalSetting.meetingTitle;
this.resolution = this.globalSetting.resolution;
this.brushColor = this.globalSetting.brushColor;
this.brushLineWidth = this.globalSetting.brushWidth;
this.settingDialogVisible = false;
this.$message.success("设置已保存");
},
// ==================== 【11】时间更新 ====================
updateClock() {
const now = new Date();
this.currentTime = now.toLocaleTimeString("zh-CN", { hour12: false });
},
// ==================== 【12】网络监控 ====================
startNetworkMonitoring() {
setInterval(() => {
this.networkDelay = Math.floor(Math.random() * 50);
if (this.networkDelay < 20) {
this.networkQuality = { text: "良好", color: "#00e676" };
} else if (this.networkDelay < 40) {
this.networkQuality = { text: "一般", color: "#ffab00" };
} else {
this.networkQuality = { text: "较差", color: "#ff5252" };
}
}, 2000);
},
// ==================== 【13】会议成员、邀请、病例 ====================
handleInvite() {
this.inviteDialogVisible = true;
},
handleInviteConfirm(selectedContacts) {
console.log("邀请的联系人:", selectedContacts);
this.$message.success(`已邀请 ${selectedContacts.length} 位联系人`);
this.inviteDialogVisible = false;
},
1 month ago
// 取消质控
cancelQualityControl(index) {
this.$confirm("确定要取消该成员的质控吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
this.gridSlots[index].user = null;
this.gridSlots[index].stream = null;
1 month ago
this.gridSlots[index].signalLevel = 0;
this.$message({
type: "success",
message: "已取消质控",
});
})
.catch(() => {});
},
1 month ago
// 打开添加用户弹窗
openAddUserDialog(index) {
this.currentAddIndex = index;
this.showAddDialog = true;
},
1 month ago
// 显示用户详情
showUserDetail(user) {
this.$message.info(`用户: ${user.name}`);
1 month ago
},
1 month ago
// 添加成员成功回调
handleAddMembers(members) {
const emptySlots = this.gridSlots
.map((slot, index) => ({ slot, index }))
.filter((item) => !item.slot.user);
members.forEach((member, idx) => {
if (emptySlots[idx]) {
this.gridSlots[emptySlots[idx].index].user = {
name: member.name,
status: "在线",
avatar:
member.avatar ||
"https://img.icons8.com/fluency/96/doctor-male.png",
};
this.gridSlots[emptySlots[idx].index].signalLevel = 5;
}
});
this.showAddDialog = false;
this.$message({
type: "success",
message: `已添加 ${members.length} 位质控成员`,
});
},
// ==================== 【14】结束会议 ====================
endMeeting() {
this.$confirm("确定结束质控?", "提示", { type: "warning" }).then(() => {
this.stopShare();
if (this.localStream)
this.localStream.getTracks().forEach((t) => t.stop());
this.$message.success("质控已结束");
this.$store.dispatch("app/toggleSideBarHide", false);
this.$store.dispatch("settings/changeSetting", {
key: "fixedHeader",
value: true,
});
this.$store.dispatch("settings/changeSetting", {
key: "tagsView",
value: true,
});
if (window.opener && !window.opener.closed) {
window.opener.location.reload();
}
if (window.name === "qualityControlWindow") {
window.close();
} else {
this.$router.push("/");
}
});
1 month ago
},
},
};
</script>
<style lang="scss" scoped>
.meeting-container {
1 month ago
display: flex;
flex-direction: column;
height: 100vh;
background-color: #00413d;
color: #ffffff;
1 month ago
}
// 顶部导航栏
.top-bar {
height: 36px;
background-color: #00584d;
1 month ago
display: flex;
align-items: center;
padding: 0 12px;
1 month ago
justify-content: space-between;
border-bottom: 1px solid #00695c;
.logo {
display: flex;
align-items: center;
font-weight: 600;
font-size: 14px;
color: #ffffff;
img {
width: 24px;
height: 24px;
margin-right: 6px;
}
}
.title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 13px;
color: #ffffff;
font-weight: 500;
}
.window-controls {
display: flex;
align-items: center;
i {
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 14px;
color: #ffffff;
border-radius: 3px;
transition: background-color 0.2s;
&:hover {
background-color: rgba(255, 255, 255, 0.1);
}
}
}
1 month ago
}
// 主内容区
.main-content {
flex: 1;
display: flex;
overflow: hidden;
padding: 4px;
1 month ago
}
// 2x2 网格布局
1 month ago
.grid-container {
flex: 1;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 4px;
1 month ago
}
// 单个分屏
1 month ago
.grid-slot {
background-color: #00413d;
position: relative;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #00695c;
overflow: hidden;
1 month ago
}
// 信号强度图标
1 month ago
.signal-icon {
position: absolute;
top: 8px;
left: 8px;
display: flex;
align-items: flex-end;
gap: 2px;
height: 14px;
z-index: 10;
.bar {
width: 3px;
background: #666;
border-radius: 1px;
&:nth-child(1) { height: 4px; }
&:nth-child(2) { height: 7px; }
&:nth-child(3) { height: 10px; }
&:nth-child(4) { height: 13px; }
&:nth-child(5) { height: 16px; }
&.active {
background: #4cd964;
}
}
1 month ago
}
// 顶部用户名
1 month ago
.slot-name {
position: absolute;
top: 8px;
left: 50%;
transform: translateX(-50%);
font-size: 12px;
color: #fff;
z-index: 10;
1 month ago
}
// 取消质控按钮
1 month ago
.cancel-btn {
position: absolute;
top: 8px;
right: 8px;
z-index: 10;
1 month ago
}
// 视频区域
.slot-video {
width: 100%;
height: 100%;
object-fit: cover;
}
// 添加质控按钮
1 month ago
.add-btn {
width: 60px;
height: 60px;
background-color: #00897b;
border-radius: 8px;
1 month ago
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background-color 0.2s, transform 0.2s;
border: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
&:hover {
background-color: #00a396;
transform: scale(1.05);
}
&:active {
transform: scale(0.95);
}
.el-icon-plus {
font-size: 28px;
color: #ffffff;
font-weight: bold;
}
1 month ago
}
// 有成员时的用户按钮
1 month ago
.user-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
cursor: pointer;
.user-avatar {
width: 56px;
height: 56px;
border-radius: 10px;
background-color: #00b3ad;
transition: background 0.2s;
&:hover {
background-color: #00d4cd;
}
.avatar-text {
font-size: 20px;
color: #fff;
}
}
.user-info-text {
display: flex;
align-items: center;
gap: 4px;
.user-name {
font-size: 12px;
color: #fff;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
&.online {
background-color: #4cd964;
}
&.offline {
background-color: #999;
}
}
}
1 month ago
}
</style>