index.tsx 48 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414
  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 { postMessage, promisefiyPostMessage } from '@/helpers/native-message'
  16. import {
  17. Button,
  18. Cell,
  19. CellGroup,
  20. Checkbox,
  21. Dialog,
  22. Icon,
  23. Image,
  24. Popup,
  25. RadioGroup,
  26. Sticky,
  27. Tag,
  28. Radio,
  29. Toast,
  30. Picker
  31. } from 'vant'
  32. import styles from './index.module.less'
  33. // import Item from '../list/item'
  34. import { useRect } from '@vant/use'
  35. import { Vue3Lottie } from 'vue3-lottie'
  36. import { getRandomKey, handleGotoMusicScore, musicBuy } from '../music'
  37. import { getOssUploadUrl, state } from '@/state'
  38. // import { useEventTracking } from '@/helpers/hooks'
  39. import ColSticky from '@/components/col-sticky'
  40. import { browser, moneyFormat } from '@/helpers/utils'
  41. import { orderStatus } from '@/views/order-detail/orderStatus'
  42. import iconShare from '@/views/music/album/icon_share.svg'
  43. import iconAlbum from './images/icon_album.png'
  44. import iconAlbum2 from './images/icon_album2.png'
  45. import iconDownload from './images/icon_download.png'
  46. import iconChange from './images/icon-change.png'
  47. import iconAddCourse from './images/icon-add-course.png'
  48. import iconRemoveCourse from './images/icon-remove-course.png'
  49. import AstronautJSON from './animate/bigLoad.json'
  50. import ColShare from '@/components/col-share'
  51. import iconCollect from './images/icon_collect.png'
  52. import iconCollectActive from './images/icon_collect_active.png'
  53. import iconListen from './images/icon_listen.png'
  54. import iconTeacher from '@common/images/icon_teacher.png'
  55. import emtpy from './images/emtpy.png'
  56. import activeButtonIcon from '@common/images/icon_checkbox.png'
  57. import inactiveButtonIcon from '@common/images/icon_checkbox_default.png'
  58. import staffDetafult from './images/staff-default.png'
  59. // import staffActive from './images/staff-active.png'
  60. import firstDefault from './images/first-default.png'
  61. // import firstActive from './images/first-active.png'
  62. import fixedDefault from './images/fixed-default.png'
  63. import fixedActive from './images/fixed-active.png'
  64. import qs from 'query-string'
  65. import Plyr from 'plyr'
  66. import 'plyr/dist/plyr.css'
  67. import Download from './download'
  68. import { getInstrumentName } from '@/constant/instruments'
  69. import { getUploadSign, onOnlyFileUpload } from '@/helpers/oss-file-upload'
  70. import { svgtopng } from '@/tenant/music/music-detail/formatSvgToImg'
  71. import { useThrottleFn } from '@vueuse/core'
  72. import {
  73. formatXML,
  74. getCustomInfo,
  75. onlyVisible
  76. } from '@/tenant/music/music-detail/instrument'
  77. export const getAssetsHomeFile = (fileName: string) => {
  78. const path = `../component/images/${fileName}`
  79. const modules = import.meta.globEager('../component/images/*')
  80. return modules[path].default
  81. }
  82. export default defineComponent({
  83. name: 'MusicDetail',
  84. setup() {
  85. localStorage.setItem('behaviorId', getRandomKey())
  86. const router = useRouter()
  87. const route = useRoute()
  88. const loading = ref(false)
  89. const aId = Number(route.query.activityId) || 0
  90. const studentActivityId = ref(aId)
  91. const isError = ref(false)
  92. const headers = ref(null)
  93. const footers = ref(null)
  94. const heightInfo = ref<any>('0')
  95. const musicDetail = ref<any>(null)
  96. const audioFileUrl = ref('')
  97. const showImg = ref([] as any)
  98. const firstList = ref<Array<any>>([])
  99. const fixedList = ref<Array<any>>([])
  100. const staffList = ref<Array<any>>([])
  101. const musicPdfUrl = ref('')
  102. const defaultImgs = ref({
  103. first: false,
  104. fixed: false,
  105. staff: false
  106. })
  107. const accompanyUrl = ref<string>('')
  108. const downloadStatus = ref<boolean>(false)
  109. const staff = reactive({
  110. status: false,
  111. radio: 'staff' // staff first fixed
  112. })
  113. const colors: any = {
  114. FREE: {
  115. color: '#01B84F',
  116. text: '免费'
  117. },
  118. VIP: {
  119. color: '#CD863E',
  120. text: '会员'
  121. },
  122. CHARGE: {
  123. color: '#3591CE',
  124. text: '点播'
  125. }
  126. }
  127. // 更改预览状态
  128. const onChangeStaff = (type: string) => {
  129. staff.radio = type
  130. staff.status = false
  131. if (type == 'first') {
  132. loading.value = false
  133. const tempPdf = musicDetail.value?.firstPdfUrl
  134. initIframe(tempPdf, 'first', staffData.musicXml)
  135. } else if (type == 'fixed') {
  136. loading.value = false
  137. const tempPdf = musicDetail.value?.jianPdfUrl
  138. console.log(tempPdf, 'tempPdf')
  139. initIframe(tempPdf, 'fixed', staffData.musicXml)
  140. } else {
  141. loading.value = false
  142. const tempPdf = musicDetail.value?.musicPdfUrl
  143. initIframe(tempPdf, 'staff', staffData.musicXml)
  144. }
  145. }
  146. const initIframe = (tempPdf: string, staff: string, xml: string) => {
  147. if (tempPdf) {
  148. musicPdfUrl.value = tempPdf
  149. renderStaff()
  150. } else {
  151. musicPdfUrl.value = ''
  152. // 为了处理,之前是使用pdf渲染,现在又用osmd,iframe没有重新加载
  153. if (
  154. !staffData.iframeSrc ||
  155. staffData.iframeSrc.indexOf('pdf/web') !== -1
  156. ) {
  157. renderStaff()
  158. } else {
  159. resetRenderPage(staff, xml)
  160. }
  161. }
  162. }
  163. watch(
  164. () => staff.radio,
  165. (val: string) => {
  166. if (val == 'first') {
  167. showImg.value = firstList.value
  168. } else if (val == 'fixed') {
  169. showImg.value = fixedList.value
  170. } else {
  171. showImg.value = staffList.value
  172. }
  173. }
  174. )
  175. const FetchList = async (id?: any) => {
  176. if (loading.value) {
  177. return
  178. }
  179. loading.value = true
  180. isError.value = false
  181. try {
  182. const res = await request.get(`/music/sheet/detail/${route.query.id}`, {
  183. prefix:
  184. state.platformType === 'TEACHER' ? '/api-teacher' : '/api-student'
  185. })
  186. musicDetail.value = res.data
  187. // console.log(musicDetail.value.notation, 'musicDetail')
  188. // 取原音,如果有多个则默认第一个
  189. const background = res.data.background
  190. audioFileUrl.value =
  191. background && background.length > 0 ? background[0].audioFileUrl : ''
  192. // const arrImgs = res.data.musicImg ? res.data.musicImg.split(',') : []
  193. showImg.value = res.data.musicImg ? res.data.musicImg.split(',') : []
  194. firstList.value = res.data.firstTone
  195. ? res.data.firstTone.split(',')
  196. : []
  197. fixedList.value = res.data.fixedTone
  198. ? res.data.fixedTone.split(',')
  199. : []
  200. staffList.value = res.data.musicImg ? res.data.musicImg.split(',') : []
  201. // 初始化默认数据是否有值
  202. if (firstList.value.length > 0) {
  203. defaultImgs.value.first = true
  204. }
  205. if (fixedList.value.length > 0) {
  206. defaultImgs.value.fixed = true
  207. }
  208. if (staffList.value.length > 0) {
  209. defaultImgs.value.staff = true
  210. }
  211. nextTick(async () => {
  212. await toDetail()
  213. renderStaff()
  214. })
  215. if (res.data.auditStatus === 'DOING') {
  216. Dialog.confirm({
  217. message: '曲目审核中',
  218. showConfirmButton: true,
  219. showCancelButton: false,
  220. confirmButtonColor: 'var(--van-primary)'
  221. }).then(() => {
  222. if (browser().isApp) {
  223. postMessage({ api: 'goBack' })
  224. } else {
  225. router.back()
  226. }
  227. })
  228. }
  229. } catch (error) {
  230. isError.value = true
  231. }
  232. if (musicDetail.value?.musicSheetType !== 'CONCERT') {
  233. loading.value = false
  234. }
  235. }
  236. const player = ref<any>(null)
  237. const audio = ref<any>(null)
  238. const freeRate = ref<any>(0)
  239. const initAudio = async () => {
  240. const controls = [
  241. 'play-large',
  242. 'play',
  243. 'progress',
  244. 'captions',
  245. // 'fullscreen',
  246. 'duration'
  247. ]
  248. player.value = new Plyr(audio.value, {
  249. controls: controls
  250. })
  251. const config = await request.get(
  252. '/sysConfig/queryByParamNameList',
  253. {
  254. prefix: state.platformType === 'TEACHER' ? '/api-teacher' : '/api-student',
  255. params: {
  256. paramNames: 'music_sheet_free_rate'
  257. }
  258. }
  259. )
  260. freeRate.value = config.data[0]?.paramValue || 0
  261. player.value.on('timeupdate', () => {
  262. // 允许播放时间
  263. const players = player.value
  264. const playTime = (players.duration * freeRate.value) / 100 || 0
  265. // 时间,不能播放
  266. if (players.currentTime >= playTime && !buyState.value.play) {
  267. players.stop()
  268. // players.pause()
  269. }
  270. })
  271. }
  272. const showLoading = async (e: any) => {
  273. if (e.data?.api === 'musicStaffRender') {
  274. const osmdImg = e.data.osmdImg
  275. showImg.value = []
  276. const imgs: any = []
  277. for (let i = 0; i < osmdImg.length; i++) {
  278. const img = await svgtopng(
  279. osmdImg[i].img,
  280. osmdImg[i].width,
  281. osmdImg[i].height
  282. )
  283. imgs.push(img)
  284. }
  285. showImg.value = imgs
  286. loading.value = e.data.loading
  287. }
  288. }
  289. onMounted(async () => {
  290. postMessage({
  291. api: 'setStatusBarTextColor',
  292. content: { statusBarTextColor: true }
  293. })
  294. await FetchList()
  295. const { height } = useRect(headers as any)
  296. const footer = useRect(footers as any)
  297. heightInfo.value = height + footer.height
  298. // 初始化音频
  299. if (audioFileUrl.value) {
  300. initAudio()
  301. }
  302. window.addEventListener('message', showLoading)
  303. })
  304. onUnmounted(() => {
  305. postMessage({
  306. api: 'setStatusBarTextColor',
  307. content: { statusBarTextColor: false }
  308. })
  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('/userOrder/getPendingOrder', {
  392. prefix: state.platformType === 'TEACHER' ? '/api-teacher' : '/api-student',
  393. data: {
  394. goodType: 'MUSIC',
  395. bizId: music.id
  396. }
  397. })
  398. const result = res.data
  399. if (result) {
  400. Dialog.confirm({
  401. title: '提示',
  402. message: '您有一个未支付的订单,是否继续支付?',
  403. confirmButtonColor: '#269a93',
  404. cancelButtonText: '取消订单',
  405. confirmButtonText: '继续支付'
  406. })
  407. .then(async () => {
  408. orderStatus.orderObject.orderNo = result.orderNo
  409. orderStatus.orderObject.actualPrice = result.actualPrice
  410. orderStatus.orderObject.discountPrice = result.discountPrice
  411. orderStatus.orderObject.paymentConfig = {
  412. ...result.paymentConfig,
  413. paymentVendor: result.paymentVendor,
  414. paymentVersion: result.paymentVersion
  415. }
  416. routerTo()
  417. })
  418. .catch(() => {
  419. Dialog.close()
  420. // 只用取消订单,不用做其它处理
  421. cancelPayment(result.orderNo)
  422. })
  423. } else {
  424. routerTo()
  425. }
  426. }
  427. const routerTo = () => {
  428. const music = musicDetail.value
  429. router.push({
  430. path: '/orderDetail',
  431. query: {
  432. orderType: 'MUSIC',
  433. musicId: music.id
  434. }
  435. })
  436. }
  437. const cancelPayment = async (orderNo: string) => {
  438. try {
  439. await request.post('/userOrder/orderCancel', {
  440. prefix: state.platformType === 'TEACHER' ? '/api-teacher' : '/api-student',
  441. data: {
  442. orderNo
  443. }
  444. })
  445. } catch {}
  446. }
  447. const paymentType = computed(() => {
  448. let paymentType = musicDetail.value?.paymentType
  449. if (typeof paymentType === 'string') {
  450. paymentType = paymentType.split(',')
  451. return paymentType
  452. }
  453. return []
  454. })
  455. const buyState = computed(() => {
  456. const music = musicDetail.value
  457. return {
  458. hasTenantAlbum: route.query?.tenantAlbumId ? true : false, // 是否从专辑来的
  459. play: music.play ? true : false, // 是否可以播放
  460. free: music?.paymentType.includes('FREE'),
  461. charge: music?.paymentType.includes('CHARGE'),
  462. vip: music?.paymentType.includes('VIP'),
  463. buy: music?.orderStatus === 'PAID' // 是否已买
  464. }
  465. })
  466. const shareStatus = ref(false)
  467. const shareUrl = ref('')
  468. const shareDiscount = ref(0)
  469. // console.log(data)
  470. const onShare = async () => {
  471. try {
  472. const res = await request.post('/api-teacher/open/musicShareProfit', {
  473. data: {
  474. bizId: musicDetail.value?.id,
  475. userId: state.user.data?.userId
  476. }
  477. })
  478. let url =
  479. location.origin +
  480. `/teacher/#/shareMusic?id=${musicDetail.value?.id}&recomUserId=${state.user.data?.userId}&userType=${state.platformType}`
  481. // 判断是否有活动
  482. if (res.data.discount === 1) {
  483. url += `&activityId=${res.data.activityId}`
  484. }
  485. shareDiscount.value = res.data.discount || 0
  486. console.log(url)
  487. shareUrl.value = url
  488. shareStatus.value = true
  489. return
  490. } catch {}
  491. }
  492. const staffData = reactive({
  493. open: false,
  494. iframeSrc: '',
  495. musicXml: '',
  496. instrumentName: '',
  497. iframeRef: null as any,
  498. partIndex: 0,
  499. partXmlIndex: 0,
  500. tempPartList: [] as any[],
  501. partList: [] as any[],
  502. xmlPartList: [] as any[]
  503. })
  504. /** 渲染五线谱 */
  505. const sortList = {
  506. 长笛: 1,
  507. 单簧管: 2,
  508. 中音单簧管: 3,
  509. 低音单簧管: 4,
  510. 高音萨克斯风: 5,
  511. 中音萨克斯风: 6,
  512. 次中音萨克斯风: 7,
  513. 低音萨克斯风: 8,
  514. 小号: 9,
  515. 长号: 10,
  516. 圆号: 11,
  517. 大号: 12,
  518. 上低音号: 13
  519. }
  520. const instrumentSort = (list: Array<any>) => {
  521. list.sort((a, b) => {
  522. return (
  523. (sortList[getInstrumentName(a.track)] || 20) -
  524. (sortList[getInstrumentName(b.track)] || 20)
  525. )
  526. })
  527. return list
  528. }
  529. const toDetail = async () => {
  530. try {
  531. if (musicDetail.value?.xmlFileUrl) {
  532. // 获取文件
  533. const res = await umiRequest.get(musicDetail.value?.xmlFileUrl, {
  534. mode: 'cors'
  535. })
  536. let partNames: string[] = []
  537. const xml: any = new DOMParser().parseFromString(res, 'text/xml')
  538. for (const item of xml.getElementsByTagName('part-name')) {
  539. if (item.textContent) {
  540. partNames.push(item.textContent?.trim())
  541. }
  542. }
  543. partNames = partNames.filter(
  544. (item: any) => !item?.toLocaleUpperCase()?.includes('COMMON')
  545. )
  546. const partList: any = []
  547. for (let j = 0; j < partNames.length; j++) {
  548. partList.push({
  549. name: partNames[j],
  550. value: j
  551. })
  552. }
  553. staffData.xmlPartList = partList
  554. }
  555. // staffData.iframeSrc = `${location.origin}/osmd/index.html`
  556. // staffData.iframeSrc = `${location.origin}${location.pathname}osmd/index.html`
  557. staffData.musicXml = musicDetail.value?.xmlFileUrl || ''
  558. const tempList = musicDetail.value?.background || []
  559. const tempPartList = [] as any
  560. staffData.xmlPartList.forEach((part: any) => {
  561. const item = tempList.find((item: any) => item.track === part.name)
  562. if (item) {
  563. tempPartList.push({
  564. ...item,
  565. index: part.value
  566. })
  567. }
  568. })
  569. staffData.partList = tempPartList
  570. staffData.tempPartList = JSON.parse(JSON.stringify(staffData.partList))
  571. staffData.partList = instrumentSort(staffData.partList)
  572. staffData.partXmlIndex = staffData.partList[0]?.index || 0
  573. staffData.instrumentName =
  574. musicDetail.value?.musicSheetType === 'CONCERT'
  575. ? getInstrumentName(staffData.partList[staffData.partIndex]?.track)
  576. : ''
  577. if (musicDetail.value?.musicSheetType === 'SINGLE') {
  578. musicPdfUrl.value = musicDetail.value?.musicPdfUrl
  579. } else {
  580. musicPdfUrl.value = staffData.partList[0]?.musicPdfUrl
  581. }
  582. } catch (error) {
  583. //
  584. console.log(error, 'error')
  585. }
  586. }
  587. const renderStaff = async () => {
  588. try {
  589. nextTick(() => {
  590. if (musicPdfUrl.value) {
  591. // const url = `${location.origin}/pdf/web/viewer-pdf.html?file=${encodeURIComponent(
  592. // musicPdfUrl.value
  593. // )}&t=${Date.now()}`
  594. const url = `${location.origin}${
  595. location.pathname
  596. }pdf/web/viewer-pdf.html?file=${encodeURIComponent(
  597. musicPdfUrl.value
  598. )}&t=${Date.now()}`
  599. const iframeRef = document.querySelector('#staffIframeRef') as any
  600. iframeRef.contentWindow.location.replace(url)
  601. staffData.iframeSrc = url
  602. } else {
  603. const url = `${location.origin}${
  604. location.pathname
  605. }osmd/index.html?t=${new Date().getTime()}`
  606. // const url = `${location.origin}/osmd/index.html`
  607. const iframeRef = document.querySelector('#staffIframeRef') as any
  608. iframeRef.contentWindow.location.replace(
  609. `${location.origin}${location.pathname}osmd/index.html`
  610. )
  611. staffData.iframeSrc = url
  612. }
  613. })
  614. } catch (error) {
  615. //
  616. }
  617. }
  618. const musicIframeLoad = async () => {
  619. const iframeRef: any = document.getElementById('staffIframeRef')
  620. if (iframeRef && iframeRef.contentWindow.renderXml) {
  621. const res = await umiRequest.get(staffData.musicXml, {
  622. mode: 'cors'
  623. })
  624. const parseXmlInfo = getCustomInfo(res)
  625. const xml = formatXML(parseXmlInfo.parsedXML)
  626. const currentXml = onlyVisible(xml, staffData.partXmlIndex)
  627. iframeRef.contentWindow.renderXml(currentXml, 0, staff.radio)
  628. // iframeRef.contentWindow.renderXml(
  629. // staffData.musicXml,
  630. // staffData.partXmlIndex
  631. // )
  632. }
  633. }
  634. const resetRender = async () => {
  635. const iframeRef: any = document.getElementById('staffIframeRef')
  636. if (iframeRef && iframeRef.contentWindow.renderXml) {
  637. loading.value = true
  638. // iframeRef.contentWindow.resetRender(staffData.partXmlIndex)
  639. const res = await umiRequest.get(staffData.musicXml, {
  640. mode: 'cors'
  641. })
  642. const parseXmlInfo = getCustomInfo(res)
  643. const xml = formatXML(parseXmlInfo.parsedXML)
  644. const currentXml = onlyVisible(xml, staffData.partXmlIndex)
  645. iframeRef.contentWindow.renderXml(currentXml, 0)
  646. staffData.instrumentName = getInstrumentName(
  647. staffData.partList[staffData.partIndex]?.track
  648. )
  649. }
  650. }
  651. const resetRenderPage = async (type: string, xmlUrl: string) => {
  652. const iframeRef: any = document.getElementById('staffIframeRef')
  653. if (iframeRef && iframeRef.contentWindow.renderXml) {
  654. loading.value = true
  655. const res = await umiRequest.get(staffData.musicXml, {
  656. mode: 'cors'
  657. })
  658. const parseXmlInfo = getCustomInfo(res)
  659. const xml = formatXML(parseXmlInfo.parsedXML)
  660. const currentXml = onlyVisible(xml, staffData.partXmlIndex)
  661. iframeRef.contentWindow.resetRenderPage(type, currentXml)
  662. }
  663. }
  664. const partColumns = computed(() => {
  665. return staffData.partList.map((item: any, index: number) => {
  666. const instrumentName =
  667. musicDetail.value?.musicSheetType === 'CONCERT'
  668. ? getInstrumentName(item.track)
  669. : ''
  670. return {
  671. text: item.track + (instrumentName ? `(${instrumentName})` : ''),
  672. value: index,
  673. instrumentName,
  674. musicPdfUrl: item.musicPdfUrl,
  675. firstPdfUrl: item.firstPdfUrl,
  676. jianPdfUrl: item.jianPdfUrl,
  677. xmlValue: item.index,
  678. track: item.track
  679. }
  680. })
  681. })
  682. return () => {
  683. return (
  684. <div class={styles.detail}>
  685. <ColSticky position="top">
  686. <div ref={headers}>
  687. <ColHeader
  688. background="transparent"
  689. hideHeader={false}
  690. border={false}
  691. isFixed={false}
  692. color="#fff"
  693. title={musicDetail.value?.musicSheetName}
  694. backIconColor="white"
  695. v-slots={{
  696. right: () => (
  697. <div
  698. class={styles.shareBtn}
  699. style={{
  700. color: '#fff'
  701. }}
  702. onClick={onShare}
  703. >
  704. <Image src={iconShare} />
  705. 分享
  706. </div>
  707. )
  708. }}
  709. />
  710. </div>
  711. </ColSticky>
  712. <img class={styles.bgImg} src={musicDetail.value?.titleImg} />
  713. <div class={styles.bgContent}></div>
  714. <div
  715. class={styles.musicContainer}
  716. style={{
  717. marginTop: '16px',
  718. height: `calc(100vh - var(--header-height) - var(--bottom-height) - 16px)`
  719. }}
  720. >
  721. <div>
  722. <Cell
  723. border={false}
  724. center
  725. class={styles.musicInfo}
  726. v-slots={{
  727. icon: () => (
  728. <Image
  729. class={styles.pImg}
  730. src={musicDetail.value?.titleImg}
  731. />
  732. ),
  733. title: () => (
  734. <div class={styles.info}>
  735. <h4
  736. class="van-ellipsis"
  737. // onClick={() => handleGotoMusicScore(musicDetail.value)}
  738. >
  739. {musicDetail.value?.musicSheetName}
  740. </h4>
  741. <p
  742. style={{
  743. display: 'flex',
  744. alignItems: 'center'
  745. }}
  746. >
  747. {paymentType.value.map(
  748. tag =>
  749. tag && (
  750. <Tag
  751. style={{ color: colors[tag]?.color }}
  752. class={styles.tag}
  753. type="success"
  754. plain
  755. >
  756. {colors[tag].text}
  757. </Tag>
  758. )
  759. )}
  760. {musicDetail.value?.exquisiteFlag === 1 && (
  761. <Image
  762. class={styles.exquisiteFlag}
  763. src={getAssetsHomeFile('icon_exquisite.png')}
  764. />
  765. )}
  766. {musicDetail.value?.albumNums > 0 && (
  767. <Image
  768. class={styles.songAlbum}
  769. src={getAssetsHomeFile('icon_album_active.png')}
  770. />
  771. )}
  772. <span
  773. class={[
  774. styles.coomposer,
  775. browser().isApp &&
  776. musicDetail.value?.sourceType === 'TEACHER' &&
  777. state.platformType === 'STUDENT' &&
  778. styles.links
  779. ]}
  780. onClick={() => {
  781. if (
  782. browser().isApp &&
  783. musicDetail.value?.sourceType === 'TEACHER' &&
  784. state.platformType === 'STUDENT'
  785. ) {
  786. router.push({
  787. path: '/teacherHome',
  788. query: {
  789. teacherId: musicDetail.value?.userId,
  790. tabs: 'music'
  791. }
  792. })
  793. }
  794. }}
  795. >
  796. <img class={styles.iconAlbum2} src={iconAlbum2} />
  797. <span>
  798. {musicDetail.value?.userName ||
  799. '游客' + (musicDetail.value?.userId || '')}
  800. </span>
  801. </span>
  802. </p>
  803. </div>
  804. ),
  805. value: () => (
  806. <>
  807. <span
  808. style={{
  809. visibility:
  810. state.platformType === 'TEACHER'
  811. ? 'visible'
  812. : 'hidden'
  813. }}
  814. class={styles.download}
  815. onClick={() => {
  816. if (musicDetail.value?.coursewareStatus) {
  817. removeCourse()
  818. } else {
  819. onAddCourse()
  820. }
  821. }}
  822. >
  823. <img
  824. src={
  825. musicDetail.value?.coursewareStatus
  826. ? iconRemoveCourse
  827. : iconAddCourse
  828. }
  829. />
  830. <span>
  831. {musicDetail.value?.coursewareStatus
  832. ? '移出课件'
  833. : '添加课件'}
  834. </span>
  835. </span>
  836. <span
  837. class={styles.download}
  838. onClick={() => toggleFavorite()}
  839. >
  840. <img
  841. src={
  842. musicDetail.value?.favorite
  843. ? iconCollectActive
  844. : iconCollect
  845. }
  846. />
  847. <span>收藏</span>
  848. </span>
  849. </>
  850. )
  851. }}
  852. />
  853. <div class={styles.functionSection}>
  854. <div
  855. class={styles.functionItem}
  856. onClick={() => {
  857. router.push({
  858. path: '/look-album-list',
  859. query: {
  860. id: musicDetail.value?.id,
  861. musicSubject: musicDetail.value?.musicSubject
  862. }
  863. })
  864. }}
  865. >
  866. <img src={iconAlbum} />
  867. <span>专辑</span>
  868. </div>
  869. {/* {musicDetail.value?.notation ? (
  870. <span
  871. class={styles.download}
  872. onClick={() => {
  873. staff.status = true
  874. }}
  875. style={{
  876. display:
  877. musicDetail.value?.musicSheetType !== 'CONCERT'
  878. ? ''
  879. : 'none'
  880. }}
  881. >
  882. <img src={iconChangeStaff} />
  883. <span>转谱</span>
  884. </span>
  885. ) : null} */}
  886. <div
  887. class={styles.functionItem}
  888. style={{
  889. display:
  890. musicDetail.value?.musicSheetType === 'CONCERT'
  891. ? ''
  892. : 'none'
  893. }}
  894. onClick={() => {
  895. if (musicDetail.value?.musicSheetType === 'CONCERT') {
  896. staffData.open = true
  897. }
  898. }}
  899. >
  900. <img src={iconChange} />
  901. <span>切换乐器</span>
  902. </div>
  903. {musicDetail.value?.notation ? (
  904. <div
  905. class={styles.functionItem}
  906. style={{
  907. display:
  908. musicDetail.value?.musicSheetType === 'SINGLE'
  909. ? ''
  910. : 'none'
  911. }}
  912. onClick={() => {
  913. staff.status = true
  914. }}
  915. >
  916. <img src={iconChange} />
  917. <span>转谱</span>
  918. </div>
  919. ) : null}
  920. <div
  921. class={styles.functionItem}
  922. onClick={() => {
  923. if (musicPdfUrl.value) {
  924. const songName =
  925. musicDetail.value?.musicSheetName +
  926. (staffData.instrumentName
  927. ? `(${staffData.instrumentName})`
  928. : '')
  929. promisefiyPostMessage({
  930. api: 'downloadFile',
  931. content: {
  932. downloadUrl: musicPdfUrl.value,
  933. fileName: songName
  934. }
  935. })
  936. return
  937. }
  938. if (showImg.value.length > 0) {
  939. downloadStatus.value = true
  940. } else {
  941. Toast('暂无图片')
  942. }
  943. }}
  944. >
  945. <img src={iconDownload} />
  946. <span>下载</span>
  947. </div>
  948. </div>
  949. </div>
  950. <div class={styles.musicContent}>
  951. {musicDetail.value?.musicSheetType === 'CONCERT' ||
  952. musicPdfUrl.value ||
  953. !defaultImgs.value[staff.radio] ? (
  954. <>
  955. {musicPdfUrl.value ? (
  956. <iframe
  957. id="staffIframeRef"
  958. style={{
  959. opacity: loading.value ? 0 : 1
  960. }}
  961. src={staffData.iframeSrc}
  962. onLoad={() => {
  963. // 判断是用哪个渲染的
  964. loading.value = false
  965. }}
  966. ></iframe>
  967. ) : (
  968. <>
  969. <p class={styles.musicTitle}>
  970. {(musicDetail.value?.musicSheetName
  971. ? musicDetail.value?.musicSheetName
  972. : '') +
  973. (staffData.instrumentName
  974. ? `(${staffData.instrumentName})`
  975. : '')}
  976. </p>
  977. {loading.value && (
  978. <div>
  979. <Vue3Lottie
  980. animationData={AstronautJSON}
  981. class={styles.finch}
  982. ></Vue3Lottie>
  983. {/* <p class={styles.finchLoad}>加载中...</p> */}
  984. </div>
  985. )}
  986. <iframe
  987. id="staffIframeRef"
  988. style={{
  989. opacity: loading.value ? 0 : 1
  990. }}
  991. // src={staffData.iframeSrc}
  992. onLoad={() => {
  993. musicIframeLoad()
  994. }}
  995. ></iframe>
  996. </>
  997. )}
  998. </>
  999. ) : (
  1000. // <>
  1001. // {loading.value && (
  1002. // <>
  1003. // <Vue3Lottie
  1004. // animationData={AstronautJSON}
  1005. // class={styles.finch}
  1006. // ></Vue3Lottie>
  1007. // <p class={styles.finchLoad}>加载中...</p>
  1008. // </>
  1009. // )}
  1010. // <iframe
  1011. // id="staffIframeRef"
  1012. // style={{
  1013. // opacity: loading.value ? 0 : 1
  1014. // }}
  1015. // src={staffData.iframeSrc}
  1016. // onLoad={() => {
  1017. // if (!defaultImgs.value[staff.radio]) {
  1018. // onChangeStaff(staff.radio)
  1019. // } else {
  1020. // musicIframeLoad()
  1021. // }
  1022. // }}
  1023. // ></iframe>
  1024. // </>
  1025. <>
  1026. <p class={styles.musicTitle}>
  1027. {(musicDetail.value?.musicSheetName
  1028. ? musicDetail.value?.musicSheetName
  1029. : '') +
  1030. (staffData.instrumentName
  1031. ? `(${staffData.instrumentName})`
  1032. : '')}
  1033. </p>
  1034. {showImg.value.length > 0 ? (
  1035. <div class={styles.musicImg}>
  1036. <img src={showImg.value[0]} alt="" />
  1037. </div>
  1038. ) : loading.value ? (
  1039. <>
  1040. <Vue3Lottie
  1041. animationData={AstronautJSON}
  1042. class={styles.finch}
  1043. ></Vue3Lottie>
  1044. <p class={styles.finchLoad}>加载中...</p>
  1045. </>
  1046. ) : (
  1047. <div class={styles.empty}>
  1048. <Image src={emtpy} class={styles.emptyImg} />
  1049. <p class={styles.emptyTip}>暂无乐谱预览图</p>
  1050. </div>
  1051. )}
  1052. </>
  1053. )}
  1054. </div>
  1055. </div>
  1056. {musicDetail.value?.id && (
  1057. <ColSticky
  1058. position="bottom"
  1059. background="white"
  1060. varName="--bottom-height"
  1061. >
  1062. <div ref={footers}>
  1063. <div class={styles.videoOperation}>
  1064. {audioFileUrl.value && (
  1065. <>
  1066. {!buyState.value.play &&
  1067. freeRate.value != 100 &&
  1068. freeRate.value != 0 && (
  1069. <div class={[styles.audition]}>
  1070. <img src={iconListen} />
  1071. <span>每首曲目可试听{freeRate.value}%</span>
  1072. </div>
  1073. )}
  1074. <div class={[styles.audio, styles.collectCell]}>
  1075. <audio id="player" controls ref={audio}>
  1076. <source src={audioFileUrl.value} type="audio/mp3" />
  1077. </audio>
  1078. </div>
  1079. </>
  1080. )}
  1081. </div>
  1082. {/* 判断是否是免费的,或者已经购买过,是否从专辑过来的 */}
  1083. {buyState.value.play ||
  1084. (state.platformType === 'TEACHER' &&
  1085. buyState.value.hasTenantAlbum) ? (
  1086. <Button
  1087. round
  1088. block
  1089. type="primary"
  1090. color="linear-gradient(180deg, #59E5D5 0%, #2DC7AA 100%)"
  1091. onClick={() => {
  1092. const throttleFn = useThrottleFn(() => {
  1093. player.value && player.value.stop()
  1094. const item: any = partColumns.value.find(
  1095. (c: any) => c.value === staffData.partIndex
  1096. )
  1097. // const index = staffData.tempPartList.findIndex(
  1098. // (i: any) => i.track === item?.track
  1099. // )
  1100. // 新版云教练的谱面类型使用musicRenderType字段
  1101. const musicRenderType = staff.radio === 'staff' ? 'staff' : staff.radio === 'first' ? 'firstTone' : staff.radio === 'fixed' ? 'fixedTone' : '';
  1102. musicBuy(musicDetail.value, () => {}, {
  1103. 'part-index': item?.xmlValue || 0,
  1104. musicRenderType
  1105. })
  1106. }, 500)
  1107. throttleFn()
  1108. }}
  1109. >
  1110. 立即练习
  1111. </Button>
  1112. ) : (
  1113. <div class={styles.colSticky}>
  1114. {/* 只有,有点播类型的才显示价格 */}
  1115. {buyState.value.charge && (
  1116. <div class={styles.priceSection}>
  1117. <span>点播价:</span>
  1118. <span class={styles.price}>
  1119. <i>¥</i>
  1120. {moneyFormat(musicDetail.value?.musicPrice)}
  1121. </span>
  1122. </div>
  1123. )}
  1124. <div class={[styles.buyBtn]}>
  1125. {/* 判断是否是需要收费的 */}
  1126. {buyState.value.charge && (
  1127. <Button
  1128. round
  1129. type="primary"
  1130. color="linear-gradient(180deg, #59E5D5 0%, #2DC7AA 100%)"
  1131. class={styles.primary}
  1132. onClick={onBuy}
  1133. >
  1134. 立即点播
  1135. </Button>
  1136. )}
  1137. {/* 判断是否有会员的 */}
  1138. {buyState.value.vip && (
  1139. <Button
  1140. round
  1141. block={!buyState.value.charge ? true : false}
  1142. type="primary"
  1143. color="linear-gradient(180deg, #F7BD8D 0%, #CD8806 100%)"
  1144. class={styles.memeber}
  1145. onClick={() => {
  1146. router.push({
  1147. path: '/memberCenter',
  1148. query: {
  1149. ...route.query
  1150. }
  1151. })
  1152. }}
  1153. >
  1154. {studentActivityId.value > 0 && (
  1155. <div class={[styles.buttonDiscount]}>专属优惠</div>
  1156. )}
  1157. 开通会员
  1158. </Button>
  1159. )}
  1160. </div>
  1161. </div>
  1162. )}
  1163. </div>
  1164. </ColSticky>
  1165. )}
  1166. <Popup
  1167. v-model:show={shareStatus.value}
  1168. style={{ background: 'transparent' }}
  1169. teleport="body"
  1170. >
  1171. <ColShare
  1172. teacherId={state.user.data?.userId}
  1173. shareUrl={shareUrl.value}
  1174. shareType="music"
  1175. >
  1176. <div class={styles.shareMate}>
  1177. {shareDiscount.value === 1 && (
  1178. <div class={styles.tagDiscount}>专属优惠</div>
  1179. )}
  1180. <img
  1181. class={styles.icon}
  1182. crossorigin="anonymous"
  1183. src={musicDetail.value?.titleImg + `?t=${+new Date()}`}
  1184. />
  1185. <div class={styles.info}>
  1186. <h4 class="van-multi-ellipsis--l2">
  1187. {musicDetail.value?.musicSheetName}
  1188. </h4>
  1189. <p>作曲人:{musicDetail.value?.composer}</p>
  1190. </div>
  1191. </div>
  1192. </ColShare>
  1193. </Popup>
  1194. <Popup v-model:show={downloadStatus.value} position="bottom" round>
  1195. {downloadStatus.value && (
  1196. <Download
  1197. imgList={JSON.parse(JSON.stringify(showImg.value))}
  1198. musicSheetName={
  1199. musicDetail.value?.musicSheetName +
  1200. (staffData.instrumentName
  1201. ? `(${staffData.instrumentName})`
  1202. : '')
  1203. }
  1204. />
  1205. )}
  1206. </Popup>
  1207. <Popup
  1208. v-model:show={staff.status}
  1209. teleport="body"
  1210. closeable
  1211. style={{ width: '80%' }}
  1212. round
  1213. >
  1214. <div class={styles.staffContainer}>
  1215. <div class={styles.staffTitle}>选择转换曲谱</div>
  1216. <RadioGroup v-model={staff.radio}>
  1217. <CellGroup border={false}>
  1218. <Cell
  1219. center
  1220. border={false}
  1221. class={staff.radio === 'staff' ? styles.active : ''}
  1222. onClick={() => onChangeStaff('staff')}
  1223. >
  1224. {{
  1225. icon: () => (
  1226. <Image src={staffDetafult} class={styles.staffImg} />
  1227. ),
  1228. title: () => <span class={styles.name}>五线谱</span>,
  1229. value: () => (
  1230. <Radio name="staff">
  1231. {{
  1232. icon: (props: any) => (
  1233. <Icon
  1234. class={styles.boxStyle}
  1235. size={16}
  1236. name={
  1237. props.checked
  1238. ? activeButtonIcon
  1239. : inactiveButtonIcon
  1240. }
  1241. />
  1242. )
  1243. }}
  1244. </Radio>
  1245. )
  1246. }}
  1247. </Cell>
  1248. <Cell
  1249. center
  1250. border={false}
  1251. class={staff.radio === 'first' ? styles.active : ''}
  1252. onClick={() => onChangeStaff('first')}
  1253. >
  1254. {{
  1255. icon: () => (
  1256. <Image src={firstDefault} class={styles.staffImg} />
  1257. ),
  1258. title: () => <span class={styles.name}>简谱-首调</span>,
  1259. value: () => (
  1260. <Radio name="first">
  1261. {{
  1262. icon: (props: any) => (
  1263. <Icon
  1264. class={styles.boxStyle}
  1265. size={16}
  1266. name={
  1267. props.checked
  1268. ? activeButtonIcon
  1269. : inactiveButtonIcon
  1270. }
  1271. />
  1272. )
  1273. }}
  1274. </Radio>
  1275. )
  1276. }}
  1277. </Cell>
  1278. <Cell
  1279. center
  1280. border={false}
  1281. class={staff.radio === 'fixed' ? styles.active : ''}
  1282. onClick={() => onChangeStaff('fixed')}
  1283. >
  1284. {{
  1285. icon: () => (
  1286. <Image src={fixedDefault} class={styles.staffImg} />
  1287. ),
  1288. title: () => <span class={styles.name}>简谱-固定调</span>,
  1289. value: () => (
  1290. <Radio name="fixed">
  1291. {{
  1292. icon: (props: any) => (
  1293. <Icon
  1294. class={styles.boxStyle}
  1295. size={16}
  1296. name={
  1297. props.checked
  1298. ? activeButtonIcon
  1299. : inactiveButtonIcon
  1300. }
  1301. />
  1302. )
  1303. }}
  1304. </Radio>
  1305. )
  1306. }}
  1307. </Cell>
  1308. </CellGroup>
  1309. </RadioGroup>
  1310. </div>
  1311. </Popup>
  1312. <Popup
  1313. teleport="body"
  1314. position="bottom"
  1315. round
  1316. v-model:show={staffData.open}
  1317. >
  1318. <Picker
  1319. columns={partColumns.value}
  1320. onConfirm={value => {
  1321. staffData.open = false
  1322. staffData.partIndex = value.value
  1323. staffData.partXmlIndex = value.xmlValue
  1324. staffData.instrumentName = value.instrumentName
  1325. showImg.value = []
  1326. nextTick(() => {
  1327. let tempPdf = value?.musicPdfUrl
  1328. if (musicDetail.value?.musicSheetType !== 'CONCERT') {
  1329. // tempPdf = ''
  1330. if (staff.radio === 'first') {
  1331. tempPdf = value?.firstPdfUrl
  1332. } else if (staff.radio === 'fixed') {
  1333. tempPdf = value?.jianPdfUrl
  1334. } else if (staff.radio === 'staff') {
  1335. tempPdf = value?.musicPdfUrl
  1336. }
  1337. }
  1338. if (tempPdf) {
  1339. musicPdfUrl.value = tempPdf
  1340. renderStaff()
  1341. } else {
  1342. musicPdfUrl.value = ''
  1343. loading.value = true
  1344. // 为了处理,之前是使用pdf渲染,现在又用osmd,iframe没有重新加载
  1345. if (staffData.iframeSrc.indexOf('pdf/web') !== -1) {
  1346. renderStaff()
  1347. } else {
  1348. resetRender()
  1349. }
  1350. }
  1351. })
  1352. }}
  1353. onCancel={() => (staffData.open = false)}
  1354. />
  1355. </Popup>
  1356. </div>
  1357. )
  1358. }
  1359. }
  1360. })