index.tsx 14 KB

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