new-index.tsx 45 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396
  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({
  396. api: 'setStatusBarTextColor',
  397. content: { statusBarTextColor: true }
  398. })
  399. await FetchList()
  400. const { height } = useRect(headers as any)
  401. const footer = useRect(footers as any)
  402. heightInfo.value = height + footer.height
  403. // 初始化音频
  404. if (audioFileUrl.value) {
  405. initAudio()
  406. }
  407. window.addEventListener('message', showLoading)
  408. })
  409. onUnmounted(() => {
  410. postMessage({
  411. api: 'setStatusBarTextColor',
  412. content: { statusBarTextColor: false }
  413. })
  414. window.removeEventListener('message', showLoading)
  415. })
  416. const toggleFavorite = async () => {
  417. try {
  418. await request.post('/music/sheet/favorite/' + musicDetail.value?.id, {
  419. prefix:
  420. state.platformType === 'TEACHER' ? '/api-teacher' : '/api-student'
  421. })
  422. musicDetail.value.favorite = musicDetail.value?.favorite ? 0 : 1
  423. musicDetail.value.favoriteCount = musicDetail.value?.favorite
  424. ? musicDetail.value.favoriteCount + 1
  425. : musicDetail.value.favoriteCount - 1 < 0
  426. ? 0
  427. : musicDetail.value.favoriteCount - 1
  428. setTimeout(() => {
  429. Toast(musicDetail.value?.favorite ? '收藏成功' : '取消收藏成功')
  430. }, 100)
  431. } catch (error) {
  432. //
  433. }
  434. }
  435. const onAddCourse = async () => {
  436. try {
  437. const res = await request.post('/api-teacher/courseCourseware/submit', {
  438. data: {
  439. musicSheetId: musicDetail.value.id,
  440. clientType: 'TEACHER',
  441. userId: state.user.data?.userId
  442. }
  443. })
  444. console.log(res)
  445. setTimeout(() => {
  446. musicDetail.value.coursewareId = res.data.id || ''
  447. Toast('添加成功')
  448. musicDetail.value.coursewareStatus = 1
  449. }, 100)
  450. } catch {
  451. //
  452. }
  453. }
  454. const removeCourse = async () => {
  455. Dialog.confirm({
  456. title: '提示',
  457. message: '您是否确定移除课件',
  458. confirmButtonColor: '#269a93',
  459. cancelButtonText: '取消',
  460. confirmButtonText: '确定'
  461. }).then(async () => {
  462. try {
  463. await request.post(
  464. '/api-teacher/courseCourseware/remove/' +
  465. musicDetail.value.coursewareId,
  466. {
  467. data: {}
  468. }
  469. )
  470. setTimeout(() => {
  471. Toast('移除成功')
  472. musicDetail.value.coursewareStatus = 0
  473. }, 100)
  474. } catch {
  475. //
  476. }
  477. })
  478. }
  479. const onBuy = async () => {
  480. const music = musicDetail.value
  481. orderStatus.orderObject.orderType = 'MUSIC'
  482. orderStatus.orderObject.orderName = music.musicSheetName
  483. orderStatus.orderObject.orderDesc = music.musicSheetName
  484. orderStatus.orderObject.actualPrice = music.musicPrice
  485. orderStatus.orderObject.recomUserId = route.query.recomUserId || 0
  486. orderStatus.orderObject.activityId = route.query.activityId || 0
  487. orderStatus.orderObject.orderNo = ''
  488. orderStatus.orderObject.orderList = [
  489. {
  490. orderType: 'MUSIC',
  491. goodsName: music.musicSheetName,
  492. actualPrice: music.musicPrice,
  493. ...music
  494. }
  495. ]
  496. const res = await request.post('/api-student/userOrder/getPendingOrder', {
  497. data: {
  498. goodType: 'MUSIC',
  499. bizId: music.id
  500. }
  501. })
  502. const result = res.data
  503. if (result) {
  504. Dialog.confirm({
  505. title: '提示',
  506. message: '您有一个未支付的订单,是否继续支付?',
  507. theme: 'round-button',
  508. className: 'confirm-button-group',
  509. cancelButtonText: '取消订单',
  510. confirmButtonText: '继续支付'
  511. })
  512. .then(async () => {
  513. orderStatus.orderObject.orderNo = result.orderNo
  514. orderStatus.orderObject.actualPrice = result.actualPrice
  515. orderStatus.orderObject.discountPrice = result.discountPrice
  516. orderStatus.orderObject.paymentConfig = {
  517. ...result.paymentConfig,
  518. paymentVendor: result.paymentVendor,
  519. paymentVersion: result.paymentVersion
  520. }
  521. routerTo()
  522. })
  523. .catch(() => {
  524. Dialog.close()
  525. // 只用取消订单,不用做其它处理
  526. cancelPayment(result.orderNo)
  527. })
  528. } else {
  529. routerTo()
  530. }
  531. }
  532. const routerTo = () => {
  533. const music = musicDetail.value
  534. router.push({
  535. path: '/orderDetail',
  536. query: {
  537. orderType: 'MUSIC',
  538. musicId: music.id
  539. }
  540. })
  541. }
  542. const cancelPayment = async (orderNo: string) => {
  543. try {
  544. await request.post('/api-student/userOrder/orderCancel', {
  545. data: {
  546. orderNo
  547. }
  548. })
  549. } catch {
  550. //
  551. }
  552. }
  553. const paymentType = computed(() => {
  554. let paymentType = musicDetail.value?.paymentType
  555. if (typeof paymentType === 'string') {
  556. paymentType = paymentType.split(',')
  557. return paymentType
  558. }
  559. return []
  560. })
  561. const buyState = computed(() => {
  562. const music = musicDetail.value
  563. return {
  564. hasTenantAlbum: route.query?.tenantAlbumId ? true : false, // 是否从专辑来的
  565. play: music.play ? true : false, // 是否可以播放
  566. free: music?.paymentType.includes('FREE'),
  567. charge: music?.paymentType.includes('CHARGE'),
  568. vip: music?.paymentType.includes('VIP'),
  569. buy: music?.orderStatus === 'PAID' // 是否已买
  570. }
  571. })
  572. const shareStatus = ref(false)
  573. const shareUrl = ref('')
  574. const shareDiscount = ref(0)
  575. const onShare = async () => {
  576. try {
  577. const res = await request.post('/api-teacher/open/musicShareProfit', {
  578. data: {
  579. bizId: musicDetail.value?.id,
  580. userId: state.user.data?.userId
  581. }
  582. })
  583. let url =
  584. location.origin +
  585. `/teacher/#/shareMusic?id=${musicDetail.value?.id}&recomUserId=${state.user.data?.userId}&userType=${state.platformType}`
  586. // 判断是否有活动
  587. if (res.data.discount === 1) {
  588. url += `&activityId=${res.data.activityId}`
  589. }
  590. shareDiscount.value = res.data.discount || 0
  591. console.log(url)
  592. shareUrl.value = url
  593. shareStatus.value = true
  594. return
  595. } catch {
  596. //
  597. }
  598. }
  599. const staffData = reactive({
  600. open: false,
  601. iframeSrc: '',
  602. musicXml: '',
  603. instrumentName: '',
  604. iframeRef: null as any,
  605. partIndex: 0,
  606. partXmlIndex: 0,
  607. partList: [] as any[],
  608. tempPartList: [] as any[],
  609. xmlPartList: [] as any[]
  610. })
  611. /** 渲染五线谱 */
  612. // 长笛、单簧管、萨克斯、小号、长号、圆号、大号、上低音号
  613. const sortList = {
  614. 长笛: 1,
  615. 单簧管: 2,
  616. 中音单簧管: 3,
  617. 低音单簧管: 4,
  618. 高音萨克斯管: 5,
  619. 中音萨克斯管: 6,
  620. 次中音萨克斯管: 7,
  621. 低音萨克斯管: 8,
  622. 小号: 9,
  623. 长号: 10,
  624. 圆号: 11,
  625. 大号: 12,
  626. 上低音号: 13
  627. }
  628. const instrumentSort = (list: Array<any>) => {
  629. list.sort((a, b) => {
  630. return (
  631. (sortList[getInstrumentName(a.track)] || 20) -
  632. (sortList[getInstrumentName(b.track)] || 20)
  633. )
  634. })
  635. return list
  636. }
  637. const renderStaff = async () => {
  638. try {
  639. if (musicDetail.value?.xmlFileUrl) {
  640. // 获取文件
  641. const res = await umiRequest.get(musicDetail.value?.xmlFileUrl, {
  642. mode: 'cors'
  643. })
  644. let partNames: string[] = []
  645. const xml: any = new DOMParser().parseFromString(res, 'text/xml')
  646. for (const item of xml.getElementsByTagName('part-name')) {
  647. if (item.textContent) {
  648. partNames.push(item.textContent)
  649. }
  650. }
  651. partNames = partNames.filter(
  652. (item: any) => !item?.toLocaleUpperCase()?.includes('COMMON')
  653. )
  654. const partList: any = []
  655. for (let j = 0; j < partNames.length; j++) {
  656. partList.push({
  657. name: partNames[j],
  658. value: j
  659. })
  660. }
  661. staffData.xmlPartList = partList
  662. }
  663. // staffData.iframeSrc = `${
  664. // location.origin
  665. // }/osmd/index.html?t=${new Date().getTime()}`
  666. staffData.iframeSrc = `${location.origin}${
  667. location.pathname
  668. }osmd/index.html?t=${new Date().getTime()}`
  669. staffData.musicXml = musicDetail.value?.xmlFileUrl || ''
  670. const tempList = musicDetail.value?.background || []
  671. const tempPartList = [] as any
  672. staffData.xmlPartList.forEach((part: any) => {
  673. const item = tempList.find((item: any) => item.track === part.name)
  674. tempPartList.push({
  675. ...item,
  676. index: part.index
  677. })
  678. })
  679. staffData.partList = tempPartList
  680. // staffData.partList.forEach((part: any) => {
  681. // const item = staffData.xmlPartList.find(
  682. // item => item.name === part.track
  683. // )
  684. // if (item) {
  685. // part.index = item.value
  686. // }
  687. // })
  688. // console.log(staffData.partList, '-staffData.partList')
  689. staffData.tempPartList = JSON.parse(JSON.stringify(staffData.partList))
  690. staffData.partList = instrumentSort(staffData.partList)
  691. staffData.partXmlIndex = staffData.partList[0].index || 0
  692. // console.log(
  693. // staffData.partList,
  694. // 'parts',
  695. // staffData.xmlPartList,
  696. // musicDetail.value?.background
  697. // )
  698. staffData.instrumentName = getInstrumentName(
  699. staffData.partList[staffData.partIndex]?.track
  700. )
  701. } catch (error) {
  702. //
  703. }
  704. }
  705. const musicIframeLoad = () => {
  706. const iframeRef: any = document.getElementById('staffIframeRef')
  707. if (iframeRef && iframeRef.contentWindow.renderXml) {
  708. iframeRef.contentWindow.renderXml(
  709. staffData.musicXml,
  710. staffData.partXmlIndex
  711. )
  712. }
  713. }
  714. const resetRender = () => {
  715. const iframeRef: any = document.getElementById('staffIframeRef')
  716. if (iframeRef && iframeRef.contentWindow.renderXml) {
  717. iframeRef.contentWindow.resetRender(staffData.partXmlIndex)
  718. staffData.instrumentName = getInstrumentName(
  719. staffData.partList[staffData.partIndex]?.track
  720. )
  721. }
  722. }
  723. const resetRenderPage = (type: string, xmlUrl: string) => {
  724. const iframeRef: any = document.getElementById('staffIframeRef')
  725. if (iframeRef && iframeRef.contentWindow.renderXml) {
  726. // console.log('resetRenderPage')
  727. iframeRef.contentWindow.resetRenderPage(type, xmlUrl)
  728. }
  729. }
  730. const partColumns = computed(() => {
  731. return staffData.partList.map((item: any, index: number) => {
  732. const instrumentName = getInstrumentName(item.track)
  733. return {
  734. text: item.track + (instrumentName ? `(${instrumentName})` : ''),
  735. value: index,
  736. xmlValue: index,
  737. track: item.track
  738. }
  739. })
  740. })
  741. const onDownloadApp = () => {
  742. Dialog.alert({
  743. title: '提示',
  744. message: '请在酷乐秀APP中使用',
  745. confirmButtonColor: '#2dc7aa'
  746. }).then(() => {
  747. window.location.href = location.origin + '/student/#/download'
  748. })
  749. }
  750. // 购买
  751. const onSubmit = async () => {
  752. const url =
  753. apiSuffix.value +
  754. '/tenantGroupAlbum/buyAlbumInfo?tenantGroupAlbumId=' +
  755. (route.query.taId || '')
  756. // if (state.albumId) {
  757. // url = url + '?albumId=' + state.albumId
  758. // }
  759. const { data } = await request.get(url)
  760. const details = data[0]
  761. orderStatus.orderObject.orderType = 'TENANT_ALBUM'
  762. orderStatus.orderObject.orderName = details.name
  763. orderStatus.orderObject.orderDesc = details.name
  764. orderStatus.orderObject.actualPrice = details.actualPrice
  765. // orderStatus.orderObject.recomUserId = route.query.recomUserId || 0
  766. // orderStatus.orderObject.activityId = route.query.activityId || 0
  767. orderStatus.orderObject.orderNo = ''
  768. orderStatus.orderObject.orderList = [
  769. {
  770. orderType: 'TENANT_ALBUM',
  771. goodsName: details.name,
  772. actualPrice: details.actualPrice,
  773. price: details.actualPrice,
  774. ...details
  775. }
  776. ]
  777. const res = await request.post('/api-student/userOrder/getPendingOrder', {
  778. data: {
  779. goodType: 'TENANT_ALBUM',
  780. bizId: details.id
  781. }
  782. })
  783. const result = res.data
  784. if (result) {
  785. Dialog.confirm({
  786. title: '提示',
  787. message: '您有一个未支付的订单,是否继续支付?',
  788. theme: 'round-button',
  789. className: 'confirm-button-group',
  790. cancelButtonText: '取消订单',
  791. confirmButtonText: '继续支付'
  792. })
  793. .then(async () => {
  794. orderStatus.orderObject.orderNo = result.orderNo
  795. orderStatus.orderObject.actualPrice = result.actualPrice
  796. orderStatus.orderObject.discountPrice = result.discountPrice
  797. orderStatus.orderObject.paymentConfig = {
  798. ...result.paymentConfig,
  799. paymentVendor: result.paymentVendor,
  800. paymentVersion: result.paymentVersion
  801. }
  802. routerToALBUM(details.id)
  803. })
  804. .catch(() => {
  805. Dialog.close()
  806. // 只用取消订单,不用做其它处理
  807. cancelPaymentALBUM(result.orderNo)
  808. })
  809. } else {
  810. routerToALBUM(details.id)
  811. }
  812. }
  813. const cancelPaymentALBUM = async (orderNo: string) => {
  814. try {
  815. await request.post('/api-student/userOrder/orderCancel/v2', {
  816. data: {
  817. orderNo
  818. }
  819. })
  820. } catch {
  821. //
  822. }
  823. }
  824. const routerToALBUM = (id: string) => {
  825. router.push({
  826. path: '/orderDetail',
  827. query: {
  828. orderType: 'ALBUM',
  829. album: id
  830. }
  831. })
  832. }
  833. return () => (
  834. <div class={styles.detail}>
  835. <TheSticky position="top">
  836. <ColHeader
  837. background={background.value}
  838. border={false}
  839. isFixed={false}
  840. color={color.value}
  841. backIconColor="white"
  842. />
  843. </TheSticky>
  844. <img class={styles.bgImg} src={musicDetail.value?.titleImg} />
  845. <div class={styles.musicContentBg}></div>
  846. <div class={styles.bg}>
  847. <div class={styles.alumWrap}>
  848. <div class={styles.img}>
  849. {/* {albumDetail.value?.paymentType === 'CHARGE' && (
  850. <span class={styles.albumType}>付费</span>
  851. )} */}
  852. <Image
  853. class={styles.image}
  854. width="100%"
  855. height="100%"
  856. fit="cover"
  857. src={musicDetail.value?.titleImg}
  858. />
  859. </div>
  860. <div class={styles.alumDes}>
  861. <div class={[styles.alumTitle, 'van-ellipsis']}>
  862. {musicDetail.value?.musicSheetName}
  863. </div>
  864. <div class={[styles.des, 'van-multi-ellipsis--l2']}>
  865. {!musicDetail.value?.composer
  866. ? `上传者:${musicDetail.value?.addName || ''}`
  867. : `作曲:${musicDetail.value?.composer || ''}`}
  868. </div>
  869. <div class={styles.tags}>
  870. {musicDetail.value?.id && (
  871. <>
  872. {musicDetail.value?.musicTagNames &&
  873. musicDetail.value?.musicTagNames.split(',').map(name => (
  874. <span
  875. style={{
  876. borderColor: colors.FREE.color,
  877. color: colors.FREE.color
  878. }}
  879. class={styles.tag}
  880. >
  881. {name}
  882. </span>
  883. ))}
  884. </>
  885. )}
  886. </div>
  887. </div>
  888. </div>
  889. <div
  890. class={[
  891. styles.alumCollect
  892. // musicDetail.value?.musicSheetType === 'CONCERT'
  893. // ? styles.alumCollectCencert
  894. // : ''
  895. ]}
  896. >
  897. <div
  898. class={styles.alumCollectItem}
  899. onClick={() => {
  900. if (browser().isApp) {
  901. onShare()
  902. } else {
  903. onDownloadApp()
  904. }
  905. }}
  906. >
  907. <Image src={iconShare} />
  908. <span>分享</span>
  909. </div>
  910. {/* {musicDetail.value?.musicSheetType !== 'CONCERT' && ( */}
  911. <div
  912. class={[
  913. styles.alumCollectItem,
  914. showImg.value.length <= 0 ? styles.alumCollectItemActive : ''
  915. ]}
  916. onClick={() => {
  917. if (browser().isApp) {
  918. if (showImg.value.length > 0) {
  919. downloadStatus.value = true
  920. }
  921. } else {
  922. onDownloadApp()
  923. }
  924. }}
  925. >
  926. <img src={iconDownload} />
  927. <span>下载</span>
  928. </div>
  929. {/* )} */}
  930. <div
  931. class={styles.alumCollectItem}
  932. onClick={() => toggleFavorite()}
  933. >
  934. <img src={musicDetail.value?.favorite ? iStart : oStart} />
  935. <span>{musicDetail.value?.favoriteCount}</span>
  936. </div>
  937. </div>
  938. {/* {buyState.value.hasTenantAlbum ? 'true' : 'false'} */}
  939. {musicDetail.value?.id &&
  940. !buyState.value.play &&
  941. !buyState.value.hasTenantAlbum && (
  942. <div class={styles.albumTips}>
  943. {buyState.value.charge && buyState.value.vip ? (
  944. <>
  945. <span>开通会员或点播单曲,即可自由练习该曲谱</span>
  946. <span class={styles.albumPrice}>
  947. ¥{moneyFormat(musicDetail.value?.musicPrice)}
  948. </span>
  949. </>
  950. ) : buyState.value.vip ? (
  951. <span>
  952. <img src={iconMemberSmall} class={styles.iconMemberSmall} />
  953. 此曲谱为会员专享,开通会员即可自由练习该曲谱
  954. </span>
  955. ) : buyState.value.charge ? (
  956. <>
  957. <span>此曲谱为点播曲谱,点播即可自由练习该曲谱</span>
  958. <span class={styles.albumPrice}>
  959. ¥{moneyFormat(musicDetail.value?.musicPrice)}
  960. </span>
  961. </>
  962. ) : (
  963. ''
  964. )}
  965. </div>
  966. )}
  967. </div>
  968. <div class={styles.musicContent}>
  969. {musicDetail.value?.notation ? (
  970. <span
  971. class={styles.iconTransfer}
  972. style={{
  973. display:
  974. musicDetail.value?.musicSheetType === 'SINGLE' ? '' : 'none',
  975. opacity: showImg.value.length <= 0 ? 0.6 : 1
  976. }}
  977. onClick={() => {
  978. if (showImg.value.length <= 0) return
  979. staff.status = true
  980. }}
  981. >
  982. 转谱
  983. </span>
  984. ) : null}
  985. <span
  986. class={styles.iconTransfer}
  987. style={{
  988. display:
  989. musicDetail.value?.musicSheetType === 'CONCERT' ? '' : 'none'
  990. }}
  991. onClick={() => {
  992. staffData.open = true
  993. }}
  994. >
  995. 切换声轨
  996. </span>
  997. <p class={styles.musicTitle}>
  998. {(musicDetail.value?.musicSheetName
  999. ? musicDetail.value?.musicSheetName
  1000. : '') +
  1001. (staffData.instrumentName ? `(${staffData.instrumentName})` : '')}
  1002. </p>
  1003. {musicDetail.value?.musicSheetType === 'CONCERT' ||
  1004. !defaultImgs.value[staff.radio] ? (
  1005. <>
  1006. {loading.value && (
  1007. <div>
  1008. <Vue3Lottie
  1009. animationData={AstronautJSON}
  1010. class={styles.finch}
  1011. ></Vue3Lottie>
  1012. {/* <p class={styles.finchLoad}>加载中...</p> */}
  1013. </div>
  1014. )}
  1015. <iframe
  1016. id="staffIframeRef"
  1017. style={{
  1018. opacity: loading.value ? 0 : 1
  1019. }}
  1020. src={staffData.iframeSrc}
  1021. onLoad={musicIframeLoad}
  1022. ></iframe>
  1023. {/* <OsmdPreview ref={osmdPreviewRef} /> */}
  1024. </>
  1025. ) : (
  1026. <>
  1027. {showImg.value.length > 0 ? (
  1028. <img src={showImg.value[0]} alt="" class={styles.musicImg} />
  1029. ) : loading.value ? (
  1030. <>
  1031. <Vue3Lottie
  1032. animationData={AstronautJSON}
  1033. class={styles.finch}
  1034. ></Vue3Lottie>
  1035. {/* <p class={styles.finchLoad}>加载中...</p> */}
  1036. </>
  1037. ) : (
  1038. <div class={styles.empty}>
  1039. <Image src={emtpy} class={styles.emptyImg} />
  1040. <p class={styles.emptyTip}>暂无乐谱预览图</p>
  1041. </div>
  1042. )}
  1043. </>
  1044. )}
  1045. </div>
  1046. {musicDetail.value?.id && (
  1047. <TheSticky position="bottom">
  1048. <div style={{ backgroundColor: '#fff' }}>
  1049. <div class={styles.videoOperation}>
  1050. {audioFileUrl.value && (
  1051. <>
  1052. {!buyState.value.play &&
  1053. freeRate.value != 100 &&
  1054. freeRate.value != 0 && (
  1055. <div class={[styles.audition]}>
  1056. <img src={iconListen} />
  1057. <span>每首曲目可试听{freeRate.value}%</span>
  1058. </div>
  1059. )}
  1060. <div class={[styles.audio, styles.collectCell]}>
  1061. <audio id="player" controls ref={audio}>
  1062. <source src={audioFileUrl.value} type="audio/mp3" />
  1063. </audio>
  1064. </div>
  1065. </>
  1066. )}
  1067. </div>
  1068. <div ref={footers} class={styles.footers}>
  1069. {/* 判断是否是免费的,或者已经购买过 */}
  1070. {buyState.value.play ? (
  1071. <Button
  1072. round
  1073. block
  1074. type="primary"
  1075. color="linear-gradient( 270deg, #FF204B 0%, #FE5B71 100%)"
  1076. onClick={() => {
  1077. if (!browser().isApp) {
  1078. onDownloadApp()
  1079. return
  1080. }
  1081. const throttleFn = useThrottleFn(() => {
  1082. player.value && player.value.stop()
  1083. const item: any = partColumns.value.find(
  1084. (c: any) => c.value === staffData.partIndex
  1085. )
  1086. // const index = staffData.tempPartList.findIndex(
  1087. // (i: any) => i.track === item?.track
  1088. // )
  1089. musicBuy(musicDetail.value, () => {}, {
  1090. 'part-index': item.xmlValue || 0,
  1091. sett: staff.radio,
  1092. // 1:忽略系统节拍器
  1093. ignoreSysMetronome:
  1094. route.query.subjectType === 'MUSIC' ? 1 : 0
  1095. })
  1096. }, 500)
  1097. throttleFn()
  1098. }}
  1099. >
  1100. 立即练习
  1101. </Button>
  1102. ) : buyState.value.hasTenantAlbum && !buyState.value.play ? (
  1103. <Button
  1104. disabled={route.query.buyStatus === '1'}
  1105. round
  1106. block
  1107. type="primary"
  1108. color="linear-gradient( 270deg, #FF204B 0%, #FE5B71 100%)"
  1109. onClick={() => {
  1110. if (!browser().isApp) {
  1111. onDownloadApp()
  1112. return
  1113. }
  1114. if (route.query.type === 'search') {
  1115. router.push('train-tool')
  1116. } else {
  1117. onSubmit()
  1118. }
  1119. }}
  1120. >
  1121. 开通训练教程
  1122. </Button>
  1123. ) : (
  1124. <div class={[styles.buyBtn]}>
  1125. {/* 判断是否是需要收费的 */}
  1126. {buyState.value.charge && (
  1127. <Button
  1128. round
  1129. type="primary"
  1130. color="linear-gradient(270deg, #FF204B 0%, #FE5B71 100%)"
  1131. class={styles.primary}
  1132. onClick={() => {
  1133. if (!browser().isApp) {
  1134. onDownloadApp()
  1135. return
  1136. }
  1137. onBuy()
  1138. }}
  1139. >
  1140. 立即点播
  1141. </Button>
  1142. )}
  1143. {/* 判断是否有会员的 */}
  1144. {buyState.value.vip && (
  1145. <Button
  1146. round
  1147. block={!buyState.value.charge ? true : false}
  1148. type="primary"
  1149. color="linear-gradient(270deg, #FF204B 0%, #FE5B71 100%)"
  1150. class={styles.memeber}
  1151. onClick={() => {
  1152. if (!browser().isApp) {
  1153. onDownloadApp()
  1154. return
  1155. }
  1156. router.push({
  1157. path: '/memberCenter',
  1158. query: {
  1159. ...route.query
  1160. }
  1161. })
  1162. }}
  1163. >
  1164. {studentActivityId.value > 0 && (
  1165. <div class={[styles.buttonDiscount]}>专属优惠</div>
  1166. )}
  1167. 开通会员
  1168. </Button>
  1169. )}
  1170. </div>
  1171. )}
  1172. </div>
  1173. </div>
  1174. </TheSticky>
  1175. )}
  1176. <Popup
  1177. v-model:show={shareStatus.value}
  1178. style={{ background: 'transparent' }}
  1179. teleport="body"
  1180. >
  1181. <ColShare
  1182. teacherId={state.user.data?.userId}
  1183. shareUrl={shareUrl.value}
  1184. shareType="music"
  1185. type="tenant"
  1186. >
  1187. <div class={styles.shareMate}>
  1188. {shareDiscount.value === 1 && (
  1189. <div class={styles.tagDiscount}>专属优惠</div>
  1190. )}
  1191. <img
  1192. class={styles.icon}
  1193. crossorigin="anonymous"
  1194. src={musicDetail.value?.titleImg + `?t=${+new Date()}`}
  1195. />
  1196. <div class={styles.info}>
  1197. <h4 class="van-multi-ellipsis--l2">
  1198. {musicDetail.value?.musicSheetName}
  1199. </h4>
  1200. <p>作曲人:{musicDetail.value?.composer}</p>
  1201. </div>
  1202. </div>
  1203. </ColShare>
  1204. </Popup>
  1205. <Popup v-model:show={downloadStatus.value} position="bottom" round>
  1206. {downloadStatus.value && (
  1207. <Download
  1208. imgList={JSON.parse(JSON.stringify(showImg.value))}
  1209. musicSheetName={musicDetail.value.musicSheetName}
  1210. />
  1211. )}
  1212. </Popup>
  1213. <Popup
  1214. v-model:show={staff.status}
  1215. teleport="body"
  1216. closeable
  1217. style={{ width: '80%' }}
  1218. class={styles.staffChange}
  1219. round
  1220. >
  1221. <div class={styles.staffContainer}>
  1222. <div class={styles.staffTitle}>选择转换曲谱</div>
  1223. <RadioGroup v-model={staff.radio}>
  1224. <CellGroup border={false}>
  1225. <Cell
  1226. center
  1227. border={false}
  1228. class={staff.radio === 'staff' ? styles.active : ''}
  1229. onClick={() => onChangeStaff('staff')}
  1230. >
  1231. {{
  1232. icon: () => (
  1233. <Image src={staffDetafult} class={styles.staffImg} />
  1234. ),
  1235. title: () => <span class={styles.name}>五线谱</span>,
  1236. value: () => (
  1237. <Radio name="staff">
  1238. {{
  1239. icon: (props: any) => (
  1240. <Icon
  1241. class={styles.boxStyle}
  1242. name={
  1243. props.checked
  1244. ? activeButtonIcon
  1245. : inactiveButtonIcon
  1246. }
  1247. />
  1248. )
  1249. }}
  1250. </Radio>
  1251. )
  1252. }}
  1253. </Cell>
  1254. <Cell
  1255. center
  1256. border={false}
  1257. class={staff.radio === 'first' ? styles.active : ''}
  1258. onClick={() => onChangeStaff('first')}
  1259. >
  1260. {{
  1261. icon: () => (
  1262. <Image src={firstDefault} class={styles.staffImg} />
  1263. ),
  1264. title: () => <span class={styles.name}>简谱-首调</span>,
  1265. value: () => (
  1266. <Radio name="first">
  1267. {{
  1268. icon: (props: any) => (
  1269. <Icon
  1270. class={styles.boxStyle}
  1271. name={
  1272. props.checked
  1273. ? activeButtonIcon
  1274. : inactiveButtonIcon
  1275. }
  1276. />
  1277. )
  1278. }}
  1279. </Radio>
  1280. )
  1281. }}
  1282. </Cell>
  1283. <Cell
  1284. center
  1285. border={false}
  1286. class={staff.radio === 'fixed' ? styles.active : ''}
  1287. onClick={() => onChangeStaff('fixed')}
  1288. >
  1289. {{
  1290. icon: () => (
  1291. <Image src={fixedDefault} class={styles.staffImg} />
  1292. ),
  1293. title: () => <span class={styles.name}>简谱-固定调</span>,
  1294. value: () => (
  1295. <Radio name="fixed">
  1296. {{
  1297. icon: (props: any) => (
  1298. <Icon
  1299. class={styles.boxStyle}
  1300. name={
  1301. props.checked
  1302. ? activeButtonIcon
  1303. : inactiveButtonIcon
  1304. }
  1305. />
  1306. )
  1307. }}
  1308. </Radio>
  1309. )
  1310. }}
  1311. </Cell>
  1312. </CellGroup>
  1313. </RadioGroup>
  1314. </div>
  1315. </Popup>
  1316. <Popup
  1317. teleport="body"
  1318. position="bottom"
  1319. round
  1320. v-model:show={staffData.open}
  1321. >
  1322. <Picker
  1323. columns={partColumns.value}
  1324. onConfirm={value => {
  1325. staffData.open = false
  1326. staffData.partIndex = value.value
  1327. staffData.partXmlIndex = value.xmlValue
  1328. showImg.value = []
  1329. nextTick(() => {
  1330. resetRender()
  1331. })
  1332. }}
  1333. onCancel={() => (staffData.open = false)}
  1334. />
  1335. </Popup>
  1336. </div>
  1337. )
  1338. }
  1339. })