index.tsx 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107
  1. import {
  2. computed,
  3. defineComponent,
  4. nextTick,
  5. onMounted,
  6. onUnmounted,
  7. reactive,
  8. ref,
  9. watch
  10. } from 'vue'
  11. import umiRequest from 'umi-request'
  12. import { useRoute, useRouter } from 'vue-router'
  13. import request from '@/helpers/request'
  14. import ColHeader from '@/components/col-header'
  15. import {
  16. Button,
  17. Cell,
  18. CellGroup,
  19. Checkbox,
  20. Dialog,
  21. Icon,
  22. Image,
  23. Popup,
  24. RadioGroup,
  25. Sticky,
  26. Tag,
  27. Radio,
  28. Toast,
  29. Picker
  30. } from 'vant'
  31. import styles from './index.module.less'
  32. // import Item from '../list/item'
  33. import { useRect } from '@vant/use'
  34. import { Vue3Lottie } from 'vue3-lottie'
  35. import { getRandomKey, musicBuy } from '../music'
  36. import { getOssUploadUrl, state } from '@/state'
  37. import { useEventTracking } from '@/helpers/hooks'
  38. import ColSticky from '@/components/col-sticky'
  39. import { browser, moneyFormat } from '@/helpers/utils'
  40. import { orderStatus } from '@/views/order-detail/orderStatus'
  41. import iconShare from '@/views/music/album/icon_share.svg'
  42. import iconAlbum from '@/views/music/component/images/icon_album.png'
  43. import iconDownload from './images/icon_download.png'
  44. import iconChangeStaff from './images/icon-change-staff.png'
  45. import AstronautJSON from './animate/bigLoad.json'
  46. import ColShare from '@/components/col-share'
  47. import iconCollect from './images/icon_collect.png'
  48. import iconCollectActive from './images/icon_collect_active.png'
  49. import iconListen from './images/icon_listen.png'
  50. import iconTeacher from '@common/images/icon_teacher.png'
  51. import emtpy from './images/emtpy.png'
  52. import activeButtonIcon from '@common/images/icon_checkbox.png'
  53. import inactiveButtonIcon from '@common/images/icon_checkbox_default.png'
  54. import staffDetafult from './images/staff-default.png'
  55. import staffActive from './images/staff-active.png'
  56. import firstDefault from './images/first-default.png'
  57. import firstActive from './images/first-active.png'
  58. import fixedDefault from './images/fixed-default.png'
  59. import fixedActive from './images/fixed-active.png'
  60. import Plyr from 'plyr'
  61. import 'plyr/dist/plyr.css'
  62. import Download from './download'
  63. import { getInstrumentName } from '@/constant/instruments'
  64. export const getAssetsHomeFile = (fileName: string) => {
  65. const path = `../component/images/${fileName}`
  66. const modules = import.meta.globEager('../component/images/*')
  67. return modules[path].default
  68. }
  69. export default defineComponent({
  70. name: 'MusicDetail',
  71. setup() {
  72. localStorage.setItem('behaviorId', getRandomKey())
  73. const router = useRouter()
  74. const route = useRoute()
  75. const loading = ref(false)
  76. const aId = Number(route.query.activityId) || 0
  77. const studentActivityId = ref(aId)
  78. const isError = ref(false)
  79. const headers = ref(null)
  80. const footers = ref(null)
  81. const heightInfo = ref<any>('0')
  82. const musicDetail = ref<any>(null)
  83. const audioFileUrl = ref('')
  84. let showImg = [] as any
  85. const firstList = ref<Array<any>>([])
  86. const fixedList = ref<Array<any>>([])
  87. const staffList = ref<Array<any>>([])
  88. const accompanyUrl = ref<string>('')
  89. const downloadStatus = ref<boolean>(false)
  90. const staff = reactive({
  91. status: false,
  92. radio: 'staff' // staff first fixed
  93. })
  94. const colors: any = {
  95. FREE: {
  96. color: '#01B84F',
  97. text: '免费'
  98. },
  99. VIP: {
  100. color: '#CD863E',
  101. text: '会员'
  102. },
  103. CHARGE: {
  104. color: '#3591CE',
  105. text: '点播'
  106. }
  107. }
  108. // 更改预览状态
  109. const onChangeStaff = (type: string) => {
  110. staff.radio = type
  111. staff.status = false
  112. }
  113. watch(
  114. () => staff.radio,
  115. (val: string) => {
  116. if (val == 'first') {
  117. showImg = firstList.value
  118. } else if (val == 'fixed') {
  119. showImg = fixedList.value
  120. } else {
  121. showImg = staffList.value
  122. }
  123. }
  124. )
  125. const FetchList = async (id?: any) => {
  126. if (loading.value) {
  127. return
  128. }
  129. loading.value = true
  130. isError.value = false
  131. try {
  132. const res = await request.get(`/music/sheet/detail/${route.query.id}`, {
  133. prefix:
  134. state.platformType === 'TEACHER' ? '/api-teacher' : '/api-student'
  135. })
  136. musicDetail.value = res.data
  137. console.log(musicDetail.value.notation, 'musicDetail')
  138. // 取原音,如果有多个则默认第一个
  139. const background = res.data.background
  140. audioFileUrl.value =
  141. background && background.length > 0 ? background[0].audioFileUrl : ''
  142. // const arrImgs = res.data.musicImg ? res.data.musicImg.split(',') : []
  143. showImg = res.data.musicImg ? res.data.musicImg.split(',') : []
  144. firstList.value = res.data.firstTone
  145. ? res.data.firstTone.split(',')
  146. : []
  147. fixedList.value = res.data.fixedTone
  148. ? res.data.fixedTone.split(',')
  149. : []
  150. staffList.value = res.data.musicImg ? res.data.musicImg.split(',') : []
  151. // if (!showImg.value) {
  152. // setAccompanyUrl()
  153. // window.addEventListener(
  154. // 'message',
  155. // async e => {
  156. // // 给图片设置背景色
  157. // const tempCanvas = await imgToCanvas(e.data)
  158. // const img = convasToImg(tempCanvas)
  159. // // 开始上传图片
  160. // uploadFunction(img)
  161. // },
  162. // false
  163. // )
  164. // }
  165. nextTick(() => {
  166. renderStaff()
  167. })
  168. } catch (error) {
  169. isError.value = true
  170. }
  171. if (musicDetail.value?.musicSheetType !== 'CONCERT') {
  172. loading.value = false
  173. }
  174. }
  175. const base64ToBlob = data => {
  176. const arr = data.split(','),
  177. mime = arr[0].match(/:(.*?);/)[1]
  178. const bstr = atob(arr[1])
  179. let n = bstr.length
  180. const u8arr = new Uint8Array(n)
  181. while (n--) {
  182. u8arr[n] = bstr.charCodeAt(n)
  183. }
  184. return new Blob([u8arr], { type: mime })
  185. }
  186. const uploadFunction = async file => {
  187. try {
  188. const formData = new FormData()
  189. const fileName =
  190. new Date().getTime() + Math.ceil(Math.random() * 1000) + '.png'
  191. const keyTime = new Date().getTime() + fileName
  192. const obj = {
  193. filename: fileName,
  194. bucketName: 'cloud-coach',
  195. postData: {
  196. filename: fileName,
  197. acl: 'public-read',
  198. key: keyTime
  199. }
  200. }
  201. const res = await request.post(state.platformApi + '/getUploadSign', {
  202. data: obj
  203. })
  204. Toast.loading({
  205. message: '加载中...',
  206. forbidClick: true,
  207. loadingType: 'spinner',
  208. duration: 0
  209. })
  210. const dataObj = {
  211. policy: res.data.policy,
  212. signature: res.data.signature,
  213. key: keyTime,
  214. KSSAccessKeyId: res.data.kssAccessKeyId,
  215. acl: 'public-read',
  216. name: fileName
  217. }
  218. for (const key in dataObj) {
  219. formData.append(key, dataObj[key])
  220. }
  221. const files = base64ToBlob(file)
  222. formData.append('file', files, fileName)
  223. const ossUploadUrl = getOssUploadUrl('cloud-coach')
  224. await umiRequest(ossUploadUrl, {
  225. method: 'POST',
  226. data: formData
  227. })
  228. Toast.clear()
  229. const imgurl = getOssUploadUrl('cloud-coach') + keyTime
  230. await request.post(state.platformApi + '/open/music/sheet/img', {
  231. data: { musicSheetId: musicDetail.value.id, musicImg: imgurl }
  232. })
  233. // showImg.value = imgurl
  234. } catch (e) {
  235. console.log(e)
  236. }
  237. }
  238. const setAccompanyUrl = () => {
  239. let url = location.origin
  240. if (
  241. location.host.includes('dev.colexiu') ||
  242. location.host.includes('192.168') ||
  243. location.host.includes('localhost')
  244. ) {
  245. url = 'https://dev.colexiu.com'
  246. }
  247. const music = musicDetail.value
  248. let subjectId = ''
  249. if (music.background && music.background.length > 0) {
  250. subjectId = music.background[0].id
  251. }
  252. accompanyUrl.value =
  253. url +
  254. `/accompany/colxiu-website.html?id=${music.id}&part-index=${subjectId}`
  255. }
  256. const player = ref<any>(null)
  257. const audio = ref<any>(null)
  258. const freeRate = ref<any>(0)
  259. const initAudio = async () => {
  260. const controls = [
  261. 'play-large',
  262. 'play',
  263. 'progress',
  264. 'captions',
  265. // 'fullscreen',
  266. 'duration'
  267. ]
  268. player.value = new Plyr(audio.value, {
  269. controls: controls
  270. })
  271. const config = await request.get(
  272. '/api-student/sysConfig/queryByParamNameList',
  273. {
  274. params: {
  275. paramNames: 'music_sheet_free_rate'
  276. }
  277. }
  278. )
  279. freeRate.value = config.data[0]?.paramValue || 0
  280. player.value.on('timeupdate', () => {
  281. // 允许播放时间
  282. const players = player.value
  283. const playTime = (players.duration * freeRate.value) / 100 || 0
  284. // 时间,不能播放
  285. if (players.currentTime >= playTime && !buyState.value.play) {
  286. players.stop()
  287. // players.pause()
  288. }
  289. })
  290. }
  291. const showLoading = (e: any) => {
  292. console.log(e)
  293. if (e.data?.api === 'musicStaffRender') {
  294. loading.value = e.data.loading
  295. }
  296. }
  297. onMounted(async () => {
  298. await FetchList()
  299. const { height } = useRect(headers as any)
  300. const footer = useRect(footers as any)
  301. heightInfo.value = height + footer.height
  302. // 初始化音频
  303. if (audioFileUrl.value) {
  304. initAudio()
  305. }
  306. window.addEventListener('message', showLoading)
  307. })
  308. onUnmounted(() => {
  309. window.removeEventListener('message', showLoading)
  310. })
  311. const toggleFavorite = async () => {
  312. try {
  313. await request.post('/music/sheet/favorite/' + musicDetail.value?.id, {
  314. prefix:
  315. state.platformType === 'TEACHER' ? '/api-teacher' : '/api-student'
  316. })
  317. musicDetail.value.favorite = musicDetail.value?.favorite ? 0 : 1
  318. musicDetail.value.favoriteCount = musicDetail.value?.favorite
  319. ? musicDetail.value.favoriteCount + 1
  320. : musicDetail.value.favoriteCount - 1 < 0
  321. ? 0
  322. : musicDetail.value.favoriteCount - 1
  323. setTimeout(() => {
  324. Toast(musicDetail.value?.favorite ? '收藏成功' : '取消收藏成功')
  325. }, 100)
  326. } catch (error) {
  327. //
  328. }
  329. }
  330. const onAddCourse = async () => {
  331. try {
  332. const res = await request.post('/api-teacher/courseCourseware/submit', {
  333. data: {
  334. musicSheetId: musicDetail.value.id,
  335. clientType: 'TEACHER',
  336. userId: state.user.data?.userId
  337. }
  338. })
  339. console.log(res)
  340. setTimeout(() => {
  341. musicDetail.value.coursewareId = res.data.id || ''
  342. Toast('添加成功')
  343. musicDetail.value.coursewareStatus = 1
  344. }, 100)
  345. } catch {
  346. //
  347. }
  348. }
  349. const removeCourse = async () => {
  350. Dialog.confirm({
  351. title: '提示',
  352. message: '您是否确定移除课件',
  353. confirmButtonColor: '#269a93',
  354. cancelButtonText: '取消',
  355. confirmButtonText: '确定'
  356. }).then(async () => {
  357. try {
  358. await request.post(
  359. '/api-teacher/courseCourseware/remove/' +
  360. musicDetail.value.coursewareId,
  361. {
  362. data: {}
  363. }
  364. )
  365. setTimeout(() => {
  366. Toast('移除成功')
  367. musicDetail.value.coursewareStatus = 0
  368. }, 100)
  369. } catch {
  370. //
  371. }
  372. })
  373. }
  374. const onBuy = async () => {
  375. const music = musicDetail.value
  376. orderStatus.orderObject.orderType = 'MUSIC'
  377. orderStatus.orderObject.orderName = music.musicSheetName
  378. orderStatus.orderObject.orderDesc = music.musicSheetName
  379. orderStatus.orderObject.actualPrice = music.musicPrice
  380. orderStatus.orderObject.recomUserId = route.query.recomUserId || 0
  381. orderStatus.orderObject.activityId = route.query.activityId || 0
  382. orderStatus.orderObject.orderNo = ''
  383. orderStatus.orderObject.orderList = [
  384. {
  385. orderType: 'MUSIC',
  386. goodsName: music.musicSheetName,
  387. actualPrice: music.musicPrice,
  388. ...music
  389. }
  390. ]
  391. const res = await request.post('/api-student/userOrder/getPendingOrder', {
  392. data: {
  393. goodType: 'MUSIC',
  394. bizId: music.id
  395. }
  396. })
  397. const result = res.data
  398. if (result) {
  399. Dialog.confirm({
  400. title: '提示',
  401. message: '您有一个未支付的订单,是否继续支付?',
  402. confirmButtonColor: '#269a93',
  403. cancelButtonText: '取消订单',
  404. confirmButtonText: '继续支付'
  405. })
  406. .then(async () => {
  407. orderStatus.orderObject.orderNo = result.orderNo
  408. orderStatus.orderObject.actualPrice = result.actualPrice
  409. orderStatus.orderObject.discountPrice = result.discountPrice
  410. routerTo()
  411. })
  412. .catch(() => {
  413. Dialog.close()
  414. // 只用取消订单,不用做其它处理
  415. cancelPayment(result.orderNo)
  416. })
  417. } else {
  418. routerTo()
  419. }
  420. }
  421. const routerTo = () => {
  422. const music = musicDetail.value
  423. router.push({
  424. path: '/orderDetail',
  425. query: {
  426. orderType: 'MUSIC',
  427. musicId: music.id
  428. }
  429. })
  430. }
  431. const cancelPayment = async (orderNo: string) => {
  432. try {
  433. await request.post('/api-student/userOrder/orderCancel', {
  434. data: {
  435. orderNo
  436. }
  437. })
  438. } catch {}
  439. }
  440. const paymentType = computed(() => {
  441. let paymentType = musicDetail.value?.paymentType
  442. if (typeof paymentType === 'string') {
  443. paymentType = paymentType.split(',')
  444. return paymentType
  445. }
  446. return []
  447. })
  448. const buyState = computed(() => {
  449. const music = musicDetail.value
  450. return {
  451. play: music.play ? true : false, // 是否可以播放
  452. free: music?.paymentType.includes('FREE'),
  453. charge: music?.paymentType.includes('CHARGE'),
  454. vip: music?.paymentType.includes('VIP'),
  455. buy: music?.orderStatus === 'PAID' // 是否已买
  456. }
  457. })
  458. const shareStatus = ref(false)
  459. const shareUrl = ref('')
  460. const shareDiscount = ref(0)
  461. // console.log(data)
  462. const onShare = async () => {
  463. try {
  464. const res = await request.post('/api-teacher/open/musicShareProfit', {
  465. data: {
  466. bizId: musicDetail.value?.id,
  467. userId: state.user.data?.userId
  468. }
  469. })
  470. let url =
  471. location.origin +
  472. `/teacher/#/shareMusic?id=${musicDetail.value?.id}&recomUserId=${state.user.data?.userId}&userType=${state.platformType}`
  473. // 判断是否有活动
  474. if (res.data.discount === 1) {
  475. url += `&activityId=${res.data.activityId}`
  476. }
  477. shareDiscount.value = res.data.discount || 0
  478. console.log(url)
  479. shareUrl.value = url
  480. shareStatus.value = true
  481. return
  482. } catch {}
  483. }
  484. const staffData = reactive({
  485. open: false,
  486. iframeSrc: '',
  487. musicXml: '',
  488. instrumentName: '',
  489. iframeRef: null as any,
  490. partIndex: 0,
  491. partList: [] as any[]
  492. })
  493. /** 渲染五线谱 */
  494. const renderStaff = () => {
  495. staffData.iframeSrc = `${location.origin}${location.pathname}osmd/index.html`
  496. staffData.musicXml = musicDetail.value?.xmlFileUrl || ''
  497. staffData.partList = musicDetail.value?.background || []
  498. staffData.instrumentName = getInstrumentName(
  499. staffData.partList[staffData.partIndex]?.track
  500. )
  501. }
  502. const musicIframeLoad = () => {
  503. const iframeRef: any = document.getElementById('staffIframeRef')
  504. if (iframeRef && iframeRef.contentWindow.renderXml) {
  505. iframeRef.contentWindow.renderXml(
  506. staffData.musicXml,
  507. staffData.partIndex
  508. )
  509. }
  510. }
  511. const resetRender = () => {
  512. const iframeRef: any = document.getElementById('staffIframeRef')
  513. if (iframeRef && iframeRef.contentWindow.renderXml) {
  514. iframeRef.contentWindow.resetRender(staffData.partIndex)
  515. staffData.instrumentName = getInstrumentName(
  516. staffData.partList[staffData.partIndex]?.track
  517. )
  518. }
  519. }
  520. const partColumns = computed(() => {
  521. return staffData.partList.map((item: any, index: number) => {
  522. const instrumentName = getInstrumentName(item.track)
  523. return {
  524. text: item.track + (instrumentName ? `(${instrumentName})` : ''),
  525. value: index
  526. }
  527. })
  528. })
  529. return () => {
  530. return (
  531. <div class={styles.detail}>
  532. <Sticky position="top">
  533. <div ref={headers}>
  534. <ColHeader
  535. background="transparent"
  536. border={false}
  537. isFixed={false}
  538. color="#fff"
  539. title={musicDetail.value?.musicSheetName}
  540. backIconColor="white"
  541. v-slots={{
  542. right: () => (
  543. <div
  544. class={styles.shareBtn}
  545. style={{
  546. color: '#fff'
  547. }}
  548. onClick={onShare}
  549. >
  550. <Image src={iconShare} />
  551. 分享
  552. </div>
  553. )
  554. }}
  555. />
  556. </div>
  557. </Sticky>
  558. <img class={styles.bgImg} src={musicDetail.value?.titleImg} />
  559. <div class={styles.bgContent}></div>
  560. <div
  561. class={styles.musicContainer}
  562. style={{
  563. marginTop: '16px',
  564. height: `calc(100vh - ${heightInfo.value + 16 + 'px'})`
  565. }}
  566. >
  567. <Cell
  568. border={false}
  569. center
  570. class={styles.musicInfo}
  571. v-slots={{
  572. icon: () => (
  573. <Image
  574. class={styles.pImg}
  575. src={musicDetail.value?.titleImg}
  576. />
  577. ),
  578. title: () => (
  579. <div class={styles.info}>
  580. <h4
  581. class="van-ellipsis"
  582. // onClick={() => handleGotoMusicScore(musicDetail.value)}
  583. >
  584. {musicDetail.value?.musicSheetName}
  585. </h4>
  586. <p
  587. style={{
  588. display: 'flex'
  589. }}
  590. >
  591. {paymentType.value.map(tag => (
  592. <Tag
  593. style={{ color: colors[tag].color }}
  594. class={styles.tag}
  595. type="success"
  596. plain
  597. >
  598. {colors[tag].text}
  599. </Tag>
  600. ))}
  601. {musicDetail.value?.exquisiteFlag === 1 && (
  602. <Image
  603. class={styles.exquisiteFlag}
  604. src={getAssetsHomeFile('icon_exquisite.png')}
  605. />
  606. )}
  607. {musicDetail.value?.albumNums > 0 && (
  608. <Image
  609. class={styles.songAlbum}
  610. src={getAssetsHomeFile('icon_album_active.png')}
  611. />
  612. )}
  613. <span class={styles.coomposer}>
  614. {musicDetail.value?.composer}
  615. </span>
  616. </p>
  617. </div>
  618. ),
  619. value: () => (
  620. <>
  621. <div
  622. class="van-cell__value"
  623. style={{
  624. display:
  625. musicDetail.value?.musicSheetType === 'SINGLE'
  626. ? ''
  627. : 'none'
  628. }}
  629. >
  630. {musicDetail.value?.notation ? (
  631. <span
  632. class={styles.download}
  633. onClick={() => {
  634. staff.status = true
  635. }}
  636. >
  637. <img src={iconChangeStaff} />
  638. <span>转谱</span>
  639. </span>
  640. ) : null}
  641. <span
  642. class={styles.download}
  643. onClick={() => {
  644. if (showImg.length > 0) {
  645. downloadStatus.value = true
  646. } else {
  647. Toast('暂无图片')
  648. }
  649. }}
  650. >
  651. <img src={iconDownload} />
  652. <span>下载曲谱</span>
  653. </span>
  654. </div>
  655. <span
  656. style={{
  657. display:
  658. musicDetail.value?.musicSheetType === 'CONCERT'
  659. ? ''
  660. : 'none'
  661. }}
  662. class={styles.download}
  663. onClick={() => {
  664. staffData.open = true
  665. }}
  666. >
  667. <Icon
  668. style={{
  669. background: 'rgba(246,246,246,1)',
  670. borderRadius: '50%',
  671. padding: '4px'
  672. }}
  673. size="20px"
  674. name="exchange"
  675. />
  676. <span>切换乐器</span>
  677. </span>
  678. </>
  679. )
  680. }}
  681. />
  682. <div class={styles.musicContent}>
  683. <p class={styles.musicTitle}>
  684. {(musicDetail.value?.musicSheetName
  685. ? musicDetail.value?.musicSheetName
  686. : '') +
  687. (staffData.instrumentName
  688. ? `(${staffData.instrumentName})`
  689. : '')}
  690. </p>
  691. {musicDetail.value?.musicSheetType === 'CONCERT' ? (
  692. <>
  693. {loading.value && (
  694. <>
  695. <Vue3Lottie
  696. animationData={AstronautJSON}
  697. class={styles.finch}
  698. ></Vue3Lottie>
  699. <p class={styles.finchLoad}>加载中...</p>
  700. </>
  701. )}
  702. <iframe
  703. id="staffIframeRef"
  704. src={staffData.iframeSrc}
  705. onLoad={musicIframeLoad}
  706. ></iframe>
  707. </>
  708. ) : (
  709. <>
  710. {showImg.length > 0 ? (
  711. <img src={showImg[0]} alt="" class={styles.musicImg} />
  712. ) : loading.value ? (
  713. <>
  714. <Vue3Lottie
  715. animationData={AstronautJSON}
  716. class={styles.finch}
  717. ></Vue3Lottie>
  718. <p class={styles.finchLoad}>加载中...</p>
  719. </>
  720. ) : (
  721. <div class={styles.empty}>
  722. <Image src={emtpy} class={styles.emptyImg} />
  723. <p class={styles.emptyTip}>暂无乐谱预览图</p>
  724. </div>
  725. )}
  726. </>
  727. )}
  728. <div class={styles.videoOperation}>
  729. {audioFileUrl.value && (
  730. <>
  731. {!buyState.value.play &&
  732. freeRate.value != 100 &&
  733. freeRate.value != 0 && (
  734. <div class={[styles.audition]}>
  735. <img src={iconListen} />
  736. <span>每首曲目可试听{freeRate.value}%</span>
  737. </div>
  738. )}
  739. <div class={[styles.audio, styles.collectCell]}>
  740. <audio id="player" controls ref={audio}>
  741. <source src={audioFileUrl.value} type="audio/mp3" />
  742. </audio>
  743. </div>
  744. </>
  745. )}
  746. <div class={[styles.collect, styles.collectCell]}>
  747. <div
  748. class={[styles.userInfo]}
  749. onClick={() => {
  750. if (
  751. browser().isApp &&
  752. musicDetail.value?.sourceType === 'TEACHER' &&
  753. state.platformType === 'STUDENT'
  754. ) {
  755. router.push({
  756. path: '/teacherHome',
  757. query: {
  758. teacherId: musicDetail.value?.userId,
  759. tabs: 'music'
  760. }
  761. })
  762. }
  763. }}
  764. >
  765. <img src={musicDetail.value?.userAvatar || iconTeacher} />
  766. <span>{musicDetail.value?.userName}</span>
  767. </div>
  768. <div class={styles.functionSection}>
  769. <div
  770. class={[styles.collectSection]}
  771. onClick={() => toggleFavorite()}
  772. >
  773. <span>{musicDetail.value?.favoriteCount}人收藏</span>
  774. <img
  775. src={
  776. musicDetail.value?.favorite
  777. ? iconCollectActive
  778. : iconCollect
  779. }
  780. />
  781. </div>
  782. {state.platformType === 'TEACHER' && (
  783. <div
  784. class={[styles.collectSection]}
  785. onClick={() => {
  786. if (musicDetail.value?.coursewareStatus) {
  787. removeCourse()
  788. } else {
  789. onAddCourse()
  790. }
  791. }}
  792. >
  793. <span>
  794. {musicDetail.value?.coursewareStatus
  795. ? '移出课件'
  796. : '添加到课件'}
  797. </span>
  798. {musicDetail.value?.coursewareStatus ? (
  799. <Icon name="clear" />
  800. ) : (
  801. <Icon name="add" size={18} />
  802. )}
  803. </div>
  804. )}
  805. </div>
  806. </div>
  807. </div>
  808. </div>
  809. <div
  810. class={[styles.lookAlbum, styles.collectCell]}
  811. onClick={() => {
  812. router.push({
  813. path: '/look-album-list',
  814. query: {
  815. id: musicDetail.value?.id,
  816. musicSubject: musicDetail.value?.musicSubject
  817. }
  818. })
  819. }}
  820. >
  821. <div>
  822. <img src={iconAlbum} />
  823. <span>进入曲目所在专辑列表</span>
  824. </div>
  825. <Icon name="arrow" size={16} color="#666" />
  826. </div>
  827. </div>
  828. {musicDetail.value?.id && (
  829. <ColSticky position="bottom" background="white">
  830. <div ref={footers}>
  831. {/* 判断是否是免费的,或者已经购买过 */}
  832. {buyState.value.play ? (
  833. <Button
  834. round
  835. block
  836. type="primary"
  837. color="linear-gradient(180deg, #59E5D5 0%, #2DC7AA 100%)"
  838. onClick={() => {
  839. player.value && player.value.stop()
  840. musicBuy(musicDetail.value, () => {}, {
  841. 'part-index': staffData.partIndex || 0
  842. })
  843. }}
  844. >
  845. 立即练习
  846. </Button>
  847. ) : (
  848. <div class={styles.colSticky}>
  849. {/* 只有,有点播类型的才显示价格 */}
  850. {buyState.value.charge && (
  851. <div class={styles.priceSection}>
  852. <span>点播价:</span>
  853. <span class={styles.price}>
  854. <i>¥</i>
  855. {moneyFormat(musicDetail.value?.musicPrice)}
  856. </span>
  857. </div>
  858. )}
  859. <div class={[styles.buyBtn]}>
  860. {/* 判断是否是需要收费的 */}
  861. {buyState.value.charge && (
  862. <Button
  863. round
  864. type="primary"
  865. color="linear-gradient(180deg, #59E5D5 0%, #2DC7AA 100%)"
  866. class={styles.primary}
  867. onClick={onBuy}
  868. >
  869. 立即点播
  870. </Button>
  871. )}
  872. {/* 判断是否有会员的 */}
  873. {buyState.value.vip && (
  874. <Button
  875. round
  876. block={!buyState.value.charge ? true : false}
  877. type="primary"
  878. color="linear-gradient(180deg, #F7BD8D 0%, #CD8806 100%)"
  879. class={styles.memeber}
  880. onClick={() => {
  881. router.push({
  882. path: '/memberCenter',
  883. query: {
  884. ...route.query
  885. }
  886. })
  887. }}
  888. >
  889. {studentActivityId.value > 0 && (
  890. <div class={[styles.buttonDiscount]}>专属优惠</div>
  891. )}
  892. 开通会员
  893. </Button>
  894. )}
  895. </div>
  896. </div>
  897. )}
  898. </div>
  899. </ColSticky>
  900. )}
  901. <Popup
  902. v-model:show={shareStatus.value}
  903. style={{ background: 'transparent' }}
  904. teleport="body"
  905. >
  906. <ColShare
  907. teacherId={state.user.data?.userId}
  908. shareUrl={shareUrl.value}
  909. shareType="music"
  910. >
  911. <div class={styles.shareMate}>
  912. {shareDiscount.value === 1 && (
  913. <div class={styles.tagDiscount}>专属优惠</div>
  914. )}
  915. <img
  916. class={styles.icon}
  917. crossorigin="anonymous"
  918. src={
  919. musicDetail.value?.titleImg +
  920. `@base@tag=imgScale&h=80&w=80&m=1?t=${+new Date()}`
  921. }
  922. />
  923. <div class={styles.info}>
  924. <h4 class="van-multi-ellipsis--l2">
  925. {musicDetail.value?.musicSheetName}
  926. </h4>
  927. <p>作曲人:{musicDetail.value?.composer}</p>
  928. </div>
  929. </div>
  930. </ColShare>
  931. </Popup>
  932. <Popup v-model:show={downloadStatus.value} position="bottom" round>
  933. <Download
  934. imgList={JSON.parse(JSON.stringify(showImg))}
  935. musicSheetName={musicDetail.value.musicSheetName}
  936. />
  937. </Popup>
  938. <Popup
  939. v-model:show={staff.status}
  940. teleport="body"
  941. closeable
  942. style={{ width: '80%' }}
  943. round
  944. >
  945. <div class={styles.staffContainer}>
  946. <div class={styles.staffTitle}>选择转换曲谱</div>
  947. <RadioGroup v-model={staff.radio}>
  948. <CellGroup border={false}>
  949. <Cell
  950. center
  951. border={false}
  952. class={staff.radio === 'staff' ? styles.active : ''}
  953. onClick={() => onChangeStaff('staff')}
  954. >
  955. {{
  956. icon: () => (
  957. <Image src={staffDetafult} class={styles.staffImg} />
  958. ),
  959. title: () => <span class={styles.name}>五线谱</span>,
  960. value: () => (
  961. <Radio name="staff">
  962. {{
  963. icon: (props: any) => (
  964. <Icon
  965. class={styles.boxStyle}
  966. size={16}
  967. name={
  968. props.checked
  969. ? activeButtonIcon
  970. : inactiveButtonIcon
  971. }
  972. />
  973. )
  974. }}
  975. </Radio>
  976. )
  977. }}
  978. </Cell>
  979. <Cell
  980. center
  981. border={false}
  982. class={staff.radio === 'first' ? styles.active : ''}
  983. onClick={() => onChangeStaff('first')}
  984. >
  985. {{
  986. icon: () => (
  987. <Image src={firstDefault} class={styles.staffImg} />
  988. ),
  989. title: () => <span class={styles.name}>简谱-首调</span>,
  990. value: () => (
  991. <Radio name="first">
  992. {{
  993. icon: (props: any) => (
  994. <Icon
  995. class={styles.boxStyle}
  996. size={16}
  997. name={
  998. props.checked
  999. ? activeButtonIcon
  1000. : inactiveButtonIcon
  1001. }
  1002. />
  1003. )
  1004. }}
  1005. </Radio>
  1006. )
  1007. }}
  1008. </Cell>
  1009. <Cell
  1010. center
  1011. border={false}
  1012. class={staff.radio === 'fixed' ? styles.active : ''}
  1013. onClick={() => onChangeStaff('fixed')}
  1014. >
  1015. {{
  1016. icon: () => (
  1017. <Image src={fixedDefault} class={styles.staffImg} />
  1018. ),
  1019. title: () => <span class={styles.name}>简谱-固定调</span>,
  1020. value: () => (
  1021. <Radio name="fixed">
  1022. {{
  1023. icon: (props: any) => (
  1024. <Icon
  1025. class={styles.boxStyle}
  1026. size={16}
  1027. name={
  1028. props.checked
  1029. ? activeButtonIcon
  1030. : inactiveButtonIcon
  1031. }
  1032. />
  1033. )
  1034. }}
  1035. </Radio>
  1036. )
  1037. }}
  1038. </Cell>
  1039. </CellGroup>
  1040. </RadioGroup>
  1041. </div>
  1042. </Popup>
  1043. <Popup
  1044. teleport="body"
  1045. position="bottom"
  1046. round
  1047. v-model:show={staffData.open}
  1048. >
  1049. <Picker
  1050. columns={partColumns.value}
  1051. onConfirm={value => {
  1052. staffData.open = false
  1053. staffData.partIndex = value.value
  1054. nextTick(() => {
  1055. resetRender()
  1056. })
  1057. }}
  1058. onCancel={() => (staffData.open = false)}
  1059. />
  1060. </Popup>
  1061. </div>
  1062. )
  1063. }
  1064. }
  1065. })