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

627 lines
15 KiB

1 month ago
<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,
});
1 month ago
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();
1 month ago
window.hirtcwebsdk.join(roomId, uid, uName);
window.hirtcwebsdk.addListener("joined", () => {
this.$message.success("加入会议成功");
});
},
methods: {
minimizeWindow() {
// 返回父页面(视频通信视讯)
this.$router.push("/video/index");
1 month ago
},
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>