index.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. import { defineComponent, onMounted, Ref, ref, watch } from 'vue'
  2. import request from '/src/helpers/request'
  3. import originRequest from 'umi-request'
  4. import MusicSheet from '/src/music-sheet'
  5. import runtime from '/src/pages/detail/runtime'
  6. import {
  7. formatXML,
  8. onlyVisible,
  9. getAllNodes,
  10. getCustomInfo,
  11. getBoundingBoxByverticalNote,
  12. getParentNote,
  13. } from '/src/pages/detail/helpers'
  14. import SettingState from '/src/pages/detail/setting-state'
  15. import detailState from '/src/pages/detail/state'
  16. import { useOriginSearch } from '../colexiu/uses'
  17. import styles from '../colexiu/index.module.less'
  18. import detailStyles from './index.module.less'
  19. import { OpenSheetMusicDisplay } from '/osmd-extended/src'
  20. import { MusicSheelDetail, ShaeetStatusType } from '../colexiu/index.d'
  21. import Header, { active } from './header'
  22. import { colorsClass } from '/src/pages/report'
  23. import { getLeveByScoreMeasure } from '/src/pages/detail/evaluating/helper'
  24. import { Button, Skeleton } from 'vant'
  25. import Empty from '/src/components/empty'
  26. import { useSpecialShapedScreen } from '../colexiu/uses/use-app'
  27. const search = useOriginSearch()
  28. const useXml = async (url: string, detail: MusicSheelDetail) => {
  29. const xml = await originRequest(url)
  30. let score = ref<string>('')
  31. const parseXmlInfo = getCustomInfo(xml)
  32. score.value = formatXML(parseXmlInfo.parsedXML, {
  33. title: detail.musicSheetName,
  34. })
  35. const partIndex = Number(search['part-index']) || 0
  36. score.value = onlyVisible(score.value, partIndex)
  37. return score
  38. }
  39. const useDetail = (id: number | string): [Ref<ShaeetStatusType>, Ref<MusicSheelDetail>, Ref<any>] => {
  40. const status = ref<ShaeetStatusType>('loading')
  41. const data = ref<MusicSheelDetail>({})
  42. const record = ref<any>({})
  43. onMounted(async () => {
  44. status.value = 'loading'
  45. try {
  46. const recordRes = await request.get('/musicPracticeRecord/getLastEvaluationMusicalNotesPlayStats',{
  47. params: {
  48. recordId: search.id,
  49. },
  50. })
  51. if (!recordRes.data) {
  52. status.value = 'error'
  53. return
  54. }
  55. record.value = recordRes.data
  56. const res = await request.get(`/musicSheet/detail/${record.value?.musicalNotesPlayStats.examSongId}`)
  57. data.value = res.data
  58. detailState.partIndex = recordRes.data.partIndex || 0
  59. status.value = 'success'
  60. } catch (error) {
  61. status.value = 'error'
  62. console.log(error)
  63. }
  64. })
  65. return [status, data, record]
  66. }
  67. export default defineComponent({
  68. name: 'Colexiu',
  69. setup() {
  70. const headerRef = ref()
  71. const renderLoading = ref(true)
  72. const renderError = ref(false)
  73. const score = ref<string>('')
  74. const useedid = ref<string[]>([])
  75. const allNote = ref<any[]>([])
  76. const [detailStatus, detail, record] = useDetail(search.id as string)
  77. watch(detailStatus, async () => {
  78. if (detailStatus.value === 'success' && detail.value.xmlFileUrl) {
  79. const xml = await useXml(detail.value.xmlFileUrl, detail.value)
  80. // console.log(runtime.songs, detailState.partListNames)
  81. score.value = xml.value
  82. }
  83. })
  84. // useUser()
  85. useSpecialShapedScreen()
  86. const getOffsetPosition = (type: keyof typeof colorsClass): string => {
  87. switch (type) {
  88. case 'CADENCE_FAST':
  89. return 'translateX(2px)'
  90. case 'CADENCE_SLOW':
  91. return 'translateX(-2px)'
  92. case 'INTONATION_HIGH':
  93. return 'translateY(-2px)'
  94. case 'INTONATION_LOW':
  95. return 'translateY(2px)'
  96. default:
  97. return ''
  98. }
  99. }
  100. const filterNotes = () => {
  101. const include = ['RIGHT', 'WRONG', 'CADENCE_WRONG']
  102. if (active.value === 'pitch') {
  103. include.push(...['CADENCE_FAST', 'CADENCE_SLOW'])
  104. } else if (active.value === 'rhythm') {
  105. include.push(...['INTONATION_HIGH', 'INTONATION_LOW'])
  106. } else if (active.value === 'completion') {
  107. include.push(...['INTEGRITY_WRONG'])
  108. }
  109. return record.value.musicalNotesPlayStats.notesData.filter((item: any) => include.includes(item.musicalErrorType))
  110. }
  111. const setViewColor = () => {
  112. clearViewColor()
  113. renderLoading.value = false
  114. for (const note of filterNotes()) {
  115. const active = allNote.value[note.musicalNotesIndex]
  116. setTimeout(() => {
  117. if (useedid.value.includes(active.id)) {
  118. return
  119. }
  120. useedid.value.push(active.id)
  121. const svgEl = document.getElementById('vf-' + active.id)
  122. const stemEl = document.getElementById('vf-' + active.id + '-stem')
  123. const errType = note.musicalErrorType as keyof typeof colorsClass
  124. const isNeedCopyElement = ['INTONATION_HIGH', 'INTONATION_LOW', 'CADENCE_FAST', 'CADENCE_SLOW'].includes(
  125. errType
  126. )
  127. stemEl?.classList.add(colorsClass[errType])
  128. svgEl?.classList.add(colorsClass[errType])
  129. if (svgEl && isNeedCopyElement) {
  130. stemEl?.classList.remove(colorsClass[errType])
  131. stemEl?.classList.add(colorsClass.RIGHT)
  132. svgEl?.classList.remove(colorsClass[errType])
  133. svgEl?.classList.add(colorsClass.RIGHT)
  134. const copySvg = svgEl.querySelector('.vf-notehead')!.cloneNode(true) as SVGSVGElement
  135. copySvg.style.transform = getOffsetPosition(errType)
  136. svgEl.style.opacity = '.7'
  137. if (stemEl) {
  138. stemEl.style.opacity = '.7'
  139. }
  140. copySvg.id = 'vf-' + active.id + '-copy'
  141. copySvg?.classList.add(colorsClass[errType])
  142. // stemEl?.classList.add(colorsClass.RIGHT)
  143. // @ts-ignore
  144. osmd?.container.querySelector('svg')!.insertAdjacentElement('afterbegin', copySvg)
  145. // svgEl?.parentElement?.appendChild(copySvg)
  146. }
  147. }, 300)
  148. }
  149. }
  150. const removeClass = (el?: HTMLElement | null) => {
  151. if (!el) return
  152. const classList = el.classList.values()
  153. for (const val of classList) {
  154. if (val?.indexOf('vf-') !== 0) {
  155. el.classList.remove(val)
  156. }
  157. }
  158. }
  159. const clearViewColor = () => {
  160. for (const id of useedid.value) {
  161. removeClass(document.getElementById('vf-' + id))
  162. removeClass(document.getElementById('vf-' + id + '-stem'))
  163. const qid = 'vf-' + id + '-copy'
  164. const copyEl = document.getElementById(qid)
  165. if (copyEl) {
  166. copyEl.remove()
  167. }
  168. }
  169. useedid.value = []
  170. }
  171. const onRerender = (osmd: OpenSheetMusicDisplay) => {
  172. renderLoading.value = false
  173. headerRef.value?.autoShow()
  174. setTimeout(() => {
  175. for (const item of Array.from(document.querySelectorAll('.vf-beam'))) {
  176. ;(item as SVGAElement).querySelector('path')?.setAttribute('fill', '#aeaeae')
  177. }
  178. })
  179. runtime.osmd = osmd
  180. allNote.value = getAllNodes(runtime.osmd)
  181. setViewColor()
  182. const setEvaluatings = (note: any, data: any, dontTransition = true) => {
  183. const startNote = getBoundingBoxByverticalNote(note)
  184. detailState.evaluatings = {
  185. ...detailState.evaluatings,
  186. [startNote.measureIndex]: {
  187. ...startNote,
  188. ...getLeveByScoreMeasure(data.score),
  189. score: data.score,
  190. dontTransition,
  191. },
  192. }
  193. }
  194. if (record.value.userMeasureScore) {
  195. for (const key in record.value.userMeasureScore) {
  196. if (Object.prototype.hasOwnProperty.call(record.value.userMeasureScore, key)) {
  197. const data = record.value.userMeasureScore[key]
  198. for (const time of allNote.value) {
  199. if (data.measureRenderIndex == time.noteElement.sourceMeasure.MeasureNumberXML - 1) {
  200. if (!time.noteElement.tie) {
  201. setEvaluatings(time, data)
  202. } else {
  203. for (const item of time.noteElement.tie.notes) {
  204. const note = getParentNote(item)
  205. if (!note) continue
  206. setEvaluatings(note, data, item !== time.noteElement.tie.StartNote)
  207. }
  208. }
  209. }
  210. }
  211. }
  212. }
  213. }
  214. // console.log(detailState.evaluatings, record.value.userMeasureScore)
  215. // detailState.activeDetail.originalSpeed = osmd.Sheet.userStartTempoInBPM
  216. // RuntimeUtils.setAudioInit()
  217. }
  218. const onRenderError = () => {
  219. renderError.value = true
  220. renderLoading.value = false
  221. }
  222. return () => {
  223. const loading = renderLoading.value || detailStatus.value === 'loading'
  224. const error = renderError.value || detailStatus.value === 'error'
  225. // console.log('ColexiuRender', detail.value.musicSubject, score.value)
  226. return (
  227. <div
  228. class={[
  229. styles.container,
  230. SettingState.sett.eyeProtection && 'eyeProtection',
  231. SettingState.sett.camera && 'openCamera',
  232. ]}
  233. >
  234. <Header
  235. className={styles.header}
  236. detail={detail.value}
  237. record={record}
  238. ref={headerRef}
  239. style={{
  240. paddingLeft: detailState.isSpecialShapedScreen ? detailState.notchHeight / 2 + 'px' : 'auto',
  241. }}
  242. onActiveChange={() => setViewColor()}
  243. />
  244. <div
  245. id="colexiu-detail-music-sheet"
  246. class={[styles.musicSheet, detailStyles.musicSheet]}
  247. style={{
  248. paddingLeft: detailState.isSpecialShapedScreen ? detailState.notchHeight / 2 + 'px' : 'auto',
  249. }}
  250. >
  251. {loading && !error && <Skeleton class={styles.skeleton} rowWidth="80%" title row={3} />}
  252. {error && <Empty />}
  253. {score.value && (
  254. <>
  255. <h3
  256. style={{
  257. fontSize: '24px',
  258. fontWeight: 'normal',
  259. textAlign: 'center',
  260. padding: '0 10px',
  261. marginTop: '36px',
  262. marginBottom: '0px',
  263. marginLeft: 'auto',
  264. }}
  265. class="van-ellipsis"
  266. >
  267. {detail.value.musicSheetName}
  268. </h3>
  269. <MusicSheet
  270. score={score.value}
  271. showSection
  272. opotions={{
  273. drawTitle: false,
  274. drawComposer: false,
  275. drawLyricist: false,
  276. drawMetronomeMarks: true,
  277. drawMeasureNumbers: true,
  278. autoResize: false,
  279. }}
  280. EngravingRules={{
  281. DefaultColorNotehead: '#aeaeae',
  282. DefaultColorRest: '#aeaeae',
  283. DefaultColorMusic: '#aeaeae',
  284. DefaultColorStem: '#aeaeae',
  285. DefaultColorChordSymbol: '#aeaeae',
  286. DefaultColorLabel: '#aeaeae',
  287. DYMusicScoreType: SettingState.sett.type,
  288. }}
  289. onRerender={onRerender}
  290. onRenderError={onRenderError}
  291. />
  292. </>
  293. )}
  294. </div>
  295. </div>
  296. )
  297. }
  298. },
  299. })