Files
meeting-client/src/views/meeting.vue
2022-05-29 13:30:19 +08:00

644 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div id="app-container" v-loading="isMeetingLoading" ref="appContainer">
<div class="row">
<div class="meeting-container" ref="meetingContainerRef">
<div id="video-element" ref="videoElementRef"></div>
<el-button @click="setFullScreen" class="fullscreen-btn">
全屏</el-button
>
</div>
<div class="chat-right" ref="chatRightRef">
<messageList :messageList="messages"></messageList>
<div class="option-bar">
<el-input
v-model="editingMessage"
style="margin-right: 15px"
></el-input>
<el-button
type="success"
@click="sendMessage"
:disabled="editingMessage.length === 0"
>发送</el-button
>
</div>
</div>
</div>
<el-tabs class="tabs" type="border-card">
<el-tab-pane label="会议介绍">
<div class="meeting-info meeting-note" v-html="meetingNote"></div>
</el-tab-pane>
<el-tab-pane label="会议日程"
><div
class="meeting-info meeting-schedule"
v-html="meetingSchedule"
></div
></el-tab-pane>
<el-tab-pane label="专家介绍"
><div class="meeting-info expert-info" v-html="expertInfo"></div
></el-tab-pane>
<el-tab-pane label="聊天" class="chat-pane" v-if="screenWidth < 768"
><div class="meeting-info chat">
<messageList :messageList="messages"></messageList>
<div class="option-bar">
<el-input
v-model="editingMessage"
style="margin-right: 15px"
></el-input>
<!-- <svg-icon :icon-class="search" style="height: 30px; width: 16px" /> -->
<el-button
type="success"
@click="sendMessage"
:disabled="editingMessage.length === 0"
>发送</el-button
>
</div>
</div></el-tab-pane
>
</el-tabs>
<el-dialog
v-model="showSignDialog"
:close-on-click-modal="false"
title="签到"
width="30%"
>
<span>是否确认签到</span>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="submitSign">确定</el-button>
<el-button type="primary" @click="showSignDialog = false"
>取消</el-button
>
</span>
</template>
</el-dialog>
<questions
mode="1"
v-if="showExamDialog"
:showDialog="showExamDialog"
@close="showExamDialog = $event"
></questions>
<questions
mode="2"
v-if="showQuestionnaireDialog"
:showDialog="showQuestionnaireDialog"
@close="showQuestionnaireDialog = $event"
></questions>
</div>
</template>
<script setup>
import {
computed,
reactive,
watch,
ref,
onMounted,
onUnmounted,
nextTick,
} from "vue";
import { useStore } from "vuex";
import { signMeeting, generateSignature } from "@/api/meeting";
import questions from "@/components/questions";
import messageList from "@/components/messageList";
import dayjs from "dayjs";
import ZoomMtgEmbedded from "@zoomus/websdk/embedded";
import _ from "lodash";
import ReconnectingWebSocket from "reconnecting-websocket";
import { ElMessage, ElMessageBox } from "element-plus";
import { useRoute } from "vue-router";
const route = useRoute();
const store = useStore();
const meetingNote = computed(() => store.getters.meetingNote); // 会议介绍
const meetingSchedule = computed(() => store.getters.meetingSchedule); // 会议日程
const expertInfo = computed(() => store.getters.expertInfo); // 专家信息
const isMeetingLoading = ref(true); // 视频课件是否正在加载
const templateId = ref(6); // 布局 id
if (store.getters.templateId) {
templateId.value = store.getters.templateId;
}
const joinAccount = ref(""); // 参会账号
const joinName = ref(""); // 参会名称
const screenWidth = ref(0); // 屏幕宽度
const meetingWidth = ref(0); // 视频课件容器元素宽度
const meetingHeight = ref(0); // 视频课件容器元素高度
screenWidth.value = document.body.offsetWidth;
joinAccount.value =
store.getters.icCard || store.getters.phone || store.getters.nickname;
joinName.value =
store.getters.nickname || store.getters.phone || store.getters.icCard;
// 设置会议背景大小、图片
const meetingContainerRef = ref(null); // 会议容器元素(包含背景)
const videoElementRef = ref(null); // 视频课件容器元素
const chatRightRef = ref(null); // 右侧聊天元素
onMounted(() => {
meetingContainerRef.value.style.background = `url(${store.getters.templateBackgroundPic}) 0% 0% / cover no-repeat`; // 设置背景图片
setTextLabel();
meetingWidth.value = meetingContainerRef.value.offsetWidth * 0.95; // zoom 会议组件宽度是父元素宽度的 95%
meetingHeight.value = (meetingWidth.value * 9) / 16; // 根据 zoom 会议组件长宽比 16:9 计算出会议组件高度
meetingContainerRef.value.style.paddingTop = `${meetingHeight.value * 0.2}px`; // 父元素顶部padding 是会议组件高度的20%
videoElementRef.value.style.width = meetingWidth.value + "px";
videoElementRef.value.style.height = meetingHeight.value + "px";
});
// 设置文本标签
const setTextLabel = () => {
document.querySelectorAll(".text-tag").forEach((el) => {
el.remove();
});
store.getters.textLabelList.forEach((item) => {
const labelObj = JSON.parse(item.textLabel);
const textEl = document.createElement("div");
textEl.id = `${_.uniqueId("tag-")}`;
textEl.innerHTML = labelObj.content;
textEl.className = "text-tag";
textEl.style.backgroundColor = labelObj.backgroundColor;
textEl.style.visibility =
labelObj.visibility === "1" ? "visible" : "hidden";
textEl.style.left = `${labelObj.x}`;
textEl.style.top = `${labelObj.y}`;
meetingContainerRef.value.appendChild(textEl);
});
};
// 会议配置
const meetingConfig = reactive({
client: ZoomMtgEmbedded.createClient(),
// This Sample App has been updated to use SDK App type credentials https://marketplace.zoom.us/docs/guides/build/sdk-app
sdkKey: "99Spa64AWHYVZD95imUpVyMD0KF9CpEIrIb1",
// meetingNumber: store.getters.meetingNumber,
meetingNumber: store.getters.meetingNumber,
passWord: store.state.password,
// sdkKey: "99Spa64AWHYVZD95imUpVyMD0KF9CpEIrIb1",
// meetingNumber: "97097842319",
// passWord: "111916",
// role: route.name === "Host" ? 1 : 0,
role: 0,
// signatureEndpoint: "http://120.26.107.74:4000",
userEmail: store.getters.email,
// userEmail: "934510341@qq.com",
userName: joinName.value,
// userName: "afasde1",
// pass in the registrant's token if your meeting or webinar requires registration. More info here:
// Meetings: https://marketplace.zoom.us/docs/sdk/native-sdks/web/component-view/meetings#join-registered
// Webinars: https://marketplace.zoom.us/docs/sdk/native-sdks/web/component-view/webinars#join-registered
// registrantToken:
// "Xhi0bKUzyNBnJwe2EJWZ0JZ3IYqtPgyyWE1CXW3z2X4.DQMAAAAWm3t-jxZqSS14R0dCSFRDNkFibVQwakpHbFd3AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
registrantToken: store.getters.token,
});
console.log(meetingConfig);
// 生成签名
const getSignature = async () => {
const { sign } = await generateSignature({
meetingNumber: store.getters.meetingNumber,
role: meetingConfig.role,
});
console.log(sign);
startMeeting(sign);
// axios
// .post("http://localhost:4000", {
// meetingNumber: store.getters.meetingNumber,
// role: meetingConfig.role,
// })
// .then((res) => {
// console.log(res.data.signature);
// startMeeting(res.data.signature);
// })
// .catch((error) => {
// console.log(error);
// });
};
// 开始会议
const startMeeting = async (signature) => {
let meetingSDKElement = document.getElementById("video-element");
try {
meetingConfig.client.init({
debug: true,
zoomAppRoot: meetingSDKElement,
language: "zh-CN",
customize: {
video: {
isResizable: false,
popper: {
disableDraggable: true,
},
viewSizes: {
default: {
width: meetingWidth.value,
height: meetingHeight.value - 45,
},
},
},
meetingInfo: [
"topic",
"host",
"mn",
"pwd",
"telPwd",
"invite",
"participant",
"dc",
"enctype",
],
},
});
await meetingConfig.client.join({
sdkKey: meetingConfig.sdkKey,
signature: signature,
meetingNumber: meetingConfig.meetingNumber,
password: meetingConfig.passWord,
userName: meetingConfig.userName,
userEmail: meetingConfig.userEmail,
tk: meetingConfig.registrantToken,
});
document.querySelector("#suspension-view-tab-thumbnail-gallery").click();
initDesktopLayout();
isMeetingLoading.value = false;
} catch (error) {
isMeetingLoading.value = false;
}
};
const setFullScreen = () => {
videoElementRef.value.requestFullscreen();
};
onMounted(() => {
getSignature();
});
// 初始化
const initDesktopLayout = () => {
// 初始化高度
const heightEl = document.querySelectorAll(
".zmwebsdk-MuiBox-root.zmwebsdk-MuiBox-root"
)[2];
heightEl.style.height = `${meetingHeight.value - 45}px`;
console.log(meetingHeight.value);
// 初始化宽度
document.querySelectorAll(
".zmwebsdk-MuiPaper-root.zmwebsdk-MuiPaper-elevation1.zmwebsdk-MuiPaper-rounded"
)[1].style.width = `${meetingWidth.value - 4}px`;
// 加载完成显示 video element
document.querySelector("#video-element").style.visibility = "visible";
};
// 检测屏幕共享开启状态变化
const inSharing = ref(false); // 是否开启屏幕共享
setInterval(() => {
const elSharing = document.querySelector(
"div[class*=zmwebsdk-makeStyles-inSharing]"
);
if (elSharing) {
inSharing.value = true;
} else {
inSharing.value = false;
}
}, 500);
const avatarList = ref("");
setInterval(() => {
const avatarElList = document.querySelectorAll(
'ul[class^="zmwebsdk-makeStyles-avatarList"] li'
);
const arr = [];
avatarElList.forEach((el) => {
arr.push({
videoOn: el.className.includes("videoOn"),
});
});
avatarList.value = arr
.map((el) => {
return el.videoOn ? "on" : "off";
})
.join(",");
// console.log(avatarList.value);
}, 500);
watch(avatarList, (val) => {
if (inSharing.value) {
document
.querySelectorAll(`ul[class^="zmwebsdk-makeStyles-avatarList"] li`)
.forEach((el) => {
if (el.className.includes("videoOn")) {
el.style.visibility = "visible";
} else {
el.style.visibility = "hidden";
}
});
}
});
// 当观众被设为嘉宾时,摄图会自动改为 ribbon,通过setInterval 监听变化将其重新设为gallery view
const isRibbon = ref(null);
setInterval(() => {
isRibbon.value = document.querySelector(
"#suspension-view-tab-thumbnail-ribbon.zmwebsdk-MuiTab-selected"
);
}, 500);
watch(isRibbon, (val) => {
if (val) {
const galleryViewButton = document.querySelector(
"#suspension-view-tab-thumbnail-gallery"
);
if (galleryViewButton) galleryViewButton.click();
initDesktopLayout();
}
});
// 当被设为嘉宾时自动点击同意按钮
setInterval(() => {
const isSetAsGuest = document.querySelector(
".zmwebsdk-MuiButtonBase-root.zmwebsdk-MuiButton-root.zmwebsdk-MuiButton-contained.zmwebsdk-MuiButton-containedPrimary.zmwebsdk-MuiButton-containedSizeSmall.zmwebsdk-MuiButton-sizeSmall.zmwebsdk-MuiButton-disableElevation"
);
if (isSetAsGuest) {
isSetAsGuest.click();
}
}, 500);
// 根据id设置布局
const setLayout = (templateId) => {
console.log(templateId);
const v_s_wrap_el = document.querySelectorAll(
".zmwebsdk-MuiBox-root.zmwebsdk-MuiBox-root"
)[2]; // 包含视频和课件的容器
v_s_wrap_el.style.flexDirection = "";
const v_wrap_el = v_s_wrap_el.lastChild;
if (!inSharing.value) return;
const s_wrap_el = document.querySelector(
`div[class*="zmwebsdk-makeStyles-inSharing"]`
);
if (templateId === "1") {
// 课件|视频 对半分
v_wrap_el.style.width = `${meetingWidth.value / 2 - 2}px`;
} else if (templateId === "2") {
// 视频|课件 对半分
v_wrap_el.style.width = `${meetingWidth.value / 2 - 2}px`;
v_s_wrap_el.style.flexDirection = "row-reverse";
} else if (templateId === "3") {
// 课件|视频 左4/5 | 右边1/5
v_wrap_el.style.width = `${(meetingWidth.value - 4) / 5}px`;
} else if (templateId === "4") {
// 视频|课件 左1/4 | 右边3/4
v_wrap_el.style.width = `${(meetingWidth.value - 4) / 5}px`;
v_s_wrap_el.style.flexDirection = "row-reverse";
} else if (templateId === "5") {
// 只显示课件
v_wrap_el.style.display = `none`;
} else if (templateId === "6") {
// 只显示视频
s_wrap_el.style.display = "none";
v_wrap_el.style.width = `${meetingWidth.value - 4}px`;
}
};
watch(inSharing, (newVal) => {
console.log(newVal);
if (newVal) {
setLayout(templateId.value);
} else {
const galleryViewButton = document.querySelector(
"#suspension-view-tab-thumbnail-gallery"
);
if (galleryViewButton) galleryViewButton.click();
const v_s_wrap_el = document.querySelectorAll(
".zmwebsdk-MuiBox-root.zmwebsdk-MuiBox-root"
)[2];
if (!v_s_wrap_el) return;
const v_wrap_el = v_s_wrap_el.lastChild;
v_wrap_el.style.width = "";
v_s_wrap_el.style.flexDirection = "";
}
});
let socket = reactive({});
const initWebSocket = () => {
// 建立websocket连接
socket = new ReconnectingWebSocket(
`wss://meeting.chuhuankj.com/wss/websocket/meeting/${store.getters.meetingId}/${joinAccount.value}`
);
socket.addEventListener("open", () => {
// socket.send("Hello Server!");
console.log("websocket,已连接");
});
// 监听websocket消息
socket.addEventListener("message", async (event) => {
const data = JSON.parse(JSON.parse(event.data));
console.log(data);
// 会议信息更新时
if (data.type === "isRefreshMeeting") {
await store.dispatch("getMeetingInfo", store.getters.meetingId);
meetingContainerRef.value.style.background = ` url(${store.getters.templateBackgroundPic}) 0% 0% / cover no-repeat`;
templateId.value = store.getters.templateId;
setLayout(templateId.value);
setTextLabel();
}
// 开始签到时
else if (data.type === "isStartSign") {
console.log(data);
showSignDialog.value = true;
}
// 签到结束时
else if (data.type === "isEndSign") {
showSignDialog.value = false;
}
// 收到聊天消息时
else if (data.type === "isChat") {
console.log(JSON.parse(JSON.parse(JSON.parse(data.content).msg)));
messages.value.push({
...JSON.parse(JSON.parse(JSON.parse(data.content).msg)),
id: _.uniqueId(),
time: dayjs().format("YYYY-MM-DD HH:mm:ss"),
});
}
// 开始考试时
else if (data.type === "isStartExam") {
showExamDialog.value = true;
}
// 开始问卷时
else if (data.type === "isStartQuestionnaire") {
showQuestionnaireDialog.value = true;
}
// 会议结束时
else if (data.type === "isCloseMeeting") {
showExamDialog.value = false;
showQuestionnaireDialog.value = false;
ElMessageBox.alert("会议已结束");
socket.close();
}
});
socket.addEventListener("close", (event) => {
console.log(event, "close");
});
socket.addEventListener("error", (event) => {
console.log(event, "error");
});
};
initWebSocket();
/* 签到功能 */
// 提交签到
const showSignDialog = ref(false); //是否显示签到窗口
const submitSign = async () => {
await signMeeting({
meetingId: store.getters.meetingId,
signTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
account: joinAccount.value,
});
showSignDialog.value = false;
ElMessage.success("签到成功");
};
/* 聊天功能 */
const messages = ref([]); // 消息列表
const editingMessage = ref(""); // 正在编辑的内容
// 发送消息
const sendMessage = () => {
console.log(
JSON.stringify(
JSON.stringify({
account: joinName.value,
msg: editingMessage.value,
})
)
);
socket.send(
JSON.stringify(
JSON.stringify({
account: joinName.value,
msg: editingMessage.value,
})
)
);
// socket.send(editingMessage.value);
messages.value.push({
id: _.uniqueId(),
account: joinName.value,
msg: editingMessage.value,
isMe: true,
time: dayjs().format("YYYY-MM-DD HH:mm:ss"),
});
editingMessage.value = "";
};
const leaveConference = () => {
meetingConfig.client.leaveMeeting();
};
window.addEventListener("beforeunload", leaveConference);
onUnmounted(() => {
window.removeEventListener("beforeunload", leaveConference);
});
// 是否显示考试和问卷弹窗
const showExamDialog = ref(false);
const showQuestionnaireDialog = ref(false);
</script>
<style scoped lang="scss">
#app-container {
width: 100%;
margin: 0 auto;
:deep(.text-tag) {
position: absolute;
z-index: 999;
border-radius: 4px;
padding: 5px;
background-color: #fff;
* {
margin: 0;
padding: 0;
}
}
}
.row {
width: 100%;
display: flex;
}
.meeting-container {
position: relative;
width: 80%;
display: flex;
justify-content: center;
}
.chat-right {
width: 20%;
display: flex;
flex-direction: column;
justify-content: space-between;
.option-bar {
margin-top: 0;
margin: 5px;
}
:deep(.message-list) {
overflow-y: scroll;
margin-bottom: 0;
// height: 80%;
flex: 1;
p {
margin: 0;
}
}
}
@media screen and (max-width: 768px) {
.meeting-container {
width: 100%;
}
.chat-right {
display: none;
}
}
#video-element {
// visibility: hidden;
// height: 600px;
}
.tabs {
width: 100%;
.meeting-info {
:deep(p) {
margin: 0;
}
}
// :deep(.el-tabs__content) {
// .chat {
// }
// }
}
.option-bar {
display: flex;
align-items: center;
justify-content: space-around;
}
:deep(.zmwebsdk-MuiPaper-root) {
background: transparent;
box-shadow: 0 0;
}
:deep(.zmwebsdk-MuiToolbar-root) {
display: none;
}
:deep(.zmwebsdk-makeStyles-singleView-7) {
// background-color: transparent;
padding: 0;
}
:deep(.zmwebsdk-MuiPaper-root.zmwebsdk-makeStyles-root-50.zmwebsdk-MuiPaper-elevation1.zmwebsdk-MuiPaper-rounded) {
width: 20%;
background: #ccc;
}
.fullscreen-btn {
position: absolute;
right: 0;
bottom: 0;
}
</style>