music-detail.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. import OHeader from '@/components/o-header'
  2. import OSticky from '@/components/o-sticky'
  3. import { defineComponent, nextTick, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
  4. import styles from './music-detail.module.less'
  5. import { Button, Image, Picker, Popup, Skeleton } from 'vant'
  6. import iconBg from './images/music-img-default.png'
  7. import iconDownload from './images/icon-download.png'
  8. import iconChange from './images/icon-change.png'
  9. import iconMusic from './images/icon-music.png'
  10. import { postMessage, promisefiyPostMessage } from '@/helpers/native-message'
  11. import request from '@/helpers/request'
  12. import { state } from '@/state'
  13. import { useRoute } from 'vue-router'
  14. import Plyr from 'plyr'
  15. import 'plyr/dist/plyr.css'
  16. import deepClone from '@/helpers/deep-clone'
  17. import StaffChange from './staff-change'
  18. import Download from './download'
  19. import { svgtopng } from './formatSvgToImg'
  20. import requestOrigin from 'umi-request'
  21. import { getInstrumentName } from '@/constant/instruments'
  22. import { formatXML, getCustomInfo, onlyVisible } from './instrument'
  23. export default defineComponent({
  24. name: 'music-detail',
  25. setup() {
  26. const route = useRoute()
  27. const audioRef = ref()
  28. const player = ref<any>(null)
  29. const partColumns = ref<any>([])
  30. const staffData = reactive({
  31. details: {} as any,
  32. musicPdfUrl: '',
  33. status: false,
  34. open: false,
  35. audioReady: false,
  36. iframeSrc: '',
  37. isComberRender: false, // 是否为
  38. musicXml: [] as any,
  39. instrumentName: '',
  40. iframeRef: null as any,
  41. imgs: [] as any,
  42. radio: 'staff' as any,
  43. partList: [] as any[],
  44. partNames: [] as any[],
  45. selectedPartName: '' as any,
  46. selectedPartIndex: 0,
  47. partXmlIndex: 0
  48. })
  49. const loading = ref(false)
  50. const downloadStatus = ref(false)
  51. const showImg = ref([] as any)
  52. watch(
  53. () => staffData.radio,
  54. (val: string) => {
  55. if (val == 'first') {
  56. showImg.value = deepClone(staffData.details.musicFirstSvg?.split(',') || [])
  57. } else if (val == 'fixed') {
  58. showImg.value = deepClone(staffData.details.musicJianSvg?.split(',') || [])
  59. } else {
  60. showImg.value = deepClone(staffData.details.musicImg?.split(',') || [])
  61. }
  62. }
  63. )
  64. const musicIframeLoad = async () => {
  65. const iframeRef: any = document.getElementById('staffIframeRef')
  66. if (iframeRef && iframeRef.contentWindow.renderXml) {
  67. const res = await requestOrigin.get(staffData.details.xmlFileUrl, { mode: 'cors' })
  68. const parseXmlInfo = getCustomInfo(res)
  69. const xml = formatXML(parseXmlInfo.parsedXML)
  70. if (staffData.isComberRender) {
  71. iframeRef.contentWindow.renderXml(xml, staffData.partXmlIndex, staffData.isComberRender)
  72. } else {
  73. const currentXml = onlyVisible(xml, staffData.partXmlIndex)
  74. iframeRef.contentWindow.renderXml(
  75. currentXml,
  76. staffData.partXmlIndex,
  77. staffData.isComberRender
  78. )
  79. }
  80. // iframeRef.contentWindow.renderXml(staffData.details.xmlFileUrl, staffData.partXmlIndex)
  81. }
  82. }
  83. const resetRender = async () => {
  84. const iframeRef: any = document.getElementById('staffIframeRef')
  85. if (iframeRef && iframeRef.contentWindow.renderXml) {
  86. loading.value = true
  87. // iframeRef.contentWindow.resetRender(staffData.partXmlIndex)
  88. // const res = await requestOrigin.get(staffData.details.xmlFileUrl, { mode: 'cors' })
  89. // const parseXmlInfo = getCustomInfo(res)
  90. // const xml = formatXML(parseXmlInfo.parsedXML)
  91. // const currentXml = onlyVisible(xml, staffData.selectedPartIndex)
  92. // iframeRef.contentWindow.renderXml(currentXml, staffData.selectedPartIndex)
  93. const res = await requestOrigin.get(staffData.details.xmlFileUrl, { mode: 'cors' })
  94. const parseXmlInfo = getCustomInfo(res)
  95. const xml = formatXML(parseXmlInfo.parsedXML)
  96. if (staffData.isComberRender) {
  97. iframeRef.contentWindow.renderXml(xml, staffData.partXmlIndex, staffData.isComberRender)
  98. } else {
  99. const currentXml = onlyVisible(xml, staffData.partXmlIndex)
  100. iframeRef.contentWindow.renderXml(currentXml, 0, staffData.isComberRender)
  101. }
  102. }
  103. }
  104. const resetRenderPage = (type: string, xmlUrl: string) => {
  105. const iframeRef: any = document.getElementById('staffIframeRef')
  106. if (iframeRef && iframeRef.contentWindow.renderXml) {
  107. iframeRef.contentWindow.resetRenderPage(type, xmlUrl)
  108. }
  109. }
  110. const renderStaff = async () => {
  111. try {
  112. if (staffData.musicPdfUrl) {
  113. const iframeRef = document.querySelector("#staffIframeRef") as any
  114. iframeRef.contentWindow.location.replace( `${location.origin}${
  115. location.pathname
  116. }pdf/web/viewer.html?file=${encodeURIComponent(staffData.musicPdfUrl)}&t=${Date.now()}`);
  117. } else {
  118. const iframeRef = document.querySelector("#staffIframeRef") as any
  119. iframeRef.contentWindow.location.replace(`${location.origin}${location.pathname}osmd/index.html`);
  120. }
  121. } catch (error) {
  122. //
  123. }
  124. }
  125. const getPartNames = async (xmlUrl: string) => {
  126. const partNames: string[] = []
  127. try {
  128. const res = await requestOrigin.get(xmlUrl, { mode: 'cors' })
  129. const xml: any = new DOMParser().parseFromString(res, 'text/xml')
  130. for (const item of xml.getElementsByTagName('part-name')) {
  131. if (item.textContent) {
  132. partNames.push(item.textContent)
  133. }
  134. }
  135. } catch (error) {
  136. //
  137. }
  138. return partNames.filter((text: string) => text.toLocaleUpperCase() !== 'COMMON') || []
  139. }
  140. /** 获取分轨信息 */
  141. const getInstrumentItem = (instruments: any, name = '') => {
  142. name = name.toLocaleLowerCase().replace(/ /g, '') //.replace(/\d*/gi, '')
  143. if (!name) return ''
  144. for (let key in instruments) {
  145. const item = instruments[key]
  146. const _key = item.track?.toLocaleLowerCase().replace(/ /g, '') //.replace(/\d*/gi, '')
  147. console.log(_key)
  148. if (_key === name) {
  149. return item
  150. }
  151. }
  152. return ''
  153. }
  154. const toDetail = async (row: any) => {
  155. if (row.musicSheetType === 'SINGLE') {
  156. loading.value = false
  157. staffData.musicPdfUrl = row.musicPdfUrl
  158. return
  159. }
  160. staffData.partNames = await getPartNames(row.xmlFileUrl)
  161. let partList = row.background || []
  162. partList = partList.filter(
  163. (item: any) => !item.track?.toLocaleUpperCase()?.includes('COMMON')
  164. )
  165. partColumns.value = partList.map((item: any, index: number) => {
  166. const instrumentName = getInstrumentName(item.track)
  167. const xmlIndex = staffData.partNames.findIndex((name: any) => name === item.track)
  168. return {
  169. text: item.track + (instrumentName ? `(${instrumentName})` : ''),
  170. instrumentName: instrumentName,
  171. track: item.track,
  172. xmlIndex,
  173. value: index
  174. }
  175. })
  176. // 初始化数据
  177. const defaultShowStaff = partColumns.value[staffData.selectedPartIndex]
  178. staffData.selectedPartName = defaultShowStaff.instrumentName
  179. staffData.partXmlIndex = defaultShowStaff.xmlIndex
  180. // 是否为并
  181. if(staffData.isComberRender) {
  182. staffData.musicPdfUrl = row.musicPdfUrl
  183. } else {
  184. const item = getInstrumentItem(
  185. staffData.details?.background || [],
  186. partColumns.value[staffData.selectedPartIndex]?.track
  187. )
  188. console.log(item, 'item', staffData.details?.background, partColumns.value[staffData.selectedPartIndex]?.track)
  189. if (item) {
  190. staffData.musicPdfUrl = item.musicPdfUrl
  191. } else {
  192. staffData.musicPdfUrl = ''
  193. }
  194. }
  195. }
  196. const getMusicDetail = async () => {
  197. loading.value = true
  198. try {
  199. if (!route.query.id) return
  200. const { data } = await request.get(
  201. state.platformApi + '/musicSheet/detail/' + route.query.id
  202. )
  203. staffData.details = data || {}
  204. showImg.value = staffData.details.musicImg?.split(',') || []
  205. staffData.isComberRender = data.musicSubject === '1'
  206. nextTick(async () => {
  207. if (data.audioFileUrl) {
  208. initAudio()
  209. } else {
  210. await toDetail(staffData.details)
  211. renderStaff()
  212. }
  213. })
  214. } catch (e) {
  215. //
  216. console.log(e)
  217. }
  218. }
  219. const initAudio = async () => {
  220. const controls = [
  221. // 'play-large',
  222. 'play',
  223. 'progress',
  224. 'captions',
  225. // 'fullscreen',
  226. 'current-time',
  227. 'duration'
  228. ]
  229. player.value = new Plyr(audioRef.value, {
  230. controls: controls
  231. })
  232. player.value.on('ready', () => {
  233. staffData.audioReady = true
  234. player.value.muted = false
  235. nextTick(async () => {
  236. // if (staffData.details.musicSheetType === 'SINGLE') {
  237. // loading.value = false
  238. // return
  239. // }
  240. await toDetail(staffData.details)
  241. renderStaff()
  242. })
  243. })
  244. }
  245. //进入云练习
  246. const openView = async (item: any) => {
  247. let src = `${location.origin}/orchestra-music-score/?id=${item.id}&part-index=${staffData.selectedPartIndex}`
  248. if (staffData.details.musicSheetType === 'SINGLE') {
  249. // 默认进页面显示对应的曲谱
  250. let lineType = 'staff'
  251. if (staffData.radio === 'first') {
  252. lineType = 'firstTone'
  253. } else if (staffData.radio === 'fixed') {
  254. lineType = 'fixedTone'
  255. } else if (staffData.radio === 'staff') {
  256. lineType = 'staff'
  257. }
  258. src += '&musicRenderType=' + lineType
  259. }
  260. console.log('🚀 ~ src:', src)
  261. postMessage({
  262. api: 'openAccompanyWebView',
  263. content: {
  264. url: src,
  265. orientation: 0,
  266. isHideTitle: true,
  267. statusBarTextColor: false,
  268. isOpenLight: true
  269. }
  270. })
  271. }
  272. const onSubmit = () => {
  273. player.value?.pause()
  274. openView(staffData.details)
  275. }
  276. const showLoading = async (e: any) => {
  277. if (e.data?.api === 'musicStaffRender') {
  278. try {
  279. const osmdImg = e.data.osmdImg
  280. const imgs: any = []
  281. for (let i = 0; i < osmdImg.length; i++) {
  282. const img: any = await svgtopng(osmdImg[i].img, osmdImg[i].width, osmdImg[i].height)
  283. imgs.push(img)
  284. }
  285. showImg.value = imgs
  286. } catch (e) {
  287. //
  288. }
  289. loading.value = e.data.loading
  290. }
  291. }
  292. onMounted(async () => {
  293. await getMusicDetail()
  294. window.addEventListener('message', showLoading)
  295. })
  296. onUnmounted(() => {
  297. window.removeEventListener('message', showLoading)
  298. })
  299. return () => (
  300. <div class={styles.musicDetail}>
  301. <OSticky mode="sticky" position="top">
  302. <OHeader border={false} background={'transparent'} />
  303. </OSticky>
  304. <div class={styles.musicContainer}>
  305. <div class={styles.musicInfos}>
  306. <div class={styles.musicImg}>
  307. <Image src={iconBg} />
  308. </div>
  309. {staffData.details.id && !staffData.musicPdfUrl && <div class={styles.info}>
  310. <p class={styles.names}>
  311. {staffData.details.musicSheetName}
  312. {staffData.details.musicSheetType === 'CONCERT' && staffData.selectedPartName
  313. ? `(${staffData.selectedPartName})`
  314. : ''}
  315. </p>
  316. <p class={styles.author}>{staffData.details.composer}</p>
  317. </div>}
  318. </div>
  319. <div class={[styles.showImgContainer, staffData.musicPdfUrl && styles.pdfContainer]}>
  320. {/* {staffData.details?.musicSheetType === 'CONCERT' ? (
  321. <> */}
  322. {loading.value && (
  323. <>
  324. <Skeleton title row={7} />
  325. </>
  326. )}
  327. {staffData.details.id ? staffData.musicPdfUrl ? (
  328. <iframe
  329. style={{
  330. opacity: loading.value ? 0 : 1,
  331. width: '100%',
  332. height: '100%'
  333. }}
  334. id="staffIframeRef"
  335. // src={staffData.iframeSrc}
  336. onLoad={() => {
  337. // 判断是用哪个渲染的
  338. loading.value = false
  339. }}
  340. ></iframe>
  341. ) : (
  342. <iframe
  343. id="staffIframeRef"
  344. style={{
  345. opacity: loading.value ? 0 : 1,
  346. width: '100%',
  347. height: '100%'
  348. }}
  349. // src={staffData.iframeSrc}
  350. onLoad={() => {
  351. musicIframeLoad()
  352. }}
  353. ></iframe>
  354. ) : ''}
  355. {/* <iframe
  356. id="staffIframeRef"
  357. style={{
  358. opacity: loading.value ? 0 : 1,
  359. width: '100%',
  360. height: '100%'
  361. }}
  362. src={staffData.iframeSrc}
  363. onLoad={musicIframeLoad}
  364. ></iframe> */}
  365. {/* </> */}
  366. {/* // ) : (
  367. // <>
  368. // {showImg.value.length > 0 && (
  369. // <>
  370. // <img src={showImg.value[0]} alt="" class={styles.musicImg} />
  371. // </>
  372. // )}
  373. // </>
  374. // )} */}
  375. </div>
  376. </div>
  377. {staffData.details.id && (
  378. <OSticky position="bottom" varName="--footer-height">
  379. <div class={styles.bottomStyle} style={{ background: '#fff' }}>
  380. {staffData.details?.audioFileUrl && (
  381. <div
  382. class={[styles.audio, styles.collectCell]}
  383. style={{ opacity: staffData.audioReady ? 1 : 0 }}
  384. >
  385. <audio id="player" controls ref={audioRef} style={{ height: '40px' }}>
  386. <source src={staffData.details?.audioFileUrl} type="audio/mp3" />
  387. </audio>
  388. </div>
  389. )}
  390. <div class={styles.footers}>
  391. <div class={styles.iconGroup}>
  392. <div
  393. class={styles.icon}
  394. onClick={() => {
  395. if (loading.value) return
  396. if (staffData.musicPdfUrl) {
  397. const songName =
  398. staffData.details?.musicSheetName +
  399. (staffData.details.musicSheetType === 'CONCERT' && staffData.selectedPartName
  400. ? `(${
  401. staffData.selectedPartName
  402. })`
  403. : "");
  404. promisefiyPostMessage({
  405. api: "downloadFile",
  406. content: {
  407. downloadUrl: staffData.musicPdfUrl,
  408. fileName: songName,
  409. },
  410. });
  411. } else {
  412. downloadStatus.value = true
  413. }
  414. }}
  415. >
  416. <img src={iconDownload} />
  417. <span>下载</span>
  418. </div>
  419. {staffData.details?.musicSheetType === 'CONCERT' ? (
  420. <div
  421. class={styles.icon}
  422. onClick={() => {
  423. if (loading.value) return
  424. staffData.open = true
  425. }}
  426. >
  427. <img src={iconMusic} />
  428. <span>声轨</span>
  429. </div>
  430. ) : (
  431. <div></div>
  432. )}
  433. {/* <div
  434. class={styles.icon}
  435. onClick={() => {
  436. if (loading.value) return
  437. staffData.status = true
  438. }}
  439. >
  440. <img src={iconChange} />
  441. <span>转谱</span>
  442. </div> */}
  443. </div>
  444. <Button
  445. round
  446. block
  447. type="primary"
  448. disabled={loading.value}
  449. color={'#FF8057'}
  450. onClick={onSubmit}
  451. >
  452. 开始练习
  453. </Button>
  454. </div>
  455. </div>
  456. </OSticky>
  457. )}
  458. <Popup
  459. v-model:show={staffData.status}
  460. teleport="body"
  461. closeable
  462. style={{ width: '80%' }}
  463. class={styles.staffChange}
  464. round
  465. >
  466. <StaffChange
  467. radio={staffData.radio}
  468. onClose={() => (staffData.status = false)}
  469. onChange={(type: string) => {
  470. // 更改预览状态
  471. staffData.radio = type
  472. staffData.status = false
  473. if (type == 'first') {
  474. loading.value = true
  475. resetRenderPage('first', staffData.details.xmlFileUrl)
  476. } else if (type == 'fixed') {
  477. loading.value = true
  478. resetRenderPage('fixed', staffData.details.xmlFileUrl)
  479. } else {
  480. loading.value = true
  481. resetRenderPage('staff', staffData.details.xmlFileUrl)
  482. }
  483. }}
  484. />
  485. </Popup>
  486. <Popup v-model:show={downloadStatus.value} position="bottom" round>
  487. {downloadStatus.value && (
  488. <Download
  489. imgList={JSON.parse(JSON.stringify(showImg.value))}
  490. musicSheetName={staffData.details.musicSheetName}
  491. />
  492. )}
  493. </Popup>
  494. <Popup teleport="body" position="bottom" round v-model:show={staffData.open}>
  495. <Picker
  496. columns={partColumns.value}
  497. onConfirm={(value) => {
  498. staffData.open = false
  499. staffData.selectedPartIndex = value.selectedValues[0]
  500. staffData.selectedPartName = value.selectedOptions[0].instrumentName
  501. staffData.partXmlIndex = value.selectedOptions[0].xmlIndex
  502. // openView({ id: staffData.instrumentName })
  503. nextTick(() => {
  504. const item = getInstrumentItem(staffData.details.background || [], value.selectedOptions[0].track);
  505. console.log(item, 'nextTick', staffData.details)
  506. let tempPdf = ""
  507. if (staffData?.isComberRender) {
  508. if (staffData?.musicPdfUrl) {
  509. tempPdf = staffData?.musicPdfUrl
  510. }
  511. } else {
  512. tempPdf = item?.musicPdfUrl
  513. }
  514. if (tempPdf) {
  515. staffData.musicPdfUrl = tempPdf
  516. // staffLoading.value = true
  517. renderStaff()
  518. } else {
  519. staffData.musicPdfUrl = ""
  520. loading.value = true
  521. // 为了处理,之前是使用pdf渲染,现在又用osmd,iframe没有重新加载
  522. if (staffData.iframeSrc.indexOf("pdf/web") !== -1) {
  523. renderStaff()
  524. } else {
  525. resetRender()
  526. }
  527. }
  528. // console.log(value, "value", item);
  529. });
  530. }}
  531. onCancel={() => (staffData.open = false)}
  532. />
  533. </Popup>
  534. </div>
  535. )
  536. }
  537. })