Browse Source

Merge branch 'new-feature-tianyong' into kt-dev

TIANYONG 1 year ago
parent
commit
f0b5f32208
76 changed files with 3146 additions and 436 deletions
  1. 1 2
      instrument.html
  2. 1 0
      public/flexible.js
  3. BIN
      src/assets/tick.wav
  4. BIN
      src/assets/tock.wav
  5. 1 0
      src/constant/instruments.ts
  6. 86 0
      src/helpers/communication.ts
  7. 793 0
      src/helpers/customMusicScore.ts
  8. 40 19
      src/helpers/formateMusic.ts
  9. 80 9
      src/helpers/metronome.ts
  10. 116 0
      src/helpers/midiPlay.tsx
  11. 0 0
      src/page-instrument/component/mode-type-mode/icon/index.json
  12. 4 2
      src/page-instrument/component/mode-type-mode/index.tsx
  13. 1 1
      src/page-instrument/component/the-music-list/list.tsx
  14. 5 1
      src/page-instrument/custom-plugins/guide-page/api.ts
  15. 8 1
      src/page-instrument/custom-plugins/helper-model/screen-model/index.tsx
  16. 7 1
      src/page-instrument/custom-plugins/the-vip/index.tsx
  17. BIN
      src/page-instrument/evaluat-model/delay-check/image/icon_2_3.png
  18. 9 0
      src/page-instrument/evaluat-model/delay-check/index.module.less
  19. 8 2
      src/page-instrument/evaluat-model/delay-check/index.tsx
  20. 1 1
      src/page-instrument/evaluat-model/earphone/index.tsx
  21. 15 3
      src/page-instrument/evaluat-model/evaluat-result/index.tsx
  22. 27 12
      src/page-instrument/evaluat-model/index.tsx
  23. 15 7
      src/page-instrument/header-top/index.module.less
  24. 22 16
      src/page-instrument/header-top/index.tsx
  25. 2 2
      src/page-instrument/header-top/music-type/index.tsx
  26. 3 1
      src/page-instrument/header-top/settting/index.module.less
  27. 27 4
      src/page-instrument/header-top/settting/index.tsx
  28. 1 1
      src/page-instrument/header-top/speed/index.tsx
  29. 2 2
      src/page-instrument/header-top/title/index.module.less
  30. 3 0
      src/page-instrument/theme.css
  31. 18 1
      src/page-instrument/view-detail/index.module.less
  32. 50 11
      src/page-instrument/view-detail/index.tsx
  33. 17 0
      src/page-instrument/view-evaluat-report/component/note/bottomArrow.tsx
  34. 17 0
      src/page-instrument/view-evaluat-report/component/note/leftArrow.tsx
  35. 17 0
      src/page-instrument/view-evaluat-report/component/note/rightArrow.tsx
  36. 17 0
      src/page-instrument/view-evaluat-report/component/note/topArrow.tsx
  37. 16 0
      src/page-instrument/view-evaluat-report/component/share-top/image/first-bottom.svg
  38. 13 0
      src/page-instrument/view-evaluat-report/component/share-top/image/first-correct.svg
  39. 13 0
      src/page-instrument/view-evaluat-report/component/share-top/image/first-error.svg
  40. 13 0
      src/page-instrument/view-evaluat-report/component/share-top/image/first-lack.svg
  41. 16 0
      src/page-instrument/view-evaluat-report/component/share-top/image/first-left.svg
  42. 13 0
      src/page-instrument/view-evaluat-report/component/share-top/image/first-not.svg
  43. 16 0
      src/page-instrument/view-evaluat-report/component/share-top/image/first-right.svg
  44. 14 0
      src/page-instrument/view-evaluat-report/component/share-top/image/first-top.svg
  45. 4 2
      src/page-instrument/view-evaluat-report/component/share-top/image/icon-back.svg
  46. 4 4
      src/page-instrument/view-evaluat-report/component/share-top/image/icon-shiyi.svg
  47. 15 0
      src/page-instrument/view-evaluat-report/component/share-top/image/shiyi-close.svg
  48. BIN
      src/page-instrument/view-evaluat-report/component/share-top/image/shiyi-top.png
  49. 131 11
      src/page-instrument/view-evaluat-report/component/share-top/index.module.less
  50. 249 75
      src/page-instrument/view-evaluat-report/component/share-top/index.tsx
  51. 41 16
      src/page-instrument/view-evaluat-report/index.module.less
  52. 256 21
      src/page-instrument/view-evaluat-report/index.tsx
  53. 2 1
      src/page-instrument/view-figner/guide/detail-guide.tsx
  54. 24 0
      src/page-instrument/view-figner/guide/index.module.less
  55. 325 58
      src/state.ts
  56. 20 4
      src/utils/index.ts
  57. 6 9
      src/view/abnormal-pop/index.tsx
  58. 32 8
      src/view/audio-list/index.tsx
  59. 4 0
      src/view/audio-list/midiPlayer.tsx
  60. 6 0
      src/view/evaluating/index.module.less
  61. 44 8
      src/view/evaluating/index.tsx
  62. 48 24
      src/view/fingering/fingering-config.ts
  63. 57 15
      src/view/follow-practice/index.tsx
  64. 2 1
      src/view/music-score/index.module.less
  65. 31 5
      src/view/music-score/index.tsx
  66. BIN
      src/view/plugins/move-music-score/image/right_hide_icon.png
  67. 16 0
      src/view/plugins/move-music-score/index.module.less
  68. 187 53
      src/view/plugins/move-music-score/index.tsx
  69. 8 0
      src/view/plugins/toggleMusicSheet/choosePartName/index.module.less
  70. 12 3
      src/view/plugins/toggleMusicSheet/choosePartName/index.tsx
  71. 5 2
      src/view/plugins/toggleMusicSheet/index.tsx
  72. 18 5
      src/view/selection/index.module.less
  73. 12 4
      src/view/selection/index.tsx
  74. 55 5
      src/view/tick/index.tsx
  75. 1 1
      src/view/transfer-to-img/index.tsx
  76. 3 2
      vite.config.ts

+ 1 - 2
instrument.html

@@ -28,8 +28,7 @@
         instance.postMessage(JSON.stringify(data))
       }
     }
-    console.info(location.href)
-    if (!location.href.includes('iscurseplay=play')) {
+    if (!location.href.includes('iscurseplay=play') && !location.href.includes('isPreView=true')) {
       _postMessage({
         api: 'cloudLoading',
         content: {

+ 1 - 0
public/flexible.js

@@ -7,6 +7,7 @@
     b / i < 375 && (b = 375 * i);
     var c = b / 10;
     f.style.fontSize = c + "px", k.rem = a.rem = c
+    window.fontSize = c
   }
   var d, e = a.document,
     f = e.documentElement,

BIN
src/assets/tick.wav


BIN
src/assets/tock.wav


+ 1 - 0
src/constant/instruments.ts

@@ -155,6 +155,7 @@ const instruments: any = {
 	"Drum Set": "架子鼓",
 	"Hulusi flute": "葫芦丝",
 	Melodica: "口风琴",
+	Nai: "口风琴",
 	"Snare Drum": "小军鼓",
 	Cymbal: "镲",
 	Cymbals: "镲",

+ 86 - 0
src/helpers/communication.ts

@@ -362,4 +362,90 @@ export const removeSocketStatus = (callback: CallBack) => {
 /** 检查APP端websocket状态 */
 export const api_disconnectSocket = () => {
 	return promisefiyPostMessage({ api: "disconnectSocket" });
+};
+
+
+
+// MIDI播放&评测相关的api
+
+/** 发送midi音频等信息 */
+export const api_cloudDetail = (content: any, callback: CallBack) => {
+	postMessage(
+		{
+			api: "cloudDetail",
+			content,
+		},
+		callback
+	);
+};
+
+/** 检查midi播放器状态,status: 'init' | 'play' | 'suspend' */
+export const api_cloudGetMediaStatus = () => {
+	return promisefiyPostMessage({ api: "cloudGetMediaStatus" });
+};
+
+/** midi开始播放 */
+export const api_cloudPlay = (content: any) => {
+	promisefiyPostMessage({
+		api: "cloudPlay",
+		content,
+	});
+};
+
+/** midi暂停播放 */
+export const api_cloudSuspend = (content: any) => {
+	promisefiyPostMessage({
+		api: "cloudSuspend",
+		content,
+	});
+};
+
+/** midi跳转到指定位置播放 */
+export const api_cloudSetCurrentTime = (content: any) => {
+	promisefiyPostMessage({
+		api: "cloudSetCurrentTime",
+		content,
+	});
+};
+
+/** midi调整播放速度 */
+export const api_cloudChangeSpeed = (content: any) => {
+	promisefiyPostMessage({
+		api: "cloudChangeSpeed",
+		content,
+	});
+};
+
+/** midi设置声轨音量 */
+export const api_cloudVolume = (content: any) => {
+	promisefiyPostMessage({
+		api: "cloudVolume",
+		content,
+	});
+};
+
+/** midi,播放系统节拍器 */
+export const api_cloudMetronome = (content: any, callback: CallBack) => {
+	postMessage(
+		{
+			api: "cloudMetronome",
+			content,
+		},
+		callback
+	);
+};
+
+/** midi练习播放&评测播放回调 */
+export const api_cloudTimeUpdae = (callback: any) => {
+	listenerMessage("cloudTimeUpdae", callback);
+};
+
+/** 卸载监听midi播放回调 */
+export const api_remove_cloudTimeUpdae = (callback: any) => {
+	removeListenerMessage("cloudTimeUpdae", callback);
+};
+
+/** midi播放结束回调 */
+export const api_cloudplayed = (callback: any) => {
+	listenerMessage("cloudplayed", callback);
 };

+ 793 - 0
src/helpers/customMusicScore.ts

@@ -0,0 +1,793 @@
+import { ref } from "vue";
+import state, { customData } from "../state"
+import { getQuery } from "/src/utils/queryString";
+import { setGlobalData } from "/src/utils";
+const query: any = getQuery();
+
+interface IItem {
+	id?: string
+	y?: number
+	isLast?: boolean
+	childIndex?: number[]
+	
+}
+interface IItemList {
+	parts: string[],
+	tieId?: string[],
+	staveSection?: IItem[],
+	vfmodifiers?: IItem[],
+	voltas?: number
+	vfcurve?: IItem[]
+	stavenote?: IItem[]
+}
+interface IMusicList {
+	[_key: string]: IItemList[]
+}
+
+const container = ref();
+
+/** 曲谱配置: 重叠 */
+export const resetGivenFormate = () => {
+	interface IItem {
+		id?: string
+		y?: number
+		isLast?: boolean
+		childIndex?: number[]
+		
+	}
+	interface IItemList {
+		parts: string[],
+		tieId?: string[],
+		staveSection?: IItem[],
+		vfmodifiers?: IItem[],
+		voltas?: number
+		vfcurve?: IItem[]
+		stavenote?: IItem[]
+	}
+	interface IMusicList {
+		[_key: string]: IItemList[]
+	}
+	const musicList: IMusicList = {
+		'12200': [
+			{parts: ['0', '1'], tieId: ['1483']},
+			{parts: ['2'], tieId: ['1463']},
+			{parts: ['10'], tieId: ['1246']},
+			{parts: ['11'], tieId: ['2455']},
+			{parts: ['13'], tieId: ['1488', '1688']},
+			{parts: ['14', '15'], tieId: ['1272']},
+			{parts: ['16'], tieId: ['1264', '1368'], staveSection: [{id: 'section-0', y: -10}]},
+		],
+		'12420': [
+			{parts: ['0'], tieId: ['1298', '1405', '1998', '2598', '3229', '2731', '2617']}
+		],
+		'7729': [
+			{parts: ['3'], tieId: ['1498', '1660']}
+		],
+		'7439': [
+			{parts: ['23'], vfmodifiers: [{id: 'modifiers-130', y: -18, isLast: true}]}
+		],
+		'12711': [
+			{ parts: ['0'], voltas: -12},
+			{ parts: ['4'],voltas: -8},
+		],
+		'3581': [
+			{ parts: ['0'], voltas: -8},
+		],
+		'6244': [
+			{ parts: ['15'], stavenote: [{id: 'vf-auto1608', y: -15}]},
+		],
+		'7473': [
+			{ parts: ['0'], voltas: -8},
+		]
+	}
+	const tieList = musicList[state.cbsExamSongId as string]
+	if (tieList) {
+		const partIndex = query["part-index"] || '0'
+		const tie = tieList.find((item) => item.parts.includes(partIndex))
+		if (!tie) return
+		// 延音线和连线重叠
+		if (tie.tieId && tie.tieId.length) {
+			for(let tieIndex = 0; tieIndex < tie.tieId.length; tieIndex++){
+				const vftie: any = document.querySelector(`#vf-auto${tie.tieId[tieIndex]}-tie`)
+				const vfcurve = vftie?.parentNode?.parentNode?.querySelectorAll('.vf-curve')
+				if (vfcurve && vfcurve.length){
+					for(let i = 0; i < vfcurve.length; i++){
+						const result = collisionDetection(vftie, vfcurve[i])
+						if (result.isCollision){
+							vfcurve[i].style.transform = `translateY(-8px)`;
+							break;
+						}
+					}
+				}
+			}
+		}
+		
+		// 小节数字
+		if (tie.staveSection && tie.staveSection.length) {
+			const sectionList = document.querySelectorAll('.vf-StaveSection')
+			sectionList.forEach((node, index) => {
+				node.classList.add(`section-${index}`)
+			})
+			for(let i = 0; i < tie.staveSection.length; i++){
+				const item: any = document.querySelector( '.' + tie.staveSection[i].id)
+				if (item){
+					item.style.transform = `translateY(${tie.staveSection[i].y}px)`;
+				}
+			}
+		}
+
+		// modifiers 里面的符号
+		if(tie.vfmodifiers && tie.vfmodifiers.length){
+			const modifierList = document.querySelectorAll('.vf-modifiers')
+			modifierList.forEach((node, index) => {
+				node.classList.add(`modifiers-${index}`)
+			}) 
+			for(let i = 0; i < tie.vfmodifiers.length; i++){
+				const modifier = tie.vfmodifiers[i]
+				const item: SVGAElement = document.querySelector( '.' + modifier.id)!
+				if (item){
+					if (modifier.isLast){
+						const lastEle: any = Array.from(item.childNodes).at(-1)
+						if (lastEle){
+							lastEle.style.transform = `translateY(${modifier.y}px)`;
+						}
+					}
+				}
+			}
+		}
+
+		// 房子
+		if (tie.voltas){
+			const modifierList = document.querySelectorAll('.vf-Volta') as unknown as HTMLElement[]
+			modifierList.forEach((node, index) => {
+				node.style.transform = `translateY(${tie.voltas}px)`;
+			}) 
+		}
+
+		// 单个音符
+		if (tie.stavenote && tie.stavenote.length) {
+			for(let i = 0; i < tie.stavenote.length; i++){
+				const item = tie.stavenote[i]
+				const ele = document.querySelector('#' + item.id)! as unknown as HTMLElement
+				ele && (ele.style.transform = `translateY(${item.y}px)`)
+			}
+		}
+	}
+	
+};
+
+
+// 谱面优化
+export const resetFormate = () => {
+	container.value = document.getElementById('scrollContainer')
+	if (state.extStyleConfigJson || !container.value) return;
+	const stafflines: SVGAElement[] = Array.from((container.value as HTMLElement).querySelectorAll(".staffline"));
+	const baseStep = 4; // 两个元素相间,的间距
+	const musicalDistance = 28; // 音阶与第一条线谱的间距,默认设置为28
+	for (let i = 0, len = stafflines.length; i < len; i++) {
+		const staffline = stafflines[i];
+		const stafflineBox = staffline.getBBox();
+		const stafflineCenter = stafflineBox.y + stafflineBox.height / 2;
+		const vfmeasures: SVGAElement[] = Array.from(staffline.querySelectorAll(".vf-measure"));
+		const vfcurve: SVGAElement[] = Array.from(staffline.querySelectorAll(".vf-curve"));
+		const vfvoices: SVGAElement[] = Array.from(staffline.querySelectorAll(".vf-measure > .vf-voices"));
+		const vfbeams: SVGAElement[] = Array.from(staffline.querySelectorAll(".vf-measure > .vf-beams"));
+		const vfties: SVGAElement[] = Array.from(staffline.querySelectorAll(".vf-ties"));
+		const vflines: SVGAElement[] = Array.from(staffline.querySelectorAll(".vf-line"));
+		const texts: SVGAElement[] = Array.from(staffline.querySelectorAll(".vf-measure > .vf-stave text"));
+		const rects: SVGAElement[] = Array.from(staffline.querySelectorAll(".vf-measure > .vf-stave rect[fill=none]"));
+		const staveSection: SVGAElement[] = Array.from(staffline.querySelectorAll(".vf-measure .vf-staveSection"));
+		const paths: SVGAElement[] = Array.from(staffline.querySelectorAll(".vf-measure > .vf-stave path"));
+		// 获取第一个线谱的y轴坐标
+		const firstLinePathY = paths[0]?.getBBox().y || 0
+		// 反复标记 和 小节碰撞
+		const repetWord = ["To Coda", "D.S. al Coda", "Coda"];
+		texts
+			.filter((n) => repetWord.includes(n.textContent || ""))
+			.forEach((t) => {
+				vfbeams.forEach((curve) => {
+					const result = collisionDetection(t, curve);
+					const prePath: SVGAElement = t?.previousSibling as unknown as SVGAElement;
+					if (result.isCollision) {
+						const shift_y = Number(t.getAttribute("y")) - (result.b1 - result.t2) - baseStep + "";
+						t.setAttribute("y", shift_y);
+						// console.log('音阶间距',shift_y)
+						if (prePath && prePath.getAttribute("stroke-width") === "0.3" && prePath.getAttribute("stroke") === "none" && (prePath.getAttribute("d")?.length || 0) > 3000) {
+							prePath.style.transform = `translateY(${-(result.b1 - result.t2 + baseStep)}px)`;
+						}
+					}
+				});
+				vfvoices.forEach((curve) => {
+					const result = collisionDetection(t, curve);
+					const prePath: SVGAElement = t?.previousSibling as unknown as SVGAElement;
+					if (result.isCollision) {
+						const shift_y = Number(t.getAttribute("y")) - (result.b1 - result.t2) - baseStep + "";
+						t.setAttribute("y", shift_y);
+						// console.log('音阶间距',shift_y)
+						if (prePath && prePath.getAttribute("stroke-width") === "0.3" && prePath.getAttribute("stroke") === "none" && (prePath.getAttribute("d")?.length || 0) > 3000) {
+							prePath.style.transform = `translateY(${-(result.b1 - result.t2 + baseStep)}px)`;
+						}
+					}
+				});
+			});
+		// 文字方框和飞线碰撞
+		staveSection.forEach((t) => {
+			let shift_y = 0;
+			[...vfcurve, ...vfties, ...vfvoices].forEach((curve) => {
+				const result = collisionDetection(t, curve);
+				if (result.isCollision) {
+					shift_y = Math.min(shift_y, result.t2 - result.b1 - baseStep);
+				}
+			});
+			t.style.transform = `translateY(${shift_y}px)`;
+		});
+
+		// 文字和小节碰撞
+		let vftexts = Array.from(staffline.querySelectorAll(".vf-text > text")).filter((n: any) => n.getBBox().y < stafflineCenter);
+		for (let i = 0; i < vftexts.length; i++) {
+			const _text = vftexts[i];
+			for (let j = 0; j < vftexts.length; j++) {
+				if (_text.parentNode === vftexts[j].parentNode) continue;
+				const result = collisionDetection(_text as SVGAElement, vftexts[j] as SVGAElement);
+				if (result.isCollision) {
+					if (_text.textContent === vftexts[j].textContent) {
+						vftexts[j].parentNode?.removeChild(vftexts[j]);
+						continue;
+					}
+				}
+			}
+		}
+		vftexts = Array.from(staffline.querySelectorAll(".vf-text > text")).filter((n: any) => n.getBBox().y < stafflineCenter);
+		let maxY = 0;
+		let _vftexts: SVGAElement[] = [];
+
+		vftexts.forEach((vftext: any) => {
+			const textBox = vftext.getBBox();
+			if (textBox.y < stafflineCenter) {
+				maxY = Math.max(maxY, textBox.y + textBox.height);
+				//console.log('音阶间距',textBox.y, textBox.height)
+				_vftexts.push(vftext as SVGAElement);
+			}
+		});
+		if (maxY !== 0 && _vftexts.length > 1) {
+			_vftexts.forEach((vftext) => {
+				vftext.setAttribute("y", maxY + "");
+				//console.log('音阶间距',maxY)
+			});
+		}
+		vftexts.forEach((vftext) => {
+			[...vfcurve, ...vfmeasures, ...vflines].forEach((vfmeasure) => {
+				let result = collisionDetection(vftext as SVGAElement, vfmeasure);
+				if (result.isCollision && result.b1 < result.b2 && result.t1 < result.b2 - (result.b2 - result.t2) / 2) {
+					const shift_y = Number(vftext.getAttribute("y")) - (result.b1 - result.t2) - baseStep + "";
+					vftext.setAttribute("y", shift_y);
+					//console.log('音阶间距',shift_y)
+				}
+			});
+		});
+
+		vftexts.forEach((vftext) => {
+			vftexts.forEach((text) => {
+				if (vftext.parentNode !== text.parentNode && !["marcato", "legato"].includes(vftext.textContent as string)) {
+					if (["marcato", "legato"].includes(text.textContent as string)) {
+						const result = collisionDetection(vftext as SVGAElement, text as SVGAElement, 30, 30);
+						if (result.isCollision) {
+							const textBBox = (vftext as SVGAElement).getBBox();
+							text.setAttribute("x", textBBox.x + textBBox.width + 5 + "");
+							text.setAttribute("y", textBBox.y + textBBox.height - 5 + "");
+							//console.log('音阶间距',textBBox.y + textBBox.height - 5 + "")
+						}
+					} else {
+						const result = collisionDetection(vftext as SVGAElement, text as SVGAElement);
+						if (result.isCollision) {
+							const _y = Number(vftext.getAttribute("y"));
+							const shift_y = result.b2 - result.t2 < 24 ? 24 : result.b2 - result.t2;
+							text.setAttribute("y", _y - shift_y - 0.5 + "");
+							//console.log('音阶间距',_y - shift_y - 0.5 + "")
+						}
+					}
+				}
+			});
+		});
+		// 修改音阶和线谱的间距
+		const clefList = ['C', 'G', 'D', 'A', 'E', 'B', 'F#', 'C#', 'G#', 'F', 'Bb', 'Eb', 'Ab', 'Db', 'Gb', 'Cb', 'Fb', 'D#', 'A#', 'E#']
+		const btransList = ['Bb', 'Eb', 'Ab', 'Db', 'Gb', 'Cb', 'Fb']
+		const jtrsnsList = ['F#', 'C#', 'G#', 'D#', 'A#', 'E#', 'B#']
+		vftexts.forEach((label: any) => {
+			const labelText = label.textContent as string
+			if (clefList.includes(labelText)){
+				const _y = Number(label.getAttribute("y"))
+				const endY = firstLinePathY ? firstLinePathY - musicalDistance : _y
+				label.setAttribute("y", endY)
+			}
+			if (btransList.includes(labelText)) {
+				label.textContent = labelText.replace('b','♭')
+			}
+			if (jtrsnsList.includes(labelText)) {
+				label.textContent = labelText.replace('#','♯')
+			}
+		});
+		const vftextBottom = Array.from(staffline.querySelectorAll(".vf-text > text")).filter((n: any) => n.getBBox().y > stafflineCenter);
+		const vflineBottom = Array.from(staffline.querySelectorAll(".vf-line")).filter((n: any) => n.getBBox().y > stafflineCenter);
+		// 去重
+		for (let i = 0; i < vftextBottom.length; i++) {
+			const _text = vftextBottom[i];
+			for (let j = 0; j < vftextBottom.length; j++) {
+				if (_text.parentNode === vftextBottom[j].parentNode) continue;
+				const result = collisionDetection(_text as SVGAElement, vftextBottom[j] as SVGAElement);
+				if (result.isCollision) {
+					if (_text.textContent === vftextBottom[j].textContent) {
+						vftextBottom[j].parentNode?.removeChild(vftextBottom[j]);
+						continue;
+					}
+				}
+			}
+		}
+		// 1,2线谱底部文字重叠问题
+		vftextBottom.forEach((vftext) => {
+			[...vfmeasures].forEach((n) => {
+				let result = collisionDetection(vftext as SVGAElement, n);
+				if (result.isCollision) {
+					vftext.setAttribute("y", result.b2 + Math.abs(result.t1 - Number(vftext.getAttribute("y"))) + "");
+					//console.log('音阶间距', result.b2 + Math.abs(result.t1 - Number(vftext.getAttribute("y"))) + "")
+				}
+			});
+		});
+		// 如果渐弱渐强有平行的文字
+		vflineBottom.forEach((line) => {
+			const texts: any[] = [];
+			if (line.nextElementSibling?.classList.contains("vf-line")) {
+				vftextBottom.forEach((text) => {
+					let result = collisionDetection(line as SVGAElement, text as SVGAElement, 20, 20);
+					if (result.isCollision) {
+						texts.push({
+							text: text as SVGAElement,
+							result,
+						});
+					}
+				});
+			}
+			if (texts.length === 1) {
+				const result = texts[0].result;
+				const text = texts[0].text;
+				if (result.x2 + result.w2 < result.x1) {
+					// 左
+					if (Math.abs(result.y2 - result.y1) > 10) {
+						text.setAttribute("y", result.y1 + result.h2 / 2 + "");
+						//console.log('音阶间距', result.y1 + result.h2 / 2 + "")
+					}
+				} else if (result.x2 > result.x1 + result.w1) {
+					// 右
+					if (Math.abs(result.y2 - result.y1) > 10) {
+						text.setAttribute("y", result.y1 + result.h2 / 2 + "");
+						//console.log('音阶间距', result.y1 + result.h2 / 2 + "")
+					}
+				} else {
+					if (Math.abs(result.x2 - result.x1) < Math.abs(result.x2 + result.w2 - result.x1 - result.w1)) {
+						// console.log(text, '有交集', '靠左')
+						text.setAttribute("x", result.x1 - result.w2 - 5 + "");
+						if (Math.abs(result.y2 - result.y1) > 10) {
+							text.setAttribute("y", result.y1 + result.h2 / 2 + "");
+							//console.log('音阶间距', result.y1 + result.h2 / 2 + "")
+						}
+					} else {
+						// console.log(text, '有交集', '靠右')
+						text.setAttribute("x", result.x1 + result.w1 + 5 + "");
+						if (Math.abs(result.y2 - result.y1) > 10) {
+							text.setAttribute("y", result.y1 + result.h2 / 2 + "");
+							//console.log('音阶间距', result.y1 + result.h2 / 2 + "")
+						}
+					}
+				}
+			} else if (texts.length === 2) {
+				const result1 = texts[0].result;
+				const text1 = texts[0].text;
+				const result2 = texts[1].result;
+				const text2 = texts[1].text;
+				text1.setAttribute("x", result1.x1 - result1.w2 - 5 + "");
+				if (Math.abs(result1.y2 - result1.y1) > 10) {
+					text1.setAttribute("y", result1.y1 + result1.h2 / 2 + "");
+					//console.log('音阶间距', result1.y1 + result1.h2 / 2 + "")
+				}
+				text2.setAttribute("x", result2.x1 + result2.w1 + 5 + "");
+				if (Math.abs(result2.y2 - result2.y1) > 10) {
+					text2.setAttribute("y", result2.y1 + result2.h2 / 2 + "");
+					//console.log('音阶间距', result2.y1 + result2.h2 / 2 + "")
+				}
+			} else if (texts.length === 3) {
+				// console.log(texts)
+			}
+		});
+
+		vftextBottom.forEach((vftext) => {
+			vftextBottom.forEach((text) => {
+				if (vftext.parentNode !== text.parentNode && !["marcato", "legato", "cresc.", "Cantabile"].includes(vftext.textContent as string)) {
+					if (["marcato", "legato", "cresc.", "Cantabile"].includes(text.textContent as string)) {
+						const result = collisionDetection(vftext as SVGAElement, text as SVGAElement, 30, 30);
+						if (result.isCollision) {
+							const textBBox = (vftext as SVGAElement).getBBox();
+							text.setAttribute("x", textBBox.x + textBBox.width + 5 + "");
+							text.setAttribute("y", textBBox.y + textBBox.height - 5 + "");
+							//console.log('音阶间距', textBBox.y + textBBox.height - 5 + "")
+						}
+					} else {
+						const result = collisionDetection(vftext as SVGAElement, text as SVGAElement);
+						if (result.isCollision) {
+							text.setAttribute("y", result.y1 + result.h1 + result.h2 + "");
+							//console.log('音阶间距', result.y1 + result.h1 + result.h2 + "")
+						}
+					}
+				}
+			});
+		});
+	}
+
+	// setTimeout(() => this.resetGlobalText());
+};
+// 技巧文本
+const resetGlobalText = () => {
+	const svg = container.value.querySelector("svg");
+	if (!svg) return;
+	const svgBBox = svg.getBBox();
+	let vfstavetempo: SVGAElement[] = Array.from(container.value.querySelectorAll(".vf-stavetempo")).reduce((eles: SVGAElement[], value: any) => {
+		if (eles.find((n) => n.outerHTML === value.outerHTML)) value?.parentNode?.removeChild(value);
+		else eles.push(value);
+		return eles;
+	}, []);
+	const staffline: SVGAElement[] = Array.from(container.value.querySelectorAll(".staffline"));
+	const vfmeasures: SVGAElement[] = Array.from(container.value.querySelectorAll(".staffline > .vf-measure"));
+	const vftexts: SVGAElement[] = Array.from(container.value.querySelectorAll(".staffline > .vf-text"));
+	const vfcurves: SVGAElement[] = Array.from(container.value.querySelectorAll(".staffline > .vf-curve"));
+
+	vfstavetempo.forEach((child: SVGAElement) => {
+		let _y = 0;
+		[...vfmeasures, ...vftexts, ...vfcurves].forEach((ele) => {
+			const result = collisionDetection(child as SVGAElement, ele);
+			if (result.isCollision && (result.b1 < result.b2 || result.r1 > result.l2 || result.l1 < result.r2)) {
+				_y = Math.min(_y, result.t2 - result.b1);
+			}
+		});
+		if (_y !== 0) {
+			child.style.transform = `translateY(${_y}px)`;
+		}
+
+		const childBBox = child.getBBox();
+		const rightY = (childBBox.x + childBBox.width) * 0.7 - Number(svg.getAttribute("width"));
+		if (rightY > 0) {
+			[...staffline, ...vfstavetempo].forEach((tempo) => {
+				if (child != tempo) {
+					const result = collisionDetection(child as SVGAElement, tempo, Math.abs(rightY), Math.abs(_y));
+					if (result.isCollision) {
+						_y = result.t2 - result.b1;
+					}
+				}
+			});
+			child.style.transform = `translate(-${rightY / 0.7}px,${_y}px)`;
+		}
+	});
+
+	if (svgBBox.y < 0) {
+		svg.setAttribute("height", Number(svg.getAttribute("height")) - svgBBox.y + 10);
+	}
+};
+
+// 碰撞检测
+const collisionDetection = (a: SVGAElement, b: SVGAElement, distance: number = 0, distance_y: number = 0) => {
+	const abbox = a.getBBox();
+	const bbbox = b.getBBox();
+	let t1 = abbox.y - distance_y;
+	let l1 = abbox.x - distance;
+	let r1 = abbox.x + abbox.width + distance;
+	let b1 = abbox.y + abbox.height + distance_y;
+
+	let t2 = bbbox.y;
+	let l2 = bbbox.x;
+	let r2 = bbbox.x + bbbox.width;
+	let b2 = bbbox.y + bbbox.height;
+	if (b1 < t2 || l1 > r2 || t1 > b2 || r1 < l2) {
+		// 表示没碰上
+		return {
+			isCollision: false,
+			t1,
+			l1,
+			r1,
+			b1,
+			t2,
+			l2,
+			r2,
+			b2,
+			x1: abbox.x,
+			y1: abbox.y,
+			x2: bbbox.x,
+			y2: bbbox.y,
+			h1: abbox.height,
+			h2: bbbox.height,
+			w1: abbox.width,
+			w2: bbbox.width,
+		};
+	} else {
+		return {
+			isCollision: true,
+			t1,
+			l1,
+			r1,
+			b1,
+			t2,
+			l2,
+			r2,
+			b2,
+			x1: abbox.x,
+			y1: abbox.y,
+			x2: bbbox.x,
+			y2: bbbox.y,
+			h1: abbox.height,
+			h2: bbbox.height,
+			w1: abbox.width,
+			w2: bbbox.width,
+		};
+	}
+};
+
+
+/** 全局曲谱配置 */
+export const setGlobalMusicSheet = () => {
+	const partIndex = query["part-index"] || '0'
+	/** 延音线方向问题 start */
+	const stavetieList = [
+	  {id: '12644', part_index: '25', direction: 1}
+	]
+	const tieItem = stavetieList.find(({id, part_index}) => {
+	  return id == state.cbsExamSongId && part_index == partIndex
+	})
+	setGlobalData('tieDirection', tieItem ? tieItem.direction : undefined)
+	/** 延音线方向问题 end */
+  
+	const graceList = [
+	  {id: '3509', part_index: '16', direction: 1}
+	]
+	const graceItem = graceList.find(({id, part_index}) => {
+	  return id == state.cbsExamSongId && part_index == partIndex
+	})
+	if (graceItem){
+	  setGlobalData('graceCustom', {direction: graceItem.direction})
+	}
+	const bassDrumList = [
+	  {id: '3030', part_index: '17', line: 4},
+	  {id: '12704', part_index: '23', line: 3}
+	]
+	const bassDrumItem = bassDrumList.find(({id, part_index}) => {
+	  return id == state.cbsExamSongId && part_index == partIndex
+	})
+	if (bassDrumItem){
+	  setGlobalData('customBassDrum', bassDrumItem.line)
+	}
+	/** 打击乐多声部,双声部休止符重叠 end */
+  
+	/** 符杆朝向 */
+	const stemDirectionList = [
+	  {
+		id: '11654', 
+		part_index: '16', 
+		stemNotes: [
+		  {id: 124, direction: 0},
+		  {id: 125, direction: 0},
+		  {id: 126, direction: 0},
+		  {id: 127, direction: 0},
+		  {id: 128, direction: 0}
+		]
+	  },
+	  {
+		id: '3581', 
+		part_index: '4', 
+		stemNotes: [
+		  {id: 380, direction: 1},
+		]
+	  },
+	  {
+		id: '3470', 
+		part_index: '0', 
+		stemNotes: [
+		  {id: 36, direction: 1},
+		  {id: 37, direction: 1},
+		]
+	  },
+	  {
+		id: '3470', 
+		part_index: '11', 
+		stemNotes: [
+		  {id: 33, direction: 1},
+		  {id: 56, direction: 1},
+		]
+	  },
+	  {
+		id: '12644', 
+		part_index: '22', 
+		stemNotes: [
+		  {id: 22, direction: 1},
+		  {id: 26, direction: 1},
+		  {id: 135, direction: 1},
+		  {id: 163, direction: 1},
+		  {id: 199, direction: 1},
+		  {id: 204, direction: 1},
+		  {id: 206, direction: 1},
+		  {id: 208, direction: 1},
+		  {id: 210, direction: 1},
+		  {id: 213, direction: 1},
+		]
+	  },
+	  {
+		id: '12303', 
+		part_index: '18', 
+		stemNotes: [
+		  {id: 1, direction: 1},
+		  {id: 4, direction: 1},
+		  {id: 6, direction: 1},
+		  {id: 9, direction: 1},
+		  {id: 12, direction: 1},
+		  {id: 14, direction: 1},
+		]
+	  },
+	  {
+		id: '12669', 
+		part_index: '24', 
+		stemNotes: [
+		  {id: 65, direction: 1},
+		  {id: 296, direction: 1},
+		  {id: 298, direction: 1},
+		  {id: 300, direction: 1},
+		  {id: 338, direction: 1},
+		]
+	  },
+	  {
+		id: '12420', 
+		part_index: '21', 
+		stemNotes: [
+		  {id: 614, direction: 0},
+		  {id: 617, direction: 0},
+		  {id: 619, direction: 0},
+		  {id: 621, direction: 0},
+		]
+	  },
+	  {
+		id: '12711', 
+		part_index: '22', 
+		stemNotes: []
+	  },
+	  {
+		id: '12973', 
+		part_index: '21', 
+		stemNotes: [
+		  {id: 619, direction: 1},
+		  {id: 622, direction: 1},
+		  {id: 745, direction: 1},
+		]
+	  },
+	]
+	const stemDirectionItem = stemDirectionList.find(({id, part_index}) => {
+	  return id == state.cbsExamSongId && part_index == partIndex
+	})
+	if (stemDirectionItem) {
+	  setGlobalData('stemDirectionNote', stemDirectionItem.stemNotes)
+	}
+  
+	/** vfcure */
+	const vfcurveList = [
+	  {
+		id: '12711', 
+		part_index: '4', 
+		vfcurve: [
+		  {MeasureNumberXML: 25, index: 1, bezierEndControlPt: {y: -2}},
+		  {MeasureNumberXML: 33, index: 1, bezierEndControlPt: {y: -2}},
+		]
+	  },
+	  {
+		id: '12059', 
+		part_index: '0', 
+		vfcurve: [
+		  {MeasureNumberXML: 15, bezierEndControlPt: {y: 2.8}, bezierEndPt:{y: 1.1}},
+		  {MeasureNumberXML: 16, bezierEndControlPt: {y: -1}},
+		  {MeasureNumberXML: 19, index: 1, bezierEndControlPt: {y: 2}},
+		  {MeasureNumberXML: 20, bezierEndControlPt: {y: -1}},
+		  {MeasureNumberXML: 42, index: 1, bezierEndControlPt: {y: -1.5}, bezierStartControlPt: {y: -1.5}},
+		  {MeasureNumberXML: 46, index: 3, bezierEndControlPt: {y: -1.5}, bezierStartControlPt: {y: -1.5}},
+		]
+	  },
+	  {
+		id: '12668', 
+		part_index: '11', 
+		vfcurve: [
+		  {MeasureNumberXML: 8, index: 2, bezierEndControlPt: {y: -3}, bezierStartControlPt:{y: -3}, bezierEndPt:{y: -1}},
+		]
+	  },
+	  {
+		id: '11976', 
+		part_index: '0', 
+		vfcurve: [
+		  {MeasureNumberXML: 14, index: 4, bezierEndControlPt: {y: -3}},
+		  {MeasureNumberXML: 14, index: 1, bezierEndPt: {y: 1.5}, bezierEndControlPt: {y: 1}},
+		]
+	  },
+	]
+	const vfcurveItem = vfcurveList.find(({id, part_index}) => {
+	  return id == state.cbsExamSongId && part_index == partIndex
+	})
+	if (vfcurveItem) {
+	  setGlobalData('vfcurveItem', vfcurveItem.vfcurve)
+	}
+	/** drum set声部 重音 */
+	const customArtPositionList = [
+	  {id: '12644', part_index: '25'}
+	]
+	const customArtPositionItem = customArtPositionList.find(({id, part_index}) => {
+	  return id == state.cbsExamSongId && part_index == partIndex
+	})
+	if (customArtPositionItem) {
+	  setGlobalData('customArtPosition', true)
+	}
+	/** 全声部声部 - & 全音符 */
+	const customTenutoList = [
+	  {id: '12645', part_index: '5'}
+	]
+	const customTenutoItem = customTenutoList.find(({id, part_index}) => {
+	  return id == state.cbsExamSongId && part_index == partIndex
+	})
+	if (customTenutoItem) {
+	  setGlobalData('customTenutoItem', true)
+	}
+	/** 全声部声部 >  */
+	const customAccentList = [
+	  {id: '12711', part_index: '22'},
+	  {id: '12711', part_index: '25'},
+	]
+	const customAccentItem = customAccentList.find(({id, part_index}) => {
+	  return id == state.cbsExamSongId && part_index == partIndex
+	})
+	if (customAccentItem) {
+	  setGlobalData('customAccentItem', true)
+	}
+	/** 全声部声部 +  */
+	const customLefthandpizzicatoList = [
+	  {id: '12711', part_index: '25'},
+	  {id: '7755', part_index: '10'},
+	  {id: '6226', part_index: '16'},
+	]
+	const customLefthandpizzicatoItem = customLefthandpizzicatoList.find(({id, part_index}) => {
+	  return id == state.cbsExamSongId && part_index == partIndex
+	})
+	if (customLefthandpizzicatoItem) {
+	  setGlobalData('customLefthandpizzicatoItem', true)
+	}
+}
+
+/** 设置自定义渐慢 */
+export const setCustomGradual = () => {
+	if (state.gradualTimes) {
+		const detailId = state.cbsExamSongId + "";
+		const partIndex = state.partIndex + "";
+		if (["12280"].includes(detailId) && ["24"].includes(partIndex)) {
+			state.gradualTimes["8"] = "00:26:10";
+			state.gradualTimes["66"] = "01:53:35";
+			state.gradualTimes["90"] = "02:41:40";
+		}
+	}
+};
+
+/** 设置自定义音符数据 */
+export const setCustomNoteRealValue = () => {
+	const detailId = state.cbsExamSongId + "";
+    const partIndex = state.partIndex + "";
+	if (["2670"].includes(detailId)) {
+		customData.customNoteRealValue = {
+			0: 0.03125,
+		};
+	}
+	if (["12673"].includes(detailId) && ['22'].includes(partIndex)) {
+		customData.customNoteRealValue = {
+			208: 0.125,
+		};
+	}
+
+    if (["12667", "12673"].includes(detailId)){
+        customData.customNoteCurrentTime = true
+    }
+};

+ 40 - 19
src/helpers/formateMusic.ts

@@ -27,11 +27,15 @@ export const getFixTime = (speed: number) => {
 	let numerator = duration.numerator || 0;
 	let denominator = duration.denominator || 4;
 	const beatUnit = duration.beatUnit || "quarter";
+	// if (state.repeatedBeats) {
+	// 	// 音频制作问题仅2拍不重复
+	// 	numerator = numerator === 2 ? 4 : numerator;
+	// } else if (numerator === 2 && denominator === 4) {
+	// 	numerator = 4
+	// }
+	// 重复节拍,拍数*2进行计算
 	if (state.repeatedBeats) {
-		// 音频制作问题仅2拍不重复
-		numerator = numerator === 2 ? 4 : numerator;
-	} else if (numerator === 2 && denominator === 4) {
-		numerator = 4
+		numerator = numerator*2;
 	}
 	// console.log('diff', speed, duration, formatBeatUnit(beatUnit), denominator, numerator, (numerator / denominator))
 	return state.isOpenMetronome ? (60 / speed) * formatBeatUnit(beatUnit) * (numerator / denominator) : 0;
@@ -356,7 +360,7 @@ export const onlyVisible = (xml: string, partIndex: number): string => {
 	const detailId = state.examSongId + "";
 	const xmlParse = new DOMParser().parseFromString(xml, "text/xml");
 	const partList = xmlParse.getElementsByTagName("part-list")?.[0]?.getElementsByTagName("score-part") || [];
-	const partListNames = Array.from(partList).map((item) => item.getElementsByTagName("part-name")?.[0]?.textContent || "");
+	const partListNames = Array.from(partList).map((item) => item.getElementsByTagName("part-name")?.[0]?.textContent?.trim() || "");
 	const parts: any = xmlParse.getElementsByTagName("part");
 	// const firstTimeInfo = parts[0]?.getElementsByTagName('metronome')[0]?.parentElement?.parentElement?.cloneNode(true)
 	const firstMeasures = [...parts[0]?.getElementsByTagName("measure")];
@@ -603,7 +607,7 @@ export const formatXML = (xml: string): string => {
 	// 	}
 	// }
 	// console.log(11111,Array.from(xmlParse.getElementsByTagName("staffline")),Array.from(xmlParse.getElementsByTagName("words")))
-	// let speed = -1
+	let speed = -1
 	let beats = -1;
 	let beatType = -1;
 	// 小节中如果没有节点默认为休止符
@@ -614,9 +618,9 @@ export const formatXML = (xml: string): string => {
 		if (beatType === -1 && measure.getElementsByTagName("beat-type").length) {
 			beatType = parseInt(measure.getElementsByTagName("beat-type")[0].textContent || "4");
 		}
-		// if (speed === -1 && measure.getElementsByTagName('per-minute').length) {
-		//   speed = parseInt(measure.getElementsByTagName('per-minute')[0].textContent || this.firstLib?.speed)
-		// }
+		if (speed === -1 && measure.getElementsByTagName('per-minute').length) {
+		  speed = Number(measure.getElementsByTagName('per-minute')[0]?.textContent)
+		}
 		const divisions = parseInt(measure.getElementsByTagName("divisions")[0]?.textContent || "256");
 		if (measure.getElementsByTagName("note").length === 0) {
 			const forwardTimeElement = measure.getElementsByTagName("forward")[0]?.getElementsByTagName("duration")[0];
@@ -634,6 +638,10 @@ export const formatXML = (xml: string): string => {
         </note>`;
 		}
 	}
+	// 如果曲谱详情接口没有返回速度,则取xml第一小节的速度,如果取不到,则取默认速度:100
+	if (!state.originSpeed) {
+		state.originSpeed = state.speed = speed || 100
+	}
 	return new XMLSerializer().serializeToString(xmlParse);
 };
 
@@ -650,15 +658,15 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 	const { originSpeed: baseSpeed } = state;
 	const formatRealKey = (realKey: number, detail: any) => {
 		// 不是管乐迷, 不处理
-		if (state.appName !== "GYM") return realKey;
+		// if (state.appName !== "GYM") return realKey;
 		// 长笛的LEVEL 2-5-1条练习是泛音练习,以每小节第一个音的指法为准,高音不变变指法。
 		const olnyOneIds = ["906"];
-		if (olnyOneIds.includes(detailId)) {
+		if (olnyOneIds.includes(state.cbsExamSongId)) {
 			return detail.measures[0]?.realKey || realKey;
 		}
 		// 圆号的LEVEL 2-5条练习是泛音练习,最后四小节指法以连音线第一个小节为准
 		const olnyOneIds2 = ["782", "784"];
-		if (olnyOneIds2.includes(detailId)) {
+		if (olnyOneIds2.includes(state.cbsExamSongId)) {
 			const measureNumbers = [14, 16, 30, 32];
 			if (measureNumbers.includes(detail.firstVerticalMeasure?.measureNumber)) {
 				return allNotes[allNotes.length - 1]?.realKey || realKey;
@@ -666,7 +674,7 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 		}
 		// 2-6 第三小节指法按照第一个音符显示
 		const filterIds = ["900", "901", "640", "641", "739", "740", "800", "801", "773", "774", "869", "872", "714", "715"];
-		if (filterIds.includes(detailId)) {
+		if (filterIds.includes(state.cbsExamSongId)) {
 			if (detail.firstVerticalMeasure?.measureNumber === 3 || detail.firstVerticalMeasure?.measureNumber === 9) {
 				return detail.measures[0]?.realKey || realKey;
 			}
@@ -772,11 +780,15 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 				currentTime,
 				isDouble,
 				isMutileSubject,
+				measuresTempoInBPM: note?.sourceMeasure?.tempoInBPM
 			});
 		}
 
 		iterator.moveToNextVisibleVoiceEntry(false);
 	}
+	// 是否是变速的曲子
+	const hasVaryingSpeed = _notes.some((item: any) => item.measuresTempoInBPM !== _notes[0].measuresTempoInBPM)
+	console.log('变速曲子',hasVaryingSpeed)
 	for (let { note, iterator, currentTime, isDouble, isMutileSubject } of _notes) {
 		if (note) {
 			if (si === 0) {
@@ -822,8 +834,14 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 			}
 
 			let relativeTime = usetime;
+			let beatSpeed = 0;
 			// 速度不能为0 此处的速度应该是按照设置的速度而不是校准后的速度,否则mp3速度不对
-			let beatSpeed = (state.isSpecialBookCategory ? measureSpeed : baseSpeed) || 1;
+			if (measureSpeed !== baseSpeed && !hasVaryingSpeed) {
+				beatSpeed = baseSpeed || measureSpeed || 100
+			} else {
+				beatSpeed = (state.isSpecialBookCategory ? measureSpeed : baseSpeed) || 1;
+			}
+			// let beatSpeed = measureSpeed || baseSpeed
 			// 如果有节拍器,需要将节拍器的时间算出来
 			if (i === 0) {
 				fixtime += getFixTime(beatSpeed);
@@ -900,7 +918,9 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 				}
 			}
 			const _noteLength = NoteRealValue;
+			// 当前音符的持续时长,当前音符的RealValue值*拍数*(60/后台设置的基准速度)
 			let noteLength = gradualLength ? gradualLength : Math.min(vRealValue, NoteRealValue) * formatBeatUnit(beatUnit) * (60 / beatSpeed);
+			// 小节时长
 			const measureLength = vRealValue * vDenominator * (60 / beatSpeed);
 			// console.table({value: iterator.currentTimeStamp.realValue, vRealValue,NoteRealValue, noteLength,measureLength, MeasureNumberXML: note.sourceMeasure.MeasureNumberXML})
 			// console.log(i, Math.min(vRealValue, NoteRealValue),noteLength,gradualLength, formatBeatUnit(beatUnit),beatSpeed, NoteRealValue * formatBeatUnit(beatUnit) * (60 / beatSpeed) )
@@ -927,8 +947,8 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 				});
 				if (_firstMeasureRealValue < vRealValue) {
 					// console.log(_firstMeasureRealValue, vRealValue)
-					// 如果是弱起,将整个小节的时值减去音符的时值,就是缺省的时值
-					difftime = measureLength - noteLength;
+					// 如果是弱起,将整个小节的时值减去该小节所有音符相加的时值,就是缺省的时值
+					difftime = measureLength - _firstMeasureRealValue * formatBeatUnit(beatUnit) * (60 / beatSpeed);
 				}
 				if (difftime > 0) {
 					fixtime += difftime;
@@ -953,13 +973,14 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 			}
 
 			// console.log(note.tie)
-			// console.log(relaEndtime, fixtime, '时间')
+			// console.log(relaEndtime, fixtime, '时间',measureLength)
+			// console.log('频率',note?.pitch?.frequency,i)
 			const nodeDetail = {
 				isStaccato: note.voiceEntry.isStaccato(),
 				isRestFlag: note.isRestFlag,
 				noteId: note.NoteToGraphicalNoteObjectId,
 				measureListIndex: note.sourceMeasure.measureListIndex,
-				MeasureNumberXML: note.sourceMeasure.MeasureNumberXML,
+				MeasureNumberXML: note.sourceMeasure.MeasureNumberXML, // 当前的小节数,(从1开始)
 				_noteLength: _noteLength,
 				svgElement: svgElement,
 				frequency: note?.pitch?.frequency || -1,
@@ -1045,7 +1066,7 @@ export const getNoteByMeasuresSlursStart = (note: any) => {
 			}
 		}
 		if (arr.length) {
-			return arr.find((n: any) => n.i === (note.i - 1))
+			return arr.find((n: any) => n.i === (note.i - 1)) || arr[0]
 		}
 	}
 	return activeNote;

+ 80 - 9
src/helpers/metronome.ts

@@ -9,12 +9,21 @@ import { browser } from "/src/utils/index";
 import state from "/src/state";
 import { Howl } from "howler";
 import tockAndTick from "/src/constant/tockAndTick.json";
+import tickWav from "/src/assets/tick.wav";
+import tockWav from "/src/assets/tock.wav";
+
 type IOptions = {
 	speed: number;
 };
 const browserInfo = browser();
 let tipsTimer: any = null; // 光标提示定时器
 
+// HTMLAudioElement 音频
+const audioData = reactive({
+	tick: null as unknown as HTMLAudioElement,
+	tock: null as unknown as HTMLAudioElement,
+});
+
 export const metronomeData = reactive({
 	disable: true,
 	initPlayerState: false,
@@ -28,6 +37,8 @@ export const metronomeData = reactive({
 	activeMetro: {} as any,
 	cursorMode: 1 as number, // 光标模式:1:音符指针;2:节拍指针;3:关闭指针
 	cursorTips: '' as string, // 光标模式提示文字
+	followAudioIndex: 1, // 当前的拍数
+	totalNumerator: 2, // 总拍数
 });
 
 watch(
@@ -99,14 +110,39 @@ class Metronome {
 		metronomeData.activeList = [];
 	}
 	initPlayer() {
-		if (!this.source1) {
-			this.source1 = this.loadAudio1();
-		}
-		if (!this.source2) {
-			this.source2 = this.loadAudio2();
-		}
-		metronomeData.initPlayerState = true;
+		// if (!this.source1) {
+		// 	this.source1 = this.loadAudio1();
+		// }
+		// if (!this.source2) {
+		// 	this.source2 = this.loadAudio2();
+		// }
+		// metronomeData.initPlayerState = true;
+
+		Promise.all([this.createAudio(tickWav), this.createAudio(tockWav)]).then(
+			([tick, tock]) => {
+				if (tick) {
+					audioData.tick = tick;
+				}
+				if (tock) {
+					audioData.tock = tock;
+				}
+				metronomeData.initPlayerState = true;
+			}
+		);		
 	}
+	createAudio = (src: string): Promise<HTMLAudioElement | null> => {
+		return new Promise((resolve) => {
+			// const a = new Audio(src + '?v=' + Date.now());
+			const a = new Audio(src);
+			a.load();
+			a.onloadedmetadata = () => {
+				resolve(a);
+			};
+			a.onerror = () => {
+				resolve(null);
+			};
+		});
+	};
 
 	// 播放
 	sound = (currentTime: number) => {
@@ -141,9 +177,40 @@ class Metronome {
 	// 播放
 	playAudio = () => {
 		if (!metronomeData.initPlayerState) return;
-		this.source = metronomeData.activeMetro?.index === 0 ? this.source1 : this.source2;
-		this.source.volume(metronomeData.disable || state.playState === 'paused' ? 0 : 0.4);
+		const beatVolume = state.setting.beatVolume / 100
+		// this.source = metronomeData.activeMetro?.index === 0 ? this.source1 : this.source2;
+		// this.source.volume(metronomeData.disable || state.playState === 'paused' ? 0 : beatVolume);
+		// Audio 播放音频
+		this.source = metronomeData.activeMetro?.index === 0 ? audioData.tick : audioData.tock;
+		this.source.volume = metronomeData.disable || state.playState === 'paused' ? 0 : beatVolume;
+		this.source.play();
+	};
+
+	/**
+	 * 跟练模式播放,跟练模式没有曲子音频播放器
+	 */
+	simulatePlayAudio = () => {
+		// console.log(333, metronomeData.followAudioIndex)
+		if (!metronomeData.initPlayerState) return;
+		const beatVolume = state.setting.beatVolume / 100
+		// this.source = metronomeData.followAudioIndex === 1 ? this.source1 : this.source2;
+		// Audio 播放音频
+		this.source = metronomeData.followAudioIndex === 1 ? audioData.tick : audioData.tock;
+		// this.source.volume(metronomeData.disable ? 0 : beatVolume);
+		this.source.volume = metronomeData.disable ? 0 : beatVolume
+		/**
+		 * https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLMediaElement/volume
+		 * volume属性在部分ios手机的Safari浏览器不被支持
+		 */
+		if (this.source.volume <= 0) {
+			this.source.muted = true
+		} else {
+			this.source.muted = false
+		}
+		console.log('音量',this.source,this.source.volume)
 		this.source.play();
+		metronomeData.followAudioIndex += 1;
+		metronomeData.followAudioIndex = metronomeData.followAudioIndex > metronomeData.totalNumerator ? 1 : metronomeData.followAudioIndex;
 	};
 
 	// 切换
@@ -152,11 +219,14 @@ class Metronome {
 	loadAudio1 = () => {
 		return new Howl({
 			src: tockAndTick.tick,
+			// 如果是ios手机,需要强制使用audio,不然部分系统版本第一次播放没有声音
+			// html5: browserInfo.ios,
 		});
 	};
 	loadAudio2 = () => {
 		return new Howl({
 			src: tockAndTick.tock,
+			// html5: browserInfo.ios,
 		});
 	};
 	getStep(time: number) {
@@ -280,6 +350,7 @@ class Metronome {
 		// 5.得到所有的节拍时间
 		metronomeData.metroList = metroList;
 		metronomeData.metroMeasure = metroMeasure;
+		// console.log(9999,metroList,7777,metroMeasure)
 		metronomeData.activeMetro = metroMeasure[0]?.[0] || {};
 	}
 }

+ 116 - 0
src/helpers/midiPlay.tsx

@@ -0,0 +1,116 @@
+/**
+ * app播放midi
+ */
+
+import { ref } from 'vue'
+import { getDuration } from "/src/helpers/formateMusic";
+import state, { onPlay } from "/src/state";
+import { OpenSheetMusicDisplay } from "/osmd-extended/src";
+import { api_cloudDestroy, api_cloudDetail, api_cloudVolume, api_cloudGetMediaStatus, 
+  api_cloudPlay, api_cloudSuspend,  } from "/src/helpers/communication";
+import { audioData } from "/src/view/audio-list"  
+
+export type IMode = 'background' | 'music'
+
+export const initMidi = (durationNum: number, midiUrl?: string) => {
+  const initial = ref(false)
+  if (midiUrl) {
+    console.log('曲谱为midi,使用app播放')
+    initial.value = true
+    state.midiPlayIniting = true
+    const duration: any = getDuration(state.osmd as unknown as OpenSheetMusicDisplay);
+    // 销毁播放器
+    api_cloudDestroy();
+    // 发送初始化信息
+    api_cloudDetail({
+      api: 'cloudDetail',
+      content: {
+        midi: midiUrl,
+        denominator: duration.denominator,
+        numerator: duration.numerator,
+        originalSpeed: state.originSpeed,
+        interval: 50,
+        duration: durationNum * 1000,
+      }
+    }, () => {
+      state.midiPlayIniting = false
+      initial.value = false
+      if (midiUrl) {
+        changeMode('music')
+      }
+    })
+    state.durationNum = durationNum
+  }
+  return {
+    initial,
+  }
+}
+
+/** 获取当前MidiId */
+export const getActiveMidiId = () => {
+  return state.osmd?.sheet?.instruments?.[0]?.subInstruments?.[0]?.midiInstrumentID ?? 0
+}
+
+/**
+ * 修改原音或伴奏
+ * @param val IMode
+ */
+export const changeMode = async (val: IMode, type?: string | undefined) => {
+  const cm: IMode = val === 'background' ? 'music' : 'background'
+  console.log(!state.songs[val], val, cm)
+  if (state.isAppPlay) {
+    const data = new Map()
+    for (const name of state.partListNames) {
+      data.set(name, 60)
+    }
+    // for (const name of getVoicePartInfo().partListNames) {
+    //   data.set(name, cm === 'background' ? 100 : 0)
+    // }
+    api_cloudVolume({
+      activeMidiId: getActiveMidiId(),
+      activeMidiVolume: cm === 'background' ? 100 : 0,
+      parts: Array.from(data.keys()).map((item) => ({
+        name: item,
+        volume: data.get(item),
+      })),
+    })
+  }
+  state.playSource = val
+  if (type === 'all') {
+    state.audiosInstance?.setMute(true, state.songs[cm])
+    state.audiosInstance?.setMute(true, state.songs[val])
+  } else {
+    state.audiosInstance?.setMute(true, state.songs[cm])
+    state.audiosInstance?.setMute(false, state.songs[val])
+  }
+}
+
+/**
+ * 切换midi播放状态
+ */
+export const cloudToggleState = async () => {
+  const cloudGetMediaStatus = await api_cloudGetMediaStatus();
+  const status = cloudGetMediaStatus?.content.status
+  if (status === 'init') {
+    return
+  }
+  if (status === 'suspend') {
+    await api_cloudPlay({
+      songID: state.examSongId,
+      startTime: audioData.progress * 1000,
+      originalSpeed: state.originSpeed, // midi初始速度
+      speed: state.speed, // 实际速度
+      hertz: 440, //SettingState.sett.hertz,
+    })
+    // startCapture()
+    onPlay()
+  } else {
+    await api_cloudSuspend({
+      songID: state.examSongId,
+    })
+    // endCapture()
+  }
+  const cloudGetMediaStatused = await api_cloudGetMediaStatus()
+  state.playState = cloudGetMediaStatused?.content.status
+  console.log(cloudGetMediaStatused, 'cloudGetMediaStatused')
+}

File diff suppressed because it is too large
+ 0 - 0
src/page-instrument/component/mode-type-mode/icon/index.json


+ 4 - 2
src/page-instrument/component/mode-type-mode/index.tsx

@@ -10,10 +10,12 @@ import { storeData } from "/src/store";
 import { studentQueryUserInfo } from "../../api";
 import { usePageVisibility } from "@vant/use";
 import GuideIndex from "../../view-figner/guide/guide-index";
+import { getQuery } from "/src/utils/queryString";
 export default defineComponent({
 	name: "modelWraper",
 
 	setup() {
+		const query = getQuery();
 		const data = reactive({
 			showPC: false,
 			showStudent: false,
@@ -96,8 +98,8 @@ export default defineComponent({
 							src={state.enableEvaluation ? icons.icon_3 : icons.icon_4}
 						/>
 					</div>
-					{data.showPC && data.showTip ? <TeacherBootom></TeacherBootom> : null}
-					{data.showStudent && data.showTip ? <StudentBottom></StudentBottom> : null}
+					{data.showPC && data.showTip && !query.isCbs ? <TeacherBootom></TeacherBootom> : null}
+					{data.showStudent && data.showTip && !query.isCbs ? <StudentBottom></StudentBottom> : null}
 					{data.showVip && <TheVip />}
 				</div>
 				{headTopData.modeType &&

+ 1 - 1
src/page-instrument/component/the-music-list/list.tsx

@@ -18,7 +18,7 @@ export default defineComponent({
 		const forms = reactive({
 			page: 1,
 			rows: 20,
-			musicSheetCategoriesId: state.musicSheetCategoriesId,
+			musicSheetCategoriesId: state.bizMusicCategoryId,
 			recentFlag: props.recentFlag ? true : null,
 			excludeMusicId: props.recentFlag ? null : state.examSongId,
 		});

+ 5 - 1
src/page-instrument/custom-plugins/guide-page/api.ts

@@ -1,8 +1,12 @@
 import request from "../../../utils/request";
 import { storeData } from "/src/store";
+import { getQuery } from "/src/utils/queryString";
+
+const query: any = getQuery();
 
 export const setGuidance = (params: any) => {
-    return request.post('/functionGuidance/save', {
+    // 内容平台无需调用该接口
+    return query.isCbs ? {} : request.post('/functionGuidance/save', {
       data: params,
       requestType: "json",
     });

+ 8 - 1
src/page-instrument/custom-plugins/helper-model/screen-model/index.tsx

@@ -7,7 +7,14 @@ export default defineComponent({
 	name: "screenModel",
 	emits: ["close"],
 	setup(props, { emit }) {
-		const origin = /(localhost|192)/.test(location.host) ? "https://test.lexiaoya.cn" : location.origin;
+		const apiUrls = {
+			'dev': 'https://dev.kt.colexiu.com',
+			'test': 'https://test.lexiaoya.cn',
+			'online': 'https://kt.colexiu.com',
+		}
+		let environment: 'dev' | 'test' | 'test2' | 'online' = location.origin.includes('//dev') ? 'dev' : location.origin.includes('//test') ? 'test' : (location.origin.includes('//online') || location.origin.includes('//kt') || location.origin.includes('//mec')) ? 'online' : 'dev'
+		const origin = /(localhost|192)/.test(location.host) ? "https://test.lexiaoya.cn" : apiUrls[environment];
+		
 		return () => (
 			<>
 				<img class={styles.closeBtn} src={iconBack} onClick={() => emit("close")} />

+ 7 - 1
src/page-instrument/custom-plugins/the-vip/index.tsx

@@ -11,6 +11,12 @@ import { getQuery } from "/src/utils/queryString";
 export default defineComponent({
 	name: "TheVip",
 	setup() {
+		const apiUrls = {
+			'dev': 'https://dev.kt.colexiu.com',
+			'test': 'https://test.lexiaoya.cn',
+			'online': 'https://kt.colexiu.com',
+		}
+		let environment: 'dev' | 'test' | 'online' = location.origin.includes('//dev') ? 'dev' : location.origin.includes('//test') ? 'test' : (location.origin.includes('//online') || location.origin.includes('//kt') || location.origin.includes('//mec')) ? 'online' : 'dev'
 		const close = () => {
 			const query = getQuery();
 			if (query.modelType){
@@ -42,7 +48,7 @@ export default defineComponent({
 								postMessage({
 									api: "openWebView",
 									content: {
-										url: `${location.origin.includes('192') ? 'https://test.lexiaoya.cn' : location.origin}/classroom-app/#/member-center`,
+										url: `${location.origin.includes('192') ? 'https://test.lexiaoya.cn' : apiUrls[environment]}/classroom-app/#/member-center`,
 										orientation: 1,
 									},
 								});

BIN
src/page-instrument/evaluat-model/delay-check/image/icon_2_3.png


+ 9 - 0
src/page-instrument/evaluat-model/delay-check/index.module.less

@@ -260,4 +260,13 @@
         line-height: 30px;
         cursor: pointer;
     }
+}
+
+.delayTest {
+    background: burlywood;
+    width: 200px;
+    height: 200px;
+    color: #000;
+    z-index: 999;
+    position: relative;
 }

+ 8 - 2
src/page-instrument/evaluat-model/delay-check/index.tsx

@@ -76,8 +76,10 @@ export default defineComponent({
 				clearTimeout(data.startTimer);
 				clearTimeout(data.stopTimer);
 				clearTimeout(startTuneTimer);
-				data.checkStatus = "init"
-				data.step = 3
+				if (data.step <= 5) {
+					data.checkStatus = "init"
+					data.step = 3
+				}
 			} else {
 				if (data.step === 3) {
 					data.step = 2
@@ -199,6 +201,9 @@ export default defineComponent({
 		};
 
 		const resetCheck = () => {
+			if (data.step > 5 && evaluatingData.accompanyErrorType === 'playError') {
+				return
+			}
 			api_toggleTune("stop");
 			clearTimeout(startTuneTimer)
 			clearTimeout(data.startAbnormalTimer);
@@ -227,6 +232,7 @@ export default defineComponent({
 			>
 				<div class={styles.delayBox}>
 					{/*返回按钮*/}
+					{/* <div class={styles.delayTest}>步骤:{data.step}{evaluatingData.accompanyErrorType}</div> */}
 					<img class={styles.delayBackBtn} src={iconBack} onClick={() => {
 						clearTimeout(startTuneTimer)
 						api_toggleTune("stop");

+ 1 - 1
src/page-instrument/evaluat-model/earphone/index.tsx

@@ -11,7 +11,7 @@ export default defineComponent({
 				<img class={styles.erji} src={icons.erji} />
 				<div class={styles.content}>
 					<div class={styles.title}>请佩戴耳机</div>
-					<div class={styles.tip}>佩戴耳机以保证测评准确率~</div>
+					<div class={styles.tip}>佩戴耳机以保证测评准确率~</div>
 					<img src={icons.erjibtn} class={styles.btn} onClick={() => emit("close")} />
 				</div>
 			</div>

+ 15 - 3
src/page-instrument/evaluat-model/evaluat-result/index.tsx

@@ -16,6 +16,7 @@ import { getQuery } from "/src/utils/queryString";
 import { browser, getBehaviorId } from "/src/utils";
 import { api_musicPracticeRecordSave } from "../../api";
 import { getAudioDuration } from "/src/view/audio-list";
+import { debounce } from "/src/utils"
 
 export default defineComponent({
 	name: "evaluatResult",
@@ -56,13 +57,24 @@ export default defineComponent({
 			data.saveLoading = false;
 		};
 
+		const saveResult = () => {
+			emit("close", "update")
+		}
+
 		onMounted(() => {
-			handleAddRecord();
+			if (!evaluatingData.isErrorState) {
+				handleAddRecord();
+			}
 		});
 
 		watch(() => evaluatingData.resulstMode, (val) => {
+			// # 9402,评测异常操作:都改为不生成评测记录
 			if (val) {
-				handleAddRecord();
+				setTimeout(() => {
+					if (!evaluatingData.isErrorState) {
+						handleAddRecord();
+					}
+				}, 0);
 			}
 		})
 		return () => (
@@ -74,7 +86,7 @@ export default defineComponent({
 				{
 				!state.isHideEvaluatReportSaveBtn &&
 				<div class={styles.headerButton}>
-					<div class={[styles.headBtn, evaluatingData.resultData.recordId ? '' : styles.disabled]} onClick={() => emit("close", "update")}>
+					<div class={[styles.headBtn, evaluatingData.resultData.recordId ? '' : styles.disabled]} onClick={debounce(saveResult,300)}>
 						保存演奏
 					</div>
 				</div>

+ 27 - 12
src/page-instrument/evaluat-model/index.tsx

@@ -1,16 +1,16 @@
 import { Transition, defineComponent, onMounted, reactive, watch } from "vue";
-import { connectWebsocket, evaluatingData, handleEndBegin, handleStartBegin, handleStartEvaluat, handleViewReport, startCheckDelay } from "/src/view/evaluating";
+import { connectWebsocket, evaluatingData, handleEndBegin, handleStartBegin, handleStartEvaluat, handleViewReport, startCheckDelay, checkUseEarphone } from "/src/view/evaluating";
 import Earphone from "./earphone";
 import styles from "./index.module.less";
 import SoundEffect from "./sound-effect";
-import state, { handleRessetState } from "/src/state";
+import state, { handleRessetState, resetPlaybackToStart, musicalInstrumentCodeInfo } from "/src/state";
 import { storeData } from "/src/store";
 import { browser } from "/src/utils";
 import { getNoteByMeasuresSlursStart } from "/src/helpers/formateMusic";
 import { Icon, Popup, showToast } from "vant";
 import EvaluatResult from "./evaluat-result";
 import EvaluatAudio from "./evaluat-audio";
-import { api_getDeviceDelay, api_openAdjustRecording, api_proxyServiceMessage, api_videoUpdate, getEarphone } from "/src/helpers/communication";
+import { api_getDeviceDelay, api_openAdjustRecording, api_proxyServiceMessage, api_videoUpdate, getEarphone, api_back } from "/src/helpers/communication";
 import EvaluatShare from "./evaluat-share";
 import { Vue3Lottie } from "vue3-lottie";
 import startData from "./data/start.json";
@@ -49,6 +49,7 @@ export default defineComponent({
     const handleDelayBack = () => {
       if (query.workRecord) {
         evaluatingData.soundEffectMode = false;
+        api_back();
       } else {
         evaluatingData.soundEffectMode = false;
         handleRessetState();
@@ -59,7 +60,7 @@ export default defineComponent({
      * 执行检测
      */
     const handlePerformDetection = async () => {
-      console.log(evaluatingData.checkStep, evaluatingData, "检测");
+      console.log(evaluatingData.checkStep, evaluatingData, "检测123");
       // 检测完成不检测了
       if (evaluatingData.checkEnd) return;
       // 延迟检测
@@ -87,6 +88,10 @@ export default defineComponent({
       }
       // 效验完成
       if (evaluatingData.checkStep === 10) {
+        const erji = await checkUseEarphone();
+        if (!erji) {
+          evaluatingData.earphoneMode = true;
+        }
         evaluatingData.checkEnd = true;
         console.log("检测结束,生成数据");
         handleConnect();
@@ -216,12 +221,12 @@ export default defineComponent({
     };
     /** 连接websocket */
     const handleConnect = async () => {
-      const behaviorId = localStorage.getItem("behaviorId") || undefined;
+      const behaviorId = localStorage.getItem("behaviorId") || localStorage.getItem("BEHAVIORID") || undefined;
       const rate = state.speed / state.originSpeed;
       calculateInfo = formatTimes()
       const content = {
         musicXmlInfos: calculateInfo.datas,
-        subjectId: state.subjectId,
+        subjectId: state.musicalCode,
         detailId: state.detailId,
         examSongId: state.examSongId,
         xmlUrl: state.xmlUrl,
@@ -230,12 +235,12 @@ export default defineComponent({
         platform: browserInfo.ios ? "IOS" : browserInfo.android ? "ANDROID" : "WEB",
         clientId: storeData.platformType === "STUDENT" ? "student" : storeData.platformType === "TEACHER" ? "teacher" : "education",
         hertz: state.setting.frequency,
-        reactionTimeMs: state.setting.reactionTimeMs,
+        reactionTimeMs: state.setting.reactionTimeMs ? Number(state.setting.reactionTimeMs) : 0,
         speed: state.speed,
         heardLevel: state.setting.evaluationDifficulty,
         // beatLength: Math.round((state.fixtime * 1000) / rate),
         beatLength: actualBeatLength,
-        evaluationCriteria: getEvaluationCriteria(),
+        evaluationCriteria: state.evaluationStandard,
       };
       await connectWebsocket(content);
       // state.playSource = "music";
@@ -269,6 +274,7 @@ export default defineComponent({
         // 再来一次
         startBtnHandle()
       }
+      resetPlaybackToStart()
       evaluatingData.resulstMode = false;
     };
 
@@ -310,6 +316,11 @@ export default defineComponent({
       if (res?.checked) {
         handleConnect();
         handleStartBegin(calculateInfo.firstNoteTime);
+        if (evaluatingData.isErrorState = true) {
+          evaluatingData.isErrorState = false;
+          evaluatingData.resulstMode = false;
+        }
+        
       }
     }
     onMounted(() => {
@@ -335,7 +346,7 @@ export default defineComponent({
           )}
         </Transition>
 
-        <div style={{ display: !evaluatingData.startBegin ? "" : "none" }} class={styles.dialogueBox} key="start">
+        <div style={{ display: !evaluatingData.startBegin && !evaluatingData.soundEffectMode ? "" : "none" }} class={styles.dialogueBox} key="start">
           <div class={styles.dialogue}>
             <img class={styles.dialoguebg} src={iconTastBg} />
             <div>演奏前请调整好乐器,保证最佳演奏状态。</div>
@@ -384,9 +395,13 @@ export default defineComponent({
 					/>
 				</Popup> */}
 
-        <Popup teleport="body" closeOnClickOverlay={false} class={["popup-custom", "van-scale"]} transition="van-scale" v-model:show={evaluatingData.resulstMode}>
-          <EvaluatResult onClose={handleEvaluatResult} />
-        </Popup>
+        {
+          
+          <Popup teleport="body" closeOnClickOverlay={false} class={["popup-custom", "van-scale"]} transition="van-scale" v-model:show={evaluatingData.resulstMode}>
+            <EvaluatResult onClose={handleEvaluatResult} />
+          </Popup>    
+        }
+
         <Popup teleport="body" closeOnClickOverlay={false} class={["popup-custom", "van-scale"]} transition="van-scale" v-model:show={evaluatModel.evaluatUpdateAudio}>
           <EvaluatAudio onClose={hanldeUpdateVideoAndAudio} />
         </Popup>

+ 15 - 7
src/page-instrument/header-top/index.module.less

@@ -4,7 +4,7 @@
     width: 100%;
     height: 100%;
     flex-shrink: 0;
-    padding: 0 10px;
+    padding: 0 20px 0 8px;
     background: var(--container-background);
     padding-bottom: 0;
     transform: translateY(-100%);
@@ -40,13 +40,13 @@
     display: flex;
     align-items: center;
     height: 100%;
-    padding: 0 11px 0 6px;
     cursor: pointer;
 
     img {
         display: block;
         width: 24px;
         height: 24px;
+        z-index: 9;
     }
 }
 
@@ -57,7 +57,7 @@
 
 .headRight {
     display: flex;
-    align-items: flex-end;
+    align-items: center;
     margin-left: auto;
     height: 100%;
 
@@ -73,11 +73,12 @@
     font-size: 10px;
     line-height: 14px;
     font-weight: 400;
-    padding: 4px 6px;
+    // padding: 4px 6px;
     border-radius: 4px;
     color: #999;
     cursor: pointer;
-
+    margin-right: 16px;
+    margin-top: -12px;
     .iconBtn {
         display: block;
         width: 25px;
@@ -86,7 +87,10 @@
 
     span {
         white-space: nowrap;
-        margin-top: 2px;
+        position: absolute;
+        left: 50%;
+        top: 27px;
+        transform: translateX(-50%);
     }
 
     .btnWrap {
@@ -104,7 +108,7 @@
         height: 85%;
     }
     .iconContent {
-        position: relative;
+        // position: relative;
         .arrowIcon {
             position: absolute;
             left: 50%;
@@ -137,6 +141,10 @@
     }     
 }
 
+.setBtn {
+    margin-right: 0;
+}
+
 .disabled {
     pointer-events: none;
     opacity: .5;

+ 22 - 16
src/page-instrument/header-top/index.tsx

@@ -44,7 +44,7 @@ export const headTopData = reactive({
       metronomeData.cursorMode = 1
     }
     if (value === 'practise') {
-      state.playIngSpeed = state.speed
+      // state.playIngSpeed = state.speed
     }
     if (value === "evaluating") {
       // 如果是pc端, 评测模式暂不可用
@@ -192,7 +192,8 @@ export default defineComponent({
       if (headTopData.modeType !== "show") return { display: false, disabled: false };
       // 评测模式 不显示,跟练模式 不显示
       if (["evaluating", "follow"].includes(state.modeType)) return { display: false, disabled: true };
-
+      // midi音频未初始化完成不可点击
+      if (state.isAppPlay && state.midiPlayIniting) return { display: true, disabled: true };
       return {
         display: true,
         disabled: false,
@@ -209,6 +210,8 @@ export default defineComponent({
       if (state.playState === "play") return { display: false, disabled: true };
       // 播放进度为0 不显示
       const currentTime = getAudioCurrentTime();
+      // midi音频未初始化完成不可点击
+      if (state.isAppPlay && state.midiPlayIniting) return { display: false, disabled: true };
       if (!currentTime) return { display: false, disabled: true };
 
       return {
@@ -380,17 +383,20 @@ export default defineComponent({
               <img style={{ display: state.playSource === "music" ? "none" : "" }} class={styles.iconBtn} src={headImg(`background.svg`)} />
               <span>{state.playSource === "music" ? "原声" : "伴奏"}</span>
             </div>
-            <div
-              class={[styles.btn]}
-              onClick={async () => {
-                metronomeData.disable = !metronomeData.disable;
-                metronomeData.metro?.initPlayer();
-              }}
-            >
-              <img style={{ display: metronomeData.disable ? "block" : "none" }} class={styles.iconBtn} src={headImg("tickoff.svg")} />
-              <img style={{ display: !metronomeData.disable ? "block" : "none" }} class={styles.iconBtn} src={headImg("tickon.svg")} />
-              <span style={{ whiteSpace: "nowrap" }}>节拍器</span>
-            </div>            
+            {
+              state.modeType !== "evaluating" && 
+                <div
+                  class={[styles.btn]}
+                  onClick={async () => {
+                    metronomeData.disable = !metronomeData.disable;
+                    metronomeData.metro?.initPlayer();
+                  }}
+                >
+                  <img style={{ display: metronomeData.disable ? "block" : "none" }} class={styles.iconBtn} src={headImg("tickoff.svg")} />
+                  <img style={{ display: !metronomeData.disable ? "block" : "none" }} class={styles.iconBtn} src={headImg("tickon.svg")} />
+                  <span style={{ whiteSpace: "nowrap" }}>节拍器</span>
+                </div>               
+            }
             <div id={state.platform === IPlatform.PC ? "teacherTop-2" : "studnetT-2"} style={{ display: selectBtn.value.display ? "" : "none" }} class={[styles.btn, selectBtn.value.disabled && styles.disabled]} onClick={() => handleChangeSection()}>
               <img style={{ display: state.section.length === 0 ? "" : "none" }} class={styles.iconBtn} src={headImg(`section0.svg`)} />
               <img style={{ display: state.section.length === 1 ? "" : "none" }} class={styles.iconBtn} src={headImg(`section1.svg`)} />
@@ -453,7 +459,7 @@ export default defineComponent({
                 }}
               </Popover> : null            
             }
-            <div id={state.platform === IPlatform.PC ? "teacherTop-6" : "studnetT-6"} style={{ display: settingBtn.value.display ? "" : "none" }} class={[styles.btn, settingBtn.value.disabled && styles.disabled]} onClick={() => (headTopData.settingMode = true)}>
+            <div id={state.platform === IPlatform.PC ? "teacherTop-6" : "studnetT-6"} style={{ display: settingBtn.value.display ? "" : "none" }} class={[styles.btn, styles.setBtn, settingBtn.value.disabled && styles.disabled]} onClick={() => (headTopData.settingMode = true)}>
               <img class={styles.iconBtn} src={headImg("icon_menu.svg")} />
               <span>设置</span>
             </div>
@@ -491,8 +497,8 @@ export default defineComponent({
         {/* 模式切换 */}
         <ModeTypeMode />
         {/* isAllBtns */}
-        {isAllBtns.value && <TeacherTop></TeacherTop>}
-        {isAllBtnsStudent.value && <StudentTop></StudentTop>}
+        {isAllBtns.value && !query.isCbs && <TeacherTop></TeacherTop>}
+        {isAllBtnsStudent.value && !query.isCbs && <StudentTop></StudentTop>}
       </>
     );
   },

+ 2 - 2
src/page-instrument/header-top/music-type/index.tsx

@@ -29,8 +29,8 @@ export default defineComponent({
 		const handleResult = (type: any) => {
 			if (type){
 				state.musicRenderType = musicTypeData.type
-				sessionStorage.setItem(musicRenderTypeKey, musicTypeData.type)
-				resetRenderMusicScore()
+				// sessionStorage.setItem(musicRenderTypeKey, musicTypeData.type)
+				resetRenderMusicScore(musicTypeData.type)
 			} else {
 				headData.musicTypeShow = false;
 				musicTypeData.type = ''

+ 3 - 1
src/page-instrument/header-top/settting/index.module.less

@@ -159,7 +159,9 @@
         border-radius: 20px;
     }
 }
-
+.sliderVolume {
+    width: 55%;
+}
 .btnsbar {
     position: absolute;
     bottom: 12px;

+ 27 - 4
src/page-instrument/header-top/settting/index.tsx

@@ -84,6 +84,7 @@ export default defineComponent({
 			}, 500);
 		};
 
+		const formatterTimeMs = (value: any) => value = String(Math.min(3000, value));
 		// 加减评测频率
 		const operateHz = (type: number) => {
 			const minFrequency = state.baseFrequency - 10, maxFrequency = state.baseFrequency + 10
@@ -92,10 +93,10 @@ export default defineComponent({
 				if (currentFrequency - 1 < minFrequency) return showToast({ message: `最低标准音高${minFrequency}HZ` })
 				currentFrequency = currentFrequency - 1
 			} else {
-				if (currentFrequency + 1 > maxFrequency) return showToast({ message: `最高标准音高${minFrequency}HZ` })
+				if (currentFrequency + 1 > maxFrequency) return showToast({ message: `最高标准音高${maxFrequency}HZ` })
 				currentFrequency = currentFrequency + 1
 			}
-			state.setting.frequency = currentFrequency
+			state.setting.frequency = currentFrequency >= 0 ? currentFrequency : 0
 		}
 		
 		return () => (
@@ -113,6 +114,26 @@ export default defineComponent({
 									extra: () => <Switch v-model={state.setting.eyeProtection}></Switch>,
 								}}
 							</Cell>
+							<Cell
+								title="节拍器音量"
+								class={styles.sliderWrap}
+								center
+							>
+								{{
+									extra: () => (
+										<Slider
+											class={[styles.slider, styles.sliderVolume]}
+											min={0}
+											max={100}
+											v-model:modelValue={state.setting.beatVolume}
+										>
+											{{
+												button: () => <div class={styles.sliderBtn}>{state.setting.beatVolume}</div>,
+											}}
+										</Slider>
+									),
+								}}
+							</Cell>							
 							<div class={styles.btnsbar}>
 								{/* <div class={styles.btn} onClick={downPng}>
 									<img src={iconDown} />
@@ -169,7 +190,6 @@ export default defineComponent({
 											v-model={state.setting.camera}
 											onChange={ async (value) => {
 												if (value) {
-													api_openCamera();
 													const res = await api_openCamera();
 													// 没有授权
 													if (res?.content?.reson) {
@@ -238,7 +258,10 @@ export default defineComponent({
 								}}
 							</Cell>
 
-							{/* <Field class={styles.reactionTime} label="反应时间(毫秒)" type="digit" v-model:modelValue={state.setting.reactionTimeMs} /> */}
+							<Field class={styles.reactionTime} label="反应时间(毫秒)" type="digit" 
+								placeholder="最大可输入3000毫秒"
+								formatter={formatterTimeMs}
+								v-model:modelValue={state.setting.reactionTimeMs} />
 						</Tab>
 					</Tabs>
 				</div>

+ 1 - 1
src/page-instrument/header-top/speed/index.tsx

@@ -47,7 +47,7 @@ export default defineComponent({
 					}}
 				</Slider>
 				<Button class={styles.btn} icon={headImg("icon_minus.svg")} disabled={state.speed == 45} onClick={minusSpeed} />
-				<Button class={styles.btn} icon={headImg("icon_speedRest.svg")} disabled={state.speed == 45} onClick={resetSpeed} />
+				<Button class={styles.btn} icon={headImg("icon_speedRest.svg")} onClick={resetSpeed} />
 			</div>
 		);
 	},

+ 2 - 2
src/page-instrument/header-top/title/index.module.less

@@ -5,11 +5,11 @@
   display: flex;
   align-items: center;
   border-radius: 18px;
-  padding: 6px;
+  padding: 6px 10px;
 
   .noticeBar {
     flex: 1;
-    padding: 0 6px;
+    padding: 0;
   }
 }
 

+ 3 - 0
src/page-instrument/theme.css

@@ -21,4 +21,7 @@ body{
     top: 0;
     right: 0;
     transform: translate(40%, -40%);
+}
+.shiyiBox > .van-icon-cross {
+    display: none !important;
 }

+ 18 - 1
src/page-instrument/view-detail/index.module.less

@@ -116,7 +116,7 @@
 
         .pcTitle {
             position: absolute;
-            left: 50%;
+            left: 20%;
             top: 50%;
             transform: translate(-50%, -50%);
 
@@ -130,4 +130,21 @@
     .headHeight.headHide {
         // margin-top: 0 !important;
     }
+}
+
+
+.preViewDetail {
+    .container {
+        height: 100%;
+        padding-bottom: 0 !important;
+        padding-right: 0 !important;
+    }
+    :global {
+        #osmdCanvasPage1 {
+            padding-bottom: 0 !important;
+        }
+        #cursorImg-0 {
+            opacity: 0 !important;
+        }
+    }
 }

+ 50 - 11
src/page-instrument/view-detail/index.tsx

@@ -27,6 +27,9 @@ import { storeData } from "/src/store";
 import ViewFigner from "../view-figner";
 import { recalculateNoteData } from "/src/view/selection";
 import ToggleMusicSheet from "/src/view/plugins/toggleMusicSheet";
+import { setCustomGradual, setCustomNoteRealValue } from "/src/helpers/customMusicScore"
+import { usePageVisibility } from "@vant/use";
+import { initMidi } from "/src/helpers/midiPlay"
 
 /**
  * 特殊教材分类id
@@ -34,6 +37,7 @@ import ToggleMusicSheet from "/src/view/plugins/toggleMusicSheet";
 export const classids = [1, 2, 6, 7, 8, 9, 3, 10, 11, 12, 13, 4, 14, 15, 16, 17, 30, 31, 35, 36, 46, 108]; // 大雅金唐, 竖笛教程, 声部训练展开的分类ID
 
 const calcCeilFrequency = (frequency: number) => {
+  if (frequency < 0) return frequency;
   if (frequency) return (frequency * 1000 * 2) / 1000;
   return 0;
 };
@@ -98,11 +102,13 @@ export default defineComponent({
       const settting = store.get("musicscoresetting");
       if (settting) {
         state.setting = settting;
+        state.setting.beatVolume = state.setting.beatVolume || 50
         if (state.setting.camera) {
           const res = await api_openCamera();
           // 没有授权
           if (res?.content?.reson) {
             state.setting.camera = false
+            store.set("musicscoresetting", state.setting);
           }
         }
       }
@@ -117,6 +123,11 @@ export default defineComponent({
     onMounted(async () => {
       (window as any).appName = "colexiu";
       const id = query.id || "43554";
+      // 如果是纯预览模式,0.65倍缩放谱面
+      state.isPreView = query.isPreView
+      if (state.isPreView) {
+        state.zoom = 0.65
+      }
       // Promise.all([sysMusicScoreAccompanimentQueryPage(id)]).then((values) => {
       //   getMusicInfo(values[0]);
       // });
@@ -133,25 +144,36 @@ export default defineComponent({
       if (state.originSpeed === 0) {
         state.originSpeed = state.speed = (osmd as any).bpm || osmd.Sheet.userStartTempoInBPM || 100;
       }
-      const saveSpeed = (store.get("speeds") || {})[state.examSongId] || (osmd as any).bpm || osmd.Sheet.userStartTempoInBPM;
+      const saveSpeed = (store.get("speeds") || {})[state.examSongId] || state.speed || (osmd as any).bpm || osmd.Sheet.userStartTempoInBPM;
       // 加载本地缓存的速度
       if (saveSpeed) {
         handleSetSpeed(saveSpeed);
       }
+      setCustomGradual();
+			setCustomNoteRealValue();
       state.times = formateTimes(osmd);
       state.times = resetFrequency(state.times);
       state.times = setNoteHalfTone(state.times);
       console.log("🚀 ~ state.times:", state.times, state.subjectId, state);
+      // 初始化midi音频信息
+      const songEndTime = state.times[state.times.length - 1 || 0]?.endtime || 0
+      if (state.isAppPlay) {
+        const durationNum = songEndTime
+        initMidi(durationNum, state.midiUrl)
+      }
+      state.measureTime = state.times[0]?.measureLength || 0
       try {
         metronomeData.metro = new Metronome();
         metronomeData.metro.init(state.times);
       } catch (error) {}
-      // 设置节拍器
-      if (state.needTick) {
+      /**
+       * 2024.1.25
+       * 设置节拍器,跟练需要播放系统节拍器,所以不需要判断needTick状态
+       */
+      // if (state.needTick) {
         const beatLengthInMilliseconds = (60 / state.speed) * 1000;
-        // console.log(state.speed, osmd?.Sheet?.SheetPlaybackSetting?.beatLengthInMilliseconds , (60 / state.speed) * 1000)
         handleInitTick(beatLengthInMilliseconds, osmd?.Sheet?.SheetPlaybackSetting?.Rhythm?.Numerator || 4);
-      }
+      // }
       api_cloudLoading();
 
       state.musicRendered = true;
@@ -246,6 +268,19 @@ export default defineComponent({
         );
       }
     );
+    const pageVisible = usePageVisibility();
+    watch(
+      () => pageVisible.value,
+      (val) => {
+        if (val === "hidden") {
+          // 如果是播放状态,需要暂停播放
+          // console.log("页面隐藏停止播放");
+          if (state.playState === "play") {
+            togglePlay("paused");
+          }
+        }
+      }
+    );    
     onMounted(() => {
       window.addEventListener("resize", resetMusicScore);
     });
@@ -287,7 +322,7 @@ export default defineComponent({
     };
     return () => (
       <div
-        class={[styles.detail, state.setting.eyeProtection && "eyeProtection", state.platform === IPlatform.PC && styles.PC]}
+        class={[styles.detail, state.setting.eyeProtection && "eyeProtection", state.platform === IPlatform.PC && styles.PC, state.isPreView && styles.preViewDetail]}
         style={{
           paddingLeft: detailData.paddingLeft,
           background: state.setting.camera ? `rgba(${state.setting.eyeProtection ? "253,244,229" : "255,255,255"} ,${state.setting.cameraOpacity / 100}) !important` : "",
@@ -300,7 +335,10 @@ export default defineComponent({
             </div>
           )}
         </Transition>
-        <div class={[styles.headHeight, detailData.headerHide && styles.headHide]}>{state.musicRendered && <HeaderTop />}</div>
+        {
+          !state.isPreView && 
+          <div class={[styles.headHeight, detailData.headerHide && styles.headHide]}>{state.musicRendered && <HeaderTop />}</div>
+        }
         <div
           id="scrollContainer"
           style={{ ...fingerConfig.value.container, height: detailData.headerHide ? "100vh" : "" }}
@@ -316,7 +354,7 @@ export default defineComponent({
           {!detailData.isLoading && <MusicScore onRendered={handleRendered} />}
 
           {/* 指法 */}
-          {state.setting.displayFingering && state.fingeringInfo?.name && (
+          {state.setting.displayFingering && state.fingeringInfo?.name && !state.isPreView &&  (
             <div style={{ ...fingerConfig.value.fingerBox }}>
               <Fingering
                 style={{
@@ -328,8 +366,9 @@ export default defineComponent({
           )}
         </div>
 
-        {/* 节拍器 */}
-        {state.needTick && <Tick />}
+        {/* 节拍器,跟练需要播放系统节拍器,所以不需要判断needTick状态 */}
+        {/* {state.needTick && <Tick />} */}
+        <Tick />
 
         {/* 播放 */}
         {!detailData.isLoading && <AudioList />}
@@ -353,7 +392,7 @@ export default defineComponent({
         {/* 切换曲谱 */}
         {!query.lessonTrainingId && !query.questionId && state.isConcert && <ToggleMusicSheet />}
 
-        {state.musicRendered && (
+        {state.musicRendered && !state.isPreView && (
           <>
             {/* 统计训练时长 */}
             {storeData.isApp && <RecordingTime />}

+ 17 - 0
src/page-instrument/view-evaluat-report/component/note/bottomArrow.tsx

@@ -0,0 +1,17 @@
+import { defineComponent } from "vue";
+
+export default defineComponent({
+	name: "ArrowSvg",
+	props: {
+		fill: String,
+	},
+	render() {
+		return (
+			<svg id="bottomSvg" width="15px" height="10px" viewBox="0 0 10 7" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+				<g id="页面-223" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+					<path d="M1.61932444,0 L8.38067556,0 C8.84091285,1.06606835e-15 9.2140089,0.373096042 9.2140089,0.833333333 C9.2140089,1.00701915 9.15973962,1.17636453 9.05878679,1.31769849 L5.67811123,6.05064428 C5.41060373,6.42515477 4.89014533,6.51189784 4.51563484,6.24439035 C4.44080524,6.19094063 4.37533849,6.12547388 4.32188877,6.05064428 L0.941213211,1.31769849 C0.673705719,0.943188006 0.760448786,0.422729599 1.13495928,0.155222107 C1.27629324,0.0542692786 1.44563862,-3.01161341e-16 1.61932444,0 Z" id="下" fill="#FF9200" transform="translate(5.000000, 3.500000) scale(1, -1) rotate(-180.000000) translate(-5.000000, -3.500000) "></path>
+				</g>
+			</svg>
+		);
+	},
+});

+ 17 - 0
src/page-instrument/view-evaluat-report/component/note/leftArrow.tsx

@@ -0,0 +1,17 @@
+import { defineComponent } from "vue";
+
+export default defineComponent({
+	name: "ArrowSvg",
+	props: {
+		fill: String,
+	},
+	render() {
+		return (
+			<svg id="leftSvg" width="15px" height="10px" viewBox="0 0 10 7" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+				<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+					<path d="M0.119324437,1.5 L6.88067556,1.5 C7.34091285,1.5 7.7140089,1.87309604 7.7140089,2.33333333 C7.7140089,2.50701915 7.65973962,2.67636453 7.55878679,2.81769849 L4.17811123,7.55064428 C3.91060373,7.92515477 3.39014533,8.01189784 3.01563484,7.74439035 C2.94080524,7.69094063 2.87533849,7.62547388 2.82188877,7.55064428 L-0.558786789,2.81769849 C-0.826294281,2.44318801 -0.739551214,1.9227296 -0.365040725,1.65522211 C-0.223706765,1.55426928 -0.0543613774,1.5 0.119324437,1.5 Z" id="左" fill="#FF9200" transform="translate(3.500000, 5.000000) scale(-1, -1) rotate(-90.000000) translate(-3.500000, -5.000000) "></path>
+				</g>
+			</svg>
+		);
+	},
+});

+ 17 - 0
src/page-instrument/view-evaluat-report/component/note/rightArrow.tsx

@@ -0,0 +1,17 @@
+import { defineComponent } from "vue";
+
+export default defineComponent({
+	name: "ArrowSvg",
+	props: {
+		fill: String,
+	},
+	render() {
+		return (
+			<svg id="rightSvg" width="15px" height="10px" viewBox="0 0 10 7" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+				<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+					<path d="M0.119324437,1.5 L6.88067556,1.5 C7.34091285,1.5 7.7140089,1.87309604 7.7140089,2.33333333 C7.7140089,2.50701915 7.65973962,2.67636453 7.55878679,2.81769849 L4.17811123,7.55064428 C3.91060373,7.92515477 3.39014533,8.01189784 3.01563484,7.74439035 C2.94080524,7.69094063 2.87533849,7.62547388 2.82188877,7.55064428 L-0.558786789,2.81769849 C-0.826294281,2.44318801 -0.739551214,1.9227296 -0.365040725,1.65522211 C-0.223706765,1.55426928 -0.0543613774,1.5 0.119324437,1.5 Z" id="右" fill="#FF9200" transform="translate(3.500000, 5.000000) scale(1, -1) rotate(-90.000000) translate(-3.500000, -5.000000) "></path>
+				</g>
+			</svg>
+		);
+	},
+});

+ 17 - 0
src/page-instrument/view-evaluat-report/component/note/topArrow.tsx

@@ -0,0 +1,17 @@
+import { defineComponent } from "vue";
+
+export default defineComponent({
+	name: "ArrowSvg",
+	props: {
+		fill: String,
+	},
+	render() {
+		return (
+			<svg id="topSvg" width="15px" height="10px" viewBox="0 0 10 7" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+				<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+					<path d="M1.61932444,0 L8.38067556,0 C8.84091285,1.06606835e-15 9.2140089,0.373096042 9.2140089,0.833333333 C9.2140089,1.00701915 9.15973962,1.17636453 9.05878679,1.31769849 L5.67811123,6.05064428 C5.41060373,6.42515477 4.89014533,6.51189784 4.51563484,6.24439035 C4.44080524,6.19094063 4.37533849,6.12547388 4.32188877,6.05064428 L0.941213211,1.31769849 C0.673705719,0.943188006 0.760448786,0.422729599 1.13495928,0.155222107 C1.27629324,0.0542692786 1.44563862,-3.01161341e-16 1.61932444,0 Z" id="上" fill="#FF9200" transform="translate(5.000000, 3.500000) scale(1, -1) translate(-5.000000, -3.500000) "></path>
+				</g>
+			</svg>
+		);
+	},
+});

+ 16 - 0
src/page-instrument/view-evaluat-report/component/share-top/image/first-bottom.svg

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="12px" height="19px" viewBox="0 0 12 19" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>编组 15</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="评测报告(音准)" transform="translate(-385.000000, -94.000000)" fill="#FF9200">
+            <g id="编组-9" transform="translate(283.000000, 82.000000)">
+                <g id="编组-11备份" transform="translate(102.000000, 11.000000)">
+                    <g id="编组-15" transform="translate(0.000000, 1.000000)">
+                        <rect id="矩形" x="0" y="0" width="12" height="12" rx="3"></rect>
+                        <path d="M3.56703087,14 L8.43296913,14 C8.73979399,14 8.98852469,14.2487307 8.98852469,14.5555556 C8.98852469,14.6695401 8.9534638,14.780766 8.88809798,14.8741457 L6.45512884,18.3498159 C6.27917634,18.6011767 5.93277053,18.6623071 5.68140981,18.4863546 C5.62827574,18.4491607 5.582065,18.40295 5.54487116,18.3498159 L3.11190202,14.8741457 C2.93594952,14.622785 2.99707996,14.2763792 3.24844068,14.1004267 C3.34182042,14.0350609 3.45304637,14 3.56703087,14 Z" id="矩形" transform="translate(6.000000, 16.500000) scale(1, -1) rotate(-180.000000) translate(-6.000000, -16.500000) "></path>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 13 - 0
src/page-instrument/view-evaluat-report/component/share-top/image/first-correct.svg

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="12px" height="12px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>矩形</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="评测报告(节奏)" transform="translate(-557.000000, -94.000000)" fill="#2ABC6F">
+            <g id="编组-9" transform="translate(355.000000, 82.000000)">
+                <g id="编组-10" transform="translate(202.000000, 11.000000)">
+                    <rect id="矩形" x="0" y="1" width="12" height="12" rx="3"></rect>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 13 - 0
src/page-instrument/view-evaluat-report/component/share-top/image/first-error.svg

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="12px" height="12px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>矩形</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="评测报告(节奏)" transform="translate(-643.000000, -94.000000)" fill="#FF2B29">
+            <g id="编组-9" transform="translate(355.000000, 82.000000)">
+                <g id="编组-10备份-3" transform="translate(288.000000, 11.000000)">
+                    <rect id="矩形" x="0" y="1" width="12" height="12" rx="3"></rect>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 13 - 0
src/page-instrument/view-evaluat-report/component/share-top/image/first-lack.svg

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="12px" height="12px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>矩形</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="评测报告(音准)" transform="translate(-643.000000, -94.000000)" fill="#8F4EFB">
+            <g id="编组-9" transform="translate(283.000000, 82.000000)">
+                <g id="编组-4" transform="translate(360.000000, 11.000000)">
+                    <rect id="矩形" x="0" y="1" width="12" height="12" rx="3"></rect>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 16 - 0
src/page-instrument/view-evaluat-report/component/share-top/image/first-left.svg

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="19px" height="12px" viewBox="0 0 19 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>编组 13</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="评测报告(节奏)" transform="translate(-371.000000, -94.000000)" fill="#FF9200">
+            <g id="编组-9" transform="translate(355.000000, 82.000000)">
+                <g id="编组-11备份" transform="translate(16.000000, 11.000000)">
+                    <g id="编组-13" transform="translate(0.000000, 1.000000)">
+                        <path d="M0.0670308675,3.5 L4.93296913,3.5 C5.23979399,3.5 5.48852469,3.74873069 5.48852469,4.05555556 C5.48852469,4.16954006 5.4534638,4.280766 5.38809798,4.37414575 L2.95512884,7.84981594 C2.77917634,8.10117666 2.43277053,8.16230709 2.18140981,7.98635459 C2.12827574,7.94916074 2.082065,7.90295 2.04487116,7.84981594 L-0.388097977,4.37414575 C-0.564050481,4.12278503 -0.502920044,3.77637921 -0.251559324,3.60042671 C-0.158179581,3.53506089 -0.0469536347,3.5 0.0670308675,3.5 Z" id="矩形" transform="translate(2.500000, 6.000000) scale(1, -1) rotate(-270.000000) translate(-2.500000, -6.000000) "></path>
+                        <rect id="矩形" x="7" y="0" width="12" height="12" rx="3"></rect>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 13 - 0
src/page-instrument/view-evaluat-report/component/share-top/image/first-not.svg

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="12px" height="12px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>矩形</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="评测报告(节奏)" transform="translate(-729.000000, -94.000000)" fill="#ADADAD">
+            <g id="编组-9" transform="translate(355.000000, 82.000000)">
+                <g id="编组-10备份" transform="translate(374.000000, 11.000000)">
+                    <rect id="矩形" x="0" y="1" width="12" height="12" rx="3"></rect>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 16 - 0
src/page-instrument/view-evaluat-report/component/share-top/image/first-right.svg

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="19px" height="12px" viewBox="0 0 19 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>编组 12</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="评测报告(节奏)" transform="translate(-464.000000, -94.000000)" fill="#FF9200">
+            <g id="编组-9" transform="translate(355.000000, 82.000000)">
+                <g id="编组-11" transform="translate(109.000000, 11.000000)">
+                    <g id="编组-12" transform="translate(0.000000, 1.000000)">
+                        <path d="M14.0670309,3.5 L18.9329691,3.5 C19.239794,3.5 19.4885247,3.74873069 19.4885247,4.05555556 C19.4885247,4.16954006 19.4534638,4.280766 19.388098,4.37414575 L16.9551288,7.84981594 C16.7791763,8.10117666 16.4327705,8.16230709 16.1814098,7.98635459 C16.1282757,7.94916074 16.082065,7.90295 16.0448712,7.84981594 L13.611902,4.37414575 C13.4359495,4.12278503 13.49708,3.77637921 13.7484407,3.60042671 C13.8418204,3.53506089 13.9530464,3.5 14.0670309,3.5 Z" id="矩形" transform="translate(16.500000, 6.000000) scale(1, -1) rotate(-90.000000) translate(-16.500000, -6.000000) "></path>
+                        <rect id="矩形" x="0" y="0" width="12" height="12" rx="3"></rect>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 14 - 0
src/page-instrument/view-evaluat-report/component/share-top/image/first-top.svg

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="12px" height="19px" viewBox="0 0 12 19" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>编组 13</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="评测报告(音准)" transform="translate(-299.000000, -87.000000)" fill="#FF9200">
+            <g id="编组-9" transform="translate(283.000000, 82.000000)">
+                <g id="编组-13" transform="translate(16.000000, 5.000000)">
+                    <rect id="矩形" x="0" y="7" width="12" height="12" rx="3"></rect>
+                    <path d="M3.56703087,0 L8.43296913,0 C8.73979399,7.10712233e-16 8.98852469,0.248730695 8.98852469,0.555555556 C8.98852469,0.669540058 8.9534638,0.780766004 8.88809798,0.874145747 L6.45512884,4.34981594 C6.27917634,4.60117666 5.93277053,4.66230709 5.68140981,4.48635459 C5.62827574,4.44916074 5.582065,4.40295 5.54487116,4.34981594 L3.11190202,0.874145747 C2.93594952,0.622785027 2.99707996,0.276379215 3.24844068,0.100426711 C3.34182042,0.035060891 3.45304637,4.65027823e-16 3.56703087,0 Z" id="矩形" transform="translate(6.000000, 2.500000) scale(1, -1) translate(-6.000000, -2.500000) "></path>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 4 - 2
src/page-instrument/view-evaluat-report/component/share-top/image/icon-back.svg

@@ -2,8 +2,10 @@
 <svg width="15px" height="24px" viewBox="0 0 15 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
     <title>形状</title>
     <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
-        <g id="评测报告(音准)" transform="translate(-40.000000, -27.000000)" fill="#494949" fill-rule="nonzero" stroke="#494949">
-            <path d="M42.2681508,37.7499394 C42.5890877,37.7499394 42.9114702,37.8699082 43.168798,38.0950183 L53.620931,47.6520856 C53.8607785,47.8704624 53.997204,48.1690827 53.9999596,48.4816468 C54.0026326,48.7942108 53.8714111,49.0948519 53.6353876,49.3168215 C53.138464,49.7783404 52.3366638,49.7837362 51.8326476,49.3289532 L41.379069,39.7718859 C41.1392215,39.5535091 41.002796,39.2548888 41.0000404,38.9423247 C40.9973673,38.6297606 41.1285889,38.3291196 41.3646123,38.1071499 C41.5948722,37.8710256 41.9246144,37.7409471 42.2667051,37.7512873 L42.2681508,37.7499394 Z M52.7106158,28 C53.0356188,28 53.3591643,28.117702 53.6185837,28.3424057 C53.8599334,28.5590403 53.9971822,28.8550613 53.9999596,29.1648752 C54.0026536,29.474689 53.8706502,29.7727125 53.6331578,29.9929083 L43.1980856,39.5695682 C42.6976948,40.027014 41.8904764,40.0329595 41.3821498,39.5829434 C41.1403528,39.3662582 41.0028184,39.0699512 41.0000404,38.7598084 C40.9973456,38.4496656 41.1296337,38.1513535 41.3675757,37.9311033 L51.8026479,28.3544434 C52.0474932,28.117702 52.3856129,28 52.7106158,28 Z" id="形状"></path>
+        <g id="评测报告(音准)" transform="translate(-23.000000, -23.000000)" fill="#494949" fill-rule="nonzero" stroke="#494949">
+            <g id="编组-14">
+                <path d="M25.2681508,33.7499394 C25.5890877,33.7499394 25.9114702,33.8699082 26.168798,34.0950183 L36.620931,43.6520856 C36.8607785,43.8704624 36.997204,44.1690827 36.9999596,44.4816468 C37.0026326,44.7942108 36.8714111,45.0948519 36.6353876,45.3168215 C36.138464,45.7783404 35.3366638,45.7837362 34.8326476,45.3289532 L24.379069,35.7718859 C24.1392215,35.5535091 24.002796,35.2548888 24.0000404,34.9423247 C23.9973673,34.6297606 24.1285889,34.3291196 24.3646123,34.1071499 C24.5948722,33.8710256 24.9246144,33.7409471 25.2667051,33.7512873 L25.2681508,33.7499394 Z M35.7106158,24 C36.0356188,24 36.3591643,24.117702 36.6185837,24.3424057 C36.8599334,24.5590403 36.9971822,24.8550613 36.9999596,25.1648752 C37.0026536,25.474689 36.8706502,25.7727125 36.6331578,25.9929083 L26.1980856,35.5695682 C25.6976948,36.027014 24.8904764,36.0329595 24.3821498,35.5829434 C24.1403528,35.3662582 24.0028184,35.0699512 24.0000404,34.7598084 C23.9973456,34.4496656 24.1296337,34.1513535 24.3675757,33.9311033 L34.8026479,24.3544434 C35.0474932,24.117702 35.3856129,24 35.7106158,24 Z" id="形状"></path>
+            </g>
         </g>
     </g>
 </svg>

+ 4 - 4
src/page-instrument/view-evaluat-report/component/share-top/image/icon-shiyi.svg

@@ -2,10 +2,10 @@
 <svg width="33px" height="33px" viewBox="0 0 33 33" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
     <title>button-normal备份 2</title>
     <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
-        <g id="评测报告(音准)" transform="translate(-690.000000, -13.000000)">
-            <g id="button-normal备份-2" transform="translate(690.000000, 13.000000)">
-                <rect id="button-normal" fill="#FFEFE5" x="0" y="0" width="33" height="33" rx="16.5"></rect>
-                <g id="编组" transform="translate(7.000000, 7.000000)" fill="#FF8800" fill-rule="nonzero" stroke="#FF8800">
+        <g id="评测报告(音准)" transform="translate(-755.000000, -11.000000)">
+            <g id="button-normal备份-2" transform="translate(755.000000, 11.000000)">
+                <rect id="button-normal" fill="#FFE5E7" x="0" y="0" width="33" height="33" rx="16.5"></rect>
+                <g id="编组" transform="translate(7.000000, 7.000000)" fill="#FF6173" fill-rule="nonzero" stroke="#FF6173">
                     <path d="M9.5,0 C4.25322823,0 0,4.25324263 0,9.5 C0,14.7467574 4.25324678,19 9.5,19 C14.7467532,19 19,14.7467574 19,9.5 C19,4.25324263 14.7467347,0 9.5,0 Z M9.49999096,17.5000093 C5.08224075,17.5000093 1.50001028,13.9181622 1.50001028,9.50000928 C1.50001028,5.08182021 5.08222266,1.50000928 9.49999096,1.50000928 C13.9185552,1.50000928 17.4999897,5.08182019 17.4999897,9.50000928 C17.4999897,13.9181622 13.9185552,17.5000093 9.49999096,17.5000093 Z" id="形状"></path>
                     <path d="M9.60242663,4.40820056 C8.69574736,4.40820056 7.97469307,4.6677898 7.4389318,5.18715088 C6.90315395,5.69822914 6.63526502,6.415299 6.63526502,7.33837705 L8.04487302,7.33837705 C8.04487302,6.81089915 8.14788552,6.40699959 8.35389392,6.12666179 C8.58443543,5.78879242 8.97191911,5.61974983 9.51601299,5.61974983 C9.94462863,5.61974983 10.2743981,5.73539395 10.5053048,5.96591863 C10.7358463,6.21324134 10.8512913,6.53471105 10.8512913,6.9303112 C10.8512913,7.24366409 10.739996,7.53608583 10.5173721,7.8081242 L10.3691112,7.9813165 C9.56127805,8.69838636 9.0790813,9.22189715 8.9224877,9.55148369 C8.75761128,9.87295341 8.67534734,10.2768696 8.67534736,10.763033 L8.67534736,10.9362087 L10.0970393,10.9362087 L10.0970393,10.763033 C10.0970393,10.4745617 10.1589364,10.2066731 10.2826809,9.95935037 C10.3977774,9.72050967 10.5626538,9.51431889 10.7769782,9.34134239 C11.3538881,8.83856355 11.7085558,8.51294411 11.8402179,8.36446749 C12.1450891,7.96075053 12.2978816,7.45383858 12.2978816,6.84373165 C12.2978816,6.08551327 12.050791,5.49202176 11.5561285,5.06338994 C11.0697654,4.62665788 10.4185259,4.40820056 9.60242663,4.40820056 Z M8.69988046,12.8015559 C8.51877034,12.9747482 8.42784181,13.1971561 8.42784181,13.4692111 C8.42784181,13.7412494 8.51877034,13.9638565 8.69988046,14.1368496 C8.8979878,14.3183413 9.1243464,14.4088879 9.38018456,14.4088879 C9.6522066,14.4088879 9.87859838,14.3223084 10.0600737,14.1493153 C10.2494998,13.976123 10.3445946,13.7495489 10.3445946,13.4692111 C10.3445946,13.2056547 10.2536329,12.9830477 10.0725228,12.8015559 C9.89104754,12.6285628 9.66015745,12.5419667 9.38018456,12.5419667 C9.11606357,12.5419667 8.88967177,12.6285628 8.69988046,12.8015559 Z" id="形状"></path>
                 </g>

+ 15 - 0
src/page-instrument/view-evaluat-report/component/share-top/image/shiyi-close.svg

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>关闭</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="评测报告-图标释义" transform="translate(-660.000000, -27.000000)">
+            <g id="关闭" transform="translate(661.000000, 28.000000)">
+                <circle id="椭圆形" stroke="#FFFFFF" stroke-width="1.22367143" stroke-linecap="round" stroke-linejoin="round" cx="15" cy="15" r="15"></circle>
+                <g id="编组-8" transform="translate(14.693878, 15.306122) rotate(-315.000000) translate(-14.693878, -15.306122) translate(7.346939, 7.959184)" fill="#FFFFFF">
+                    <path d="M0.918367347,6.42857143 L13.7755102,6.42857143 C14.2827105,6.42857143 14.6938776,6.8397385 14.6938776,7.34693878 C14.6938776,7.85413906 14.2827105,8.26530612 13.7755102,8.26530612 L0.918367347,8.26530612 C0.411167066,8.26530612 -1.09084133e-11,7.85413906 -1.09084755e-11,7.34693878 C-1.09085376e-11,6.8397385 0.411167066,6.42857143 0.918367347,6.42857143 Z" id="矩形" transform="translate(7.346939, 7.346939) rotate(-270.000000) translate(-7.346939, -7.346939) "></path>
+                    <rect id="矩形" transform="translate(7.346939, 7.346939) rotate(-180.000000) translate(-7.346939, -7.346939) " x="8.69251253e-13" y="6.42857143" width="14.6938776" height="1.83673469" rx="0.918367347"></rect>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

BIN
src/page-instrument/view-evaluat-report/component/share-top/image/shiyi-top.png


+ 131 - 11
src/page-instrument/view-evaluat-report/component/share-top/index.module.less

@@ -5,8 +5,9 @@
     width: 100%;
     height: 100%;
     flex-shrink: 0;
-    padding: 8px 10px;
+    padding: 10px 22px;
     background-color: #fff;
+    position: relative;
 }
 
 .android {
@@ -17,7 +18,7 @@
     display: flex;
     justify-content: center;
     align-items: center;
-    padding: 0 30px;
+    padding-right: 14px;
     height: 100%;
 
     img {
@@ -27,12 +28,32 @@
     }
 }
 .disabled{
-    opacity: 0;
+    //opacity: 0;
     pointer-events: none;
 }
 .left {
     display: flex;
     align-items: center;
+    .leftContent {
+        .lcName {
+            font-size: 18px;
+            font-weight: 600;
+            color: #000;
+            line-height: 25px;
+            margin-bottom: 2px;
+            padding: 0 !important;
+            :global{
+                .van-notice-bar{
+                    padding: 0 !important;
+                }
+              }
+        }
+        .lcScore {
+            font-size: 12px;
+            color: #777777;
+            line-height: 18px;
+        }
+    }
 }
 
 .center {
@@ -77,6 +98,46 @@
     }
 }
 
+.middle {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 60%;
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%,-50%);
+    .cItem {
+        width: 64px;
+        height: 50px;
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        justify-content: space-between;
+        padding: 4px 0;
+        margin: 0 6px;
+        cursor: pointer;
+        .mScore {
+            font-size: 16px;
+            line-height: 22px;
+            color: #AAAAAA;
+        }
+        .mLabel {
+            font-size: 12px;
+            line-height: 18px;
+            color: #AAAAAA;
+        }
+    }
+    .active {
+        background: #CBEEFF;
+        border-radius: 8px;
+        .mScore, .mLabel {
+            color: #000000;
+            font-weight: 600;
+        }
+    }
+}
+
 .right {
     display: flex;
     align-items: center;
@@ -90,7 +151,7 @@
         font-size: 10px;
         line-height: 14px;
         font-weight: 400;
-        padding: 0 6px;
+        padding: 0 10px;
         color: #999;
 
         .iconBtn {
@@ -161,29 +222,47 @@
     display: flex;
     justify-content: flex-end;
     align-items: center;
-    height: 36px;
-    padding: 0 14px;
+    padding: 0 16px;
+    height: 30px;
     border-radius: 18px;
     background-color: rgba(255,255,255, .9);
     z-index: 1;
+    box-sizing: content-box;
 
     &>div {
         display: flex;
         align-items: center;
-        margin-right: 6px;
-
+        margin-right: 16px;
+        &:last-child {
+            margin-right: 0;
+        }
         &>span {
             margin-left: 4px;
         }
     }
 }
-
+.shiyiClose {
+    width: 30px;
+    height: 30px;
+    position: absolute;
+    right: -38px;
+    top: -26px;
+    cursor: pointer;
+}
 .shiyiPopup{
     background: #fff;
     border-radius: 20px;
     width: 80vw;
-    max-width: 420px;
+    max-width: 460px;
     padding: 20px;
+    position: relative;
+    .shiyiTop {
+        position: absolute;
+        width: 154px;
+        left: 50%;
+        top: -6px;
+        transform: translateX(-50%);
+    }
 }
 .shiyiTitle{
     font-size: 16px;
@@ -194,16 +273,57 @@
 .items {
     display: flex;
     flex-wrap: wrap;
+    margin-top: 16px;
     .item{
         width: 50%;
         display: flex;
         align-items: center;
-        padding: 7px 0;
+        padding: 12px 0 12px 6px;
         span{
             margin-left: 12px;
+            font-size: 12px;
+            font-weight: 400;
         }
         svg {
             visibility: visible;
         }
+        &:nth-child(2n) {
+            transform: translateX(20px);
+        }
+    }
+    .itemTone {
+        width: 50%;
+        display: flex;
+        align-items: center;
+        padding: 16px 0 16px 26px;
+        position: relative;
+        &:nth-child(2n) {
+            transform: translateX(20px);
+        }
+        .firstIcon1 {
+            width: 12px;
+            height: 20px;
+        }
+        .firstIcon2 {
+            width: 19px;
+            height: 13px;
+        }
+        .firstIcon3 {
+            width: 12px;
+            height: 13px;
+        }
+        img {
+            position: absolute;
+            left: 0;
+            top: 50%;
+            transform: translateY(-50%);
+        }
+        .fiz {
+            left: -5px;
+        }
+        span {
+            font-size: 12px;
+            font-weight: 400;
+        }
     }
 }

+ 249 - 75
src/page-instrument/view-evaluat-report/component/share-top/index.tsx

@@ -6,6 +6,16 @@ import state from "/src/state";
 import iconBack from "./image/icon-back.svg";
 import iconShiyi from "./image/icon-shiyi.svg";
 import iconhuifang from "./image/icon-huifang.svg";
+import shiyiTop from "./image/shiyi-top.png";
+import shiyiClose from "./image/shiyi-close.svg";
+import firstLeft from "./image/first-left.svg";
+import firstRight from "./image/first-right.svg";
+import firstTop from "./image/first-top.svg";
+import firstBottom from "./image/first-bottom.svg";
+import firstCorrect from "./image/first-correct.svg";
+import firstError from "./image/first-error.svg";
+import firstNot from "./image/first-not.svg";
+import firstLack from "./image/first-lack.svg";
 import { Grid, GridItem, Popup } from "vant";
 import videobg from "./image/videobg.png";
 import "plyr/dist/plyr.css";
@@ -13,6 +23,7 @@ import Plyr from "plyr";
 import { browser } from "/src/utils";
 import Note from "../note";
 import { storeData } from "/src/store";
+import Title from "/src/page-instrument/header-top/title";
 
 type IItemType = "intonation" | "cadence" | "integrity";
 
@@ -24,7 +35,7 @@ export default defineComponent({
 			default: () => ({}),
 		},
 	},
-	setup(props) {
+	setup(props, {expose}) {
 		const browserInfo = browser();
 		const { scoreData } = toRefs(props);
 		const shareData = reactive({
@@ -47,6 +58,7 @@ export default defineComponent({
 
 		const handleChange = (type: IItemType) => {
 			itemType.value = type;
+			scoreData.value.itemType = type
 		};
 
 		// 资源类型
@@ -70,13 +82,50 @@ export default defineComponent({
 				shareData.isInitPlyr = true;
 			});
 		};
-
 		return () => (
 			<div class={[styles.headerTop, browserInfo.android && styles.android]}>
-				<div class={[styles.back, !storeData.isApp && styles.disabled]} onClick={handleBack}>
-					<img src={iconBack} />
+				<div class={styles.left}>
+					<div class={[styles.back, !storeData.isApp && styles.disabled]} onClick={handleBack}>
+						<img src={iconBack} />
+					</div>
+					<div class={styles.leftContent}>
+						{/* <div class={styles.lcName}>{state.examSongName}</div> */}
+						<Title class={styles.lcName} text={state.examSongName} rightView={false} />
+						<div class={styles.lcScore}>{level[scoreData.value.heardLevel]}|综合分数:{scoreData.value.score}分</div>
+					</div>
 				</div>
-				<div class={styles.center}>
+
+				{/* 音准、节奏、完整度纬度 */}
+				
+
+				<div class={styles.middle}>
+					{
+					 	state.isPercussion ? null : 
+						<div 
+							onClick={() => handleChange("intonation")}
+							class={[styles.cItem, itemType.value === "intonation" && styles.active]}>
+							<span class={styles.mScore}>{scoreData.value.intonation}分</span>
+							<span class={styles.mLabel}>音准</span>
+						</div>					 
+					}
+					<div
+						onClick={() => handleChange("cadence")}
+						class={[styles.cItem, itemType.value === "cadence" && styles.active]}>
+						<span class={styles.mScore}>{scoreData.value.cadence}分</span>
+						<span class={styles.mLabel}>节奏</span>
+					</div>
+					{
+						state.isPercussion ? null : 
+						<div						
+							onClick={() => handleChange("integrity")}
+							class={[styles.cItem, itemType.value === "integrity" && styles.active]}>
+							<span class={styles.mScore}>{scoreData.value.integrity}分</span>
+							<span class={styles.mLabel}>完成度</span>
+						</div>
+					}
+				</div>
+
+				{/* <div class={styles.center}>
 					<div class={styles.cItem}>
 						<div>{level[scoreData.value.heardLevel]}</div>
 						<div>难度</div>
@@ -110,7 +159,8 @@ export default defineComponent({
 							</div>
 						</>
 					)}
-				</div>
+				</div> */}
+
 				<div class={styles.right}>
 					<div
 						style={{ display: scoreData.value.videoFilePath ? "" : "none" }}
@@ -130,48 +180,130 @@ export default defineComponent({
 					</div> */}
 				</div>
 
-				{state.isPercussion ? null : (
-					<div class={styles.demos}>
-						<div>
-							<Note fill="#01C1B5" />
-							<span>演奏正确</span>
-						</div>
-						{itemType.value === "intonation" && (
-							<>
+				{/* 五线谱,简谱类型提示  */}
+				{
+					scoreData.value.musicType === 'staff' ? 
+					<>
+					{state.isPercussion ? null : (
+						<div class={styles.demos}>
+							{itemType.value === "intonation" && (
+								<>
+									<div>
+										<Note fill="rgba(42, 188, 111, 1)" shadowFill="#FFAB25" shadow x={-2} y={0} />
+										<span>演奏偏高</span>
+									</div>
+									<div>
+										<Note fill="rgba(42, 188, 111, 1)" shadowFill="#FFAB25" shadow x={-1} y={-3} />
+										<span>演奏偏低</span>
+									</div>
+								</>
+							)}
+							{itemType.value === "cadence" && (
+								<>
+									<div>
+										<Note fill="rgba(42, 188, 111, 1)" shadowFill="#FFAB25" shadow x={0.5} y={-1} />
+										<span>节奏偏快</span>
+									</div>
+									<div>
+										<Note fill="rgba(42, 188, 111, 1)" shadowFill="#FFAB25" shadow x={-3} y={-2.5} />
+										<span>演奏偏低</span>
+									</div>
+								</>
+							)}		
+							{(itemType.value === "intonation" || itemType.value === "cadence") && (
+								<>										
+									<div>
+										<Note fill="#2ABC6F" />
+										<span>演奏正确</span>
+									</div>
+									<div>
+										<Note fill="#FF2B29" />
+										<span>演奏错误</span>
+									</div>							
+								</>
+							)}
+
+							{(itemType.value === "intonation" || itemType.value === "integrity") && (
 								<div>
-									<Note fill="rgba(1, 193, 181, .8)" shadowFill="#FFAB25" shadow x={-2} y={0} />
-									<span>音高了</span>
+									<Note fill="#8F4EFB" />
+									<span>时值不足</span>
 								</div>
+							)}
+							{
+								itemType.value === "integrity" && 
 								<div>
-									<Note fill="rgba(1, 193, 181, .8)" shadowFill="#FFAB25" shadow x={-1} y={-3} />
-									<span>音低了</span>
-								</div>
-							</>
-						)}
-						{itemType.value === "cadence" && (
-							<>
+									<Note fill="#2ABC6F" />
+									<span>时值正确</span>
+								</div>								
+							}
+							<div>
+								<Note fill="#ADADAD" />
+								<span>未演奏</span>
+							</div>
+						</div>
+					)}
+					</> : 
+					<>
+					{state.isPercussion ? null : (
+						<div class={styles.demos}>
+							{itemType.value === "intonation" && (
+								<>
+									<div>
+										<img class={styles.firstIcon1} src={firstTop} />
+										<span>演奏偏高</span>
+									</div>
+									<div>
+										<img class={styles.firstIcon1} src={firstBottom} />
+										<span>演奏偏低</span>
+									</div>
+								</>
+							)}	
+							{itemType.value === "cadence" && (
+								<>
+									<div>
+										<img class={styles.firstIcon2} src={firstLeft} />
+										<span>节奏偏快</span>
+									</div>
+									<div>
+										<img class={styles.firstIcon2} src={firstRight} />
+										<span>节奏偏慢</span>
+									</div>
+								</>
+							)}								
+							{(itemType.value === "intonation" || itemType.value === "cadence") && (	
+								<>			
+									<div>
+										<img class={styles.firstIcon3} src={firstCorrect} />
+										<span>演奏正确</span>
+									</div>
+									<div>
+										<img class={styles.firstIcon3} src={firstError} />
+										<span>演奏错误</span>
+									</div>							
+								</>
+							)}
+							{(itemType.value === "intonation" || itemType.value === "integrity") && (
 								<div>
-									<Note fill="rgba(1, 193, 181, .8)" shadowFill="#FF4444" shadow x={0.5} y={-1} />
-									<span>节奏过快</span>
+									<img class={styles.firstIcon3} src={firstLack} />
+									<span>时值不足</span>
 								</div>
+							)}
+							{
+								itemType.value === "integrity" && 
 								<div>
-									<Note fill="rgba(1, 193, 181, .8)" shadowFill="#FF4444" shadow x={-3} y={-2.5} />
-									<span>节奏慢了</span>
-								</div>
-							</>
-						)}
-						{itemType.value === "integrity" && (
+									<img class={styles.firstIcon3} src={firstCorrect} />
+									<span>时值正确</span>
+								</div>								
+							}							
 							<div>
-								<Note fill="#CC75FF" />
-								<span>完成度不足</span>
+								<img class={styles.firstIcon3} src={firstNot} />
+								<span>未演奏</span>
 							</div>
-						)}
-						<div>
-							<Note fill="#000" />
-							<span>未演奏</span>
 						</div>
-					</div>
-				)}
+					)}
+					</>							
+				}	
+
 				<Popup
 					teleport="body"
 					class={["popup-custom", "van-scale", styles.popup]}
@@ -209,48 +341,90 @@ export default defineComponent({
 
 				<Popup
 					v-model:show={shareData.shiyiShow}
-					class="popup-custom van-scale center-closeBtn"
+					class="popup-custom van-scale center-closeBtn shiyiBox"
 					transition="van-scale"
 					teleport="body"
 					closeable
 				>
-					<div class={styles.shiyiPopup}>
-						<div class={styles.shiyiTitle}>图标释义</div>
-						<div class={styles.items}>
-							<div class={styles.item}>
-								<Note fill="#01C1B5" />
-								<span>绿色音符:演奏正确</span>
-							</div>
-							<div class={styles.item}>
-								<Note fill="#FF4444" />
-								<span>红色音符:错音</span>
-							</div>
-							<div class={styles.item}>
-								<Note fill="#CC75FF" />
-								<span>紫色音符:完成度不足</span>
-							</div>
-							<div class={styles.item}>
-								<Note fill="#AEAEAE" />
-								<span>灰色音符:未演奏</span>
-							</div>
-							<div class={styles.item}>
-								<Note fill="rgba(1, 193, 181, .8)" shadowFill="#FF4444" shadow x={0.5} y={-1} />
-								<span>音符重影(红色在前):节奏过快</span>
-							</div>
-							<div class={styles.item}>
-								<Note fill="rgba(1, 193, 181, .8)" shadowFill="#FF4444" shadow x={-3} y={-2.5} />
-								<span>音符重影(红色在后):节奏慢了</span>
-							</div>
-							<div class={styles.item}>
-								<Note fill="rgba(1, 193, 181, .8)" shadowFill="#FFAB25" shadow x={-2} y={0} />
-								<span>音符重影(黄色在上):音高了</span>
+					<img onClick={() => shareData.shiyiShow = false }  class={styles.shiyiClose} src={shiyiClose} />
+					{scoreData.value.musicType === 'staff' ?
+						<div class={styles.shiyiPopup}>
+							<img class={styles.shiyiTop} src={shiyiTop} />
+							<div class={styles.items}>
+								<div class={styles.item}>
+									<Note fill="rgba(42, 188, 111, 1)" shadowFill="#FFAB25" shadow x={-2} y={0} />
+									<span>黄色音符在上:演奏偏高</span>
+								</div>
+								<div class={styles.item}>
+									<Note fill="#2ABC6F" />
+									<span>绿色音符:演奏/时值正确</span>
+								</div>
+								<div class={styles.item}>
+									<Note fill="rgba(42, 188, 111, 1)" shadowFill="#FFAB25" shadow x={-1} y={-3} />
+									<span>黄色音符在下:演奏偏低</span>
+								</div>
+								<div class={styles.item}>
+									<Note fill="#FF2B29" />
+									<span>红色音符:演奏错误</span>
+								</div>
+								<div class={styles.item}>
+									<Note fill="rgba(42, 188, 111, 1)" shadowFill="#FFAB25" shadow x={0.5} y={-1} />
+									<span>黄色音符在左:节奏偏快</span>
+								</div>
+								<div class={styles.item}>
+									<Note fill="#8F4EFB" />
+									<span>紫色音符:时值不足</span>
+								</div>
+								<div class={styles.item}>
+									<Note fill="rgba(42, 188, 111, 1)" shadowFill="#FFAB25" shadow x={-3} y={-2.5} />
+									<span>黄色音符在右:节奏偏慢</span>
+								</div>
+								<div class={styles.item}>
+									<Note fill="#ADADAD" />
+									<span>灰色音符:未演奏</span>
+								</div>
 							</div>
-							<div class={styles.item}>
-								<Note fill="rgba(1, 193, 181, .8)" shadowFill="#FFAB25" shadow x={-1} y={-3} />
-								<span>音符重影(黄色在下):音低了</span>
+						</div> : 
+						<div class={styles.shiyiPopup}>
+							<img class={styles.shiyiTop} src={shiyiTop} />
+							<div class={styles.items}>
+								<div class={styles.itemTone}>
+									<img class={styles.firstIcon1} src={firstTop} />
+									<span>黄色箭头朝上:演奏偏高</span>
+								</div>
+								<div class={styles.itemTone}>
+									<img class={styles.firstIcon3} src={firstCorrect} />
+									<span>绿色音符:演奏/时值正确</span>
+								</div>
+								<div class={styles.itemTone}>
+									<img class={styles.firstIcon1} src={firstBottom} />
+									<span>黄色箭头朝下:演奏偏低</span>
+								</div>
+								<div class={styles.itemTone}>
+									<img class={styles.firstIcon3} src={firstError} />
+									<span>红色音符:演奏错误</span>
+								</div>
+								<div class={styles.itemTone}>
+									<img class={[styles.firstIcon2, styles.fiz]} src={firstLeft} />
+									<span>黄色箭头朝左:节奏偏快</span>
+								</div>
+								<div class={styles.itemTone}>
+									<img class={styles.firstIcon3} src={firstLack} />
+									<span>紫色音符:时值不足</span>
+								</div>
+								<div class={styles.itemTone}>
+									<img class={styles.firstIcon2} src={firstRight} />
+									<span>黄色箭头朝右:节奏偏慢</span>
+								</div>
+								<div class={styles.itemTone}>
+									<img class={styles.firstIcon3} src={firstNot} />
+									<span>灰色音符:未演奏</span>
+								</div>
 							</div>
-						</div>
-					</div>
+						</div>											
+					}
+
+
 				</Popup>
 			</div>
 		);

+ 41 - 16
src/page-instrument/view-evaluat-report/index.module.less

@@ -52,6 +52,7 @@
         overflow: initial;
         height: initial;
         max-height: initial;
+        transform: translateY(-3%) !important;
         & > #osmdCanvasPage1 {
           position: relative !important;
         }
@@ -68,48 +69,72 @@
   text-align: center;
 }
 
+.beam {
+  path {
+    fill: #ADADAD !important;
+    stroke: #ADADAD;
+  }
+}
 
 .right {
   path {
-    fill: #01C1B5;
-    stroke: #01C1B5;
+    fill: #2ABC6F;
+    stroke: #2ABC6F;
+  }
+}
+
+.inaccuracy {
+  path {
+    fill: #FF9200;
+    stroke: #FF9200;
   }
 }
 
 .wrong {
   path {
-    fill: #FF4444;
-    stroke: #FF4444;
+    fill: #FF2B29;
+    stroke: #FF2B29;
   }
 }
 
 .notPlay {
   path {
-    fill: #000;
-    stroke: #000;
+    fill: #ADADAD;
+    stroke: #ADADAD;
   }
 }
 
 // 音准
-.intonation_wrong {
-  path {
-    fill: #FFAB25;
-    stroke: #FFAB25;
+.intonation_wrong,
+.intonation_high,
+.intonation_low{
+  path{
+    fill: #FF9200;
+    stroke: #FF9200;
   }
 }
 
 // 节奏
-.cadence_wrong {
-  path {
-    fill: #FF4444;
-    stroke: #FF4444;
+.cadence_wrong,
+.cadence_fast,
+.cadence_slow {
+  path{
+    fill: #FF9200;
+    stroke: #FF9200;
   }
 }
 
 // 完成度
 .integrity_wrong {
   path {
-    fill: #CC75FF;
-    stroke: #CC75FF;
+    fill: #8F4EFB;
+    stroke: #8F4EFB;
   }
+}
+
+
+.arrowSvg {
+  opacity: 0;
+  width: 1;
+  height: 1;
 }

+ 256 - 21
src/page-instrument/view-evaluat-report/index.tsx

@@ -1,7 +1,7 @@
 import { Skeleton } from "vant";
-import { defineComponent, onBeforeMount, onBeforeUnmount, onMounted, reactive, Transition } from "vue";
+import { defineComponent, onBeforeMount, onBeforeUnmount, onMounted, reactive, Transition, watch, ref } from "vue";
 import { formateTimes } from "../../helpers/formateMusic";
-import state, { isRhythmicExercises } from "../../state";
+import state, { isRhythmicExercises, getMusicDetail, EnumMusicRenderType } from "../../state";
 import { setGlobalData } from "../../utils";
 import MusicScore, { resetMusicScore } from "../../view/music-score";
 import styles from "./index.module.less";
@@ -13,22 +13,62 @@ import {
 import { getQuery } from "/src/utils/queryString";
 import { mappingVoicePart, subjectFingering } from "/src/view/fingering/fingering-config";
 import { api_musicPracticeRecordDetail, sysMusicScoreAccompanimentQueryPage } from "../api";
+import { getMusicSheetDetail } from "/src/utils/baseApi"
 import ShareTop from "./component/share-top";
 import { addMeasureScore } from "/src/view/evaluating";
+import TopArrow from "./component/note/topArrow";
+import BottomArrow from "./component/note/bottomArrow";
+import LeftArrow from "./component/note/leftArrow";
+import RightArrow from "./component/note/rightArrow";
 
 const colorsClass: any = {
-	RIGHT: styles.right,
-	WRONG: styles.wrong,
-	NOT_PLAY: styles.notPlay,
-	CADENCE_WRONG: styles.cadence_wrong,
-	INTONATION_WRONG: styles.intonation_wrong,
-	INTEGRITY_WRONG: styles.integrity_wrong,
-};
+	RIGHT: styles.right, // 正确
+	WRONG: styles.wrong, // 错误
+	NOT_PLAYED: styles.notPlay, // 未演奏
+	EARLY: styles.cadence_fast, // 节奏快
+	LATE: styles.cadence_slow, // 节奏慢
+	HIGH: styles.intonation_high, // 音准高
+	LOW: styles.intonation_low, // 音准低
+	DURATION_INSUFFICIENT: styles.integrity_wrong // 完整性(时值)不足
+}
+
+
+// const colorsClass: any = {
+// 	/** 音准 */
+// 	pitch: {
+// 		/** 高了 */
+// 		HIGH: styles.intonation_high,
+// 		/** 正常 */
+// 		RIGHT: styles.intonation_right,
+// 		/** 低了 */
+// 		LOW: styles.intonation_low,
+// 		/** 未演奏 */
+// 		NOT_PLAYED: styles.notPlay,
+// 		/** 错误 */
+// 		WRONG: styles.intonation_wrong,
+// 		/** 时值不足 */
+// 		DURATION_INSUFFICIENT: styles.integrity_wrong
+// 	},
+// 	/** 节奏 */
+// 	rhythmic: {
+// 		/** 过早 */
+// 		EARLY: styles.cadence_fast,
+// 		/** 正常 */
+// 		RIGHT: styles.cadence_right,
+// 		/** 过迟 */
+// 		LATE: styles.cadence_slow,
+// 		/** 未演奏 */
+// 		NOT_PLAYED: styles.notPlay,
+// 		/** 错误 */
+// 		WRONG: styles.cadence_wrong
+// 	}
+// }
 
 export default defineComponent({
 	name: "music-list",
 	setup() {
 		const query: any = getQuery();
+		const useedid = ref<string[]>([])
 		const scoreData = reactive({
 			videoFilePath: "", // 回放视频路径
 			cadence: 0,
@@ -36,6 +76,8 @@ export default defineComponent({
 			intonation: 0,
 			score: 0,
 			heardLevel: "",
+			itemType: "intonation",
+			musicType: 'staff',
 		});
 
 		const detailData = reactive({
@@ -138,6 +180,8 @@ export default defineComponent({
 				console.error("解析评测结果:", error);
 			}
 			// console.log("🚀 ~ resultData:", resultData);
+			// @ts-ignore
+			// resultData.musicalNotesPlayStats?.notesData.forEach((item) => item.rhythmicAssessment.result = 'EARLY')
 			detailData.musicalNotesPlayStats = resultData.musicalNotesPlayStats?.notesData || [];
 			detailData.userMeasureScore = resultData.userMeasureScore || {};
 
@@ -147,18 +191,191 @@ export default defineComponent({
 			scoreData.intonation = res.data?.intonation;
 			scoreData.score = res.data?.score;
 			scoreData.videoFilePath = res.data?.videoFilePath || res.data?.recordFilePath;
-			Promise.all([
-				sysMusicScoreAccompanimentQueryPage(resultData.musicalNotesPlayStats?.examSongId),
-			]).then((values) => {
-				getMusicInfo(values[0]);
-			});
+			state.isEvaluatReport = true;
+			await getMusicDetail(resultData.musicalNotesPlayStats?.examSongId);
+			// 从练习记录进入评测报告,默认显示五线谱
+			// if (!query.musicRenderType) {
+			// 	state.musicRenderType = EnumMusicRenderType.staff
+			// }
+			scoreData.musicType = query.musicRenderType ? query.musicRenderType : state.musicRenderType;
+			detailData.isLoading = false;
+			// Promise.all([
+			// 	getMusicSheetDetail(resultData.musicalNotesPlayStats?.examSongId),
+			// ]).then((values) => {
+			// 	getMusicInfo(values[0]);
+			// });
 		});
 
+
+		const getOffsetPosition = (type: keyof typeof colorsClass): string => {
+			// 五线谱
+			if (scoreData.musicType === 'staff') {
+				switch (type) {
+					case 'EARLY':
+					  return 'translateX(-3px)'
+					case 'LATE':
+					  return 'translateX(3px)'
+					case 'HIGH':
+					  return 'translateY(-2px)'
+					case 'LOW':
+					  return 'translateY(2px)'
+					default:
+					  return ''
+				  }
+			} else {
+				switch (type) {
+					case 'EARLY':
+					  return 'translateX(-3px)'
+					case 'LATE':
+					  return 'translateX(3px)'
+					case 'HIGH':
+					  return 'translateY(-2px)'
+					case 'LOW':
+					  return 'translateY(-10px)'
+					default:
+					  return ''
+				  }
+			}
+		  }
+	  
+		  const filterNotes = () => {
+			let include = ['RIGHT', 'WRONG', 'NOT_PLAYED']
+			if (scoreData.itemType === 'intonation') { // 音准
+			  include.push(...['HIGH', 'LOW', 'DURATION_INSUFFICIENT'])
+			} else if (scoreData.itemType === 'cadence') { // 节奏
+			  include.push(...['EARLY', 'LATE'])
+			} else if (scoreData.itemType === 'integrity') { // 完整性
+			  include = ['DURATION_INSUFFICIENT', 'RIGHT', 'NOT_PLAYED']
+			}
+			if (scoreData.itemType === 'cadence') {
+				return detailData.musicalNotesPlayStats.filter((item: any) => include.includes(item.rhythmicAssessment.result))
+			} else {
+				return detailData.musicalNotesPlayStats.filter((item: any) => {
+					let result = item.pitchAssessment.result
+					if (scoreData.itemType === 'integrity') {
+						result = (result === 'HIGH' || result === 'LOW' || result === 'WRONG') ? 'RIGHT' : result
+					}
+					return include.includes(result)
+				})
+			}
+		  }
+	  
+		  const setViewColor = () => {
+			clearViewColor()
+			const notes = filterNotes()
+			// console.log(1111,notes)
+			for (const note of notes) {
+			  const active = state.times[note.index]
+			  setTimeout(() => {
+				if (useedid.value.includes(active.id)) {
+				  return
+				}
+				useedid.value.push(active.id)
+				const svgEl = document.getElementById('vf-' + active.id)
+				const stemEl = document.getElementById('vf-' + active.id + '-stem')
+				let errType = scoreData.itemType === 'cadence' ? note.rhythmicAssessment.result : note.pitchAssessment.result
+				// console.log(1111222,errType)
+				const isNeedCopyElement = scoreData.itemType === 'integrity' ? false : ['HIGH', 'LOW', 'EARLY', 'LATE'].includes(
+				  errType
+				)
+				if (scoreData.itemType === 'integrity') {
+					errType = errType = (note.pitchAssessment.result === 'HIGH' || note.pitchAssessment.result === 'LOW' || note.pitchAssessment.result === 'WRONG') ? 'RIGHT' : errType
+				}
+				stemEl?.classList.add(colorsClass[errType])
+				svgEl?.classList.add(colorsClass[errType])
+				if (svgEl && isNeedCopyElement) {
+				  stemEl?.classList.remove(colorsClass[errType])
+				  svgEl?.classList.remove(colorsClass[errType])
+				  let copySvg: any = null;
+				  // 五线谱
+				  if (scoreData.musicType === 'staff') {
+					stemEl?.classList.add(colorsClass.RIGHT)
+					svgEl?.classList.add(colorsClass.RIGHT)
+					copySvg = svgEl.querySelector('.vf-notehead')!.cloneNode(true) as SVGSVGElement
+				  } else {
+					//copySvg = svgEl.querySelector('.vf-numbered-note-head')!.cloneNode(true) as SVGSVGElement
+					
+					if (isNeedCopyElement) {
+						svgEl?.classList.add(styles.inaccuracy)
+						const targetId = errType === 'HIGH' ? 'topSvg' : errType === 'LOW' ? 'bottomSvg' : errType === 'EARLY' ? 'leftSvg' : errType === 'LATE' ? 'rightSvg' : ''
+						copySvg = document.getElementById(targetId)!.cloneNode(true) as SVGSVGElement
+						const { width, height } = svgEl.getBoundingClientRect() || {}
+						// @ts-ignore
+						let { x, y } = svgEl?.getBBox() || {}
+						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
+						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
+						copySvg.setAttribute("x", x)
+						copySvg.setAttribute("y", y)
+					}
+
+					// console.log(x,y,copySvg.getBoundingClientRect())
+					// const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+					// rect.setAttribute("x", 0 +'px');
+					// rect.setAttribute("y", 0+'px');
+					// rect.setAttribute("width", `50`);
+					// rect.setAttribute("height", `50`);
+					// rect.setAttribute("fill", "#FF4444");
+					// svgEl.prepend(rect);			
+				  }
+				  if (scoreData.musicType === 'staff') {
+					copySvg.style.transform = getOffsetPosition(errType)
+					//svgEl.style.opacity = '.7'
+					if (stemEl) {
+					  //stemEl.style.opacity = '.7'
+					}
+				  }
+				  copySvg.id = 'vf-' + active.id + '-copy'
+				  copySvg?.classList.add(colorsClass[errType])
+				  // stemEl?.classList.add(colorsClass.RIGHT)
+				  // @ts-ignore
+				  state.osmd?.container.querySelector('svg')!.insertAdjacentElement('afterbegin', copySvg)
+				  // svgEl?.parentElement?.appendChild(copySvg)
+				}
+			  }, 300)
+			}
+		  }
+	  
+		  const removeClass = (el?: HTMLElement | null) => {
+			if (!el) return
+			const classList = el.classList.values()
+			for (const val of classList) {
+			  if (val?.indexOf('vf-') !== 0) {
+				el.classList.remove(val)
+			  }
+			}
+		  }
+	  
+		  const clearViewColor = () => {
+			for (const id of useedid.value) {
+			  removeClass(document.getElementById('vf-' + id))
+			  removeClass(document.getElementById('vf-' + id + '-stem'))
+			  const qid = 'vf-' + id + '-copy'
+			  const copyEl = document.getElementById(qid)
+			  if (copyEl) {
+				copyEl.remove()
+			  }
+			}
+			useedid.value = []
+		  }
+		
 		const setPathColor = () => {
+			console.log(11111,detailData.musicalNotesPlayStats,scoreData.itemType)
 			for (const note of detailData.musicalNotesPlayStats) {
-				const active = state.times[note.musicalNotesIndex];
+				const active = state.times[note.index];
 				const svgEl = active?.id ? document.getElementById("vf-" + active?.id) : null;
-				svgEl?.classList.add(colorsClass[note.musicalErrorType]);
+				switch (scoreData.itemType) {
+					case "intonation":
+						svgEl?.classList.add(colorsClass.pitch[note.pitchAssessment.result]);
+						break;
+					case "cadence":
+						svgEl?.classList.add(colorsClass.rhythmic[note.rhythmicAssessment.result]);
+						break;	
+					case "integrity":
+						svgEl?.classList.add(colorsClass.pitch[note.pitchAssessment.result]);
+						break;								
+					default:
+						break;
+				}
 			}
 		};
 		const setMearureColor = () => {
@@ -173,8 +390,14 @@ export default defineComponent({
 			state.osmd = osmd;
 			state.times = formateTimes(osmd);
 			console.log("🚀 ~ state.times:", state.times);
-			setPathColor();
-			setMearureColor();
+			// @ts-ignore
+			const beams =  Array.from(new Set(document.getElementsByClassName('vf-beam')))
+			beams.forEach((item: any) => {
+				item.classList.add(styles.beam)
+			})
+			//setPathColor();
+			setViewColor();
+			// setMearureColor();
 			api_cloudLoading();
 		};
 		onMounted(() => {
@@ -183,7 +406,12 @@ export default defineComponent({
 		onBeforeUnmount(() => {
 			window.removeEventListener("resize", resetMusicScore);
 		});
-
+		watch(
+			() => scoreData.itemType,
+			() => {
+				setViewColor();
+			}
+		);
 		return () => (
 			<div
 				class={[styles.detail, state.setting.eyeProtection && "eyeProtection", styles.shareBox]}
@@ -208,9 +436,16 @@ export default defineComponent({
 					id="scrollContainer"
 					class={[styles.container, !state.setting.displayCursor && "hideCursor"]}
 				>
-					<div class={styles.musicName}>{state.examSongName}</div>
 					{/* 曲谱渲染 */}
-					{!detailData.isLoading && <MusicScore onRendered={handleRendered} />}
+					{!detailData.isLoading && <MusicScore musicColor={'#ADADAD'} onRendered={handleRendered} />}
+					{
+						<div class={styles.arrowSvg}>
+							<TopArrow />
+							<BottomArrow />
+							<LeftArrow />
+							<RightArrow />
+						</div>
+					}
 				</div>
 			</div>
 		);

+ 2 - 1
src/page-instrument/view-figner/guide/detail-guide.tsx

@@ -12,7 +12,8 @@ export default defineComponent({
 				<div class={styles.btn} onClick={() => emit("close", true)}>
 					不再提醒
 				</div>
-				<Icon class={styles.close} name="cross" onClick={() => emit("close")} />
+				<span class={styles.closeIcon} onClick={() => emit("close")}></span>
+				{/* <Icon class={styles.close} name="cross" onClick={() => emit("close")} /> */}
 			</div>
 		);
 	},

+ 24 - 0
src/page-instrument/view-figner/guide/index.module.less

@@ -28,6 +28,30 @@
         padding: 0 8px;
         padding-top: 6px;
     }
+
+    .closeIcon{
+        width: 15px;
+        height: 15px;
+        margin: 0 10px;
+        position: relative; 
+        cursor: pointer;
+    }
+    .closeIcon::before,
+    .closeIcon::after{
+        content: "";
+        position: absolute;
+        height: 15px;
+        width: 1.5px;
+        top: 4px;
+        right: 9px;
+        background: rgba(255, 255, 255, .55);
+    }
+    .closeIcon::before{
+        transform: rotate(45deg);
+    }
+    .closeIcon::after{
+        transform: rotate(-45deg);
+    }    
 }
 
 .fingerGuide {

+ 325 - 58
src/state.ts

@@ -9,10 +9,11 @@ import { handleStartTick } from "./view/tick";
 import { audioListStart, getAudioCurrentTime, getAudioDuration, setAudioCurrentTime, setAudioPlaybackRate } from "./view/audio-list";
 import { toggleFollow } from "./view/follow-practice";
 import { browser, setStorageSpeed, setGlobalData } from "./utils";
-import { api_createMusicPlayer } from "./helpers/communication";
-import { verifyCanRepeat } from "./helpers/formateMusic";
+import { api_cloudGetMediaStatus, api_createMusicPlayer, api_cloudChangeSpeed } from "./helpers/communication";
+import { verifyCanRepeat, getDuration } from "./helpers/formateMusic";
 import { getMusicSheetDetail } from "./utils/baseApi"
 import { getQuery } from "/src/utils/queryString";
+import { followData } from "/src/view/follow-practice/index"
 
 const query: any = getQuery();
 
@@ -39,88 +40,208 @@ export enum IPlatform {
   PC = "PC",
 }
 
+export type ISonges = {
+  background?: string
+  music?: string
+}
+
 /**
  * 特殊教材分类id
  */
-const classids = [1, 2, 6, 7, 8, 9, 3, 10, 11, 12, 13, 4, 14, 15, 16, 17, 30, 31, 35, 36, 46, 108]; // 大雅金唐, 竖笛教程, 声部训练展开的分类ID
+const classids = [1,2,3,4,6,7,8,9,10,11,12,13,14,15,16,17,30,31,35,36,38,108,150,151,152,153,154,155,156,157,158,178,179,180,181,182]; // 大雅金唐, 竖笛教程, 声部训练展开的分类ID
 
 // 乐器code码
-const musicalInstrumentCodeInfo = [
+export const musicalInstrumentCodeInfo = [
   {
-    name: '排箫',
-    code: 'Panpipes',
+    name: '长笛',
+    code: 'Flute',
     id: 1
   },
   {
-    name: '笛',
-    code: 'Ocarina',
+    name: '笛',
+    code: 'Piccolo',
     id: 2
   },
   {
-    name: '葫芦丝',
-    code: 'Woodwind',
+    name: '单簧管',
+    code: 'Clarinet',
     id: 3
   },
   {
-    name: '德式竖笛',
-    code: 'Tenor Recorder',
+    name: '低音单簧管',
+    code: 'Bass Clarinet',
     id: 4
   },
   {
-    name: '口风琴',
-    code: 'Nai',
+    name: '中音萨克斯',
+    code: 'Alto Saxophone',
     id: 5
   },
   {
-    name: '英式竖笛',
-    code: 'BaroqueRecorder',
+    name: '次中音萨克斯',
+    code: 'Tenor Saxophone',
     id: 6
   },
   {
-    name: '打击乐',
-    code: 'PERCUSSION',
+    name: '高音萨克斯',
+    code: 'Soprano Saxophone',
     id: 7
   },
   {
-    name: '长笛',
-    code: 'FLUTE',
+    name: '上低音萨克斯',
+    code: 'Baritone Saxophone',
     id: 8
   },
   {
-    name: '萨克斯',
-    code: 'SAX',
+    name: '双簧管',
+    code: 'Oboe',
     id: 9
   },
   {
-    name: '单簧管',
-    code: 'CLARINET',
+    name: '管',
+    code: 'Bassoon',
     id: 10
   },
   {
     name: '小号',
-    code: 'TRUMPET',
+    code: 'Trumpet',
     id: 11
   },
   {
-    name: '号',
-    code: 'TROMBONE',
+    name: '号',
+    code: 'Horn',
     id: 12
   },
   {
-    name: '号',
-    code: 'HORN',
+    name: '号',
+    code: 'Trombone',
     id: 13
   },
   {
     name: '上低音号',
-    code: 'BARITONE',
+    code: 'Baritone',
     id: 14
   },
   {
-    name: '号',
-    code: 'TUBA',
+    name: '次中音号',
+    code: 'Euphonium',
     id: 15
-  }
+  },
+  {
+    name: '大号',
+    code: 'Tuba',
+    id: 16
+  },
+  {
+    name: '钢琴',
+    code: 'Piano',
+    id: 17
+  },
+  {
+    name: '电钢琴',
+    code: 'Electronical Piano',
+    id: 18
+  },
+  {
+    name: '钢片琴',
+    code: 'Glockenspiel',
+    id: 19
+  },
+  {
+    name: '小提琴',
+    code: 'Violin',
+    id: 20
+  },
+  {
+    name: '中提琴',
+    code: 'Viola',
+    id: 21
+  },
+  {
+    name: '大提琴',
+    code: 'Violoncello',
+    id: 22
+  },
+  {
+    name: '低音提琴',
+    code: 'Contrabass',
+    id: 23
+  },
+  {
+    name: '架子鼓',
+    code: 'Drum Set',
+    id: 24
+  },
+  {
+    name: '小鼓',
+    code: 'Snare Drum',
+    id: 25
+  },
+  {
+    name: '马林巴',
+    code: 'Marimba',
+    id: 26
+  },
+  {
+    name: '颤音琴',
+    code: 'Vibraphone',
+    id: 27
+  },
+  {
+    name: '钟琴',
+    code: 'Chimes',
+    id: 28
+  },
+  {
+    name: '木琴',
+    code: 'Xylophone',
+    id: 29
+  },
+  {
+    name: '管钟',
+    code: 'Tubular Bells',
+    id: 30
+  },
+  {
+    name: '定音鼓',
+    code: 'Timpani',
+    id: 31
+  },
+  {
+    name: '键盘',
+    code: 'Mallets',
+    id: 32
+  },
+  {
+    name: '排箫',
+    code: 'Panpipes',
+    id: 33
+  },
+  {
+    name: '陶笛',
+    code: 'Ocarina',
+    id: 34
+  },
+  {
+    name: '葫芦丝',
+    code: 'Woodwind',
+    id: 35
+  },
+  {
+    name: '口风琴',
+    code: 'Nai',
+    id: 36
+  },
+  {
+    name: '德式竖笛',
+    code: 'Tenor Recorder',
+    id: 37
+  },
+  {
+    name: '英式竖笛',
+    code: 'Baroque Recorder',
+    id: 38
+  },
 ]
 
 const state = reactive({
@@ -147,6 +268,8 @@ const state = reactive({
   enableNotation: false,
   /** 曲谱ID */
   examSongId: "",
+  /** 内容平台的曲谱ID,可能会和业务端的id不一样 */
+  cbsExamSongId: "",
   /** 曲谱名称 */
   examSongName: "",
   /** 曲谱封面 */
@@ -155,7 +278,7 @@ const state = reactive({
   extConfigJson: {} as any,
   /** 扩展样式字段 */
   extStyleConfigJson: {} as any,
-  /** 是否开启节拍器 */
+  /** 是否开启节拍器(mp3节拍器) */
   isOpenMetronome: false,
   /** 是否显示指法 */
   isShowFingering: false,
@@ -169,6 +292,8 @@ const state = reactive({
   parentCategoriesId: 0,
   /** 分类ID */
   musicSheetCategoriesId: 0,
+  /** 各产品端的分类ID,(管乐迷、管乐团、酷乐秀、课堂乐器) */
+  bizMusicCategoryId: 0,  
   /** 资源类型: mp3 | midi */
   playMode: "MP3" as "MP3" | "MIDI",
   /** 设置的速度 */
@@ -230,7 +355,7 @@ const state = reactive({
     /** 显示光标 */
     displayCursor: true,
     /** 频率 */
-    frequency: 442,
+    frequency: 0,
     /** 评测难度 */
     evaluationDifficulty: "ADVANCED" as IDifficulty,
     /** 保存到相册 */
@@ -239,10 +364,12 @@ const state = reactive({
     enableAccompaniment: true,
     /** 反应时间 */
     reactionTimeMs: 0,
+    /** 节拍器音量 */
+    beatVolume: 50,
   },
   /** 后台设置的基准评测频率 */
   baseFrequency: 440,
-  /** 节拍器的时间 */
+  /** mp3节拍器的时间,统计拍数、速度计算得出 */
   fixtime: 0,
   /** 指法信息 */
   fingeringInfo: {} as IFingering,
@@ -250,6 +377,8 @@ const state = reactive({
   scrollContainer: "musicAndSelection",
   /** 是否是打击乐 */
   isPercussion: false,
+  /** 评测标准 */
+  evaluationStandard: '',
   /** 是否重复节拍器的时间 */
   repeatedBeats: 0,
   /**当前曲谱中所有声部名字 */
@@ -280,10 +409,31 @@ const state = reactive({
   repeatInfo: [],  
   /** 多分轨的曲子,可支持筛选的分轨 */
   canSelectTracks: [] as any,
-  /** 声部codeId,用于匹配乐器指法、声部转调、特殊声部处理等 */
+  /** 声部codeId */
   subjectCodeId: 0 as number,
+  /** 乐器codeId,用于匹配乐器指法、声部转调、特殊声部处理等 */
+  musicalCodeId: 0 as number,
+  /** 乐器code,用于评测传参 */
+  musicalCode: '' as any,
   /** 合奏曲目是否合并展示 */
   isCombineRender: false,
+  /** 小节的持续时长,以后台设置的播放速度计算 */
+  measureTime: 0,
+  /** 跟练模式,节拍器播放的时间 */
+  beatStartTime: 0,
+  /** 是否为详情预览模式 */
+  isPreView: false,
+  /** 是否为评测报告模式 */
+  isEvaluatReport: false,
+  /** midi播放器是否初始化中 */  
+  midiPlayIniting: false,
+  /** 曲目信息 */
+  songs: {} as ISonges,  
+  isAppPlay: false, // 是否是app播放
+  /** 音频播放器实例 */
+  audiosInstance: null as any,
+  /** midi音频的时长 */
+  durationNum: 0,
 });
 const browserInfo = browser();
 let offset_duration = 0;
@@ -319,7 +469,8 @@ const setStep = () => {
 export const onPlay = () => {
   console.log("开始播放");
   state.playEnd = false;
-  offset_duration = browserInfo.xiaomi ? 0.2 : 0.08;
+  // offset_duration = browserInfo.xiaomi ? 0.2 : 0.08;
+  offset_duration = 0.2;
   setStep();
 };
 
@@ -361,7 +512,10 @@ const handlePlaying = () => {
   // console.log(item.i,item.noteId,item.measureSpeed)
   // 练习模式下,实时刷新小节速度
   if (item && state.modeType === "practise" && state.playState === "play" && item.measureSpeed && item.measureSpeed !== state.playIngSpeed) {
-    state.playIngSpeed = item.measureSpeed
+    const ratio = state.speed / state.originSpeed
+    state.playIngSpeed = Math.ceil(ratio * item.measureSpeed) || state.speed
+  } else if (state.modeType === "practise" && state.playState === "play" && item && !item.measureSpeed) {
+    state.playIngSpeed = state.speed
   }
   if (item) {
     // 选段状态下
@@ -370,7 +524,31 @@ const handlePlaying = () => {
       const selectStartItem = state.sectionFirst ? state.sectionFirst : state.section[0];
       const selectEndItem = state.section[1];
 
-      if (Math.abs(selectEndItem.endtime - currentTime) < offset_duration) {
+      /**
+       * #9374,反复小节的曲目播放错误, bug修复
+       * 曲目:噢!苏珊娜-排箫-人音
+       * 现象:重播小节为2-9,选段为4-12,当播完第9小节后会回到第2小节重播2-8,再播放10-12
+       * 4-12,不符合重播规则,所以播完第9小节后,音频需要跳转到第10小节播放
+       */
+      if (state.repeatInfo.length) {
+        const canRepeatInfo = verifyCanRepeat(state.section[0].MeasureNumberXML, state.section[1].MeasureNumberXML)
+        const repeatIdx = canRepeatInfo.repeatIdx == -1 ? 0 : canRepeatInfo.repeatIdx
+        if (state.modeType === "practise" && !canRepeatInfo.canRepeat && state.section[1].MeasureNumberXML > state.repeatInfo[repeatIdx].end) {
+          const preItem = state.times[item.i - 1]
+          if (preItem && preItem.MeasureNumberXML > item.MeasureNumberXML) {
+            const skipItem = state.times.find((item: any) => item.MeasureNumberXML === preItem.MeasureNumberXML + 1)
+            if (skipItem) {
+              // 跳转到指定的音频位置
+              setAudioCurrentTime(skipItem.time, skipItem.i);
+              gotoNext(skipItem);
+              return
+            }
+          }
+        }
+      }
+
+      // if (Math.abs(selectEndItem.endtime - currentTime) < offset_duration) {
+        if (currentTime - selectEndItem.endtime > offset_duration) {
         console.log("选段播放结束");
         // 如果为选段评测模式
         if (state.modeType === "evaluating" && state.isSelectMeasureMode) {
@@ -390,6 +568,10 @@ const handlePlaying = () => {
     gotoNext(item);
   }
 
+  // 评测不播放叮咚节拍器
+  // if (state.modeType !== "evaluating") {
+  //   metronomeData.metro?.sound(currentTime);
+  // }
   metronomeData.metro?.sound(currentTime);
 };
 /** 跳转到指定音符开始播放 */
@@ -411,7 +593,19 @@ export const skipNotePlay = (itemIndex: number, isStart = false) => {
  * @param playState 可选: 默认 undefined, 需要切换的状态 play:播放, paused: 暂停
  */
 export const togglePlay = async (playState?: "play" | "paused") => {
-  state.playState = playState ? playState : state.playState === "paused" ? "play" : "paused";
+  // midi播放
+  if (state.isAppPlay) {
+    await api_cloudChangeSpeed({
+      speed: state.speed,
+      originalSpeed: state.originSpeed,
+      songID: state.examSongId,
+    });
+    const cloudGetMediaStatus = await api_cloudGetMediaStatus();
+    const status = cloudGetMediaStatus?.content.status
+    state.playState = status
+  } else {
+    state.playState = playState ? playState : state.playState === "paused" ? "play" : "paused";
+  }
   if (state.playState === "play" && state.sectionStatus && state.section.length == 2 && state.playProgress === 0) {
     resetPlaybackToStart();
   }
@@ -625,7 +819,8 @@ export const handleSelection = (item: any) => {
   if (state.section.length !== 2 && item) {
     state.section.push(item);
     if (state.section.length === 2) {
-      state.section = formateSelectMearure(state.section);
+      setSection(state.section[0].MeasureNumberXML,state.section[1].MeasureNumberXML)
+      //state.section = formateSelectMearure(state.section);
       closeToast();
     }
   }
@@ -652,6 +847,7 @@ export const setSection = (start: number, end: number, userSpeed?: number) => {
   let lastEndNotes = endNotes.filter((n: any) => n.noteId === lastEndId)
   // 是否符合重播规则
   const canRepeatInfo = verifyCanRepeat(start, end)
+  console.log(22222,canRepeatInfo)
   const isCanRepeat = canRepeatInfo.canRepeat
   // 如果符合重播规则,但是lastEndNotes长度为1,则需要向前找,直到找到lastEndNotes长度为2
   let currentEndNum: number = end
@@ -689,7 +885,7 @@ export const hanldeDirectSelection = (list: any[]) => {
   state.sectionStatus = true;
   setTimeout(() => {
     state.section = formateSelectMearure(list);
-    console.log(333333333,state.section)
+    //console.log(333333333,state.section)
   }, 500);
 };
 let offsetTop = 0;
@@ -723,6 +919,9 @@ export const isRhythmicExercises = () => {
 
 /** 重置状态 */
 export const handleRessetState = () => {
+  // 切换模式,清除选段
+  skipNotePlay(0, true);
+  clearSelection();
   if (state.modeType === "evaluating") {
     handleStartEvaluat();
   } else if (state.modeType === "practise") {
@@ -762,7 +961,7 @@ const getMusicInfo = (res: any) => {
     ...res.data,
     music: musicData.audioFileUrl || '',
     accompany: accompanyData.audioFileUrl || '',
-    musicSheetId: musicData.musicSheetId || res.data.id,
+    musicSheetId: musicData.musicSheetId || res.data.bizId,
     track: musicData.track || '',
   };
   console.log("🚀 ~ musicInfo:", musicInfo);
@@ -771,18 +970,24 @@ const getMusicInfo = (res: any) => {
 
 const setState = (data: any, index: number) => {
   state.appName = "COLEXIU";
-  state.detailId = data.id;
+  state.detailId = data.bizId;
   state.xmlUrl = data.xmlFileUrl;
   state.partIndex = index;
   state.trackId = data.track;
   state.subjectId = data.subjectIds ? data.subjectIds.split(',')?.[0] : 0;
-  const subjectCode = data.subjectCodes ? data.subjectCodes.split(',')?.[0] : 0;
-  const pitchSubject = musicalInstrumentCodeInfo.find((n) => n.code === subjectCode)
+  // 声部code
+  const subjectCode = data.subjectCodes ? data.subjectCodes.split(',')?.[0] : '';
+  // 乐器code
+  let musicalCode = data.musicalInstrumentIdCodes ? data.musicalInstrumentIdCodes.split(',')?.[0] : '';
+  const pitchSubject = musicalInstrumentCodeInfo.find((n) => n.code.toLocaleLowerCase() === subjectCode.toLocaleLowerCase())
+  const pitchMusical = musicalInstrumentCodeInfo.find((n) => n.code.toLocaleLowerCase() === musicalCode.toLocaleLowerCase())
   state.subjectCodeId = pitchSubject ? pitchSubject.id : 0
+  state.musicalCodeId = pitchMusical ? pitchMusical.id : 0
   state.categoriesId = data.musicCategoryId;
   state.categoriesName = data.musicTagNames;
-  state.enableEvaluation = data.isEvaluated ? true : false;
-  state.examSongId = data.id + "";
+  // state.enableEvaluation = data.isEvaluated ? true : false;
+  state.examSongId = data.bizId + "";
+  state.cbsExamSongId = data.id + "";
   state.examSongName = data.name;
   state.coverImg = data.musicCover ?? "";
   state.isCombineRender = data.musicSheetType === "SINGLE" && data.musicSheetSoundList?.length > 1
@@ -795,20 +1000,28 @@ const setState = (data: any, index: number) => {
       console.error("解析扩展字段错误:", error);
     }
   }
+  state.gradualTimes = state.extConfigJson.gradualTimes;
+  state.repeatedBeats = state.extConfigJson.repeatedBeats || 0;
   // 曲子包含节拍器,就不开启节拍器
-  state.needTick = data.isUseSystemBeat ? true : false;
-  state.isOpenMetronome = data.isUseSystemBeat ? false : true;
+  state.needTick = data.isUseSystemBeat && data.isPlayBeat ? true : false;
+  // state.isOpenMetronome = data.isUseSystemBeat ? false : true;
+  state.isOpenMetronome = data.isPlayBeat && !data.isUseSystemBeat ? true : false
   state.isShowFingering = data.isShowFingering ? true : false;
+  // 设置曲谱的播放模式, APP播放(midi音频是app播放) | h5播放
+  state.isAppPlay = data.playMode === 'MIDI';
   state.music = data.music;
   state.accompany = data.accompany;
   state.midiUrl = data.midiFileUrl;
   state.parentCategoriesId = data.musicTag;
   state.musicSheetCategoriesId = data.musicCategoryId;
+  state.bizMusicCategoryId = data.bizMusicCategoryId
   state.playMode = data.playMode === "MP3" ? "MP3" : "MIDI";
-  state.originSpeed = state.speed = data.playSpeed || 100;
+  state.originSpeed = state.speed = data.playSpeed;
+  // state.playIngSpeed = data.playSpeed;
   const track = data.code || data.track;
   state.track = track ? track.replace(/ /g, "").toLocaleLowerCase() : "";
-  state.enableNotation = data.isConvertibleScore === null ? true : data.isConvertibleScore;
+  // 能否评测,根据当前声轨有无伴奏判断
+  state.enableEvaluation = state.accompany ? true : false
   state.isConcert = data.musicSheetType === "CONCERT" ? true : false;
   // multiTracksSelection 返回为空,默认代表全部分轨
   state.canSelectTracks = data.multiTracksSelection === "null" || data.multiTracksSelection === "" || data.multiTracksSelection === null ? [] : data.multiTracksSelection?.split(',');
@@ -824,8 +1037,9 @@ const setState = (data: any, index: number) => {
    */
   // state.isPercussion = isRhythmicExercises();
   state.isPercussion = data.evaluationStandard === "AMPLITUDE" || data.evaluationStandard === "DECIBELS";
+  state.evaluationStandard = data.evaluationStandard?.toLocaleLowerCase() || ''
   // 设置是否特殊曲谱, 是特殊曲谱取反(不理解之前的思考逻辑), 使用后台设置的速度
-  // state.isSpecialBookCategory = !classids.includes(data.musicCategoryId);
+  state.isSpecialBookCategory = !classids.includes(Number(data.musicCategoryId));
 
   // 设置指法
   // const code = state.isConcert ? mappingVoicePart(state.trackId, "ENSEMBLE") : mappingVoicePart(state.subjectId, "INSTRUMENT");
@@ -833,10 +1047,14 @@ const setState = (data: any, index: number) => {
    * 各平台的乐器声部id不统一,为了兼容处理老的数据,加上乐器code码,此码唯一
    * 获取指法code
    */
-  const code = state.isConcert ? matchVoicePart(state.trackId, "CONCERT") : matchVoicePart(state.subjectCodeId, "SINGLE");
+  const code = state.isConcert ? matchVoicePart(state.trackId, "CONCERT") : matchVoicePart(state.musicalCodeId, "SINGLE");
   state.fingeringInfo = subjectFingering(code);
   console.log("🚀 ~ state.fingeringInfo:", code, state.fingeringInfo, state.trackId, state.track);
-
+  state.musicalCodeId = state.fingeringInfo?.id || 0
+  state.musicalCode = musicalInstrumentCodeInfo.find(item => item.id === state.musicalCodeId)?.code || state.trackId
+  ;(window as any).DYSubjectId = state.musicalCodeId
+  // 开启自定义每行显示的小节数
+  ;(window as any).customSectionAmount = true
   // 如果切换的声轨没有指法,择指法开关置灰并且不可点击
   if (!state.fingeringInfo.name && state.setting.displayFingering) {
     state.setting.displayFingering = false
@@ -853,12 +1071,35 @@ const setState = (data: any, index: number) => {
     state.zoom = query.zoom || 1.5;
   }
 
-  //课堂乐器, 渲染类型: 五线谱, 简谱
-  state.musicRenderType = query.musicRenderType || EnumMusicRenderType.firstTone;
+  /**
+   * 默认渲染什么谱面类型 & 能否转谱逻辑
+   * 渲染类型:首先取url参数musicRenderType,没有该参数则取musicalInstruments字段匹配的当前分轨的defaultScore,没有匹配到则取默认值('firstTone')
+   * 能否转谱:先取isConvertibleScore字段,如果isConvertibleScore为true,则取musicalInstruments字段匹配的当前分轨的transferFlag,都为true则可以转谱
+   * 
+   */
+  let pitchTrack = null
+  if (state.isConcert) {
+    musicalCode = musicalInstrumentCodeInfo.find((item: any) => item.id === state.musicalCodeId)?.code
+    pitchTrack = data.musicalInstruments?.find((item: any) => item.code === musicalCode)
+  } else {
+    pitchTrack = data.musicalInstruments?.find((item: any) => item.code === musicalCode)
+  }
+  let musicalRenderType = ''
+  if (pitchTrack?.defaultScore) {
+    musicalRenderType = pitchTrack?.defaultScore === 'STAVE' ? 'staff' : pitchTrack?.defaultScore === 'JIAN' ? 'fixedTone' : pitchTrack?.defaultScore === 'FIRST' ? 'firstTone' : ''
+  }
+  state.musicRenderType = query.musicRenderType || musicalRenderType || EnumMusicRenderType.firstTone;
+  state.enableNotation = pitchTrack ? data.isConvertibleScore && pitchTrack.transferFlag : data.isConvertibleScore
   console.log("state对象", state);
   // 评测基准频率
   state.baseFrequency = data.evaluationFrequency ? data.evaluationFrequency.split(",")[0] : 440
   state.baseFrequency = Number(state.baseFrequency)
+  // 用户上次的频率和基准频率误差超过10,则重置
+  if (Math.abs(state.setting.frequency - state.baseFrequency) > 10) {
+    state.setting.frequency = state.baseFrequency >= 0 ? state.baseFrequency : 440
+  } else {
+    state.setting.frequency = state.setting.frequency || state.baseFrequency
+  }
 };
 
 // 多分轨合并显示标示
@@ -866,4 +1107,30 @@ const setCustom = (trackNum?: number) => {
   if (trackNum || state.extConfigJson.multitrack) {
     setGlobalData("multitrack", trackNum || state.extConfigJson.multitrack);
   }
+};
+
+/** 跟练模式播放节拍器(叮咚) */
+export const followBeatPaly = () => {
+  let metroTimer: any = null;
+  if (!followData.start) {
+    clearTimeout(metroTimer)
+    metroTimer = null
+    return;
+  }
+  const time = state.measureTime*1000 / metronomeData.totalNumerator
+  requestAnimationFrame(() => {
+    const endTime = Date.now();
+    if (endTime - state.beatStartTime < time) {
+      followBeatPaly();
+    } else {
+      // metroTimer = setTimeout(() => {
+      //   metronomeData.metro?.simulatePlayAudio()
+      //   startTime = Date.now();
+      //   followBeatPaly();
+      // }, time);
+      metronomeData.metro?.simulatePlayAudio()
+      state.beatStartTime = Date.now();
+      followBeatPaly();
+    }
+  });
 };

+ 20 - 4
src/utils/index.ts

@@ -107,7 +107,7 @@ export const matchProductApiUrl = () => {
 		'cbs': {
 			'dev': 'https://dev.resource.colexiu.com',
 			'test': 'https://test.resource.colexiu.com',
-			'online': 'https://online.resource.colexiu.com'
+			'online': 'https://mec.colexiu.com'
 		},
 		'gym': {
 			'dev': 'https://dev.dayaedu.com',
@@ -127,14 +127,30 @@ export const matchProductApiUrl = () => {
 		'instrument': {
 			'dev': 'https://dev.kt.colexiu.com',
 			'test': 'https://test.kt.colexiu.com',
-			'online': 'https://kt.colexiu.com'
+			'test2': 'https://test.lexiaoya.cn',
+			'online': 'https://kt.colexiu.com',
+			// 'online': 'https://resource.colexiu.com'
 		}
 	}
-	const environment = location.origin.includes('//dev') ? 'dev' : location.origin.includes('//test') ? 'test' : location.origin.includes('//online') ? 'online' : 'dev'
+	let environment: 'dev' | 'test' | 'test2' | 'online' = location.origin.includes('//dev') ? 'dev' : location.origin.includes('//test') ? 'test' : (location.origin.includes('//online') || location.origin.includes('//kt') || location.origin.includes('//mec')) ? 'online' : 'dev'
 	if (query.isCbs) {
 		return apiUrls.cbs[environment] + '/cbs-app'
 	} else {
-		const pathName = location.pathname.includes('index') ? 'gym' : location.pathname.includes('colexiu') ? 'colexiu' : location.pathname.includes('orchestra') ? 'orchestra' : 'instrument'
+		const pathName = location.pathname.includes('gym') ? 'gym' : location.pathname.includes('colexiu') ? 'colexiu' : location.pathname.includes('orchestra') ? 'orchestra' : 'instrument'
+		// 兼容课堂乐器,测试环境两个域名
+		if (pathName === 'instrument' && environment === 'test') {
+			environment = location.origin.includes('//test.kt') ? 'test' : 'test2'
+		}
 		return apiUrls[pathName][environment] + '/edu-app'
 	}
+}
+
+/** debounce */
+export const debounce = (fn: Function, ms = 0) => {
+	let timeoutId: number | undefined;
+	return function(...args: any[]) {
+	  clearTimeout(timeoutId)
+	  // @ts-ignore
+	  timeoutId = setTimeout(() => fn.apply(this, args), ms);
+	}
 }

+ 6 - 9
src/view/abnormal-pop/index.tsx

@@ -1,10 +1,7 @@
 import { defineComponent } from "vue";
 import styles from "./index.module.less";
 import state from "/src/state";
-import icon_close from './icon_close.svg'
-import icon_bg from './icon_bg.svg'
-import icon_btn from './icon_btn.svg'
-import icon_success from './icon_success.svg'
+import { popImgs } from '/src/view/evaluating'
 import { evaluatingData } from "/src/view/evaluating";
 import { Vue3Lottie } from "vue3-lottie";
 import loading from "./loading.json";
@@ -19,14 +16,14 @@ export default defineComponent({
 				{
 					evaluatingData.socketErrorStatus === 0 && 
 					<div class={styles.fraction}>
-						<img class={styles.close} src={icon_close} onClick={() => emit("close")} />
-						<img class={styles.bg} src={icon_bg} />
+						<img class={styles.close} src={popImgs.icon_close} onClick={() => emit("close")} />
+						<img class={styles.bg} src={popImgs.icon_bg} />
 						<div class={styles.content}>
 							<div class={styles.title}>您的网络连接异常</div>
 							<div class={styles.desc}>请确保网络正常后重新连接</div>
 						</div>
 						<div>
-							<img src={icon_btn} class={styles.btn} onClick={() => emit("confirm", true)} />
+							<img src={popImgs.icon_btn} class={styles.btn} onClick={() => emit("confirm", true)} />
 						</div>
 					</div>				
 				}
@@ -34,14 +31,14 @@ export default defineComponent({
 					evaluatingData.socketErrorStatus === 1 && 
 					<div class={styles.loadColumn}>
 						<Vue3Lottie class={styles.loadIcon} animationData={loading} loop={true}></Vue3Lottie>
-						<img class={styles.close} src={icon_close} onClick={() => emit("close")} />
+						<img class={styles.close} src={popImgs.icon_close} onClick={() => emit("close")} />
 						<p>正在连接服务器,请稍后…</p>
 					</div>
 				}
 				{
 					evaluatingData.socketErrorStatus === 2 && 
 					<div class={styles.loadColumn}>
-						<img class={styles.successIcon} src={icon_success} />
+						<img class={styles.successIcon} src={popImgs.icon_success} />
 						<p>连接成功</p>
 					</div>
 				}

+ 32 - 8
src/view/audio-list/index.tsx

@@ -9,9 +9,11 @@ import {
 	setMidiCurrentTime,
 } from "./midiPlayer";
 import state, { IPlayState, onEnded, onPlay } from "/src/state";
-import { api_playProgress } from "/src/helpers/communication";
+import { api_playProgress, api_cloudTimeUpdae } from "/src/helpers/communication";
+import { evaluatingData } from "/src/view/evaluating";
+import { cloudToggleState } from "/src/helpers/midiPlay"
 
-const audioData = reactive({
+export const audioData = reactive({
 	songEle: null as unknown as HTMLAudioElement,
 	backgroundEle: null as unknown as HTMLAudioElement,
 	midiRender: false,
@@ -27,7 +29,8 @@ export const audioListStart = (type: "play" | "paused") => {
 	}
 	// 如果是midi播放
 	if (audioData.midiRender) {
-		handleTogglePlayMidi(type);
+		// handleTogglePlayMidi(type);
+		cloudToggleState();
 		return;
 	}
 	if (type === "play") {
@@ -53,10 +56,10 @@ export const setAudioPlaybackRate = (rate: number) => {
 export const getAudioCurrentTime = () => {
 	// 如果是midi播放
 	if (audioData.midiRender) {
-		const c = getMidiCurrentTime();
-		return c;
+		// const c = getMidiCurrentTime();
+		return audioData.progress;
 	}
-	// console.log('返回的时间',audioData.songEle?.currentTime,audioData.progress)
+	// console.log('返回的时间',state.playSource, audioData.songEle?.currentTime,audioData.progress)
 	if (state.playSource === "music") return audioData.songEle?.currentTime || audioData.progress;
 	if (state.playSource === "background") return audioData.backgroundEle?.currentTime || audioData.progress;
 	
@@ -66,8 +69,9 @@ export const getAudioCurrentTime = () => {
 export const getAudioDuration = () => {
 	// 如果是midi播放
 	if (audioData.midiRender) {
-		const d = getMidiDuration();
-		return d;
+		// const d = getMidiDuration();
+		const songEndTime = state.times[state.times.length - 1 || 0]?.endtime || 0
+		return audioData.duration || songEndTime;
 	}
 	return audioData.songEle?.duration || audioData.backgroundEle?.duration || audioData.duration;
 };
@@ -159,10 +163,28 @@ export default defineComponent({
 				res?.content?.totalDuration > 1000 &&
 				currentTime >= total
 			) {
+				if (evaluatingData.isAudioPlayEnd) return
+				evaluatingData.isAudioPlayEnd = true
 				onEnded();
 			}
 		};
 
+		// midi播放进度回调
+		const midiProgress = (res: any) => {
+			console.log('api',res)
+			const currentTime = res?.currentTime || res?.content?.currentTime;
+			const total = res?.totalDuration || res?.content?.totalDuration;
+			const time = currentTime / 1000;
+			audioData.progress = time;
+			audioData.duration = total / 1000;
+			if (
+				res?.content?.totalDuration > 1000 &&
+				currentTime >= total
+			) {
+				onEnded();
+			}
+		}
+
 		onMounted(() => {
 			if (state.playMode !== "MIDI") {
 				Promise.all([createAudio(state.music), createAudio(state.accompany)]).then(
@@ -187,6 +209,8 @@ export default defineComponent({
 
 				api_playProgress(progress);
 			}
+			// 监听midi播放进度
+			api_cloudTimeUpdae(midiProgress);
 		});
 
 		// console.log(state.playMode, state.midiUrl);

+ 4 - 0
src/view/audio-list/midiPlayer.tsx

@@ -1,3 +1,7 @@
+/**
+ * h5播放midi
+ */
+
 import { defineComponent, reactive } from "vue";
 import state, { gotoNext, onEnded, onPlay } from "/src/state";
 

+ 6 - 0
src/view/evaluating/index.module.less

@@ -24,4 +24,10 @@
         line-height: 30px;
         cursor: pointer;
     }
+}
+.hiddenPop {
+    width: 1px;
+    height: 1px;
+    overflow: hidden;
+    opacity: 0;
 }

+ 44 - 8
src/view/evaluating/index.tsx

@@ -41,14 +41,25 @@ import { IPostMessage } from "/src/utils/native-message";
 import { usePageVisibility } from "@vant/use";
 import { browser } from "/src/utils";
 import { getAudioCurrentTime, toggleMutePlayAudio } from "../audio-list";
-import { handleStartTick } from "../tick";
+import { handleStartTick, tickData } from "../tick";
 import AbnormalPop from "../abnormal-pop";
 import { storeData } from "../../store";
+import icon_bg from '../abnormal-pop/icon_bg.svg'
+import icon_close from '../abnormal-pop/icon_close.svg'
+import icon_btn from '../abnormal-pop/icon_btn.svg'
+import icon_success from '../abnormal-pop/icon_success.svg'
 
 const browserInfo = browser();
 
 let socketStartTime = 0
 
+export const popImgs = {
+	icon_bg,
+	icon_close,
+	icon_btn,
+	icon_success
+}
+
 export const evaluatingData = reactive({
 	/** 评测数据 */
 	contentData: {} as any,
@@ -83,6 +94,12 @@ export const evaluatingData = reactive({
 	socketErrorStatus: 0,
 	/** 延迟检测,socket状态异常 */
 	delayCheckSocketError: false,
+	/** 异常状态,不生成评测记录,不调用保存接口 */
+	isErrorState: false,
+	/** accompanyError,错误类型 */
+	accompanyErrorType: '',	
+	/** app播放结束状态,重新评测需要重置为 */
+	isAudioPlayEnd: false,
 });
 
 /** 点击开始评测按钮 */
@@ -171,7 +188,7 @@ export const sendEvaluatingOffsetTime = async (currentTime: number) => {
 };
 
 /** 检测耳机 */
-const checkUseEarphone = async () => {
+export const checkUseEarphone = async () => {
 	const res = await getEarphone();
 	return res?.content?.checkIsWired || false;
 };
@@ -277,7 +294,9 @@ const handleScoreResult = (res?: IPostMessage) => {
 			console.log("🚀 ~ 评测返回:", res);
 			// console.log("评测结束", body);
 			state.isHideEvaluatReportSaveBtn = false;
-			evaluatingData.resulstMode = true;
+			setTimeout(() => {
+				evaluatingData.resulstMode = evaluatingData.isErrorState ? false : true
+			}, 200);
 			evaluatingData.resultData = {
 				...body,
 				...getLeveByScore(body.score),
@@ -295,6 +314,7 @@ export const handleStartBegin = async (preTimes?: number) => {
 	evaluatingData.resultData = {};
 	evaluatingData.backtime = 0;
 	resetPlaybackToStart();
+	evaluatingData.isAudioPlayEnd = false;
 	const res = await startEvaluating(evaluatingData.contentData);
 	if (res?.api !== "startEvaluating") {
 		Snackbar.error("请在APP端进行评测");
@@ -312,7 +332,7 @@ export const handleStartBegin = async (preTimes?: number) => {
 		// 设置为开始播放时, 如果需要节拍,先播放节拍器
 		if (state.playState === "play" && state.needTick) {
 			const tickend = await handleStartTick();
-			// console.log("🚀 ~ tickend:", tickend)
+			console.log("🚀 ~ tickend:", tickend)
 			// 节拍器返回false, 取消播放
 			if (!tickend) {
 				state.playState = "paused";
@@ -322,6 +342,7 @@ export const handleStartBegin = async (preTimes?: number) => {
 		}
 		onPlay();
 	}
+	if (evaluatingData.isErrorState) return
 	//开始录音
 	await api_startRecording({
 		accompanimentState: state.setting.enableAccompaniment ? 1 : 0,
@@ -466,7 +487,7 @@ export const handleViewReport = (
 			url = location.origin + location.pathname + "report-share.html?id=" + id;
 			break;
 		case "instrument":
-			url = location.origin + location.pathname + "#/evaluat-report?id=" + id;
+			url = location.origin + location.pathname + "#/evaluat-report?id=" + id + "&musicRenderType=" + state.musicRenderType;
 			break;
 		default:
 			url = location.origin + location.pathname + "report-share.html?id=" + id;
@@ -507,7 +528,8 @@ const handleAccompanyError = (res?: IPostMessage) => {
 				if (evaluatingData.soundEffectMode) {
 					evaluatingData.socketErrorStatus = 0
 					evaluatingData.delayCheckSocketError = true
-					evaluatingData.socketErrorPop = type !== "enterBackground" ? true : false
+					evaluatingData.socketErrorPop = type === "socketError" ? true : false
+					evaluatingData.accompanyErrorType = type
 					// api_checkSocketStatus()
 					return
 				}
@@ -515,8 +537,15 @@ const handleAccompanyError = (res?: IPostMessage) => {
 				if (state.modeType === "evaluating" && evaluatingData.startBegin) {
 					handleCancelEvaluat();
 				}
+				if (tickData.show) {
+					tickData.tickEnd = true
+					tickData.show = false
+				}
 				evaluatingData.socketErrorStatus = 0
-				evaluatingData.socketErrorPop = type !== "enterBackground" ? true : false
+				evaluatingData.socketErrorPop = type === "socketError" ? true : false
+				evaluatingData.isErrorState = true
+				evaluatingData.accompanyErrorType = type
+				resetPlaybackToStart();
 				break;	
 			case "recordError":
 				// 录音异常
@@ -597,7 +626,7 @@ export default defineComponent({
 
 		watch(pageVisibility, (value) => {
 			if (value == "hidden" && evaluatingData.startBegin) {
-				handleEndBegin();
+				// handleEndBegin();
 			}
 		});
 		watch(
@@ -650,6 +679,13 @@ export default defineComponent({
 		});
 		return () => (
 			<div>
+				{/** 预加载一下断网需要用到的图片 */}
+				<div class={styles.hiddenPop}>
+					<img src={popImgs.icon_bg} />
+					<img src={popImgs.icon_btn} />
+					<img src={popImgs.icon_success} />
+					<img src={popImgs.icon_close} />
+				</div>
 				<Popup teleport="body" closeOnClickOverlay={false} class={["popup-custom", "van-scale"]} transition="van-scale" v-model:show={evaluatingData.socketErrorPop}>
 					<AbnormalPop 
 						onConfirm={hanldeConfirmPop}

+ 48 - 24
src/view/fingering/fingering-config.ts

@@ -21,6 +21,8 @@ export type IFingering = {
   code?: string;
   /** 是否有替指 */
   hasTizhi?: boolean;
+  /** 乐器code匹配的id */
+  id?: number;
 };
 
 type ITypeContent = {
@@ -230,21 +232,20 @@ export const mappingVoicePart = (id: number | string, soruce: "GYM" | "COLEXIU"
 export const matchVoicePart = (id: number | string, type: "SINGLE" | "CONCERT"): number => {
   if (type === "SINGLE") {
     const subject: { [_key: string | number]: any } = {
-      1: "pan-flute",
-      2: "ocarina",
-      3: "hulusi-flute",
-      4: "piccolo",
-      5: "melodica",
-      6: "baroque-recorder",
-      7: "",
-      8: 2,
-      9: 5,
-      10: 4,
+      33: "pan-flute",
+      34: "ocarina",
+      35: "hulusi-flute",
+      37: "piccolo",
+      36: "melodica",
+      38: "baroque-recorder",
+      1: 2,
+      5: 5,
+      3: 4,
       11: 12,
-      12: 14,
-      13: 13,
+      13: 14,
+      12: 13,
       14: 15,
-      15: 17,
+      16: 17,
     };
     return subject[id];
   } else {
@@ -314,11 +315,19 @@ export const matchVoicePart = (id: number | string, type: "SINGLE" | "CONCERT"):
       ocarina: "ocarina",
       nai: "melodica",
 	    BaroqueRecorder: 'baroque-recorder',
+      'Drum Set': 24,
+      'Marimba': 26,
+      'Vibraphone': 27,
+      'Tubular Bells': 30,
+      'Mallets': 32,
     };
     let _track;
     if (typeof code === "string") {
+      code = code.toLocaleLowerCase().replace(/ /g, "");
       for (let sKey in subject) {
-        if (sKey === code) {
+        let pitchKey = sKey
+        if (typeof sKey === "string") pitchKey = pitchKey.toLocaleLowerCase().replace(/ /g, "");
+        if (pitchKey === code) {
           _track = subject[sKey];
           break;
         }
@@ -340,6 +349,7 @@ export const subjectFingering = (subjectId: number | string): IFingering => {
         direction: "transverse",
         height: "1.6rem",
         hasTizhi: true,
+        id: 1,
       };
     case 4: // 单簧管
       return {
@@ -347,6 +357,7 @@ export const subjectFingering = (subjectId: number | string): IFingering => {
         direction: "vertical",
         width: "3rem",
         hasTizhi: true,
+        id: 3,
       };
     case 5: // 萨克斯
     case 6: // 中音萨克斯
@@ -355,6 +366,7 @@ export const subjectFingering = (subjectId: number | string): IFingering => {
         direction: "vertical",
         width: "4.34rem",
         hasTizhi: true,
+        id: 5,
       };
     case 12: // 小号
       return {
@@ -362,6 +374,7 @@ export const subjectFingering = (subjectId: number | string): IFingering => {
         direction: "transverse",
         height: "1.6rem",
         hasTizhi: false,
+        id: 11,
       };
     case 13: // 圆号
       return {
@@ -369,6 +382,7 @@ export const subjectFingering = (subjectId: number | string): IFingering => {
         direction: "vertical",
         width: "4.98rem",
         hasTizhi: false,
+        id: 12,
       };
     case 14: // 长号
       return {
@@ -376,6 +390,7 @@ export const subjectFingering = (subjectId: number | string): IFingering => {
         direction: "transverse",
         height: "1.6rem",
         hasTizhi: false,
+        id: 13,
       };
     case 15: // 上低音号
       return {
@@ -383,6 +398,7 @@ export const subjectFingering = (subjectId: number | string): IFingering => {
         direction: "vertical",
         width: "4.34rem",
         hasTizhi: false,
+        id: 14,
       };
     case 17: // 大号
       return {
@@ -390,6 +406,7 @@ export const subjectFingering = (subjectId: number | string): IFingering => {
         direction: "vertical",
         width: "4.34rem",
         hasTizhi: false,
+        id: 16,
       };
     case 120: // 短笛
       return {
@@ -398,6 +415,7 @@ export const subjectFingering = (subjectId: number | string): IFingering => {
         width: "3rem",
         orientation: 1,
         hasTizhi: true,
+        id: 2,
       };
     case "piccolo": // 德式竖笛
       return {
@@ -407,6 +425,7 @@ export const subjectFingering = (subjectId: number | string): IFingering => {
         orientation: 1,
         code: "竖笛",
         hasTizhi: true,
+        id: 37,
       };
     case "hulusi-flute": // 葫芦丝
       return {
@@ -416,6 +435,7 @@ export const subjectFingering = (subjectId: number | string): IFingering => {
         orientation: 1,
         code: "葫芦丝",
         hasTizhi: false,
+        id: 35,
       };
     case "pan-flute": // 排箫
       return {
@@ -426,6 +446,7 @@ export const subjectFingering = (subjectId: number | string): IFingering => {
         orientation: 0,
         code: "排箫",
         hasTizhi: false,
+        id: 33,
       };
     case "ocarina": // 陶笛
       return {
@@ -436,6 +457,7 @@ export const subjectFingering = (subjectId: number | string): IFingering => {
         orientation: 0,
         code: "陶笛",
         hasTizhi: false,
+        id: 34,
       };
     case "melodica": // 口风琴
       return {
@@ -445,18 +467,20 @@ export const subjectFingering = (subjectId: number | string): IFingering => {
         orientation: 0,
         code: "口风琴",
         hasTizhi: false,
+        id: 36,
       };
-	case "baroque-recorder": // 英式竖笛
-	  return {
-		name: "baroque-recorder",
-		direction: "vertical",
-		width: "3rem",
-		orientation: 1,
-		code: "竖笛",
-		hasTizhi: true,
-	  };	  
+    case "baroque-recorder": // 英式竖笛
+      return {
+        name: "baroque-recorder",
+        direction: "vertical",
+        width: "3rem",
+        orientation: 1,
+        code: "竖笛",
+        hasTizhi: true,
+        id: 38,
+      };	  
     default:
-      return {};
+      return typeof subjectId === 'number' ? { id: subjectId } : {};
   }
 };
 

+ 57 - 15
src/view/follow-practice/index.tsx

@@ -1,10 +1,18 @@
 import { defineComponent, onMounted, onUnmounted, reactive, ref } from "vue";
-import state, { gotoNext, resetPlaybackToStart } from "/src/state";
+import state, { gotoNext, resetPlaybackToStart, followBeatPaly } from "/src/state";
 import { IPostMessage } from "/src/utils/native-message";
 import { api_cloudFollowTime, api_cloudToggleFollow } from "/src/helpers/communication";
 import { storeData } from "/src/store";
 import { audioRecorder } from "./audioRecorder";
 import { handleStartTick } from "/src/view/tick";
+import { metronomeData } from "/src/helpers/metronome";
+import { getDuration } from "/src/helpers/formateMusic";
+import { OpenSheetMusicDisplay } from "/osmd-extended/src";
+import { browser, getBehaviorId } from "/src/utils";
+import { api_musicPracticeRecordSave } from "../../page-instrument/api";
+import { getQuery } from "/src/utils/queryString";
+
+const query: any = getQuery();
 
 export const followData = reactive({
 	list: [] as any, // 频率列表
@@ -15,6 +23,25 @@ export const followData = reactive({
 	earphone: false,
 });
 
+// 记录跟练时长
+const handleRecord = (total: number) => {
+	if (query.isCbs) return
+	if (total < 0) total = 0;
+	const totalTime = total / 1000;
+
+	const body = {
+		clientType: storeData.user.clientType,
+		musicSheetId: state.examSongId,
+		sysMusicScoreId: state.examSongId,
+		feature: "FOLLOW_UP_TRAINING",
+		practiceSource: "FOLLOW_UP_TRAINING",
+		playTime: totalTime,
+		deviceType: browser().android ? "ANDROID" : "IOS",
+		behaviorId: getBehaviorId(),
+	};
+	api_musicPracticeRecordSave(body);
+};
+
 /** 点击跟练模式 */
 export const toggleFollow = (notCancel = true) => {
 	state.modeType = state.modeType === "follow" ? "practise" : "follow";
@@ -30,13 +57,14 @@ const audioFrequency = ref(0);
 const followTime = ref(0);
 // 切换录音
 const openToggleRecord = async (open: boolean = true) => {
-	api_cloudToggleFollow(open ? "start" : "end");
+	if (!open) api_cloudToggleFollow(open ? "start" : "end");
 	// 记录跟练时长
 	if (open) {
 		followTime.value = Date.now();
 	} else {
 		const playTime = Date.now() - followTime.value;
 		if (followTime.value !== 0 && playTime > 0) {
+			handleRecord(playTime);
 			followTime.value = 0;
 		}
 	}
@@ -67,23 +95,37 @@ const onClear = () => {
 
 /** 开始跟练 */
 export const handleFollowStart = async () => {
-	// 跟练模式开始前,增加播放系统节拍器
-	const tickend = await handleStartTick();
-	// console.log("🚀 ~ tickend:", tickend)
-	// 节拍器返回false, 取消播放
-	if (!tickend) {
-		return false;
+	const res = await api_cloudToggleFollow("start");
+	// 用户没有授权,需要重置状态
+	if (res?.content?.reson) {
+		// 
+	} else {
+		// 跟练模式开始前,增加播放系统节拍器
+		followData.start = true;
+		const tickend = await handleStartTick();
+		// console.log("🚀 ~ tickend:", tickend)
+		// 节拍器返回false, 取消播放
+		if (!tickend) {
+			followData.start = false;
+			return false;
+		}
+		onClear();
+		followData.start = true;
+		followData.index = 0;
+		followData.list = [];
+		resetPlaybackToStart();
+		openToggleRecord(true);
+		getNoteIndex();
+		const duration: any = getDuration(state.osmd as unknown as OpenSheetMusicDisplay);
+		metronomeData.totalNumerator = duration.numerator || 2
+		metronomeData.followAudioIndex = 1
+		state.beatStartTime = 0
+		followBeatPaly();		
 	}
-	onClear();
-	followData.start = true;
-	followData.index = 0;
-	followData.list = [];
-	resetPlaybackToStart();
-	openToggleRecord(true);
-	getNoteIndex();
 };
 /** 结束跟练 */
 export const handleFollowEnd = () => {
+	onClear();
 	followData.start = false;
 	openToggleRecord(false);
 	followData.index = 0;

+ 2 - 1
src/view/music-score/index.module.less

@@ -5,6 +5,7 @@
         overflow-y: auto;
         height: 100%;
         max-height: 100vh;
+        transform: translateY(-5%);
         &::-webkit-scrollbar {
             width: 0;
             display: none;
@@ -26,7 +27,7 @@
 .inGradualRange{
    :global{
         #cursorImg-0{
-            opacity: 0;
+            opacity: 0 !important;
         }
    } 
 }

+ 31 - 5
src/view/music-score/index.tsx

@@ -7,9 +7,11 @@ import Selection from "../selection";
 import styles from "./index.module.less";
 import queryString from "query-string";
 import { getGradualLengthByXml } from "/src/helpers/calcSpeed";
+import { resetFormate, resetGivenFormate, setGlobalMusicSheet } from "/src/helpers/customMusicScore"
+import { setGlobalData } from "/src/utils";
 
 export const musicRenderTypeKey = "musicRenderType";
-
+let osmd: any = null;
 const musicData = reactive({
 	showSelection: false, // 可以加载点击浮层
 	isRenderLoading: true,
@@ -21,14 +23,29 @@ const musicData = reactive({
 export const resetMusicScore = () => {
 	const contaienrWidth = document.getElementById("musicAndSelection")?.offsetWidth || 625;
 	state.musicZoom = contaienrWidth / musicData.containerWidth;
+	// if (state.fingeringInfo?.name && state.fingeringInfo?.direction === 'vertical') {
+	// 	if (contaienrWidth > musicData.containerWidth) {
+	// 		setGlobalData('wrapNum', 8)
+	// 	} else {
+	// 		setGlobalData('wrapNum', 6)
+	// 	}
+	// 	musicData.showSelection = false
+	// 	osmd.zoom = state.zoom;
+	// 	osmd.render();
+	// 	setTimeout(() => {
+	// 		musicData.showSelection = true
+	// 	}, 100);
+	// }
+	
 };
 
 /** 重新渲染曲谱 */
-export const resetRenderMusicScore = () => {
+export const resetRenderMusicScore = (type?: string) => {
 	const search = queryString.parse(location.search);
 	const newSearch = queryString.stringify({
 		...search,
 		_t: Date.now(),
+		musicRenderType: type
 	});
 	location.search = "?" + newSearch;
 };
@@ -46,6 +63,10 @@ export default defineComponent({
 			type: String,
 			default: "",
 		},
+		musicColor: {
+			type: String,
+			default: "",
+		},
 	},
 	setup(props, { emit }) {
 		/** 设置 曲谱模式,五线谱还是简谱 */
@@ -66,7 +87,8 @@ export default defineComponent({
 		const init = async () => {
 			const container = document.getElementById("musicAndSelection");
 			if (!container || !musicData.score) return;
-			const osmd = new OpenSheetMusicDisplay(container, {
+			setGlobalMusicSheet();
+			osmd = new OpenSheetMusicDisplay(container, {
 				drawTitle: false,
 				drawSubtitle: false,
 				// drawMeasureNumbers: false,
@@ -74,12 +96,13 @@ export default defineComponent({
 				followCursor: false,
 				drawPartNames: false, // 是否渲染声部
 				drawComposer: false, // 渲染作者
+				defaultColorMusic: props.musicColor, // 颜色
 				// autoBeam: true,
 				// drawMetronomeMarks: false,
 				// drawLyricist: false,
 				// ...this.opotions,
+				
 			});
-
 			// osmd.EngravingRules.CompactMode = true // 紧凑模式
 			osmd.EngravingRules.PageRightMargin = 2;
 			osmd.EngravingRules.PageTopMargin = 10;
@@ -100,6 +123,8 @@ export default defineComponent({
 			osmd.render();
 			// console.log("🚀 ~ osmd:", osmd)
 			emit("rendered", osmd);
+			resetFormate();
+			resetGivenFormate();
 			musicData.showSelection = true;
 		};
 		/** 获取渲染容器的宽度 */
@@ -120,6 +145,7 @@ export default defineComponent({
 			const activeMeasureIndex = state.times[state.activeNoteIndex]?.measureListIndex || -1;
 			for (const [first, last] of state.gradual) {
 				if (first && last) {
+					// console.log('小节',first.measureIndex,last.measureIndex,activeMeasureIndex)
 					result = first.measureIndex <= activeMeasureIndex && activeMeasureIndex < last.measureIndex;
 					if (result) {
 						break;
@@ -137,7 +163,7 @@ export default defineComponent({
 					state.musicRenderType == EnumMusicRenderType.staff ? "staff" : "jianpuTone",
 				]}
 			>
-				{props.showSelection && musicData.showSelection && <Selection />}
+				{props.showSelection && musicData.showSelection && !state.isPreView && !state.isEvaluatReport && <Selection />}
 			</div>
 		);
 	},

BIN
src/view/plugins/move-music-score/image/right_hide_icon.png


+ 16 - 0
src/view/plugins/move-music-score/index.module.less

@@ -38,4 +38,20 @@
     .noteMove{
         display: none;
     }
+}
+
+.hideTool {
+    transform: translateX(-120%);
+} 
+
+.rightHideIcon {
+    width: 15px;
+    height: 30px;
+    position: absolute;
+    left: 0;
+    top: 50%;
+    z-index: 10;
+    cursor: pointer;
+    transition: all 0.5s;
+    transform: rotate(180deg);
 }

+ 187 - 53
src/view/plugins/move-music-score/index.tsx

@@ -1,5 +1,5 @@
 import { Row, showToast } from "vant";
-import { defineComponent, onMounted, reactive } from "vue";
+import { defineComponent, onMounted, reactive, nextTick, ref } from "vue";
 import state from "/src/state";
 import request from "/src/utils/request";
 import { getQuery } from "/src/utils/queryString";
@@ -8,8 +8,13 @@ import { Button, ButtonGroup, Icon, Switch, Tooltip } from "@varlet/ui";
 import "@varlet/ui/es/tooltip/style";
 import "@varlet/ui/es/button-group/style";
 import "@varlet/ui/es/switch/style";
+import { storeData } from "/src/store";
+import rightHideIcon from './image/right_hide_icon.png';
 
 let extStyleConfigJson: any = {};
+const clientWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
+const showToolBox = ref(true);
+
 export const moveData = reactive({
 	/** 开启移动 */
 	open: false,
@@ -23,8 +28,43 @@ export const moveData = reactive({
 	tool: {
 		isAddAndSub: false,
 	},
+	noteCoords: [] as any[],
 });
 
+// 所以可点击音符的dom坐标集合
+const initNoteCoord = () => {
+	const allNoteDot: any = Array.from(document.querySelectorAll('.node-dot'));
+ 	moveData.noteCoords = allNoteDot.map((note: any) => {
+		const note_bbox = note?.getBoundingClientRect?.() || { x: 0, y: 0 };
+		return {
+			x: note_bbox.x,
+			y: note_bbox.y
+		}
+	})
+}
+
+// 找出离目标元素最近的音符
+const computedDistance = (x: number, y: number) => {
+    let minDistance = -1, minidx = 0;
+    let a, b, c;
+	moveData.noteCoords.forEach((note: any, idx: any) => {
+		//a,b为直角三角形的两个直角边
+		a = Math.abs(note.x - x)
+		b = Math.abs(note.y - y)
+		//c为直角三角形的斜边
+		c = Math.sqrt(a * a + b * b) as 0
+		c = Number(c.toFixed(0)) as 0
+		if (c !== 0 && (minDistance === - 1 || c < minDistance)) {
+			//min为元素中离目标元素最近元素的距离
+			minDistance = c
+			minidx = idx
+		}		
+	})
+    return minidx
+}
+
+
+
 function initSvgId() {
 	const svg = document.querySelector("#osmdSvgPage1");
 	if (!svg) return;
@@ -123,28 +163,48 @@ function getBox(ele: SVGAElement) {
 export const filterMoveData = async () => {
 	const examSongId = state.examSongId;
 	if (examSongId) {
+		const fontSize =  (window as any).fontSize
 		const list = moveData.modelList
 			.filter((n) => n.isMove)
 			.map((n) => {
-				const item: any = {
+				/**
+				 * 找到移动后,此时与此元素距离最近的音符,并记录音符的索引和此元素与音符的x轴,y轴的间距
+				*/
+				// 元素的位置
+				const elementX = n.left + n.x, elementY = n.top + n.y;
+				// 找出距离元素最近的音符
+				const noteIdx = computedDistance(elementX, elementY);
+				// 此元素距离最近音符的x轴,y轴距离
+				const noteRelativeX = elementX - moveData.noteCoords[noteIdx]?.x, noteRelativeY = elementY - moveData.noteCoords[noteIdx]?.y;
+				let item: any = {
 					id: n.id,
 					isMove: n.isMove,
 					isDelete: n.isDelete,
 					x: n.x,
 					y: n.y,
+					xRem: Math.abs(n.x / fontSize),
+					yRem: Math.abs(n.y / fontSize),
 					zoom: n.zoom,
 					w: moveData.sw,
 					type: n.type,
+					noteIdx,
+					noteRelativeX,
+					noteRelativeY
 				};
 				if (n.type === "vf-lineGroup") {
 					item.dx = n.dx;
 				}
+				if (n.id.includes('text')) {
+					// let copyDom = document.querySelector("#" + n.id)!.cloneNode(true) as SVGSVGElement
+					const textContent = document.querySelector("#" + n.id)?.querySelector("text")?.innerHTML || ''
+					item.textContent = textContent
+				}
 				return item;
 			});
-		if (!list.length) {
-			showToast("请移动元素后再保存");
-			return
-		}
+		// if (!list.length) {
+		// 	showToast("请移动元素后再保存");
+		// 	return
+		// }
 		extStyleConfigJson[moveData.partIndex] = list;
 		console.log("🚀 ~ extStyleConfigJson", extStyleConfigJson)
 		const res = await request.post("/musicSheet/img", {
@@ -168,6 +228,7 @@ const dragData = {
 	startY: 0,
 	x: 0,
 	y: 0,
+	repeatEdit: false,
 };
 
 // 记录
@@ -187,6 +248,7 @@ function onDown(e: MouseEvent) {
 		dragData.startY = e.clientY;
 		dragData.x = item.x;
 		dragData.y = item.y;
+		dragData.repeatEdit = item.noteIdx >= 0 ? true : false;
 		// console.log("🚀 ~ 按下", index, el, item.x, item.y);
 		document.onmousemove = onMove;
 		document.onmouseup = onUp;
@@ -205,7 +267,7 @@ function onMove(e: MouseEvent) {
 	if (dragData.open) {
 		const _x = e.clientX - dragData.startX + dragData.x;
 		const _y = e.clientY - dragData.startY + dragData.y;
-		setModelPostion(moveData.modelList[moveData.activeIndex], _x, _y);
+		setModelPostion(moveData.modelList[moveData.activeIndex], _x, _y, dragData.repeatEdit);
 	}
 }
 function onUp(e: MouseEvent) {
@@ -239,16 +301,37 @@ const renderSvgItem = (item: any) => {
 };
 
 /** 设置元素位置 */
-function setModelPostion(item: any, x: number, y: number) {
+async function setModelPostion(item: any, x: number, y: number, repeatEdit?: boolean) {
+	// console.log(item)
+	//console.log('位置',x,y)
 	if (item) {
-		const g = document.querySelector("#" + item.id)!;
-		const el: HTMLElement = document.querySelector(`[data-id=${item.id}]`)!;
+		const g = document.querySelector("#" + item.id)!; // svg元素
+		const el: HTMLElement = document.querySelector(`[data-id=${item.id}]`)!; // svg元素的背景div
 		if (x === 0 && y === 0) {
 			g && g.removeAttribute("transform");
 			el && (el.style.transform = "");
 		} else {
-			g && g.setAttribute("transform", `translate(${x / moveData.zoom}, ${y / moveData.zoom})`);
-			el && (el.style.transform = `translate(${x}px, ${y}px)`);
+			/** 如果是app内嵌打开,需要通过rem转换 */
+			let tsX = x, tsY = y;
+			// if (storeData.isApp && (item.xRem || item.yRem)) {
+			// 	tsX = item.xRem * clientWidth/10
+			// 	tsY = item.yRem * clientWidth/10
+			// }
+			if (item.noteIdx >= 0 && !repeatEdit) {
+				if (!moveData.noteCoords.length) {
+					await initNoteCoord()
+				}
+				const targetX = moveData.noteCoords[item.noteIdx].x + item.noteRelativeX, targetY = moveData.noteCoords[item.noteIdx].y + item.noteRelativeY;
+				const original = document.getElementById(item.id)?.getBoundingClientRect() || { x: 0, y: 0 };
+				tsX = targetX - original.x;
+				tsY = targetY - original.y;
+				// console.log('距离',tsX,tsY,x,y)
+				g && g.setAttribute("transform", `translate(${tsX / moveData.zoom}, ${tsY / moveData.zoom})`);
+				el && (el.style.transform = `translate(${tsX}px, ${tsY}px)`);
+			} else {
+				g && g.setAttribute("transform", `translate(${tsX / moveData.zoom}, ${tsY / moveData.zoom})`);
+				el && (el.style.transform = `translate(${tsX}px, ${tsY}px)`);
+			}
 		}
 	}
 }
@@ -394,19 +477,57 @@ export const renderForMoveData = () => {
 	}
 	const list = extStyleConfigJson?.[moveData.partIndex];
 	if (list && Array.isArray(list)) {
-		console.log("🚀 ~ list", list);
-		list.forEach((item: any) => {
-			const index = moveData.modelList.findIndex((n: any) => n.id === item.id);
-			if (index > -1) {
-				moveData.modelList[index] = {
-					...moveData.modelList[index],
-					...item
-				};
-				renderSvgItem(moveData.modelList[index]);
-				if (item.type === "vf-lineGroup") {
-					renderLineGroup(moveData.modelList[index]);
+		nextTick(() => {
+			console.log("🚀 ~ list", list);
+			list.forEach((item: any) => {
+				let index = moveData.modelList.findIndex((n: any) => n.id === item.id);
+				if (item.type === 'vf-text' && item.textContent) {
+					let textValue = document.querySelector("#" + moveData.modelList[index].id)?.querySelector("text")?.innerHTML || ''
+					let targetIndex = index, preEnd = false, done = false, preIndex = index, nextIndex = index;
+					// while (textValue !== item.textContent) {
+					// 	if (preEnd) {
+					// 		targetIndex = targetIndex + 1
+					// 	} else {
+					// 		targetIndex = targetIndex > 0 ? targetIndex - 1 : targetIndex
+					// 	}
+					// 	if (targetIndex == 0) preEnd = true
+					// 	textValue = document.querySelector("#" + moveData.modelList[targetIndex].id)?.querySelector("text")?.innerHTML || ''
+					// }
+					if (textValue !== item.textContent) {
+						while (!done) {
+							let text1 = moveData.modelList[preIndex] ? document.querySelector("#" + moveData.modelList[preIndex].id)?.querySelector("text")?.innerHTML || '' : ''
+							let text2 = moveData.modelList[nextIndex] ? document.querySelector("#" + moveData.modelList[nextIndex].id)?.querySelector("text")?.innerHTML || '' : ''
+							if (text1 === item.textContent || text2 === item.textContent) {
+								done = true
+								targetIndex = text1 === item.textContent ? preIndex : nextIndex
+							} else {
+								// 有可能后台编辑的元素在部分屏幕尺寸下没有该元素,比如小节索引数,可能后台显示的是1,3,5,部分屏幕尺寸显示的1,3,6
+								if (!text1 && !text2) {
+									done = true
+									targetIndex = -1
+								}
+								preIndex = preIndex - 1
+								nextIndex = nextIndex + 1
+							}
+						}
+					}
+					// index = targetIndex + 1
+					// item.id = `text${index}`
+					index = targetIndex
+					item.id = `text${targetIndex+1}`
 				}
-			}
+				// console.log(66666666,index)
+				if (index > -1) {
+					moveData.modelList[index] = {
+						...moveData.modelList[index],
+						...item
+					};
+					renderSvgItem(moveData.modelList[index]);
+					if (item.type === "vf-lineGroup") {
+						renderLineGroup(moveData.modelList[index]);
+					}
+				}
+			});
 		});
 	}
 };
@@ -422,42 +543,55 @@ export default defineComponent({
 			// 	initSvgId();
 			// }
 			// renderForMoveData();
+			nextTick(() => initNoteCoord())
 			const toolBox = document.getElementById("toolBox");
 			toolBox && document.body.appendChild(toolBox);
 		});
 		return () => (
 			<div class={[moveData.open ? "" : styles.moveDisabled]}>
-				<div class={styles.toolBox} id="toolBox">
-					<Switch v-model={moveData.open} />
-					{moveData.open && (
-						<>
-							{moveData.tool.isAddAndSub && (
-								<ButtonGroup size="small" elevation={false}>
-									<Button onClick={() => handleAddAndSub('add')}>加</Button>
-									<Button onClick={() => handleAddAndSub('sub')}>减</Button>
-								</ButtonGroup>
-							)}
-							{/* <ButtonGroup size="small">
-								
-								<Button>
-									<Icon name="arrow-down" style={{ transform: "rotate(-90deg)" }} />
+				<div id="toolBox">
+					<div class={[styles.toolBox, !showToolBox.value && styles.hideTool]} >
+						<Switch v-model={moveData.open} />
+						{moveData.open && (
+							<>
+								{moveData.tool.isAddAndSub && (
+									<ButtonGroup size="small" elevation={false}>
+										<Button onClick={() => handleAddAndSub('add')}>加</Button>
+										<Button onClick={() => handleAddAndSub('sub')}>减</Button>
+									</ButtonGroup>
+								)}
+								{/* <ButtonGroup size="small">
+									
+									<Button>
+										<Icon name="arrow-down" style={{ transform: "rotate(-90deg)" }} />
+									</Button>
+								</ButtonGroup> */}
+								<Button size="small" onClick={handleUndo} disabled={undoData.undoList.length ? false : true}>
+									<Icon name="arrow-down" style={{ transform: "rotate(90deg)" }} />
 								</Button>
-							</ButtonGroup> */}
-							<Button size="small" onClick={handleUndo} disabled={undoData.undoList.length ? false : true}>
-								<Icon name="arrow-down" style={{ transform: "rotate(90deg)" }} />
-							</Button>
 
-							<Button size="small" onClick={handleDeleteMoveNote} disabled={moveData.activeIndex > -1 ? false : true}>
-								{moveData.modelList[moveData.activeIndex]?.isDelete ? '显示元素' : '删除元素'}
-							</Button>
-							<Button size="small" onClick={resetMoveNote}>
-								重置数据
-							</Button>
-							<Button size="small" type="primary" onClick={filterMoveData}>
-								保存数据
-							</Button>
-						</>
-					)}
+								<Button size="small" onClick={handleDeleteMoveNote} disabled={moveData.activeIndex > -1 ? false : true}>
+									{moveData.modelList[moveData.activeIndex]?.isDelete ? '显示元素' : '删除元素'}
+								</Button>
+								<Button size="small" onClick={resetMoveNote}>
+									重置数据
+								</Button>
+								<Button size="small" type="primary" onClick={filterMoveData}>
+									保存数据
+								</Button>
+								<Button size="small" type="primary" onClick={() => showToolBox.value = false}>
+									收起
+								</Button>
+							</>
+						)}
+					</div>
+					{
+					!showToolBox.value && 
+						<img 
+							class={[styles.rightHideIcon, !showToolBox.value ? styles.rightIconShow : '']} 
+							src={rightHideIcon}
+							onClick={() => showToolBox.value = true } />
+					}  
 				</div>
 				{moveData.modelList.map((item: any, index: number) => {
 					return (

+ 8 - 0
src/view/plugins/toggleMusicSheet/choosePartName/index.module.less

@@ -30,6 +30,14 @@
   .picker {
     flex: 1;
     height: 100px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    :global {
+      .van-picker__columns {
+        //height: 80% !important;
+      }
+    }
   }
 
   .button {

+ 12 - 3
src/view/plugins/toggleMusicSheet/choosePartName/index.tsx

@@ -16,12 +16,14 @@ export default defineComponent({
   },
   emits: ['close'],
   setup(props, { emit }) {
+    // #9463 bug,未更换声轨点击确定不应该重新加载,现在会导致切换错误
+    const partIndexChanged = ref(false);
     const { partListNames, partIndex } = toRefs(props)
     const selectIndex = ref((partListNames.value[partIndex.value] as any).value)
     const columns = computed(() => {
       return partListNames.value
     })
-    // console.log(partListNames.value, partIndex.value, selectIndex.value, columns.value, 999999)
+    // console.log(1111,partListNames.value, partIndex.value, selectIndex.value, columns.value, 999999)
     /**
      * 默认选中的
      * picker组件,3.x的版本可以使用defaultIndex,4.x的版本只能使用v-model传递
@@ -46,12 +48,19 @@ export default defineComponent({
           columns={columns.value}
           visibleItemCount={Math.ceil(document.body.clientHeight / 44 / 3)}
           onChange={(row) => {
-            // console.log('选择的索引', row)
+            // console.log(1111,'选择的索引', row)
+            if (!partIndexChanged.value) partIndexChanged.value = true
             selectIndex.value = row.selectedValues[0]
           }}
         />
         <Button class={styles.button} type="primary" round block onClick={() => {
-          emit('close', selectIndex.value)}
+            console.log(1111,selectIndex.value)
+            if (partIndexChanged.value) {
+              emit('close', selectIndex.value)
+            } else {
+              emit('close', partIndex.value)
+            }
+          }
         }>
           确定
         </Button>

+ 5 - 2
src/view/plugins/toggleMusicSheet/index.tsx

@@ -21,10 +21,12 @@ export default defineComponent({
 
     const partListNames = computed(() => {
       let partList = state.partListNames || []
+      console.log(777777,state.partListNames)
       partList = partList.filter((item: any) => !item?.toLocaleUpperCase()?.includes('COMMON'))
-      return partList.map((item: any, index: number) => {
+      const arr =  partList.map((item: any, index: number) => {
         // 该声轨能否被选
         const canselect = state.canSelectTracks.length == 0 || state.canSelectTracks.includes(item) ? true : false
+        // console.log(canselect,index)
         const instrumentName = getInstrumentName(item)
         const sortId = sortMusical(instrumentName, index)
         return {
@@ -34,12 +36,13 @@ export default defineComponent({
           canselect
         }
       }).filter((item: any) => item.canselect).sort((a: any, b: any) => a.sortId - b.sortId)
+      return arr
     })
 
     const trackIdx: any = computed(() => {
       if (partListNames && partListNames.value.length) {
         
-        const idx = partListNames.value.find((item: any) => item.value == state.partIndex).value
+        const idx = partListNames.value.find((item: any) => item.value == state.partIndex)?.value || 0
         console.log(3333,idx)
         return idx
       } else {

+ 18 - 5
src/view/selection/index.module.less

@@ -13,6 +13,7 @@
 
 .note {
     cursor: pointer;
+    // background: rgba(0,0,0,0.3);
 }
 
 
@@ -102,7 +103,9 @@
     opacity: var(--corsor-opacity);
     transform: translate(4PX, -50%);
 }
-
+.eyeLine {
+    background-color: rgb(255, 159, 88);
+}
 .lineStaff {
     width: 14PX;
 }
@@ -137,19 +140,19 @@
 
 :global {
     .scoreItemLeve0 {
-        background-color: rgba(255, 142, 142, 0.32);
+        background-color: rgba(255, 142, 142, 0.32) !important;
     }
 
     .scoreItemLeve1 {
-        background-color: rgba(1, 193, 181, 0.2);
+        background-color: rgba(1, 193, 181, 0.2) !important;
     }
 
     .scoreItemLeve2 {
-        background-color: rgba(255, 178, 82, 0.37);
+        background-color: rgba(255, 178, 82, 0.37) !important;
     }
 
     .scoreItemLeve3 {
-        background-color: rgba(255, 220, 64, 0.4);
+        background-color: rgba(255, 220, 64, 0.4) !important;
     }
 
     .centerTop-enter-active {
@@ -241,4 +244,14 @@
         min-height: 94Px;
         transform: translateX(8.5Px,-50%);
     } 
+}
+
+.noteDot {
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%);
+    width: 2px;
+    height: 2px;
+    // background: #07c160;
 }

+ 12 - 4
src/view/selection/index.tsx

@@ -1,4 +1,4 @@
-import { computed, defineComponent, onMounted, reactive, Transition } from "vue";
+import { computed, defineComponent, onMounted, reactive, Transition, nextTick } from "vue";
 import state, { EnumMusicRenderType, handleSelection, skipNotePlay, IPlatform } from "/src/state";
 import styles from "./index.module.less";
 import { metronomeData } from "/src/helpers/metronome";
@@ -12,6 +12,7 @@ import { getQuery } from "/src/utils/queryString";
 const selectData = reactive({
 	notes: [] as any[],
 	staves: [] as any[],
+	measureHeight: 0 as number, // 小节高度
 });
 
 /** 计算点击层数据 */
@@ -107,7 +108,8 @@ const calcNoteData = () => {
 						}
 					} catch (error) {}
 
-					// console.log("🚀 ~ staveEle:", staveEle)
+					// console.log("🚀 ~ staveEle:", staveBbox)
+					selectData.measureHeight = staveBbox.height
 					noteItem.staveBox = {
 						left: staveBbox.x - parentLeft + "px",
 						// top: ((item.stave.y || 0) - 5) * state.zoom + "px",
@@ -129,6 +131,7 @@ const calcNoteData = () => {
 							left: preItem.staveBox.left,
 							top: preItem.staveBox.top,
 							width: preItem.staveBox.width,
+							// height: preItem.staveBox.height,
 						};
 						selectData.staves.push(noteItem);
 						MeasureNumberXMLList.push(item.MeasureNumberXML);
@@ -188,6 +191,9 @@ export default defineComponent({
 								return styles.leftStaveBox;
 							}
 							if (item.MeasureNumberXML == actualEndIndex) {
+								if (!item.staveBox?.height) {
+									item.staveBox.height = selectData.measureHeight + 'px'
+								}
 								return styles.rightStaveBox;
 							}
 							return styles.staveBox;
@@ -205,7 +211,7 @@ export default defineComponent({
 			// 初始化谱面可移动的元素位置
 			try {
 			moveData.partIndex = query['part-index'] as string || '0'
-			renderForMoveData()
+			nextTick(() => renderForMoveData())
 			} catch (error) {}
 		});
 		return () => (
@@ -242,7 +248,7 @@ export default defineComponent({
 										styles.position,
 										showClass.value(item),
 										scoreItem ? `scoreItemLeve${scoreItem.leve}` : "",
-										state.platform === IPlatform.PC ? styles.linePC : ''
+										state.platform === IPlatform.PC ? styles.linePC : '',
 									]}
 									style={item.staveBox}
 									onClick={() => handleSelection(item)}
@@ -251,6 +257,7 @@ export default defineComponent({
 										<div 
 										class={[
 											styles.line,
+											state.setting.eyeProtection ? styles.eyeLine : '',
 											state.musicRenderType == EnumMusicRenderType.staff ? styles.lineStaff : styles.lineJianPu,
 										]} 
 										style={{ left: metronomeData.activeMetro.left }}></div>
@@ -292,6 +299,7 @@ export default defineComponent({
 								<Icon name="success" />
 								<Icon name="cross" />
 							</div>
+							<div class={[styles.noteDot, 'node-dot']}></div>
 						</div>
 					);
 				})}

+ 55 - 5
src/view/tick/index.tsx

@@ -1,11 +1,15 @@
-import { defineComponent, reactive } from "vue";
+import { defineComponent, reactive, onMounted } from "vue";
 import tockAndTick from "/src/constant/tockAndTick.json";
 import { Howl } from "howler";
 import { Popup } from "vant";
 import styles from "./index.module.less";
 import state from "/src/state";
+import { browser } from "/src/utils/index";
+import tickWav from "/src/assets/tick.wav";
+import tockWav from "/src/assets/tock.wav";
 
-const tickData = reactive({
+const browserInfo = browser();
+export const tickData = reactive({
 	list: [] as number[],
 	len: 0,
 	tickEnd: false,
@@ -18,7 +22,7 @@ const tickData = reactive({
 	show: false,
 });
 
-const handlePlay = (i: number, source: Howl | null) => {
+const handlePlay = (i: number, source: any | null) => {
 	return new Promise((resolve) => {
 		setTimeout(() => {
 			if (tickData.tickEnd) {
@@ -26,12 +30,40 @@ const handlePlay = (i: number, source: Howl | null) => {
 				return
 			};
 			tickData.index++;
-			if (source) source.play();
+			if (source) {
+				const beatVolume = state.setting.beatVolume / 100
+				source.volume = beatVolume;
+				if (source.volume <= 0) {
+					source.muted = true
+				} else {
+					source.muted = false
+				}
+				source.play();
+			}
 			resolve(i);
 		}, tickData.beatLengthInMilliseconds);
 	});
 };
 
+// HTMLAudioElement 音频
+const audioData = reactive({
+	tick: null as unknown as HTMLAudioElement,
+	tock: null as unknown as HTMLAudioElement,
+});
+
+const createAudio = (src: string): Promise<HTMLAudioElement | null> => {
+	return new Promise((resolve) => {
+		const a = new Audio(src + '?v=' + Date.now());
+		a.load();
+		a.onloadedmetadata = () => {
+			resolve(a);
+		};
+		a.onerror = () => {
+			resolve(null);
+		};
+	});
+};
+
 /** 设置节拍器
  * @param beatLengthInMilliseconds 节拍间隔时间
  * @param beat 节拍数
@@ -49,7 +81,10 @@ export const handleStartTick = async () => {
 	if (tickData.state !== "ok") {
 		tickData.source1 = new Howl({
 			src: tockAndTick.tick,
+			// 如果是ios手机,需要强制使用audio,不然部分系统版本第一次播放没有声音
+			html5: browserInfo.ios,
 		});
+
 		tickData.source2 = new Howl({
 			src: tockAndTick.tock,
 		});
@@ -60,7 +95,10 @@ export const handleStartTick = async () => {
 	for(let i = 0; i <= tickData.len; i++){
 		// 提前结束, 直接放回false
 		if (tickData.tickEnd) return false;
-		const source = i === 0 ? tickData.source1 : i === tickData.len ? null : tickData.source2;
+		// Howl 插件播放音频
+		// const source = i === 0 ? tickData.source1 : i === tickData.len ? null : tickData.source2;
+		// Audio 标签播放音频
+		const source = i === 0 ? audioData.tick : i === tickData.len ? null : audioData.tock;
 		await handlePlay(i, source)
 	}
 	tickData.show = false;
@@ -74,6 +112,18 @@ export default defineComponent({
 		const handleClose = () => {
 			tickData.tickEnd = true
 		};
+		onMounted(() => {
+			Promise.all([createAudio(tickWav), createAudio(tockWav)]).then(
+				([tick, tock]) => {
+					if (tick) {
+						audioData.tick = tick;
+					}
+					if (tock) {
+						audioData.tock = tock;
+					}
+				}
+			);
+		});		
 		return () => (
 			<Popup class={styles.popup} v-model:show={tickData.show} closeable onClickCloseIcon={handleClose}>
 				<div class={styles.dots}>

+ 1 - 1
src/view/transfer-to-img/index.tsx

@@ -39,7 +39,7 @@ export default defineComponent({
 
 		onMounted(() => {
 			(window as any).appName = "colexiu";
-			state.xmlUrl = query.xmlUrl;
+			state.xmlUrl = decodeURIComponent(query.xmlUrl);
 			//课堂乐器,默认简谱
 			sessionStorage.setItem(productRenderType, detailData.product[detailData.step].type);
 

+ 3 - 2
vite.config.ts

@@ -33,7 +33,7 @@ export default defineConfig({
 	build: {
 		rollupOptions: {
 			input: {
-				index: resolve(__dirname, "index.html"),
+				gym: resolve(__dirname, "index.html"),
 				colexiu: resolve(__dirname, "colexiu.html"),
 				orchestra: resolve(__dirname, "orchestra.html"),
 				"report-share": resolve(__dirname, "report-share.html"),
@@ -67,7 +67,8 @@ export default defineConfig({
 				// target: "https://kt.colexiu.com",
 				// target: "https://test.lexiaoya.cn",
 				// target: "https://dev.kt.colexiu.com",
-				// target: "https://dev.resource.colexiu.com", // 内容平台开发环境
+				// target: "https://test.resource.colexiu.com", // 内容平台开发环境,内容平台开发,需在url链接上加上isCbs=true
+				// target: "https://test.resource.colexiu.com",
 				target: "https://test.kt.colexiu.com",
 				changeOrigin: true,
 				rewrite: (path) => path.replace(/^\/instrument/, ""),

Some files were not shown because too many files changed in this diff