index.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. import { Skeleton } from "vant";
  2. import { defineComponent, onBeforeMount, onBeforeUnmount, onMounted, reactive, Transition, watch, ref } from "vue";
  3. import { formateTimes } from "../../helpers/formateMusic";
  4. import state, { isRhythmicExercises, getMusicDetail, EnumMusicRenderType } from "../../state";
  5. import { setGlobalData } from "../../utils";
  6. import MusicScore from "../../view/music-score";
  7. import styles from "./index.module.less";
  8. import { api_cloudLoading, api_setStatusBarVisibility, isSpecialShapedScreen } from "/src/helpers/communication";
  9. import { getQuery } from "/src/utils/queryString";
  10. import { mappingVoicePart, subjectFingering } from "/src/view/fingering/fingering-config";
  11. import { api_musicPracticeRecordDetail, sysMusicScoreAccompanimentQueryPage } from "../api";
  12. import { getMusicSheetDetail } from "/src/utils/baseApi";
  13. import ShareTop from "./component/share-top";
  14. import { addMeasureScore } from "/src/view/evaluating";
  15. import TopArrow from "./component/note/topArrow";
  16. import BottomArrow from "./component/note/bottomArrow";
  17. import LeftArrow from "./component/note/leftArrow";
  18. import RightArrow from "./component/note/rightArrow";
  19. const colorsClass: any = {
  20. RIGHT: styles.right, // 正确
  21. WRONG: styles.wrong, // 错误
  22. NOT_PLAYED: styles.notPlay, // 未演奏
  23. EARLY: styles.cadence_fast, // 节奏快
  24. LATE: styles.cadence_slow, // 节奏慢
  25. HIGH: styles.intonation_high, // 音准高
  26. LOW: styles.intonation_low, // 音准低
  27. SHORT: styles.integrity_wrong, // 完整性(时值)不足
  28. };
  29. // const colorsClass: any = {
  30. // /** 音准 */
  31. // pitch: {
  32. // /** 高了 */
  33. // HIGH: styles.intonation_high,
  34. // /** 正常 */
  35. // RIGHT: styles.intonation_right,
  36. // /** 低了 */
  37. // LOW: styles.intonation_low,
  38. // /** 未演奏 */
  39. // NOT_PLAYED: styles.notPlay,
  40. // /** 错误 */
  41. // WRONG: styles.intonation_wrong,
  42. // /** 时值不足 */
  43. // DURATION_INSUFFICIENT: styles.integrity_wrong
  44. // },
  45. // /** 节奏 */
  46. // rhythmic: {
  47. // /** 过早 */
  48. // EARLY: styles.cadence_fast,
  49. // /** 正常 */
  50. // RIGHT: styles.cadence_right,
  51. // /** 过迟 */
  52. // LATE: styles.cadence_slow,
  53. // /** 未演奏 */
  54. // NOT_PLAYED: styles.notPlay,
  55. // /** 错误 */
  56. // WRONG: styles.cadence_wrong
  57. // }
  58. // }
  59. export default defineComponent({
  60. name: "music-list",
  61. setup() {
  62. const query: any = getQuery();
  63. const useedid = ref<string[]>([]);
  64. const allNote = ref<any[]>([]);
  65. const scoreData = reactive({
  66. videoFilePath: "", // 回放视频路径
  67. cadence: 0,
  68. integrity: 0,
  69. intonation: 0,
  70. score: 0,
  71. speed: 0,
  72. heardLevel: "",
  73. itemType: "intonation",
  74. musicType: "staff",
  75. });
  76. const detailData = reactive({
  77. isLoading: true,
  78. paddingLeft: "",
  79. headerHide: false,
  80. musicalNotesPlayStats: [] as any[],
  81. userMeasureScore: {} as any,
  82. isNewReport: true,
  83. });
  84. const getAPPData = async () => {
  85. const screenData = await isSpecialShapedScreen();
  86. if (screenData?.content) {
  87. const { isSpecialShapedScreen, notchHeight } = screenData.content;
  88. if (isSpecialShapedScreen) {
  89. detailData.paddingLeft = 25 + "px";
  90. }
  91. }
  92. // 普通webview 没有获取异性屏的方法
  93. detailData.paddingLeft = 20 + "px";
  94. };
  95. onBeforeMount(() => {
  96. getAPPData();
  97. api_setStatusBarVisibility();
  98. });
  99. // console.log(route.params, query)
  100. /** 获取曲谱数据 */
  101. const getMusicInfo = (res: any) => {
  102. const index = state.partIndex;
  103. const musicInfo = {
  104. ...res.data,
  105. ...res.data.background[index],
  106. };
  107. // console.log("🚀 ~ musicInfo:", musicInfo);
  108. setState(musicInfo, index);
  109. setCustom();
  110. detailData.isLoading = false;
  111. };
  112. const setState = (data: any, index: number) => {
  113. // console.log("🚀 ~ data:", data)
  114. state.scrollContainer = "scrollContainer";
  115. state.detailId = data.id;
  116. state.xmlUrl = data.xmlFileUrl;
  117. state.partIndex = index;
  118. state.subjectId = data.musicSubject;
  119. state.categoriesId = data.categoriesId;
  120. state.categoriesName = data.musicTagNames;
  121. state.enableEvaluation = data.canEvaluate ? true : false;
  122. state.examSongId = data.id + "";
  123. state.examSongName = data.musicSheetName;
  124. // 解析扩展字段
  125. if (data.extConfigJson) {
  126. try {
  127. state.extConfigJson = JSON.parse(data.extConfigJson as string);
  128. } catch (error) {
  129. console.error("解析扩展字段错误:", error);
  130. }
  131. }
  132. state.isOpenMetronome = data.mp3Type === "MP3_METRONOME" ? true : false;
  133. state.needTick = data.isOpenMetronome;
  134. state.isShowFingering = data.showFingering ? true : false;
  135. state.music = data.audioFileUrl;
  136. state.accompany = data.metronomeUrl || data.metronomeUrl;
  137. state.midiUrl = data.midiUrl;
  138. state.parentCategoriesId = data.musicTag;
  139. state.playMode = data.audioType === "MP3" ? "MP3" : "MIDI";
  140. state.originSpeed = state.speed = data.speed;
  141. state.track = data.track;
  142. state.enableNotation = data.notation ? true : false;
  143. // 映射声部ID
  144. state.subjectId = mappingVoicePart(state.subjectId as any, "ORCHESTRA");
  145. // console.log("🚀 ~ state.subjectId:", state.subjectId);
  146. // 是否打击乐
  147. state.isPercussion = state.subjectId == 23 || state.subjectId == 113 || state.subjectId == 121 || isRhythmicExercises();
  148. // 设置指法
  149. state.fingeringInfo = subjectFingering(state.subjectId);
  150. // console.log("🚀 ~ state.fingeringInfo:", state.fingeringInfo, state.subjectId, state.track)
  151. // state.isOpenPrepare = true
  152. };
  153. const setCustom = () => {
  154. if (state.extConfigJson.multitrack) {
  155. setGlobalData("multitrack", state.extConfigJson.multitrack);
  156. }
  157. };
  158. onMounted(async () => {
  159. state.isEvaluatReport = true;
  160. const res = await api_musicPracticeRecordDetail(query.id);
  161. state.partIndex = Number(res?.data?.partIndex);
  162. let resultData = {} as any;
  163. try {
  164. resultData = eval('(' + res?.data?.scoreData + ')');
  165. } catch (error) {
  166. console.error("解析评测结果:", error);
  167. }
  168. // console.log("🚀 ~ resultData:", resultData);
  169. // @ts-ignore
  170. // resultData.musicalNotesPlayStats?.notesData.forEach((item) => item.rhythmicAssessment.result = 'EARLY')
  171. console.log('结果11',resultData)
  172. detailData.musicalNotesPlayStats = resultData.musicalNotesPlayStats?.notesData || [];
  173. detailData.userMeasureScore = resultData.userMeasureScore || {};
  174. detailData.isNewReport = res.data.practiceTime ? true : false;
  175. scoreData.heardLevel = res.data?.heardLevel;
  176. scoreData.cadence = res.data?.cadence;
  177. scoreData.integrity = res.data?.integrity;
  178. scoreData.intonation = res.data?.intonation;
  179. scoreData.score = res.data?.score;
  180. scoreData.speed = res.data?.speed;
  181. scoreData.videoFilePath = res.data?.videoFilePath || res.data?.recordFilePath;
  182. await getMusicDetail(resultData.musicalNotesPlayStats?.examSongId);
  183. // 从练习记录进入评测报告,默认显示五线谱
  184. // if (!query.musicRenderType) {
  185. // state.musicRenderType = EnumMusicRenderType.staff
  186. // }
  187. // 评测报告展示什么类型的谱面
  188. state.isSingleLine = false;
  189. scoreData.musicType = query.musicRenderType ? query.musicRenderType : resultData.musicType ? resultData.musicType : state.musicRenderType;
  190. // @ts-ignore
  191. state.musicRenderType = scoreData.musicType;
  192. detailData.isLoading = false;
  193. // Promise.all([
  194. // getMusicSheetDetail(resultData.musicalNotesPlayStats?.examSongId),
  195. // ]).then((values) => {
  196. // getMusicInfo(values[0]);
  197. // });
  198. });
  199. const getOffsetPosition = (type: keyof typeof colorsClass): string => {
  200. // 五线谱
  201. if (scoreData.musicType === "staff") {
  202. switch (type) {
  203. case "EARLY":
  204. return "translateX(-3px)";
  205. case "LATE":
  206. return "translateX(3px)";
  207. case "HIGH":
  208. return "translateY(-2px)";
  209. case "LOW":
  210. return "translateY(2px)";
  211. default:
  212. return "";
  213. }
  214. } else {
  215. switch (type) {
  216. case "EARLY":
  217. return "translateX(-3px)";
  218. case "LATE":
  219. return "translateX(3px)";
  220. case "HIGH":
  221. return "translateY(-2px)";
  222. case "LOW":
  223. return "translateY(-10px)";
  224. default:
  225. return "";
  226. }
  227. }
  228. };
  229. const filterNotes = () => {
  230. let include = detailData.isNewReport ? ["RIGHT", "WRONG", "NOT_PLAYED"] : ["RIGHT", "WRONG", "NOT_PLAY"];
  231. if (scoreData.itemType === "intonation") {
  232. // 音准
  233. include.push(...["HIGH", "LOW"]);
  234. } else if (scoreData.itemType === "cadence") {
  235. // 节奏
  236. include.push(...["EARLY", "LATE"]);
  237. } else if (scoreData.itemType === "integrity") {
  238. // 完整性
  239. include = detailData.isNewReport ? ["SHORT", "NORMAL", "NOT_PLAYED"] : ["INTEGRITY_WRONG", "RIGHT", "NOT_PLAY"];
  240. }
  241. if (scoreData.itemType === "cadence") {
  242. return detailData.musicalNotesPlayStats.filter((item: any) => include.includes(item.rhythmicAssessment ? item.rhythmicAssessment.result : item.musicalErrorType));
  243. } else if (scoreData.itemType === "integrity") {
  244. return detailData.musicalNotesPlayStats.filter((item: any) => include.includes(item.integrityAssessment ? item.integrityAssessment?.result : item.musicalErrorType));
  245. } else {
  246. return detailData.musicalNotesPlayStats.filter((item: any) => {
  247. let result = item.pitchAssessment ? item.pitchAssessment.result : item.musicalErrorType;
  248. // if (scoreData.itemType === "integrity") {
  249. // result = result === "HIGH" || result === "LOW" || result === "WRONG" ? "RIGHT" : result;
  250. // }
  251. return include.includes(result);
  252. });
  253. }
  254. };
  255. const setViewColor = () => {
  256. clearViewColor();
  257. const notes = filterNotes();
  258. // console.log(1111,notes)
  259. for (const note of notes) {
  260. const idx = note.musicalNotesIndex !== undefined ? note.musicalNotesIndex : note.index;
  261. const active = allNote.value.find((item: any) => item.i === idx);
  262. setTimeout(() => {
  263. if (useedid.value.includes(active.id)) {
  264. return;
  265. }
  266. useedid.value.push(active.id);
  267. const svgEl = document.getElementById("vf-" + active.id);
  268. const stemEl = document.getElementById("vf-" + active.id + "-stem");
  269. let errType = '';
  270. if (detailData.isNewReport) {
  271. errType = scoreData.itemType === "cadence" ? note.rhythmicAssessment.result : scoreData.itemType === "integrity" ? note.integrityAssessment.result : note.pitchAssessment.result;
  272. } else {
  273. errType = note.musicalErrorType
  274. }
  275. /**
  276. * 新版小酷AI不需要在当前的音符复制出来一个音符,所以注释掉isNeedCopyElement和copySvg
  277. */
  278. // const isNeedCopyElement = scoreData.itemType === "integrity" ? false : ["HIGH", "LOW", "EARLY", "LATE"].includes(errType);
  279. const isNeedCopyElement = false;
  280. // if (scoreData.itemType === "integrity") {
  281. // errType = errType = note.pitchAssessment.result === "HIGH" || note.pitchAssessment.result === "LOW" || note.pitchAssessment.result === "WRONG" ? "RIGHT" : errType;
  282. // }
  283. if (detailData.isNewReport) {
  284. if (scoreData.itemType === "integrity") {
  285. errType = errType = note.integrityAssessment.result === "NORMAL" ? "RIGHT" : note.integrityAssessment.result === "SHORT" ? "SHORT" : errType;
  286. }
  287. } else {
  288. errType = note.musicalErrorType
  289. }
  290. if (!detailData.isNewReport) {
  291. errType = errType == "NOT_PLAY" ? "NOT_PLAYED" : errType == "INTEGRITY_WRONG" ? "SHORT" : errType;
  292. }
  293. stemEl?.classList.add(colorsClass[errType]);
  294. svgEl?.classList.add(colorsClass[errType]);
  295. console.log(123456,'添加颜色',errType)
  296. // 评测过的音符,需要给小节添加背景色
  297. // if (errType !== "NOT_PLAYED") {
  298. // const staveNote = svgEl?.parentNode?.parentNode?.querySelector(".vf-stave");
  299. // if (staveNote) {
  300. // staveNote.querySelector(".vf-custom-bg")?.setAttribute("fill", "#132D4C");
  301. // staveNote.querySelector(".vf-custom-bot")?.setAttribute("fill", "#040D1E");
  302. // }
  303. // }
  304. if (svgEl && isNeedCopyElement) {
  305. stemEl?.classList.remove(colorsClass[errType]);
  306. svgEl?.classList.remove(colorsClass[errType]);
  307. let copySvg: any = null;
  308. // 五线谱
  309. if (scoreData.musicType === "staff") {
  310. stemEl?.classList.add(colorsClass.RIGHT);
  311. svgEl?.classList.add(colorsClass.RIGHT);
  312. // copySvg = svgEl.querySelector(".vf-notehead")!.cloneNode(true) as SVGSVGElement;
  313. } else {
  314. //copySvg = svgEl.querySelector('.vf-numbered-note-head')!.cloneNode(true) as SVGSVGElement
  315. if (isNeedCopyElement) {
  316. svgEl?.classList.add(styles.inaccuracy);
  317. const targetId = errType === "HIGH" ? "topSvg" : errType === "LOW" ? "bottomSvg" : errType === "EARLY" ? "leftSvg" : errType === "LATE" ? "rightSvg" : "";
  318. // copySvg = document.getElementById(targetId)!.cloneNode(true) as SVGSVGElement;
  319. const { width, height } = svgEl.getBoundingClientRect() || {};
  320. // @ts-ignore
  321. let { x, y } = svgEl?.getBBox() || {};
  322. x = errType === "HIGH" ? x + (width - 15) / 2 + 2 : errType === "LOW" ? x + (width - 15) / 2 + 2 : errType === "EARLY" ? x - Math.abs((width - 15) / 2) - 12 : errType === "LATE" ? x + width + 6 : x;
  323. y = errType === "HIGH" ? y - Math.abs((height - 10) / 2) - 10 : errType === "LOW" ? y + height + 8 : errType === "EARLY" ? y + (height - 10) / 2 : errType === "LATE" ? y + (height - 10) / 2 : y;
  324. copySvg.setAttribute("x", x);
  325. copySvg.setAttribute("y", y);
  326. }
  327. // console.log(x,y,copySvg.getBoundingClientRect())
  328. // const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
  329. // rect.setAttribute("x", 0 +'px');
  330. // rect.setAttribute("y", 0+'px');
  331. // rect.setAttribute("width", `50`);
  332. // rect.setAttribute("height", `50`);
  333. // rect.setAttribute("fill", "#FF4444");
  334. // svgEl.prepend(rect);
  335. }
  336. if (scoreData.musicType === "staff") {
  337. // copySvg.style.transform = getOffsetPosition(errType);
  338. if (stemEl) {
  339. //
  340. }
  341. }
  342. // copySvg.id = "vf-" + active.id + "-copy";
  343. // copySvg?.classList.add(colorsClass[errType]);
  344. // @ts-ignore
  345. // state.osmd?.container.querySelector("svg")!.insertAdjacentElement("afterbegin", copySvg);
  346. }
  347. }, 300);
  348. }
  349. };
  350. const removeClass = (el?: HTMLElement | null) => {
  351. if (!el) return;
  352. const classList = el.classList.values();
  353. for (const val of classList) {
  354. if (val?.indexOf("vf-") !== 0) {
  355. el.classList.remove(val);
  356. }
  357. }
  358. };
  359. const clearViewColor = () => {
  360. for (const id of useedid.value) {
  361. removeClass(document.getElementById("vf-" + id));
  362. removeClass(document.getElementById("vf-" + id + "-stem"));
  363. const qid = "vf-" + id + "-copy";
  364. const copyEl = document.getElementById(qid);
  365. if (copyEl) {
  366. copyEl.remove();
  367. }
  368. }
  369. useedid.value = [];
  370. };
  371. const setPathColor = () => {
  372. console.log(11111, detailData.musicalNotesPlayStats, scoreData.itemType);
  373. for (const note of detailData.musicalNotesPlayStats) {
  374. const active = allNote.value[note.index];
  375. const svgEl = active?.id ? document.getElementById("vf-" + active?.id) : null;
  376. switch (scoreData.itemType) {
  377. case "intonation":
  378. svgEl?.classList.add(colorsClass.pitch[note.pitchAssessment.result]);
  379. break;
  380. case "cadence":
  381. svgEl?.classList.add(colorsClass.rhythmic[note.rhythmicAssessment.result]);
  382. break;
  383. case "integrity":
  384. svgEl?.classList.add(colorsClass.pitch[note.integrityAssessment.result]);
  385. break;
  386. default:
  387. break;
  388. }
  389. }
  390. };
  391. const setMearureColor = () => {
  392. for (let key in detailData.userMeasureScore) {
  393. addMeasureScore(detailData.userMeasureScore[key], false);
  394. }
  395. };
  396. /** 渲染完成 */
  397. const handleRendered = (osmd: any) => {
  398. state.musicRendered = true;
  399. state.osmd = osmd;
  400. allNote.value = formateTimes(osmd);
  401. console.log("🚀 ~ state.times:", allNote.value);
  402. // @ts-ignore
  403. const startMeasureNum = detailData.musicalNotesPlayStats?.[0]?.measureRenderIndex, endMeasureNum = detailData.musicalNotesPlayStats?.last()?.measureRenderIndex;
  404. allNote.value = allNote.value.filter((item: any) => (item.MeasureNumberXML >= startMeasureNum+1 && item.MeasureNumberXML <= endMeasureNum+1))
  405. // @ts-ignore
  406. const beams = Array.from(new Set(document.getElementsByClassName("vf-beam")));
  407. beams.forEach((item: any) => {
  408. item.classList.add(styles.beam);
  409. });
  410. //setPathColor();
  411. setViewColor();
  412. // setMearureColor();
  413. api_cloudLoading();
  414. };
  415. watch(
  416. () => scoreData.itemType,
  417. () => {
  418. setViewColor();
  419. }
  420. );
  421. return () => (
  422. <div class={[styles.detail, state.setting.eyeProtection && "eyeProtection", styles.shareBox]} style={{ paddingLeft: detailData.paddingLeft }}>
  423. <Transition name="van-fade">
  424. {!state.musicRendered && (
  425. <div class={styles.skeleton}>
  426. <Skeleton class={styles.skeleton} row={8} />
  427. </div>
  428. )}
  429. </Transition>
  430. <div class={["headHeight", styles.headHeight, detailData.headerHide && styles.headHide]} onClick={(e: Event) => e.stopPropagation()}>
  431. <Transition name="van-slide-down">{state.musicRendered && <ShareTop scoreData={scoreData} />}</Transition>
  432. </div>
  433. <div id="scrollContainer" class={[styles.container, !state.setting.displayCursor && "hideCursor"]}>
  434. {/* 曲谱渲染 */}
  435. {!detailData.isLoading && <MusicScore musicColor={"#1B1B1B"} onRendered={handleRendered} />}
  436. {
  437. <div class={styles.arrowSvg}>
  438. <TopArrow />
  439. <BottomArrow />
  440. <LeftArrow />
  441. <RightArrow />
  442. </div>
  443. }
  444. </div>
  445. </div>
  446. );
  447. },
  448. });