index.tsx 40 KB

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