new-index.tsx 37 KB

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