new-index.tsx 44 KB

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