play.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849
  1. <template>
  2. <div class="pc-player-container">
  3. <div class="pc-player-wrapper">
  4. <div class="pc-player-left">
  5. <div class="video-section">
  6. <div class="video-title">{{ media.name || tpl.name }}</div>
  7. <div class="video-player-wrap">
  8. <video-player id="myVideo1" ref="videoPlayer" :playsinline="true" @pause="onPlayerPause($event)"
  9. @timeupdate="onPlayerTimeupdate($event)" @play="onPlayerStart($event)" @ready="playerReadied"
  10. @ended="onPlayerEnded($event)" @error="onPlayerError($event)"
  11. :globalOptions="{controls:true}" :options="options">
  12. </video-player>
  13. </div>
  14. <div class="dialog-footer pt30">
  15. <el-row class="media-footer">
  16. <el-col :span="6" class="media-time">
  17. <span>{{curTimes|useTime}}</span>
  18. <strong>/</strong>
  19. <span>{{media.duration|useTime}}</span>
  20. </el-col>
  21. <el-col :span="12" class="media-center">
  22. <span class="volume-control">
  23. <span class="vol-icon" @click="toggleMute">音量</span>
  24. <el-slider class="volume-slider" :value="volume" :min="0" :max="1" :step="0.05"
  25. @input="setVolume"></el-slider>
  26. </span>
  27. </el-col>
  28. <el-col :span="6" class="media-select">
  29. <div style="margin-top:0px;float: right;">
  30. <el-select placeholder="流畅" :value="mediaType" class="media-el-select" @change="changeMedia">
  31. <el-option label="流畅" value="ld"></el-option>
  32. <el-option label="标清" value="hls"></el-option>
  33. </el-select>
  34. </div>
  35. </el-col>
  36. </el-row>
  37. </div>
  38. </div>
  39. </div>
  40. <div class="panel-toggle" @click="showPanel = !showPanel">
  41. <i :class="showPanel ? 'el-icon-d-arrow-right' : 'el-icon-d-arrow-left'"></i>
  42. </div>
  43. <div class="pc-player-right" v-show="showPanel">
  44. <div class="playlist-header">
  45. <div class="playlist-title">
  46. <i class="el-icon-video-list"></i>
  47. 课程列表
  48. </div>
  49. <div class="playlist-header-row">
  50. <div class="playlist-count">
  51. 共 {{ list.length }} 个视频
  52. <span class="finished-count" v-if="finishedCount">,已完成 {{ finishedCount }} 个</span>
  53. </div>
  54. <el-switch
  55. v-model="hideFinished"
  56. active-text="隐藏已完成"
  57. inactive-text=""
  58. size="small"
  59. ></el-switch>
  60. </div>
  61. </div>
  62. <div class="playlist-body">
  63. <div class="playlist-item" v-for="(item, index) in filteredList" :key="item.id" :class="{
  64. 'is-active': media.id === item.id,
  65. 'is-finished': item.isFinish == 1,
  66. 'is-current-playing': media.id === item.id && onPlay
  67. }" @click="loadMedia(item)">
  68. <div class="item-index" :class="{ 'active-index': media.id === item.id }">
  69. <span v-if="item.isFinish == 1" class="finish-badge">
  70. <i class="el-icon-check"></i>
  71. </span>
  72. <span v-else>{{ index + 1 }}</span>
  73. </div>
  74. <div class="item-info">
  75. <div class="item-name">{{ item.name }}</div>
  76. <div class="item-meta">
  77. <span class="item-duration">学时:{{ item.xs / 10 }}</span>
  78. <span class="item-progress">{{item|percent}}%</span>
  79. </div>
  80. </div>
  81. <div class="item-action">
  82. <el-button v-if="media.id === item.id && onPlay" type="warning" size="mini" icon="el-icon-video-pause"
  83. circle @click.stop="doPause"></el-button>
  84. <el-button v-else-if="media.id === item.id" type="primary" size="mini" icon="el-icon-video-play" circle
  85. @click.stop="doPlay"></el-button>
  86. <el-button v-else type="text" size="mini" icon="el-icon-video-play"
  87. @click.stop="loadMedia(item)">播放</el-button>
  88. </div>
  89. </div>
  90. <div class="playlist-empty" v-if="list.length === 0">
  91. <i class="el-icon-document"></i>
  92. <p>暂无视频</p>
  93. </div>
  94. </div>
  95. </div>
  96. </div>
  97. </div>
  98. </template>
  99. <script>
  100. import { mapActions } from "vuex";
  101. import {
  102. getPercent
  103. } from '@/utils/index.js'
  104. import {
  105. videoPlayer
  106. } from 'vue-video-player';
  107. import 'video.js/dist/video-js.css'
  108. import {
  109. httpServer
  110. } from "@/components/httpServer/httpServer.js";
  111. export default {
  112. data() {
  113. return {
  114. courseId: 0,
  115. mediaId: 0,
  116. media: {},
  117. options: {
  118. controls: true,
  119. autoplay: true,
  120. muted: true,
  121. loop: false,
  122. preload: "auto",
  123. language: 'zh-CN',
  124. aspectRatio: '16:9',
  125. fluid: true,
  126. sources: [],
  127. poster: '',
  128. notSupportedMessage: '无法播放媒体源',
  129. playtimes: '',
  130. controlBar: {
  131. timeDivider: true,
  132. durationDisplay: true,
  133. remainingTimeDisplay: false,
  134. fullscreenToggle: true
  135. }
  136. },
  137. curTimes: 0,
  138. tpl: {},
  139. info: {isBan:0},
  140. list: [],
  141. timer: false,
  142. tickNum: 0,
  143. prevTime: 0,
  144. isReady: false,
  145. onPlay: false,
  146. volume: 0.5,
  147. isMuted: false,
  148. mediaType: 'hls',
  149. hasSeeked: false,
  150. hideFinished: false,
  151. showPanel: true
  152. };
  153. },
  154. components: {
  155. videoPlayer
  156. },
  157. computed: {
  158. finishedCount() {
  159. return this.list.filter(i => i.isFinish == 1).length;
  160. },
  161. filteredList() {
  162. if (this.hideFinished) {
  163. return this.list.filter(i => i.isFinish != 1)
  164. }
  165. return this.list
  166. },
  167. playPercent() {
  168. if (!this.media.duration) return 0;
  169. if(this.media.isFinish ==1) return 100;
  170. return +((this.curTimes / this.media.duration) * 100).toFixed(1);
  171. }
  172. },
  173. filters: {
  174. percent(item){
  175. if( item.isFinish == 1) return 100;
  176. return (item.position * 100 / item.duration).toFixed(1)
  177. },
  178. useTime(val) {
  179. val = parseInt(val) || 0
  180. let timestr = ""
  181. let hour = parseInt(val / 3600);
  182. let min = parseInt(val / 60 % 60);
  183. let sec = parseInt(val % 60);
  184. if (hour < 10) hour = "0" + hour;
  185. if (min < 10) min = "0" + min;
  186. if (sec < 10) sec = "0" + sec;
  187. return hour + ":" + min + ":" + sec
  188. }
  189. },
  190. methods: {
  191. getCourse() {
  192. let param = {
  193. courseId: this.courseId
  194. }
  195. httpServer("course.getCourse", param).then(res => {
  196. if (res.code == 200) {
  197. let {
  198. info,
  199. extra,
  200. list,
  201. tpl
  202. } = res.data;
  203. this.info = Object.assign(info, extra || {});
  204. this.tpl = tpl || {};
  205. this.list = list.map((item) => {
  206. item.percent = getPercent(item) || 0;
  207. return item;
  208. })
  209. let target = null
  210. if (this.mediaId) {
  211. target = this.list.find(i => i.id == this.mediaId || i.mediaId == this.mediaId)
  212. }
  213. if (!target) {
  214. target = this.list.find(i => i.isFinish != 1)
  215. }
  216. if (!target && this.list.length) {
  217. target = this.list[0]
  218. }
  219. if (target) {
  220. this.loadMedia(target)
  221. }
  222. }
  223. })
  224. },
  225. setVolume(val) {
  226. this.volume = val
  227. this.isMuted = val === 0
  228. let player = this.$refs.videoPlayer && this.$refs.videoPlayer.player
  229. if (player && player.volume) {
  230. player.volume(val)
  231. player.muted(this.isMuted)
  232. }
  233. },
  234. changeMedia(val) {
  235. if (!val || !this.mediaUrl) return;
  236. let mediaUrl = this.mediaUrl;
  237. if (val == 'ld') {
  238. mediaUrl = mediaUrl.replace('/hls/', '/ld/');
  239. } else {
  240. mediaUrl = mediaUrl.replace('/ld/', '/hls/');
  241. }
  242. this.mediaType = val;
  243. this.stopTick()
  244. this.onPlay = false
  245. this.curTimes = 0
  246. this.hasSeeked = false
  247. this.options = {
  248. ...this.options,
  249. sources: [{ src: mediaUrl, type: "application/x-mpegURL" }],
  250. playtimes: this.media.position || 0,
  251. autoplay: true
  252. }
  253. },
  254. startTick() {
  255. let tick = this.tryTick;
  256. if (this.timer) clearInterval(this.timer);
  257. this.timer = setInterval(tick, 5 * 1000);
  258. },
  259. stopTick() {
  260. if (this.timer) clearInterval(this.timer);
  261. },
  262. tryTick() {
  263. let that = this;
  264. try {
  265. that.tick()
  266. } catch (err) {
  267. that.reportErr("play", '' + err.message)
  268. }
  269. },
  270. playerReadied(audio) {
  271. this.isReady = true
  272. },
  273. onPlayerError(event) {
  274. console.error("Video error:", event)
  275. },
  276. onPlayerTimeupdate(player) {
  277. let curTimes = player.currentTime();
  278. this.curTimes = curTimes || 0
  279. if (this.media.isFinish) {
  280. return;
  281. }
  282. },
  283. safePlay(player) {
  284. if (!player) return
  285. try {
  286. const p = player.play()
  287. if (p && p.catch) p.catch(() => {})
  288. } catch (e) {}
  289. },
  290. setposition(position) {
  291. if (position > this.media.duration) position = this.media.duration;
  292. let player = this.$refs.videoPlayer.player;
  293. player.currentTime(position);
  294. this.safePlay(player)
  295. if (this.media.isFinish) return;
  296. if (this.media.position >= this.media.duration - 10 && !this.media.isFinish) {
  297. this.tick(true)
  298. }
  299. },
  300. onPlayerPause(event) {
  301. this.reportErr("play", 'pause');
  302. this.stopTick()
  303. this.onPlay = false
  304. },
  305. onPlayerEnded(event) {
  306. this.reportErr("play", 'end');
  307. this.tick(true)
  308. this.playNext()
  309. },
  310. playNext() {
  311. let currentIndex = this.list.findIndex(i => i.id === this.media.id)
  312. let nextItem = null
  313. for (let i = currentIndex + 1; i < this.list.length; i++) {
  314. if (this.list[i].isFinish != 1) {
  315. nextItem = this.list[i]
  316. break
  317. }
  318. }
  319. if (!nextItem) {
  320. for (let i = 0; i < currentIndex; i++) {
  321. if (this.list[i].isFinish != 1) {
  322. nextItem = this.list[i]
  323. break
  324. }
  325. }
  326. }
  327. if (nextItem) {
  328. this.$message.success(`即将播放: ${nextItem.name}`)
  329. setTimeout(() => {
  330. this.loadMedia(nextItem)
  331. }, 1000)
  332. }
  333. },
  334. onClose() {
  335. this.reportErr("play", 'close')
  336. this.doPause()
  337. },
  338. doPause() {
  339. this.stopTick()
  340. this.onPlay = false
  341. let myPlayer = this.$refs.videoPlayer.player;
  342. myPlayer && myPlayer.pause()
  343. },
  344. doPlay() {
  345. this.onPlay = true
  346. this.startTick();
  347. if (!this.$refs.videoPlayer || !this.$refs.videoPlayer.player) return;
  348. this.safePlay(this.$refs.videoPlayer.player)
  349. this.tickNum = 0
  350. },
  351. onPlayerStart() {
  352. this.reportErr("play", 'start');
  353. this.startTick()
  354. this.onPlay = true
  355. if (!this.hasSeeked && this.media.position > 5 && this.media.position < this.media.duration - 10) {
  356. this.hasSeeked = true
  357. let player = this.$refs.videoPlayer && this.$refs.videoPlayer.player
  358. if (player && player.currentTime) {
  359. player.currentTime(this.media.position)
  360. }
  361. }
  362. },
  363. toggleMute() {
  364. this.isMuted = !this.isMuted
  365. let player = this.$refs.videoPlayer && this.$refs.videoPlayer.player
  366. if (player && player.volume) {
  367. player.muted(this.isMuted)
  368. }
  369. },
  370. reportErr(action, msg) {
  371. httpServer("course.report", { action, msg }, true)
  372. },
  373. tick(force = false) {
  374. let media = this.media;
  375. this.tickNum++
  376. if (this.media.isFinish) {
  377. return;
  378. }
  379. let myPlayer = this.$refs.videoPlayer.player;
  380. if (!myPlayer) return;
  381. let curTimes = parseInt(myPlayer.currentTime());
  382. if (curTimes < 4) {
  383. return;
  384. }
  385. if (this.media.position > curTimes + 5 && !force) return;
  386. let isFinish = force ? 1 : 0
  387. if (curTimes >= media.duration) isFinish = 1;
  388. if (!isFinish) {
  389. if (!this.onPlay) return;
  390. }
  391. if (window.navigator.webdriver) {
  392. this.reportErr("play", 'webdriver');
  393. this.doPause()
  394. return
  395. }
  396. let param = {
  397. id: media.id,
  398. position: curTimes,
  399. isFinish
  400. };
  401. httpServer("course.tick", param, true).then(res => {
  402. if (res.code == 200) {
  403. let {
  404. skip,
  405. position,
  406. pause,
  407. closed
  408. } = res.data
  409. if (pause || closed) {
  410. this.doPause();
  411. if (closed) {
  412. this.$message.errorMsg("禁止多处同时学习", 5);
  413. } else if (pause) {
  414. this.$message.errorMsg("禁止多处同时学习", 5);
  415. }
  416. return
  417. }
  418. if (!skip) {
  419. let curTimes = parseInt(myPlayer.currentTime())
  420. if (position < curTimes + 5) {
  421. this.setposition(position)
  422. }
  423. }
  424. Object.assign(this.media, res.data)
  425. }
  426. })
  427. },
  428. loadMedia(item) {
  429. this.media = item;
  430. httpServer('course.GetMedia', {
  431. id: item.id
  432. }).then(res => {
  433. if (res.code != 200) return;
  434. let {
  435. mediaUrl,
  436. id,
  437. position,
  438. marks,
  439. duration
  440. } = res.data || {};
  441. this.mediaUrl = mediaUrl;
  442. this.media.position = position;
  443. this.media.duration = duration;
  444. this.media.id = id;
  445. this.curTimes = 0
  446. this.onPlay = false
  447. this.hasSeeked = false
  448. this.stopTick()
  449. this.options = {
  450. ...this.options,
  451. sources: [{
  452. src: mediaUrl,
  453. type: "application/x-mpegURL"
  454. }],
  455. playtimes: position || 1,
  456. autoplay: true
  457. }
  458. });
  459. }
  460. },
  461. created() {
  462. let { uid, token, courseId, mediaId } = this.$route.query
  463. this.courseId = +courseId || +this.$route.params.courseId
  464. this.mediaId = mediaId || this.$route.query.mediaId || 0
  465. if (uid) localStorage.uid = uid
  466. if (token) localStorage.token = token
  467. if (this.courseId) {
  468. this.getCourse()
  469. }
  470. },
  471. beforeDestroy() {
  472. this.stopTick()
  473. this.reportErr("play", 'destroy')
  474. }
  475. }
  476. </script>
  477. <style>
  478. @import url("./play.css");
  479. .pc-player-container {
  480. min-height: 100vh;
  481. background: #f0f2f5;
  482. padding: 20px;
  483. box-sizing: border-box;
  484. }
  485. .pc-player-wrapper {
  486. max-width: 1440px;
  487. margin: 0 auto;
  488. display: flex;
  489. gap: 20px;
  490. height: calc(100vh - 40px);
  491. }
  492. .pc-player-left {
  493. flex: 1;
  494. min-width: 0;
  495. display: flex;
  496. flex-direction: column;
  497. }
  498. .panel-toggle {
  499. flex-shrink: 0;
  500. width: 20px;
  501. display: flex;
  502. align-items: center;
  503. justify-content: center;
  504. cursor: pointer;
  505. color: #999;
  506. font-size: 14px;
  507. user-select: none;
  508. transition: color 0.2s;
  509. }
  510. .panel-toggle:hover {
  511. color: #409EFF;
  512. }
  513. .video-section {
  514. background: #fff;
  515. border-radius: 12px;
  516. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
  517. overflow: hidden;
  518. }
  519. .video-title {
  520. padding: 16px 20px;
  521. font-size: 16px;
  522. font-weight: 600;
  523. color: #1a1a2e;
  524. border-bottom: 1px solid #f0f0f0;
  525. white-space: nowrap;
  526. overflow: hidden;
  527. text-overflow: ellipsis;
  528. }
  529. .video-player-wrap {
  530. width: 100%;
  531. background: #000;
  532. touch-action: none;
  533. }
  534. .video-player-wrap .video-js {
  535. width: 100%;
  536. }
  537. .video-info-bar {
  538. padding: 12px 20px;
  539. display: flex;
  540. align-items: center;
  541. gap: 16px;
  542. border-top: 1px solid #f0f0f0;
  543. }
  544. .video-time {
  545. font-size: 14px;
  546. color: #666;
  547. white-space: nowrap;
  548. font-variant-numeric: tabular-nums;
  549. }
  550. .time-current {
  551. color: #409EFF;
  552. font-weight: 600;
  553. }
  554. .time-sep {
  555. margin: 0 4px;
  556. color: #ccc;
  557. }
  558. .video-progress {
  559. flex: 1;
  560. min-width: 100px;
  561. }
  562. .video-status {
  563. flex-shrink: 0;
  564. }
  565. .pc-player-right {
  566. width: 380px;
  567. flex-shrink: 0;
  568. background: #fff;
  569. border-radius: 12px;
  570. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
  571. display: flex;
  572. flex-direction: column;
  573. overflow: hidden;
  574. }
  575. .playlist-header {
  576. padding: 16px 20px 12px;
  577. border-bottom: 1px solid #f0f0f0;
  578. flex-shrink: 0;
  579. }
  580. .playlist-header-row {
  581. display: flex;
  582. align-items: center;
  583. justify-content: space-between;
  584. margin-top: 6px;
  585. }
  586. .playlist-title {
  587. font-size: 16px;
  588. font-weight: 600;
  589. color: #1a1a2e;
  590. margin-bottom: 4px;
  591. }
  592. .playlist-title i {
  593. margin-right: 6px;
  594. color: #409EFF;
  595. }
  596. .playlist-count {
  597. font-size: 12px;
  598. color: #999;
  599. }
  600. .finished-count {
  601. color: #67C23A;
  602. }
  603. .playlist-body {
  604. flex: 1;
  605. overflow-y: auto;
  606. padding: 8px 0;
  607. }
  608. .playlist-body::-webkit-scrollbar {
  609. width: 6px;
  610. }
  611. .playlist-body::-webkit-scrollbar-thumb {
  612. background: #ddd;
  613. border-radius: 3px;
  614. }
  615. .playlist-body::-webkit-scrollbar-track {
  616. background: transparent;
  617. }
  618. .playlist-item {
  619. display: flex;
  620. align-items: center;
  621. padding: 12px 20px;
  622. cursor: pointer;
  623. transition: all 0.2s ease;
  624. border-left: 3px solid transparent;
  625. gap: 12px;
  626. }
  627. .playlist-item:hover {
  628. background: #f5f7fa;
  629. }
  630. .playlist-item.is-active {
  631. background: #ecf5ff;
  632. border-left-color: #409EFF;
  633. }
  634. .playlist-item.is-finished {
  635. opacity: 0.65;
  636. }
  637. .playlist-item.is-current-playing {
  638. background: #ecf5ff;
  639. border-left-color: #409EFF;
  640. }
  641. .item-index {
  642. width: 28px;
  643. height: 28px;
  644. border-radius: 50%;
  645. background: #f0f2f5;
  646. display: flex;
  647. align-items: center;
  648. justify-content: center;
  649. font-size: 12px;
  650. font-weight: 600;
  651. color: #666;
  652. flex-shrink: 0;
  653. transition: all 0.2s;
  654. }
  655. .item-index.active-index {
  656. background: #409EFF;
  657. color: #fff;
  658. }
  659. .finish-badge {
  660. color: #67C23A;
  661. font-size: 14px;
  662. }
  663. .item-info {
  664. flex: 1;
  665. min-width: 0;
  666. }
  667. .item-name {
  668. font-size: 14px;
  669. color: #333;
  670. font-weight: 500;
  671. white-space: nowrap;
  672. overflow: hidden;
  673. text-overflow: ellipsis;
  674. margin-bottom: 2px;
  675. }
  676. .is-finished .item-name {
  677. color: #999;
  678. text-decoration: line-through;
  679. }
  680. .is-active .item-name {
  681. color: #409EFF;
  682. }
  683. .item-meta {
  684. display: flex;
  685. gap: 12px;
  686. font-size: 12px;
  687. color: #999;
  688. }
  689. .item-progress {
  690. color: #409EFF;
  691. }
  692. .is-finished .item-progress {
  693. color: #67C23A;
  694. }
  695. .item-action {
  696. flex-shrink: 0;
  697. }
  698. .playlist-empty {
  699. text-align: center;
  700. padding: 60px 20px;
  701. color: #ccc;
  702. }
  703. .playlist-empty i {
  704. font-size: 48px;
  705. display: block;
  706. margin-bottom: 12px;
  707. }
  708. .playlist-empty p {
  709. font-size: 14px;
  710. margin: 0;
  711. }
  712. .dialog-footer {
  713. padding: 0 16px;
  714. border-top: 1px solid #f0f0f0;
  715. background: #fafafa;
  716. }
  717. .dialog-footer.pt30 {
  718. padding: 8px 16px;
  719. }
  720. .media-footer {
  721. display: flex;
  722. align-items: center;
  723. line-height: 1;
  724. }
  725. .media-time {
  726. font-size: 14px;
  727. color: #666;
  728. white-space: nowrap;
  729. font-variant-numeric: tabular-nums;
  730. display: flex;
  731. align-items: center;
  732. gap: 4px;
  733. }
  734. .media-center {
  735. display: flex;
  736. align-items: center;
  737. justify-content: center;
  738. }
  739. .bicon {
  740. font-size: 22px !important;
  741. padding: 4px !important;
  742. }
  743. .volume-control {
  744. display: inline-flex;
  745. align-items: center;
  746. gap: 6px;
  747. }
  748. .vol-icon {
  749. font-size: 12px;
  750. font-weight: 600;
  751. cursor: pointer;
  752. color: #606266;
  753. user-select: none;
  754. line-height: 1;
  755. }
  756. .vol-icon:hover {
  757. color: #409EFF;
  758. }
  759. .volume-slider {
  760. width: 300px !important;
  761. }
  762. .volume-slider .el-slider__runway {
  763. height: 4px;
  764. }
  765. .volume-slider .el-slider__bar {
  766. height: 4px;
  767. }
  768. .volume-slider .el-slider__button {
  769. width: 12px;
  770. height: 12px;
  771. }
  772. .media-select {
  773. display: flex;
  774. align-items: center;
  775. justify-content: flex-end;
  776. }
  777. .media-el-select {
  778. width: 80px;
  779. }
  780. .media-el-select .el-input__inner {
  781. height: 28px;
  782. line-height: 28px;
  783. font-size: 12px;
  784. }
  785. @media (max-width: 900px) {
  786. .pc-player-wrapper {
  787. flex-direction: column;
  788. height: auto;
  789. }
  790. .pc-player-right {
  791. width: 100%;
  792. max-height: 400px;
  793. }
  794. .pc-player-container {
  795. padding: 12px;
  796. }
  797. }
  798. </style>