index.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. import { Icon, Toast } from 'vant'
  2. import { defineComponent, watchEffect, TransitionGroup, ref, Ref, reactive } from 'vue'
  3. import event from '/src/components/music-score/event'
  4. import SettingState from '/src/pages/detail/setting-state'
  5. import state from '../state'
  6. import runtime, { getFirsrNoteByMeasureListIndex, getBoundingBoxByNote, changeSpeed } from '../runtime'
  7. import { getActtiveNoteByTimes, getBoundingBoxByverticalNote, getNoteBySlursStart, setSettionBackground } from '../helpers'
  8. import { formatZoom } from '/src/helpers/utils'
  9. import styles from './index.module.less'
  10. import classNames from 'classnames'
  11. import { modelType } from '/src/subpages/colexiu/buttons'
  12. import { restPromptData } from '/src/helpers/restPrompt'
  13. import { unitTestData } from '/src/subpages/colexiu/unitTest'
  14. import { metronomeData } from "/src/helpers/metronome";
  15. const sectionRef: Ref = ref(null)
  16. const noteInfoItems = reactive({
  17. duration: false,
  18. numerator: false,
  19. denominator: false,
  20. i: false,
  21. time: false,
  22. speed: false,
  23. })
  24. ;(window as unknown as Window & { setNoteInfoItems: (data: typeof noteInfoItems) => void }).setNoteInfoItems = (
  25. data
  26. ) => {
  27. for (const key in data) {
  28. if (Object.prototype.hasOwnProperty.call(data, key)) {
  29. noteInfoItems[key as keyof typeof noteInfoItems] = data[key as keyof typeof noteInfoItems]
  30. }
  31. }
  32. }
  33. /** 根据位置去重复 */
  34. const uniqueByPosition = (list: any[]) => {
  35. const data: {
  36. [key in string]: any
  37. } = {}
  38. for (const item of list) {
  39. if (item && item.start_x) {
  40. data[`${item.x}-${item.y}`] = item
  41. }
  42. }
  43. return data
  44. }
  45. watchEffect(() => {
  46. // 监听状态节点选择状态并且开启提示
  47. if (state.sectionStatus) {
  48. if (!state.section.length) {
  49. state.befireSection = null
  50. Toast.clear()
  51. Toast({ duration: 0, message: '请选择开始节点', position: 'top' })
  52. } else if (state.section.length === 1) {
  53. Toast.clear()
  54. Toast({ duration: 0, message: '请选择结束节点', position: 'top' })
  55. }
  56. } else {
  57. state.section = []
  58. state.sectionBoundingBoxs = []
  59. Toast.clear()
  60. }
  61. })
  62. export default defineComponent({
  63. name: 'section-box',
  64. props: {
  65. type: {
  66. type: String,
  67. default: 'evaluating',
  68. },
  69. top: {
  70. type: Number,
  71. default: 0,
  72. },
  73. left: {
  74. type: Number,
  75. default: 0,
  76. },
  77. },
  78. data() {
  79. return {
  80. sectionTop: 0,
  81. sectionLeft: 0,
  82. }
  83. },
  84. methods: {
  85. setSection(evt: MouseEvent) {
  86. const activeNote = getActtiveNoteByTimes(evt)
  87. if (activeNote && state.section.length < 2) {
  88. const sectionLength = state.section.length
  89. if (sectionLength === 0) {
  90. const note = getNoteBySlursStart(activeNote, true)
  91. state.section.push(state.times[note.i - note.si])
  92. }
  93. if (sectionLength === 1) {
  94. const note = getNoteBySlursStart(activeNote, true, 'end')
  95. state.section.push(state.times[note.i - note.si + note.noteLength - 1])
  96. // 选段状态需要重置播放倍率为1
  97. runtime.basePlayRate = 1;
  98. const currentItem: any = state.section[0];
  99. const currentSpeed = currentItem?.measureSpeed ? currentItem.measureSpeed : state.activeSpeed;
  100. changeSpeed(currentSpeed)
  101. }
  102. }
  103. if (state.section.length === 2) {
  104. Toast.clear()
  105. setSettionBackground()
  106. }
  107. },
  108. sectionClick(evt: MouseEvent): void {
  109. metronomeData.isClick = true;
  110. if (!state.sectionStatus) {
  111. if (state.mode !== 'contact' || runtime.evaluatingStatus) {
  112. return
  113. }
  114. event.emit('section-click', evt)
  115. } else {
  116. // console.log(state.sectionStatus, evt)
  117. this.setSection(evt)
  118. }
  119. },
  120. /** 重复时仅处理一次 */
  121. filterTimes(times: any[]) {
  122. const ids: string[] = []
  123. return times.filter((item) => {
  124. const has = ids.includes(item.id)
  125. ids.push(item.id)
  126. return !has
  127. })
  128. },
  129. },
  130. mounted() {
  131. Toast.clear()
  132. state.section = []
  133. this.sectionTop = sectionRef.value?.getBoundingClientRect().top
  134. this.sectionLeft = sectionRef.value?.getBoundingClientRect().left
  135. },
  136. beforeUnmount() {
  137. Toast.clear()
  138. state.sectionStatus = false
  139. state.section = []
  140. },
  141. render() {
  142. const eyeBorderColor = SettingState.sett.eyeProtection
  143. ? 'var(--eye-section-border-color)'
  144. : 'var(--section-border-color)'
  145. const eyeBackground = (item: any) => {
  146. if (SettingState.sett.eyeProtection) {
  147. return item.before ? 'var(--section-background-color)' : 'var(--eye-section-background-color)'
  148. } else {
  149. return item.before ? 'var(--eye-section-background-color)' : 'var(--section-background-color)'
  150. }
  151. }
  152. // console.log(uniqueByPosition(Object.values(state.evaluatings)), 'state.evaluatings')
  153. // console.log('state.sectionFlash', state.sectionFlash)
  154. const activeNumberXml = state.times[runtime.activeIndex]?.noteElement?.sourceMeasure?.MeasureNumberXML || -2
  155. const restMeasure = restPromptData.list.find((n) => {
  156. const m = activeNumberXml - n.measureNumberXML
  157. return n.allRests && m >= 0 && m < n.multipleRestMeasures
  158. })
  159. const restNumber = restMeasure ? activeNumberXml - restMeasure.measureNumberXML + 1 : 0
  160. const img: HTMLElement = document.querySelector('#cursorImg-0')!
  161. if (restMeasure){
  162. img && metronomeData.cursorMode === 2 && img.classList.remove('lineHide')
  163. } else {
  164. img && metronomeData.cursorMode === 2 && img.classList.add('lineHide')
  165. }
  166. return (
  167. <div class={styles.section} ref={sectionRef}>
  168. {/* 为每个音符添加一个遮罩方便点击 */}
  169. {this.filterTimes(state.times).map((item) => {
  170. if (!item.svgElelent) {
  171. return null
  172. }
  173. let bbox: any
  174. try {
  175. bbox = item.svgElelent.bbox || item.svgElelent.getBoundingBox?.()
  176. if (!bbox && item.svgElelent?.attrs?.el) {
  177. bbox = item.svgElelent.attrs.el.getBBox()
  178. bbox.w = bbox.width < 15 ? 15 : bbox.width
  179. bbox.h = bbox.height < 11 ? 11 : bbox.height
  180. }
  181. } catch (error) {
  182. console.log(error)
  183. }
  184. if (!bbox) {
  185. return null
  186. }
  187. if (SettingState.sett.type === 'jianpu' && item.svgElelent) {
  188. if (item.svgElelent.top_y && item.svgElelent.note_height) {
  189. bbox.y = item.svgElelent.top_y - item.svgElelent.note_height
  190. }
  191. }
  192. let { x, y, h, w } = bbox
  193. let boundingBox = null
  194. // let measureBg = false
  195. const activeNumberIndex = (item?.noteElement?.sourceMeasure?.measureNumber + 1) || -2;
  196. // console.log(activeNumberIndex,'👀👀',metronomeData.activeMetro?.measureNumberXML)
  197. if (item.si === 0) {
  198. boundingBox = getBoundingBoxByNote(item.noteElement)
  199. }
  200. return (
  201. <>
  202. {item.si === 0 && boundingBox && (
  203. <div
  204. data-id={item.id}
  205. data-num={item.noteElement.sourceMeasure.MeasureNumberXML}
  206. style={{
  207. position: 'absolute',
  208. top: formatZoom(boundingBox.y) + 'px',
  209. left: formatZoom(boundingBox.x) + 'px',
  210. height: formatZoom(boundingBox.height) + 'px',
  211. width: formatZoom(boundingBox.width) + 'px',
  212. background: unitTestData.isSelectMeasureMode && state.sectionStatus
  213. ? `${
  214. item?.noteElement?.sourceMeasure?.MeasureNumberXML <
  215. state.section[0]?.noteElement?.sourceMeasure?.MeasureNumberXML ||
  216. item?.noteElement?.sourceMeasure?.MeasureNumberXML >
  217. state.section[1]?.noteElement?.sourceMeasure.MeasureNumberXML
  218. ? 'rgba(0, 0, 0,.28)'
  219. : 'var(--section-background-color)'
  220. }`
  221. : '',
  222. }}
  223. onClick={state.sectionStatus ? this.sectionClick : undefined}
  224. >
  225. {metronomeData.cursorMode === 2 && activeNumberIndex === metronomeData.activeMetro?.measureNumberXML && <div class={styles.lineTEST} style={{ left: metronomeData.activeMetro.left }}></div>}
  226. </div>
  227. )}
  228. <div
  229. data-id={item.id}
  230. data-vf={'vf' + item.id}
  231. class={styles.noteWrap}
  232. style={{
  233. position: 'absolute',
  234. top: formatZoom(y) + 'px',
  235. left: formatZoom(x) + 'px',
  236. height: formatZoom(h) + 'px',
  237. width: formatZoom(w) + 'px',
  238. // background: 'rgba(0, 0, 0, 0.5)',
  239. background: Object.values(noteInfoItems).find((v) => v === true) ? 'rgba(255, 255, 255, 0.8)' : '',
  240. }}
  241. onClick={this.sectionClick}
  242. >
  243. {noteInfoItems.duration && (
  244. <>
  245. {parseInt(item.duration * 100 + '') / 100}
  246. <br />
  247. </>
  248. )}
  249. {noteInfoItems.time && (
  250. <>
  251. {item.time.toFixed(2)}
  252. <br />
  253. </>
  254. )}
  255. {noteInfoItems.numerator && (
  256. <>
  257. {item.noteElement.sourceMeasure.activeTimeSignature.numerator}
  258. <br />
  259. <br />
  260. </>
  261. )}
  262. {noteInfoItems.denominator && (
  263. <>
  264. {item.noteElement.sourceMeasure.activeTimeSignature.denominator}
  265. <br />
  266. <br />
  267. </>
  268. )}
  269. {noteInfoItems.i && (
  270. <>
  271. {item.i}
  272. <br />
  273. <br />
  274. </>
  275. )}
  276. {noteInfoItems.speed && (
  277. <>
  278. {item.speed.toFixed(0)}
  279. <br />
  280. <br />
  281. </>
  282. )}
  283. <div class={[styles.noteBase, styles.noteRight]}>
  284. <Icon name="success" size="16" color="#2DC7AA" />
  285. </div>
  286. <div class={[styles.noteBase, styles.noteError]}>
  287. <Icon name="cross" size="16" color="red" />
  288. </div>
  289. </div>
  290. </>
  291. )
  292. })}
  293. {/* 范围的两个括号 */}
  294. {state.section.map((item, index) => {
  295. const boundingBox = getBoundingBoxByverticalNote(item)
  296. let X: number | undefined = undefined
  297. try {
  298. const bbox = item.svgElelent?.bbox || item.svgElelent?.getBoundingBox && item.svgElelent?.getBoundingBox()
  299. X = formatZoom(bbox?.x || (index === 0 ? boundingBox.start_x : boundingBox.end_x))
  300. } catch (error) {
  301. console.log(error)
  302. }
  303. if (!X) {
  304. return null
  305. }
  306. if (index === 0 && boundingBox) {
  307. // console.log('左',formatZoom(boundingBox.y) - 5, formatZoom(boundingBox.x),X,SettingState.sett.scoreSize)
  308. return (
  309. <div
  310. style={{
  311. position: 'absolute',
  312. top: formatZoom(boundingBox.y) + 'px',
  313. left: formatZoom(boundingBox.x) + 'px',
  314. height: formatZoom(boundingBox.height) + 'px',
  315. border: `5px solid ${eyeBorderColor}`,
  316. borderColor: `${eyeBorderColor} transparent ${eyeBorderColor} ${eyeBorderColor}`,
  317. borderRight: 'none',
  318. width: '5px',
  319. }}
  320. ></div>
  321. )
  322. }
  323. if (index === 1 && boundingBox) {
  324. // console.log('右',formatZoom(boundingBox.y - 5), formatZoom(boundingBox.end_x),X)
  325. return (
  326. <div
  327. style={{
  328. position: 'absolute',
  329. top: formatZoom(boundingBox.y) + 'px',
  330. left: formatZoom(boundingBox.end_x) + 'px',
  331. height: formatZoom(boundingBox.height) + 'px',
  332. border: `5px solid ${eyeBorderColor}`,
  333. borderColor: `${eyeBorderColor} ${eyeBorderColor} ${eyeBorderColor} transparent`,
  334. borderLeft: 'none',
  335. width: '5px',
  336. }}
  337. ></div>
  338. )
  339. }
  340. return null
  341. })}
  342. {/* 添加选择背景 */}
  343. {state.sectionBoundingBoxs.map((item) => {
  344. return (
  345. <div
  346. class={{
  347. [styles.flash]: item.before && state.sectionFlash && runtime.playState === 'play',
  348. }}
  349. style={{
  350. position: 'absolute',
  351. top: formatZoom(item.y) + 'px',
  352. left: formatZoom(item.x) + 'px',
  353. height: formatZoom(getBoundingBoxByverticalNote(state.section[0])?.height) + 'px',
  354. width: formatZoom(item.width) + 'px',
  355. backgroundColor: eyeBackground(item),
  356. }}
  357. ></div>
  358. )
  359. })}
  360. {/* 分数信息 */}
  361. <TransitionGroup name="list" duration={800}>
  362. {Object.values(uniqueByPosition(Object.values(state.evaluatings))).map((item: any) => {
  363. if (!item) return <span />
  364. return (
  365. <div
  366. key={item.y + item.x + item.text}
  367. class={classNames(styles[item.text], styles.measure, { [styles.dontTransition]: item.dontTransition })}
  368. style={{
  369. position: 'absolute',
  370. top: formatZoom(item.y) + this.top + 'px',
  371. left: formatZoom(item.x) + this.left + 'px',
  372. height: formatZoom(item.height) + 'px',
  373. width: formatZoom(item.width) + 'px',
  374. }}
  375. >
  376. {this.type === 'evaluating' ? (
  377. <span class={styles.after}>
  378. <span class={styles.img}></span>
  379. <span class={styles.font}>{item.score}</span>
  380. </span>
  381. ) : null}
  382. </div>
  383. )
  384. })}
  385. </TransitionGroup>
  386. {/* 休止符等待 */}
  387. {restMeasure && (
  388. <div class={['dotWrap', styles.restMeasure]} style={restMeasure.staveBox}>
  389. <div class="dot">{restNumber}</div>
  390. </div>
  391. )}
  392. </div>
  393. )
  394. },
  395. })