index.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. import {
  2. computed,
  3. defineComponent,
  4. nextTick,
  5. onMounted,
  6. reactive,
  7. ref
  8. } from 'vue'
  9. import umiRequest from 'umi-request'
  10. import { useRoute, useRouter } from 'vue-router'
  11. import request from '@/helpers/request'
  12. import ColHeader from '@/components/col-header'
  13. import { postMessage, promisefiyPostMessage } from '@/helpers/native-message'
  14. import {
  15. Button,
  16. Cell,
  17. Dialog,
  18. Icon,
  19. Image,
  20. Popup,
  21. Sticky,
  22. Tag,
  23. Toast
  24. } from 'vant'
  25. import styles from './index.module.less'
  26. // import Item from '../list/item'
  27. import { useRect } from '@vant/use'
  28. import { Vue3Lottie } from 'vue3-lottie'
  29. import { getRandomKey } from '@/views/music/music'
  30. import { getOssUploadUrl, state } from '@/state'
  31. import { useEventTracking } from '@/helpers/hooks'
  32. import ColSticky from '@/components/col-sticky'
  33. import { browser, moneyFormat } from '@/helpers/utils'
  34. import { orderStatus } from '@/views/order-detail/orderStatus'
  35. import iconAlbum from '@/views/music/component/images/icon_album.png'
  36. import iconDownload from '@/views/music/music-detail/images/icon_download.png'
  37. import AstronautJSON from '@/views/music/music-detail/animate/bigLoad.json'
  38. import ColShare from '@/components/col-share'
  39. import iconCollect from '@/views/music/music-detail/images/icon_collect.png'
  40. import iconCollectActive from '@/views/music/music-detail/images/icon_collect_active.png'
  41. import iconListen from '@/views/music/music-detail/images/icon_listen.png'
  42. import iconTeacher from '@common/images/icon_teacher.png'
  43. import {
  44. addMusicTitle,
  45. addWatermark,
  46. convasToImg,
  47. imgToCanvas
  48. } from '@/views/music/music-detail/imageFunction'
  49. import Plyr from 'plyr'
  50. import 'plyr/dist/plyr.css'
  51. import icon_exquisite from '@/views/music/component/images/icon_exquisite.png'
  52. import icon_album_active from '@/views/music/component/images/icon_album_active.png'
  53. import wx_bg from '../images/wx_bg.png'
  54. import { initJumpNativePage, shareCall } from '../share'
  55. import qs from 'query-string'
  56. export default defineComponent({
  57. name: 'MusicDetail',
  58. setup() {
  59. localStorage.setItem('behaviorId', getRandomKey())
  60. const route = useRoute()
  61. const loading = ref(false)
  62. const isError = ref(false)
  63. const headers = ref(null)
  64. const footers = ref(null)
  65. const heightInfo = ref<any>('0')
  66. const musicDetail = ref<any>(null)
  67. const showImg = ref<string>('')
  68. const accompanyUrl = ref<string>('')
  69. const wxStatus = ref<boolean>(false)
  70. const tmpUrl = `${location.origin}/student/#/music-detail?${qs.stringify(
  71. route.query
  72. )}`
  73. const jumpUrl = ref<string>(tmpUrl)
  74. const colors: any = {
  75. FREE: {
  76. color: '#01B84F',
  77. text: '免费'
  78. },
  79. VIP: {
  80. color: '#CD863E',
  81. text: '会员'
  82. },
  83. CHARGE: {
  84. color: '#3591CE',
  85. text: '点播'
  86. }
  87. }
  88. const FetchList = async (id?: any) => {
  89. if (loading.value) {
  90. return
  91. }
  92. loading.value = true
  93. isError.value = false
  94. try {
  95. const search = route.query
  96. const res = await request.post(`/open/musicShareProfit`, {
  97. prefix: '/api-teacher',
  98. requestType: 'json',
  99. data: {
  100. bizId: search.id,
  101. userId: search.recomUserId
  102. }
  103. })
  104. musicDetail.value = res.data.musicSheet
  105. showImg.value = musicDetail.value?.musicImg || ''
  106. if (!showImg.value) {
  107. setAccompanyUrl()
  108. window.addEventListener(
  109. 'message',
  110. async e => {
  111. // 给图片设置背景色
  112. const tempCanvas = await imgToCanvas(e.data)
  113. const img = convasToImg(tempCanvas)
  114. // 开始上传图片
  115. uploadFunction(img)
  116. },
  117. false
  118. )
  119. }
  120. } catch (error) {
  121. isError.value = true
  122. }
  123. loading.value = false
  124. }
  125. const base64ToBlob = data => {
  126. const arr = data.split(','),
  127. mime = arr[0].match(/:(.*?);/)[1]
  128. const bstr = atob(arr[1])
  129. let n = bstr.length
  130. const u8arr = new Uint8Array(n)
  131. while (n--) {
  132. u8arr[n] = bstr.charCodeAt(n)
  133. }
  134. return new Blob([u8arr], { type: mime })
  135. }
  136. const uploadFunction = async file => {
  137. try {
  138. const formData = new FormData()
  139. const fileName =
  140. new Date().getTime() +
  141. musicDetail.value?.musicSheetName.replaceAll(' ', '_') +
  142. '.png'
  143. const keyTime = new Date().getTime() + fileName
  144. const obj = {
  145. filename: fileName,
  146. bucketName: 'cloud-coach',
  147. postData: {
  148. filename: fileName,
  149. acl: 'public-read',
  150. key: keyTime,
  151. unknowValueField: []
  152. }
  153. }
  154. const res = await request.post(state.platformApi + '/getUploadSign', {
  155. data: obj
  156. })
  157. Toast.loading({
  158. message: '加载中...',
  159. forbidClick: true,
  160. loadingType: 'spinner',
  161. duration: 0
  162. })
  163. const dataObj = {
  164. policy: res.data.policy,
  165. signature: res.data.signature,
  166. key: keyTime,
  167. KSSAccessKeyId: res.data.kssAccessKeyId,
  168. acl: 'public-read',
  169. name: fileName
  170. }
  171. for (const key in dataObj) {
  172. formData.append(key, dataObj[key])
  173. }
  174. const files = base64ToBlob(file)
  175. formData.append('file', files, fileName)
  176. const ossUploadUrl = state.ossUploadUrl + 'cloud-coach'
  177. await umiRequest(ossUploadUrl, {
  178. method: 'POST',
  179. data: formData
  180. })
  181. Toast.clear()
  182. const imgurl = getOssUploadUrl('cloud-coach') + keyTime
  183. await request.post(state.platformApi + '/open/music/sheet/img', {
  184. data: { musicSheetId: musicDetail.value.id, musicImg: imgurl }
  185. })
  186. showImg.value = imgurl
  187. } catch (e) {
  188. console.log(e)
  189. }
  190. }
  191. const setAccompanyUrl = () => {
  192. const url = 'http://dev.colexiu.com'
  193. const music = musicDetail.value
  194. let subjectId = ''
  195. if (music.background && music.background.length > 0) {
  196. subjectId = music.background[0].id
  197. }
  198. accompanyUrl.value =
  199. url +
  200. `/accompany/colxiu-website.html?id=${music.id}&part-index=${subjectId}`
  201. }
  202. const player = ref<any>(null)
  203. const audio = ref<any>(null)
  204. const freeRate = ref<any>(0)
  205. const initAudio = async () => {
  206. const config = await request.get(
  207. state.platformApi + '/sysConfig/queryByParamNameList',
  208. {
  209. params: {
  210. paramNames: 'music_sheet_free_rate'
  211. }
  212. }
  213. )
  214. freeRate.value = config.data[0]?.paramValue || 0
  215. const controls = [
  216. // 'play-large',
  217. 'play',
  218. 'progress',
  219. // 'captions',
  220. // 'fullscreen',
  221. 'duration'
  222. ]
  223. player.value = new Plyr(audio.value, {
  224. controls: controls
  225. })
  226. player.value.on('timeupdate', () => {
  227. // 允许播放时间
  228. const players = player.value
  229. const playTime = (players.duration * freeRate.value) / 100 || 0
  230. // 时间,是否购买,是否免费
  231. if (
  232. players.currentTime >= playTime &&
  233. musicDetail.value?.orderStatus !== 'PAID' &&
  234. !paymentType.value.includes('FREE')
  235. ) {
  236. // players.stop()
  237. players.pause()
  238. }
  239. })
  240. }
  241. onMounted(async () => {
  242. initJumpNativePage(jumpUrl.value)
  243. await FetchList()
  244. const { height } = useRect(headers as any)
  245. const footer = useRect(footers as any)
  246. heightInfo.value = height + footer.height
  247. // 初始化音频
  248. if (musicDetail.value?.audioFileUrl) {
  249. initAudio()
  250. }
  251. })
  252. const paymentType = computed(() => {
  253. let paymentType = musicDetail.value?.paymentType
  254. if (typeof paymentType === 'string') {
  255. paymentType = paymentType.split(',')
  256. return paymentType
  257. }
  258. return []
  259. })
  260. const onShare = () => {
  261. console.log(browser().weixin)
  262. if (browser().weixin) {
  263. wxStatus.value = true
  264. return
  265. }
  266. // 尝试拉起app
  267. shareCall(jumpUrl.value)
  268. // 不管有没有拉起app则都跳转到下载app
  269. setTimeout(() => {
  270. window.location.href = location.origin + '/student/#/download'
  271. }, 3000)
  272. }
  273. return () => {
  274. return (
  275. <div class={styles.detail}>
  276. <Sticky position="top">
  277. <div ref={headers}>
  278. <ColHeader
  279. background="transparent"
  280. border={false}
  281. isFixed={false}
  282. color="#fff"
  283. backIconColor="white"
  284. />
  285. </div>
  286. </Sticky>
  287. <img class={styles.bgImg} src={musicDetail.value?.titleImg} />
  288. <div class={styles.bgContent}></div>
  289. <div
  290. class={styles.musicContainer}
  291. style={{
  292. marginTop: '16px',
  293. height: `calc(100vh - ${heightInfo.value + 16 + 'px'})`
  294. }}
  295. >
  296. <Cell
  297. border={false}
  298. center
  299. class={styles.musicInfo}
  300. v-slots={{
  301. icon: () => (
  302. <Image
  303. class={styles.pImg}
  304. src={musicDetail.value?.titleImg}
  305. />
  306. ),
  307. title: () => (
  308. <div class={styles.info}>
  309. <h4 class="van-ellipsis">
  310. {musicDetail.value?.musicSheetName}
  311. </h4>
  312. <p
  313. style={{
  314. display: 'flex'
  315. }}
  316. >
  317. {paymentType.value.map(tag => (
  318. <Tag
  319. style={{ color: colors[tag].color }}
  320. class={styles.tag}
  321. type="success"
  322. plain
  323. >
  324. {colors[tag].text}
  325. </Tag>
  326. ))}
  327. {musicDetail.value?.exquisiteFlag === 1 && (
  328. <Image
  329. class={styles.exquisiteFlag}
  330. src={icon_exquisite}
  331. />
  332. )}
  333. {musicDetail.value?.albumNums > 0 && (
  334. <Image
  335. class={styles.songAlbum}
  336. src={icon_album_active}
  337. />
  338. )}
  339. <span style={{ paddingTop: '2px', paddingLeft: '6px' }}>
  340. {musicDetail.value?.composer}
  341. </span>
  342. </p>
  343. </div>
  344. ),
  345. value: () => (
  346. <span class={styles.download} onClick={() => onShare()}>
  347. <img src={iconDownload} />
  348. 下载曲谱
  349. </span>
  350. )
  351. }}
  352. />
  353. <div class={styles.musicContent}>
  354. <iframe
  355. id="containerPrint"
  356. ref="print"
  357. style="width: 100%;page-break-after:always; height: 0"
  358. src={accompanyUrl.value}
  359. />
  360. <p class={styles.musicTitle}>
  361. {musicDetail.value?.musicSheetName}
  362. </p>
  363. {showImg.value ? (
  364. <img src={showImg.value} alt="" class={styles.musicImg} />
  365. ) : (
  366. <>
  367. <Vue3Lottie
  368. animationData={AstronautJSON}
  369. class={styles.finch}
  370. ></Vue3Lottie>
  371. <p class={styles.finchLoad}>加载中...</p>
  372. </>
  373. )}
  374. <div class={styles.videoOperation}>
  375. {musicDetail.value?.audioFileUrl && (
  376. <>
  377. {(paymentType.value.includes('CHARGE') ||
  378. paymentType.value.includes('VIP')) &&
  379. musicDetail.value?.orderStatus !== 'PAID' && (
  380. <div class={[styles.audition]}>
  381. <img src={iconListen} />
  382. <span>每首曲目可试听{freeRate.value}%</span>
  383. </div>
  384. )}
  385. <div class={[styles.audio, styles.collectCell]}>
  386. <audio id="player" controls ref={audio}>
  387. <source
  388. src={musicDetail.value?.audioFileUrl}
  389. type="audio/mp3"
  390. />
  391. </audio>
  392. </div>
  393. </>
  394. )}
  395. <div class={[styles.collect, styles.collectCell]}>
  396. <div class={[styles.userInfo]}>
  397. <img src={musicDetail.value?.userAvatar || iconTeacher} />
  398. <span>{musicDetail.value?.userName}</span>
  399. </div>
  400. <div
  401. class={[styles.collectSection]}
  402. onClick={() => onShare()}
  403. >
  404. <span>{musicDetail.value?.favoriteCount}人收藏</span>
  405. <img
  406. src={
  407. musicDetail.value?.favorite
  408. ? iconCollectActive
  409. : iconCollect
  410. }
  411. />
  412. </div>
  413. </div>
  414. </div>
  415. </div>
  416. <div
  417. class={[styles.lookAlbum, styles.collectCell]}
  418. onClick={() => {
  419. onShare()
  420. }}
  421. >
  422. <div>
  423. <img src={iconAlbum} />
  424. <span>进入曲目所在专辑列表</span>
  425. </div>
  426. <Icon name="arrow" size={16} color="#666" />
  427. </div>
  428. </div>
  429. {musicDetail.value?.id && (
  430. <ColSticky position="bottom" background="white">
  431. <div ref={footers}>
  432. <Button
  433. round
  434. block
  435. type="primary"
  436. color="linear-gradient(180deg, #59E5D5 0%, #2DC7AA 100%)"
  437. onClick={() => onShare()}
  438. >
  439. 下载酷乐秀进入详情
  440. </Button>
  441. </div>
  442. </ColSticky>
  443. )}
  444. {wxStatus.value && (
  445. <div
  446. class={styles.wxpopup}
  447. onClick={() => {
  448. wxStatus.value = false
  449. }}
  450. >
  451. <img src={wx_bg} alt="" />
  452. </div>
  453. )}
  454. </div>
  455. )
  456. }
  457. }
  458. })