index.tsx 15 KB

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