music-detail.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  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 } 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. status: false,
  33. open: false,
  34. audioReady: false,
  35. iframeSrc: '',
  36. isComberRender: false, // 是否为
  37. musicXml: [] as any,
  38. instrumentName: '',
  39. iframeRef: null as any,
  40. imgs: [] as any,
  41. radio: 'staff' as any,
  42. partList: [] as any[],
  43. partNames: [] as any[],
  44. selectedPartName: '' as any,
  45. selectedPartIndex: 0,
  46. partXmlIndex: 0
  47. })
  48. const loading = ref(false)
  49. const downloadStatus = ref(false)
  50. const showImg = ref([] as any)
  51. watch(
  52. () => staffData.radio,
  53. (val: string) => {
  54. if (val == 'first') {
  55. showImg.value = deepClone(staffData.details.musicFirstSvg?.split(',') || [])
  56. } else if (val == 'fixed') {
  57. showImg.value = deepClone(staffData.details.musicJianSvg?.split(',') || [])
  58. } else {
  59. showImg.value = deepClone(staffData.details.musicImg?.split(',') || [])
  60. }
  61. }
  62. )
  63. const musicIframeLoad = async () => {
  64. const iframeRef: any = document.getElementById('staffIframeRef')
  65. if (iframeRef && iframeRef.contentWindow.renderXml) {
  66. const res = await requestOrigin.get(staffData.details.xmlFileUrl, { mode: 'cors' })
  67. const parseXmlInfo = getCustomInfo(res)
  68. const xml = formatXML(parseXmlInfo.parsedXML)
  69. if (staffData.isComberRender) {
  70. iframeRef.contentWindow.renderXml(xml, staffData.partXmlIndex, staffData.isComberRender)
  71. } else {
  72. const currentXml = onlyVisible(xml, staffData.partXmlIndex)
  73. iframeRef.contentWindow.renderXml(
  74. currentXml,
  75. staffData.partXmlIndex,
  76. staffData.isComberRender
  77. )
  78. }
  79. // iframeRef.contentWindow.renderXml(staffData.details.xmlFileUrl, staffData.partXmlIndex)
  80. }
  81. }
  82. const resetRender = async () => {
  83. const iframeRef: any = document.getElementById('staffIframeRef')
  84. if (iframeRef && iframeRef.contentWindow.renderXml) {
  85. loading.value = true
  86. // iframeRef.contentWindow.resetRender(staffData.partXmlIndex)
  87. // const res = await requestOrigin.get(staffData.details.xmlFileUrl, { mode: 'cors' })
  88. // const parseXmlInfo = getCustomInfo(res)
  89. // const xml = formatXML(parseXmlInfo.parsedXML)
  90. // const currentXml = onlyVisible(xml, staffData.selectedPartIndex)
  91. // iframeRef.contentWindow.renderXml(currentXml, staffData.selectedPartIndex)
  92. const res = await requestOrigin.get(staffData.details.xmlFileUrl, { mode: 'cors' })
  93. const parseXmlInfo = getCustomInfo(res)
  94. const xml = formatXML(parseXmlInfo.parsedXML)
  95. if (staffData.isComberRender) {
  96. iframeRef.contentWindow.renderXml(xml, staffData.partXmlIndex, staffData.isComberRender)
  97. } else {
  98. const currentXml = onlyVisible(xml, staffData.partXmlIndex)
  99. iframeRef.contentWindow.renderXml(currentXml, 0, staffData.isComberRender)
  100. }
  101. }
  102. }
  103. const resetRenderPage = (type: string, xmlUrl: string) => {
  104. const iframeRef: any = document.getElementById('staffIframeRef')
  105. if (iframeRef && iframeRef.contentWindow.renderXml) {
  106. iframeRef.contentWindow.resetRenderPage(type, xmlUrl)
  107. }
  108. }
  109. const renderStaff = async () => {
  110. try {
  111. // staffData.iframeSrc = `https://mantest.dayaedu.com/accompany/osmd/index.html`
  112. staffData.iframeSrc = `${location.origin}${location.pathname}osmd/index.html`
  113. } catch (error) {
  114. //
  115. }
  116. }
  117. const getPartNames = async (xmlUrl: string) => {
  118. const partNames: string[] = []
  119. try {
  120. const res = await requestOrigin.get(xmlUrl, { mode: 'cors' })
  121. const xml: any = new DOMParser().parseFromString(res, 'text/xml')
  122. for (const item of xml.getElementsByTagName('part-name')) {
  123. if (item.textContent) {
  124. partNames.push(item.textContent)
  125. }
  126. }
  127. } catch (error) {
  128. //
  129. }
  130. return partNames.filter((text: string) => text.toLocaleUpperCase() !== 'COMMON') || []
  131. }
  132. const toDetail = async (row: any) => {
  133. if (row.musicSheetType === 'SINGLE') {
  134. loading.value = false
  135. return
  136. }
  137. staffData.partNames = await getPartNames(row.xmlFileUrl)
  138. let partList = row.background || []
  139. partList = partList.filter(
  140. (item: any) => !item.track?.toLocaleUpperCase()?.includes('COMMON')
  141. )
  142. partColumns.value = partList.map((item: any, index: number) => {
  143. const instrumentName = getInstrumentName(item.track)
  144. const xmlIndex = staffData.partNames.findIndex((name: any) => name === item.track)
  145. return {
  146. text: item.track + (instrumentName ? `(${instrumentName})` : ''),
  147. instrumentName: instrumentName,
  148. xmlIndex,
  149. value: index
  150. }
  151. })
  152. // 初始化数据
  153. const defaultShowStaff = partColumns.value[staffData.selectedPartIndex]
  154. staffData.selectedPartName = defaultShowStaff.instrumentName
  155. staffData.partXmlIndex = defaultShowStaff.xmlIndex
  156. }
  157. const getMusicDetail = async () => {
  158. loading.value = true
  159. try {
  160. if (!route.query.id) return
  161. const { data } = await request.get(
  162. state.platformApi + '/musicSheet/detail/' + route.query.id
  163. )
  164. staffData.details = data || {}
  165. showImg.value = staffData.details.musicImg?.split(',') || []
  166. staffData.isComberRender = data.musicSubject === '1'
  167. nextTick(async () => {
  168. if (data.audioFileUrl) {
  169. initAudio()
  170. } else {
  171. await toDetail(staffData.details)
  172. renderStaff()
  173. }
  174. })
  175. } catch (e) {
  176. //
  177. console.log(e)
  178. }
  179. }
  180. const initAudio = async () => {
  181. const controls = [
  182. // 'play-large',
  183. 'play',
  184. 'progress',
  185. 'captions',
  186. // 'fullscreen',
  187. 'current-time',
  188. 'duration'
  189. ]
  190. player.value = new Plyr(audioRef.value, {
  191. controls: controls
  192. })
  193. player.value.on('ready', () => {
  194. staffData.audioReady = true
  195. player.value.muted = false
  196. nextTick(async () => {
  197. // if (staffData.details.musicSheetType === 'SINGLE') {
  198. // loading.value = false
  199. // return
  200. // }
  201. await toDetail(staffData.details)
  202. renderStaff()
  203. })
  204. })
  205. }
  206. //进入云练习
  207. const openView = async (item: any) => {
  208. const src = `${location.origin}/orchestra-music-score/?id=${item.id}&part-index=${staffData.selectedPartIndex}`
  209. console.log('🚀 ~ src:', src)
  210. postMessage({
  211. api: 'openAccompanyWebView',
  212. content: {
  213. url: src,
  214. orientation: 0,
  215. isHideTitle: true,
  216. statusBarTextColor: false,
  217. isOpenLight: true
  218. }
  219. })
  220. }
  221. const onSubmit = () => {
  222. player.value?.pause()
  223. openView(staffData.details)
  224. }
  225. const showLoading = async (e: any) => {
  226. if (e.data?.api === 'musicStaffRender') {
  227. try {
  228. const osmdImg = e.data.osmdImg
  229. const imgs: any = []
  230. for (let i = 0; i < osmdImg.length; i++) {
  231. const img: any = await svgtopng(osmdImg[i].img, osmdImg[i].width, osmdImg[i].height)
  232. imgs.push(img)
  233. }
  234. showImg.value = imgs
  235. } catch (e) {
  236. //
  237. }
  238. loading.value = e.data.loading
  239. }
  240. }
  241. onMounted(async () => {
  242. await getMusicDetail()
  243. window.addEventListener('message', showLoading)
  244. })
  245. onUnmounted(() => {
  246. window.removeEventListener('message', showLoading)
  247. })
  248. return () => (
  249. <div class={styles.musicDetail}>
  250. <OSticky mode="sticky" position="top">
  251. <OHeader border={false} background={'transparent'} />
  252. </OSticky>
  253. <div class={styles.musicContainer}>
  254. <div class={styles.musicInfos}>
  255. <div class={styles.musicImg}>
  256. <Image src={iconBg} />
  257. </div>
  258. <div class={styles.info}>
  259. <p class={styles.names}>
  260. {staffData.details.musicSheetName}
  261. {staffData.details.musicSheetType === 'CONCERT' && staffData.selectedPartName
  262. ? `(${staffData.selectedPartName})`
  263. : ''}
  264. </p>
  265. <p class={styles.author}>{staffData.details.composer}</p>
  266. </div>
  267. </div>
  268. <div class={styles.showImgContainer}>
  269. {/* {staffData.details?.musicSheetType === 'CONCERT' ? (
  270. <> */}
  271. {loading.value && (
  272. <>
  273. <Skeleton title row={7} />
  274. </>
  275. )}
  276. <iframe
  277. id="staffIframeRef"
  278. style={{
  279. opacity: loading.value ? 0 : 1,
  280. width: '100%',
  281. height: '100%'
  282. }}
  283. src={staffData.iframeSrc}
  284. onLoad={musicIframeLoad}
  285. ></iframe>
  286. {/* </> */}
  287. {/* // ) : (
  288. // <>
  289. // {showImg.value.length > 0 && (
  290. // <>
  291. // <img src={showImg.value[0]} alt="" class={styles.musicImg} />
  292. // </>
  293. // )}
  294. // </>
  295. // )} */}
  296. </div>
  297. </div>
  298. {staffData.details.id && (
  299. <OSticky position="bottom" varName="--footer-height">
  300. <div class={styles.bottomStyle} style={{ background: '#fff' }}>
  301. {staffData.details?.audioFileUrl && (
  302. <div
  303. class={[styles.audio, styles.collectCell]}
  304. style={{ opacity: staffData.audioReady ? 1 : 0 }}
  305. >
  306. <audio id="player" controls ref={audioRef} style={{ height: '40px' }}>
  307. <source src={staffData.details?.audioFileUrl} type="audio/mp3" />
  308. </audio>
  309. </div>
  310. )}
  311. <div class={styles.footers}>
  312. <div class={styles.iconGroup}>
  313. <div
  314. class={styles.icon}
  315. onClick={() => {
  316. if (loading.value) return
  317. downloadStatus.value = true
  318. }}
  319. >
  320. <img src={iconDownload} />
  321. <span>下载</span>
  322. </div>
  323. {staffData.details?.musicSheetType === 'CONCERT' ? (
  324. <div
  325. class={styles.icon}
  326. onClick={() => {
  327. if (loading.value) return
  328. staffData.open = true
  329. }}
  330. >
  331. <img src={iconMusic} />
  332. <span>声轨</span>
  333. </div>
  334. ) : (
  335. <div
  336. class={styles.icon}
  337. onClick={() => {
  338. if (loading.value) return
  339. staffData.status = true
  340. }}
  341. >
  342. <img src={iconChange} />
  343. <span>转谱</span>
  344. </div>
  345. )}
  346. </div>
  347. <Button
  348. round
  349. block
  350. type="primary"
  351. disabled={loading.value}
  352. color={'#FF8057'}
  353. onClick={onSubmit}
  354. >
  355. 开始练习
  356. </Button>
  357. </div>
  358. </div>
  359. </OSticky>
  360. )}
  361. <Popup
  362. v-model:show={staffData.status}
  363. teleport="body"
  364. closeable
  365. style={{ width: '80%' }}
  366. class={styles.staffChange}
  367. round
  368. >
  369. <StaffChange
  370. radio={staffData.radio}
  371. onClose={() => (staffData.status = false)}
  372. onChange={(type: string) => {
  373. // 更改预览状态
  374. staffData.radio = type
  375. staffData.status = false
  376. if (type == 'first') {
  377. loading.value = true
  378. resetRenderPage('first', staffData.details.xmlFileUrl)
  379. } else if (type == 'fixed') {
  380. loading.value = true
  381. resetRenderPage('fixed', staffData.details.xmlFileUrl)
  382. } else {
  383. loading.value = true
  384. resetRenderPage('staff', staffData.details.xmlFileUrl)
  385. }
  386. }}
  387. />
  388. </Popup>
  389. <Popup v-model:show={downloadStatus.value} position="bottom" round>
  390. {downloadStatus.value && (
  391. <Download
  392. imgList={JSON.parse(JSON.stringify(showImg.value))}
  393. musicSheetName={staffData.details.musicSheetName}
  394. />
  395. )}
  396. </Popup>
  397. <Popup teleport="body" position="bottom" round v-model:show={staffData.open}>
  398. <Picker
  399. columns={partColumns.value}
  400. onConfirm={(value) => {
  401. staffData.open = false
  402. staffData.selectedPartIndex = value.selectedValues[0]
  403. staffData.selectedPartName = value.selectedOptions[0].instrumentName
  404. staffData.partXmlIndex = value.selectedOptions[0].xmlIndex
  405. // openView({ id: staffData.instrumentName })
  406. nextTick(() => {
  407. resetRender()
  408. })
  409. }}
  410. onCancel={() => (staffData.open = false)}
  411. />
  412. </Popup>
  413. </div>
  414. )
  415. }
  416. })