index.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. import { computed, defineComponent, onMounted, reactive, Transition, nextTick, watch } from "vue";
  2. import state, { EnumMusicRenderType, handleSelection, skipNotePlay, IPlatform, resetBaseRate } from "/src/state";
  3. import styles from "./index.module.less";
  4. import { metronomeData } from "/src/helpers/metronome";
  5. import { evaluatingData } from "../evaluating";
  6. import { leveByScoreMeasureIcons } from "../evaluating/evaluatResult";
  7. import { Icon, showToast } from "vant";
  8. import MoveMusicScore, { moveData, renderForMoveData } from "../plugins/move-music-score";
  9. import { useRoute } from "vue-router";
  10. import { getQuery } from "/src/utils/queryString";
  11. import IntonationDown from "./imgs/pitchLow.png"
  12. import IntonationUp from "./imgs/pitchHigh.png"
  13. import MultipleRestMeasures from "./multipleRestMeasures"
  14. import { browser } from "../../utils";
  15. import { transform } from "lodash";
  16. export default defineComponent({
  17. name: "selection",
  18. setup() {
  19. const browsInfo = browser();
  20. const isPad = navigator?.userAgent?.includes("UAWEIVRD-W09") || browsInfo?.iPad || browsInfo.isTablet;
  21. const route = useRoute();
  22. const query: any = {
  23. ...getQuery(),
  24. ...route.query,
  25. };
  26. const selectData = reactive({
  27. notes: [] as any[],
  28. staves: [] as any[],
  29. measureHeight: 0 as number, // 小节高度
  30. // beatWidth: '100%' as string, // 节拍指针占用的宽度,带有拍号的小节,节拍指针占用的宽度需要减去拍号左侧的宽度
  31. // beatLeft: 0 as number, // 小节有拍号时,拍号左侧宽度
  32. });
  33. const beatMeasureWidths: any = {}; // 节拍指针需要占用的宽度
  34. /** 计算点击层数据 */
  35. const calcNoteData = () => {
  36. const musicContainer = document.getElementById("musicAndSelection")?.getBoundingClientRect() || {
  37. x: 0,
  38. y: 0,
  39. };
  40. const parentLeft = musicContainer.x || 0;
  41. const parentTop = musicContainer.y || 0;
  42. const notes = state.times;
  43. const notesList: string[] = [];
  44. const MeasureNumberXMLList: number[] = [];
  45. let minMeasureHeigt: number = 0;
  46. for (let i = 0; i < notes.length; i++) {
  47. const item = notes[i];
  48. // console.log("🚀 ~ item:", item)
  49. const noteItem = {
  50. ...item,
  51. index: item.i,
  52. bbox: null as any,
  53. staveBox: null as any,
  54. };
  55. if (!notesList.includes(item.noteId)) {
  56. let staveBbox: any = {}, customBgBox: any = {};
  57. if (item.stave?.attrs?.id) {
  58. const staveEle = document.querySelector(`#${item.stave.attrs.id}`);
  59. staveBbox = staveEle?.parentElement?.parentElement?.getBoundingClientRect?.() || {
  60. x: 0,
  61. width: 0,
  62. };
  63. customBgBox = staveEle?.querySelector('.vf-custom-bg')?.getBoundingClientRect() || { y: 0, height: 0 }
  64. // console.log("🚀 ~ staveBbox:", staveBbox.height)
  65. }
  66. if (item.svgElement) {
  67. const noteEle = document.querySelector(`#vf-${item.svgElement?.attrs?.id}`);
  68. if (noteEle) {
  69. const noteBbox = noteEle.getBoundingClientRect?.() || { x: 0, width: 0 };
  70. if (state.musicRenderType !== EnumMusicRenderType.staff) {
  71. noteItem.bbox = {
  72. left: noteBbox.x - parentLeft - noteBbox.width / 4 + "px",
  73. top: noteBbox.y - parentTop - noteBbox.height + "px",
  74. width: noteBbox.width * 1.5 + "px",
  75. height: noteBbox.height * 3 + "px",
  76. x: item.bbox?.x,
  77. y: item.bbox?.y,
  78. originWidth: item.bbox?.width
  79. };
  80. const noteHead = noteEle.querySelector(".vf-numbered-note-head");
  81. const noteHeadBbox = noteHead?.getBoundingClientRect?.();
  82. if (noteHeadBbox) {
  83. item.bbox = {
  84. left: noteHeadBbox.x - parentLeft - noteHeadBbox.width / 4,
  85. width: noteHeadBbox.width * 1.5,
  86. x: item.bbox?.x,
  87. y: item.bbox?.y,
  88. originWidth: item.bbox?.width
  89. }
  90. }
  91. } else {
  92. const needTransY = -(staveBbox.height - customBgBox.height) / 2 + "px";
  93. noteItem.bbox = {
  94. left: noteBbox.x - parentLeft - noteBbox.width / 4 + "px",
  95. top: customBgBox.y ? customBgBox.y - parentTop + "px" : staveBbox.y - parentTop + "px",
  96. width: noteBbox.width * 1.5 + "px",
  97. height: staveBbox.height + "px",
  98. x: item.bbox?.x,
  99. y: item.bbox?.y,
  100. originWidth: item.bbox?.width,
  101. transform: `translateY(${needTransY})`
  102. };
  103. }
  104. }
  105. if (selectData.notes.find((item:any) => item.id === noteItem.id)) {
  106. //
  107. } else {
  108. selectData.notes.push(noteItem);
  109. }
  110. // selectData.notes.push(noteItem);
  111. notesList.push(item.noteId);
  112. }
  113. }
  114. // 如果小节里面有拍号,需要减去拍号左侧的宽度
  115. let beatWidth = '100%';
  116. let beatLeft = 0;
  117. if (!MeasureNumberXMLList.includes(item.MeasureNumberXML)) {
  118. if (item.stave) {
  119. if (item.stave?.attrs?.id) {
  120. const staveEle = document.querySelector(`#${item.stave.attrs.id}`);
  121. const list = [
  122. Array.from(staveEle?.querySelectorAll(".vf-clef") || []),
  123. Array.from(staveEle?.querySelectorAll(".vf-keysignature") || []),
  124. Array.from(staveEle?.getElementsByTagName("text") || []),
  125. ].flat();
  126. try {
  127. if (list.length) {
  128. // console.log("🚀 ~ list:", list)
  129. list.forEach((_el: any) => {
  130. _el?.style?.setProperty("display", "none");
  131. });
  132. }
  133. } catch (error) {}
  134. const staveBbox = staveEle?.getBoundingClientRect?.() || { x: 0, width: 0, y: 0, height: 0 };
  135. const timesignatureDom = staveEle?.querySelector('.vf-timesignature') || staveEle?.querySelector('.vf-keysignature')
  136. // 休止符才是根据小节宽度等分
  137. if (timesignatureDom && item.measures.length == 1) {
  138. const timesignatureBbox = timesignatureDom.getBoundingClientRect()
  139. const leftWidth = timesignatureBbox.x + timesignatureBbox.width - staveBbox.x
  140. beatLeft = leftWidth
  141. beatWidth = `calc(100% - ${leftWidth+'px'})`
  142. }
  143. if (i === 0) {
  144. minMeasureHeigt = staveBbox.height
  145. }
  146. try {
  147. if (list.length) {
  148. list.forEach((_el: any) => {
  149. _el?.style?.removeProperty("display");
  150. });
  151. }
  152. } catch (error) {}
  153. // console.log("🚀 ~ staveEle:", staveBbox.height)
  154. selectData.measureHeight = staveBbox.height
  155. let compareVal = staveBbox.height - minMeasureHeigt
  156. compareVal = compareVal > 0 ? compareVal : 0
  157. selectData.measureHeight = staveBbox.height - compareVal
  158. noteItem.staveBox = {
  159. left: staveBbox.x - parentLeft + "px",
  160. // top: ((item.stave.y || 0) - 5) * state.zoom + "px",
  161. top: staveBbox.y - parentTop + compareVal + "px",
  162. width: staveBbox.width + "px",
  163. height: staveBbox.height - compareVal + "px",
  164. // background: 'rgba(0,0,0,.2)'
  165. };
  166. selectData.staves.push(noteItem);
  167. }
  168. MeasureNumberXMLList.push(item.MeasureNumberXML);
  169. beatMeasureWidths[item.MeasureNumberXML] = {
  170. beatLeft,
  171. beatWidth
  172. }
  173. } else {
  174. if (item.multipleRestMeasures) {
  175. if (state.isCombineRender) {
  176. let currentItem = null;
  177. for (let index = 0; index < state.vfmeasures.length; index++) {
  178. const element = state.vfmeasures[index];
  179. const measureNum = element.getAttribute('data-num') ? Number(element.getAttribute('data-num')) : -1;
  180. const nextMeasureNum = state.vfmeasures[index+1]?.getAttribute('data-num') ? Number(state.vfmeasures[index+1]?.getAttribute('data-num')) : -1;
  181. if (measureNum === item.MeasureNumberXML || item.MeasureNumberXML < nextMeasureNum || nextMeasureNum == -1) {
  182. currentItem = element
  183. break;
  184. }
  185. }
  186. const staveBbox = currentItem?.querySelector('.vf-stave')?.getBoundingClientRect() || { x: 0, width: 0, y: 0, height: 0 };
  187. if (currentItem) {
  188. noteItem.staveBox = {
  189. left: staveBbox.x - parentLeft + "px",
  190. // top: ((item.stave.y || 0) - 5) * state.zoom + "px",
  191. top: staveBbox.y - parentTop + "px",
  192. width: staveBbox.width + "px",
  193. height: staveBbox.height + "px",
  194. // height: preItem.staveBox.height,
  195. };
  196. selectData.staves.push(noteItem);
  197. MeasureNumberXMLList.push(item.MeasureNumberXML);
  198. beatMeasureWidths[item.MeasureNumberXML] = {
  199. beatLeft,
  200. beatWidth
  201. }
  202. }
  203. } else {
  204. const preItem = selectData.staves.find(
  205. (n: any) => n.MeasureNumberXML === item.MeasureNumberXML - 1
  206. );
  207. if (preItem?.staveBox) {
  208. noteItem.staveBox = {
  209. left: preItem.staveBox.left,
  210. top: preItem.staveBox.top,
  211. width: preItem.staveBox.width,
  212. // height: preItem.staveBox.height,
  213. };
  214. selectData.staves.push(noteItem);
  215. MeasureNumberXMLList.push(item.MeasureNumberXML);
  216. const preBeatMeasure = beatMeasureWidths[item.MeasureNumberXML-1]
  217. beatMeasureWidths[item.MeasureNumberXML] = preBeatMeasure ?
  218. {
  219. beatLeft: preBeatMeasure.beatLeft,
  220. beatWidth: preBeatMeasure.beatWidth
  221. } : {
  222. beatLeft,
  223. beatWidth
  224. }
  225. }
  226. }
  227. }
  228. }
  229. }
  230. }
  231. // 部分浏览器渲染的第一小节的位置信息会包含拍号、调号,需要处理一下,剔除掉拍号、调号的位置
  232. if (selectData.staves[0]?.staveBox?.top !== selectData.staves[1]?.staveBox?.top) {
  233. selectData.staves[0].staveBox.top = selectData.staves[1]?.staveBox?.top || selectData.staves[0]?.staveBox?.top
  234. }
  235. console.log("🚀 ~ selectData.notes:", selectData.notes, selectData.staves,beatMeasureWidths,MeasureNumberXMLList);
  236. };
  237. /** 是否可以点击音符 */
  238. const disableClickNote = computed(() => {
  239. return (state.sectionStatus && state.section.length != 2) || (state.modeType === "evaluating");
  240. });
  241. // 选段符号
  242. const sectionPosData = computed(() => {
  243. if(state.sectionStatus) {
  244. return state.section.map(((item,index) => {
  245. if(index === 0){
  246. const currItem = selectData.staves.find(stave => {
  247. return stave.MeasureNumberXML === item.MeasureNumberXML
  248. })
  249. // 获取stave里面vf-custom-bg的位置坐标,才是准确的坐标
  250. // const currBgX = document.getElementById(currItem.stave.attrs.id)?.querySelector('.vf-custom-bg')?.getBoundingClientRect()?.x || 0;
  251. const currBgX = currItem.stave?.attrs && currItem.stave.attrs.id ? document.getElementById(currItem.stave.attrs.id)?.querySelector('.vf-custom-bg')?.getBBox()?.x * state.zoom || 0 : 0;
  252. return currItem && {
  253. left: currBgX ? currBgX + 'px' : currItem.staveBox.left,
  254. top: currItem.staveBox.top,
  255. height: selectData.measureHeight + 'px' // 小节的高度
  256. }
  257. } else {
  258. // 实际的结束位置
  259. const actualEndIndex = state.userChooseEndIndex > item.MeasureNumberXML ? state.userChooseEndIndex : item.MeasureNumberXML
  260. const currItem = selectData.staves.find(stave => {
  261. return stave.MeasureNumberXML === actualEndIndex
  262. })
  263. return currItem && {
  264. left: parseFloat(currItem.staveBox.left)+parseFloat(currItem.staveBox.width)-2 +"px",
  265. top: currItem.staveBox.top,
  266. height: selectData.measureHeight + 'px'
  267. }
  268. }
  269. }))
  270. }
  271. return []
  272. })
  273. console.time('dom挂载')
  274. onMounted(() => {
  275. console.timeEnd('dom挂载')
  276. selectData.notes = [];
  277. selectData.staves = [];
  278. console.time('添加dom时间')
  279. calcNoteData();
  280. console.timeEnd('添加dom时间')
  281. const img: HTMLElement = document.querySelector('#cursorImg-0')!
  282. if (metronomeData.cursorMode === 2){
  283. img.classList.add('lineHide')
  284. } else {
  285. img.classList.remove('lineHide')
  286. }
  287. // 初始化谱面可移动的元素位置
  288. try {
  289. moveData.partIndex = state.partIndex + ""
  290. // 速度标记元素和谱面并非同时渲染,初始化可移动元素的时候,需要加个延迟
  291. setTimeout(() => {
  292. renderForMoveData()
  293. }, 0);
  294. } catch (error) {}
  295. });
  296. return () => (
  297. <>
  298. <div
  299. id="selectionBox"
  300. class={[
  301. styles.selectionContainer,
  302. isPad && styles.isPad,
  303. state.zoom == 1.25 ? styles.middleZoom : state.zoom == 1.5 ? styles.bigZoom : state.zoom == 1.75 ? styles.largeZoom : state.zoom == 2 ? styles.largeZoom2 : state.zoom == 2.25 ? styles.largeZoom3 :
  304. state.zoom == 0.65 ? styles.smallZoom : state.zoom == 0.5 ? styles.litteZoom : ''
  305. ]}
  306. onClick={(e: Event) => e.stopPropagation()}
  307. >
  308. {selectData.staves.map((item: any, index) => {
  309. // 评测得分
  310. const scoreItem = item.id && evaluatingData.evaluatings[item.measureListIndex];
  311. // for(let idx in evaluatingData.evaluatings) {
  312. // const { show, measureIndex } = evaluatingData.evaluatings[idx]
  313. // if (show && measureIndex !== item.measureListIndex) {
  314. // evaluatingData.evaluatings[idx].show = false
  315. // }
  316. // }
  317. // 高级模式下,显示节拍线
  318. // 不是报告模式
  319. // 不是多小节休止符
  320. // 节拍线开关
  321. // 当前小节
  322. // 当前小节
  323. /* 节拍指针,现在没有节拍器指针了,但是以后要加上的话,这里需要性能优化。现在这样每次节拍指针更新都会刷新这里的虚拟dom */
  324. const lineShow =
  325. !state.isReport &&
  326. metronomeData.cursorMode === 2 &&
  327. item.MeasureNumberXML === metronomeData.activeMetro?.measureNumberXML &&
  328. state.times[state.activeNoteIndex].MeasureNumberXML === item.MeasureNumberXML;
  329. // console.log('显示节拍指针',lineShow,state.times[state.activeNoteIndex].MeasureNumberXML,item.MeasureNumberXML,metronomeData.activeMetro?.measureNumberXML)
  330. return (
  331. <>
  332. {item.staveBox && (
  333. <div
  334. key={item.id}
  335. class={[
  336. styles.position,
  337. // scoreItem ? `scoreItemLeve${scoreItem.leve}` : "", // 去掉评测小节得分的背景色
  338. (state.platform === IPlatform.PC && state.zoom > 0.8) ? styles.linePC : '',
  339. `measureIndex_${item.MeasureNumberXML}`
  340. ]}
  341. style={item.staveBox}
  342. onClick={() => {
  343. // 当为连续休止小节的结束选段的时候 应该传休止小节 结束的位置
  344. let staveItem = item
  345. if(state.section.length === 1 && item.totalMultipleRestMeasures > 0){
  346. staveItem = selectData.staves[index + item.totalMultipleRestMeasures - 1]
  347. }
  348. handleSelection(staveItem)
  349. }}
  350. >
  351. {lineShow && (
  352. <div style={{height: selectData.measureHeight + 'px', position: 'relative', width: metronomeData.activeMetro.isPercent ? beatMeasureWidths[item.MeasureNumberXML].beatWidth : '100%', left: metronomeData.activeMetro.isPercent ? beatMeasureWidths[item.MeasureNumberXML].beatLeft + 'px' : 0}}>
  353. <div
  354. class={[
  355. styles.line,
  356. state.setting.eyeProtection ? styles.eyeLine : '',
  357. state.musicRenderType == EnumMusicRenderType.staff ? styles.lineStaff : styles.lineJianPu,
  358. ]}
  359. style={{ left: metronomeData.activeMetro.left }}></div>
  360. </div>
  361. )}
  362. {!state.isReport &&
  363. !!item.multipleRestMeasures &&
  364. <MultipleRestMeasures item = {item}></MultipleRestMeasures>
  365. }
  366. <Transition
  367. name="centerTop"
  368. onAfterEnter={() => {
  369. scoreItem.show = false;
  370. }}
  371. >
  372. {scoreItem?.show && (
  373. <div
  374. class={styles.scoreItem}
  375. style={{ color: leveByScoreMeasureIcons[scoreItem.leve]?.color || "" }}
  376. >
  377. <img src={leveByScoreMeasureIcons[scoreItem.leve]?.icon} />
  378. <span>{scoreItem.score}</span>
  379. </div>
  380. )}
  381. </Transition>
  382. </div>
  383. )}
  384. </>
  385. );
  386. })}
  387. {selectData.notes.map((item: any) => {
  388. return (
  389. <div
  390. class={[styles.position, disableClickNote.value && styles.disable, styles.note, `noteIndex_${item.index}`]}
  391. style={item.bbox}
  392. onClick={() => skipNotePlay(item.index, false, 'manual')}
  393. >
  394. {/* <div class={styles.noteFollow} data-vf={"vf" + item.id}>
  395. <Icon name="success" />
  396. <Icon name="cross" />
  397. </div> */}
  398. <div class={styles.noteFollow} data-vf={"vf" + item.id}>
  399. {/* <Icon name="success" />
  400. <Icon name="cross" /> */}
  401. <div class={[styles.followTipUp, 'tip-up']}>
  402. <img src={IntonationUp} />
  403. {/* <span>音准<i>高了</i></span> */}
  404. </div>
  405. <div class={[styles.followTipDown, 'tip-down']}>
  406. <img src={IntonationDown} />
  407. {/* <span>音准<i>低了</i></span> */}
  408. </div>
  409. </div>
  410. <div class={[styles.noteDot, 'node-dot']}></div>
  411. </div>
  412. );
  413. })}
  414. {/* 选段 */}
  415. {
  416. sectionPosData.value.map((item,index) =>{
  417. return (
  418. item && <div class={styles.selectBox} style={item}>
  419. <div class={[styles.selectHandle,index>0&&styles.selectHandleRight,(state.playState==="play" || state.isHomeWork)&&styles.playIng]} onClick={()=>{
  420. // 如果选择了2个 删除左边的时候
  421. if (state.section.length===1&&index === 0) {
  422. // #bug:11552
  423. resetBaseRate(state.activeNoteIndex);
  424. }
  425. if(state.section.length===2&&index === 0){
  426. state.section = []
  427. // 重置速度和播放倍率
  428. resetBaseRate(state.activeNoteIndex);
  429. showToast({
  430. message: "请选择开始小节",
  431. duration: 0,
  432. position: "top",
  433. className: "selectionToast",
  434. });
  435. }else{
  436. state.section.splice(index,1)
  437. state.section = [...state.section] // 触发 watch
  438. showToast({
  439. message: state.section.length?"请选择结束小节":"请选择开始小节",
  440. duration: 0,
  441. position: "top",
  442. className: "selectionToast",
  443. });
  444. }
  445. // IOS18.1.1浏览器渲染更新有问题,需要手动更新一下
  446. const selectionDom = document.getElementById('selectionBox')
  447. if (selectionDom) {
  448. selectionDom.style.display = 'none';
  449. requestAnimationFrame(() => {
  450. selectionDom.style.display = 'block';
  451. })
  452. }
  453. }}></div>
  454. </div>
  455. )
  456. })
  457. }
  458. {/* 移动模块 */}
  459. {query.isMove == "1" && <MoveMusicScore />}
  460. </div>
  461. </>
  462. );
  463. },
  464. });