enjoyPlayer.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  1. <template>
  2. <div
  3. class="enjoyPlayer"
  4. v-click-outside="handleListOutside"
  5. :style="{
  6. transform: `scale(${scaleStyle})`
  7. }"
  8. >
  9. <div v-if="!isBase" v-show="isShowEnjoyPlayerList" class="enjoyPlayerList">
  10. <div class="titNameCon">
  11. <div class="titName">
  12. <div class="tit">{{ `音频列表 (${elementInfo.enjoyList?.length || 0})` }}</div>
  13. </div>
  14. </div>
  15. <div class="enjoyPlayerListCon">
  16. <draggable
  17. :forceFallback="true"
  18. :handle="'.drag-handle_enjoyPlayer'"
  19. v-model="elementInfo.enjoyList"
  20. itemKey="id"
  21. :animation="200"
  22. :scroll="true"
  23. :scrollSensitivity="50"
  24. @end="handleDragEnd"
  25. >
  26. <template #item="{ element }">
  27. <div class="playerItem" :class="{ active: element.id === elementInfo.sid }" @click="handlePlayMusic(element)">
  28. <div class="itemLeft">
  29. {{ element.title }}
  30. </div>
  31. <div class="itemRight">
  32. <template v-if="!isScreening">
  33. <div class="itemListBtn drag-handle_enjoyPlayer" @click.stop></div>
  34. <div class="itemCloseBtn" @click.stop="handleEnjoyDel(element.id)"></div>
  35. </template>
  36. </div>
  37. </div>
  38. </template>
  39. </draggable>
  40. </div>
  41. </div>
  42. <audio
  43. v-if="!isBase"
  44. ref="audioRef"
  45. :src="elementInfo.src"
  46. @durationchange="handleDurationchange()"
  47. @timeupdate="handleTimeupdate()"
  48. @play="handlePlayed()"
  49. @pause="handlePaused()"
  50. @ended="handleEnded()"
  51. @progress="handleProgress()"
  52. @error="handleError()"
  53. ></audio>
  54. <div class="playerCon">
  55. <img class="tipImg" src="./imgs/tip.png" alt="" />
  56. <div class="operateBtn" :class="{ paused: paused }" @click="toggle"></div>
  57. <div class="operateMidCon">
  58. <div class="titleCon">
  59. <div class="title">{{ elementInfo.title }}</div>
  60. <div class="timesCon">
  61. {{ `${ptime}/${dtime}` }}
  62. </div>
  63. </div>
  64. <div class="bar-wrap" ref="playBarWrap" @mousedown="handleMousedownPlayBar()" @touchstart="handleMousedownPlayBar()">
  65. <div class="bar">
  66. <div class="loaded" :style="{ width: loadedBarWidth }"></div>
  67. <div class="played" :style="{ width: playedBarWidth }">
  68. <div class="thumb"></div>
  69. </div>
  70. </div>
  71. </div>
  72. </div>
  73. <div class="operateRightBtn">
  74. <div class="preBtn" @click="handleChangeMusic('pre')"></div>
  75. <div class="nextBtn" @click="handleChangeMusic('next')"></div>
  76. <div class="listBtn" @click="hanleEnjoyList"></div>
  77. </div>
  78. </div>
  79. </div>
  80. </template>
  81. <script setup lang="ts">
  82. import { computed, ref, onUnmounted } from "vue"
  83. import message from "@/utils/message"
  84. import type { PPTEnjoyElement } from "@/types/slides"
  85. import { useSlidesStore, useMainStore } from "@/store"
  86. import draggable from "vuedraggable"
  87. import queryParams from "@/queryParams"
  88. const slidesStore = useSlidesStore()
  89. const mainStore = useMainStore()
  90. const props = withDefaults(
  91. defineProps<{
  92. scale?: number
  93. elementInfo: PPTEnjoyElement
  94. isScreening?: boolean
  95. isBase?: boolean
  96. }>(),
  97. {
  98. scale: 1,
  99. isScreening: false,
  100. isBase: false
  101. }
  102. )
  103. // 学生端 不是isBase预览下面的缩放比例
  104. const scaleStyle = queryParams.fromType === "CLASS" && !props.isBase && props.isScreening ? 1.5 : 1
  105. function handleListOutside() {
  106. if (!props.isBase && isShowEnjoyPlayerList.value) {
  107. hanleEnjoyList()
  108. }
  109. }
  110. const isShowEnjoyPlayerList = ref(false)
  111. function hanleEnjoyList() {
  112. if (props.isBase) {
  113. return
  114. }
  115. isShowEnjoyPlayerList.value = !isShowEnjoyPlayerList.value
  116. mainStore.setIsPPTWheelPageState(!isShowEnjoyPlayerList.value)
  117. }
  118. // 卸载之后让ppt能滚动
  119. onUnmounted(() => {
  120. mainStore.setIsPPTWheelPageState(true)
  121. })
  122. function handleDragEnd() {
  123. slidesStore.updateElement({
  124. id: props.elementInfo.id,
  125. props: {
  126. enjoyList: props.elementInfo.enjoyList
  127. }
  128. })
  129. }
  130. function handleEnjoyDel(id: string) {
  131. if (id === props.elementInfo.sid) {
  132. //删除组件
  133. if (props.elementInfo.enjoyList.length === 1) {
  134. slidesStore.deleteElement(props.elementInfo.id)
  135. } else {
  136. const index = props.elementInfo.enjoyList.findIndex(item => {
  137. return item.id === id
  138. })
  139. // eslint-disable-next-line vue/no-mutating-props
  140. props.elementInfo.enjoyList.splice(index, 1)
  141. const enjoyList = props.elementInfo.enjoyList
  142. slidesStore.updateElement({
  143. id: props.elementInfo.id,
  144. props: {
  145. sid: enjoyList[0].id,
  146. title: enjoyList[0].title,
  147. src: enjoyList[0].src,
  148. enjoyList: enjoyList
  149. }
  150. })
  151. // 关闭播放按钮
  152. paused.value = true
  153. }
  154. } else {
  155. const index = props.elementInfo.enjoyList.findIndex(item => {
  156. return item.id === id
  157. })
  158. // eslint-disable-next-line vue/no-mutating-props
  159. props.elementInfo.enjoyList.splice(index, 1)
  160. slidesStore.updateElement({
  161. id: props.elementInfo.id,
  162. props: {
  163. enjoyList: props.elementInfo.enjoyList
  164. }
  165. })
  166. }
  167. }
  168. function handleChangeMusic(type: "pre" | "next") {
  169. if (props.elementInfo.enjoyList.length === 1) {
  170. seek(0)
  171. } else {
  172. let index = props.elementInfo.enjoyList.findIndex(item => {
  173. return item.id === props.elementInfo.sid
  174. })
  175. index += type === "next" ? 1 : -1
  176. if (index > props.elementInfo.enjoyList.length - 1) {
  177. index = 0
  178. } else if (index < 0) {
  179. index = props.elementInfo.enjoyList.length - 1
  180. }
  181. const enjoyData = props.elementInfo.enjoyList[index]
  182. slidesStore.updateElement({
  183. id: props.elementInfo.id,
  184. props: {
  185. sid: enjoyData.id,
  186. title: enjoyData.title,
  187. src: enjoyData.src
  188. }
  189. })
  190. }
  191. }
  192. function handlePlayMusic(item: Record<string, any>) {
  193. if (item.id === props.elementInfo.sid) {
  194. toggle()
  195. } else {
  196. slidesStore.updateElement({
  197. id: props.elementInfo.id,
  198. props: {
  199. sid: item.id,
  200. title: item.title,
  201. src: item.src
  202. }
  203. })
  204. }
  205. }
  206. const secondToTime = (second = 0) => {
  207. if (second === 0 || isNaN(second)) return "00:00"
  208. const add0 = (num: number) => (num < 10 ? "0" + num : "" + num)
  209. const hour = Math.floor(second / 3600)
  210. const min = Math.floor((second - hour * 3600) / 60)
  211. const sec = Math.floor(second - hour * 3600 - min * 60)
  212. return (hour > 0 ? [hour, min, sec] : [min, sec]).map(add0).join(":")
  213. }
  214. const getBoundingClientRectViewLeft = (element: HTMLElement) => {
  215. return element.getBoundingClientRect().left
  216. }
  217. const audioRef = ref<HTMLAudioElement>()
  218. const playBarWrap = ref<HTMLElement>()
  219. const paused = ref(true)
  220. const currentTime = ref(0)
  221. const duration = ref(0)
  222. const loaded = ref(0)
  223. const isThumbDown = ref(false)
  224. const ptime = computed(() => secondToTime(currentTime.value))
  225. const dtime = computed(() => secondToTime(duration.value))
  226. const playedBarWidth = computed(() => (currentTime.value / duration.value) * 100 + "%")
  227. const loadedBarWidth = computed(() => (loaded.value / duration.value) * 100 + "%")
  228. const seek = (time: number) => {
  229. if (!audioRef.value) return
  230. time = Math.max(time, 0)
  231. time = Math.min(time, duration.value)
  232. audioRef.value.currentTime = time
  233. currentTime.value = time
  234. }
  235. const play = () => {
  236. if (!audioRef.value) return
  237. paused.value = false
  238. audioRef.value.play()
  239. }
  240. const pause = () => {
  241. if (!audioRef.value) return
  242. paused.value = true
  243. audioRef.value.pause()
  244. }
  245. const toggle = () => {
  246. if (paused.value) play()
  247. else pause()
  248. }
  249. const handleDurationchange = () => {
  250. duration.value = audioRef.value?.duration || 0
  251. }
  252. const handleTimeupdate = () => {
  253. currentTime.value = audioRef.value?.currentTime || 0
  254. }
  255. const handlePlayed = () => {
  256. paused.value = false
  257. }
  258. const handlePaused = () => {
  259. paused.value = true
  260. }
  261. const handleEnded = () => {
  262. return
  263. if (isThumbDown.value) {
  264. return
  265. }
  266. seek(0)
  267. play()
  268. }
  269. const handleProgress = () => {
  270. loaded.value = audioRef.value?.buffered.length ? audioRef.value.buffered.end(audioRef.value.buffered.length - 1) : 0
  271. if (!paused.value) {
  272. play()
  273. }
  274. }
  275. const handleError = () => message.error("音频加载失败")
  276. const thumbMove = (e: MouseEvent | TouchEvent) => {
  277. if (!audioRef.value || !playBarWrap.value) return
  278. const clientX = "clientX" in e ? e.clientX : e.changedTouches[0].clientX
  279. let percentage = (clientX - getBoundingClientRectViewLeft(playBarWrap.value)) / playBarWrap.value.clientWidth / props.scale / scaleStyle
  280. percentage = Math.max(percentage, 0)
  281. percentage = Math.min(percentage, 1)
  282. const time = percentage * duration.value
  283. audioRef.value.currentTime = time
  284. currentTime.value = time
  285. }
  286. const thumbUp = (e: MouseEvent | TouchEvent) => {
  287. if (!audioRef.value || !playBarWrap.value) return
  288. const clientX = "clientX" in e ? e.clientX : e.changedTouches[0].clientX
  289. let percentage = (clientX - getBoundingClientRectViewLeft(playBarWrap.value)) / playBarWrap.value.clientWidth / props.scale / scaleStyle
  290. percentage = Math.max(percentage, 0)
  291. percentage = Math.min(percentage, 1)
  292. const time = percentage * duration.value
  293. audioRef.value.currentTime = time
  294. currentTime.value = time
  295. const _time = setTimeout(() => {
  296. clearTimeout(_time)
  297. isThumbDown.value = false
  298. }, 500)
  299. document.removeEventListener("mousemove", thumbMove)
  300. document.removeEventListener("touchmove", thumbMove)
  301. document.removeEventListener("mouseup", thumbUp)
  302. document.removeEventListener("touchend", thumbUp)
  303. }
  304. const handleMousedownPlayBar = () => {
  305. isThumbDown.value = true
  306. document.addEventListener("mousemove", thumbMove)
  307. document.addEventListener("touchmove", thumbMove)
  308. document.addEventListener("mouseup", thumbUp)
  309. document.addEventListener("touchend", thumbUp)
  310. }
  311. </script>
  312. <style lang="scss" scoped>
  313. .enjoyPlayer {
  314. transform-origin: center bottom;
  315. .playerCon {
  316. background: linear-gradient(180deg, #ffffff 0%, #dfdfdf 100%);
  317. box-shadow:
  318. 0px 3px 16px 0px rgba(0, 0, 0, 0.08),
  319. inset 0px -6px 6px 0px rgba(0, 0, 0, 0.08);
  320. border-radius: 112px;
  321. outline: 2px solid rgba(0, 0, 0, 0.03);
  322. padding: 16px 24px;
  323. width: 706px;
  324. display: flex;
  325. align-items: center;
  326. position: relative;
  327. z-index: 1;
  328. .tipImg {
  329. position: absolute;
  330. width: 44px;
  331. height: 12px;
  332. right: 36px;
  333. top: 7px;
  334. }
  335. .operateBtn {
  336. flex-shrink: 0;
  337. background: url("./imgs/pause.png") no-repeat;
  338. background-size: 100% 100%;
  339. width: 70px;
  340. height: 70px;
  341. cursor: pointer;
  342. &.paused {
  343. background: url("./imgs/play.png") no-repeat;
  344. background-size: 100% 100%;
  345. }
  346. }
  347. .operateMidCon {
  348. margin: 0 30px;
  349. flex-grow: 1;
  350. .titleCon {
  351. display: flex;
  352. align-items: center;
  353. justify-content: space-between;
  354. .title {
  355. width: 200px;
  356. font-weight: 600;
  357. font-size: 22px;
  358. color: #131415;
  359. line-height: 30px;
  360. white-space: nowrap;
  361. overflow: hidden;
  362. text-overflow: ellipsis;
  363. }
  364. .timesCon {
  365. flex-shrink: 0;
  366. font-weight: 400;
  367. font-size: 18px;
  368. color: #999999;
  369. line-height: 25px;
  370. }
  371. }
  372. .bar-wrap {
  373. position: relative;
  374. cursor: pointer;
  375. width: 100%;
  376. margin-top: 12px;
  377. .bar {
  378. position: relative;
  379. height: 10px;
  380. width: 100%;
  381. background: #dcdcdc;
  382. box-shadow: inset 0px 2px 3px 0px #a9a9a9;
  383. border-radius: 5px;
  384. outline: 2px solid rgba(255, 255, 255, 0.5);
  385. .loaded {
  386. position: absolute;
  387. left: 0;
  388. top: 0;
  389. bottom: 0;
  390. background: rgba(255, 255, 255, 0.4);
  391. height: 10px;
  392. transition: all 0.5s ease;
  393. will-change: width;
  394. border-radius: 6px;
  395. }
  396. .played {
  397. position: absolute;
  398. left: 0;
  399. top: 0;
  400. bottom: 0;
  401. height: 10px;
  402. will-change: width;
  403. background: linear-gradient(270deg, #97d1fd 0%, #0d93ff 100%);
  404. border-radius: 6px;
  405. .thumb {
  406. position: absolute;
  407. top: -12px;
  408. right: -20px;
  409. cursor: pointer;
  410. width: 38px;
  411. height: 38px;
  412. background: url("./imgs/td.png") no-repeat;
  413. background-size: 100% 100%;
  414. }
  415. }
  416. }
  417. }
  418. }
  419. .operateRightBtn {
  420. flex-shrink: 0;
  421. display: flex;
  422. align-items: center;
  423. .preBtn {
  424. background: url("./imgs/pre.png") no-repeat;
  425. background-size: 100% 100%;
  426. width: 56px;
  427. height: 56px;
  428. margin-right: 20px;
  429. cursor: pointer;
  430. }
  431. .nextBtn {
  432. background: url("./imgs/next.png") no-repeat;
  433. background-size: 100% 100%;
  434. width: 56px;
  435. height: 56px;
  436. margin-right: 20px;
  437. cursor: pointer;
  438. }
  439. .listBtn {
  440. background: url("./imgs/list.png") no-repeat;
  441. background-size: 100% 100%;
  442. width: 56px;
  443. height: 56px;
  444. cursor: pointer;
  445. }
  446. }
  447. }
  448. .enjoyPlayerList {
  449. width: 100%;
  450. height: 524px;
  451. position: absolute;
  452. left: 0;
  453. bottom: 51px;
  454. box-shadow: 0px 3px 22px 0px rgba(0, 0, 0, 0.12);
  455. border-radius: 20px 20px 0px 0px;
  456. background-color: rgb(255, 255, 255);
  457. padding: 20px 0 61px 10px;
  458. .titNameCon {
  459. padding-left: 10px;
  460. display: flex;
  461. align-items: center;
  462. height: 38px;
  463. .titName {
  464. font-weight: 600;
  465. font-size: 22px;
  466. color: #131415;
  467. line-height: 30px;
  468. position: relative;
  469. &::after {
  470. position: absolute;
  471. left: 0;
  472. bottom: 0;
  473. content: "";
  474. width: 100%;
  475. height: 12px;
  476. background: linear-gradient(90deg, #77bbff 0%, rgba(163, 231, 255, 0.22) 100%);
  477. }
  478. .tit {
  479. position: relative;
  480. z-index: 1;
  481. }
  482. }
  483. }
  484. .enjoyPlayerListCon {
  485. margin-top: 20px;
  486. overflow-y: auto;
  487. height: calc(100% - 58px);
  488. padding-right: 10px;
  489. .playerItem {
  490. padding: 18px 5px 18px 10px;
  491. display: flex;
  492. justify-content: space-between;
  493. align-items: center;
  494. &.active {
  495. background: #e6f3ff;
  496. border-radius: 10px;
  497. .itemLeft {
  498. color: #198cfe;
  499. font-weight: 600;
  500. }
  501. }
  502. .itemLeft {
  503. font-weight: 400;
  504. font-size: 20px;
  505. color: #131415;
  506. line-height: 28px;
  507. white-space: nowrap;
  508. overflow: hidden;
  509. text-overflow: ellipsis;
  510. }
  511. .itemRight {
  512. margin-left: 10px;
  513. flex-shrink: 0;
  514. display: flex;
  515. .itemListBtn {
  516. width: 34px;
  517. height: 34px;
  518. margin-right: 10px;
  519. background: url("./imgs/td2.png") no-repeat;
  520. background-size: 24px 24px;
  521. background-position: center;
  522. cursor: grab;
  523. }
  524. .itemCloseBtn {
  525. width: 34px;
  526. height: 34px;
  527. background: url("./imgs/del.png") no-repeat;
  528. background-size: 24px 24px;
  529. background-position: center;
  530. cursor: pointer;
  531. }
  532. }
  533. }
  534. }
  535. }
  536. }
  537. </style>