index.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664
  1. import {
  2. computed,
  3. defineComponent,
  4. onMounted,
  5. reactive,
  6. ref,
  7. watch,
  8. nextTick
  9. } from 'vue'
  10. import {
  11. Image,
  12. Tabs,
  13. Tab,
  14. List,
  15. Button,
  16. Popup,
  17. Dialog,
  18. Sticky,
  19. Swipe,
  20. SwipeItem
  21. } from 'vant'
  22. import styles from './index.module.less'
  23. import TheSticky from '@/components/the-sticky'
  24. import ColHeader from '@/components/col-header'
  25. import { useWindowScroll, useEventListener } from '@vueuse/core'
  26. import request from '@/helpers/request'
  27. import iconMenu from './images/icon-menu.png'
  28. import iconRightTop from './images/icon-right-top.png'
  29. import iconAlbumCover from '../../images/icon-album-cover.png'
  30. import iconTimer from './images/icon-timer.png'
  31. import { state as baseState, setLogout } from '@/state'
  32. import Song from '../component/song'
  33. import { useRoute, useRouter } from 'vue-router'
  34. import ColResult from '@/components/col-result'
  35. import { moneyFormat } from '@/helpers/utils'
  36. import { orderStatus } from '@/views/order-detail/orderStatus'
  37. import { postMessage } from '@/helpers/native-message'
  38. import { browser } from '@/helpers/utils'
  39. // Import Swiper Vue.js components
  40. // import Swiper core and required modules
  41. import { Pagination } from 'swiper/modules'
  42. import { Swiper, SwiperSlide } from 'swiper/vue'
  43. // Import Swiper styles
  44. import 'swiper/css'
  45. import 'swiper/css/pagination'
  46. import CourseItem from '../lessonCourseware/component/CourseItem'
  47. export default defineComponent({
  48. name: 'train-tool',
  49. setup() {
  50. const sessionStorageToolSubject =
  51. sessionStorage.getItem('tool-subject-type')
  52. const toolSubject =
  53. sessionStorageToolSubject && JSON.parse(sessionStorageToolSubject)
  54. sessionStorage.removeItem('tool-subject-type')
  55. const route = useRoute()
  56. const router = useRouter()
  57. const background = ref<string>('rgba(55, 205, 177, 0)')
  58. const color = ref<string>('#fff')
  59. const state = reactive({
  60. details: {} as any,
  61. buy: route.query.buy as any,
  62. albumId: route.query.albumId || null,
  63. activeTab:
  64. toolSubject?.activeTab || route.query.subjectType || 'COURSEWARE', // 有缓存 默认用缓存,之后用请求头,最后默认
  65. loadingAlbum: false,
  66. loading: false,
  67. finished: false,
  68. isError: false,
  69. list: [] as any,
  70. popupStatus: false,
  71. selectMember: {} as any, // 购买的月份
  72. ensembleCounts: false,
  73. musicCounts: false,
  74. subjectCounts: false,
  75. coursewareCounts: false,
  76. tenantAlbumStatus: 0 as any,
  77. ablumStatus: false,
  78. heightV: 0,
  79. hasBuyStatus: true, // 是否能继续购买
  80. albumList: [] as any, // 专辑列表
  81. initialSlide: 0
  82. })
  83. const params = reactive({
  84. page: 1,
  85. rows: 20
  86. })
  87. const apiSuffix = ref(
  88. baseState.platformType === 'STUDENT' ? '/api-student' : '/api-teacher'
  89. )
  90. const isSingleAlbum = computed(() => {
  91. const query = route.query
  92. if (query.taId || (query.albumId && state.buy === '1')) {
  93. return true
  94. } else {
  95. return false
  96. }
  97. })
  98. const getDetails = async () => {
  99. state.loadingAlbum = true
  100. try {
  101. // tenantGroupAlbum/buyAlbumInfo
  102. // 当我的曲目过来的时候才走单个查询
  103. if (state.albumId && state.buy === '1') {
  104. let url = apiSuffix.value + '/userTenantAlbumRecord/detail'
  105. if (state.albumId) {
  106. url = url + '?albumId=' + state.albumId
  107. }
  108. const { data } = await request.post(url)
  109. state.albumList = [data || {}]
  110. state.details = data || {}
  111. } else {
  112. const url =
  113. apiSuffix.value +
  114. `/tenantGroupAlbum/buyAlbumInfo?tenantGroupAlbumId=${
  115. route.query.taId || ''
  116. }`
  117. //&tenantAlbumId=${state.albumId || ''}
  118. // if (state.albumId) {
  119. // url = url + '?albumId=' + state.albumId
  120. // }
  121. const { data } = await request.get(url)
  122. state.albumList = data || []
  123. if (state.albumList.length > 0) {
  124. let index = 0
  125. // 以缓存为优先 其次 请求头 state.albumId
  126. if (toolSubject?.tenantGroupAlbumId || state.albumId) {
  127. index = state.albumList.findIndex(item => {
  128. return toolSubject?.tenantGroupAlbumId
  129. ? (baseState.platformType === 'STUDENT'
  130. ? item.tenantGroupAlbumId
  131. : item.id) === toolSubject?.tenantGroupAlbumId
  132. : item.id == state.albumId // 这里不全等 因为state.albumId为字符串 id为number
  133. })
  134. index < 0 && (index = 0)
  135. }
  136. state.initialSlide = index //默认展示第几个
  137. state.details = state.albumList[index] // 有缓存 就用缓存里面的数据
  138. } else {
  139. // state.albumList
  140. if (!browser().isApp) {
  141. Dialog.alert({
  142. title: '提示',
  143. message: '该教程不可购买',
  144. confirmButtonText: '确定',
  145. confirmButtonColor: '#2dc7aa'
  146. }).then(() => {
  147. if (browser().isApp) {
  148. postMessage({ api: 'back' })
  149. } else {
  150. setLogout()
  151. router.replace({
  152. path: '/login' as any,
  153. query: {
  154. returnUrl: '/train-tool',
  155. ...route.query
  156. }
  157. })
  158. }
  159. })
  160. }
  161. }
  162. }
  163. } catch {
  164. //
  165. }
  166. state.loadingAlbum = false
  167. }
  168. watch(
  169. () => state.details,
  170. () => {
  171. state.ensembleCounts = state.details?.ensembleCounts ? true : false
  172. state.subjectCounts = state.details?.subjectCounts ? true : false
  173. state.musicCounts = state.details?.musicCounts ? true : false
  174. state.coursewareCounts = state.details?.coursewareCounts ? true : false
  175. if (state.details.buyTimesFlag) {
  176. if (state.details.buyedTimes >= state.details.buyTimes) {
  177. state.hasBuyStatus = false
  178. } else {
  179. state.hasBuyStatus = true
  180. }
  181. } else {
  182. state.hasBuyStatus = true
  183. }
  184. }
  185. )
  186. let listController
  187. const FetchList = async (hideLoading = false) => {
  188. if (!state.details.id) {
  189. return
  190. }
  191. if (listController) {
  192. listController.abort()
  193. }
  194. state.loading = true
  195. state.isError = false
  196. const tempParams = {
  197. albumId: state.details.id || null,
  198. subjectType: state.activeTab,
  199. ...params
  200. }
  201. try {
  202. listController = new AbortController()
  203. const { signal } = listController
  204. const { data } = await request.post(
  205. `${apiSuffix.value}/tenantAlbumMusic/page`,
  206. {
  207. hideLoading,
  208. data: tempParams,
  209. signal
  210. }
  211. )
  212. if (state.list.length > 0 && data.pageNo === 1) {
  213. return
  214. }
  215. state.list = state.list.concat(data.rows || [])
  216. params.page = data.pageNo + 1
  217. // showContact.value = state.list.length > 0
  218. state.loading = false
  219. state.finished = data.pageNo >= data.totalPage
  220. params.page = data.pageNo + 1
  221. } catch (error) {
  222. state.isError = true
  223. }
  224. state.loading = false
  225. }
  226. onMounted(async () => {
  227. // useEventListener(document, 'scroll', evt => {
  228. // const { y } = useWindowScroll()
  229. // if (y.value > 20) {
  230. // background.value = `rgba(255, 255, 255)`
  231. // } else {
  232. // background.value = 'transparent'
  233. // }
  234. // })
  235. state.loading = true
  236. state.loadingAlbum = true
  237. await getDetails()
  238. await FetchList()
  239. state.loadingAlbum = false
  240. state.loading = false
  241. // 为了处理 swiper 会不显示的问题
  242. document.body.scrollIntoView()
  243. window.scrollTo(1, 0)
  244. })
  245. function handleChangeActiveTab() {
  246. state.activeTab = state.details?.coursewareCounts
  247. ? 'COURSEWARE'
  248. : state.details?.subjectCounts
  249. ? 'SUBJECT'
  250. : state.details?.musicCounts
  251. ? 'MUSIC'
  252. : 'ENSEMBLE'
  253. }
  254. const onSubmit = async () => {
  255. const album = state.details
  256. const details = state.details
  257. orderStatus.orderObject.orderType = 'TENANT_ALBUM'
  258. orderStatus.orderObject.orderName = details.name
  259. orderStatus.orderObject.orderDesc = details.name
  260. orderStatus.orderObject.actualPrice = album.actualPrice
  261. // orderStatus.orderObject.recomUserId = route.query.recomUserId || 0
  262. // orderStatus.orderObject.activityId = route.query.activityId || 0
  263. orderStatus.orderObject.orderNo = ''
  264. orderStatus.orderObject.orderList = [
  265. {
  266. orderType: 'TENANT_ALBUM',
  267. goodsName: details.name,
  268. actualPrice: album.actualPrice,
  269. price: album.actualPrice,
  270. ...details,
  271. ...album
  272. }
  273. ]
  274. const res = await request.post('/api-student/userOrder/getPendingOrder', {
  275. data: {
  276. goodType: 'TENANT_ALBUM',
  277. bizId: details.id
  278. }
  279. })
  280. const result = res.data
  281. if (result) {
  282. state.popupStatus = false
  283. Dialog.confirm({
  284. title: '提示',
  285. message: '您有一个未支付的订单,是否继续支付?',
  286. theme: 'round-button',
  287. className: 'confirm-button-group',
  288. cancelButtonText: '取消订单',
  289. confirmButtonText: '继续支付'
  290. })
  291. .then(async () => {
  292. orderStatus.orderObject.orderNo = result.orderNo
  293. orderStatus.orderObject.actualPrice = result.actualPrice
  294. orderStatus.orderObject.discountPrice = result.discountPrice
  295. orderStatus.orderObject.paymentConfig = {
  296. ...result.paymentConfig,
  297. paymentVendor: result.paymentVendor,
  298. paymentVersion: result.paymentVersion
  299. }
  300. routerTo()
  301. })
  302. .catch(() => {
  303. Dialog.close()
  304. // 只用取消订单,不用做其它处理
  305. cancelPayment(result.orderNo)
  306. })
  307. } else {
  308. routerTo()
  309. }
  310. }
  311. const routerTo = () => {
  312. const album = state.details
  313. sessionStorage.setItem(
  314. 'tool-subject-type',
  315. JSON.stringify({
  316. activeTab: state.activeTab,
  317. tenantGroupAlbumId:
  318. baseState.platformType === 'STUDENT'
  319. ? state.details.tenantGroupAlbumId
  320. : state.details.id // 老师用专辑id当唯一值
  321. })
  322. )
  323. router.push({
  324. path: '/orderDetail',
  325. query: {
  326. orderType: 'ALBUM',
  327. album: album.id
  328. }
  329. })
  330. }
  331. const cancelPayment = async (orderNo: string) => {
  332. try {
  333. await request.post('/api-student/userOrder/orderCancel/v2', {
  334. data: {
  335. orderNo
  336. }
  337. })
  338. } catch {
  339. //
  340. }
  341. }
  342. return () => (
  343. <div class={styles.trainTool}>
  344. {!state.loading && !state.details.id && state.buy != '1' ? (
  345. <>
  346. <TheSticky
  347. class={styles.theSticky}
  348. position="top"
  349. onBarHeight={(height: any) => {
  350. console.log(height, 'height', height)
  351. state.heightV = height
  352. }}
  353. >
  354. <ColHeader border={false} isFixed={false} />
  355. </TheSticky>
  356. {!state.loading && (
  357. <div
  358. class={styles.colResultBox}
  359. style={{
  360. height: 'calc(100vh - var(--header-height))',
  361. display: 'flex',
  362. alignItems: 'center'
  363. }}
  364. >
  365. <ColResult
  366. tips="暂无教程"
  367. classImgSize="SMALL"
  368. btnStatus={false}
  369. />
  370. </div>
  371. )}
  372. </>
  373. ) : (
  374. !state.loadingAlbum && (
  375. <>
  376. <TheSticky
  377. class={styles.theSticky}
  378. position="top"
  379. onBarHeight={(height: any) => {
  380. state.heightV = height
  381. }}
  382. >
  383. <ColHeader
  384. background={background.value}
  385. border={false}
  386. isFixed={false}
  387. hideHeader={route.query.taId ? true : false}
  388. // color={color.value}
  389. // backIconColor="white"
  390. />
  391. </TheSticky>
  392. {/* <img class={styles.bgImg} src={state.details?.coverImg} /> */}
  393. <div class={styles.musicContent}></div>
  394. <div class={styles.bg}>
  395. <div class={styles.alumWrap}>
  396. {isSingleAlbum.value ? (
  397. <div class={styles.singleAlbum}>
  398. <div class={styles.img}>
  399. {state.details?.buyTimesFlag && (
  400. <span class={styles.quota}>
  401. 限购:{state.details?.buyedTimes}/
  402. {state.details?.buyTimes}次
  403. </span>
  404. )}
  405. <Image
  406. class={styles.image}
  407. width="100%"
  408. height="100%"
  409. fit="cover"
  410. src={state.details?.coverImg || iconAlbumCover}
  411. errorIcon={iconAlbumCover}
  412. />
  413. <div class={styles.iconPian}></div>
  414. </div>
  415. </div>
  416. ) : (
  417. state.albumList &&
  418. state.albumList.length > 0 && (
  419. <Swiper
  420. initialSlide={state.initialSlide}
  421. watchSlidesProgress={true}
  422. slidesPerView={'auto'}
  423. centeredSlides={true}
  424. modules={[Pagination]}
  425. pagination={{ clickable: true }}
  426. // onTransitionEnd={(swiper: any) => {}} onSlideChange
  427. onSlideChange={(swiper: any) => {
  428. state.details = state.albumList[swiper.activeIndex]
  429. // 等tab渲染完了之后再切换 不然tab会自动重新赋值
  430. nextTick(() => {
  431. // 当有初始值的时候不刷新
  432. if (state.initialSlide) {
  433. state.initialSlide = 0
  434. return
  435. }
  436. handleChangeActiveTab()
  437. params.page = 1
  438. state.list = []
  439. FetchList(true)
  440. })
  441. }}
  442. >
  443. {state.albumList.map((album: any) => (
  444. <SwiperSlide>
  445. <div class={styles.img}>
  446. {album.buyTimesFlag && (
  447. <span class={styles.quota}>
  448. 限购{album.buyedTimes}/{album.buyTimes}次
  449. </span>
  450. )}
  451. <Image
  452. class={styles.image}
  453. width="100%"
  454. height="100%"
  455. fit="cover"
  456. src={album?.coverImg || iconAlbumCover}
  457. errorIcon={iconAlbumCover}
  458. />
  459. <div class={styles.iconPian}></div>
  460. </div>
  461. </SwiperSlide>
  462. ))}
  463. </Swiper>
  464. )
  465. )}
  466. <div class={styles.alumDes}>
  467. <div class={[styles.alumTitle, 'van-ellipsis']}>
  468. {state.details?.name}
  469. </div>
  470. <div
  471. class={[styles.des, 'van-multi-ellipsis--l2']}
  472. style={{
  473. height: '32px',
  474. lineHeight: '16px'
  475. }}
  476. >
  477. {state.details?.describe}
  478. </div>
  479. </div>
  480. {state.buy != '1' && baseState.platformType === 'STUDENT' && (
  481. <div class={styles.albumPriceGroup}>
  482. <div class={styles.albumTimer}>
  483. <img src={iconTimer} class={styles.iconTimer} />
  484. <span>有效期:{state.details?.purchaseNum || 0}天</span>
  485. </div>
  486. <div class={styles.albumPriceList}>
  487. {(state.details?.originalPrice || 0) >
  488. (state.details?.actualPrice || 0) && (
  489. <del class={styles.originPrice}>
  490. 原价:¥
  491. {moneyFormat(state.details?.originalPrice || 0)}
  492. </del>
  493. )}
  494. <span class={styles.currentPrice}>
  495. <span>
  496. ¥{moneyFormat(state.details?.actualPrice || 0)}
  497. </span>
  498. </span>
  499. </div>
  500. </div>
  501. )}
  502. </div>
  503. </div>
  504. <div class={styles.musicList}>
  505. <Sticky position="top" offsetTop={state.heightV}>
  506. <Tabs
  507. color="var(--van-primary)"
  508. background="transparent"
  509. lineWidth={20}
  510. shrink
  511. v-model:active={state.activeTab}
  512. onClick-tab={val => {
  513. state.activeTab = val.name
  514. params.page = 1
  515. state.list = []
  516. FetchList()
  517. }}
  518. >
  519. {state.coursewareCounts && (
  520. <Tab title="云课堂" name="COURSEWARE"></Tab>
  521. )}
  522. {state.subjectCounts && (
  523. <Tab title="声部云练" name="SUBJECT"></Tab>
  524. )}
  525. {state.musicCounts && (
  526. <Tab title="独奏云练" name="MUSIC"></Tab>
  527. )}
  528. {state.ensembleCounts && (
  529. <Tab title="合奏云练" name="ENSEMBLE"></Tab>
  530. )}
  531. </Tabs>
  532. </Sticky>
  533. <div
  534. class={[
  535. styles.alumnList,
  536. state.activeTab === 'COURSEWARE'
  537. ? styles.alumnListCourseware
  538. : ''
  539. ]}
  540. >
  541. <List
  542. loading={state.loading}
  543. finished={state.finished}
  544. finished-text={' '}
  545. onLoad={FetchList}
  546. immediateCheck={false}
  547. error={state.isError}
  548. >
  549. {state.list && state.list.length ? (
  550. state.activeTab === 'COURSEWARE' ? (
  551. <CourseItem
  552. list={state.list.map(item => {
  553. return {
  554. name: item.musicSheetName,
  555. coverImg: item.titleImg,
  556. id: item.id
  557. }
  558. })}
  559. onItemClick={row => {
  560. sessionStorage.setItem(
  561. 'tool-subject-type',
  562. JSON.stringify({
  563. activeTab: state.activeTab,
  564. tenantGroupAlbumId:
  565. baseState.platformType === 'STUDENT'
  566. ? state.details.tenantGroupAlbumId
  567. : state.details.id // 老师用专辑id当唯一值
  568. })
  569. )
  570. router.push({
  571. path: '/courseList',
  572. query: {
  573. id: row.id,
  574. albumId: state.details.id,
  575. taId: state.details.tenantGroupAlbumId, // 当通过我的曲目进来的时候 这个值为空
  576. buyStatus: state.hasBuyStatus ? '0' : '1' //默认能购买
  577. }
  578. })
  579. }}
  580. />
  581. ) : (
  582. <Song
  583. showNumber
  584. list={state.list}
  585. onDetail={(item: any) => {
  586. sessionStorage.setItem(
  587. 'tool-subject-type',
  588. JSON.stringify({
  589. activeTab: state.activeTab,
  590. tenantGroupAlbumId:
  591. baseState.platformType === 'STUDENT'
  592. ? state.details.tenantGroupAlbumId
  593. : state.details.id // 老师用专辑id当唯一值
  594. })
  595. )
  596. router.push({
  597. path: '/music-detail',
  598. query: {
  599. id: item.id,
  600. tenantAlbumId: item.tenantAlbumId,
  601. taId: state.details.tenantGroupAlbumId, // 当通过我的曲目进来的时候 这个值为空
  602. buyStatus: state.hasBuyStatus ? '0' : '1' //默认能购买
  603. }
  604. })
  605. }}
  606. />
  607. )
  608. ) : (
  609. !state.loading && (
  610. <ColResult
  611. tips={
  612. state.activeTab === 'COURSEWARE'
  613. ? '暂无教材'
  614. : '暂无曲目'
  615. }
  616. classImgSize="SMALL"
  617. btnStatus={false}
  618. />
  619. )
  620. )}
  621. </List>
  622. </div>
  623. </div>
  624. {baseState.platformType === 'STUDENT' && state.buy != '1' && (
  625. <TheSticky position="bottom">
  626. <div class={styles.btnGroup}>
  627. <Button
  628. round
  629. block
  630. disabled={!state.hasBuyStatus}
  631. color="linear-gradient(270deg, #FF204B 0%, #FE5B71 100%)"
  632. onClick={onSubmit}
  633. >
  634. 开通训练教程
  635. </Button>
  636. </div>
  637. </TheSticky>
  638. )}
  639. </>
  640. )
  641. )}
  642. </div>
  643. )
  644. }
  645. })