index.tsx 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154
  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. let showImg = [] 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 = firstList.value
  120. } else if (val == 'fixed') {
  121. showImg = fixedList.value
  122. } else {
  123. showImg = 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 = 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) {
  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 = 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. loading.value = e.data.loading
  303. const osmdImg = e.data.osmdImg
  304. showImg = []
  305. const img = await svgtopng(osmdImg.img, osmdImg.width, osmdImg.height)
  306. const fileName =
  307. route.query.id + state.user.data.userId + +new Date() + '.png'
  308. const obj = {
  309. filename: fileName,
  310. bucketName: 'cloud-coach',
  311. postData: {
  312. filename: fileName,
  313. acl: 'public-read',
  314. key: fileName
  315. }
  316. }
  317. const { data } = await getUploadSign(obj, true)
  318. const dataObj = {
  319. policy: data.policy,
  320. signature: data.signature,
  321. key: fileName,
  322. KSSAccessKeyId: data.kssAccessKeyId,
  323. acl: 'public-read',
  324. name: fileName
  325. }
  326. const files = base64ToBlob(img)
  327. const ossUploadUrl = getOssUploadUrl('cloud-coach')
  328. const imgurl = await onOnlyFileUpload(ossUploadUrl, {
  329. ...dataObj,
  330. file: files
  331. })
  332. showImg = [imgurl]
  333. }
  334. }
  335. onMounted(async () => {
  336. await FetchList()
  337. const { height } = useRect(headers as any)
  338. const footer = useRect(footers as any)
  339. heightInfo.value = height + footer.height
  340. // 初始化音频
  341. if (audioFileUrl.value) {
  342. initAudio()
  343. }
  344. window.addEventListener('message', showLoading)
  345. })
  346. onUnmounted(() => {
  347. window.removeEventListener('message', showLoading)
  348. })
  349. const toggleFavorite = async () => {
  350. try {
  351. await request.post('/music/sheet/favorite/' + musicDetail.value?.id, {
  352. prefix:
  353. state.platformType === 'TEACHER' ? '/api-teacher' : '/api-student'
  354. })
  355. musicDetail.value.favorite = musicDetail.value?.favorite ? 0 : 1
  356. musicDetail.value.favoriteCount = musicDetail.value?.favorite
  357. ? musicDetail.value.favoriteCount + 1
  358. : musicDetail.value.favoriteCount - 1 < 0
  359. ? 0
  360. : musicDetail.value.favoriteCount - 1
  361. setTimeout(() => {
  362. Toast(musicDetail.value?.favorite ? '收藏成功' : '取消收藏成功')
  363. }, 100)
  364. } catch (error) {
  365. //
  366. }
  367. }
  368. const onAddCourse = async () => {
  369. try {
  370. const res = await request.post('/api-teacher/courseCourseware/submit', {
  371. data: {
  372. musicSheetId: musicDetail.value.id,
  373. clientType: 'TEACHER',
  374. userId: state.user.data?.userId
  375. }
  376. })
  377. console.log(res)
  378. setTimeout(() => {
  379. musicDetail.value.coursewareId = res.data.id || ''
  380. Toast('添加成功')
  381. musicDetail.value.coursewareStatus = 1
  382. }, 100)
  383. } catch {
  384. //
  385. }
  386. }
  387. const removeCourse = async () => {
  388. Dialog.confirm({
  389. title: '提示',
  390. message: '您是否确定移除课件',
  391. confirmButtonColor: '#269a93',
  392. cancelButtonText: '取消',
  393. confirmButtonText: '确定'
  394. }).then(async () => {
  395. try {
  396. await request.post(
  397. '/api-teacher/courseCourseware/remove/' +
  398. musicDetail.value.coursewareId,
  399. {
  400. data: {}
  401. }
  402. )
  403. setTimeout(() => {
  404. Toast('移除成功')
  405. musicDetail.value.coursewareStatus = 0
  406. }, 100)
  407. } catch {
  408. //
  409. }
  410. })
  411. }
  412. const onBuy = async () => {
  413. const music = musicDetail.value
  414. orderStatus.orderObject.orderType = 'MUSIC'
  415. orderStatus.orderObject.orderName = music.musicSheetName
  416. orderStatus.orderObject.orderDesc = music.musicSheetName
  417. orderStatus.orderObject.actualPrice = music.musicPrice
  418. orderStatus.orderObject.recomUserId = route.query.recomUserId || 0
  419. orderStatus.orderObject.activityId = route.query.activityId || 0
  420. orderStatus.orderObject.orderNo = ''
  421. orderStatus.orderObject.orderList = [
  422. {
  423. orderType: 'MUSIC',
  424. goodsName: music.musicSheetName,
  425. actualPrice: music.musicPrice,
  426. ...music
  427. }
  428. ]
  429. const res = await request.post('/api-student/userOrder/getPendingOrder', {
  430. data: {
  431. goodType: 'MUSIC',
  432. bizId: music.id
  433. }
  434. })
  435. const result = res.data
  436. if (result) {
  437. Dialog.confirm({
  438. title: '提示',
  439. message: '您有一个未支付的订单,是否继续支付?',
  440. confirmButtonColor: '#269a93',
  441. cancelButtonText: '取消订单',
  442. confirmButtonText: '继续支付'
  443. })
  444. .then(async () => {
  445. orderStatus.orderObject.orderNo = result.orderNo
  446. orderStatus.orderObject.actualPrice = result.actualPrice
  447. orderStatus.orderObject.discountPrice = result.discountPrice
  448. orderStatus.orderObject.paymentConfig = {
  449. ...result.paymentConfig,
  450. paymentVendor: result.paymentVendor,
  451. paymentVersion: result.paymentVersion
  452. }
  453. routerTo()
  454. })
  455. .catch(() => {
  456. Dialog.close()
  457. // 只用取消订单,不用做其它处理
  458. cancelPayment(result.orderNo)
  459. })
  460. } else {
  461. routerTo()
  462. }
  463. }
  464. const routerTo = () => {
  465. const music = musicDetail.value
  466. router.push({
  467. path: '/orderDetail',
  468. query: {
  469. orderType: 'MUSIC',
  470. musicId: music.id
  471. }
  472. })
  473. }
  474. const cancelPayment = async (orderNo: string) => {
  475. try {
  476. await request.post('/api-student/userOrder/orderCancel', {
  477. data: {
  478. orderNo
  479. }
  480. })
  481. } catch {}
  482. }
  483. const paymentType = computed(() => {
  484. let paymentType = musicDetail.value?.paymentType
  485. if (typeof paymentType === 'string') {
  486. paymentType = paymentType.split(',')
  487. return paymentType
  488. }
  489. return []
  490. })
  491. const buyState = computed(() => {
  492. const music = musicDetail.value
  493. return {
  494. play: music.play ? true : false, // 是否可以播放
  495. free: music?.paymentType.includes('FREE'),
  496. charge: music?.paymentType.includes('CHARGE'),
  497. vip: music?.paymentType.includes('VIP'),
  498. buy: music?.orderStatus === 'PAID' // 是否已买
  499. }
  500. })
  501. const shareStatus = ref(false)
  502. const shareUrl = ref('')
  503. const shareDiscount = ref(0)
  504. // console.log(data)
  505. const onShare = async () => {
  506. try {
  507. const res = await request.post('/api-teacher/open/musicShareProfit', {
  508. data: {
  509. bizId: musicDetail.value?.id,
  510. userId: state.user.data?.userId
  511. }
  512. })
  513. let url =
  514. location.origin +
  515. `/teacher/#/shareMusic?id=${musicDetail.value?.id}&recomUserId=${state.user.data?.userId}&userType=${state.platformType}`
  516. // 判断是否有活动
  517. if (res.data.discount === 1) {
  518. url += `&activityId=${res.data.activityId}`
  519. }
  520. shareDiscount.value = res.data.discount || 0
  521. console.log(url)
  522. shareUrl.value = url
  523. shareStatus.value = true
  524. return
  525. } catch {}
  526. }
  527. const staffData = reactive({
  528. open: false,
  529. iframeSrc: '',
  530. musicXml: '',
  531. instrumentName: '',
  532. iframeRef: null as any,
  533. partIndex: 0,
  534. partList: [] as any[]
  535. })
  536. /** 渲染五线谱 */
  537. const renderStaff = () => {
  538. staffData.iframeSrc = `${location.origin}${location.pathname}osmd/index.html`
  539. staffData.musicXml = musicDetail.value?.xmlFileUrl || ''
  540. staffData.partList = musicDetail.value?.background || []
  541. staffData.instrumentName = getInstrumentName(
  542. staffData.partList[staffData.partIndex]?.track
  543. )
  544. }
  545. const musicIframeLoad = () => {
  546. const iframeRef: any = document.getElementById('staffIframeRef')
  547. if (iframeRef && iframeRef.contentWindow.renderXml) {
  548. iframeRef.contentWindow.renderXml(
  549. staffData.musicXml,
  550. staffData.partIndex
  551. )
  552. }
  553. }
  554. const resetRender = () => {
  555. const iframeRef: any = document.getElementById('staffIframeRef')
  556. if (iframeRef && iframeRef.contentWindow.renderXml) {
  557. iframeRef.contentWindow.resetRender(staffData.partIndex)
  558. staffData.instrumentName = getInstrumentName(
  559. staffData.partList[staffData.partIndex]?.track
  560. )
  561. }
  562. }
  563. const partColumns = computed(() => {
  564. return staffData.partList.map((item: any, index: number) => {
  565. const instrumentName = getInstrumentName(item.track)
  566. return {
  567. text: item.track + (instrumentName ? `(${instrumentName})` : ''),
  568. value: index
  569. }
  570. })
  571. })
  572. return () => {
  573. return (
  574. <div class={styles.detail}>
  575. <Sticky position="top">
  576. <div ref={headers}>
  577. <ColHeader
  578. background="transparent"
  579. border={false}
  580. isFixed={false}
  581. color="#fff"
  582. title={musicDetail.value?.musicSheetName}
  583. backIconColor="white"
  584. v-slots={{
  585. right: () => (
  586. <div
  587. class={styles.shareBtn}
  588. style={{
  589. color: '#fff'
  590. }}
  591. onClick={onShare}
  592. >
  593. <Image src={iconShare} />
  594. 分享
  595. </div>
  596. )
  597. }}
  598. />
  599. </div>
  600. </Sticky>
  601. <img class={styles.bgImg} src={musicDetail.value?.titleImg} />
  602. <div class={styles.bgContent}></div>
  603. <div
  604. class={styles.musicContainer}
  605. style={{
  606. marginTop: '16px',
  607. height: `calc(100vh - ${heightInfo.value + 16 + 'px'})`
  608. }}
  609. >
  610. <Cell
  611. border={false}
  612. center
  613. class={styles.musicInfo}
  614. v-slots={{
  615. icon: () => (
  616. <Image
  617. class={styles.pImg}
  618. src={musicDetail.value?.titleImg}
  619. />
  620. ),
  621. title: () => (
  622. <div class={styles.info}>
  623. <h4
  624. class="van-ellipsis"
  625. // onClick={() => handleGotoMusicScore(musicDetail.value)}
  626. >
  627. {musicDetail.value?.musicSheetName}
  628. </h4>
  629. <p
  630. style={{
  631. display: 'flex'
  632. }}
  633. >
  634. {paymentType.value.map(tag => (
  635. <Tag
  636. style={{ color: colors[tag].color }}
  637. class={styles.tag}
  638. type="success"
  639. plain
  640. >
  641. {colors[tag].text}
  642. </Tag>
  643. ))}
  644. {musicDetail.value?.exquisiteFlag === 1 && (
  645. <Image
  646. class={styles.exquisiteFlag}
  647. src={getAssetsHomeFile('icon_exquisite.png')}
  648. />
  649. )}
  650. {musicDetail.value?.albumNums > 0 && (
  651. <Image
  652. class={styles.songAlbum}
  653. src={getAssetsHomeFile('icon_album_active.png')}
  654. />
  655. )}
  656. <span class={styles.coomposer}>
  657. {musicDetail.value?.composer}
  658. </span>
  659. </p>
  660. </div>
  661. ),
  662. value: () => (
  663. <>
  664. {musicDetail.value?.notation ? (
  665. <span
  666. class={styles.download}
  667. onClick={() => {
  668. staff.status = true
  669. }}
  670. style={{
  671. display:
  672. musicDetail.value?.musicSheetType !== 'CONCERT'
  673. ? ''
  674. : 'none'
  675. }}
  676. >
  677. <img src={iconChangeStaff} />
  678. <span>转谱</span>
  679. </span>
  680. ) : null}
  681. <span
  682. class={styles.download}
  683. onClick={() => {
  684. if (showImg.length > 0) {
  685. downloadStatus.value = true
  686. } else {
  687. Toast('暂无图片')
  688. }
  689. }}
  690. >
  691. <img src={iconDownload} />
  692. <span>下载曲谱</span>
  693. </span>
  694. <span
  695. style={{
  696. display:
  697. musicDetail.value?.musicSheetType === 'CONCERT'
  698. ? ''
  699. : 'none'
  700. }}
  701. class={styles.download}
  702. onClick={() => {
  703. staffData.open = true
  704. }}
  705. >
  706. <Icon
  707. style={{
  708. background: 'rgba(246,246,246,1)',
  709. borderRadius: '50%',
  710. padding: '4px'
  711. }}
  712. size="20px"
  713. name="exchange"
  714. />
  715. <span>切换乐器</span>
  716. </span>
  717. </>
  718. )
  719. }}
  720. />
  721. <div class={styles.musicContent}>
  722. <p class={styles.musicTitle}>
  723. {(musicDetail.value?.musicSheetName
  724. ? musicDetail.value?.musicSheetName
  725. : '') +
  726. (staffData.instrumentName
  727. ? `(${staffData.instrumentName})`
  728. : '')}
  729. </p>
  730. {musicDetail.value?.musicSheetType === 'CONCERT' ? (
  731. <>
  732. {loading.value && (
  733. <>
  734. <Vue3Lottie
  735. animationData={AstronautJSON}
  736. class={styles.finch}
  737. ></Vue3Lottie>
  738. <p class={styles.finchLoad}>加载中...</p>
  739. </>
  740. )}
  741. <iframe
  742. id="staffIframeRef"
  743. style={{
  744. opacity: loading.value ? 0 : 1
  745. }}
  746. src={staffData.iframeSrc}
  747. onLoad={musicIframeLoad}
  748. ></iframe>
  749. </>
  750. ) : (
  751. <>
  752. {showImg.length > 0 ? (
  753. <img src={showImg[0]} alt="" class={styles.musicImg} />
  754. ) : loading.value ? (
  755. <>
  756. <Vue3Lottie
  757. animationData={AstronautJSON}
  758. class={styles.finch}
  759. ></Vue3Lottie>
  760. <p class={styles.finchLoad}>加载中...</p>
  761. </>
  762. ) : (
  763. <div class={styles.empty}>
  764. <Image src={emtpy} class={styles.emptyImg} />
  765. <p class={styles.emptyTip}>暂无乐谱预览图</p>
  766. </div>
  767. )}
  768. </>
  769. )}
  770. <div class={styles.videoOperation}>
  771. {audioFileUrl.value && (
  772. <>
  773. {!buyState.value.play &&
  774. freeRate.value != 100 &&
  775. freeRate.value != 0 && (
  776. <div class={[styles.audition]}>
  777. <img src={iconListen} />
  778. <span>每首曲目可试听{freeRate.value}%</span>
  779. </div>
  780. )}
  781. <div class={[styles.audio, styles.collectCell]}>
  782. <audio id="player" controls ref={audio}>
  783. <source src={audioFileUrl.value} type="audio/mp3" />
  784. </audio>
  785. </div>
  786. </>
  787. )}
  788. <div class={[styles.collect, styles.collectCell]}>
  789. <div
  790. class={[styles.userInfo]}
  791. onClick={() => {
  792. if (
  793. browser().isApp &&
  794. musicDetail.value?.sourceType === 'TEACHER' &&
  795. state.platformType === 'STUDENT'
  796. ) {
  797. router.push({
  798. path: '/teacherHome',
  799. query: {
  800. teacherId: musicDetail.value?.userId,
  801. tabs: 'music'
  802. }
  803. })
  804. }
  805. }}
  806. >
  807. <img src={musicDetail.value?.userAvatar || iconTeacher} />
  808. <span>{musicDetail.value?.userName}</span>
  809. </div>
  810. <div class={styles.functionSection}>
  811. <div
  812. class={[styles.collectSection]}
  813. onClick={() => toggleFavorite()}
  814. >
  815. <span>{musicDetail.value?.favoriteCount}人收藏</span>
  816. <img
  817. src={
  818. musicDetail.value?.favorite
  819. ? iconCollectActive
  820. : iconCollect
  821. }
  822. />
  823. </div>
  824. {state.platformType === 'TEACHER' && (
  825. <div
  826. class={[styles.collectSection]}
  827. onClick={() => {
  828. if (musicDetail.value?.coursewareStatus) {
  829. removeCourse()
  830. } else {
  831. onAddCourse()
  832. }
  833. }}
  834. >
  835. <span>
  836. {musicDetail.value?.coursewareStatus
  837. ? '移出课件'
  838. : '添加到课件'}
  839. </span>
  840. {musicDetail.value?.coursewareStatus ? (
  841. <Icon name="clear" />
  842. ) : (
  843. <Icon name="add" size={18} />
  844. )}
  845. </div>
  846. )}
  847. </div>
  848. </div>
  849. </div>
  850. </div>
  851. <div
  852. class={[styles.lookAlbum, styles.collectCell]}
  853. onClick={() => {
  854. router.push({
  855. path: '/look-album-list',
  856. query: {
  857. id: musicDetail.value?.id,
  858. musicSubject: musicDetail.value?.musicSubject
  859. }
  860. })
  861. }}
  862. >
  863. <div>
  864. <img src={iconAlbum} />
  865. <span>进入曲目所在平台专辑列表</span>
  866. </div>
  867. <Icon name="arrow" size={16} color="#666" />
  868. </div>
  869. </div>
  870. {musicDetail.value?.id && (
  871. <ColSticky position="bottom" background="white">
  872. <div ref={footers}>
  873. {/* 判断是否是免费的,或者已经购买过 */}
  874. {buyState.value.play ? (
  875. <Button
  876. round
  877. block
  878. type="primary"
  879. color="linear-gradient(180deg, #59E5D5 0%, #2DC7AA 100%)"
  880. onClick={() => {
  881. player.value && player.value.stop()
  882. musicBuy(musicDetail.value, () => {}, {
  883. 'part-index': staffData.partIndex || 0,
  884. sett: staff.radio
  885. })
  886. }}
  887. >
  888. 立即练习
  889. </Button>
  890. ) : (
  891. <div class={styles.colSticky}>
  892. {/* 只有,有点播类型的才显示价格 */}
  893. {buyState.value.charge && (
  894. <div class={styles.priceSection}>
  895. <span>点播价:</span>
  896. <span class={styles.price}>
  897. <i>¥</i>
  898. {moneyFormat(musicDetail.value?.musicPrice)}
  899. </span>
  900. </div>
  901. )}
  902. <div class={[styles.buyBtn]}>
  903. {/* 判断是否是需要收费的 */}
  904. {buyState.value.charge && (
  905. <Button
  906. round
  907. type="primary"
  908. color="linear-gradient(180deg, #59E5D5 0%, #2DC7AA 100%)"
  909. class={styles.primary}
  910. onClick={onBuy}
  911. >
  912. 立即点播
  913. </Button>
  914. )}
  915. {/* 判断是否有会员的 */}
  916. {buyState.value.vip && (
  917. <Button
  918. round
  919. block={!buyState.value.charge ? true : false}
  920. type="primary"
  921. color="linear-gradient(180deg, #F7BD8D 0%, #CD8806 100%)"
  922. class={styles.memeber}
  923. onClick={() => {
  924. router.push({
  925. path: '/memberCenter',
  926. query: {
  927. ...route.query
  928. }
  929. })
  930. }}
  931. >
  932. {studentActivityId.value > 0 && (
  933. <div class={[styles.buttonDiscount]}>专属优惠</div>
  934. )}
  935. 开通会员
  936. </Button>
  937. )}
  938. </div>
  939. </div>
  940. )}
  941. </div>
  942. </ColSticky>
  943. )}
  944. <Popup
  945. v-model:show={shareStatus.value}
  946. style={{ background: 'transparent' }}
  947. teleport="body"
  948. >
  949. <ColShare
  950. teacherId={state.user.data?.userId}
  951. shareUrl={shareUrl.value}
  952. shareType="music"
  953. >
  954. <div class={styles.shareMate}>
  955. {shareDiscount.value === 1 && (
  956. <div class={styles.tagDiscount}>专属优惠</div>
  957. )}
  958. <img
  959. class={styles.icon}
  960. crossorigin="anonymous"
  961. src={musicDetail.value?.titleImg + `?t=${+new Date()}`}
  962. />
  963. <div class={styles.info}>
  964. <h4 class="van-multi-ellipsis--l2">
  965. {musicDetail.value?.musicSheetName}
  966. </h4>
  967. <p>作曲人:{musicDetail.value?.composer}</p>
  968. </div>
  969. </div>
  970. </ColShare>
  971. </Popup>
  972. <Popup v-model:show={downloadStatus.value} position="bottom" round>
  973. {downloadStatus.value && (
  974. <Download
  975. imgList={JSON.parse(JSON.stringify(showImg))}
  976. musicSheetName={musicDetail.value.musicSheetName}
  977. />
  978. )}
  979. </Popup>
  980. <Popup
  981. v-model:show={staff.status}
  982. teleport="body"
  983. closeable
  984. style={{ width: '80%' }}
  985. round
  986. >
  987. <div class={styles.staffContainer}>
  988. <div class={styles.staffTitle}>选择转换曲谱</div>
  989. <RadioGroup v-model={staff.radio}>
  990. <CellGroup border={false}>
  991. <Cell
  992. center
  993. border={false}
  994. class={staff.radio === 'staff' ? styles.active : ''}
  995. onClick={() => onChangeStaff('staff')}
  996. >
  997. {{
  998. icon: () => (
  999. <Image src={staffDetafult} class={styles.staffImg} />
  1000. ),
  1001. title: () => <span class={styles.name}>五线谱</span>,
  1002. value: () => (
  1003. <Radio name="staff">
  1004. {{
  1005. icon: (props: any) => (
  1006. <Icon
  1007. class={styles.boxStyle}
  1008. size={16}
  1009. name={
  1010. props.checked
  1011. ? activeButtonIcon
  1012. : inactiveButtonIcon
  1013. }
  1014. />
  1015. )
  1016. }}
  1017. </Radio>
  1018. )
  1019. }}
  1020. </Cell>
  1021. <Cell
  1022. center
  1023. border={false}
  1024. class={staff.radio === 'first' ? styles.active : ''}
  1025. onClick={() => onChangeStaff('first')}
  1026. >
  1027. {{
  1028. icon: () => (
  1029. <Image src={firstDefault} class={styles.staffImg} />
  1030. ),
  1031. title: () => <span class={styles.name}>简谱-首调</span>,
  1032. value: () => (
  1033. <Radio name="first">
  1034. {{
  1035. icon: (props: any) => (
  1036. <Icon
  1037. class={styles.boxStyle}
  1038. size={16}
  1039. name={
  1040. props.checked
  1041. ? activeButtonIcon
  1042. : inactiveButtonIcon
  1043. }
  1044. />
  1045. )
  1046. }}
  1047. </Radio>
  1048. )
  1049. }}
  1050. </Cell>
  1051. <Cell
  1052. center
  1053. border={false}
  1054. class={staff.radio === 'fixed' ? styles.active : ''}
  1055. onClick={() => onChangeStaff('fixed')}
  1056. >
  1057. {{
  1058. icon: () => (
  1059. <Image src={fixedDefault} class={styles.staffImg} />
  1060. ),
  1061. title: () => <span class={styles.name}>简谱-固定调</span>,
  1062. value: () => (
  1063. <Radio name="fixed">
  1064. {{
  1065. icon: (props: any) => (
  1066. <Icon
  1067. class={styles.boxStyle}
  1068. size={16}
  1069. name={
  1070. props.checked
  1071. ? activeButtonIcon
  1072. : inactiveButtonIcon
  1073. }
  1074. />
  1075. )
  1076. }}
  1077. </Radio>
  1078. )
  1079. }}
  1080. </Cell>
  1081. </CellGroup>
  1082. </RadioGroup>
  1083. </div>
  1084. </Popup>
  1085. <Popup
  1086. teleport="body"
  1087. position="bottom"
  1088. round
  1089. v-model:show={staffData.open}
  1090. >
  1091. <Picker
  1092. columns={partColumns.value}
  1093. onConfirm={value => {
  1094. staffData.open = false
  1095. staffData.partIndex = value.value
  1096. nextTick(() => {
  1097. resetRender()
  1098. })
  1099. }}
  1100. onCancel={() => (staffData.open = false)}
  1101. />
  1102. </Popup>
  1103. </div>
  1104. )
  1105. }
  1106. }
  1107. })