detailInfo.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679
  1. <template>
  2. <div id="detailInfo">
  3. <el-row>
  4. <el-col :span="24" class="info">
  5. <el-col :span="4" class="left">
  6. <el-col :span="24" class="leftTop">
  7. <el-image :src="roomInfo.filedir"></el-image>
  8. <p>{{ roomInfo.title }}</p>
  9. <p>{{ roomInfo.content }}</p>
  10. </el-col>
  11. <el-col :span="24" class="leftDown">
  12. <el-col :span="8" class="btn" @click.native="shexiangBtn()">
  13. <i class="iconfont iconshexiangtou"></i>
  14. <p>摄像头</p>
  15. </el-col>
  16. <el-col :span="8" class="btn" @click.native="tianchongBtn()">
  17. <i class="iconfont iconmaikefeng-tianchong"></i>
  18. <p>麦克风</p>
  19. </el-col>
  20. <el-col :span="8" class="btn" @click.native="chatBtn()">
  21. <i class="el-icon-user"></i>
  22. <p>聊天</p>
  23. </el-col>
  24. </el-col>
  25. <el-col :span="24" class="leftDown">
  26. <el-col :span="8" class="btn" @click.native="lookuserBtn()">
  27. <i class="el-icon-user"></i>
  28. <p>成员</p>
  29. </el-col>
  30. <el-col :span="8" class="btn" @click.native="queBtn()">
  31. <i class="el-icon-question"></i>
  32. <p>问卷</p>
  33. </el-col>
  34. <el-col :span="8" class="btn" @click.native="queCloseBtn()">
  35. <i class="el-icon-circle-close"></i>
  36. <p>停卷</p>
  37. </el-col>
  38. </el-col>
  39. </el-col>
  40. <el-col :span="20" class="right">
  41. <el-col :span="24" class="rightTop">
  42. <span @click="liveon"><i class="iconfont iconshexiangtou"></i>直播</span>
  43. <span @click="liveclose"><i class="el-icon-switch-button"></i>关闭</span>
  44. <span @click="shareon"><i class="iconfont iconfenxiang"></i>屏幕</span>
  45. <span><el-switch @change="recordclick" v-model="isrecord" active-text="录制" inactive-text="停录"> </el-switch></span>
  46. </el-col>
  47. <el-col :span="2" class="noVideo"> </el-col>
  48. <el-col :span="20" class="video">
  49. <el-col :span="24" class="videoMeet">
  50. <el-col :span="18" class="one">
  51. <div id="main-video" class="video-box col-div" style="justify-content: flex-end"></div>
  52. </el-col>
  53. </el-col>
  54. </el-col>
  55. <el-col :span="2" class="noVideo"> </el-col>
  56. <el-col :span="24" class="rightDown">
  57. <!-- 开始直播 -->
  58. </el-col>
  59. </el-col>
  60. </el-col>
  61. </el-row>
  62. <el-dialog title="摄像头" :visible.sync="shexiangDia" width="30%" :before-close="handleClose">
  63. <el-select @change="cameraChange" v-model="cameraId" filterable placeholder="请选择摄像头">
  64. <el-option v-for="item in cameras" :key="item.deviceId" :label="item.label" :value="item.deviceId"> </el-option>
  65. </el-select>
  66. </el-dialog>
  67. <el-dialog title="麦克风" :visible.sync="tianchongDia" width="30%" :before-close="handleClose">
  68. <el-select @change="micrChange" v-model="microphoneId" filterable placeholder="请选择麦克风">
  69. <el-option v-for="item in microphones" :key="item.deviceId" :label="item.label" :value="item.deviceId"> </el-option>
  70. </el-select>
  71. </el-dialog>
  72. <el-dialog title="讨论" :visible.sync="chatDia" width="50%" :before-close="handleClose">
  73. <el-row>
  74. <el-col :span="24" class="chatList">
  75. <el-col :span="24" class="list" v-for="(item, index) in dataList" :key="index">
  76. <p>
  77. <span :class="item.sendname == user.name ? 'selfColor' : ''">{{ item.sendname }}</span>
  78. <span>{{ item.content }}</span>
  79. </p>
  80. </el-col>
  81. </el-col>
  82. <el-col :span="24" class="chatInput">
  83. <el-col :span="19" class="input">
  84. <el-input type="textarea" maxlength="5000" show-word-limit v-model="content"></el-input>
  85. </el-col>
  86. <el-col :span="5" class="btn">
  87. <el-button type="primary" size="mini" @click="chatCreate">发送</el-button>
  88. </el-col>
  89. </el-col>
  90. </el-row>
  91. </el-dialog>
  92. <el-dialog title="问卷" :visible.sync="queDia" width="38%" :before-close="handleClose">
  93. <el-row>
  94. <el-col :span="24">
  95. <el-col :span="12">
  96. <el-select v-model="queid" filterable placeholder="请选择问卷">
  97. <el-option v-for="item in questList" :key="item.id" :label="item.name" :value="item.id"> </el-option>
  98. </el-select>
  99. </el-col>
  100. <el-col :span="12" class="btn">
  101. <el-button type="primary" size="mini" @click="queCreate">发送</el-button>
  102. </el-col>
  103. </el-col>
  104. </el-row>
  105. </el-dialog>
  106. <el-dialog title="成员" :visible.sync="lookuserDia" width="60%" height="450px" :before-close="handleClose" :close-on-click-modal="(clo = false)">
  107. <el-row>
  108. <el-col :span="24" class="sudoku_row">
  109. <el-col :span="24" class="sudoku_item" v-for="(item, index) in userList" :key="index">
  110. <div :id="forId(item.userid)" class="video-box col-div lookvideo" style="justify-content: flex-end"></div>
  111. <p>
  112. <i class="el-icon-user"></i>
  113. <span class="selfColor">{{ item.username }}</span>
  114. <span v-if="item.switchrole === 'anchor'"
  115. ><el-button type="danger" size="mini" @click="lookuserUpdate(item.id, 'audience')">移除</el-button></span
  116. >
  117. <span v-else><el-button type="primary" size="mini" @click="lookuserUpdate(item.id, 'anchor')">连麦</el-button></span>
  118. </p>
  119. </el-col>
  120. </el-col>
  121. </el-row>
  122. </el-dialog>
  123. </div>
  124. </template>
  125. <script>
  126. import Vue from 'vue';
  127. import { mapState, createNamespacedHelpers } from 'vuex';
  128. const { mapActions: gensign } = createNamespacedHelpers('gensign');
  129. const { mapActions: chat } = createNamespacedHelpers('chat');
  130. const { mapActions: room } = createNamespacedHelpers('room');
  131. const { mapActions: lookuser } = createNamespacedHelpers('lookuser');
  132. import TRTC from 'trtc-js-sdk';
  133. export default {
  134. name: 'detailInfo',
  135. props: {
  136. roomInfo: null,
  137. },
  138. components: {},
  139. data: function() {
  140. return {
  141. // 摄像头
  142. shexiangDia: false,
  143. cameraId: '',
  144. cameras: [],
  145. // 麦克风
  146. tianchongDia: false,
  147. chatDia: false,
  148. queDia: false,
  149. questList: [],
  150. queid: '',
  151. microphoneId: '',
  152. microphones: [],
  153. client_: '',
  154. localStream_: '',
  155. sdkAppId_: '1400380125',
  156. userId_: '1111',
  157. open_: false,
  158. content: '',
  159. dataList: [],
  160. isrecord: false,
  161. shareid: '',
  162. userList: [],
  163. lookuserDia: false,
  164. index_: 0,
  165. ov1: '',
  166. ov2: '',
  167. ov3: '',
  168. ov4: '',
  169. ov5: '',
  170. ov6: '',
  171. ov7: '',
  172. showbtn_: false,
  173. };
  174. },
  175. created() {
  176. this.initclient();
  177. this.getDevices();
  178. this.chatSearch();
  179. },
  180. mounted() {
  181. this.channel();
  182. },
  183. methods: {
  184. ...gensign(['gensignFetch']),
  185. ...chat(['query', 'create', 'fetch']),
  186. ...room(['startrecord', 'stoprecord', 'roomquest', 'roomquestclose', 'questquery', 'updateanchor']),
  187. ...lookuser(['lookquery', 'lookupdate']),
  188. async queCreate() {
  189. const data = {};
  190. data.roomid = this.id;
  191. data.queid = this.queid;
  192. const res = await this.roomquest(data);
  193. if (this.$checkRes(res)) {
  194. console.log(res.data);
  195. this.$message({
  196. message: '操作成功',
  197. type: 'success',
  198. });
  199. }
  200. },
  201. async lookuserBtn() {
  202. this.lookuserDia = true;
  203. this.lookuserSearch();
  204. },
  205. async lookuserSearch({ skip = 0, limit = 1000 } = {}) {
  206. const info = { roomid: this.id };
  207. let res = await this.lookquery({ skip, limit, ...info });
  208. console.log(res.data);
  209. this.$set(this, `userList`, res.data);
  210. },
  211. async lookuserUpdate(_id, _switchrole) {
  212. console.log(_id);
  213. let data = {};
  214. data.id = _id;
  215. data.switchrole = _switchrole;
  216. const res = await this.lookupdate(data);
  217. if (this.$checkRes(res)) {
  218. console.log(res.data);
  219. this.$message({
  220. message: '操作成功',
  221. type: 'success',
  222. });
  223. this.lookuserSearch();
  224. } else {
  225. this.$message.error(res.errmsg);
  226. }
  227. },
  228. async recordclick() {
  229. console.log(this.isrecord);
  230. if (this.isrecord) {
  231. const info = { roomid: this.id, roomname: this.name, shareid: this.shareid };
  232. let res = await this.startrecord({ ...info });
  233. } else {
  234. const info = { roomid: this.id, roomname: this.name };
  235. let res = await this.stoprecord({ ...info });
  236. }
  237. },
  238. async chatSearch({ skip = 0, limit = 1000 } = {}) {
  239. const info = { roomid: this.id };
  240. let res = await this.query({ skip, limit, ...info });
  241. this.$set(this, `dataList`, res.data);
  242. },
  243. async questSearch({ skip = 0, limit = 1000 } = {}) {
  244. const info = { status: '1' };
  245. let res = await this.questquery({ skip, limit, ...info });
  246. console.log(res);
  247. if (this.$checkRes(res)) {
  248. this.$set(this, `questList`, res.data);
  249. }
  250. },
  251. async chatCreate() {
  252. let data = {};
  253. data.roomid = this.id;
  254. data.type = '0';
  255. data.content = this.content;
  256. data.sendid = this.user.uid;
  257. data.sendname = this.user.name;
  258. const res = await this.create(data);
  259. if (this.$checkRes(res)) {
  260. console.log(res.data);
  261. this.content = '';
  262. }
  263. },
  264. channel() {
  265. console.log('in function:');
  266. this.$stomp({
  267. [`/exchange/public_chat_` + this.id]: this.onMessage,
  268. });
  269. },
  270. onMessage(message) {
  271. // console.log('receive a message: ', message.body);
  272. let body = _.get(message, 'body');
  273. if (body) {
  274. body = JSON.parse(body);
  275. this.dataList.push(body);
  276. this.content = '';
  277. }
  278. // const { content, contenttype, sendid, sendname, icon, groupid, sendtime, type } = message.headers;
  279. // let object = { content, contenttype, sendid, sendname, icon, groupid, sendtime, type };
  280. // this.list.push(object);
  281. },
  282. async getDevices() {
  283. this.cameras = await TRTC.getCameras();
  284. this.microphones = await TRTC.getMicrophones();
  285. },
  286. async initclient() {
  287. console.log(this.user.uid);
  288. if (this.anchorid === this.user.uid) {
  289. this.showbtn_ = true;
  290. this.userId_ = 'mainr-' + this.user.uid;
  291. } else {
  292. this.userId_ = 'other-' + this.user.uid;
  293. }
  294. const res = await this.gensignFetch({ userid: this.userId_ });
  295. if (this.$checkRes(res)) {
  296. console.log(res.data);
  297. this.client_ = TRTC.createClient({
  298. mode: 'live',
  299. sdkAppId: this.sdkAppId_,
  300. userId: this.userId_,
  301. userSig: res.data,
  302. });
  303. }
  304. },
  305. async liveon() {
  306. this.open_ = true;
  307. console.log('8888--' + this.userId_);
  308. if (this.cameraId === '' || this.microphoneId === '') {
  309. this.$message({
  310. message: '请选择摄像头和麦克风',
  311. type: 'warning',
  312. });
  313. return;
  314. }
  315. await this.client_.join({ roomId: this.name, role: 'anchor' });
  316. this.localStream_ = await TRTC.createStream({
  317. audio: true,
  318. video: true,
  319. cameraId: this.cameraId,
  320. microphoneId: this.microphoneId,
  321. userId: this.userId_,
  322. });
  323. this.localStream_.setVideoProfile('480p');
  324. await this.localStream_.initialize();
  325. console.log('initialize local stream success');
  326. // publish the local stream
  327. await this.client_.publish(this.localStream_);
  328. this.localStream_.play('main-video');
  329. //$('#mask_main').appendTo($('#player_' + this.localStream_.getId()));
  330. // 订阅其他用户音视频
  331. this.client_.on('stream-subscribed', event => {
  332. const remoteStream = event.stream;
  333. // 远端流订阅成功,播放远端音视频流
  334. const usertempid_ = remoteStream.getUserId();
  335. console.log('111' + remoteStream.getUserId());
  336. if (usertempid_) {
  337. const usersplit_ = usertempid_.substring(0, 5);
  338. if (usersplit_ === 'other') {
  339. this.index_ = this.index_ + 1;
  340. const id_ = 'othe-video-' + this.index_;
  341. const ovid = usertempid_.substring(5);
  342. if (this.index_ === 1) {
  343. this.ov1 = ovid;
  344. } else if (this.index_ === 2) {
  345. this.ov2 = ovid;
  346. } else if (this.index_ === 3) {
  347. this.ov3 = ovid;
  348. } else if (this.index_ === 4) {
  349. this.ov4 = ovid;
  350. } else if (this.index_ === 5) {
  351. this.ov5 = ovid;
  352. } else if (this.index_ === 6) {
  353. this.ov6 = ovid;
  354. } else if (this.index_ === 7) {
  355. this.ov7 = ovid;
  356. }
  357. this.ov1 = '';
  358. remoteStream.play(id_);
  359. } else if (usersplit_ === 'wxxcx') {
  360. const id_ = 'look-video-' + usertempid_;
  361. remoteStream.play(id_);
  362. }
  363. }
  364. });
  365. // 监听远端流增加事件
  366. this.client_.on('stream-added', event => {
  367. const remoteStream = event.stream;
  368. console.log('222' + remoteStream.getType());
  369. // 订阅远端音频和视频流
  370. this.client_.subscribe(remoteStream, { audio: true, video: true }).catch(e => {
  371. console.error('failed to subscribe remoteStream');
  372. });
  373. });
  374. this.client_.on('stream-removed', event => {
  375. const remoteStream = event.stream;
  376. console.log('stop----');
  377. const usertempid_ = remoteStream.getUserId();
  378. if (usertempid_) {
  379. const usersplit_ = usertempid_.substring(0, 5);
  380. if (usersplit_ === 'other') {
  381. this.index_ = this.index_ - 1;
  382. }
  383. }
  384. remoteStream.stop();
  385. });
  386. this.client_.on('mute-video', event => {
  387. const remoteStream = event.stream;
  388. // 订阅远端音频和视频流
  389. const usertempid_ = remoteStream.getUserId();
  390. if (usertempid_) {
  391. const usersplit_ = usertempid_.substring(0, 4);
  392. if (usersplit_ === 'othe') {
  393. this.index_ = this.index_ - 1;
  394. }
  395. }
  396. });
  397. },
  398. async shareon() {
  399. const shareId = 'share-' + this.userId_;
  400. this.shareid = shareId;
  401. const res = await this.gensignFetch({ userid: shareId });
  402. if (this.$checkRes(res)) {
  403. const shareClient = TRTC.createClient({
  404. mode: 'videoCall',
  405. sdkAppId: this.sdkAppId_,
  406. userId: shareId,
  407. userSig: res.data,
  408. });
  409. shareClient.setDefaultMuteRemoteStreams(true);
  410. await shareClient.join({ roomId: this.name });
  411. const localStream = TRTC.createStream({ audio: false, screen: true });
  412. //localStream.setScreenProfile({ width: 200, height: 200, float: 'left', frameRate: 5, bitrate: 1600 /* kbps */ });
  413. await localStream.initialize();
  414. console.log('initialize share stream success');
  415. await shareClient.publish(localStream);
  416. this.client_.on('stream-added', event => {
  417. const remoteStream = event.stream;
  418. const remoteUserId = remoteStream.getUserId();
  419. if (remoteUserId === shareId) {
  420. // 取消订阅自己的屏幕分享流
  421. this.client_.unsubscribe(remoteStream);
  422. } else {
  423. // 订阅其他一般远端流
  424. this.client_.subscribe(remoteStream);
  425. }
  426. });
  427. }
  428. },
  429. async liveclose() {
  430. // 关闭视频通话
  431. console.log(this.open_);
  432. if (this.open_) {
  433. const videoTrack = this.localStream_.getVideoTrack();
  434. if (videoTrack) {
  435. this.localStream_.removeTrack(videoTrack).then(() => {
  436. console.log('remove video call success');
  437. // 关闭摄像头
  438. videoTrack.stop();
  439. this.client_.unpublish(this.localStream_).then(() => {
  440. // 取消发布本地流成功
  441. });
  442. });
  443. }
  444. }
  445. },
  446. async roomAnchorid(type) {
  447. const data = {};
  448. data.roomid = this.id;
  449. if (type === 'othe1') {
  450. data.otheid = this.ov1;
  451. } else if (type === 'othe2') {
  452. data.otheid = this.ov2;
  453. } else if (type === 'othe3') {
  454. data.otheid = this.ov3;
  455. } else if (type === 'othe4') {
  456. data.otheid = this.ov4;
  457. } else if (type === 'othe5') {
  458. data.otheid = this.ov5;
  459. } else if (type === 'othe6') {
  460. data.otheid = this.ov6;
  461. } else if (type === 'othe7') {
  462. data.otheid = this.ov7;
  463. }
  464. const res = await this.updateanchor(data);
  465. if (this.$checkRes(res)) {
  466. this.$message({
  467. message: '操作成功',
  468. type: 'success',
  469. });
  470. }
  471. },
  472. async cameraChange() {
  473. //await this.localStream_.switchDevice('video', this.cameraId);
  474. },
  475. async micrChange() {
  476. //await this.localStream_.switchDevice('audio', this.microphoneId);
  477. },
  478. // 选择打开摄像头
  479. shexiangBtn() {
  480. this.shexiangDia = true;
  481. },
  482. // 选择打开麦克风
  483. tianchongBtn() {
  484. this.tianchongDia = true;
  485. },
  486. chatBtn() {
  487. this.chatDia = true;
  488. },
  489. async queBtn() {
  490. this.queDia = true;
  491. this.questSearch();
  492. },
  493. async queCloseBtn() {
  494. // 关闭问卷
  495. const data = {};
  496. data.roomid = this.id;
  497. const res = await this.roomquestclose(data);
  498. if (this.$checkRes(res)) {
  499. this.$message({
  500. message: '操作成功',
  501. type: 'success',
  502. });
  503. }
  504. },
  505. forId(itemid) {
  506. return 'look-video-wxxcx-' + itemid;
  507. },
  508. // 关闭摄像头&麦克风
  509. handleClose(done) {
  510. done();
  511. },
  512. },
  513. computed: {
  514. ...mapState(['user']),
  515. id() {
  516. return this.$route.query.id;
  517. },
  518. name() {
  519. return this.$route.query.name;
  520. },
  521. anchorid() {
  522. return this.$route.query.anchorid;
  523. },
  524. pageTitle() {
  525. return `${this.$route.meta.title}`;
  526. },
  527. },
  528. metaInfo() {
  529. return { title: this.$route.meta.title };
  530. },
  531. };
  532. </script>
  533. <style lang="less" scoped>
  534. .info {
  535. background-color: #2a2b30;
  536. min-height: 840px;
  537. .left {
  538. .leftTop {
  539. min-height: 540px;
  540. padding: 0 15px;
  541. margin: 20px 0;
  542. p {
  543. color: #ccc;
  544. }
  545. p:nth-child(2) {
  546. padding: 10px 0;
  547. }
  548. p:nth-child(3) {
  549. line-height: 25px;
  550. }
  551. }
  552. .leftDown {
  553. padding: 10px 0 0 0;
  554. border-top: 1px solid #000;
  555. .btn {
  556. margin: 0 0 15px 0;
  557. color: #cccccc;
  558. text-align: center;
  559. }
  560. .btn:hover {
  561. cursor: pointer;
  562. }
  563. }
  564. }
  565. .right {
  566. .rightTop {
  567. height: 100px;
  568. background-color: #232428;
  569. text-align: right;
  570. color: #ccc;
  571. line-height: 100px;
  572. span {
  573. margin: 0 10px 0 0;
  574. }
  575. span:hover {
  576. cursor: pointer;
  577. color: #409eff;
  578. }
  579. }
  580. .video {
  581. min-height: 640px;
  582. background-color: #000;
  583. overflow: hidden;
  584. position: relative;
  585. .videoMeet {
  586. .one {
  587. height: 480px;
  588. overflow: hidden;
  589. }
  590. .two {
  591. height: 480px;
  592. overflow: hidden;
  593. .twoOne {
  594. height: 160px;
  595. overflow: hidden;
  596. }
  597. }
  598. .three {
  599. height: 160px;
  600. overflow: hidden;
  601. }
  602. }
  603. }
  604. .noVideo {
  605. min-height: 640px;
  606. background-color: #151618;
  607. }
  608. .rightDown {
  609. height: 100px;
  610. background-color: #232428;
  611. }
  612. }
  613. }
  614. /deep/.el-dialog__body {
  615. min-height: 100px;
  616. }
  617. #main-video {
  618. float: left;
  619. width: 100%;
  620. height: 640px;
  621. min-height: 600px;
  622. grid-area: 1/1/3/4;
  623. }
  624. .chatList {
  625. height: 400px;
  626. padding: 5px 5px 5px 0px;
  627. overflow-y: auto;
  628. .list {
  629. margin: 0 0 10px 0;
  630. span:first-child {
  631. float: left;
  632. width: 15%;
  633. text-align: center;
  634. overflow: hidden;
  635. text-overflow: ellipsis;
  636. white-space: nowrap;
  637. // font-weight: bold;
  638. }
  639. span:last-child {
  640. float: right;
  641. width: 84%;
  642. }
  643. }
  644. }
  645. .chatInput {
  646. position: absolute;
  647. bottom: 0;
  648. .el-button {
  649. width: 100%;
  650. padding: 20px 0;
  651. }
  652. }
  653. .sudoku_row {
  654. display: flex;
  655. align-items: center;
  656. width: 100%;
  657. height: 430px;
  658. flex-wrap: wrap;
  659. overflow-y: auto;
  660. }
  661. .sudoku_item {
  662. display: flex;
  663. justify-content: center;
  664. align-items: center;
  665. flex-direction: column;
  666. width: 30%;
  667. padding: 10px 10px 10px 10px;
  668. }
  669. .lookvideo {
  670. width: 100%;
  671. height: 160px;
  672. min-height: 160px;
  673. background-color: black;
  674. grid-area: 1/1/3/4;
  675. }
  676. </style>