index.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. import { defineComponent, onMounted, onUnmounted, reactive, Ref, ref, watch,nextTick } from 'vue'
  2. import MusicSheet from '/src/music-sheet'
  3. import store from 'store'
  4. // import { throttle } from 'lodash'
  5. import runtime, * as RuntimeUtils from '/src/pages/detail/runtime'
  6. import { typeById, ITypeContentItem } from '/src/constant/fingering-colexiu'
  7. import detailState from '/src/pages/detail/state'
  8. import EvaluatingTips from '/src/pages/detail/evaluating-tips'
  9. import { getAllNodes, getDuration } from '/src/pages/detail/helpers'
  10. import SettingState from '/src/pages/detail/setting-state'
  11. import {
  12. useMidi,
  13. useOriginSearch,
  14. useFee,
  15. useCamera,
  16. useUser,
  17. useFingering,
  18. useDetail,
  19. useSpecialShapedScreen,
  20. useSuspendPlay,
  21. useXml,
  22. useActivity,
  23. useConfigMusicSheetFreeRate,
  24. } from './uses'
  25. import Buttons, { modelType } from './buttons'
  26. import ButtonsPlayer from './buttons/player'
  27. import Permission from './popups/permission'
  28. import MusicList from './music-list'
  29. import TickPopup from '/src/pages/detail/tick-popup'
  30. import SoundEffect from './popups/sound-effect'
  31. import HelperPopup from './popups/helper'
  32. import { OpenSheetMusicDisplay, PageFormat } from '/osmd-extended/src'
  33. import Fingering from './fingering'
  34. import { Skeleton, Toast } from 'vant'
  35. import Empty from '/src/components/empty'
  36. import formatId from './fingering/format-id'
  37. import { browser } from '/src/helpers/utils'
  38. import { postMessage } from '/src/helpers/native-message'
  39. import { svgtopng } from './helpers'
  40. import { restPromptMain } from '/src/helpers/restPrompt'
  41. import ProductJson from './popups/productJson'
  42. import { useRoute } from 'vue-router'
  43. import styles from './index.module.less'
  44. import Tips from './tips'
  45. const search = useOriginSearch()
  46. const browserInfo = browser()
  47. // json化曲谱的note信息和svg
  48. export const musicJSON = reactive({
  49. json: '',
  50. svg: '',
  51. rended: false, // 渲染完成
  52. })
  53. /** 暴露曲谱实例方便其他地方重新绘制曲谱 */
  54. export const MusicSheetRef = ref()
  55. export default defineComponent({
  56. name: 'Colexiu',
  57. setup() {
  58. const route = useRoute()
  59. // console.log("🚀 ~ route", route.query, search)
  60. detailState.midiPlayIniting = true
  61. const renderLoading = ref(true)
  62. const renderError = ref(false)
  63. const compulsionEvaluating = ref(false)
  64. const score = ref<string>('')
  65. const fingeringStatus: Ref<string> = ref('init')
  66. const fingeringWidth = ref('')
  67. const activeType = ref<object>({})
  68. const fingeringDetail = ref<ITypeContentItem | object>({})
  69. const [detailStatus, detail] = useDetail(search.id as string)
  70. const pages = new PageFormat(650, 884)
  71. /** 监听详情的获取状态,设置指法等信息 */
  72. watch(detailStatus, async () => {
  73. if (detailStatus.value === 'success' && detail.value.xmlFileUrl) {
  74. pushAppMusic(detail.value)
  75. fingeringDetail.value = typeById[formatId(detail.value.code || '')] || {}
  76. const { showFingering, frozenMode, compulsionEvaluating: compulsion } = useActivity()
  77. const [status, width, atype] = await useFingering(showFingering.value ? detail.value.code : undefined)
  78. fingeringStatus.value = status.value as string
  79. fingeringWidth.value = width.value as string
  80. activeType.value = atype.value as object
  81. detailState.frozenMode = frozenMode.value
  82. compulsionEvaluating.value = compulsion.value
  83. }
  84. if (detailStatus.value === 'success' && detail.value.xmlFileUrl) {
  85. const xml = await useXml(detail.value.xmlFileUrl, detail.value)
  86. if (!xml.value) {
  87. renderLoading.value = false
  88. renderError.value = true
  89. return
  90. } else {
  91. score.value = xml.value
  92. }
  93. }
  94. })
  95. function throttle(fn: any, delay: number) {
  96. let valid = true
  97. return function () {
  98. if (!valid) {
  99. return false
  100. }
  101. valid = false
  102. setTimeout(() => {
  103. fn()
  104. valid = true
  105. }, delay)
  106. }
  107. }
  108. const settingFingeringChange = throttle(() => {
  109. // console.log('settingFingeringChange')
  110. const { direction } = fingeringDetail.value as ITypeContentItem
  111. if (direction === 'vertical') {
  112. Toast('加载中,请稍后...')
  113. setTimeout(() => {
  114. MusicSheetRef.value.reRender()
  115. }, 16)
  116. }
  117. }, 300)
  118. onMounted(() => {
  119. ;(window as any).appName = 'colexiu'
  120. RuntimeUtils.event.on('settingFingeringChange', settingFingeringChange)
  121. postMessage({
  122. api: 'setEventTracking',
  123. content: {
  124. type: 'klx_xiaokuAI',
  125. },
  126. })
  127. })
  128. onUnmounted(() => {
  129. RuntimeUtils.event.off('settingFingeringChange', settingFingeringChange)
  130. if (typeof runtime?.audiosInstance?.destroy === 'function') {
  131. runtime.audiosInstance?.destroy()
  132. }
  133. })
  134. useUser()
  135. useSpecialShapedScreen()
  136. useSuspendPlay()
  137. //需要生成缓存数据的条件
  138. const isProductJson = ref(false)
  139. const productRef = ref()
  140. /** 当渲染完成后的回调 */
  141. const onRerender = async (osmd: OpenSheetMusicDisplay) => {
  142. // @ts-ignore
  143. window.isLoading = false
  144. console.log('onRerender', '渲染结束')
  145. postMessage({
  146. api: 'cloudLoading',
  147. content: {
  148. show: false,
  149. type: 'fullscreen',
  150. },
  151. })
  152. console.log('cloudLoading', false)
  153. detailState.initRendered = true
  154. // console.log('onRerender', osmd)
  155. console.log(search)
  156. console.time('获取数据')
  157. runtime.osmd = osmd
  158. detailState.isSpecialBookCategory = true
  159. if (detailState.renderType === 'native') {
  160. detailState.times = getAllNodes(osmd)
  161. }
  162. isProductJson.value =
  163. search.modeType === 'json' ||
  164. !detailState.activeDetail?.musicSvg ||
  165. !detailState.activeDetail?.musicJianSvg ||
  166. !detailState.activeDetail?.musicFirstSvg
  167. if (isProductJson.value) {
  168. const { numerator, denominator } = getDuration(osmd)
  169. try {
  170. musicJSON.json = JSON.stringify({
  171. musicId: detailState.activeDetail.id,
  172. musicSheetName: encodeURIComponent(detailState.activeDetail.musicSheetName),
  173. osmd: {
  174. product: true,
  175. bpm: osmd?.Sheet?.userStartTempoInBPM || (osmd as any)?.bpm,
  176. numerator,
  177. denominator,
  178. scoreSize: SettingState.sett.scoreSize || 'middle'
  179. },
  180. times: detailState.times,
  181. })
  182. // console.log('生成缓存数据', musicJSON)
  183. musicJSON.svg = document.getElementById('osmdSvgPage1')?.outerHTML || '';
  184. nextTick(() => {
  185. musicJSON.svg = document.getElementById('osmdSvgPage1')?.outerHTML || '';
  186. musicJSON.rended = true
  187. productRef.value?.autoProduct()
  188. })
  189. // console.log("🚀 ~ detailState.times", document.getElementById('osmdSvgPage1'))
  190. } catch (error) {
  191. console.log(error)
  192. }
  193. }
  194. console.timeEnd('获取数据')
  195. // console.time('循环引用')
  196. // if (detailState.times.length) {
  197. // for(let i = 0; i < detailState.times.length;i++){
  198. // let item = detailState.times[i]
  199. // item.measures = item.measures.map((n: any) => detailState.times.find((m: any) => m.NoteToGraphicalNoteObjectId === n.NoteToGraphicalNoteObjectId))
  200. // }
  201. // }
  202. // console.timeEnd('循环引用')
  203. console.log('🚀 ~ detailState.times', detailState.times)
  204. const saveSpeed = (store.get('speeds') || {})[search.id as string]
  205. const bpm = (osmd as any).bpm || osmd.Sheet.userStartTempoInBPM
  206. detailState.activeSpeed = saveSpeed || bpm || 100
  207. detailState.baseSpeed = bpm || 100
  208. detailState.code = detail.value?.code || ''
  209. detailState.activeDetail.originalSpeed = detailState.baseSpeed
  210. const songEndTime = detailState.times[detailState.times.length - 1 || 0]?.endtime || 0
  211. if (detailState.isAppPlay) {
  212. const durationNum = songEndTime
  213. useMidi(durationNum, detail.value.midiUrl)
  214. }
  215. if (!runtime.durationNum) {
  216. runtime.durationNum = songEndTime
  217. }
  218. const freeRate = await useConfigMusicSheetFreeRate()
  219. detailState.freeRate = freeRate.value
  220. useFee(detail.value.paymentType || detail.value.chargeType, detail.value.orderStatus)
  221. useCamera()
  222. RuntimeUtils.changeSpeed(detailState.activeSpeed)
  223. if (((detailState.setting?.resets || []) as string[]).includes('SPEED')) {
  224. if (detailState.activeDetail) {
  225. RuntimeUtils.changeSpeed(detailState.activeDetail?.originalSpeed)
  226. }
  227. }
  228. RuntimeUtils.setAudioInit()
  229. renderLoading.value = false
  230. try {
  231. restPromptMain(detailState.times)
  232. } catch (error) {}
  233. if (compulsionEvaluating.value) {
  234. runtime.evaluatingStatus = true
  235. modelType.value = 'evaluation'
  236. }
  237. }
  238. const onStartRender = async () => {
  239. renderLoading.value = true
  240. postMessage({
  241. api: 'cloudLoading',
  242. content: {
  243. show: true,
  244. type: 'fullscreen',
  245. },
  246. })
  247. }
  248. const onRenderError = () => {
  249. // @ts-ignore
  250. window.isLoading = false
  251. postMessage({
  252. api: 'cloudLoading',
  253. content: {
  254. show: false,
  255. type: 'fullscreen',
  256. },
  257. })
  258. renderError.value = true
  259. renderLoading.value = false
  260. }
  261. //给app传伴奏
  262. const pushAppMusic = (detail: any) => {
  263. postMessage({
  264. api: 'cloudAccompanyMessage',
  265. content: {
  266. accompanyUrl: detail.audioFileUrl || detail.metronomeUrl || detail.url || '',
  267. },
  268. })
  269. }
  270. return () => {
  271. const loading = renderLoading.value || detailStatus.value === 'loading'
  272. const error = renderError.value || detailStatus.value === 'error'
  273. // console.log('fingeringStatus', fingeringStatus.value, fingeringWidth.value, fingeringDetail.value)
  274. const { width, paddingRight, paddingLeft, direction } = fingeringDetail.value as ITypeContentItem
  275. const fingeringInited = fingeringStatus.value !== 'init'
  276. const calcWidh = width || '0px'
  277. const calcRight = paddingRight || '0px'
  278. const calcLeft = paddingLeft || '0px'
  279. const isVertical = direction === 'vertical'
  280. const calcRightWidth = direction === 'vertical' ? '20px' : '0px'
  281. const needFingering = fingeringStatus.value === 'show' && SettingState.sett.fingering && !runtime.evaluatingStatus
  282. const needFingeringWidth = direction === 'vertical' && needFingering
  283. const musicSheetStyle = {
  284. ...(isVertical && { margin: 'auto', marginRight: 0 }),
  285. width:
  286. fingeringDetail.value && needFingeringWidth
  287. ? `calc(100% - ${calcWidh} - ${calcRight} - ${calcLeft} - ${calcRightWidth})`
  288. : '',
  289. }
  290. return (
  291. <div
  292. class={[
  293. styles.container,
  294. SettingState.sett.eyeProtection && 'eyeProtection',
  295. browserInfo.android && 'android',
  296. ]}
  297. >
  298. <Buttons class={styles.buttons} />
  299. <div
  300. id="colexiu-detail-music-sheet"
  301. class={[styles.musicSheet, { evaluating: runtime.evaluatingStatus || modelType.value === 'follow' }]}
  302. style={{
  303. paddingLeft: detailState.isSpecialShapedScreen ? detailState.notchHeight / 2 + 'px' : 'auto',
  304. paddingBottom:
  305. needFingering && (fingeringDetail.value as any).height ? (fingeringDetail.value as any).height : '40px',
  306. background: SettingState.sett.camera
  307. ? `rgba(${SettingState.sett.eyeProtection ? '253,244,229' : '255,255,255'} ,${
  308. SettingState.sett.opacity / 100
  309. }) !important`
  310. : '',
  311. }}
  312. >
  313. {loading && !error && <Skeleton class={styles.skeleton} rowWidth="80%" title row={3} />}
  314. {error && <Empty />}
  315. {score.value && fingeringInited && (
  316. <>
  317. <h3
  318. style={{
  319. fontSize: '24px',
  320. fontWeight: 'normal',
  321. textAlign: 'center',
  322. padding: '0 10px',
  323. marginTop: '36px',
  324. marginBottom: '0px',
  325. marginLeft: 'auto',
  326. width: musicSheetStyle.width,
  327. }}
  328. class="van-ellipsis"
  329. >
  330. {detail.value.musicSheetName}
  331. </h3>
  332. <MusicSheet
  333. ref={MusicSheetRef}
  334. style={musicSheetStyle}
  335. score={score.value}
  336. EngravingRules={
  337. search.pageType === 'multiple'
  338. ? {
  339. PageFormat: pages,
  340. DYMusicScoreType: SettingState.sett.type,
  341. }
  342. : {
  343. DYMusicScoreType: SettingState.sett.type,
  344. }
  345. }
  346. opotions={{
  347. drawTitle: false,
  348. drawComposer: true,
  349. drawLyricist: false,
  350. drawMetronomeMarks: true,
  351. drawMeasureNumbers: true,
  352. autoResize: false,
  353. }}
  354. onStartRender={onStartRender}
  355. onRenderError={onRenderError}
  356. onRerender={onRerender}
  357. onLoaddingEnd={() => {
  358. renderLoading.value = false
  359. ;(window as any).isLoading = false
  360. postMessage({
  361. api: 'cloudLoading',
  362. content: {
  363. show: false,
  364. type: 'fullscreen',
  365. },
  366. })
  367. }}
  368. />
  369. {needFingering && (
  370. <Fingering
  371. style={{
  372. background: SettingState.sett.camera
  373. ? `rgba(${SettingState.sett.eyeProtection ? '253,244,229' : '255,255,255'} ,${
  374. SettingState.sett.opacity / 100
  375. })`
  376. : '',
  377. boxShadow: SettingState.sett.camera ? 'none' : '',
  378. }}
  379. code={detail.value.code}
  380. />
  381. )}
  382. </>
  383. )}
  384. </div>
  385. {!loading && !error && <ButtonsPlayer />}
  386. {/* 节拍器弹窗 */}
  387. <TickPopup score={score.value} />
  388. <Permission />
  389. {/* 效音 */}
  390. <SoundEffect />
  391. {/* 投屏帮助 */}
  392. <HelperPopup />
  393. {/** 曲目列表 */}
  394. <MusicList />
  395. {/* 保存json */}
  396. <ProductJson ref={productRef} />
  397. {/* 引导 */}
  398. <Tips />
  399. </div>
  400. )
  401. }
  402. },
  403. })