index.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691
  1. <script setup lang="ts">
  2. import { computed, onMounted, reactive, ref, watch } from 'vue'
  3. import { useRoute } from 'vue-router'
  4. import { useAudioManager } from '@/hook'
  5. import OpenApp from '@/components/OpenApp/index.vue'
  6. import { registerWxopenButton } from '@/utils/utils'
  7. import { getTrialAudioLXXRequest, getTrialAudioPTRequest } from '@/api/sectionAudition'
  8. import dayjs from 'dayjs'
  9. const logo = require('../assets/logo.png')
  10. const pause = require('../assets/pause.png')
  11. const play = require('../assets/play.png')
  12. const remindLogo = require('../assets/remind_logo.png')
  13. const close = require('../assets/close.png')
  14. const space = 32
  15. const defaultSection = {
  16. id: '',
  17. name: '',
  18. videoId: null,
  19. aiCourseSkuId: '',
  20. sort: 0,
  21. createTime: '',
  22. updateTime: '',
  23. readCount: 0,
  24. aiCourseItemChapterId: '',
  25. type: 1,
  26. payType: 0,
  27. freeTime: 0,
  28. audioId: '',
  29. imgCover: '',
  30. chapterName: '',
  31. chapterSort: 0,
  32. richText: null,
  33. splitList: null,
  34. readCountStr: '',
  35. audio: {
  36. id: null,
  37. audioUrl: '',
  38. parentId: null,
  39. createTime: null,
  40. updateTime: null,
  41. duration: ''
  42. }
  43. }
  44. const defaultCourse = {
  45. id: '',
  46. name: '',
  47. imgCover: '',
  48. imgCoverMini: '',
  49. createTime: '',
  50. updateTime: '',
  51. price: 0,
  52. markingPrice: 0,
  53. categoryId: '',
  54. courseCount: 0,
  55. aiCourseSpuId: '',
  56. suitAge: '',
  57. wxNumber: null,
  58. wxQrCode: null,
  59. wxName: null,
  60. wxHeadImg: null,
  61. description: '',
  62. mediaType: 1,
  63. simpleDescription: '',
  64. sort: 0,
  65. showChapter: 1,
  66. courseType: 0,
  67. subCategoryId: '',
  68. activityTag: null,
  69. courseTags: [],
  70. paidCount: 0,
  71. latestLearnedRecordId: null,
  72. latestLearnedRecord: null,
  73. abilityList: null,
  74. commentCount: null,
  75. shareCount: null,
  76. collectCount: null,
  77. isCollect: 0,
  78. hasPaid: 0,
  79. isComment: null,
  80. isShare: null,
  81. imgCoverWidth: null,
  82. imgCoverHeight: null,
  83. shareUrl: '',
  84. shareUrlQRCode: null,
  85. groupBuyActivity: null,
  86. groupBuyActivityId: null,
  87. groupBuyActivityUrl: null,
  88. totalLearnUser: 0,
  89. cashbackActivity: 0,
  90. cashbackActivityId: null,
  91. textbook: null,
  92. checkInExplain: null,
  93. chapterList: [
  94. {
  95. id: '',
  96. name: '',
  97. createTime: '',
  98. updateTime: null,
  99. aiCourseSkuId: '',
  100. sort: 0,
  101. coverImgUrl: '',
  102. itemList: [
  103. {
  104. id: '',
  105. name: '',
  106. videoId: null,
  107. aiCourseSkuId: '',
  108. sort: 0,
  109. createTime: '',
  110. updateTime: '',
  111. readCount: 0,
  112. aiCourseItemChapterId: '',
  113. type: 1,
  114. payType: 0,
  115. freeTime: 0,
  116. audioId: '',
  117. imgCover: '',
  118. chapterName: '',
  119. chapterSort: 0,
  120. richText: null,
  121. splitList: null,
  122. readCountStr: '',
  123. audio: {
  124. id: null,
  125. audioUrl: null,
  126. parentId: null,
  127. createTime: null,
  128. updateTime: null,
  129. duration: ''
  130. }
  131. }
  132. ]
  133. }
  134. ],
  135. spu: {
  136. id: '',
  137. name: '',
  138. imgCover: '',
  139. imgCoverMini: '',
  140. videoUrl: '',
  141. createTime: '',
  142. updateTime: '',
  143. price: 0,
  144. markingPrice: 0,
  145. categoryId: '',
  146. suitAge: '',
  147. parentNotice: null,
  148. courseCount: 0,
  149. description: null,
  150. outline: null,
  151. notice: null,
  152. simpleDescription: '',
  153. mediaType: 1,
  154. status: 1,
  155. isDelete: 0,
  156. sort: 0,
  157. showChapter: 1,
  158. isShow: true,
  159. showCoverImg: 1
  160. },
  161. showCoverImg: 1,
  162. position: 0,
  163. trialLearn: 0
  164. }
  165. const { id: ptId } = useRoute().query
  166. const { id: lxxId } = useRoute().params
  167. const [fco, atx] = useAudioManager({
  168. url: '',
  169. format: 'mm:ss'
  170. })
  171. const course = reactive(defaultCourse)
  172. const section = reactive(defaultSection)
  173. const percentage = ref(0)
  174. const playing = ref(false)
  175. const show = ref(false)
  176. const progressRef = ref()
  177. const progressBarWidth = ref(0)
  178. const isFirstClick = ref(false)
  179. // 剩余时间
  180. const remainingTime = computed(() => {
  181. // console.log(1 - percentage.value, '1 - percentage.value')
  182. return Math.round(section.freeTime - timeToSeconds(fco.updateTime))
  183. })
  184. const totalTime = computed(() => {
  185. // console.log(fco.duration, 'fco.durationfco.duration')
  186. if (!fco.duration) {
  187. return secondsToTime(section.audio.duration)
  188. }
  189. return fco.duration
  190. })
  191. const extinfo = computed(() => {
  192. return `home/wx/play?skuId=${course.id}&index=${course.position}`
  193. })
  194. const maxPercentage = computed(() => {
  195. if (section.freeTime && section.audio.duration) {
  196. return section.freeTime / Number(section.audio.duration)
  197. } else {
  198. return 1
  199. }
  200. })
  201. watch(
  202. () => fco.percentage,
  203. () => {
  204. setPercentage(fco.percentage || 0)
  205. if (fco.percentage >= maxPercentage.value) {
  206. fco.pause()
  207. playing.value = false
  208. show.value = true
  209. }
  210. }
  211. )
  212. watch(
  213. () => course.name,
  214. () => {
  215. console.log(section, 'duration')
  216. fco.src = section.audio.audioUrl
  217. document.title = course.name
  218. }
  219. )
  220. const setPercentage = (num: number) => {
  221. if (num > maxPercentage.value) {
  222. percentage.value = maxPercentage.value
  223. } else if (num < 0) {
  224. percentage.value = 0
  225. } else {
  226. percentage.value = num
  227. }
  228. }
  229. const timeToSeconds = (timeStr: string | number) => {
  230. if (typeof timeStr === 'string') {
  231. let [minutes, seconds] = timeStr.split(':').map(Number)
  232. // console.log(minutes, typeof seconds, 'minutes, seconds')
  233. if (minutes === undefined) minutes = 0
  234. if (seconds === undefined) seconds = 0
  235. return minutes * 60 + seconds
  236. } else {
  237. return 0
  238. }
  239. }
  240. function secondsToTime (seconds:string) {
  241. return dayjs(Math.round(Number(seconds) * 1000)).format('mm:ss')
  242. }
  243. function playAudio () {
  244. if (!isFirstClick.value) isFirstClick.value = true
  245. if (!playing.value) {
  246. if (fco.percentage >= maxPercentage.value) {
  247. fco.setPercentage(0)
  248. }
  249. fco.play()
  250. } else {
  251. fco.pause()
  252. }
  253. playing.value = !playing.value
  254. }
  255. async function getTrialAudio () {
  256. const id = ptId || lxxId
  257. const request = ptId ? getTrialAudioPTRequest : getTrialAudioLXXRequest
  258. const { data, status } = await request(id as string)
  259. if (status === 200) {
  260. Object.assign(course, data)
  261. if (data?.chapterList?.length > 0 && data.chapterList[0]?.itemList.length > 0) {
  262. Object.assign(section, data.chapterList[0].itemList[0])
  263. console.log(fco.duration, 'fco.duration')
  264. console.log(section.audio.duration, 'section.audio.duration')
  265. }
  266. }
  267. }
  268. function initAudio () {
  269. progressBarWidth.value = progressRef.value.clientWidth - space * 2
  270. progressRef.value.addEventListener('touchstart', () => {
  271. console.log('touchstart')
  272. if (!isFirstClick.value) {
  273. fco.play()
  274. playing.value = true
  275. isFirstClick.value = true
  276. }
  277. fco.pause()
  278. playing.value = false
  279. })
  280. progressRef.value.addEventListener('touchmove', (event: { touches: { pageX: number }[] }) => {
  281. console.log('touchmove')
  282. const { pageX } = event.touches[0]
  283. let num
  284. if (pageX <= space) {
  285. num = 0
  286. } else if ((pageX - space) / progressBarWidth.value > maxPercentage.value) {
  287. num = maxPercentage.value
  288. } else {
  289. num = (pageX - space) / progressBarWidth.value
  290. }
  291. fco.setPercentage(num)
  292. })
  293. progressRef.value.addEventListener('touchend', () => {
  294. console.log('touchend')
  295. fco.play()
  296. playing.value = true
  297. })
  298. }
  299. onMounted(async () => {
  300. initAudio()
  301. console.log(ptId, '拼图id')
  302. console.log(lxxId, '逻小熊id')
  303. await getTrialAudio()
  304. await registerWxopenButton()
  305. })
  306. </script>
  307. <template>
  308. <div class="section-audition sa">
  309. <div class="sa-cover" @click="playAudio">
  310. <img :src="section.imgCover" alt="" class="sa-cover-img" />
  311. <div class="sa-cover-tips">
  312. 试听剩余{{remainingTime}}s,体验完整版前往APP
  313. </div>
  314. <img :src="playing ? pause : play" alt="" class="sa-cover-btn" />
  315. </div>
  316. <div class="sa-process" :style="{opacity: course.imgCover ? 1: 0}">
  317. <div class="progress" ref="progressRef">
  318. <div
  319. class="progress-bar"
  320. :style="{ width: `calc(100vw - ${space * 2}px)` }"
  321. >
  322. <!--最大播放范围-->
  323. <div
  324. class="progress-bar-track"
  325. :style="{ width: `${maxPercentage * progressBarWidth}px` }"
  326. ></div>
  327. <!--最大播放位置小竖线-->
  328. <div
  329. v-if="maxPercentage < 1 && maxPercentage > 0"
  330. class="progress-bar-max"
  331. :style="{ left: `${maxPercentage * progressBarWidth}px` }"
  332. ></div>
  333. <!--当前位置圆点-->
  334. <div
  335. class="progress-bar-pivot"
  336. :style="{ left: `${percentage * progressBarWidth}px` }"
  337. ></div>
  338. <!--已经播放过的部分-->
  339. <div
  340. class="progress-bar-line"
  341. :style="{ width: `${percentage * progressBarWidth}px` }"
  342. ></div>
  343. </div>
  344. </div>
  345. <div class="sa-process-time">
  346. <div>{{ fco.updateTime || "00:00" }}</div>
  347. <div>{{ totalTime }}</div>
  348. </div>
  349. </div>
  350. <div class="sa-title">{{ course.name }}</div>
  351. <div class="sa-open">
  352. <img :src="logo" alt="logo" class="sa-open-logo" />
  353. <div class="sa-open-text">
  354. <h4>逻辑狗APP</h4>
  355. <div>少儿思维教育,就选逻辑狗</div>
  356. </div>
  357. <div class="sa-open-btn" @click="playAudio">
  358. <OpenApp :extinfo="extinfo">
  359. <div
  360. style="
  361. display: flex;
  362. justify-content: center;
  363. align-items: center;
  364. width: 74px;
  365. height: 32px;
  366. background: #0b47a4;
  367. border-radius: 16px;
  368. font-size: 14px;
  369. font-family: PingFang SC-Semibold, PingFang SC;
  370. font-weight: 600;
  371. color: #ffffff;
  372. "
  373. >
  374. 打开
  375. </div>
  376. </OpenApp>
  377. </div>
  378. </div>
  379. <VanOverlay :show="show" :z-index="3" @click="show = false">
  380. <div class="sa-remind" @click.stop>
  381. <img :src="remindLogo" alt="" class="sa-remind-logo" />
  382. <img
  383. :src="close"
  384. alt=""
  385. class="sa-remind-close"
  386. @click="show = false"
  387. />
  388. <div class="sa-remind-container">
  389. <div class="title">前往APP继续观看</div>
  390. <div class="desc">试听已结束,收听完整资源</div>
  391. <div class="desc mb12">请前往APP继续</div>
  392. <OpenApp :extinfo="extinfo">
  393. <div
  394. style="
  395. display: flex;
  396. justify-content: center;
  397. align-items: center;
  398. margin: 0 auto;
  399. width: 182px;
  400. height: 49px;
  401. background: #2654bf;
  402. border-radius: 25px 25px 25px 25px;
  403. font-size: 18px;
  404. font-family: PingFang SC-Medium, PingFang SC;
  405. font-weight: 500;
  406. color: #ffffff;
  407. "
  408. >
  409. 前往APP
  410. </div>
  411. </OpenApp>
  412. </div>
  413. </div>
  414. </VanOverlay>
  415. </div>
  416. </template>
  417. <style scoped lang="scss">
  418. .sa {
  419. position: fixed;
  420. top: 0;
  421. left: 0;
  422. width: 100%;
  423. min-height: 100vh;
  424. background-color: #fff;
  425. overflow: hidden;
  426. &-cover {
  427. position: relative;
  428. margin: 20px auto 0;
  429. width: 335px;
  430. border-radius: 20px;
  431. overflow: hidden;
  432. &-img {
  433. display: block;
  434. width: 100%;
  435. }
  436. &-tips {
  437. display: flex;
  438. justify-content: center;
  439. align-items: center;
  440. position: absolute;
  441. right: 16px;
  442. top: 16px;
  443. width: 226px;
  444. height: 24px;
  445. background: rgba(0, 0, 0, 0.5);
  446. border-radius: 40px;
  447. font-size: 13px;
  448. font-family: PingFang SC-Regular, PingFang SC;
  449. font-weight: 400;
  450. color: #ffffff;
  451. }
  452. &-btn {
  453. display: block;
  454. position: absolute;
  455. top: 50%;
  456. left: 50%;
  457. transform: translate(-50%, -50%);
  458. width: 60px;
  459. height: 60px;
  460. }
  461. }
  462. &-process {
  463. width: 100%;
  464. .progress {
  465. padding: 27px 0;
  466. width: 100%;
  467. &-bar {
  468. position: relative;
  469. margin: 0 auto;
  470. width: calc(100vw - 40px);
  471. height: 3px;
  472. background-color: #ededed;
  473. border-radius: 2px;
  474. &-track {
  475. height: 3px;
  476. background-color: #b7d2ff;
  477. border-radius: 2px;
  478. }
  479. &-max {
  480. position: absolute;
  481. top: 50%;
  482. left: 0;
  483. transform: translate(-50%, -50%);
  484. width: 2px;
  485. height: 7px;
  486. background: #B7D2FF;
  487. border-radius: 1px;
  488. }
  489. &-pivot {
  490. position: absolute;
  491. top: 50%;
  492. left: 0;
  493. transform: translate(-50%, -50%);
  494. z-index: 3;
  495. width: 13px;
  496. height: 13px;
  497. background-color: #0643A2;
  498. border-radius: 50%;
  499. }
  500. &-line {
  501. position: absolute;
  502. top: 50%;
  503. left: 0;
  504. transform: translateY(-50%);
  505. z-index: 3;
  506. height: 3px;
  507. background-color: #0643A2;
  508. border-radius: 2px;
  509. }
  510. }
  511. }
  512. &-time {
  513. display: flex;
  514. justify-content: space-between;
  515. align-items: center;
  516. padding-left: 31px;
  517. padding-right: 29px;
  518. position: relative;
  519. top: -13px;
  520. left: 0;
  521. z-index: 2;
  522. ont-size: 13px;
  523. font-family: PingFang SC-Regular, PingFang SC;
  524. font-weight: 400;
  525. color: #333333;
  526. box-sizing: border-box;
  527. }
  528. }
  529. &-title {
  530. margin-top: 18px;
  531. //height: 16px;
  532. font-size: 18px;
  533. font-family: PingFang SC-Semibold, PingFang SC;
  534. font-weight: 600;
  535. color: #333333;
  536. //line-height: 16px;
  537. text-align: center;
  538. }
  539. &-open {
  540. display: flex;
  541. align-items: center;
  542. position: fixed;
  543. bottom: 0;
  544. left: 0;
  545. width: 100vw;
  546. height: 67px;
  547. background: #f1f1f1;
  548. &-logo {
  549. display: block;
  550. margin-left: 14px;
  551. margin-right: 17px;
  552. width: 43px;
  553. height: 43px;
  554. }
  555. &-text {
  556. h4 {
  557. margin: 0 0 3px;
  558. height: 17px;
  559. font-size: 14px;
  560. font-family: PingFang SC-Semibold, PingFang SC;
  561. font-weight: 600;
  562. color: #333333;
  563. line-height: 17px;
  564. }
  565. div {
  566. height: 14px;
  567. font-size: 12px;
  568. font-family: PingFang SC-Regular, PingFang SC;
  569. font-weight: 400;
  570. color: #999999;
  571. line-height: 14px;
  572. }
  573. }
  574. &-btn {
  575. position: absolute;
  576. right: 13px;
  577. top: 50%;
  578. transform: translateY(-50%);
  579. }
  580. }
  581. &-remind {
  582. position: fixed;
  583. top: 50%;
  584. left: 50%;
  585. transform: translate(-50%, -65%);
  586. z-index: 5;
  587. img {
  588. display: block;
  589. }
  590. &-logo {
  591. position: relative;
  592. z-index: 2;
  593. width: 290px;
  594. height: 126px;
  595. }
  596. &-close {
  597. position: absolute;
  598. right: -6px;
  599. top: 107px;
  600. z-index: 3;
  601. width: 36px;
  602. height: 36px;
  603. }
  604. &-container {
  605. display: flex;
  606. flex-direction: column;
  607. align-items: center;
  608. margin-top: -14px;
  609. width: 290px;
  610. height: 186px;
  611. background: linear-gradient(180deg, #f4ffde 0%, #ffffff 100%);
  612. box-shadow: inset 0 0 5px 0 #a5a8a4;
  613. border-radius: 20px;
  614. overflow: hidden;
  615. .title {
  616. margin-top: 27px;
  617. margin-bottom: 14px;
  618. height: 24px;
  619. font-size: 20px;
  620. font-family: PingFang SC-Medium, PingFang SC;
  621. font-weight: 500;
  622. color: #093708;
  623. line-height: 24px;
  624. letter-spacing: 1px;
  625. }
  626. .desc {
  627. font-size: 16px;
  628. font-family: PingFang SC-Regular, PingFang SC;
  629. font-weight: 400;
  630. color: #759a6b;
  631. line-height: 20px;
  632. }
  633. .mb12 {
  634. margin-bottom: 12px;
  635. }
  636. }
  637. }
  638. }
  639. </style>