index.tsx 17 KB

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