index.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. import {
  2. computed,
  3. defineComponent,
  4. nextTick,
  5. onMounted,
  6. reactive,
  7. ref
  8. } from 'vue'
  9. import { useRoute, useRouter } from 'vue-router'
  10. import request from '@/helpers/request'
  11. import ColHeader from '@/components/col-header'
  12. import { postMessage } from '@/helpers/native-message'
  13. import { Button, Dialog, Icon, Image, Popup } from 'vant'
  14. import styles from './index.module.less'
  15. import { useRect } from '@vant/use'
  16. import { useEventListener, useWindowScroll } from '@vueuse/core'
  17. import { getRandomKey, musicBuy } from '../music'
  18. import { openDefaultWebView, state } from '@/state'
  19. import IconPan from './icon-pan.png'
  20. import oStart from './icon-hart.png'
  21. import iStart from './icon-hart-active.png'
  22. import Song from '../component/song'
  23. import ColResult from '@/components/col-result'
  24. import MusicGrid from '../component/music-grid'
  25. import { useEventTracking } from '@/helpers/hooks'
  26. import ColSticky from '@/components/col-sticky'
  27. import { moneyFormat } from '@/helpers/utils'
  28. import { orderStatus } from '@/views/order-detail/orderStatus'
  29. import iconShare from '../../images/icon-share.png'
  30. import ColShare from '@/components/col-share'
  31. import SongShare from '../component/song-share'
  32. import icon_music_list from './icon_music_list.png'
  33. import iconMenu from './icon-menu.png'
  34. import TheSticky from '@/components/the-sticky'
  35. const noop = () => {}
  36. export default defineComponent({
  37. name: 'AlbumDetail',
  38. props: {
  39. onItemClick: {
  40. type: Function,
  41. default: noop
  42. }
  43. },
  44. setup({ onItemClick }) {
  45. localStorage.setItem('behaviorId', getRandomKey())
  46. const router = useRouter()
  47. const route = useRoute()
  48. const params = reactive({
  49. search: '',
  50. relatedNum: 6, //相关专辑数
  51. page: 1,
  52. rows: 200
  53. })
  54. const albumDetail = ref<any>(null)
  55. // const data = ref<any>(null)
  56. const rows = ref<any[]>([])
  57. const loading = ref(false)
  58. const aId = Number(route.query.activityId) || 0
  59. const studentActivityId = ref(aId)
  60. // const finished = ref(false)
  61. const isError = ref(false)
  62. const favorited = ref(0)
  63. const albumFavoriteCount = ref(0)
  64. const background = ref<string>('rgba(55, 205, 177, 0)')
  65. const color = ref<string>('#fff')
  66. const FetchList = async (id?: any) => {
  67. if (loading.value) {
  68. return
  69. }
  70. loading.value = true
  71. isError.value = false
  72. try {
  73. const res = await request.post('/music/album/detail', {
  74. prefix:
  75. state.platformType === 'TEACHER' ? '/api-teacher' : '/api-student',
  76. data: { id: id || route.params.id, ...params }
  77. })
  78. const { musicSheetList, ...rest } = res.data
  79. rows.value = [...musicSheetList.rows]
  80. const musicTagNames = rest?.musicTagNames
  81. ? rest?.musicTagNames?.split(',')
  82. : []
  83. albumDetail.value = {
  84. ...rest,
  85. musicTagNames
  86. }
  87. favorited.value = rest.favorite
  88. albumFavoriteCount.value = rest.albumFavoriteCount
  89. } catch (error) {
  90. isError.value = true
  91. }
  92. loading.value = false
  93. }
  94. const favoriteLoading = ref(false)
  95. onMounted(() => {
  96. FetchList()
  97. useEventListener(document, 'scroll', evt => {
  98. const { y } = useWindowScroll()
  99. if (y.value > 20) {
  100. background.value = `rgba(255, 255, 255)`
  101. color.value = 'black'
  102. postMessage({
  103. api: 'backIconChange',
  104. content: { iconStyle: 'black' }
  105. })
  106. } else {
  107. background.value = 'transparent'
  108. color.value = '#fff'
  109. postMessage({
  110. api: 'backIconChange',
  111. content: { iconStyle: 'white' }
  112. })
  113. }
  114. })
  115. useEventTracking('专辑')
  116. })
  117. const toggleFavorite = async (id: number) => {
  118. favoriteLoading.value = true
  119. try {
  120. await request.post('/music/album/favorite/' + id, {
  121. prefix:
  122. state.platformType === 'TEACHER' ? '/api-teacher' : '/api-student'
  123. })
  124. favorited.value = favorited.value === 1 ? 0 : 1
  125. albumFavoriteCount.value += favorited.value ? 1 : -1
  126. } catch (error) {}
  127. favoriteLoading.value = false
  128. }
  129. const onBuy = async () => {
  130. const album = albumDetail.value
  131. orderStatus.orderObject.orderType = 'ALBUM'
  132. orderStatus.orderObject.orderName = album.albumName
  133. orderStatus.orderObject.orderDesc = album.albumName
  134. orderStatus.orderObject.actualPrice = album.albumPrice
  135. orderStatus.orderObject.recomUserId = route.query.recomUserId || 0
  136. orderStatus.orderObject.activityId = route.query.activityId || 0
  137. orderStatus.orderObject.orderNo = ''
  138. orderStatus.orderObject.orderList = [
  139. {
  140. orderType: 'ALBUM',
  141. goodsName: album.albumName,
  142. recomUserId: route.query.recomUserId || 0,
  143. price: album.albumPrice,
  144. ...album
  145. }
  146. ]
  147. const res = await request.post('/api-student/userOrder/getPendingOrder', {
  148. data: {
  149. goodType: 'ALBUM',
  150. bizId: album.id
  151. }
  152. })
  153. const result = res.data
  154. if (result) {
  155. Dialog.confirm({
  156. title: '提示',
  157. message: '您有一个未支付的订单,是否继续支付?',
  158. theme: 'round-button',
  159. className: 'confirm-button-group',
  160. cancelButtonText: '取消订单',
  161. confirmButtonText: '继续支付'
  162. })
  163. .then(async () => {
  164. orderStatus.orderObject.orderNo = result.orderNo
  165. orderStatus.orderObject.actualPrice = result.actualPrice
  166. orderStatus.orderObject.discountPrice = result.discountPrice
  167. routerTo()
  168. })
  169. .catch(() => {
  170. Dialog.close()
  171. // 只用取消订单,不用做其它处理
  172. cancelPayment(result.orderNo)
  173. })
  174. } else {
  175. routerTo()
  176. }
  177. // this.$router.push({
  178. // path: '/orderDetail',
  179. // query: {
  180. // orderType: 'VIP'
  181. // }
  182. // })
  183. }
  184. const routerTo = () => {
  185. const album = albumDetail.value
  186. router.push({
  187. path: '/orderDetail',
  188. query: {
  189. orderType: 'ALBUM',
  190. album: album.id
  191. }
  192. })
  193. }
  194. const cancelPayment = async (orderNo: string) => {
  195. try {
  196. await request.post('/api-student/userOrder/orderCancel/v2', {
  197. data: {
  198. orderNo
  199. }
  200. })
  201. // this.routerTo()
  202. } catch {}
  203. }
  204. const shareStatus = ref<boolean>(false)
  205. const shareUrl = ref<string>('')
  206. const shareDiscount = ref<number>(0)
  207. const onShare = async () => {
  208. const userId = state.user.data.userId
  209. const id = route.params.id
  210. let activityId = 0
  211. console.log(state.user, userId)
  212. if (state.platformType === 'TEACHER') {
  213. const res = await request.post('/api-teacher/open/vipProfit', {
  214. data: {
  215. bizId: id,
  216. userId
  217. }
  218. })
  219. // 如果有会员则显示
  220. if (buyVip.value) {
  221. activityId = res.data.activityId || 0
  222. shareDiscount.value = res.data.discount || 0
  223. }
  224. }
  225. shareUrl.value = `${location.origin}/teacher/#/shareAblum?id=${id}&recomUserId=${userId}&activityId=${activityId}&userType=${state.platformType}&p=tenant`
  226. // console.log(shareUrl.value, 'shareUrl')
  227. shareStatus.value = true
  228. }
  229. const buyVip = computed(() => {
  230. const album = albumDetail.value?.musicPaymentTypes
  231. return album && album.includes('VIP')
  232. })
  233. /** 分享曲谱列表, 最大数量2 */
  234. const shareMusicList = computed(() => {
  235. return rows.value.length > 2 ? rows.value.slice(0, 2) : rows.value
  236. })
  237. return () => {
  238. return (
  239. <div class={styles.detail}>
  240. <TheSticky position="top">
  241. <ColHeader
  242. background={background.value}
  243. border={false}
  244. isFixed={false}
  245. color={color.value}
  246. backIconColor="white"
  247. />
  248. </TheSticky>
  249. <img class={styles.bgImg} src={albumDetail.value?.albumCoverUrl} />
  250. <div class={styles.musicContent}></div>
  251. <div class={styles.bg}>
  252. <div class={styles.alumWrap}>
  253. <div class={styles.img}>
  254. {/* {albumDetail.value?.paymentType === 'CHARGE' && (
  255. <span class={styles.albumType}>付费</span>
  256. )} */}
  257. <Image
  258. class={styles.image}
  259. width="100%"
  260. height="100%"
  261. fit="cover"
  262. src={albumDetail.value?.albumCoverUrl}
  263. />
  264. </div>
  265. <div class={styles.alumDes}>
  266. <div class={[styles.alumTitle, 'van-ellipsis']}>
  267. {albumDetail.value?.albumName}
  268. </div>
  269. <div class={[styles.des, 'van-multi-ellipsis--l2']}>
  270. {albumDetail.value?.albumDesc}
  271. </div>
  272. <div class={styles.tags}>
  273. {albumDetail.value?.musicTagNames?.map((tag: any) => (
  274. <span class={styles.tag}>{tag}</span>
  275. ))}
  276. </div>
  277. </div>
  278. </div>
  279. <div class={styles.alumCollect}>
  280. <div class={styles.alumCollectItem} onClick={onShare}>
  281. <Image src={iconShare} />
  282. <span>分享</span>
  283. </div>
  284. <div
  285. class={styles.alumCollectItem}
  286. onClick={() =>
  287. router.push({
  288. path: '/look-album-list',
  289. query: {
  290. id: route.params.id
  291. }
  292. })
  293. }
  294. >
  295. <img src={IconPan} />
  296. <span>相关专辑</span>
  297. </div>
  298. <div
  299. class={styles.alumCollectItem}
  300. onClick={() => toggleFavorite(albumDetail.value?.id)}
  301. >
  302. <img src={favorited.value ? iStart : oStart} />
  303. <span>{albumFavoriteCount.value}</span>
  304. </div>
  305. </div>
  306. {albumDetail.value?.paymentType === 'CHARGE' &&
  307. albumDetail.value?.orderStatus !== 'PAID' && (
  308. <div class={styles.albumTips}>
  309. <span>开通会员或购买专辑,即可自由练习该专辑</span>
  310. <span class={styles.albumPrice}>
  311. ¥{moneyFormat(albumDetail.value?.albumPrice)}
  312. </span>
  313. </div>
  314. )}
  315. </div>
  316. <div class={styles.alumnContainer}>
  317. <div class={styles.alumnList}>
  318. {/* <Title title="曲目列表" isMore={false} /> */}
  319. <div class={styles.alumnTitle}>
  320. <img src={iconMenu} class={styles.iconMenu} />
  321. 曲目列表{' '}
  322. <span>({albumDetail.value?.musicSheetCount || 0})</span>
  323. </div>
  324. <Song
  325. showNumber
  326. list={rows.value}
  327. onDetail={(item: any) => {
  328. if (onItemClick === noop || !onItemClick) {
  329. const url =
  330. location.origin +
  331. location.pathname +
  332. '#/music-detail?id=' +
  333. item.id +
  334. '&albumId=' +
  335. route.params.id
  336. openDefaultWebView(url, () => {
  337. router.push({
  338. path: '/music-detail',
  339. query: {
  340. id: item.id,
  341. albumId: route.params.id
  342. }
  343. })
  344. })
  345. } else {
  346. onItemClick(item)
  347. }
  348. }}
  349. />
  350. {rows.value && rows.value.length <= 0 && (
  351. <ColResult btnStatus={false} tips="暂无曲目" />
  352. )}
  353. </div>
  354. {/* {albumDetail.value?.relatedMusicAlbum &&
  355. albumDetail.value?.relatedMusicAlbum.length > 0 && (
  356. <>
  357. <Title
  358. title="相关专辑"
  359. onMore={() => {
  360. router.push({
  361. path: '/music-album'
  362. })
  363. }}
  364. />
  365. <MusicGrid
  366. list={albumDetail.value?.relatedMusicAlbum}
  367. onGoto={(n: any) => {
  368. router
  369. .push({
  370. name: 'music-album-detail',
  371. params: {
  372. id: n.id
  373. }
  374. })
  375. .then(() => {
  376. FetchList(n.id)
  377. window.scrollTo(0, 0)
  378. })
  379. }}
  380. />
  381. </>
  382. )} */}
  383. </div>
  384. {/* 判断是否是收费 是否是已经购买 */}
  385. {albumDetail.value?.paymentType === 'CHARGE' &&
  386. albumDetail.value?.orderStatus !== 'PAID' && (
  387. <ColSticky position="bottom" class={styles.btnStickyBottom}>
  388. <div
  389. class={[
  390. 'btnGroup',
  391. buyVip.value && !state.user.data.isVip && 'btnMore'
  392. ]}
  393. style={{ paddingTop: '12px' }}
  394. >
  395. <Button
  396. block
  397. round
  398. type="primary"
  399. style={{ fontSize: '16px' }}
  400. onClick={onBuy}
  401. color="linear-gradient(270deg, #FF204B 0%, #FE5B71 100%)"
  402. >
  403. 购买专辑
  404. </Button>
  405. {buyVip.value && !state.user.data.isVip && (
  406. <Button
  407. block
  408. round
  409. type="primary"
  410. style={{ fontSize: '16px' }}
  411. color="linear-gradient(270deg, #FF204B 0%, #FE5B71 100%)"
  412. onClick={() => {
  413. router.push({
  414. path: '/memberCenter',
  415. query: {
  416. ...route.query
  417. }
  418. })
  419. }}
  420. >
  421. {studentActivityId.value > 0 && (
  422. <div class={[styles.buttonDiscount]}>专属优惠</div>
  423. )}
  424. 开通会员
  425. </Button>
  426. )}
  427. </div>
  428. </ColSticky>
  429. )}
  430. <Popup
  431. v-model:show={shareStatus.value}
  432. style={{ background: 'transparent' }}
  433. class={styles.albumShare}
  434. >
  435. <ColShare
  436. type="tenant"
  437. teacherId={state.user.data.userId}
  438. shareUrl={shareUrl.value}
  439. shareType="album"
  440. shareLength={1}
  441. >
  442. <div class={styles.shareVip}>
  443. {shareDiscount.value === 1 && (
  444. <div class={styles.tagDiscount}>专属优惠</div>
  445. )}
  446. <img
  447. class={styles.icon}
  448. crossorigin="anonymous"
  449. src={albumDetail.value?.albumCoverUrl + `?t=${+new Date()}`}
  450. />
  451. <div class={styles.info}>
  452. <h4 class="van-multi-ellipsis--l2">
  453. {albumDetail.value?.albumName}
  454. </h4>
  455. <p
  456. class={['van-multi-ellipsis--l3']}
  457. style={{
  458. lineHeight: '16px',
  459. margin: '5px 0 10px 0'
  460. }}
  461. >
  462. {albumDetail.value?.albumDesc}
  463. </p>
  464. <div class={styles.shareAlumCollect}>
  465. <img src={icon_music_list} />
  466. <span>
  467. <span style="color: var(--van-primary-color);">
  468. {albumDetail.value?.musicSheetCount}
  469. </span>
  470. 首曲目
  471. </span>
  472. </div>
  473. </div>
  474. </div>
  475. <div class={[styles.shareVip, styles.shareMusicList]}>
  476. <SongShare list={shareMusicList.value} />
  477. </div>
  478. </ColShare>
  479. </Popup>
  480. </div>
  481. )
  482. }
  483. }
  484. })