index.tsx 19 KB

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