index.tsx 15 KB

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