index.tsx 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109
  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. orderStatus.orderObject.paymentConfig = {
  411. ...result.paymentConfig,
  412. paymentVendor: result.paymentVendor,
  413. paymentVersion: result.paymentVersion
  414. }
  415. routerTo()
  416. })
  417. .catch(() => {
  418. Dialog.close()
  419. // 只用取消订单,不用做其它处理
  420. cancelPayment(result.orderNo)
  421. })
  422. } else {
  423. routerTo()
  424. }
  425. }
  426. const routerTo = () => {
  427. const music = musicDetail.value
  428. router.push({
  429. path: '/orderDetail',
  430. query: {
  431. orderType: 'MUSIC',
  432. musicId: music.id
  433. }
  434. })
  435. }
  436. const cancelPayment = async (orderNo: string) => {
  437. try {
  438. await request.post('/api-student/userOrder/orderCancel', {
  439. data: {
  440. orderNo
  441. }
  442. })
  443. } catch {}
  444. }
  445. const paymentType = computed(() => {
  446. let paymentType = musicDetail.value?.paymentType
  447. if (typeof paymentType === 'string') {
  448. paymentType = paymentType.split(',')
  449. return paymentType
  450. }
  451. return []
  452. })
  453. const buyState = computed(() => {
  454. const music = musicDetail.value
  455. return {
  456. play: music.play ? true : false, // 是否可以播放
  457. free: music?.paymentType.includes('FREE'),
  458. charge: music?.paymentType.includes('CHARGE'),
  459. vip: music?.paymentType.includes('VIP'),
  460. buy: music?.orderStatus === 'PAID' // 是否已买
  461. }
  462. })
  463. const shareStatus = ref(false)
  464. const shareUrl = ref('')
  465. const shareDiscount = ref(0)
  466. // console.log(data)
  467. const onShare = async () => {
  468. try {
  469. const res = await request.post('/api-teacher/open/musicShareProfit', {
  470. data: {
  471. bizId: musicDetail.value?.id,
  472. userId: state.user.data?.userId
  473. }
  474. })
  475. let url =
  476. location.origin +
  477. `/teacher/#/shareMusic?id=${musicDetail.value?.id}&recomUserId=${state.user.data?.userId}&userType=${state.platformType}`
  478. // 判断是否有活动
  479. if (res.data.discount === 1) {
  480. url += `&activityId=${res.data.activityId}`
  481. }
  482. shareDiscount.value = res.data.discount || 0
  483. console.log(url)
  484. shareUrl.value = url
  485. shareStatus.value = true
  486. return
  487. } catch {}
  488. }
  489. const staffData = reactive({
  490. open: false,
  491. iframeSrc: '',
  492. musicXml: '',
  493. instrumentName: '',
  494. iframeRef: null as any,
  495. partIndex: 0,
  496. partList: [] as any[]
  497. })
  498. /** 渲染五线谱 */
  499. const renderStaff = () => {
  500. staffData.iframeSrc = `${location.origin}${location.pathname}osmd/index.html`
  501. staffData.musicXml = musicDetail.value?.xmlFileUrl || ''
  502. staffData.partList = musicDetail.value?.background || []
  503. staffData.instrumentName = getInstrumentName(
  504. staffData.partList[staffData.partIndex]?.track
  505. )
  506. }
  507. const musicIframeLoad = () => {
  508. const iframeRef: any = document.getElementById('staffIframeRef')
  509. if (iframeRef && iframeRef.contentWindow.renderXml) {
  510. iframeRef.contentWindow.renderXml(
  511. staffData.musicXml,
  512. staffData.partIndex
  513. )
  514. }
  515. }
  516. const resetRender = () => {
  517. const iframeRef: any = document.getElementById('staffIframeRef')
  518. if (iframeRef && iframeRef.contentWindow.renderXml) {
  519. iframeRef.contentWindow.resetRender(staffData.partIndex)
  520. staffData.instrumentName = getInstrumentName(
  521. staffData.partList[staffData.partIndex]?.track
  522. )
  523. }
  524. }
  525. const partColumns = computed(() => {
  526. return staffData.partList.map((item: any, index: number) => {
  527. const instrumentName = getInstrumentName(item.track)
  528. return {
  529. text: item.track + (instrumentName ? `(${instrumentName})` : ''),
  530. value: index
  531. }
  532. })
  533. })
  534. return () => {
  535. return (
  536. <div class={styles.detail}>
  537. <Sticky position="top">
  538. <div ref={headers}>
  539. <ColHeader
  540. background="transparent"
  541. border={false}
  542. isFixed={false}
  543. color="#fff"
  544. title={musicDetail.value?.musicSheetName}
  545. backIconColor="white"
  546. v-slots={{
  547. right: () => (
  548. <div
  549. class={styles.shareBtn}
  550. style={{
  551. color: '#fff'
  552. }}
  553. onClick={onShare}
  554. >
  555. <Image src={iconShare} />
  556. 分享
  557. </div>
  558. )
  559. }}
  560. />
  561. </div>
  562. </Sticky>
  563. <img class={styles.bgImg} src={musicDetail.value?.titleImg} />
  564. <div class={styles.bgContent}></div>
  565. <div
  566. class={styles.musicContainer}
  567. style={{
  568. marginTop: '16px',
  569. height: `calc(100vh - ${heightInfo.value + 16 + 'px'})`
  570. }}
  571. >
  572. <Cell
  573. border={false}
  574. center
  575. class={styles.musicInfo}
  576. v-slots={{
  577. icon: () => (
  578. <Image
  579. class={styles.pImg}
  580. src={musicDetail.value?.titleImg}
  581. />
  582. ),
  583. title: () => (
  584. <div class={styles.info}>
  585. <h4
  586. class="van-ellipsis"
  587. // onClick={() => handleGotoMusicScore(musicDetail.value)}
  588. >
  589. {musicDetail.value?.musicSheetName}
  590. </h4>
  591. <p
  592. style={{
  593. display: 'flex'
  594. }}
  595. >
  596. {paymentType.value.map(tag => (
  597. <Tag
  598. style={{ color: colors[tag].color }}
  599. class={styles.tag}
  600. type="success"
  601. plain
  602. >
  603. {colors[tag].text}
  604. </Tag>
  605. ))}
  606. {musicDetail.value?.exquisiteFlag === 1 && (
  607. <Image
  608. class={styles.exquisiteFlag}
  609. src={getAssetsHomeFile('icon_exquisite.png')}
  610. />
  611. )}
  612. {musicDetail.value?.albumNums > 0 && (
  613. <Image
  614. class={styles.songAlbum}
  615. src={getAssetsHomeFile('icon_album_active.png')}
  616. />
  617. )}
  618. <span class={styles.coomposer}>
  619. {musicDetail.value?.composer}
  620. </span>
  621. </p>
  622. </div>
  623. ),
  624. value: () => (
  625. <>
  626. <div
  627. class="van-cell__value"
  628. style={{
  629. display:
  630. musicDetail.value?.musicSheetType === 'SINGLE'
  631. ? ''
  632. : 'none'
  633. }}
  634. >
  635. {musicDetail.value?.notation ? (
  636. <span
  637. class={styles.download}
  638. onClick={() => {
  639. staff.status = true
  640. }}
  641. >
  642. <img src={iconChangeStaff} />
  643. <span>转谱</span>
  644. </span>
  645. ) : null}
  646. <span
  647. class={styles.download}
  648. onClick={() => {
  649. if (showImg.length > 0) {
  650. downloadStatus.value = true
  651. } else {
  652. Toast('暂无图片')
  653. }
  654. }}
  655. >
  656. <img src={iconDownload} />
  657. <span>下载曲谱</span>
  658. </span>
  659. </div>
  660. <span
  661. style={{
  662. display:
  663. musicDetail.value?.musicSheetType === 'CONCERT'
  664. ? ''
  665. : 'none'
  666. }}
  667. class={styles.download}
  668. onClick={() => {
  669. staffData.open = true
  670. }}
  671. >
  672. <Icon
  673. style={{
  674. background: 'rgba(246,246,246,1)',
  675. borderRadius: '50%',
  676. padding: '4px'
  677. }}
  678. size="20px"
  679. name="exchange"
  680. />
  681. <span>切换乐器</span>
  682. </span>
  683. </>
  684. )
  685. }}
  686. />
  687. <div class={styles.musicContent}>
  688. <p class={styles.musicTitle}>
  689. {(musicDetail.value?.musicSheetName
  690. ? musicDetail.value?.musicSheetName
  691. : '') +
  692. (staffData.instrumentName
  693. ? `(${staffData.instrumentName})`
  694. : '')}
  695. </p>
  696. {musicDetail.value?.musicSheetType === 'CONCERT' ? (
  697. <>
  698. {loading.value && (
  699. <>
  700. <Vue3Lottie
  701. animationData={AstronautJSON}
  702. class={styles.finch}
  703. ></Vue3Lottie>
  704. <p class={styles.finchLoad}>加载中...</p>
  705. </>
  706. )}
  707. <iframe
  708. id="staffIframeRef"
  709. src={staffData.iframeSrc}
  710. onLoad={musicIframeLoad}
  711. ></iframe>
  712. </>
  713. ) : (
  714. <>
  715. {showImg.length > 0 ? (
  716. <img src={showImg[0]} alt="" class={styles.musicImg} />
  717. ) : loading.value ? (
  718. <>
  719. <Vue3Lottie
  720. animationData={AstronautJSON}
  721. class={styles.finch}
  722. ></Vue3Lottie>
  723. <p class={styles.finchLoad}>加载中...</p>
  724. </>
  725. ) : (
  726. <div class={styles.empty}>
  727. <Image src={emtpy} class={styles.emptyImg} />
  728. <p class={styles.emptyTip}>暂无乐谱预览图</p>
  729. </div>
  730. )}
  731. </>
  732. )}
  733. <div class={styles.videoOperation}>
  734. {audioFileUrl.value && (
  735. <>
  736. {!buyState.value.play &&
  737. freeRate.value != 100 &&
  738. freeRate.value != 0 && (
  739. <div class={[styles.audition]}>
  740. <img src={iconListen} />
  741. <span>每首曲目可试听{freeRate.value}%</span>
  742. </div>
  743. )}
  744. <div class={[styles.audio, styles.collectCell]}>
  745. <audio id="player" controls ref={audio}>
  746. <source src={audioFileUrl.value} type="audio/mp3" />
  747. </audio>
  748. </div>
  749. </>
  750. )}
  751. <div class={[styles.collect, styles.collectCell]}>
  752. <div
  753. class={[styles.userInfo]}
  754. onClick={() => {
  755. if (
  756. browser().isApp &&
  757. musicDetail.value?.sourceType === 'TEACHER' &&
  758. state.platformType === 'STUDENT'
  759. ) {
  760. router.push({
  761. path: '/teacherHome',
  762. query: {
  763. teacherId: musicDetail.value?.userId,
  764. tabs: 'music'
  765. }
  766. })
  767. }
  768. }}
  769. >
  770. <img src={musicDetail.value?.userAvatar || iconTeacher} />
  771. <span>{musicDetail.value?.userName}</span>
  772. </div>
  773. <div class={styles.functionSection}>
  774. <div
  775. class={[styles.collectSection]}
  776. onClick={() => toggleFavorite()}
  777. >
  778. <span>{musicDetail.value?.favoriteCount}人收藏</span>
  779. <img
  780. src={
  781. musicDetail.value?.favorite
  782. ? iconCollectActive
  783. : iconCollect
  784. }
  785. />
  786. </div>
  787. {state.platformType === 'TEACHER' && (
  788. <div
  789. class={[styles.collectSection]}
  790. onClick={() => {
  791. if (musicDetail.value?.coursewareStatus) {
  792. removeCourse()
  793. } else {
  794. onAddCourse()
  795. }
  796. }}
  797. >
  798. <span>
  799. {musicDetail.value?.coursewareStatus
  800. ? '移出课件'
  801. : '添加到课件'}
  802. </span>
  803. {musicDetail.value?.coursewareStatus ? (
  804. <Icon name="clear" />
  805. ) : (
  806. <Icon name="add" size={18} />
  807. )}
  808. </div>
  809. )}
  810. </div>
  811. </div>
  812. </div>
  813. </div>
  814. <div
  815. class={[styles.lookAlbum, styles.collectCell]}
  816. onClick={() => {
  817. router.push({
  818. path: '/look-album-list',
  819. query: {
  820. id: musicDetail.value?.id,
  821. musicSubject: musicDetail.value?.musicSubject
  822. }
  823. })
  824. }}
  825. >
  826. <div>
  827. <img src={iconAlbum} />
  828. <span>进入曲目所在平台专辑列表</span>
  829. </div>
  830. <Icon name="arrow" size={16} color="#666" />
  831. </div>
  832. </div>
  833. {musicDetail.value?.id && (
  834. <ColSticky position="bottom" background="white">
  835. <div ref={footers}>
  836. {/* 判断是否是免费的,或者已经购买过 */}
  837. {buyState.value.play ? (
  838. <Button
  839. round
  840. block
  841. type="primary"
  842. color="linear-gradient(180deg, #59E5D5 0%, #2DC7AA 100%)"
  843. onClick={() => {
  844. player.value && player.value.stop()
  845. musicBuy(musicDetail.value, () => {}, {
  846. 'part-index': staffData.partIndex || 0
  847. })
  848. }}
  849. >
  850. 立即练习
  851. </Button>
  852. ) : (
  853. <div class={styles.colSticky}>
  854. {/* 只有,有点播类型的才显示价格 */}
  855. {buyState.value.charge && (
  856. <div class={styles.priceSection}>
  857. <span>点播价:</span>
  858. <span class={styles.price}>
  859. <i>¥</i>
  860. {moneyFormat(musicDetail.value?.musicPrice)}
  861. </span>
  862. </div>
  863. )}
  864. <div class={[styles.buyBtn]}>
  865. {/* 判断是否是需要收费的 */}
  866. {buyState.value.charge && (
  867. <Button
  868. round
  869. type="primary"
  870. color="linear-gradient(180deg, #59E5D5 0%, #2DC7AA 100%)"
  871. class={styles.primary}
  872. onClick={onBuy}
  873. >
  874. 立即点播
  875. </Button>
  876. )}
  877. {/* 判断是否有会员的 */}
  878. {buyState.value.vip && (
  879. <Button
  880. round
  881. block={!buyState.value.charge ? true : false}
  882. type="primary"
  883. color="linear-gradient(180deg, #F7BD8D 0%, #CD8806 100%)"
  884. class={styles.memeber}
  885. onClick={() => {
  886. router.push({
  887. path: '/memberCenter',
  888. query: {
  889. ...route.query
  890. }
  891. })
  892. }}
  893. >
  894. {studentActivityId.value > 0 && (
  895. <div class={[styles.buttonDiscount]}>专属优惠</div>
  896. )}
  897. 开通会员
  898. </Button>
  899. )}
  900. </div>
  901. </div>
  902. )}
  903. </div>
  904. </ColSticky>
  905. )}
  906. <Popup
  907. v-model:show={shareStatus.value}
  908. style={{ background: 'transparent' }}
  909. teleport="body"
  910. >
  911. <ColShare
  912. teacherId={state.user.data?.userId}
  913. shareUrl={shareUrl.value}
  914. shareType="music"
  915. >
  916. <div class={styles.shareMate}>
  917. {shareDiscount.value === 1 && (
  918. <div class={styles.tagDiscount}>专属优惠</div>
  919. )}
  920. <img
  921. class={styles.icon}
  922. crossorigin="anonymous"
  923. src={musicDetail.value?.titleImg + `?t=${+new Date()}`}
  924. />
  925. <div class={styles.info}>
  926. <h4 class="van-multi-ellipsis--l2">
  927. {musicDetail.value?.musicSheetName}
  928. </h4>
  929. <p>作曲人:{musicDetail.value?.composer}</p>
  930. </div>
  931. </div>
  932. </ColShare>
  933. </Popup>
  934. <Popup v-model:show={downloadStatus.value} position="bottom" round>
  935. <Download
  936. imgList={JSON.parse(JSON.stringify(showImg))}
  937. musicSheetName={musicDetail.value.musicSheetName}
  938. />
  939. </Popup>
  940. <Popup
  941. v-model:show={staff.status}
  942. teleport="body"
  943. closeable
  944. style={{ width: '80%' }}
  945. round
  946. >
  947. <div class={styles.staffContainer}>
  948. <div class={styles.staffTitle}>选择转换曲谱</div>
  949. <RadioGroup v-model={staff.radio}>
  950. <CellGroup border={false}>
  951. <Cell
  952. center
  953. border={false}
  954. class={staff.radio === 'staff' ? styles.active : ''}
  955. onClick={() => onChangeStaff('staff')}
  956. >
  957. {{
  958. icon: () => (
  959. <Image src={staffDetafult} class={styles.staffImg} />
  960. ),
  961. title: () => <span class={styles.name}>五线谱</span>,
  962. value: () => (
  963. <Radio name="staff">
  964. {{
  965. icon: (props: any) => (
  966. <Icon
  967. class={styles.boxStyle}
  968. size={16}
  969. name={
  970. props.checked
  971. ? activeButtonIcon
  972. : inactiveButtonIcon
  973. }
  974. />
  975. )
  976. }}
  977. </Radio>
  978. )
  979. }}
  980. </Cell>
  981. <Cell
  982. center
  983. border={false}
  984. class={staff.radio === 'first' ? styles.active : ''}
  985. onClick={() => onChangeStaff('first')}
  986. >
  987. {{
  988. icon: () => (
  989. <Image src={firstDefault} class={styles.staffImg} />
  990. ),
  991. title: () => <span class={styles.name}>简谱-首调</span>,
  992. value: () => (
  993. <Radio name="first">
  994. {{
  995. icon: (props: any) => (
  996. <Icon
  997. class={styles.boxStyle}
  998. size={16}
  999. name={
  1000. props.checked
  1001. ? activeButtonIcon
  1002. : inactiveButtonIcon
  1003. }
  1004. />
  1005. )
  1006. }}
  1007. </Radio>
  1008. )
  1009. }}
  1010. </Cell>
  1011. <Cell
  1012. center
  1013. border={false}
  1014. class={staff.radio === 'fixed' ? styles.active : ''}
  1015. onClick={() => onChangeStaff('fixed')}
  1016. >
  1017. {{
  1018. icon: () => (
  1019. <Image src={fixedDefault} class={styles.staffImg} />
  1020. ),
  1021. title: () => <span class={styles.name}>简谱-固定调</span>,
  1022. value: () => (
  1023. <Radio name="fixed">
  1024. {{
  1025. icon: (props: any) => (
  1026. <Icon
  1027. class={styles.boxStyle}
  1028. size={16}
  1029. name={
  1030. props.checked
  1031. ? activeButtonIcon
  1032. : inactiveButtonIcon
  1033. }
  1034. />
  1035. )
  1036. }}
  1037. </Radio>
  1038. )
  1039. }}
  1040. </Cell>
  1041. </CellGroup>
  1042. </RadioGroup>
  1043. </div>
  1044. </Popup>
  1045. <Popup
  1046. teleport="body"
  1047. position="bottom"
  1048. round
  1049. v-model:show={staffData.open}
  1050. >
  1051. <Picker
  1052. columns={partColumns.value}
  1053. onConfirm={value => {
  1054. staffData.open = false
  1055. staffData.partIndex = value.value
  1056. nextTick(() => {
  1057. resetRender()
  1058. })
  1059. }}
  1060. onCancel={() => (staffData.open = false)}
  1061. />
  1062. </Popup>
  1063. </div>
  1064. )
  1065. }
  1066. }
  1067. })