index.vue 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. <template>
  2. <BaseLive :room-info="roomInfo">
  3. <div class="student">
  4. <div class="middle">
  5. <!-- 教师学员列表 -->
  6. <PersonnelList
  7. :has-video="isShowPersonnelList"
  8. :student-list="student_list"
  9. :teacher-name="roomInfo.teacher_name"
  10. />
  11. <!-- 直播视频与连线 -->
  12. <div v-show="!isShowDraw" class="live-main">
  13. <div class="live-connection" :style="{ 'padding-top': connect || callLoading ? '131px' : '0' }">
  14. <div class="live">
  15. <div v-show="isTVideo" id="live"></div>
  16. </div>
  17. <div v-show="callLoading" class="call-loading">
  18. <span>等待接通...</span>
  19. </div>
  20. <div v-show="connect" id="student" class="student-video"></div>
  21. </div>
  22. <div v-show="(callLoading || connect) && invite" class="close">
  23. <el-button type="danger" @click="() => (connect ? handsDown() : inviteAccept())">
  24. {{ connect ? '结束连线' : '连线' }}
  25. </el-button>
  26. </div>
  27. </div>
  28. <div class="draw" :style="{ left: isShowDraw ? '0' : '-3000px' }">
  29. <div id="draw-parent"></div>
  30. </div>
  31. <!-- 聊天 -->
  32. <ChatPage ref="cPage" :chat-list="chatList" :chat-show="chatShow" @sendMsg="sendMsg" />
  33. <!-- 成员列表 -->
  34. <MemberList :member-show="memberShow" :student-list="student_list" />
  35. </div>
  36. <div class="bottom">
  37. <div class="operation"></div>
  38. <div class="operation">
  39. <div :class="['operation-item', { active: chatShow }]" @click="toggle(toggleList[0].type)">
  40. <img src="@/assets/live/communication.png" alt="" />
  41. <span>聊天</span>
  42. </div>
  43. <div :class="['operation-item', { active: memberShow }]" @click="toggle(toggleList[1].type)">
  44. <img src="@/assets/live/peoples.png" alt="" />
  45. <span>成员列表</span>
  46. </div>
  47. </div>
  48. <div class="operation">
  49. <button class="live-button" @click="exitRoom(task_id, room_user_id)">退出房间</button>
  50. </div>
  51. </div>
  52. </div>
  53. <!-- 选择设备 -->
  54. <SelectDevice
  55. :dialog-visible-device="dialogVisibleDevice"
  56. :device="device"
  57. @dialogDeviceClose="dialogDeviceClose"
  58. />
  59. <!-- 学员当前推送资料 -->
  60. <CurMaterial
  61. ref="material"
  62. :task-id="task_id"
  63. :material-id="materialId"
  64. :material-type="materialType"
  65. :is-finished="is_finished"
  66. @dialogMaterialClose="dialogMaterialClose"
  67. />
  68. </BaseLive>
  69. </template>
  70. <script>
  71. export default {
  72. name: 'StudentLive'
  73. };
  74. </script>
  75. <script setup>
  76. import { ref, watch, computed, inject, provide, onBeforeUnmount } from 'vue';
  77. import { useRoute } from 'vue-router/composables';
  78. import { Message } from 'element-ui';
  79. import { useInit, useChat, useMemberList, useLive } from '../common/common';
  80. import { useInitListener, useConnect, useStudentLive } from './live';
  81. import store from '@/store';
  82. import { app } from '@/store/mutation-types';
  83. import BaseLive from '../common/BaseLive.vue';
  84. import SelectDevice from '../common/SelectDevice';
  85. import ChatPage from '../common/ChatList.vue';
  86. import MemberList from '../common/MemberList.vue';
  87. import PersonnelList from '../common/PersonnelList.vue';
  88. import CurMaterial from '@/components/live/CurMaterial.vue';
  89. const route = useRoute();
  90. const room_user_id = route.query.room_user_id;
  91. provide('room_user_id', room_user_id);
  92. const $t = inject('$t');
  93. const task_id = route.query.task_id;
  94. provide('task_id', task_id);
  95. let remoteStreamType = ref(-1);
  96. let roomData = ref({
  97. desc: '直播间标题',
  98. name: '姓名',
  99. user: {
  100. id: '',
  101. name: '',
  102. role: 'talker',
  103. rommid: ''
  104. },
  105. max_users: 1,
  106. allow_chat: true,
  107. allow_audio: true,
  108. allow_speak: true
  109. });
  110. let invite = ref(false);
  111. let speakData = ref({});
  112. let roomContext = ref({});
  113. // 推送的资料
  114. let material = ref();
  115. let materialId = ref('');
  116. let materialType = ref('');
  117. let is_finished = ref(false);
  118. function showMaterial(id, type, isFinished) {
  119. materialId.value = id;
  120. materialType.value = type;
  121. is_finished.value = isFinished === 'true';
  122. material.value.dialogShow();
  123. }
  124. function dialogMaterialClose() {
  125. materialId.value = '';
  126. materialType.value = '';
  127. is_finished.value = false;
  128. }
  129. // 本地视频流画面、声音
  130. let device = ref({
  131. video: [],
  132. audio: []
  133. });
  134. let hasVideo = ref(false);
  135. let hasAudio = ref(false);
  136. /**
  137. * 本地流视频开启、关闭
  138. */
  139. function playOrPauseVideo() {
  140. if (device.value.video.length === 0) {
  141. return Message.warning($t('Key399'));
  142. }
  143. if (hasVideo.value) {
  144. pauseVideo({
  145. streamName: 'main',
  146. success: () => {
  147. Message.success($t('Key433'));
  148. hasVideo.value = false;
  149. },
  150. fail: (str) => {
  151. Message.warning(str);
  152. }
  153. });
  154. } else {
  155. playVideo({
  156. streamName: 'main',
  157. success: () => {
  158. Message.success($t('Key434'));
  159. hasVideo.value = true;
  160. },
  161. fail: (str) => {
  162. Message.warning(str);
  163. }
  164. });
  165. }
  166. }
  167. /**
  168. * 本地流音频开启、关闭
  169. */
  170. function playOrPauseAudio() {
  171. if (device.value.audio.length === 0) {
  172. return Message.warning($t('Key401'));
  173. }
  174. if (hasAudio.value) {
  175. pauseAudio({
  176. streamName: 'main',
  177. success: () => {
  178. Message.success($t('Key435'));
  179. hasAudio.value = false;
  180. },
  181. fail: (str) => {
  182. Message.warning(str);
  183. }
  184. });
  185. } else {
  186. playAudio({
  187. streamName: 'main',
  188. success: () => {
  189. Message.success($t('Key436'));
  190. hasAudio.value = true;
  191. },
  192. fail: (str) => {
  193. Message.warning(str);
  194. }
  195. });
  196. }
  197. }
  198. let dialogVisibleDevice = ref(false);
  199. let isRecreate = ref(false);
  200. // 设置音视频设备
  201. function setDevice(isRec) {
  202. isRecreate.value = isRec;
  203. dialogVisibleDevice.value = true;
  204. }
  205. function dialogDeviceClose(device) {
  206. store.commit(`app/${app.SET_DEVICE}`, device);
  207. dialogVisibleDevice.value = false;
  208. }
  209. const { pauseVideo, playVideo, pauseAudio, playAudio, unSubscribeStream } = useLive();
  210. // 聊天
  211. let cPage = ref();
  212. const { chatList, chatShow, sendMsg, toggle: chatToggle } = useChat(cPage);
  213. // 成员列表
  214. const { memberShow, toggle: memberToggle } = useMemberList();
  215. // 教师流
  216. let tStream = ref(null);
  217. // 教师流是否有视频流
  218. let isTVideo = ref(false);
  219. setInterval(() => {
  220. isTVideo.value = tStream.value?.hasVideo() ?? false;
  221. }, 1000);
  222. let isShowDraw = ref(false);
  223. let isShowPersonnelList = computed(() => {
  224. return isTVideo.value || connect.value || callLoading.value || isShowDraw.value;
  225. });
  226. const { exitRoom } = useStudentLive();
  227. const { callLoading, connect, connectStudent, material_list, roomInfo, student_list, dealStudentConnection } = useInit(
  228. useInitListener,
  229. {
  230. roomData,
  231. chatList,
  232. device,
  233. roomContext,
  234. speakData,
  235. remoteStreamType,
  236. isShowDraw,
  237. setDevice,
  238. invite,
  239. room_user_id,
  240. tStream,
  241. exitRoom
  242. }
  243. );
  244. watch(
  245. () => roomInfo.value.is_enable_whiteboard,
  246. (val) => {
  247. isShowDraw.value = val;
  248. }
  249. );
  250. const { handsDown, inviteAccept } = useConnect({
  251. roomInfo,
  252. room_user_id,
  253. connect,
  254. callLoading,
  255. invite,
  256. dealStudentConnection
  257. });
  258. onBeforeUnmount(() => {
  259. if (callLoading.value || connect.value) handsDown();
  260. if (tStream.value) unSubscribeStream(tStream.value);
  261. });
  262. // 多个附属界面切换
  263. const toggleList = [
  264. {
  265. type: 'chat',
  266. toggle: chatToggle
  267. },
  268. {
  269. type: 'member',
  270. toggle: memberToggle
  271. }
  272. ];
  273. function toggle(type) {
  274. let item = toggleList.find((item) => item.type === type);
  275. if (item === undefined) return;
  276. item.toggle();
  277. toggleList.forEach((item) => {
  278. if (item.type !== type) {
  279. item.toggle(false);
  280. }
  281. });
  282. }
  283. </script>
  284. <style lang="scss" scoped>
  285. .student {
  286. display: flex;
  287. flex-direction: column;
  288. height: 100%;
  289. .middle {
  290. position: relative;
  291. flex: 1;
  292. overflow: hidden;
  293. .live-main {
  294. display: flex;
  295. flex-direction: column;
  296. width: 100%;
  297. height: 100%;
  298. background-color: #434343;
  299. .live-connection {
  300. display: flex;
  301. flex: 1;
  302. .live {
  303. flex: 1;
  304. background-color: #000;
  305. #live {
  306. width: 100%;
  307. height: 100%;
  308. }
  309. }
  310. .call-loading {
  311. display: flex;
  312. flex-basis: 50%;
  313. align-items: center;
  314. justify-content: center;
  315. color: #fff;
  316. background-color: #333;
  317. }
  318. .student-video {
  319. flex-basis: 50%;
  320. }
  321. }
  322. .close {
  323. display: flex;
  324. justify-content: center;
  325. padding: 90px 0 16px;
  326. }
  327. }
  328. .draw {
  329. position: absolute;
  330. width: 100%;
  331. height: 100%;
  332. overflow: hidden;
  333. #draw-parent {
  334. height: 100%;
  335. margin: auto;
  336. }
  337. }
  338. }
  339. .bottom {
  340. display: flex;
  341. align-items: center;
  342. justify-content: space-between;
  343. height: 96px;
  344. padding: 12px 16px;
  345. background-color: #f8f8f8;
  346. .operation {
  347. display: flex;
  348. column-gap: 16px;
  349. &-item {
  350. display: flex;
  351. flex-direction: column;
  352. row-gap: 4px;
  353. align-items: center;
  354. width: 80px;
  355. height: 70px;
  356. padding: 8px;
  357. cursor: pointer;
  358. border-radius: 8px;
  359. transition: background-color 0.3s;
  360. &.active,
  361. &:active {
  362. background-color: #e5e5e5;
  363. }
  364. img {
  365. width: 32px;
  366. height: 32px;
  367. }
  368. }
  369. .line {
  370. width: 2px;
  371. height: 70px;
  372. background-color: #d0d0d0;
  373. }
  374. .live-button {
  375. height: 48px;
  376. padding: 8px 24px;
  377. font-size: 20px;
  378. font-weight: 500;
  379. color: #e52e2e;
  380. cursor: pointer;
  381. border: 2px solid #e52e2e;
  382. border-radius: 4px;
  383. }
  384. }
  385. }
  386. }
  387. </style>