Jelajahi Sumber

Merge branch 'new-feature-tianyong' into ktyq-pre

TIANYONG 1 tahun lalu
induk
melakukan
5daa7be184
57 mengubah file dengan 3657 tambahan dan 491 penghapusan
  1. 53 0
      src/constant/instruments.ts
  2. 41 0
      src/helpers/communication.ts
  3. 792 0
      src/helpers/customMusicScore.ts
  4. 34 22
      src/helpers/formateMusic.ts
  5. 22 1
      src/helpers/metronome.ts
  6. 1 1
      src/page-instrument/App.tsx
  7. 0 0
      src/page-instrument/component/mode-type-mode/icon/index.json
  8. 6 3
      src/page-instrument/component/mode-type-mode/index.tsx
  9. 6 3
      src/page-instrument/custom-plugins/guide-page/api.ts
  10. 3 3
      src/page-instrument/custom-plugins/helper-model/recommendation/index.tsx
  11. TEMPAT SAMPAH
      src/page-instrument/evaluat-model/delay-check/image/icon_2_3.png
  12. 33 0
      src/page-instrument/evaluat-model/delay-check/index.module.less
  13. 116 30
      src/page-instrument/evaluat-model/delay-check/index.tsx
  14. 1 1
      src/page-instrument/evaluat-model/earphone/index.tsx
  15. 6 1
      src/page-instrument/evaluat-model/evaluat-result/index.tsx
  16. 32 13
      src/page-instrument/evaluat-model/index.tsx
  17. TEMPAT SAMPAH
      src/page-instrument/header-top/image/add.png
  18. TEMPAT SAMPAH
      src/page-instrument/header-top/image/subtract.png
  19. 24 14
      src/page-instrument/header-top/index.tsx
  20. 2 2
      src/page-instrument/header-top/music-type/index.tsx
  21. 20 1
      src/page-instrument/header-top/settting/index.module.less
  22. 59 6
      src/page-instrument/header-top/settting/index.tsx
  23. 1 1
      src/page-instrument/header-top/speed/index.tsx
  24. 4 1
      src/page-instrument/main.ts
  25. 1 1
      src/page-instrument/router.ts
  26. 12 0
      src/page-instrument/view-detail/index.module.less
  27. 57 113
      src/page-instrument/view-detail/index.tsx
  28. 3 3
      src/page-instrument/view-evaluat-report/component/share-top/index.tsx
  29. 49 0
      src/page-instrument/view-evaluat-report/index.module.less
  30. 353 176
      src/page-instrument/view-evaluat-report/index.tsx
  31. 1 1
      src/page-instrument/view-product-img/index.tsx
  32. 467 8
      src/state.ts
  33. 1 1
      src/store.ts
  34. 21 0
      src/utils/baseApi.ts
  35. 46 0
      src/utils/index.ts
  36. 34 6
      src/utils/request.ts
  37. 467 0
      src/view/abnormal-pop/icon_bg.svg
  38. 51 0
      src/view/abnormal-pop/icon_btn.svg
  39. 16 0
      src/view/abnormal-pop/icon_close.svg
  40. 17 0
      src/view/abnormal-pop/icon_success.svg
  41. 74 0
      src/view/abnormal-pop/index.module.less
  42. 48 0
      src/view/abnormal-pop/index.tsx
  43. 0 0
      src/view/abnormal-pop/loading.json
  44. 29 0
      src/view/evaluating/index.module.less
  45. 200 18
      src/view/evaluating/index.tsx
  46. 130 9
      src/view/fingering/fingering-config.ts
  47. 35 10
      src/view/follow-practice/index.tsx
  48. 9 4
      src/view/music-score/index.tsx
  49. 22 17
      src/view/plugins/move-music-score/index.tsx
  50. 13 4
      src/view/plugins/toggleMusicSheet/choosePartName/index.tsx
  51. 22 4
      src/view/plugins/toggleMusicSheet/index.tsx
  52. 7 5
      src/view/selection/index.module.less
  53. 16 4
      src/view/selection/index.tsx
  54. 40 3
      src/view/tick/index.tsx
  55. 32 0
      src/view/transfer-to-img/index.module.less
  56. 123 0
      src/view/transfer-to-img/index.tsx
  57. 5 1
      vite.config.ts

+ 53 - 0
src/constant/instruments.ts

@@ -216,3 +216,56 @@ export const getInstrumentName = (name = '') => {
   }
   return ''
 };
+
+/**
+ * 乐器排序
+ * 排序顺序:长笛、单簧管、中音单簧管、低音单簧管、高音萨克斯风、中音萨克斯风、次中音萨克斯风、低音萨克斯风、小号、长号、圆号、大号、上低音号
+ * */ 
+export const sortMusical = (name: string, index: number) => {
+	let sortId = 0
+	switch (name) {
+	  case '长笛':
+		sortId = 1
+		break;
+	  case '单簧管':
+		sortId = 2
+		break;
+	  case '中音单簧管':
+		sortId = 3
+		break;
+	  case '低音单簧管':
+		sortId = 4
+		break;
+	  case '高音萨克斯风':
+		sortId = 5
+		break;
+	  case '中音萨克斯风':
+		sortId = 6
+		break;
+	  case '次中音萨克斯风':
+		sortId = 7
+		break;
+	  case '低音萨克斯风':
+		sortId = 8  
+		break;  
+	  case '小号':
+		sortId = 9  
+		break;     
+	  case '长号':
+		sortId = 10 
+		break;       
+	  case '圆号':
+		sortId =11  
+		break;       
+	  case '大号':
+		sortId = 12  
+		break; 
+	  case '上低音号':
+		sortId = 13 
+		break; 
+	  default:
+		sortId = index + 14
+		break;
+	}
+	return sortId
+  }

+ 41 - 0
src/helpers/communication.ts

@@ -321,4 +321,45 @@ export const api_onoffAccompaniment = () => {
 			state: 0,
 		},
 	});
+};
+
+
+/** 延迟检测校音,校音音频播放完成回调 */
+export const addCheckPlayEnd = (callback: CallBack) => {
+	listenerMessage("checkPlayEnd", callback);
+};
+
+/** 取消监听延迟检测校返回 */
+export const removeCheckPlayEnd = (callback: CallBack) => {
+	removeListenerMessage("checkPlayEnd", callback);
+};
+
+/** 检查APP端websocket状态 */
+export const api_checkSocketStatus = () => {
+	return promisefiyPostMessage({ api: "checkSocketStatus" });
+};
+
+/** 监听APP发送的异常信息 */
+export const addAccompanyError = (callback: CallBack) => {
+	listenerMessage("accompanyError", callback);
+};
+
+/** 取消监听APP发送的异常信息 */
+export const removeAccompanyError = (callback: CallBack) => {
+	removeListenerMessage("accompanyError", callback);
+};
+
+/** 检测socket链接状态 */
+export const addSocketStatus = (callback: CallBack) => {
+	listenerMessage("socketConnectSuccessReport", callback);
+};
+
+/** 取消检测socket链接状态 */
+export const removeSocketStatus = (callback: CallBack) => {
+	removeListenerMessage("socketConnectSuccessReport", callback);
+};
+
+/** 检查APP端websocket状态 */
+export const api_disconnectSocket = () => {
+	return promisefiyPostMessage({ api: "disconnectSocket" });
 };

+ 792 - 0
src/helpers/customMusicScore.ts

@@ -0,0 +1,792 @@
+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
+    }
+};

+ 34 - 22
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")];
@@ -372,6 +376,7 @@ export const onlyVisible = (xml: string, partIndex: number): string => {
 	}
 	const visiblePartInfo = partList[partIndex];
 	// console.log(visiblePartInfo, partIndex)
+	// 根据后台已选择的分轨筛选出能切换的声轨
 	state.partListNames = partListNames;
 	if (visiblePartInfo) {
 		const id = visiblePartInfo.getAttribute("id");
@@ -602,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;
 	// 小节中如果没有节点默认为休止符
@@ -613,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];
@@ -633,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);
 };
 
@@ -649,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;
@@ -665,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;
 			}
@@ -794,7 +803,6 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 			if (metronomeNoteIndex !== 0 && metronomeNoteIndex > si) {
 				measureSpeed = allNotes[allNotes.length - 1]?.speed || 100;
 			}
-
 			const activeVerticalMeasureList = [note.sourceMeasure.verticalMeasureList?.[0]] || [];
 			const { realValue } = iterator.currentTimeStamp;
 			const { RealValue: vRealValue, Denominator: vDenominator } = formatDuration(
@@ -823,7 +831,8 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 
 			let relativeTime = usetime;
 			// 速度不能为0 此处的速度应该是按照设置的速度而不是校准后的速度,否则mp3速度不对
-			let beatSpeed = (state.isSpecialBookCategory ? measureSpeed : baseSpeed) || 1;
+			// let beatSpeed = (state.isSpecialBookCategory ? measureSpeed : baseSpeed) || 1;
+			let beatSpeed = measureSpeed || baseSpeed
 			// 如果有节拍器,需要将节拍器的时间算出来
 			if (i === 0) {
 				fixtime += getFixTime(beatSpeed);
@@ -900,7 +909,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 +938,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,7 +964,7 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 			}
 
 			// console.log(note.tie)
-			// console.log(relaEndtime, fixtime, '时间')
+			// console.log(relaEndtime, fixtime, '时间',measureLength)
 			const nodeDetail = {
 				isStaccato: note.voiceEntry.isStaccato(),
 				isRestFlag: note.isRestFlag,
@@ -979,10 +990,10 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 				relaMeasureLength,
 				id: svgElement?.attrs.id,
 				note: note.halfTone + 12, // see issue #224
-				fixtime,
+				fixtime, // 弱起补充的时间
 				relativeTime: retain(relativeTime),
-				time: retain(relativeTime + fixtime),
-				endtime: retain(relaEndtime + fixtime),
+				time: retain(relativeTime + fixtime), // 开始播放的时间
+				endtime: retain(relaEndtime + fixtime), // 播放完成的时间
 				relaEndtime: retain(relaEndtime),
 				realValue,
 				halfTone: note.halfTone,
@@ -997,6 +1008,7 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 				osdmContext: osmd,
 				speedbeatUnit: beatUnit,
 				multipleRestMeasures: multipleRestMeasures,
+				measureSpeed,  // 小节速度
 			};
 			nodeDetail.realKey = formatRealKey(note.halfTone - fixedKey * 12, nodeDetail);
 			nodeDetail.duration = nodeDetail.endtime - nodeDetail.time;
@@ -1044,7 +1056,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;

+ 22 - 1
src/helpers/metronome.ts

@@ -28,6 +28,8 @@ export const metronomeData = reactive({
 	activeMetro: {} as any,
 	cursorMode: 1 as number, // 光标模式:1:音符指针;2:节拍指针;3:关闭指针
 	cursorTips: '' as string, // 光标模式提示文字
+	followAudioIndex: 1, // 当前的拍数
+	totalNumerator: 2, // 总拍数
 });
 
 watch(
@@ -141,22 +143,40 @@ class Metronome {
 	// 播放
 	playAudio = () => {
 		if (!metronomeData.initPlayerState) return;
+		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 : 0.4);
+		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;
+		this.source.volume(metronomeData.disable ? 0 : beatVolume);
+		this.source.play();
+		metronomeData.followAudioIndex += 1;
+		metronomeData.followAudioIndex = metronomeData.followAudioIndex > metronomeData.totalNumerator ? 1 : metronomeData.followAudioIndex;
+	};
+
 	// 切换
 	selectPlay() {}
 
 	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 +300,7 @@ class Metronome {
 		// 5.得到所有的节拍时间
 		metronomeData.metroList = metroList;
 		metronomeData.metroMeasure = metroMeasure;
+		// console.log(9999,metroList,7777,metroMeasure)
 		metronomeData.activeMetro = metroMeasure[0]?.[0] || {};
 	}
 }

+ 1 - 1
src/page-instrument/App.tsx

@@ -18,7 +18,7 @@ export default defineComponent({
     };
     const setUser = async () => {
       try {
-        const res = await getUserInfo();
+        const res = query.isCbs ? {code:200,data:{}} : await getUserInfo();
         if (res?.code === 5000) {
           const browserInfo = browser();
           showToast(res.message);

File diff ditekan karena terlalu besar
+ 0 - 0
src/page-instrument/component/mode-type-mode/icon/index.json


+ 6 - 3
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,
@@ -85,8 +87,9 @@ export default defineComponent({
 						/>
 						<img
 							id="modeType-1"
+							style={{ cursor: state.isPercussion ? "not-allowed" : "pointer" }}
 							onClick={() => headTopData.handleChangeModeType("follow")}
-							src={icons.icon_2}
+							src={state.isPercussion ? icons.icon_5 : icons.icon_2}
 						/>
 						<img
 							id="modeType-2"
@@ -95,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 &&

+ 6 - 3
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",
     });
@@ -12,10 +16,9 @@ export const setGuidance = (params: any) => {
    * 获取引导页
    */
   export const getGuidance = (params: any) => {
-    return request.get('/functionGuidance/queryTagDetail', {
+    return storeData.platformApi == "/cbs-app" ? { data: null } : request.get('/functionGuidance/queryTagDetail', {
       data: params,
       params,
-  
     });
   };
   

+ 3 - 3
src/page-instrument/custom-plugins/helper-model/recommendation/index.tsx

@@ -21,10 +21,10 @@ export default defineComponent({
 		// 获取建议类别
 		const getTypeList = async () => {
 			try {
-			  const res = await getSuggestionList({ rows: 9999, page: 1 });
-			  suggestionTypeList.value = res.data.rows || [];
+				const res = await getSuggestionList({ rows: 9999, page: 1 });
+				suggestionTypeList.value = res.data.rows || [];
 			} catch (e) {
-			  //
+				//
 			}
 		};
 

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


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

@@ -236,4 +236,37 @@
     100% {
         opacity: 1;
     }
+}
+
+.tipBox {
+    position: fixed;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%,-50%);
+    z-index: 999;
+    background: #fff;
+    border-radius: 20px;
+    font-size: 12px;
+    .tipContent {
+        padding: 20px 20px 0;
+    }
+    .tipBtn {
+        border-radius: 20px;
+        background-color: var(--van-primary-color);
+        color: #fff;
+        width: 100px;
+        margin: 20px auto;
+        text-align: center;
+        line-height: 30px;
+        cursor: pointer;
+    }
+}
+
+.delayTest {
+    background: burlywood;
+    width: 200px;
+    height: 200px;
+    color: #000;
+    z-index: 999;
+    position: relative;
 }

+ 116 - 30
src/page-instrument/evaluat-model/delay-check/index.tsx

@@ -1,7 +1,7 @@
 import { Button, Popup } from "vant";
 import styles from "./index.module.less";
-import { defineComponent, onMounted, onUnmounted, reactive, ref } from "vue";
-import { api_toggleTune, getEarphone, removeResult, sendResult } from "/src/helpers/communication";
+import { defineComponent, onMounted, onUnmounted, reactive, ref, watch } from "vue";
+import { api_toggleTune, getEarphone, removeResult, sendResult, addCheckPlayEnd, removeCheckPlayEnd } from "/src/helpers/communication";
 import { Vue3Lottie } from "vue3-lottie";
 import bg from "./json/bg.json";
 import bg1 from "./json/bg1.json";
@@ -27,7 +27,7 @@ import icon_3_0 from "./image/icon_3_0.png";
 import icon_3_1 from "./image/icon_3_1.png";
 import icon_3_2 from "./image/icon_3_2.png";
 import iconBack from "./image/icon-back.png";
-import { evaluatingData } from "/src/view/evaluating";
+import { evaluatingData, startCheckDelay } from "/src/view/evaluating";
 import { IPostMessage } from "/src/utils/native-message";
 import state from "/src/state";
 
@@ -37,6 +37,7 @@ export default defineComponent({
 	setup(props, { emit }) {
 		// startTune定时器
 		let startTuneTimer: any = null
+		let checkErjiTimer: any = null
 		const anim = ref();
 		const data = reactive({
 			show: false,
@@ -48,7 +49,12 @@ export default defineComponent({
 			stopTimer: null as any,
 			volumeTimer: null as any,
 			earPhoneType: "" as "" | "有线耳机" | "蓝牙耳机",
+			startAbnormalTimer: null as any, // 发送startTune后,超过10秒没有收到sendResult回调,处理这类异常的定时器
+			startTuneResult: false, // 发送startTune后,能否收到正常的result回调
+			abnormalPopShow: false, // 异常弹窗
 		});
+		// 调用'isWiredHeadsetOn'最小时间间隔,1秒
+		let minInterval = 0;
 		/** 获取耳机状态 */
 		const getEarphoneState = async () => {
 			const res = await getEarphone();
@@ -58,21 +64,29 @@ export default defineComponent({
 		/** 持续检测耳机状态 */
 		const keepCheckEarphone = async () => {
 			if (data.step >= 7 || !data.show) return;
+			let momentTime = +new Date()
+			// console.log('间隔123',momentTime - minInterval)
+			if (momentTime - minInterval < 1000) {
+			  return
+			}
+			minInterval = momentTime
 			evaluatingData.earphone = await getEarphoneState();
 			// console.log('erji',evaluatingData.earphone,data.step)
 			if (evaluatingData.earphone) {
 				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
 					data.checkStatus = "init"
 				}
 			}
-			setTimeout(() => {
+			checkErjiTimer = setTimeout(() => {
 				keepCheckEarphone();
 			}, 1000);
 		};
@@ -93,39 +107,60 @@ export default defineComponent({
 				}
 			}
 		};
+
+		// 监听到校音音频播放完成
+		const checkAudioPlayEnd = (res?: IPostMessage) => {
+			console.log("🚀 ~ res:校音音频", res);
+			// startTune效音通过返回后,调用endTune
+			if (res) {
+				clearTimeout(data.startAbnormalTimer);
+				data.startTuneResult = true
+				toggleTune("stop");
+			}
+		}
+
 		onMounted(() => {
+			clearTimeout(checkErjiTimer)
 			data.show = true;
 			sendResult(listenerResult);
+			addCheckPlayEnd(checkAudioPlayEnd);
 			keepCheckEarphone();
 		});
 		onUnmounted(() => {
+			clearTimeout(checkErjiTimer)
 			data.show = false;
 			removeResult(listenerResult);
+			removeCheckPlayEnd(checkAudioPlayEnd);
 		});
 
 		const handleStartTune = async () => {
-			// data.step = evaluatingData.earphone ? 4 : 3;
-			if (evaluatingData.earphone) {
-				if (data.step <= 5) {
-					if (data.checkStatus === "ing") {
-						data.count = 0;
-						clearTimeout(data.startTimer);
-						clearTimeout(data.stopTimer);
-						toggleTune("stop");
+			// 检测APP端socket状态
+			const res: any = await startCheckDelay();
+			if (res?.checked) {
+				// data.step = evaluatingData.earphone ? 4 : 3;
+				if (evaluatingData.earphone) {
+					if (data.step <= 5) {
+						if (data.checkStatus === "ing") {
+							data.count = 0;
+							clearTimeout(data.startTimer);
+							clearTimeout(data.stopTimer);
+							// toggleTune("stop");
+						}
+						data.step = 3;
+					}
+				} else {
+					if (data.step === 2 || data.step === 3) {
+						data.step = 4;
+						startTuneTimer = setTimeout(() => {
+							data.step = 5;
+							data.checkStatus = "ing";
+							data.count = 0;
+							toggleTune("start");
+						}, 2000);
 					}
-					data.step = 3;
-				}
-			} else {
-				if (data.step === 2 || data.step === 3) {
-					data.step = 4;
-					startTuneTimer = setTimeout(() => {
-						data.step = 5;
-						data.checkStatus = "ing";
-						data.count = 0;
-						toggleTune("start");
-					}, 2000);
 				}
-			}			
+			}
+			
 		};
 		const hanldeEndTune = () => {
 			console.log('设置soundEffect')
@@ -136,16 +171,57 @@ export default defineComponent({
 
 		const toggleTune = async (state: "start" | "stop" | "finishTune") => {
 			if (state === "start") {
-				api_toggleTune("start", data.count);
-				data.stopTimer = setTimeout(() => {
-					api_toggleTune("stop");
-				}, 2000);
+				const res = await api_toggleTune("start", data.count);
+				// 用户没有授权,需要重置状态
+				if (res?.content?.reson) {
+					data.step = 2
+					data.checkStatus = 'init'
+					data.count = 0
+				} else {
+					// data.stopTimer = setTimeout(() => {
+					// 	api_toggleTune("stop");
+					// }, 2000);
+					data.startTuneResult = false
+					data.startAbnormalTimer = setTimeout(() => {
+						/** startTune发出后,超过10秒没有收到回调,异常处理 */
+						if (!data.startTuneResult) {
+							api_toggleTune("stop");
+							// 给出异常提示,再来一次
+							data.abnormalPopShow = true
+						}
+					}, 10000);
+				}
 			} else if (state === "finishTune") {
 				data.checkStatus = "finish";
 				data.step = 6;
 				api_toggleTune("finishTune");
+			} else if (state === "stop") {
+				api_toggleTune("stop");
 			}
 		};
+
+		const resetCheck = () => {
+			if (data.step > 5 && evaluatingData.accompanyErrorType === 'playError') {
+				return
+			}
+			api_toggleTune("stop");
+			clearTimeout(startTuneTimer)
+			clearTimeout(data.startAbnormalTimer);
+			data.abnormalPopShow = false
+			data.step = 2
+			data.checkStatus = 'init'
+			data.count = 0
+			data.startTuneResult = false
+		}
+		watch(
+			() => evaluatingData.delayCheckSocketError,
+			() => {
+				// 监听到网络异常,重置延迟检测状态
+				if (evaluatingData.delayCheckSocketError) {
+					resetCheck()
+				}
+			}
+		);
 		return () => (
 			<Popup
 				teleport="body"
@@ -156,10 +232,20 @@ 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");
 						emit("back")
 					}} />
+					{/* 异常提示弹窗 */}
+					{
+						data.abnormalPopShow && 
+						<div class={styles.tipBox}>
+							<div class={styles.tipContent}>检测失败,请确保设备麦克风、扬声器正常,重新开始检测</div>
+							<div class={styles.tipBtn} onClick={resetCheck}>再来一次</div>
+						</div>
+					}
 					<div class={[styles.item, data.step !== 7 && styles.show]}>
 						<Vue3Lottie animationData={bg}></Vue3Lottie>
 					</div>

+ 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>

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

@@ -61,8 +61,13 @@ export default defineComponent({
 		});
 
 		watch(() => evaluatingData.resulstMode, (val) => {
+			// # 9402,评测异常操作:都改为不生成评测记录
 			if (val) {
-				handleAddRecord();
+				setTimeout(() => {
+					if (!evaluatingData.isErrorState) {
+						handleAddRecord();
+					}
+				}, 0);
 			}
 		})
 		return () => (

+ 32 - 13
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 } 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();
@@ -221,7 +226,7 @@ export default defineComponent({
       calculateInfo = formatTimes()
       const content = {
         musicXmlInfos: calculateInfo.datas,
-        subjectId: state.subjectId,
+        subjectId: state.musicalCode,
         detailId: state.detailId,
         examSongId: state.examSongId,
         xmlUrl: state.xmlUrl,
@@ -235,7 +240,7 @@ export default defineComponent({
         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;
     };
 
@@ -304,9 +310,18 @@ export default defineComponent({
       showToast("上传成功");
     };
 
-    const startBtnHandle = () => {
-      handleConnect();
-      handleStartBegin(calculateInfo.firstNoteTime);
+    const startBtnHandle = async () => {
+      // 检测APP端socket状态
+      const res: any = await startCheckDelay();
+      if (res?.checked) {
+        handleConnect();
+        handleStartBegin(calculateInfo.firstNoteTime);
+        if (evaluatingData.isErrorState = true) {
+          evaluatingData.isErrorState = false;
+          evaluatingData.resulstMode = false;
+        }
+        
+      }
     }
     onMounted(() => {
       evaluatingData.isDisabledPlayMusic = true;
@@ -331,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>
@@ -380,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>
+        {
+          !evaluatingData.isErrorState && 
+          <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>

TEMPAT SAMPAH
src/page-instrument/header-top/image/add.png


TEMPAT SAMPAH
src/page-instrument/header-top/image/subtract.png


+ 24 - 14
src/page-instrument/header-top/index.tsx

@@ -35,11 +35,17 @@ export const headTopData = reactive({
   settingMode: false,
   /** 切换模式 */
   handleChangeModeType(value: "practise" | "follow" | "evaluating") {
+    // 后台设置为不能评测
     if (value === 'evaluating' && !state.enableEvaluation) return
+    // 打击乐&节奏练习不支持跟练模式
+    if (value === 'follow' && state.isPercussion) return
     // 跟练模式,光标只有音符模式,无节拍模式
     if (value === 'follow' && metronomeData.cursorMode === 2) {
       metronomeData.cursorMode = 1
     }
+    if (value === 'practise') {
+      // state.playIngSpeed = state.speed
+    }
     if (value === "evaluating") {
       // 如果是pc端, 评测模式暂不可用
       if (state.platform === IPlatform.PC) {
@@ -51,6 +57,7 @@ export const headTopData = reactive({
         });
         return;
       }
+      state.playIngSpeed = state.originSpeed
       handleStartEvaluat();
     } else if (value === "follow") {
       toggleFollow();
@@ -373,17 +380,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`)} />
@@ -415,7 +425,7 @@ export default defineComponent({
                       headData.speedShow = !headData.speedShow;
                     }}
                   >
-                    <Badge class={styles.badge} content={state.speed}>
+                    <Badge class={styles.badge} content={state.playState === "play" ? state.playIngSpeed : state.speed}>
                       <img class={styles.iconBtn} src={headImg("icon_speed.svg")} />
                     </Badge>
                     <span>速度</span>
@@ -484,8 +494,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 = ''

+ 20 - 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;
@@ -195,4 +197,21 @@
 .disabled{
     opacity: .3;
     pointer-events: none;
+}
+
+.operateHz {
+    display: flex;
+    align-items: center;
+    font-size: 14px;
+    color: var(--van-primary-color);
+    font-weight: 500;
+    img {
+        width: 28px;
+        height: 28px;
+    }
+    span {
+        margin: 0 6px;
+        width: 50px;
+        text-align: center;
+    }
 }

+ 59 - 6
src/page-instrument/header-top/settting/index.tsx

@@ -23,6 +23,8 @@ import iconInfo from "../image/info.svg";
 import iconDown from "../image/down.svg";
 import iconTv from "../image/tv.svg";
 import iconYijian from "../image/yijian.svg";
+import iconSubtract from "../image/subtract.png";
+import iconAdd from "../image/add.png";
 import ScreenModel from "../../custom-plugins/helper-model/screen-model";
 import Recommendation from "../../custom-plugins/helper-model/recommendation";
 import { svg2canvas } from "/src/utils/svg2canvas";
@@ -81,6 +83,21 @@ 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
+			let currentFrequency = state.setting.frequency
+			if (type === 1) {
+				if (currentFrequency - 1 < minFrequency) return showToast({ message: `最低标准音高${minFrequency}HZ` })
+				currentFrequency = currentFrequency - 1
+			} else {
+				if (currentFrequency + 1 > maxFrequency) return showToast({ message: `最高标准音高${maxFrequency}HZ` })
+				currentFrequency = currentFrequency + 1
+			}
+			state.setting.frequency = currentFrequency >= 0 ? currentFrequency : 0
+		}
 		
 		return () => (
 			<div class={styles["header-settting"]}>
@@ -97,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} />
@@ -120,11 +157,11 @@ export default defineComponent({
 							</Cell>
 							<Cell class={[state.modeType == "evaluating" && styles.disabled]} title="显示指法" center>
 								{{
-									extra: () => <Switch v-model={state.setting.displayFingering}></Switch>,
+									extra: () => <Switch v-model={state.setting.displayFingering} disabled={!state.fingeringInfo.name}></Switch>,
 								}}
 							</Cell>
 						</Tab>
-						<Tab title="评测">
+						<Tab title="评测设置">
 							<Cell class={[query.workRecord && styles.disabled]} title="评测难度" center>
 								{{
 									extra: () => (
@@ -151,9 +188,13 @@ export default defineComponent({
 									extra: () => (
 										<Switch
 											v-model={state.setting.camera}
-											onChange={(value) => {
+											onChange={ async (value) => {
 												if (value) {
-													api_openCamera();
+													const res = await api_openCamera();
+													// 没有授权
+													if (res?.content?.reson) {
+														state.setting.camera = false
+													}
 												} else {
 													api_closeCamera();
 												}
@@ -194,7 +235,7 @@ export default defineComponent({
 								}}
 							</Cell>
 							<Cell title="标准音高" center>
-								{{
+								{/* {{
 									extra: () => (
 										<RadioGroup
 											iconSize={20}
@@ -205,10 +246,22 @@ export default defineComponent({
 											<Radio name={442}>442Hz</Radio>
 										</RadioGroup>
 									),
+								}} */}
+								{{
+									extra: () => (
+										<div class={styles.operateHz}>
+											<img src={iconSubtract} onClick={() => operateHz(1)} />
+											<span>{state.setting.frequency}HZ</span>
+											<img src={iconAdd} onClick={() => operateHz(2)} />
+										</div>
+									)
 								}}
 							</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>
 		);
 	},

+ 4 - 1
src/page-instrument/main.ts

@@ -10,8 +10,10 @@ import "../style.css";
 import App from "./App";
 import router from "./router";
 import "./theme.css";
+import { getQuery } from "/src/utils/queryString";
 
 (function () {
+	const query = getQuery();
 	const u = navigator.userAgent;
 	const instance: any =
 	(window as any).DAYA ||
@@ -20,9 +22,10 @@ import "./theme.css";
 	(window as any).webkit?.messageHandlers?.COLEXIU ||
 	(window as any).ORCHESTRA ||
 	(window as any).webkit?.messageHandlers?.ORCHESTRA;
+	const apiPrefix = query.isCbs ? "/cbs-app" : u.includes("COLEXIUSTUDENT") ? "/edu-app" : "/edu-app"
 	setStoreData({
 		isApp: instance ? true : false,
-		platformApi: u.includes("COLEXIUSTUDENT") ? "/edu-app" : "/edu-app",
+		platformApi: apiPrefix,
 		platformType: u.includes("COLEXIUSTUDENT") ? "" : "",
 		proxy: import.meta.env.DEV ? "/instrument" : ""
 	});

+ 1 - 1
src/page-instrument/router.ts

@@ -9,7 +9,7 @@ const routes: RouteRecordRaw[] = [
   },
   {
     path: "/product-img",
-    component: () => import("./view-product-img/index"),
+    component: () => import("/src/view/transfer-to-img/index"),
   },
   {
     path: "/evaluat-report",

+ 12 - 0
src/page-instrument/view-detail/index.module.less

@@ -130,4 +130,16 @@
     .headHeight.headHide {
         // margin-top: 0 !important;
     }
+}
+
+
+.preViewDetail {
+    .container {
+        height: 100%;
+    }
+    :global {
+        #osmdCanvasPage1 {
+            padding-bottom: 0 !important;
+        }
+    }
 }

+ 57 - 113
src/page-instrument/view-detail/index.tsx

@@ -2,7 +2,7 @@ import { Popup, Skeleton } from "vant";
 import { computed, defineComponent, nextTick, onBeforeMount, onBeforeUnmount, onMounted, reactive, Transition, watch, watchEffect } from "vue";
 import { formateTimes } from "../../helpers/formateMusic";
 import Metronome, { metronomeData } from "../../helpers/metronome";
-import state, { EnumMusicRenderType, evaluatCreateMusicPlayer, handleSetSpeed, IAudioState, IPlatform, isRhythmicExercises, resetPlaybackToStart, togglePlay } from "/src/state";
+import state, { EnumMusicRenderType, evaluatCreateMusicPlayer, handleSetSpeed, IAudioState, IPlatform, isRhythmicExercises, resetPlaybackToStart, togglePlay, getMusicDetail } from "/src/state";
 import { browser, setGlobalData } from "../../utils";
 import AudioList from "../../view/audio-list";
 import MusicScore, { resetMusicScore } from "../../view/music-score";
@@ -27,6 +27,8 @@ 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";
 
 /**
  * 特殊教材分类id
@@ -90,7 +92,7 @@ export default defineComponent({
         }
       }
     };
-    onBeforeMount(() => {
+    onBeforeMount(async () => {
       console.time("渲染加载耗时");
       api_keepScreenLongLight();
       getAPPData();
@@ -98,8 +100,14 @@ export default defineComponent({
       const settting = store.get("musicscoresetting");
       if (settting) {
         state.setting = settting;
+        state.setting.beatVolume = state.setting.beatVolume || 50
         if (state.setting.camera) {
-          api_openCamera();
+          const res = await api_openCamera();
+          // 没有授权
+          if (res?.content?.reson) {
+            state.setting.camera = false
+            store.set("musicscoresetting", state.setting);
+          }
         }
       }
     });
@@ -109,107 +117,21 @@ export default defineComponent({
       api_cloudAccompanyMessage(state.accompany);
     };
     // console.log(route.params, query)
-    /** 获取曲谱数据 */
-    const getMusicInfo = (res: any) => {
-      const index = query["part-index"] ? parseInt(query["part-index"] as string) : 0;
-      const musicData = res.data.background?.[index] || {};
-      const musicInfo = {
-        ...res.data,
-        music: musicData.audioFileUrl || res.data.audioFileUrl,
-        accompany: musicData.metronomeUrl || res.data.metronomeUrl,
-        musicSheetId: musicData.musicSheetId || res.data.id,
-        track: musicData.track || res.data.track,
-      };
-      console.log("🚀 ~ musicInfo:", musicInfo);
-      setState(musicInfo, index);
-      setCustom();
-      detailData.isLoading = false;
-    };
-
-    const setState = (data: any, index: number) => {
-      state.appName = "COLEXIU";
-      state.detailId = data.id;
-      state.xmlUrl = data.xmlFileUrl;
-      state.partIndex = index;
-      state.trackId = data.track;
-      state.subjectId = data.musicSubject;
-      state.categoriesId = data.categoriesId;
-      state.categoriesName = data.musicTagNames;
-      state.enableEvaluation = data.canEvaluate ? true : false;
-      state.examSongId = data.id + "";
-      state.examSongName = data.musicSheetName;
-      state.coverImg = data.titleImg ?? "";
-      // 解析扩展字段
-      if (data.extConfigJson) {
-        try {
-          state.extConfigJson = JSON.parse(data.extConfigJson as string);
-        } catch (error) {
-          console.error("解析扩展字段错误:", error);
-        }
-      }
-      state.isOpenMetronome = data.mp3Type === "MP3_METRONOME" ? true : false;
-      // 曲子包含节拍器,就不开启节拍器
-      state.needTick = data.mp3Type === "MP3_METRONOME" ? false : true;
-      state.isShowFingering = data.showFingering ? true : false;
-      state.music = data.music;
-      state.accompany = data.accompany;
-      state.midiUrl = data.midiUrl;
-      state.parentCategoriesId = data.musicTag;
-      state.musicSheetCategoriesId = data.musicSheetCategoriesId;
-      state.playMode = data.audioType === "MP3" ? "MP3" : "MIDI";
-      state.originSpeed = state.speed = data.playSpeed;
-      const track = data.code || data.track;
-      state.track = track ? track.replace(/ /g, "").toLocaleLowerCase() : "";
-      state.enableNotation = data.notation ? true : false;
-      // 是否是合奏,先根据background判断
-      state.isConcert = data.background?.length > 1;
-      // 开启预备小节
-      state.isOpenPrepare = true;
-      // console.log("🚀 ~ state.subjectId:", state.subjectId, state.track as any , state.subjectId)
-      // 是否打击乐
-      // state.isPercussion =
-      // 	state.subjectId == 23 ||
-      // 	state.subjectId == 113 ||
-      // 	state.subjectId == 121 ||
-      // 	isRhythmicExercises();
-      state.isPercussion = isRhythmicExercises();
-      // 设置是否特殊曲谱, 是特殊曲谱取反(不理解之前的思考逻辑), 使用后台设置的速度
-      state.isSpecialBookCategory = !classids.includes(data.musicSheetCategoriesId);
-
-      // 设置指法
-      const code = state.isConcert ? mappingVoicePart(state.trackId, "ENSEMBLE") : mappingVoicePart(state.subjectId, "INSTRUMENT");
-      state.fingeringInfo = subjectFingering(code);
-      console.log("🚀 ~ state.fingeringInfo:", code, state.fingeringInfo, state.trackId, state.track);
-
-      // 检测是否原音和伴奏都有
-      if (!state.music || !state.accompany) {
-        state.playSource = state.music ? "music" : "background";
-      }
-
-      // 如果是PC端,放大曲谱
-      state.platform = query.platform?.toLocaleUpperCase() || "";
-      if (state.platform === IPlatform.PC) {
-        state.zoom = query.zoom || 1.5;
-      }
-
-      //课堂乐器, 渲染类型: 五线谱, 简谱
-      state.musicRenderType = query.musicRenderType || EnumMusicRenderType.firstTone;
-      console.log("state对象", state);
-    };
 
-    const setCustom = () => {
-      if (state.extConfigJson.multitrack) {
-        setGlobalData("multitrack", state.extConfigJson.multitrack);
-      }
-    };
-
-    onMounted(() => {
+    onMounted(async () => {
       (window as any).appName = "colexiu";
       const id = query.id || "43554";
-      Promise.all([sysMusicScoreAccompanimentQueryPage(id)]).then((values) => {
-        getMusicInfo(values[0]);
-      });
-      api_setEventTracking();
+      // 如果是纯预览模式,0.65倍缩放谱面
+      state.isPreView = query.isPreView
+      if (state.isPreView) {
+        state.zoom = 0.65
+      }
+      // Promise.all([sysMusicScoreAccompanimentQueryPage(id)]).then((values) => {
+      //   getMusicInfo(values[0]);
+      // });
+      await getMusicDetail(id);
+      detailData.isLoading = false;
+      // api_setEventTracking();
     });
 
     /** 渲染完成 */
@@ -225,20 +147,25 @@ export default defineComponent({
       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);
+      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,7 +173,7 @@ export default defineComponent({
       evaluatCreateMusicPlayer();
       resetPlaybackToStart();
 
-      pushAppMusic();
+      // pushAppMusic();
       console.timeEnd("渲染加载耗时");
     };
     /** 指法配置 */
@@ -333,6 +260,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);
     });
@@ -374,7 +314,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` : "",
@@ -387,7 +327,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" : "" }}
@@ -403,7 +346,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={{
@@ -415,8 +358,9 @@ export default defineComponent({
           )}
         </div>
 
-        {/* 节拍器 */}
-        {state.needTick && <Tick />}
+        {/* 节拍器,跟练需要播放系统节拍器,所以不需要判断needTick状态 */}
+        {/* {state.needTick && <Tick />} */}
+        <Tick />
 
         {/* 播放 */}
         {!detailData.isLoading && <AudioList />}
@@ -440,14 +384,14 @@ export default defineComponent({
         {/* 切换曲谱 */}
         {!query.lessonTrainingId && !query.questionId && state.isConcert && <ToggleMusicSheet />}
 
-        {state.musicRendered && (
+        {state.musicRendered && !state.isPreView && (
           <>
             {/* 统计训练时长 */}
             {storeData.isApp && <RecordingTime />}
             {/* 作业 */}
             {query.workRecord && <WorkIndex />}
             {/* 曲谱列表 */}
-            {state.playState == "play" || followData.start || evaluatingData.startBegin || query.workRecord || query.modelType || state.platform === IPlatform.PC ? null : <TheMusicList />}
+            {state.playState == "play" || followData.start || evaluatingData.startBegin || query.workRecord || query.modelType || state.platform === IPlatform.PC || query.isCbs ? null : <TheMusicList />}
           </>
         )}
 

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

@@ -24,7 +24,7 @@ export default defineComponent({
 			default: () => ({}),
 		},
 	},
-	setup(props) {
+	setup(props, {expose}) {
 		const browserInfo = browser();
 		const { scoreData } = toRefs(props);
 		const shareData = reactive({
@@ -47,6 +47,7 @@ export default defineComponent({
 
 		const handleChange = (type: IItemType) => {
 			itemType.value = type;
+			scoreData.value.itemType = type
 		};
 
 		// 资源类型
@@ -70,7 +71,6 @@ 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}>
@@ -167,7 +167,7 @@ export default defineComponent({
 							</div>
 						)}
 						<div>
-							<Note fill="#000" />
+							<Note fill="#AEAEAE" />
 							<span>未演奏</span>
 						</div>
 					</div>

+ 49 - 0
src/page-instrument/view-evaluat-report/index.module.less

@@ -126,4 +126,53 @@
     fill: #CC75FF;
     stroke: #CC75FF;
   }
+}
+
+
+
+
+.right{
+  path{
+    fill: #01C1B5;
+    stroke: #01C1B5;
+  }
+}
+
+.wrong{
+  path{
+    fill: #FF4444;
+    stroke: #FF4444;
+  }
+}
+
+.notPlay{
+  path{
+    fill: #AEAEAE;
+    stroke: #AEAEAE;
+  }
+}
+
+.cadence_wrong,
+.cadence_fast,
+.cadence_slow {
+  path{
+    fill: #FF4444;
+    stroke: #FF4444;
+  }
+}
+
+.intonation_wrong,
+.intonation_high,
+.intonation_low{
+  path{
+    fill: #FFAB25;
+    stroke: #FFAB25;
+  }
+}
+
+.integrity_wrong{
+  path{
+    fill: #CC75FF;
+    stroke: #CC75FF;
+  }
 }

+ 353 - 176
src/page-instrument/view-evaluat-report/index.tsx

@@ -1,200 +1,377 @@
 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";
-import { api_cloudLoading, api_setStatusBarVisibility, isSpecialShapedScreen } from "/src/helpers/communication";
+import {
+	api_cloudLoading,
+	api_setStatusBarVisibility,
+	isSpecialShapedScreen,
+} from "/src/helpers/communication";
 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";
 
 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 scoreData = reactive({
-      videoFilePath: "", // 回放视频路径
-      cadence: 0,
-      integrity: 0,
-      intonation: 0,
-      score: 0,
-      heardLevel: "",
-    });
+	name: "music-list",
+	setup() {
+		const query: any = getQuery();
+		const useedid = ref<string[]>([])
+		const scoreData = reactive({
+			videoFilePath: "", // 回放视频路径
+			cadence: 0,
+			integrity: 0,
+			intonation: 0,
+			score: 0,
+			heardLevel: "",
+			itemType: "intonation",
+		});
 
-    const detailData = reactive({
-      isLoading: true,
-      paddingLeft: "",
-      headerHide: false,
-      musicalNotesPlayStats: [] as any[],
-      userMeasureScore: {} as any,
-    });
-    const getAPPData = async () => {
-      const screenData = await isSpecialShapedScreen();
-      if (screenData?.content) {
-        const { isSpecialShapedScreen, notchHeight } = screenData.content;
-        if (isSpecialShapedScreen) {
-          detailData.paddingLeft = 25 + "px";
-        }
-      }
-      // 普通webview 没有获取异性屏的方法
-      detailData.paddingLeft = 20 + "px";
-    };
-    onBeforeMount(() => {
-      const query: any = getQuery();
-      if (query.platform !== "webTeacher") {
-        getAPPData();
-      }
-      api_setStatusBarVisibility();
-    });
-    // console.log(route.params, query)
-    /** 获取曲谱数据 */
-    const getMusicInfo = (res: any) => {
-      const index = state.partIndex;
-      const musicInfo = {
-        ...res.data,
-        ...res.data.background[index],
-      };
-      // console.log("🚀 ~ musicInfo:", musicInfo);
-      setState(musicInfo, index);
-      setCustom();
-      detailData.isLoading = false;
-    };
+		const detailData = reactive({
+			isLoading: true,
+			paddingLeft: "",
+			headerHide: false,
+			musicalNotesPlayStats: [] as any[],
+			userMeasureScore: {} as any,
+		});
+		const getAPPData = async () => {
+			const screenData = await isSpecialShapedScreen();
+			if (screenData?.content) {
+				const { isSpecialShapedScreen, notchHeight } = screenData.content;
+				if (isSpecialShapedScreen) {
+					detailData.paddingLeft = 25 + "px";
+				}
+			}
+			// 普通webview 没有获取异性屏的方法
+			detailData.paddingLeft = 20 + "px";
+		};
+		onBeforeMount(() => {
+			getAPPData();
+			api_setStatusBarVisibility();
+		});
+		// console.log(route.params, query)
+		/** 获取曲谱数据 */
+		const getMusicInfo = (res: any) => {
+			const index = state.partIndex;
+			const musicInfo = {
+				...res.data,
+				...res.data.background[index],
+			};
+			// console.log("🚀 ~ musicInfo:", musicInfo);
+			setState(musicInfo, index);
+			setCustom();
+			detailData.isLoading = false;
+		};
 
-    const setState = (data: any, index: number) => {
-      // console.log("🚀 ~ data:", data)
-      state.scrollContainer = "scrollContainer";
-      state.detailId = data.id;
-      state.xmlUrl = data.xmlFileUrl;
-      state.partIndex = index;
-      state.subjectId = data.musicSubject;
-      state.categoriesId = data.categoriesId;
-      state.categoriesName = data.musicTagNames;
-      state.enableEvaluation = data.canEvaluate ? true : false;
-      state.examSongId = data.id + "";
-      state.examSongName = data.musicSheetName;
-      // 解析扩展字段
-      if (data.extConfigJson) {
-        try {
-          state.extConfigJson = JSON.parse(data.extConfigJson as string);
-        } catch (error) {
-          console.error("解析扩展字段错误:", error);
-        }
-      }
-      state.isOpenMetronome = data.mp3Type === "MP3_METRONOME" ? true : false;
-      state.needTick = data.isOpenMetronome;
-      state.isShowFingering = data.showFingering ? true : false;
-      state.music = data.audioFileUrl;
-      state.accompany = data.metronomeUrl || data.metronomeUrl;
-      state.midiUrl = data.midiUrl;
-      state.parentCategoriesId = data.musicTag;
-      state.playMode = data.audioType === "MP3" ? "MP3" : "MIDI";
-      state.originSpeed = state.speed = data.speed;
-      state.track = data.track;
-      state.enableNotation = data.notation ? true : false;
+		const setState = (data: any, index: number) => {
+			// console.log("🚀 ~ data:", data)
+			state.scrollContainer = "scrollContainer";
+			state.detailId = data.id;
+			state.xmlUrl = data.xmlFileUrl;
+			state.partIndex = index;
+			state.subjectId = data.musicSubject;
+			state.categoriesId = data.categoriesId;
+			state.categoriesName = data.musicTagNames;
+			state.enableEvaluation = data.canEvaluate ? true : false;
+			state.examSongId = data.id + "";
+			state.examSongName = data.musicSheetName;
+			// 解析扩展字段
+			if (data.extConfigJson) {
+				try {
+					state.extConfigJson = JSON.parse(data.extConfigJson as string);
+				} catch (error) {
+					console.error("解析扩展字段错误:", error);
+				}
+			}
+			state.isOpenMetronome = data.mp3Type === "MP3_METRONOME" ? true : false;
+			state.needTick = data.isOpenMetronome;
+			state.isShowFingering = data.showFingering ? true : false;
+			state.music = data.audioFileUrl;
+			state.accompany = data.metronomeUrl || data.metronomeUrl;
+			state.midiUrl = data.midiUrl;
+			state.parentCategoriesId = data.musicTag;
+			state.playMode = data.audioType === "MP3" ? "MP3" : "MIDI";
+			state.originSpeed = state.speed = data.speed;
+			state.track = data.track;
+			state.enableNotation = data.notation ? true : false;
 
-      // 映射声部ID
-      state.subjectId = mappingVoicePart(state.subjectId as any, "ORCHESTRA");
-      // console.log("🚀 ~ state.subjectId:", state.subjectId);
-      // 是否打击乐
-      state.isPercussion = state.subjectId == 23 || state.subjectId == 113 || state.subjectId == 121 || isRhythmicExercises();
+			// 映射声部ID
+			state.subjectId = mappingVoicePart(state.subjectId as any, "ORCHESTRA");
+			// console.log("🚀 ~ state.subjectId:", state.subjectId);
+			// 是否打击乐
+			state.isPercussion =
+				state.subjectId == 23 ||
+				state.subjectId == 113 ||
+				state.subjectId == 121 ||
+				isRhythmicExercises();
 
-      // 设置指法
-      state.fingeringInfo = subjectFingering(state.subjectId);
-      // console.log("🚀 ~ state.fingeringInfo:", state.fingeringInfo, state.subjectId, state.track)
-      // state.isOpenPrepare = true
-    };
+			// 设置指法
+			state.fingeringInfo = subjectFingering(state.subjectId);
+			// console.log("🚀 ~ state.fingeringInfo:", state.fingeringInfo, state.subjectId, state.track)
+			// state.isOpenPrepare = true
+		};
 
-    const setCustom = () => {
-      if (state.extConfigJson.multitrack) {
-        setGlobalData("multitrack", state.extConfigJson.multitrack);
-      }
-    };
+		const setCustom = () => {
+			if (state.extConfigJson.multitrack) {
+				setGlobalData("multitrack", state.extConfigJson.multitrack);
+			}
+		};
 
-    onMounted(async () => {
-      const res = await api_musicPracticeRecordDetail(query.id);
-      state.partIndex = Number(res?.data?.partIndex);
-      let resultData = {} as any;
-      try {
-        resultData = JSON.parse(res?.data?.scoreData);
-      } catch (error) {
-        console.error("解析评测结果:", error);
-      }
-      // console.log("🚀 ~ resultData:", resultData);
-      detailData.musicalNotesPlayStats = resultData.musicalNotesPlayStats?.notesData || [];
-      detailData.userMeasureScore = resultData.userMeasureScore || {};
+		onMounted(async () => {
+			const res = await api_musicPracticeRecordDetail(query.id);
+			state.partIndex = Number(res?.data?.partIndex);
+			let resultData = {} as any;
+			try {
+				resultData = JSON.parse(res?.data?.scoreData);
+			} catch (error) {
+				console.error("解析评测结果:", error);
+			}
+			// console.log("🚀 ~ resultData:", resultData);
+			detailData.musicalNotesPlayStats = resultData.musicalNotesPlayStats?.notesData || [];
+			detailData.userMeasureScore = resultData.userMeasureScore || {};
 
-      scoreData.heardLevel = res.data?.heardLevel;
-      scoreData.cadence = res.data?.cadence;
-      scoreData.integrity = res.data?.integrity;
-      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]);
-      });
-    });
+			scoreData.heardLevel = res.data?.heardLevel;
+			scoreData.cadence = res.data?.cadence;
+			scoreData.integrity = res.data?.integrity;
+			scoreData.intonation = res.data?.intonation;
+			scoreData.score = res.data?.score;
+			scoreData.videoFilePath = res.data?.videoFilePath || res.data?.recordFilePath;
+			await getMusicDetail(resultData.musicalNotesPlayStats?.examSongId);
+			// 从练习记录进入评测报告,默认显示五线谱
+			if (!query.musicRenderType) {
+				state.musicRenderType = EnumMusicRenderType.staff
+			}
+			detailData.isLoading = false;
+			// Promise.all([
+			// 	getMusicSheetDetail(resultData.musicalNotesPlayStats?.examSongId),
+			// ]).then((values) => {
+			// 	getMusicInfo(values[0]);
+			// });
+		});
 
-    const setPathColor = () => {
-      for (const note of detailData.musicalNotesPlayStats) {
-        const active = state.times[note.musicalNotesIndex];
-        const svgEl = active?.id ? document.getElementById("vf-" + active?.id) : null;
-        svgEl?.classList.add(colorsClass[note.musicalErrorType]);
-      }
-    };
-    const setMearureColor = () => {
-      for (let key in detailData.userMeasureScore) {
-        addMeasureScore(detailData.userMeasureScore[key], false);
-      }
-    };
 
-    /** 渲染完成 */
-    const handleRendered = (osmd: any) => {
-      state.musicRendered = true;
-      state.osmd = osmd;
-      state.times = formateTimes(osmd);
-      console.log("🚀 ~ state.times:", state.times);
-      setPathColor();
-      setMearureColor();
-      api_cloudLoading();
-    };
-    onMounted(() => {
-      window.addEventListener("resize", resetMusicScore);
-    });
-    onBeforeUnmount(() => {
-      window.removeEventListener("resize", resetMusicScore);
-    });
+		const getOffsetPosition = (type: keyof typeof colorsClass): string => {
+			switch (type) {
+			  case 'EARLY':
+				return 'translateX(-2px)'
+			  case 'LATE':
+				return 'translateX(2px)'
+			  case 'HIGH':
+				return 'translateY(-2px)'
+			  case 'LOW':
+				return 'translateY(2px)'
+			  default:
+				return ''
+			}
+		  }
+	  
+		  const filterNotes = () => {
+			const include = ['RIGHT', 'WRONG', 'NOT_PLAYED']
+			if (scoreData.itemType === 'intonation') { // 音准
+			  include.push(...['HIGH', 'LOW'])
+			} else if (scoreData.itemType === 'cadence') { // 节奏
+			  include.push(...['EARLY', 'LATE'])
+			} else if (scoreData.itemType === 'integrity') { // 完整性
+			  include.push(...['DURATION_INSUFFICIENT'])
+			}
+			if (scoreData.itemType === 'cadence') {
+				return detailData.musicalNotesPlayStats.filter((item: any) => include.includes(item.rhythmicAssessment.result))
+			} else {
+				return detailData.musicalNotesPlayStats.filter((item: any) => include.includes(item.pitchAssessment.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')
+				const errType = scoreData.itemType === 'cadence' ? note.rhythmicAssessment.result : note.pitchAssessment.result
+				// console.log(1111222,errType)
+				const isNeedCopyElement = ['HIGH', 'LOW', 'EARLY', 'LATE'].includes(
+				  errType
+				)
+				stemEl?.classList.add(colorsClass[errType])
+				svgEl?.classList.add(colorsClass[errType])
+				if (svgEl && isNeedCopyElement) {
+				  stemEl?.classList.remove(colorsClass[errType])
+				  stemEl?.classList.add(colorsClass.RIGHT)
+				  svgEl?.classList.remove(colorsClass[errType])
+				  svgEl?.classList.add(colorsClass.RIGHT)
+				  const copySvg = svgEl.querySelector('.vf-notehead')!.cloneNode(true) as SVGSVGElement
+				  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.index];
+				const svgEl = active?.id ? document.getElementById("vf-" + active?.id) : null;
+				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 = () => {
+			for (let key in detailData.userMeasureScore) {
+				addMeasureScore(detailData.userMeasureScore[key], false);
+			}
+		};
 
-    return () => (
-      <div class={[styles.detail, state.setting.eyeProtection && "eyeProtection", styles.shareBox]} style={{ paddingLeft: detailData.paddingLeft }}>
-        <Transition name="van-fade">
-          {!state.musicRendered && (
-            <div class={styles.skeleton}>
-              <Skeleton class={styles.skeleton} row={8} />
-            </div>
-          )}
-        </Transition>
-        <div class={[styles.headHeight, detailData.headerHide && styles.headHide]} onClick={(e: Event) => e.stopPropagation()}>
-          <Transition name="van-slide-down">{state.musicRendered && <ShareTop scoreData={scoreData} />}</Transition>
-        </div>
-        <div id="scrollContainer" class={[styles.container, !state.setting.displayCursor && "hideCursor"]}>
-          <div class={styles.musicName}>{state.examSongName}</div>
-          {/* 曲谱渲染 */}
-          {!detailData.isLoading && <MusicScore onRendered={handleRendered} />}
-        </div>
-      </div>
-    );
-  },
-});
+		/** 渲染完成 */
+		const handleRendered = (osmd: any) => {
+			state.musicRendered = true;
+			state.osmd = osmd;
+			state.times = formateTimes(osmd);
+			console.log("🚀 ~ state.times:", state.times);
+			//setPathColor();
+			setViewColor();
+			setMearureColor();
+			api_cloudLoading();
+		};
+		onMounted(() => {
+			window.addEventListener("resize", resetMusicScore);
+		});
+		onBeforeUnmount(() => {
+			window.removeEventListener("resize", resetMusicScore);
+		});
+		watch(
+			() => scoreData.itemType,
+			() => {
+				setViewColor();
+			}
+		);
+		return () => (
+			<div
+				class={[styles.detail, state.setting.eyeProtection && "eyeProtection", styles.shareBox]}
+				style={{ paddingLeft: detailData.paddingLeft }}
+			>
+				<Transition name="van-fade">
+					{!state.musicRendered && (
+						<div class={styles.skeleton}>
+							<Skeleton class={styles.skeleton} row={8} />
+						</div>
+					)}
+				</Transition>
+				<div
+					class={[styles.headHeight, detailData.headerHide && styles.headHide]}
+					onClick={(e: Event) => e.stopPropagation()}
+				>
+					<Transition name="van-slide-down">
+						{state.musicRendered && <ShareTop scoreData={scoreData} />}
+					</Transition>
+				</div>
+				<div
+					id="scrollContainer"
+					class={[styles.container, !state.setting.displayCursor && "hideCursor"]}
+				>
+					<div class={styles.musicName}>{state.examSongName}</div>
+					{/* 曲谱渲染 */}
+					{!detailData.isLoading && <MusicScore onRendered={handleRendered} />}
+				</div>
+			</div>
+		);
+	},
+});

+ 1 - 1
src/page-instrument/view-product-img/index.tsx

@@ -74,7 +74,7 @@ export default defineComponent({
 			}
 
 			closeToast();
-			console.log(detailData.product);
+			console.log(detailData.product,123456);
 			window.parent?.postMessage(
 				{
 					api: "webApi_renderSvg",

+ 467 - 8
src/state.ts

@@ -4,13 +4,18 @@ import { OpenSheetMusicDisplay } from "../osmd-extended/src";
 import { metronomeData } from "./helpers/metronome";
 import { GradualNote, GradualTimes, GradualVersion } from "./type";
 import { handleEndEvaluat, handleStartEvaluat } from "./view/evaluating";
-import { IFingering } from "src/view/fingering/fingering-config";
+import { IFingering, mappingVoicePart, subjectFingering, matchVoicePart } from "/src/view/fingering/fingering-config";
 import { handleStartTick } from "./view/tick";
 import { audioListStart, getAudioCurrentTime, getAudioDuration, setAudioCurrentTime, setAudioPlaybackRate } from "./view/audio-list";
 import { toggleFollow } from "./view/follow-practice";
-import { browser, setStorageSpeed } from "./utils";
+import { browser, setStorageSpeed, setGlobalData } from "./utils";
 import { api_createMusicPlayer } from "./helpers/communication";
-import { verifyCanRepeat } from "./helpers/formateMusic";
+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();
 
 /** 入门 | 进阶 | 大师 */
 export type IDifficulty = "BEGINNER" | "ADVANCED" | "PERFORMER";
@@ -35,6 +40,205 @@ export enum IPlatform {
   PC = "PC",
 }
 
+/**
+ * 特殊教材分类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
+
+// 乐器code码
+export const musicalInstrumentCodeInfo = [
+  {
+    name: '长笛',
+    code: 'Flute',
+    id: 1
+  },
+  {
+    name: '短笛',
+    code: 'Piccolo',
+    id: 2
+  },
+  {
+    name: '单簧管',
+    code: 'Clarinet',
+    id: 3
+  },
+  {
+    name: '低音单簧管',
+    code: 'Bass Clarinet',
+    id: 4
+  },
+  {
+    name: '中音萨克斯',
+    code: 'Alto Saxophone',
+    id: 5
+  },
+  {
+    name: '次中音萨克斯',
+    code: 'Tenor Saxophone',
+    id: 6
+  },
+  {
+    name: '高音萨克斯',
+    code: 'Soprano Saxophone',
+    id: 7
+  },
+  {
+    name: '上低音萨克斯',
+    code: 'Baritone Saxophone',
+    id: 8
+  },
+  {
+    name: '双簧管',
+    code: 'Oboe',
+    id: 9
+  },
+  {
+    name: '大管',
+    code: 'Bassoon',
+    id: 10
+  },
+  {
+    name: '小号',
+    code: 'Trumpet',
+    id: 11
+  },
+  {
+    name: '圆号',
+    code: 'Horn',
+    id: 12
+  },
+  {
+    name: '长号',
+    code: 'Trombone',
+    id: 13
+  },
+  {
+    name: '上低音号',
+    code: 'Baritone',
+    id: 14
+  },
+  {
+    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({
   /** 来源 : PC , app */
   platform: "" as IPlatform,
@@ -59,6 +263,8 @@ const state = reactive({
   enableNotation: false,
   /** 曲谱ID */
   examSongId: "",
+  /** 内容平台的曲谱ID,可能会和业务端的id不一样 */
+  cbsExamSongId: "",
   /** 曲谱名称 */
   examSongName: "",
   /** 曲谱封面 */
@@ -67,7 +273,7 @@ const state = reactive({
   extConfigJson: {} as any,
   /** 扩展样式字段 */
   extStyleConfigJson: {} as any,
-  /** 是否开启节拍器 */
+  /** 是否开启节拍器(mp3节拍器) */
   isOpenMetronome: false,
   /** 是否显示指法 */
   isShowFingering: false,
@@ -87,6 +293,8 @@ const state = reactive({
   speed: 0,
   /** 曲谱音频正常的速度 */
   originSpeed: 0,
+  /** 播放过程中显示的速度 */
+  playIngSpeed: 0,
   /** 分轨名称 */
   track: "",
   /** 当前显示声部索引 */
@@ -140,7 +348,7 @@ const state = reactive({
     /** 显示光标 */
     displayCursor: true,
     /** 频率 */
-    frequency: 442,
+    frequency: 0,
     /** 评测难度 */
     evaluationDifficulty: "ADVANCED" as IDifficulty,
     /** 保存到相册 */
@@ -149,7 +357,11 @@ const state = reactive({
     enableAccompaniment: true,
     /** 反应时间 */
     reactionTimeMs: 0,
+    /** 节拍器音量 */
+    beatVolume: 50,
   },
+  /** 后台设置的基准评测频率 */
+  baseFrequency: 440,
   /** 节拍器的时间 */
   fixtime: 0,
   /** 指法信息 */
@@ -158,6 +370,8 @@ const state = reactive({
   scrollContainer: "musicAndSelection",
   /** 是否是打击乐 */
   isPercussion: false,
+  /** 评测标准 */
+  evaluationStandard: '',
   /** 是否重复节拍器的时间 */
   repeatedBeats: 0,
   /**当前曲谱中所有声部名字 */
@@ -186,6 +400,22 @@ const state = reactive({
   userChooseEndIndex: 0,
   /** 重播小节集合信息 */
   repeatInfo: [],  
+  /** 多分轨的曲子,可支持筛选的分轨 */
+  canSelectTracks: [] as any,
+  /** 声部codeId */
+  subjectCodeId: 0 as number,
+  /** 乐器codeId,用于匹配乐器指法、声部转调、特殊声部处理等 */
+  musicalCodeId: 0 as number,
+  /** 乐器code,用于评测传参 */
+  musicalCode: '' as any,
+  /** 合奏曲目是否合并展示 */
+  isCombineRender: false,
+  /** 小节的持续时长,以后台设置的播放速度计算 */
+  measureTime: 0,
+  /** 跟练模式,节拍器播放的时间 */
+  beatStartTime: 0,
+  /** 是否为详情预览模式 */
+  isPreView: false,
 });
 const browserInfo = browser();
 let offset_duration = 0;
@@ -260,6 +490,14 @@ const handlePlaying = () => {
   state.playProgress = (currentTime / duration) * 100;
   let item = getNote(currentTime);
   // console.log(11111,currentTime,duration,state.playSource, item.i)
+  // console.log(item.i,item.noteId,item.measureSpeed)
+  // 练习模式下,实时刷新小节速度
+  if (item && state.modeType === "practise" && state.playState === "play" && item.measureSpeed && item.measureSpeed !== state.playIngSpeed) {
+    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) {
     // 选段状态下
     if (state.sectionStatus && state.section.length === 2) {
@@ -267,6 +505,29 @@ const handlePlaying = () => {
       const selectStartItem = state.sectionFirst ? state.sectionFirst : state.section[0];
       const selectEndItem = state.section[1];
 
+      /**
+       * #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) {
         console.log("选段播放结束");
         // 如果为选段评测模式
@@ -287,7 +548,10 @@ const handlePlaying = () => {
     gotoNext(item);
   }
 
-  metronomeData.metro?.sound(currentTime);
+  // 评测不播放叮咚节拍器
+  if (state.modeType !== "evaluating") {
+    metronomeData.metro?.sound(currentTime);
+  }
 };
 /** 跳转到指定音符开始播放 */
 export const skipNotePlay = (itemIndex: number, isStart = false) => {
@@ -522,7 +786,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();
     }
   }
@@ -549,6 +814,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
@@ -586,7 +852,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;
@@ -620,6 +886,9 @@ export const isRhythmicExercises = () => {
 
 /** 重置状态 */
 export const handleRessetState = () => {
+  // 切换模式,清除选段
+  skipNotePlay(0, true);
+  clearSelection();
   if (state.modeType === "evaluating") {
     handleStartEvaluat();
   } else if (state.modeType === "practise") {
@@ -637,3 +906,193 @@ export const evaluatCreateMusicPlayer = () => {
     tuneSrc: "https://oss.dayaedu.com/cloud-coach/1686725501654check_music1_(1).mp3", //效音音频url
   });
 };
+
+
+
+/** 获取内容平台的接口详情并初始化state信息 */
+export const getMusicDetail = async (id: string) => {
+  const res = await getMusicSheetDetail(id);
+  if (res?.code === 200) {
+    getMusicInfo(res)
+  }
+};
+
+
+const getMusicInfo = (res: any) => {
+  const index = query["part-index"] ? parseInt(query["part-index"] as string) : 0;
+  // 原音声轨
+  const musicData = res.data.musicSheetSoundList?.[index] || {};
+  // 伴奏声轨
+  const accompanyData = res.data.musicSheetAccompanimentList?.[0] || {}
+  const musicInfo = {
+    ...res.data,
+    music: musicData.audioFileUrl || '',
+    accompany: accompanyData.audioFileUrl || '',
+    musicSheetId: musicData.musicSheetId || res.data.bizId,
+    track: musicData.track || '',
+  };
+  console.log("🚀 ~ musicInfo:", musicInfo);
+  setState(musicInfo, index);
+};
+
+const setState = (data: any, index: number) => {
+  state.appName = "COLEXIU";
+  state.detailId = data.bizId;
+  state.xmlUrl = data.xmlFileUrl;
+  state.partIndex = index;
+  state.trackId = data.track;
+  state.subjectId = data.subjectIds ? data.subjectIds.split(',')?.[0] : 0;
+  // 声部code
+  const subjectCode = data.subjectCodes ? data.subjectCodes.split(',')?.[0] : 0;
+  // 乐器code
+  let musicalCode = data.musicalInstrumentIdCodes ? data.musicalInstrumentIdCodes.split(',')?.[0] : 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.bizId + "";
+  state.cbsExamSongId = data.id + "";
+  state.examSongName = data.name;
+  state.coverImg = data.musicCover ?? "";
+  state.isCombineRender = data.musicSheetType === "SINGLE" && data.musicSheetSoundList?.length > 1
+  setCustom(state.isCombineRender ? data.musicSheetSoundList?.length : 0);
+  // 解析扩展字段
+  if (data.extConfigJson) {
+    try {
+      state.extConfigJson = JSON.parse(data.extConfigJson as string);
+    } catch (error) {
+      console.error("解析扩展字段错误:", error);
+    }
+  }
+  state.gradualTimes = state.extConfigJson.gradualTimes;
+  state.repeatedBeats = state.extConfigJson.repeatedBeats || 0;
+  // 曲子包含节拍器,就不开启节拍器
+  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;
+  state.music = data.music;
+  state.accompany = data.accompany;
+  state.midiUrl = data.midiFileUrl;
+  state.parentCategoriesId = data.musicTag;
+  state.musicSheetCategoriesId = data.musicCategoryId;
+  state.playMode = data.playMode === "MP3" ? "MP3" : "MIDI";
+  state.originSpeed = state.speed = data.playSpeed;
+  state.playIngSpeed = data.playSpeed;
+  const track = data.code || data.track;
+  state.track = track ? track.replace(/ /g, "").toLocaleLowerCase() : "";
+  // 能否评测,根据当前声轨有无伴奏判断
+  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(',');
+  // 开启预备小节
+  state.isOpenPrepare = true;
+  state.extStyleConfigJson = data.extStyleConfigJson || {}
+  // console.log("🚀 ~ state.subjectId:", state.subjectId, state.track as any , state.subjectId)
+  // 是否打击乐
+  /**
+   * 是否打击乐:AMPLITUDE & 节奏练习:DECIBELS
+   * evaluationStandard:("评测标准 节奏 AMPLITUDE 音准 FREQUENCY 分贝 DECIBELS")
+   * 打击乐&节奏练习,没有跟练模式
+   */
+  // state.isPercussion = isRhythmicExercises();
+  state.isPercussion = data.evaluationStandard === "AMPLITUDE" || data.evaluationStandard === "DECIBELS";
+  state.evaluationStandard = data.evaluationStandard?.toLocaleLowerCase() || ''
+  // 设置是否特殊曲谱, 是特殊曲谱取反(不理解之前的思考逻辑), 使用后台设置的速度
+  // state.isSpecialBookCategory = !classids.includes(data.musicCategoryId);
+
+  // 设置指法
+  // const code = state.isConcert ? mappingVoicePart(state.trackId, "ENSEMBLE") : mappingVoicePart(state.subjectId, "INSTRUMENT");
+  /**
+   * 各平台的乐器声部id不统一,为了兼容处理老的数据,加上乐器code码,此码唯一
+   * 获取指法code
+   */
+  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
+  // 如果切换的声轨没有指法,择指法开关置灰并且不可点击
+  if (!state.fingeringInfo.name && state.setting.displayFingering) {
+    state.setting.displayFingering = false
+  }
+
+  // 检测是否原音和伴奏都有
+  if (!state.music || !state.accompany) {
+    state.playSource = state.music ? "music" : "background";
+  }
+
+  // 如果是PC端,放大曲谱
+  state.platform = query.platform?.toLocaleUpperCase() || "";
+  if (state.platform === IPlatform.PC) {
+    state.zoom = query.zoom || 1.5;
+  }
+
+  /**
+   * 默认渲染什么谱面类型 & 能否转谱逻辑
+   * 渲染类型:首先取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' ? 'firstTone' : pitchTrack?.defaultScore === 'FIRST' ? 'fixedTone' : ''
+  }
+  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
+  }
+};
+
+// 多分轨合并显示标示
+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();
+    }
+  });
+};

+ 1 - 1
src/store.ts

@@ -23,7 +23,7 @@ type IUser = {
 };
 type IStatus = "init" | "login" | "logout" | "error";
 type IPlatformType = "STUDENT" | "TEACHER" | "WEB" | "";
-type IPlatformApi = "/api-student" | "/api-teacher" | "/api-web" | "/api-backend" | "/edu-app";
+type IPlatformApi = "/api-student" | "/api-teacher" | "/api-web" | "/api-backend" | "/edu-app" | "/cbs-app";
 type IProxy = "" | "/gym" | "/colexiu" | "/orchestra" | "/instrument";
 
 export interface IStoreData {

+ 21 - 0
src/utils/baseApi.ts

@@ -0,0 +1,21 @@
+import request from "./request";
+
+/** 获取内容平台的曲谱详情 */
+// export const getMusicSheetDetail = (sysMusicScoreId: string) => {
+//   return request.get("/open/musicSheet/detail/" + sysMusicScoreId, {
+//     isContentCenter: true, // 内容平台
+//   });
+// };
+
+/** 获取内容平台的曲谱详情 */
+export const getMusicSheetDetail = (sysMusicScoreId: string) => {
+  return request.get("/musicSheet/cbsDetail/" + sysMusicScoreId);
+};
+
+/** 生成图片 */
+export const creatMusicSheetImg = (data: any) => {
+  return request.post("/open/musicSheet/img", {
+    isContentCenter: true, // 内容平台
+    data
+  });
+};

+ 46 - 0
src/utils/index.ts

@@ -1,4 +1,6 @@
 import store from 'store'
+import { getQuery } from "./queryString";
+
 /** 获取浏览器信息 */
 export const browser = () => {
 	const u = navigator.userAgent;
@@ -97,3 +99,47 @@ export const getStorageSpeed = (id: any) => {
 	const speeds = store.get(SPEEDKEY) || {}
 	return speeds[id] || 0
 }
+
+/** 返回各产品应用对应的服务接口地址 */
+export const matchProductApiUrl = () => {
+	const query: any = getQuery();
+	const apiUrls = {
+		'cbs': {
+			'dev': 'https://dev.resource.colexiu.com',
+			'test': 'https://test.resource.colexiu.com',
+			'online': 'https://online.resource.colexiu.com'
+		},
+		'gym': {
+			'dev': 'https://dev.dayaedu.com',
+			'test': 'https://test.dayaedu.com',
+			'online': 'https://online.dayaedu.com'
+		},
+		'colexiu': {
+			'dev': 'https://dev.colexiu.com/',
+			'test': 'https://test.colexiu.com',
+			'online': 'https://online.colexiu.com'
+		},
+		'orchestra': {
+			'dev': 'https://dev.lexiaoya.cn',
+			'test': 'https://test.lexiaoya.cn',
+			'online': 'https://online.lexiaoya.cn'
+		},
+		'instrument': {
+			'dev': 'https://dev.kt.colexiu.com',
+			'test': 'https://test.kt.colexiu.com',
+			'test2': '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') ? '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'
+		// 兼容课堂乐器,测试环境两个域名
+		if (pathName === 'instrument' && environment === 'test') {
+			environment = location.origin.includes('//test.kt') ? 'test' : 'test2'
+		}
+		return apiUrls[pathName][environment] + '/edu-app'
+	}
+}

+ 34 - 6
src/utils/request.ts

@@ -5,29 +5,57 @@ import whiteUrl from "../constant/whiteUrl";
 import { storeData } from "../store";
 import { browser, getToken } from ".";
 import { postMessage } from "./native-message";
+import { matchProductApiUrl } from "./index"
+import { getQuery } from "/src/utils/queryString";
 
 const apiRouter = whiteUrl();
 const browserInfo = browser();
+const query = getQuery();
 
 const request = extend({
 	requestType: "form",
 	timeout: 10000,
+	//prefix: 'https://www.baidu.com',
 });
+// 返回各应用实际的接口api地址
+// if (!import.meta.env.DEV) {
+// 	const domainUrl = matchProductApiUrl()
+// 	request.extendOptions({ prefix: domainUrl})
+// }
 
 request.interceptors.request.use(
 	(url, options) => {
-		const _prefix = storeData.proxy + storeData.platformApi;
-		// 只有后台才去设置
-		if (storeData.platformType === "WEB" && (apiRouter as any)[url]) {
-			url = (apiRouter as any)[url];
+		// console.log(9999,storeData.proxy,storeData.platformApi,options)
+		// 内容平台的前缀为cbs-app
+		const platformApi = options.isContentCenter ? '/cbs-app' : storeData.platformApi
+		const _prefix = storeData.proxy + platformApi;
+		/** 
+		 * 只有后台才去设置
+		 * 暂时没有用到,先注释掉(2024.1.19)
+		 */
+		// if (storeData.platformType === "WEB" && (apiRouter as any)[url]) {
+		// 	url = (apiRouter as any)[url];
+		// }
+
+		/**
+		 * 区分开发环境和非开发环境
+		 * 
+		 */
+		if (!import.meta.env.DEV) {
+			const domainUrl = matchProductApiUrl()
+			url = domainUrl + url
+		} else {
+			url = _prefix + url
 		}
+
 		const Authorization = getToken();
 		const authHeaders: any = {};
-		if (Authorization) {
+		// 内容平台的接口不需要传token,需要传签名
+		if (Authorization && !options.isContentCenter) {
 			authHeaders.Authorization = Authorization;
 		}
 		return {
-			url: _prefix + url,
+			url: url,
 			options: {
 				...options,
 				params: cleanDeep(options.params),

+ 467 - 0
src/view/abnormal-pop/icon_bg.svg

@@ -0,0 +1,467 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="264px" height="143px" viewBox="0 0 264 143" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>弹窗</title>
+    <defs>
+        <linearGradient x1="-0.0244894336%" y1="49.9949859%" x2="70.0234389%" y2="49.9949859%" id="linearGradient-1">
+            <stop stop-color="#F2F6FA" stop-opacity="0" offset="0%"></stop>
+            <stop stop-color="#DDE4F2" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="0%" y1="49.9487602%" x2="70%" y2="49.9487602%" id="linearGradient-2">
+            <stop stop-color="#F2F6FA" stop-opacity="0" offset="0%"></stop>
+            <stop stop-color="#DDE4F2" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="5.09029467%" y1="50%" x2="76.7880827%" y2="50%" id="linearGradient-3">
+            <stop stop-color="#EBF1F4" offset="0%"></stop>
+            <stop stop-color="#DAE0ED" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="50%" y1="0%" x2="50%" y2="95.2575452%" id="linearGradient-4">
+            <stop stop-color="#D1E0F4" stop-opacity="0.614780239" offset="0%"></stop>
+            <stop stop-color="#D6DBE2" stop-opacity="0.555939788" offset="64.7084234%"></stop>
+            <stop stop-color="#D8D8D8" stop-opacity="0" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="5.09029467%" y1="50%" x2="76.7880827%" y2="50%" id="linearGradient-5">
+            <stop stop-color="#FFFFFF" offset="0%"></stop>
+            <stop stop-color="#FFFFFF" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="50%" y1="0%" x2="50%" y2="58.318734%" id="linearGradient-6">
+            <stop stop-color="#D4E0F4" offset="0%"></stop>
+            <stop stop-color="#EEEEEE" stop-opacity="0" offset="100%"></stop>
+            <stop stop-color="#DDE4F2" stop-opacity="0" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="326.303745%" y1="63.1438775%" x2="-84.820187%" y2="63.1438775%" id="linearGradient-7">
+            <stop stop-color="#FFD65E" offset="0%"></stop>
+            <stop stop-color="#FFAC0D" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="326.303745%" y1="63.1438775%" x2="-84.820187%" y2="63.1438775%" id="linearGradient-8">
+            <stop stop-color="#FFD65E" offset="0%"></stop>
+            <stop stop-color="#FFAC0D" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="326.303745%" y1="63.1438775%" x2="-84.820187%" y2="63.1438775%" id="linearGradient-9">
+            <stop stop-color="#FFD65E" offset="0%"></stop>
+            <stop stop-color="#FFAC0D" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-10">
+            <stop stop-color="#FFFFFF" stop-opacity="0.378442724" offset="0%"></stop>
+            <stop stop-color="#FFFFFF" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-11">
+            <stop stop-color="#FFFFFF" stop-opacity="0.378442724" offset="0%"></stop>
+            <stop stop-color="#FFFFFF" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-12">
+            <stop stop-color="#FFFFFF" stop-opacity="0.378442724" offset="0%"></stop>
+            <stop stop-color="#FFFFFF" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-13">
+            <stop stop-color="#FFFFFF" stop-opacity="0.214718265" offset="0%"></stop>
+            <stop stop-color="#FFFFFF" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-14">
+            <stop stop-color="#FFFFFF" stop-opacity="0.214718265" offset="0%"></stop>
+            <stop stop-color="#FFFFFF" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-15">
+            <stop stop-color="#FFFFFF" stop-opacity="0.214718265" offset="0%"></stop>
+            <stop stop-color="#FFFFFF" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="49.999948%" y1="0.000119714316%" x2="49.999948%" y2="99.9996127%" id="linearGradient-16">
+            <stop stop-color="#7EDAF5" offset="0%"></stop>
+            <stop stop-color="#00B4E0" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="49.999948%" y1="0.000119714316%" x2="49.999948%" y2="99.9996127%" id="linearGradient-17">
+            <stop stop-color="#7EDAF5" offset="0%"></stop>
+            <stop stop-color="#00B4E0" offset="100%"></stop>
+        </linearGradient>
+        <radialGradient cx="50%" cy="36.3210319%" fx="50%" fy="36.3210319%" r="62.043198%" gradientTransform="translate(0.500000,0.363210),scale(0.943592,1.000000),rotate(92.796051),translate(-0.500000,-0.363210)" id="radialGradient-18">
+            <stop stop-color="#7EDAF5" offset="0%"></stop>
+            <stop stop-color="#00B4E0" offset="100%"></stop>
+        </radialGradient>
+        <path d="M0,12.3503567 C0,18.8389356 5.71727466,24.0988465 12.7697329,24.0988465 L12.7697329,24.0988465 C19.8223854,24.0988465 25.5394658,18.8389356 25.5394658,12.3503567 L25.5394658,12.3503567 C25.5394658,5.8617779 22.3909428,0 12.7697329,0 L12.7697329,0 C3.42033926,0 0,5.8617779 0,12.3503567" id="path-19"></path>
+        <filter x="-5.9%" y="-6.2%" width="111.7%" height="112.4%" filterUnits="objectBoundingBox" id="filter-20">
+            <feGaussianBlur stdDeviation="1" in="SourceAlpha" result="shadowBlurInner1"></feGaussianBlur>
+            <feOffset dx="0" dy="1" in="shadowBlurInner1" result="shadowOffsetInner1"></feOffset>
+            <feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
+            <feColorMatrix values="0 0 0 0 0.963654435   0 0 0 0 1   0 0 0 0 0.817785942  0 0 0 0.5 0" type="matrix" in="shadowInnerInner1" result="shadowMatrixInner1"></feColorMatrix>
+            <feGaussianBlur stdDeviation="0.5" in="SourceAlpha" result="shadowBlurInner2"></feGaussianBlur>
+            <feOffset dx="0" dy="-2" in="shadowBlurInner2" result="shadowOffsetInner2"></feOffset>
+            <feComposite in="shadowOffsetInner2" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner2"></feComposite>
+            <feColorMatrix values="0 0 0 0 0.0379071198   0 0 0 0 0.660020874   0 0 0 0 0.808870177  0 0 0 1 0" type="matrix" in="shadowInnerInner2" result="shadowMatrixInner2"></feColorMatrix>
+            <feMerge>
+                <feMergeNode in="shadowMatrixInner1"></feMergeNode>
+                <feMergeNode in="shadowMatrixInner2"></feMergeNode>
+            </feMerge>
+        </filter>
+        <path d="M9.57236739,5.87845189 C9.57236739,5.87845189 3.84340912,12.2054888 1.53312229,11.637332 C-0.777164539,11.069416 4.9148392,6.34400813 7.68517226,5.12556581 C10.4555053,3.9071235 10.0764071,5.34016002 9.57236739,5.87845189" id="path-21"></path>
+        <filter x="-16.8%" y="-21.4%" width="133.6%" height="142.7%" filterUnits="objectBoundingBox" id="filter-22">
+            <feGaussianBlur stdDeviation="1" in="SourceAlpha" result="shadowBlurInner1"></feGaussianBlur>
+            <feOffset dx="0" dy="1" in="shadowBlurInner1" result="shadowOffsetInner1"></feOffset>
+            <feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
+            <feColorMatrix values="0 0 0 0 0.510063282   0 0 0 0 0.510063282   0 0 0 0 0.510063282  0 0 0 0.130195244 0" type="matrix" in="shadowInnerInner1"></feColorMatrix>
+        </filter>
+        <path d="M8.91251528,3.87143005 C8.91251528,3.87143005 2.76800705,9.82997184 0.50095954,9.11971558 C-1.76608797,8.40945933 4.23311604,4.04700713 7.08012348,3.00293766 C9.92713091,1.95910904 9.4517498,3.36565245 8.91251528,3.87143005" id="path-23"></path>
+        <filter x="-16.1%" y="-22.9%" width="132.2%" height="145.8%" filterUnits="objectBoundingBox" id="filter-24">
+            <feGaussianBlur stdDeviation="1" in="SourceAlpha" result="shadowBlurInner1"></feGaussianBlur>
+            <feOffset dx="0" dy="1" in="shadowBlurInner1" result="shadowOffsetInner1"></feOffset>
+            <feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
+            <feColorMatrix values="0 0 0 0 0.510063282   0 0 0 0 0.510063282   0 0 0 0 0.510063282  0 0 0 0.130195244 0" type="matrix" in="shadowInnerInner1"></feColorMatrix>
+        </filter>
+        <path d="M1.08886343,5.98117291 C-1.12614606,5.13339332 5.15260517,1.14786586 8.06346602,0.280818549 C10.9743269,-0.586228761 10.4059309,0.788041225 9.83401554,1.25985947 C9.83401554,1.25985947 3.30362153,6.82895251 1.08886343,5.98117291 Z" id="path-25"></path>
+        <filter x="-15.5%" y="-24.7%" width="131.0%" height="149.4%" filterUnits="objectBoundingBox" id="filter-26">
+            <feGaussianBlur stdDeviation="1" in="SourceAlpha" result="shadowBlurInner1"></feGaussianBlur>
+            <feOffset dx="0" dy="1" in="shadowBlurInner1" result="shadowOffsetInner1"></feOffset>
+            <feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
+            <feColorMatrix values="0 0 0 0 0.510063282   0 0 0 0 0.510063282   0 0 0 0 0.510063282  0 0 0 0.130195244 0" type="matrix" in="shadowInnerInner1"></feColorMatrix>
+        </filter>
+        <linearGradient x1="49.999948%" y1="0.000119714316%" x2="49.999948%" y2="99.9996127%" id="linearGradient-27">
+            <stop stop-color="#7EDAF5" offset="0%"></stop>
+            <stop stop-color="#00B4E0" offset="100%"></stop>
+        </linearGradient>
+        <path d="M4.09007989,6.03494397 C4.09007989,6.03494397 3.22378176,8.09040582 5.45069366,8.40894438 C5.45069366,8.40894438 5.53440841,10.9186493 7.83083344,9.78967287 C10.6051302,8.4255226 14.6117279,2.58478057 12.3309993,0.50539873 C11.9380882,0.147072451 11.4295709,0 10.8612574,0 C8.62861504,0 5.47112404,2.27003132 4.7580538,2.92794962 C4.7580538,2.92794962 1.87089167,5.26689895 4.09007989,6.03494397 Z" id="path-28"></path>
+        <filter x="-15.4%" y="-14.9%" width="130.8%" height="129.8%" filterUnits="objectBoundingBox" id="filter-29">
+            <feGaussianBlur stdDeviation="1" in="SourceAlpha" result="shadowBlurInner1"></feGaussianBlur>
+            <feOffset dx="0" dy="0" in="shadowBlurInner1" result="shadowOffsetInner1"></feOffset>
+            <feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
+            <feColorMatrix values="0 0 0 0 0.963654435   0 0 0 0 1   0 0 0 0 0.817785942  0 0 0 0.5 0" type="matrix" in="shadowInnerInner1" result="shadowMatrixInner1"></feColorMatrix>
+            <feGaussianBlur stdDeviation="0.5" in="SourceAlpha" result="shadowBlurInner2"></feGaussianBlur>
+            <feOffset dx="0" dy="-2" in="shadowBlurInner2" result="shadowOffsetInner2"></feOffset>
+            <feComposite in="shadowOffsetInner2" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner2"></feComposite>
+            <feColorMatrix values="0 0 0 0 0.0379071198   0 0 0 0 0.660020874   0 0 0 0 0.808870177  0 0 0 0.759662335 0" type="matrix" in="shadowInnerInner2" result="shadowMatrixInner2"></feColorMatrix>
+            <feMerge>
+                <feMergeNode in="shadowMatrixInner1"></feMergeNode>
+                <feMergeNode in="shadowMatrixInner2"></feMergeNode>
+            </feMerge>
+        </filter>
+        <path d="M9.57236739,5.87845189 C9.57236739,5.87845189 3.84340912,12.2054888 1.53312229,11.637332 C-0.777164539,11.069416 4.9148392,6.34400813 7.68517226,5.12556581 C10.4555053,3.9071235 10.0764071,5.34016002 9.57236739,5.87845189" id="path-30"></path>
+        <filter x="-16.8%" y="-21.4%" width="133.6%" height="142.7%" filterUnits="objectBoundingBox" id="filter-31">
+            <feGaussianBlur stdDeviation="1" in="SourceAlpha" result="shadowBlurInner1"></feGaussianBlur>
+            <feOffset dx="0" dy="1" in="shadowBlurInner1" result="shadowOffsetInner1"></feOffset>
+            <feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
+            <feColorMatrix values="0 0 0 0 0.510063282   0 0 0 0 0.510063282   0 0 0 0 0.510063282  0 0 0 0.130195244 0" type="matrix" in="shadowInnerInner1"></feColorMatrix>
+        </filter>
+        <path d="M8.91251528,3.87143005 C8.91251528,3.87143005 2.76800705,9.82997184 0.50095954,9.11971558 C-1.76608797,8.40945933 4.23311604,4.04700713 7.08012348,3.00293766 C9.92713091,1.95910904 9.4517498,3.36565245 8.91251528,3.87143005" id="path-32"></path>
+        <filter x="-16.1%" y="-22.9%" width="132.2%" height="145.8%" filterUnits="objectBoundingBox" id="filter-33">
+            <feGaussianBlur stdDeviation="1" in="SourceAlpha" result="shadowBlurInner1"></feGaussianBlur>
+            <feOffset dx="0" dy="1" in="shadowBlurInner1" result="shadowOffsetInner1"></feOffset>
+            <feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
+            <feColorMatrix values="0 0 0 0 0.510063282   0 0 0 0 0.510063282   0 0 0 0 0.510063282  0 0 0 0.130195244 0" type="matrix" in="shadowInnerInner1"></feColorMatrix>
+        </filter>
+        <path d="M1.08886343,5.98117291 C-1.12614606,5.13339332 5.15260517,1.14786586 8.06346602,0.280818549 C10.9743269,-0.586228761 10.4059309,0.788041225 9.83401554,1.25985947 C9.83401554,1.25985947 3.30362153,6.82895251 1.08886343,5.98117291 Z" id="path-34"></path>
+        <filter x="-15.5%" y="-24.7%" width="131.0%" height="149.4%" filterUnits="objectBoundingBox" id="filter-35">
+            <feGaussianBlur stdDeviation="1" in="SourceAlpha" result="shadowBlurInner1"></feGaussianBlur>
+            <feOffset dx="0" dy="1" in="shadowBlurInner1" result="shadowOffsetInner1"></feOffset>
+            <feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
+            <feColorMatrix values="0 0 0 0 0.510063282   0 0 0 0 0.510063282   0 0 0 0 0.510063282  0 0 0 0.130195244 0" type="matrix" in="shadowInnerInner1"></feColorMatrix>
+        </filter>
+        <path d="M4.09007989,6.03494397 C4.09007989,6.03494397 3.22378176,8.09040582 5.45069366,8.40894438 C5.45069366,8.40894438 5.53440841,10.9186493 7.83083344,9.78967287 C10.6051302,8.4255226 14.6117279,2.58478057 12.3309993,0.50539873 C11.9380882,0.147072451 11.4295709,5.69092099e-14 10.8612574,5.69092099e-14 C8.62861504,5.69092099e-14 5.47112404,2.27003132 4.7580538,2.92794962 C4.7580538,2.92794962 1.87089167,5.26689895 4.09007989,6.03494397 Z" id="path-36"></path>
+        <filter x="-15.4%" y="-14.9%" width="130.8%" height="129.8%" filterUnits="objectBoundingBox" id="filter-37">
+            <feGaussianBlur stdDeviation="1" in="SourceAlpha" result="shadowBlurInner1"></feGaussianBlur>
+            <feOffset dx="0" dy="0" in="shadowBlurInner1" result="shadowOffsetInner1"></feOffset>
+            <feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
+            <feColorMatrix values="0 0 0 0 0.963654435   0 0 0 0 1   0 0 0 0 0.817785942  0 0 0 0.5 0" type="matrix" in="shadowInnerInner1" result="shadowMatrixInner1"></feColorMatrix>
+            <feGaussianBlur stdDeviation="0.5" in="SourceAlpha" result="shadowBlurInner2"></feGaussianBlur>
+            <feOffset dx="0" dy="-2" in="shadowBlurInner2" result="shadowOffsetInner2"></feOffset>
+            <feComposite in="shadowOffsetInner2" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner2"></feComposite>
+            <feColorMatrix values="0 0 0 0 0.0379071198   0 0 0 0 0.660020874   0 0 0 0 0.808870177  0 0 0 0.759662335 0" type="matrix" in="shadowInnerInner2" result="shadowMatrixInner2"></feColorMatrix>
+            <feMerge>
+                <feMergeNode in="shadowMatrixInner1"></feMergeNode>
+                <feMergeNode in="shadowMatrixInner2"></feMergeNode>
+            </feMerge>
+        </filter>
+        <radialGradient cx="50%" cy="36.3210319%" fx="50%" fy="36.3210319%" r="62.0517369%" gradientTransform="translate(0.500000,0.363210),scale(0.893334,1.000000),rotate(92.953083),translate(-0.500000,-0.363210)" id="radialGradient-38">
+            <stop stop-color="#7EDAF5" offset="0%"></stop>
+            <stop stop-color="#00B4E0" offset="100%"></stop>
+        </radialGradient>
+        <path d="M19.7758976,36.7114737 C31.9559983,36.7897164 40.8500079,33.7380017 41.0909502,20.4285424 C41.3318924,7.11908315 33.7332894,0 19.7758976,0 C7.25603358,0 0.0960723846,9.3468521 0.0239292217,19.4466941 C-0.480625281,33.1279799 7.02292854,36.6293047 19.7758976,36.7114737 Z" id="path-39"></path>
+        <filter x="-3.6%" y="-4.1%" width="107.3%" height="108.2%" filterUnits="objectBoundingBox" id="filter-40">
+            <feGaussianBlur stdDeviation="1" in="SourceAlpha" result="shadowBlurInner1"></feGaussianBlur>
+            <feOffset dx="0" dy="1" in="shadowBlurInner1" result="shadowOffsetInner1"></feOffset>
+            <feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
+            <feColorMatrix values="0 0 0 0 0.963654435   0 0 0 0 1   0 0 0 0 0.817785942  0 0 0 0.5 0" type="matrix" in="shadowInnerInner1" result="shadowMatrixInner1"></feColorMatrix>
+            <feGaussianBlur stdDeviation="0.5" in="SourceAlpha" result="shadowBlurInner2"></feGaussianBlur>
+            <feOffset dx="0" dy="-2" in="shadowBlurInner2" result="shadowOffsetInner2"></feOffset>
+            <feComposite in="shadowOffsetInner2" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner2"></feComposite>
+            <feColorMatrix values="0 0 0 0 0.0379071198   0 0 0 0 0.660020874   0 0 0 0 0.808870177  0 0 0 1 0" type="matrix" in="shadowInnerInner2" result="shadowMatrixInner2"></feColorMatrix>
+            <feMerge>
+                <feMergeNode in="shadowMatrixInner1"></feMergeNode>
+                <feMergeNode in="shadowMatrixInner2"></feMergeNode>
+            </feMerge>
+        </filter>
+        <filter x="-15.7%" y="-16.4%" width="131.4%" height="132.8%" filterUnits="objectBoundingBox" id="filter-41">
+            <feGaussianBlur stdDeviation="0.4599936" in="SourceGraphic"></feGaussianBlur>
+        </filter>
+        <filter x="-15.7%" y="-16.4%" width="131.4%" height="132.8%" filterUnits="objectBoundingBox" id="filter-42">
+            <feGaussianBlur stdDeviation="0.4599936" in="SourceGraphic"></feGaussianBlur>
+        </filter>
+        <path d="M0,3.79107995 C0,5.88502391 1.74292375,7.58250935 3.89296033,7.58250935 L3.89296033,7.58250935 C6.0429969,7.58250935 7.78610008,5.88502391 7.78610008,3.79107995 L7.78610008,3.79107995 C7.78610008,1.69748544 6.0429969,0 3.89296033,0 L3.89296033,0 C1.74292375,0 0,1.69748544 0,3.79107995" id="path-43"></path>
+        <filter x="-6.4%" y="-6.6%" width="112.8%" height="113.2%" filterUnits="objectBoundingBox" id="filter-44">
+            <feOffset dx="0" dy="-1" in="SourceAlpha" result="shadowOffsetInner1"></feOffset>
+            <feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
+            <feColorMatrix values="0 0 0 0 1   0 0 0 0 1   0 0 0 0 1  0 0 0 0.5 0" type="matrix" in="shadowInnerInner1"></feColorMatrix>
+        </filter>
+        <linearGradient x1="49.9994754%" y1="99.9999421%" x2="49.9994754%" y2="0.00100872426%" id="linearGradient-45">
+            <stop stop-color="#3DD8CD" offset="0%"></stop>
+            <stop stop-color="#007FC1" offset="100%"></stop>
+        </linearGradient>
+        <path d="M6.10794835,2.97405041 C6.10794835,4.61667117 4.74073825,5.94827555 3.05397418,5.94827555 C1.36721011,5.94827555 0,4.61667117 0,2.97405041 C0,1.33160438 1.36721011,0 3.05397418,0 C4.74073825,0 6.10794835,1.33160438 6.10794835,2.97405041" id="path-46"></path>
+        <linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-48">
+            <stop stop-color="#2785AB" offset="0%"></stop>
+            <stop stop-color="#175169" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="0.00080078125%" y1="50.000744%" x2="99.9996171%" y2="50.000744%" id="linearGradient-49">
+            <stop stop-color="#0B2B56" offset="0%"></stop>
+            <stop stop-color="#1C8795" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="-0.00139152473%" y1="50.000508%" x2="99.99952%" y2="50.000508%" id="linearGradient-50">
+            <stop stop-color="#F4CF07" offset="0%"></stop>
+            <stop stop-color="#F29942" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="0%" y1="59.2364439%" x2="101.94585%" y2="59.2364439%" id="linearGradient-51">
+            <stop stop-color="#F9DE13" offset="0%"></stop>
+            <stop stop-color="#F8B911" offset="48.5652346%"></stop>
+            <stop stop-color="#F7D510" offset="49.0626726%"></stop>
+            <stop stop-color="#F5CB0D" offset="100%"></stop>
+            <stop stop-color="#F1BA08" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="25.7675489%" y1="99.1220678%" x2="74.2348192%" y2="0.868067724%" id="linearGradient-52">
+            <stop stop-color="#F4CF07" offset="0%"></stop>
+            <stop stop-color="#F28042" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="74.2260743%" y1="99.1220678%" x2="25.771556%" y2="0.868067724%" id="linearGradient-53">
+            <stop stop-color="#F4CF07" offset="0%"></stop>
+            <stop stop-color="#F28042" offset="100%"></stop>
+        </linearGradient>
+        <path d="M6.79551688,12.1382042 C6.79551688,12.1382042 4.35061946,13.368173 2.0527837,12.2918061 C1.96718209,12.1445256 4.62971303,15.7594538 6.79551688,12.1382042 Z" id="path-54"></path>
+        <filter x="-63.2%" y="-185.6%" width="226.5%" height="471.1%" filterUnits="objectBoundingBox" id="filter-55">
+            <feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+            <feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+            <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.156990105 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+        </filter>
+        <path d="M0,3.79107995 C0,5.88502391 1.74292375,7.58250935 3.89296033,7.58250935 L3.89296033,7.58250935 C6.0429969,7.58250935 7.78610008,5.88502391 7.78610008,3.79107995 L7.78610008,3.79107995 C7.78610008,1.69748544 6.0429969,0 3.89296033,0 L3.89296033,0 C1.74292375,0 0,1.69748544 0,3.79107995" id="path-56"></path>
+        <filter x="-6.4%" y="-6.6%" width="112.8%" height="113.2%" filterUnits="objectBoundingBox" id="filter-57">
+            <feOffset dx="0" dy="-1" in="SourceAlpha" result="shadowOffsetInner1"></feOffset>
+            <feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
+            <feColorMatrix values="0 0 0 0 1   0 0 0 0 1   0 0 0 0 1  0 0 0 0.5 0" type="matrix" in="shadowInnerInner1"></feColorMatrix>
+        </filter>
+        <path d="M6.10794835,2.97405041 C6.10794835,4.61667117 4.74073825,5.94827555 3.05397418,5.94827555 C1.36721011,5.94827555 0,4.61667117 0,2.97405041 C0,1.33160438 1.36721011,0 3.05397418,0 C4.74073825,0 6.10794835,1.33160438 6.10794835,2.97405041" id="path-58"></path>
+        <linearGradient x1="57.4785438%" y1="0%" x2="60.3470534%" y2="11.9988623%" id="linearGradient-60">
+            <stop stop-color="#FFFFFF" offset="0%"></stop>
+            <stop stop-color="#FFFFFF" stop-opacity="0" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="57.4790082%" y1="0%" x2="60.3476959%" y2="11.9988623%" id="linearGradient-61">
+            <stop stop-color="#FFFFFF" offset="0%"></stop>
+            <stop stop-color="#FFFFFF" stop-opacity="0" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="49.999948%" y1="0.000119714316%" x2="49.999948%" y2="99.9996127%" id="linearGradient-62">
+            <stop stop-color="#00B4E0" offset="0%"></stop>
+            <stop stop-color="#7EDAF5" offset="100%"></stop>
+        </linearGradient>
+        <path d="M19.7282816,3.87486046 C19.9195842,5.48006804 20.7449349,7.88312197 20.0861111,8.63687727 L20.0861111,8.63687727 C19.5794337,9.21646263 19.9620566,9.44437173 19.9620566,9.44437173 L19.9620566,9.44437173 C20.3183147,9.63667546 20.7234711,9.3237875 21.3420451,8.87086536 L21.3420451,8.87086536 C22.2620022,8.19683552 24.2811494,6.15479616 24.8198373,4.59395311 L24.8198373,4.59395311 C25.300654,3.20024379 25.0855818,1.38019251 23.2086318,0.864284816 L23.2086318,0.864284816 C22.9129444,0.783082917 22.6238442,0.744022428 22.3460002,0.743292409 L22.3460002,0.743292409 C20.7311222,0.739286289 19.5088327,2.03243553 19.7282816,3.87486046" id="path-63"></path>
+        <filter x="-37.5%" y="-22.8%" width="175.0%" height="145.7%" filterUnits="objectBoundingBox" id="filter-64">
+            <feGaussianBlur stdDeviation="1" in="SourceAlpha" result="shadowBlurInner1"></feGaussianBlur>
+            <feOffset dx="-1" dy="2" in="shadowBlurInner1" result="shadowOffsetInner1"></feOffset>
+            <feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
+            <feColorMatrix values="0 0 0 0 0.963654435   0 0 0 0 1   0 0 0 0 0.817785942  0 0 0 0.5 0" type="matrix" in="shadowInnerInner1"></feColorMatrix>
+        </filter>
+        <path d="M0.14736819,0.0146874316 C-0.0263255418,0.0868231717 -0.0499231311,0.406786081 0.0949505936,0.729351826 L0.0949505936,0.729351826 C0.239824318,1.05191757 0.498430687,1.25493878 0.672124418,1.18261712 L0.672124418,1.18261712 C0.846011572,1.11048138 0.869415739,0.790518471 0.724735437,0.467952726 L0.724735437,0.467952726 C0.599397421,0.189449276 0.389727037,0 0.222803106,0 L0.222803106,0 C0.196497597,0 0.171159202,0.00483383825 0.14736819,0.0146874316" id="path-65"></path>
+        <linearGradient x1="34.99868%" y1="1.21970101%" x2="65.0004535%" y2="98.7839599%" id="linearGradient-67">
+            <stop stop-color="#F2A305" offset="0%"></stop>
+            <stop stop-color="#F2D677" offset="100%"></stop>
+        </linearGradient>
+        <path d="M0.0949637336,0.467952726 C-0.0499300399,0.790518471 -0.0263291849,1.11048138 0.147388584,1.18261712 L0.147388584,1.18261712 C0.321299802,1.25493878 0.579555059,1.05191757 0.724642282,0.729351826 L0.724642282,0.729351826 C0.869536056,0.406786081 0.845935201,0.0868231717 0.672023982,0.0146874316 L0.672023982,0.0146874316 C0.648423128,0.00483383825 0.623081226,0 0.596772076,0 L0.596772076,0 C0.429825045,0 0.219932195,0.189449276 0.0949637336,0.467952726" id="path-68"></path>
+        <linearGradient x1="65.0010633%" y1="1.21970101%" x2="34.9951379%" y2="98.7839599%" id="linearGradient-70">
+            <stop stop-color="#F2A305" offset="0%"></stop>
+            <stop stop-color="#F2D677" offset="100%"></stop>
+        </linearGradient>
+    </defs>
+    <g id="权限弹窗、" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="网络异常" transform="translate(-274.000000, -45.000000)">
+            <g id="编组-5备份" transform="translate(274.000000, 45.000000)">
+                <g id="编组-6" transform="translate(0.000000, -2.000000)">
+                    <rect id="矩形备份" x="46" y="0" width="173" height="173"></rect>
+                    <g id="编组-5" transform="translate(58.254167, 27.391667)">
+                        <path d="M0.0177655446,68.7205949 C0.868348878,65.5777615 4.22743221,56.0483449 12.4809739,61.8582615 C12.4809739,61.8582615 17.1015155,46.5838032 30.9847655,58.5928865 C30.9847655,58.5928865 38.0345155,54.1669699 41.5089322,62.9178865 C41.5089322,62.9178865 45.8339322,64.7271782 45.1130989,68.3601782 L0.745807211,69.6000115 C0.522440351,69.6077526 0.308117751,69.5114439 0.165591877,69.3392839 C0.0230660024,69.1671239 -0.0315387657,68.9385897 0.0177655446,68.7205949 Z" id="路径" fill="url(#linearGradient-1)" fill-rule="nonzero"></path>
+                        <path d="M108.193224,38.8420532 C108.193224,38.8420532 110.420599,32.6140532 114.125682,34.9928032 C114.125682,34.9928032 118.378599,27.0636365 126.185224,32.9168032 C126.185224,32.9168032 129.868682,31.8499699 131.822141,36.1533449 C131.822141,36.1533449 133.746766,35.8289699 134.510849,37.2634282 L108.193224,38.8420532 Z" id="路径" fill="url(#linearGradient-2)" fill-rule="nonzero"></path>
+                        <g id="编组-4" transform="translate(33.802111, 0.000000)">
+                            <g id="编组-3">
+                                <path d="M31.1166962,1.44521986 L31.1366962,1.44521986 C35.9084364,1.44521986 39.7766962,5.31347962 39.7766962,10.0852199 L39.7766962,15.1510532 C39.7766962,19.9227934 35.9084364,23.7910532 31.1366962,23.7910532 L31.1166962,23.7910532 C26.3449559,23.7910532 22.4766962,19.9227934 22.4766962,15.1510532 L22.4766962,10.0852199 C22.4766962,5.31347962 26.3449559,1.44521986 31.1166962,1.44521986 Z" id="矩形" fill="#E2E5ED" transform="translate(31.126696, 12.618137) rotate(-11.000000) translate(-31.126696, -12.618137) "></path>
+                                <circle id="椭圆形" fill="#FDFDFD" cx="26.8016962" cy="6.4910532" r="2.1625"></circle>
+                                <ellipse id="椭圆形" fill="url(#linearGradient-3)" transform="translate(34.010030, 22.709803) rotate(-10.000000) translate(-34.010030, -22.709803) " cx="34.0100295" cy="22.7098032" rx="32.4375" ry="11.89375"></ellipse>
+                            </g>
+                            <path d="M23.382899,26.5271687 L8.28879311,118.22022 L93.8010874,113.008595 L45.7013601,22.7098032 C43.0668962,21.3032813 39.2014448,21.0812553 34.1050059,22.0437253 C29.0085669,23.0061952 25.4345313,24.5006764 23.382899,26.5271687 Z" id="路径-18" fill="url(#linearGradient-4)"></path>
+                            <ellipse id="椭圆形" fill="url(#linearGradient-5)" opacity="0.596843175" transform="translate(34.730863, 25.593137) rotate(-10.000000) translate(-34.730863, -25.593137) " cx="34.7308628" cy="25.5931365" rx="11.5333333" ry="3.60416667"></ellipse>
+                            <path d="M34.0100295,54.8383746 C35.7300392,54.8383746 37.2707931,55.6052737 38.3095139,56.8155867 C38.4757609,57.0131433 38.5763799,57.2679208 38.5763799,57.5463266 C38.5763799,58.1719166 38.0692319,58.6790647 37.4436419,58.6790647 C37.1028293,58.6790647 36.7974148,58.5283398 36.589752,58.2901639 L36.4516296,58.1387121 C35.8338844,57.5006037 34.9683272,57.1038508 34.0100295,57.1038508 C32.9780166,57.1038508 32.0535607,57.5639903 31.4303424,58.2901816 L31.3355057,58.3867006 C31.1346605,58.568328 30.8685422,58.6790647 30.5764172,58.6790647 C29.9508271,58.6790647 29.4436791,58.1719166 29.4436791,57.5463266 C29.4436791,57.2679208 29.5442981,57.0131433 29.7109168,56.8159053 C30.7492659,55.6052737 32.2900198,54.8383746 34.0100295,54.8383746 Z M34.0100295,49.7410532 C37.0174491,49.7410532 39.7205516,51.0434542 41.5865252,53.1148428 C41.8036216,53.3211958 41.9391962,53.6125396 41.9391962,53.935724 C41.9391962,54.561314 41.4320481,55.068462 40.8064581,55.068462 C40.4809198,55.068462 40.1876645,54.9309406 39.981046,54.7110478 C38.5239377,53.0546661 36.3894344,52.0065294 34.0100295,52.0065294 C31.6306246,52.0065294 29.4961213,53.0546661 28.0427475,54.7143221 C27.8323945,54.9309406 27.5391392,55.068462 27.2136009,55.068462 C26.5880109,55.068462 26.0808628,54.561314 26.0808628,53.935724 C26.0808628,53.6125396 26.2164374,53.3211958 26.4335338,53.1148428 C28.2995074,51.0434542 31.0026099,49.7410532 34.0100295,49.7410532 Z M32.5941069,61.3516187 C32.5941069,62.1336111 33.228037,62.7675413 34.0100295,62.7675413 C34.792022,62.7675413 35.4259521,62.1336111 35.4259521,61.3516187 C35.4259521,60.5696262 34.792022,59.9356961 34.0100295,59.9356961 C33.228037,59.9356961 32.5941069,60.5696262 32.5941069,61.3516187 Z" id="形状结合" fill="#C0C6DE" fill-rule="nonzero" transform="translate(34.010030, 56.254297) rotate(-18.000000) translate(-34.010030, -56.254297) "></path>
+                        </g>
+                        <path d="M56.8926263,93.109235 L60.4351184,98.7300027 C66.0475319,97.9062226 71.9366279,97.4229405 77.9732064,97.3318433 L78.8317742,94.1899817 C79.0652523,93.3355937 79.7697574,92.6790041 80.4053302,92.7234477 L88.4609755,93.2867533 C89.0965484,93.3311969 89.7028176,94.0794448 89.8151158,94.9580117 L90.1647623,97.6852267 C121.562623,99.9915234 147.103807,112.82537 147.103807,128.311887 C147.103807,145.430413 115.602579,137.853056 79.7731063,137.853056 C43.9436331,137.853056 13.0288072,145.430413 13.0288072,128.311887 C13.0288072,118.810458 22.6429686,110.307541 37.325284,104.621789 L37.1554678,99.1434899 C37.1169651,97.9063752 38.0116318,96.6204259 39.1537616,96.2712417 L53.6298249,91.8454651 C54.7719547,91.4962809 56.2327607,92.06209 56.8926263,93.109235 Z" id="形状结合" fill="url(#linearGradient-6)" opacity="0.607172648"></path>
+                        <g id="鸟" transform="translate(86.139621, 83.616666) rotate(-1.000000) translate(-86.139621, -83.616666) translate(59.829080, 49.737499)">
+                            <rect id="矩形" x="0.238475119" y="0" width="52.0457558" height="67.7583333"></rect>
+                            <g id="编组-2" transform="translate(-0.000000, 0.193174)">
+                                <g id="编组-18" transform="translate(16.270577, 56.052121)">
+                                    <g id="编组-13" transform="translate(10.779863, 0.000000)">
+                                        <path d="M4.97039622,0 C5.44326037,2.58792154 5.34919104,5.01753667 4.90506194,7.14975799 L4.83908211,7.45295379 C4.82810502,7.50124499 4.81715137,7.54840666 4.80624164,7.59442561 L4.74147439,7.85672095 C4.73082236,7.89811618 4.72025525,7.93834227 4.70979354,7.97738601 L4.64845222,8.19735477 L4.59062447,8.3883135 L4.537294,8.54962838 L4.51262265,8.6189713 L2.67929101,7.97671702 L2.73096105,7.81926885 L2.77088443,7.68622625 C2.84737605,7.42425832 2.92463008,7.11715522 2.99719883,6.76875959 C3.37691922,4.94575834 3.47147251,2.86656995 3.10865295,0.652907288 L3.05369161,0.335768116 L4.97039622,0 Z" id="Stroke-13" fill="url(#linearGradient-7)" fill-rule="nonzero"></path>
+                                        <g id="编组-12" transform="translate(0.000000, 6.951804)">
+                                            <path d="M0,1.34591361 C0,2.08939679 0.6227124,2.69203837 1.39095709,2.69203837 L1.39095709,2.69203837 C2.15920179,2.69203837 2.78191419,2.08939679 2.78191419,1.34591361 L2.78191419,1.34591361 C2.78191419,0.602430424 2.15920179,0 1.39095709,0 L1.39095709,0 C0.6227124,0 0,0.602430424 0,1.34591361" id="Fill-19" fill="url(#linearGradient-8)" fill-rule="nonzero"></path>
+                                            <path d="M3.59597865,2.69203837 C4.36400515,2.69203837 4.98693574,2.08939679 4.98693574,1.34591361 C4.98693574,0.602430424 4.36400515,-2.84546049e-14 3.59597865,-2.84546049e-14 C2.82773395,-2.84546049e-14 2.20523974,0.602430424 2.20523974,1.34591361 C2.20523974,2.08939679 2.82773395,2.69203837 3.59597865,2.69203837 Z" id="Fill-17" fill="url(#linearGradient-9)" fill-rule="nonzero"></path>
+                                            <path d="M6.08028257,2.69203837 C6.84874545,2.69203837 7.47145785,2.08939679 7.47145785,1.34591361 C7.47145785,0.602430424 6.84874545,0 6.08028257,0 C5.31225606,0 4.68954366,0.602430424 4.68954366,1.34591361 C4.68954366,2.08939679 5.31225606,2.69203837 6.08028257,2.69203837 Z" id="Fill-21" fill="url(#linearGradient-8)" fill-rule="nonzero"></path>
+                                            <g id="编组-17" transform="translate(0.849848, 1.302415)">
+                                                <path d="M0,0.383837446 C0.00698205914,0.4369742 0.0240008283,0.490326956 0.0521472542,0.541951689 L0.0521472542,0.541951689 L0.234117171,0.875028414 C0.322047478,1.0363827 0.553546376,1.0428628 0.650640636,0.886476577 L0.650640636,0.886476577 L0.850938458,0.564200005 C0.884103239,0.511063251 0.904613038,0.454902454 0.91399518,0.39895766 L0.91399518,0.39895766 C0.886066943,0.188786678 0.710860896,0.00691209807 0.46867072,0.000216003065 L0.46867072,0.000216003065 C0.464088744,0.000216003065 0.459506767,0 0.454924791,0 L0.454924791,0 C0.215789265,0 0.0349102957,0.174962482 0,0.383837446" id="Fill-43" fill="url(#linearGradient-10)"></path>
+                                                <path d="M2.28793351,0.383621443 C2.29513375,0.436758196 2.31215252,0.490326956 2.34029895,0.541951689 L2.34029895,0.541951689 L2.52205068,0.875028414 C2.61019917,1.0363827 2.84191626,1.0428628 2.93857414,0.886476577 L2.93857414,0.886476577 L3.13930834,0.564200005 C3.17225493,0.511063251 3.19276473,0.455118457 3.20236506,0.399173663 L3.20236506,0.399173663 C3.17443683,0.189002681 2.99923078,0.00691209806 2.7570406,0.000216003065 L2.7570406,0.000216003065 C2.75245863,0.000216003065 2.74765846,-2.91076637e-14 2.74329467,-2.91076637e-14 L2.74329467,-2.91076637e-14 C2.50394096,-2.91076637e-14 2.32306199,0.174962482 2.28793351,0.383621443" id="Fill-47" fill="url(#linearGradient-11)"></path>
+                                                <path d="M4.9079512,0.383837446 C4.91515145,0.4369742 4.93217022,0.490326956 4.96031664,0.541951689 L4.96031664,0.541951689 L5.14185018,0.875028414 C5.22999868,1.0363827 5.46171576,1.0428628 5.55859183,0.886476577 L5.55859183,0.886476577 L5.75888966,0.564200005 C5.79205444,0.510847248 5.81256424,0.454686451 5.82216457,0.398525654 L5.82216457,0.398525654 C5.79379995,0.188570675 5.61881209,0.00691209806 5.37684011,0.000216003065 L5.37684011,0.000216003065 C5.37225813,0.000216003065 5.36745797,-2.91076637e-14 5.36309418,-2.91076637e-14 L5.36309418,-2.91076637e-14 C5.12374046,-2.91076637e-14 4.94286149,0.174962482 4.9079512,0.383837446" id="Fill-51" fill="url(#linearGradient-12)"></path>
+                                            </g>
+                                        </g>
+                                    </g>
+                                    <g id="左脚" transform="translate(0.000000, 0.000000)">
+                                        <path d="M2.57688243,0 L4.49358704,0.335768116 C4.0674477,2.66797108 4.15227751,4.85894876 4.55007982,6.76875959 C4.60813482,7.04747609 4.6691884,7.29976538 4.73045134,7.52366001 L4.79740283,7.75718538 L4.85897355,7.95120075 L4.86798764,7.97671702 L3.034656,8.6189713 L2.98394306,8.47271604 C2.97504782,8.44582583 2.96594469,8.41768724 2.95665418,8.3883135 L2.89882643,8.19735477 L2.8374851,7.97738601 C2.8270234,7.93834227 2.81645629,7.89811618 2.80580426,7.85672095 L2.74103701,7.59442561 L2.67521545,7.30470484 C2.66421198,7.2541678 2.65320557,7.20251445 2.64221671,7.14975799 C2.21739757,5.11024194 2.11285996,2.79863024 2.5184018,0.336645784 L2.57688243,0 Z" id="Stroke-15" fill="url(#linearGradient-7)" fill-rule="nonzero"></path>
+                                        <path d="M0,8.29771747 C0,9.04120065 0.622930589,9.64384223 1.39095709,9.64384223 L1.39095709,9.64384223 C2.15920179,9.64384223 2.78191419,9.04120065 2.78191419,8.29771747 L2.78191419,8.29771747 C2.78191419,7.55423429 2.15920179,6.95180386 1.39095709,6.95180386 L1.39095709,6.95180386 C0.622930589,6.95180386 0,7.55423429 0,8.29771747" id="Fill-25" fill="url(#linearGradient-8)" fill-rule="nonzero"></path>
+                                        <path d="M4.68954366,8.29771747 C4.68954366,9.04120065 5.31247425,9.64384223 6.08071894,9.64384223 L6.08071894,9.64384223 C6.84874545,9.64384223 7.47123966,9.04120065 7.47123966,8.29771747 L7.47123966,8.29771747 C7.47123966,7.55423429 6.84874545,6.95180386 6.08071894,6.95180386 L6.08071894,6.95180386 C5.31247425,6.95180386 4.68954366,7.55423429 4.68954366,8.29771747" id="Fill-27" fill="url(#linearGradient-9)" fill-rule="nonzero"></path>
+                                        <path d="M2.20502155,8.29771747 C2.20502155,9.04120065 2.82795214,9.64384223 3.59619684,9.64384223 L3.59619684,9.64384223 C4.36422334,9.64384223 4.98693574,9.04120065 4.98693574,8.29771747 L4.98693574,8.29771747 C4.98693574,7.55423429 4.36422334,6.95180386 3.59619684,6.95180386 L3.59619684,6.95180386 C2.82795214,6.95180386 2.20502155,7.55423429 2.20502155,8.29771747" id="Fill-23" fill="url(#linearGradient-8)" fill-rule="nonzero"></path>
+                                        <g id="编组-16" transform="translate(0.833579, 8.124991)">
+                                            <path d="M0.0561969924,0.659020453 L0.237948719,0.984624279 C0.326097216,1.14235843 0.557814304,1.14869313 0.654690374,0.99581559 L0.654690374,0.99581559 L0.854988196,0.680769606 C1.0413219,0.387895089 0.83011461,0.00992437081 0.47533873,0.000211156826 L0.47533873,0.000211156826 C0.470756754,0.000211156826 0.466174778,0 0.461810991,0 L0.461810991,0 C0.11401717,0 -0.11049967,0.360444702 0.0561969924,0.659020453" id="Fill-29" fill="url(#linearGradient-13)"></path>
+                                            <path d="M2.30442004,0.504453657 C2.3114021,0.556398236 2.32842086,0.608553972 2.35678548,0.659020453 L2.35678548,0.659020453 L2.53831902,0.984624279 C2.6266857,1.14235843 2.85796641,1.14869313 2.95527886,0.99581559 L2.95527886,0.99581559 L3.15557668,0.680769606 C3.18852328,0.628825027 3.20903307,0.573924252 3.21863341,0.519234635 L3.21863341,0.519234635 C3.19048698,0.313779043 3.01528093,0.135984996 2.77309076,0.129439134 L2.77309076,0.129439134 C2.76850878,0.129439134 2.7639268,0.129227977 2.75934483,0.129227977 L2.75934483,0.129227977 C2.5202093,0.129227977 2.33911214,0.300265006 2.30442004,0.504453657" id="Fill-35" fill="url(#linearGradient-14)"></path>
+                                            <path d="M4.94167469,0.504453657 C4.94887494,0.556398236 4.96589371,0.608553972 4.99404013,0.659020453 L4.99404013,0.659020453 L5.17579186,0.984624279 C5.26415854,1.14235843 5.49543925,1.14869313 5.5927517,0.99581559 L5.5927517,0.99581559 L5.79326771,0.680769606 C5.82621431,0.628825027 5.84650591,0.574135409 5.85610625,0.519445791 L5.85610625,0.519445791 C5.82817801,0.3139902 5.65297196,0.135984996 5.41078179,0.129439134 L5.41078179,0.129439134 C5.40619981,0.129439134 5.40161783,0.129227977 5.39703586,0.129227977 L5.39703586,0.129227977 C5.15768214,0.129227977 4.97658498,0.300265006 4.94167469,0.504453657" id="Fill-39" fill="url(#linearGradient-15)"></path>
+                                        </g>
+                                    </g>
+                                </g>
+                                <path d="M24.3744895,59.1952954 C24.3708866,59.2147729 23.9159262,61.7120075 25.4201178,63.5951404 L25.4201178,63.5951404 C25.4919747,63.6854109 25.641293,63.6766086 25.7015407,63.5792214 L25.7015407,63.5792214 C26.0384076,63.0336653 26.8850783,61.3919405 26.4799574,59.1966064 L26.4799574,59.1966064 C26.4797572,59.1958573 26.4791568,59.1952954 26.4783561,59.1952954 L26.4783561,59.1952954 L24.3758906,59.1952954 C24.3752901,59.1952954 24.3748898,59.1951082 24.3746896,59.1951082 L24.3746896,59.1951082 C24.3746896,59.1951082 24.3744895,59.1951082 24.3744895,59.1952954" id="Fill-3备份" fill="url(#linearGradient-16)"></path>
+                                <path d="M22.9928276,58.7492256 C22.990032,58.7664902 22.6258527,61.0144137 23.8292844,62.7090867 L23.8292844,62.7090867 C23.8866883,62.7901124 24.0063419,62.7820687 24.0544269,62.6945688 L24.0544269,62.6945688 C24.3241134,62.2037061 25.0014049,60.7262133 24.6771101,58.7502065 L24.6771101,58.7502065 C24.6771101,58.749618 24.676551,58.7492256 24.6759919,58.7492256 L24.6759919,58.7492256 L22.9939459,58.7492256 C22.9935731,58.7492256 22.9932004,58.7488332 22.993014,58.7488332 L22.993014,58.7488332 C22.993014,58.7488332 22.9928276,58.7490294 22.9928276,58.7492256" id="Fill-5备份" fill="url(#linearGradient-17)"></path>
+                                <path d="M26.1852608,58.7492256 C26.1824652,58.7664902 25.8182862,61.0144137 27.021717,62.7090867 L27.021717,62.7090867 C27.0791209,62.7901124 27.1987744,62.7820687 27.2470458,62.6945688 L27.2470458,62.6945688 C27.5165457,62.2037061 28.1938367,60.7262133 27.8695422,58.7502065 L27.8695422,58.7502065 C27.8693558,58.749618 27.8689831,58.7492256 27.8682376,58.7492256 L27.8682376,58.7492256 L26.1863791,58.7492256 C26.1858199,58.7492256 26.1856336,58.7488332 26.1854472,58.7488332 L26.1854472,58.7488332 C26.1854472,58.7488332 26.1852608,58.7490294 26.1852608,58.7492256" id="Fill-7备份" fill="url(#linearGradient-17)"></path>
+                                <g id="Fill-19" transform="translate(12.905573, 36.435086)">
+                                    <use fill="url(#radialGradient-18)" fill-rule="evenodd" xlink:href="#path-19"></use>
+                                    <use fill="black" fill-opacity="1" filter="url(#filter-20)" xlink:href="#path-19"></use>
+                                </g>
+                                <g id="翅膀备份" transform="translate(7.601926, 39.099982) scale(1, -1) rotate(10.000000) translate(-7.601926, -39.099982) translate(1.099020, 32.202047)">
+                                    <g id="编组备份-25" transform="translate(-0.000000, 2.122441)">
+                                        <g id="Fill-21">
+                                            <use fill="#5DD0EF" fill-rule="evenodd" xlink:href="#path-21"></use>
+                                            <use fill="black" fill-opacity="1" filter="url(#filter-22)" xlink:href="#path-21"></use>
+                                        </g>
+                                        <g id="Fill-23">
+                                            <use fill="#5DD0EF" fill-rule="evenodd" xlink:href="#path-23"></use>
+                                            <use fill="black" fill-opacity="1" filter="url(#filter-24)" xlink:href="#path-23"></use>
+                                        </g>
+                                        <g id="Fill-25">
+                                            <use fill="#5DD0EF" fill-rule="evenodd" xlink:href="#path-25"></use>
+                                            <use fill="black" fill-opacity="1" filter="url(#filter-26)" xlink:href="#path-25"></use>
+                                        </g>
+                                    </g>
+                                    <g id="Fill-27">
+                                        <use fill="url(#linearGradient-27)" fill-rule="evenodd" xlink:href="#path-28"></use>
+                                        <use fill="black" fill-opacity="1" filter="url(#filter-29)" xlink:href="#path-28"></use>
+                                    </g>
+                                </g>
+                                <g id="翅膀备份-2" transform="translate(44.226717, 42.003910) scale(-1, -1) rotate(19.000000) translate(-44.226717, -42.003910) translate(37.723811, 35.105976)">
+                                    <g id="编组备份-25" transform="translate(0.000000, 2.122441)">
+                                        <g id="Fill-21">
+                                            <use fill="#5DD0EF" fill-rule="evenodd" xlink:href="#path-30"></use>
+                                            <use fill="black" fill-opacity="1" filter="url(#filter-31)" xlink:href="#path-30"></use>
+                                        </g>
+                                        <g id="Fill-23">
+                                            <use fill="#5DD0EF" fill-rule="evenodd" xlink:href="#path-32"></use>
+                                            <use fill="black" fill-opacity="1" filter="url(#filter-33)" xlink:href="#path-32"></use>
+                                        </g>
+                                        <g id="Fill-25">
+                                            <use fill="#5DD0EF" fill-rule="evenodd" xlink:href="#path-34"></use>
+                                            <use fill="black" fill-opacity="1" filter="url(#filter-35)" xlink:href="#path-34"></use>
+                                        </g>
+                                    </g>
+                                    <g id="Fill-27">
+                                        <use fill="url(#linearGradient-27)" fill-rule="evenodd" xlink:href="#path-36"></use>
+                                        <use fill="black" fill-opacity="1" filter="url(#filter-37)" xlink:href="#path-36"></use>
+                                    </g>
+                                </g>
+                                <path d="M35.2526056,48.0435351 C35.2526056,52.9700885 30.9647884,56.9637335 25.6753059,56.9637335 C20.386017,56.9637335 16.0980062,52.9700885 16.0980062,48.0435351 C16.0980062,43.1169817 18.663261,38.6664612 25.6753059,38.6664612 C32.8910531,38.6664612 35.2526056,43.1169817 35.2526056,48.0435351" id="Fill-37备份-2" fill="#F4FBFF"></path>
+                                <g id="头" transform="translate(27.412877, 22.538376) rotate(3.000000) translate(-27.412877, -22.538376) translate(6.864623, 1.045958)">
+                                    <g id="Fill-41" transform="translate(0.000000, 6.271908)">
+                                        <use fill="url(#radialGradient-38)" fill-rule="evenodd" xlink:href="#path-39"></use>
+                                        <use fill="black" fill-opacity="1" filter="url(#filter-40)" xlink:href="#path-39"></use>
+                                    </g>
+                                    <path d="M29.8441512,15.8498146 C25.1085338,15.8498146 21.7542997,20.1528226 21.7542997,25.4606171 L18.5500474,25.4606171 C18.5500474,20.1528226 15.1959896,15.8498146 10.4603722,15.8498146 C5.72493105,15.8498146 2.0053477,19.8321857 2.0053477,25.3958796 C2.07725435,26.9197522 2.19040159,28.4666726 2.44859584,29.7787079 C2.98542839,34.9578748 6.10473215,42.6114705 20.322335,42.5881903 C38.477079,42.3157456 38.5244127,32.7379898 38.4722451,25.5946677 C38.4722451,20.2868732 34.5795923,15.8498146 29.8441512,15.8498146 Z" id="Fill-45备份-2" fill="#F4FBFF"></path>
+                                    <path d="M30.0879403,27.1999512 C33.5385568,27.1999512 33.9017632,29.7030865 33.9017632,28.1573998 C33.9017632,26.6117131 34.7929979,20.2499778 29.6722289,20.2499778 C24.5514599,20.2499778 25.2046736,26.1269804 25.2046736,27.6726671 C25.2046736,29.2183538 26.6373237,27.1999512 30.0879403,27.1999512 Z" id="椭圆形" fill="#D8D8D8" opacity="0.479851132" filter="url(#filter-41)"></path>
+                                    <path d="M11.0615868,28.2026691 C14.5122034,28.2026691 14.8754098,30.7058043 14.8754098,29.1601177 C14.8754098,27.614431 15.7666445,21.2526956 10.6458755,21.2526956 C5.52510643,21.2526956 6.17832014,27.1296982 6.17832014,28.6753849 C6.17832014,30.2210716 7.61097026,28.2026691 11.0615868,28.2026691 Z" id="椭圆形备份-7" fill="#D8D8D8" opacity="0.479851132" filter="url(#filter-42)"></path>
+                                    <g id="右眼备份-4" transform="translate(6.922885, 21.836006)">
+                                        <g id="Fill-86">
+                                            <use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-43"></use>
+                                            <use fill="black" fill-opacity="1" filter="url(#filter-44)" xlink:href="#path-43"></use>
+                                        </g>
+                                        <g id="椭圆形备份-17" transform="translate(0.838986, 0.817030)">
+                                            <mask id="mask-47" fill="white">
+                                                <use xlink:href="#path-46"></use>
+                                            </mask>
+                                            <use id="蒙版" fill="url(#linearGradient-45)" xlink:href="#path-46"></use>
+                                            <ellipse fill="url(#linearGradient-48)" mask="url(#mask-47)" cx="3.1973609" cy="1.13206466" rx="2.43392419" ry="2.27146791"></ellipse>
+                                        </g>
+                                        <path d="M1.91786205,3.79107995 C1.91786205,4.85342802 2.80206302,5.71448909 3.89296033,5.71448909 L3.89296033,5.71448909 C4.9836782,5.71448909 5.8680586,4.85342802 5.8680586,3.79107995 L5.8680586,3.79107995 C5.8680586,2.72890661 4.9836782,1.86784554 3.89296033,1.86784554 L3.89296033,1.86784554 C2.80206302,1.86784554 1.91786205,2.72890661 1.91786205,3.79107995" id="Fill-92" fill="url(#linearGradient-49)"></path>
+                                        <path d="M3.0856455,3.25962302 C3.0856455,3.67669947 2.73846025,4.01479874 2.31035483,4.01479874 C1.88206999,4.01479874 1.53488474,3.67669947 1.53488474,3.25962302 C1.53488474,2.84254656 1.88206999,2.5044473 2.31035483,2.5044473 C2.73846025,2.5044473 3.0856455,2.84254656 3.0856455,3.25962302" id="Fill-96" fill="#FFFFFF"></path>
+                                        <path d="M3.29761389,4.84226288 C3.29761389,5.07465152 3.10419493,5.2628339 2.86574056,5.2628339 C2.62710678,5.2628339 2.43368782,5.07465152 2.43368782,4.84226288 C2.43368782,4.60987424 2.62710678,4.42169186 2.86574056,4.42169186 C3.10419493,4.42169186 3.29761389,4.60987424 3.29761389,4.84226288" id="Fill-98" fill="#FFFFFF"></path>
+                                        <path d="M3.7541831,4.08561204 C3.7541831,4.21061078 3.65014564,4.31183156 3.52188423,4.31183156 C3.39352632,4.31183156 3.28948885,4.21061078 3.28948885,4.08561204 C3.28948885,3.96061331 3.39352632,3.85939253 3.52188423,3.85939253 C3.65014564,3.85939253 3.7541831,3.96061331 3.7541831,4.08561204" id="Fill-98备份" fill="#FFFFFF"></path>
+                                    </g>
+                                    <g id="嘴" transform="translate(15.312501, 22.322772)">
+                                        <path d="M2.08333286,6.61914012 C2.11835357,7.34009924 2.25975141,8.8704561 2.9673281,9.42588018 C3.67490479,9.98130427 4.13917831,9.51308622 4.61470037,9.5661119 C5.04880643,9.61451926 5.43795929,9.86439758 6.16663342,9.5661119 C6.89530756,9.26782622 6.44756304,8.86835534 6.79343767,7.81850214 C7.04496098,7.05538685 7.20345676,6.26999083 7.13185084,5.91539794 C7.04977482,5.50902765 6.49088763,4.97428993 5.77109774,4.63742036 C5.05130784,4.30055079 5.043365,3.60402452 4.51251845,3.59820115 C4.50999118,3.59820115 4.50746391,3.59820115 4.50493665,3.59820115 C3.98468055,3.59820115 4.29481239,4.19775556 3.31266805,4.7785739 C2.30994452,5.37167197 2.04807146,5.89805441 2.08333286,6.61914012 Z" id="Fill-85备份" fill="url(#linearGradient-50)"></path>
+                                        <path d="M5.58717265,4.63959943 C4.90321239,4.33352224 4.89571256,3.70083904 4.39137947,3.69548291 C3.88692343,3.69020363 4.18888396,4.23767988 3.25128178,4.76784268 C2.29855698,5.30653854 2.04983298,5.78488463 2.08315192,6.43995166 C2.1165938,7.09501868 2.62679259,8.96601789 3.25128178,9.30959508 C3.87577097,9.65317227 4.1572072,9.28455783 4.61470037,9.30959508 C5.00213428,9.3307982 5.59931644,9.59705311 6.00863076,9.30959508 C6.41794508,9.02213705 6.21814228,8.34777099 6.54678254,7.39404682 C6.78567069,6.70076652 6.94808512,6.12286805 6.88009483,5.80059041 C6.80214573,5.43144277 6.27113291,4.94567662 5.58717265,4.63959943 Z" id="Fill-87备份" fill="#B12D1F"></path>
+                                        <g id="编组">
+                                            <g transform="translate(2.080281, 0.000000)" fill="#FFFFFF">
+                                                <path d="M0.0642707036,3.7743965 C0.139637886,4.12202599 0.658724747,4.57848293 1.32793123,4.86539165 C1.99726067,5.15230037 2.00316218,5.74837797 2.49716763,5.75223293 C2.99117307,5.75604536 2.69658944,5.24084634 3.61624119,4.73912708 C4.55064672,4.22924576 4.79543638,3.77798286 4.76433051,3.16075811 C4.7331017,2.54353335 4.1776222,1.51078562 3.64378157,0.680481675 C3.10994093,-0.149698603 1.98730187,-0.200154965 1.3621108,0.419419472 C0.736919736,1.03899391 0.707166297,1.24477672 0.383075116,2.14433449 C0.147506565,2.79816484 -0.00162947581,3.47079266 0.0642707036,3.7743965" id="Fill-89"></path>
+                                            </g>
+                                            <g transform="translate(-0.000000, 1.028057)">
+                                                <path d="M4.03359235,0.0808171066 C3.68306718,0.20642293 3.40570573,0.47885678 3.15954286,0.786578709 C2.99315046,0.994605643 2.84095035,1.218796 2.68985137,1.424602 C2.61167142,1.5308364 2.54829549,1.61880983 2.49323918,1.69752939 C2.4643652,1.73861656 2.43647001,1.78032066 2.40881951,1.82227152 C2.29197778,2.00006916 2.18443446,2.18489973 2.07909339,2.37145769 C1.95833655,2.58503695 1.84039371,2.80083713 1.71400889,3.01046807 C1.54700475,3.28746716 0.995794553,3.75297657 0.791596822,4.01677347 C0.348272802,4.52419446 -0.468110335,6.37882534 0.348272802,7.38347422 C1.16465594,8.38812311 3.15954286,7.66134765 3.57482377,7.84382458 C3.99010467,8.02630151 4.30324591,8.03105848 4.4887245,8.03356734 C4.66050018,8.03587049 5.31547247,7.83918767 5.58781475,7.70128009 C5.86015704,7.56337251 7.328336,8.2829868 8.44816415,7.2992587 C9.56799231,6.31553061 7.88063319,3.78733798 7.70078258,3.59016892 C7.56938152,3.44593198 7.46824958,3.2920324 7.33917312,3.16235487 C7.22649121,3.04921092 7.12004901,2.94272976 7.02853319,2.84463877 C7.0071224,2.82156579 6.98632335,2.79898636 6.96650308,2.77665369 C6.88269515,2.64759309 6.7761306,2.47139946 6.65916653,2.27521747 C6.40859916,1.85496852 6.11068335,1.34218089 5.88666535,1.00089827 C5.63964604,0.624080802 5.33304358,0.283415105 4.94104266,0.103890082 C4.79434819,0.0367686989 4.64765371,0 4.50842242,0 C4.50181567,0 4.49520891,0 4.4887245,0.000246769791 C4.32502374,0.00431847135 4.17563762,0.0298591447 4.03359235,0.0808171066 Z" id="Fill-91" fill="url(#linearGradient-51)"></path>
+                                                <path d="M2.26229507,2.28787282 C2.09538045,2.63607836 2.06283764,2.97177787 2.18948063,3.03766772 L2.18948063,3.03766772 C2.31612362,3.10355756 2.55426627,2.87468009 2.72118143,2.52672309 L2.72118143,2.52672309 C2.88809605,2.17851755 2.9206394,1.84306658 2.79387646,1.7770522 L2.79387646,1.7770522 C2.77925918,1.76943996 2.76321438,1.76580115 2.7459806,1.76576321 L2.7459806,1.76576321 C2.61337623,1.76547384 2.40990924,1.98001204 2.26229507,2.28787282" id="Fill-143" fill="url(#linearGradient-52)" transform="translate(2.491726, 2.407368) rotate(15.000000) translate(-2.491726, -2.407368) "></path>
+                                                <path d="M5.70646975,1.77696534 C5.57960647,1.84296789 5.61231111,2.17844768 5.7792578,2.52670116 L5.7792578,2.52670116 C5.946205,2.87470609 6.18423834,3.10363247 6.3109817,3.03775444 L6.3109817,3.03775444 C6.43760539,2.97187665 6.40490125,2.63614831 6.23795456,2.28789483 L6.23795456,2.28789483 C6.09031203,1.97999166 5.88695336,1.76541024 5.75436012,1.76568195 L5.75436012,1.76568195 C5.73712778,1.76571756 5.7210848,1.76935449 5.70646975,1.77696534" id="Fill-145" fill="url(#linearGradient-53)" transform="translate(6.008631, 2.407368) rotate(-14.000000) translate(-6.008631, -2.407368) "></path>
+                                            </g>
+                                        </g>
+                                        <g id="下巴" transform="translate(4.423144, 12.946512) scale(1, -1) translate(-4.423144, -12.946512) ">
+                                            <use fill="black" fill-opacity="1" filter="url(#filter-55)" xlink:href="#path-54"></use>
+                                            <use fill="#FFF9EF" fill-rule="evenodd" xlink:href="#path-54"></use>
+                                        </g>
+                                    </g>
+                                    <g id="右眼备份-5" transform="translate(25.773443, 21.836006)">
+                                        <g id="Fill-86">
+                                            <use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-56"></use>
+                                            <use fill="black" fill-opacity="1" filter="url(#filter-57)" xlink:href="#path-56"></use>
+                                        </g>
+                                        <g id="椭圆形备份-17" transform="translate(0.838986, 0.817030)">
+                                            <mask id="mask-59" fill="white">
+                                                <use xlink:href="#path-58"></use>
+                                            </mask>
+                                            <use id="蒙版" fill="url(#linearGradient-45)" xlink:href="#path-58"></use>
+                                            <ellipse fill="url(#linearGradient-48)" mask="url(#mask-59)" cx="3.1973609" cy="1.13206466" rx="2.43392419" ry="2.27146791"></ellipse>
+                                        </g>
+                                        <path d="M1.91786205,3.79107995 C1.91786205,4.85342802 2.80206302,5.71448909 3.89296033,5.71448909 L3.89296033,5.71448909 C4.9836782,5.71448909 5.8680586,4.85342802 5.8680586,3.79107995 L5.8680586,3.79107995 C5.8680586,2.72890661 4.9836782,1.86784554 3.89296033,1.86784554 L3.89296033,1.86784554 C2.80206302,1.86784554 1.91786205,2.72890661 1.91786205,3.79107995" id="Fill-92" fill="url(#linearGradient-49)"></path>
+                                        <path d="M3.0856455,3.25962302 C3.0856455,3.67669947 2.73846025,4.01479874 2.31035483,4.01479874 C1.88206999,4.01479874 1.53488474,3.67669947 1.53488474,3.25962302 C1.53488474,2.84254656 1.88206999,2.5044473 2.31035483,2.5044473 C2.73846025,2.5044473 3.0856455,2.84254656 3.0856455,3.25962302" id="Fill-96" fill="#FFFFFF"></path>
+                                        <path d="M3.29761389,4.84226288 C3.29761389,5.07465152 3.10419493,5.2628339 2.86574056,5.2628339 C2.62710678,5.2628339 2.43368782,5.07465152 2.43368782,4.84226288 C2.43368782,4.60987424 2.62710678,4.42169186 2.86574056,4.42169186 C3.10419493,4.42169186 3.29761389,4.60987424 3.29761389,4.84226288" id="Fill-98" fill="#FFFFFF"></path>
+                                        <path d="M3.80217665,4.13233987 C3.80217665,4.25733861 3.69813918,4.35855939 3.56987778,4.35855939 C3.44151987,4.35855939 3.3374824,4.25733861 3.3374824,4.13233987 C3.3374824,4.00734114 3.44151987,3.90612036 3.56987778,3.90612036 C3.69813918,3.90612036 3.80217665,4.00734114 3.80217665,4.13233987" id="Fill-98备份-3" fill="#FFFFFF"></path>
+                                    </g>
+                                    <path d="M11.8867645,38.7689121 C15.2890404,38.7705674 16.7093273,32.037917 16.708618,30.5717506 C16.7079082,29.1055841 14.5805616,28.0246499 11.264897,28.0230358 C10.1871669,28.022512 8.26342153,28.3582734 7.45944697,28.6190462 C4.28611885,29.6483294 8.48448866,38.7672562 11.8867645,38.7689121 Z" id="椭圆形" fill="url(#linearGradient-60)" transform="translate(11.521320, 33.395974) rotate(5.000000) translate(-11.521320, -33.395974) "></path>
+                                    <path d="M30.6095675,38.809385 C34.0119068,38.8113668 35.4315773,32.0789781 35.4307281,30.6128388 C35.4298782,29.1466996 33.3023888,28.0655813 29.9866623,28.0636485 C28.9089121,28.0630215 26.985163,28.3985919 26.1811983,28.6592826 C23.0079094,29.688242 27.2072281,38.8074022 30.6095675,38.809385 Z" id="椭圆形" fill="url(#linearGradient-61)" transform="translate(30.243337, 33.436517) rotate(6.000000) translate(-30.243337, -33.436517) "></path>
+                                    <path d="M5.99378411,29.7183894 C5.95595362,30.1907053 6.98634006,30.6601333 8.29500997,30.7671078 C9.60367988,30.8740823 10.6951639,30.5779276 10.7328172,30.1056117 C10.7706477,29.6334754 9.74026128,29.1638679 8.43159137,29.0568934 C7.12309867,28.9499188 6.0316146,29.2460736 5.99378411,29.7183894" id="Fill-96" fill="#FF9191" opacity="0.427512759" transform="translate(8.363301, 29.912001) rotate(-1.000000) translate(-8.363301, -29.912001) "></path>
+                                    <path d="M33.8011134,29.7187988 C33.8392314,30.1911075 32.8090618,30.6598756 31.5003721,30.7660433 C30.1916823,30.8722111 29.0999492,30.5754081 29.0620084,30.1030995 C29.0238905,29.6309704 30.0540599,29.1620227 31.3625724,29.0558549 C32.6712622,28.9496871 33.7629954,29.2464901 33.8011134,29.7187988" id="Fill-93" fill="#FF9191" opacity="0.427512759" transform="translate(31.431561, 29.910949) rotate(3.000000) translate(-31.431561, -29.910949) "></path>
+                                    <path d="M17.9873426,19.840609 C17.9873426,20.5018682 17.4369724,21.0378473 16.7579584,21.0378473 C16.0789443,21.0378473 15.5285741,20.5018682 15.5285741,19.840609 C15.5285741,19.1793499 16.0789443,18.6433707 16.7579584,18.6433707 C17.4369724,18.6433707 17.9873426,19.1793499 17.9873426,19.840609" id="Fill-85备份-2" fill="#FFFFFF"></path>
+                                    <path d="M24.1342637,19.840609 C24.1342637,20.5018682 23.5838935,21.0378473 22.9048795,21.0378473 C22.2258655,21.0378473 21.6754953,20.5018682 21.6754953,19.840609 C21.6754953,19.1793499 22.2258655,18.6433707 22.9048795,18.6433707 C23.5838935,18.6433707 24.1342637,19.1793499 24.1342637,19.840609" id="Fill-87备份-2" fill="#FFFFFF"></path>
+                                    <g id="Fill-98备份-2" transform="translate(22.370887, 5.122693) rotate(-146.000000) translate(-22.370887, -5.122693) ">
+                                        <use fill="url(#linearGradient-62)" fill-rule="evenodd" xlink:href="#path-63"></use>
+                                        <use fill="black" fill-opacity="1" filter="url(#filter-64)" xlink:href="#path-63"></use>
+                                    </g>
+                                    <g id="编组备份-33" transform="translate(15.938369, 24.629562)">
+                                        <path d="M3.86434251,0.0130782029 C3.05634854,0.123283859 2.32816517,0.902047247 1.86572282,1.4736519 L1.86572282,1.4736519 C1.5676691,1.84175972 1.29221662,2.25067153 1.04978311,2.66586088 L1.04978311,2.66586088 C0.84019202,3.02490114 0.654967879,3.38830081 0.500820429,3.73443724 L0.500820429,3.73443724 C0.295467068,4.19583624 0.14485106,4.62584755 0.0652170394,4.97198399 L0.0652170394,4.97198399 C-0.0555582823,5.49528648 -0.0149466975,5.82590345 0.240553142,5.78143756 L0.240553142,5.78143756 C0.930773512,5.66059496 1.7082205,5.69390079 2.34900068,5.99766385 L2.34900068,5.99766385 C2.87271355,6.2461497 3.29560375,6.71905752 3.7548678,7.06711209 L3.7548678,7.06711209 C3.87440712,7.15761325 4.01831339,7.19545285 4.15833507,7.18010776 L4.15833507,7.18010776 C4.29606131,7.1652858 4.40200458,7.0976279 4.50812441,7.01514803 L4.50812441,7.01514803 C4.74614362,6.83013505 5.07827575,6.5649091 5.47821157,6.22679396 L5.47821157,6.22679396 C5.94824653,5.8299141 6.92875144,5.60165586 7.95534167,5.78143756 L7.95534167,5.78143756 C8.21084151,5.82590345 8.2514531,5.49528648 8.13067778,4.97198399 L8.13067778,4.97198399 C8.05086718,4.62584755 7.90042775,4.19583624 7.69489781,3.73443724 L7.69489781,3.73443724 C7.54075036,3.38830081 7.3557028,3.02490114 7.1461117,2.66586088 L7.1461117,2.66586088 C6.90350163,2.25067153 6.62840228,1.84175972 6.33017199,1.4736519 L6.33017199,1.4736519 C5.73794914,0.741621292 5.05585109,0.170888518 4.36315871,0.0320851911 L4.36315871,0.0320851911 C4.28670299,0.0167400997 4.20989412,0.00662628947 4.1334384,0.0022668885 L4.1334384,0.0022668885 C4.10712916,0.000697504154 4.08099649,0 4.05486381,0 L4.05486381,0 C3.99094471,0 3.92737875,0.004533777 3.86434251,0.0130782029" id="Fill-104"></path>
+                                    </g>
+                                    <g id="编组备份-34" transform="translate(20.855906, 25.427721)">
+                                        <mask id="mask-66" fill="white">
+                                            <use xlink:href="#path-65"></use>
+                                        </mask>
+                                        <g id="Clip-107"></g>
+                                        <path d="M0.14736819,0.0146874316 C-0.0263255418,0.0868231717 -0.0499231311,0.406786081 0.0949505936,0.729351826 L0.0949505936,0.729351826 C0.239824318,1.05191757 0.498430687,1.25493878 0.672124418,1.18261712 L0.672124418,1.18261712 C0.846011572,1.11048138 0.869415739,0.790518471 0.724735437,0.467952726 L0.724735437,0.467952726 C0.599397421,0.189449276 0.389727037,0 0.222803106,0 L0.222803106,0 C0.196497597,0 0.171159202,0.00483383825 0.14736819,0.0146874316" id="Fill-106" fill="url(#linearGradient-67)" mask="url(#mask-66)"></path>
+                                    </g>
+                                    <g id="编组备份-35" transform="translate(17.987343, 25.427721)">
+                                        <mask id="mask-69" fill="white">
+                                            <use xlink:href="#path-68"></use>
+                                        </mask>
+                                        <g id="Clip-109"></g>
+                                        <path d="M0.0949637336,0.467952726 C-0.0499300399,0.790518471 -0.0263291849,1.11048138 0.147388584,1.18261712 L0.147388584,1.18261712 C0.321299802,1.25493878 0.579555059,1.05191757 0.724642282,0.729351826 L0.724642282,0.729351826 C0.869536056,0.406786081 0.845935201,0.0868231717 0.672023982,0.0146874316 L0.672023982,0.0146874316 C0.648423128,0.00483383825 0.623081226,0 0.596772076,0 L0.596772076,0 C0.429825045,0 0.219932195,0.189449276 0.0949637336,0.467952726" id="Fill-108" fill="url(#linearGradient-70)" mask="url(#mask-69)"></path>
+                                    </g>
+                                </g>
+                            </g>
+                        </g>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 51 - 0
src/view/abnormal-pop/icon_btn.svg

@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="117px" height="36px" viewBox="0 0 117 36" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>button-normal备份</title>
+    <defs>
+        <linearGradient x1="47.3839597%" y1="0%" x2="47.3839597%" y2="100%" id="linearGradient-1">
+            <stop stop-color="#5BECFF" offset="0%"></stop>
+            <stop stop-color="#259CFE" offset="100%"></stop>
+        </linearGradient>
+        <path d="M0,17 C0,7.61115925 7.60904153,0 16.9971663,0 L100.002834,0 C109.390109,0 117,7.60909491 117,17 L117,17 C117,26.3888407 109.390958,34 100.002834,34 L16.9971663,34 C7.60989058,34 0,26.3909051 0,17 L0,17 Z" id="path-2"></path>
+        <filter x="-0.9%" y="-2.9%" width="101.7%" height="111.8%" filterUnits="objectBoundingBox" id="filter-3">
+            <feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+            <feColorMatrix values="0 0 0 0 0.140022123   0 0 0 0 0.580018696   0 0 0 0 0.741724871  0 0 0 1 0" type="matrix" in="shadowOffsetOuter1"></feColorMatrix>
+        </filter>
+        <filter x="-1.7%" y="-5.9%" width="103.4%" height="117.6%" filterUnits="objectBoundingBox" id="filter-4">
+            <feGaussianBlur stdDeviation="1.5" in="SourceAlpha" result="shadowBlurInner1"></feGaussianBlur>
+            <feOffset dx="0" dy="1" in="shadowBlurInner1" result="shadowOffsetInner1"></feOffset>
+            <feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
+            <feColorMatrix values="0 0 0 0 1   0 0 0 0 1   0 0 0 0 1  0 0 0 0.5 0" type="matrix" in="shadowInnerInner1"></feColorMatrix>
+        </filter>
+        <text id="text-5" font-family="STYuanti-SC-Bold, Yuanti SC" font-size="16" font-weight="bold" letter-spacing="0.914285714" fill="#FFFFFF">
+            <tspan x="24.6714286" y="22">重新连接</tspan>
+        </text>
+        <filter x="-2.7%" y="-4.5%" width="105.4%" height="118.2%" filterUnits="objectBoundingBox" id="filter-6">
+            <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+            <feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+            <feColorMatrix values="0 0 0 0 0.161975631   0 0 0 0 0.583690214   0 0 0 0 0.702658833  0 0 0 1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+        </filter>
+    </defs>
+    <g id="权限弹窗、" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="网络异常" transform="translate(-348.000000, -276.000000)">
+            <g id="编组-5备份" transform="translate(274.000000, 45.000000)">
+                <g id="button-normal备份" transform="translate(74.000000, 231.000000)">
+                    <g id="button-normal">
+                        <use fill="black" fill-opacity="1" filter="url(#filter-3)" xlink:href="#path-2"></use>
+                        <use fill="url(#linearGradient-1)" fill-rule="evenodd" xlink:href="#path-2"></use>
+                        <use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-2"></use>
+                    </g>
+                    <path d="M5.29281147,8.58018897 C7.4390966,13.9921969 10.1571046,16.6982008 13.4468356,16.6982008 C18.2676985,16.6982008 95.1625096,16.6982008 101.90185,16.6982008 C106.394743,16.6982008 109.960365,13.7988005 112.598716,8 C113.992892,13.5751446 114.565408,17.2438095 114.316264,19.0059945 C114.316264,19.0059945 114.316264,19.0059945 114.316264,19.0059945 C113.296432,26.2192179 107.122625,31.58155 99.8376644,31.5815344 L11.3446737,31.5815344 C5.11489123,25.4571796 2,20.6627351 2,17.1982008 C2,13.7336665 3.09760382,10.8609959 5.29281147,8.58018897 Z" id="矩形" fill="#1CACF1" opacity="0.499798729"></path>
+                    <g id="编组-6" opacity="0.85" transform="translate(7.706999, 10.293001) rotate(-5.000000) translate(-7.706999, -10.293001) translate(1.477345, 3.038666)" fill="#FFFFFF">
+                        <ellipse id="椭圆形" opacity="0.882866269" transform="translate(7.522655, 4.961334) rotate(44.000000) translate(-7.522655, -4.961334) " cx="7.52265471" cy="4.96133431" rx="3" ry="4"></ellipse>
+                        <ellipse id="椭圆形备份-3" opacity="0.882866269" transform="translate(3.175326, 11.321004) rotate(44.000000) translate(-3.175326, -11.321004) " cx="3.17532553" cy="11.3210042" rx="2" ry="2.5"></ellipse>
+                    </g>
+                    <g id="重新连接" fill="#FFFFFF" fill-opacity="1">
+                        <use filter="url(#filter-6)" xlink:href="#text-5"></use>
+                        <use xlink:href="#text-5"></use>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 16 - 0
src/view/abnormal-pop/icon_close.svg

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="19px" height="19px" viewBox="0 0 19 19" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>切片</title>
+    <g id="权限弹窗、" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="网络异常" transform="translate(-509.000000, -55.000000)" fill="#CCCCCC">
+            <g id="编组-5备份" transform="translate(274.000000, 45.000000)">
+                <g id="通用/选中/橙色备份" transform="translate(231.064971, 6.064971)">
+                    <g id="编组" transform="translate(13.435029, 13.435029) rotate(-315.000000) translate(-13.435029, -13.435029) translate(3.935029, 3.935029)">
+                        <rect id="矩形" x="8.5" y="0" width="2" height="19" rx="1"></rect>
+                        <rect id="矩形" transform="translate(9.500000, 9.500000) rotate(-270.000000) translate(-9.500000, -9.500000) " x="8.5" y="-4.97379915e-14" width="2" height="19" rx="1"></rect>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 17 - 0
src/view/abnormal-pop/icon_success.svg

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>形状</title>
+    <defs>
+        <linearGradient x1="0%" y1="50%" x2="100%" y2="50%" id="linearGradient-1">
+            <stop stop-color="#44C9FF" offset="0%"></stop>
+            <stop stop-color="#259CFE" offset="100%"></stop>
+        </linearGradient>
+    </defs>
+    <g id="权限弹窗、" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="连接成功" transform="translate(-361.000000, -178.000000)" fill="url(#linearGradient-1)" fill-rule="nonzero">
+            <g id="编组-11" transform="translate(343.000000, 165.000000)">
+                <path d="M28,13 C22.47716,13 18,17.47716 18,23 C18,28.52288 22.47716,33 28,33 C33.52288,33 38,28.52288 38,23 C38,17.47716 33.52284,13 28,13 Z M33.73576,20.40612 L27.26824,26.87368 C27.26824,26.87368 27.26816,26.87376 27.26804,26.87388 C26.96988,27.17212 26.51524,27.21868 26.16816,27.01368 C26.10388,26.97568 26.04328,26.92904 25.98808,26.87388 C25.98804,26.8738 25.98796,26.87376 25.98796,26.87376 L22.26428,23.15008 C21.91084,22.79664 21.91084,22.22352 22.26428,21.87 C22.61772,21.51656 23.19084,21.51656 23.54428,21.87 L26.62812,24.95384 L32.4558,19.12616 C32.80928,18.77272 33.3824,18.77272 33.73584,19.12616 C34.0892,19.4796 34.0892,20.05268 33.73576,20.40612 Z" id="形状"></path>
+            </g>
+        </g>
+    </g>
+</svg>

+ 74 - 0
src/view/abnormal-pop/index.module.less

@@ -0,0 +1,74 @@
+.fraction {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    color: #fff;
+    background-color: #fff;
+    border-radius: 20px;
+    width: 264px;
+    position: relative;
+    .close {
+        position: absolute;
+        width: 19px;
+        height: 19px;
+        right: 10px;
+        top: 10px;
+    }
+    .bg {
+        width: 264px;
+    }
+    .content {
+        .title {
+            margin: 20px 0 10px;
+            font-size: 16px;
+            font-family: PingFangSC, PingFang SC;
+            font-weight: 500;
+            color: #1A1A1A;
+            text-align: center;
+        }
+        .desc {
+            font-size: 14px;
+            font-family: PingFangSC, PingFang SC;
+            font-weight: 400;
+            color: #808080;     
+            margin-bottom: 16px;     
+            text-align: center;  
+        }
+    }
+    .btn {
+        width: 117px;
+        height: 36px;
+        margin-bottom: 17px;
+    }
+}
+
+.loadColumn {
+    display: flex;
+    align-items: center;
+    background: #fff;
+    border-radius: 10px;
+    padding: 18px;
+    position: relative;
+    .close {
+        position: absolute;
+        width: 19px;
+        height: 19px;
+        right: -30px;
+        top: -22px;
+    }
+    .loadIcon {
+        width: 17px;
+        height: 17px;
+    }
+    .successIcon {
+        width: 20px;
+        height: 20px;
+    }
+    p {
+        font-size: 16px;
+        font-family: PingFangSC, PingFang SC;
+        font-weight: 500;
+        color: #1A1A1A;
+        margin-left: 10px;
+    }
+}

+ 48 - 0
src/view/abnormal-pop/index.tsx

@@ -0,0 +1,48 @@
+import { defineComponent } from "vue";
+import styles from "./index.module.less";
+import state from "/src/state";
+import { popImgs } from '/src/view/evaluating'
+import { evaluatingData } from "/src/view/evaluating";
+import { Vue3Lottie } from "vue3-lottie";
+import loading from "./loading.json";
+
+export default defineComponent({
+	name: "abnormal-pop",
+	props:{},
+	emits: ["close", "confirm"],
+	setup(props, { emit }) {
+		return () => (
+			<>
+				{
+					evaluatingData.socketErrorStatus === 0 && 
+					<div class={styles.fraction}>
+						<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={popImgs.icon_btn} class={styles.btn} onClick={() => emit("confirm", true)} />
+						</div>
+					</div>				
+				}
+				{
+					evaluatingData.socketErrorStatus === 1 && 
+					<div class={styles.loadColumn}>
+						<Vue3Lottie class={styles.loadIcon} animationData={loading} loop={true}></Vue3Lottie>
+						<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={popImgs.icon_success} />
+						<p>连接成功</p>
+					</div>
+				}
+			</>
+		);
+	},
+});

File diff ditekan karena terlalu besar
+ 0 - 0
src/view/abnormal-pop/loading.json


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

@@ -1,4 +1,33 @@
 .scoreMode{
     background: transparent;
     left: 10vw;
+}
+.tipBox {
+    position: fixed;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%,-50%);
+    z-index: 999;
+    background: #fff;
+    border-radius: 20px;
+    font-size: 12px;
+    .tipContent {
+        padding: 20px 20px 0;
+    }
+    .tipBtn {
+        border-radius: 20px;
+        background-color: var(--van-primary-color);
+        color: #fff;
+        width: 100px;
+        margin: 20px auto;
+        text-align: center;
+        line-height: 30px;
+        cursor: pointer;
+    }
+}
+.hiddenPop {
+    width: 1px;
+    height: 1px;
+    overflow: hidden;
+    opacity: 0;
 }

+ 200 - 18
src/view/evaluating/index.tsx

@@ -1,6 +1,6 @@
 import styles from "./index.module.less";
 import { Snackbar } from "@varlet/ui";
-import { closeToast, showLoadingToast, showToast } from "vant";
+import { closeToast, showLoadingToast, showToast, Popup } from "vant";
 import { defineComponent, onMounted, onUnmounted, reactive, ref, watch } from "vue";
 import { getLeveByScore, getLeveByScoreMeasure, IEvaluatings } from "./evaluatResult";
 import {
@@ -22,6 +22,12 @@ import {
 	api_endCapture,
 	api_getDeviceDelay,
 	hideComplexButton,
+	api_checkSocketStatus,
+	addAccompanyError,
+	removeAccompanyError,
+	addSocketStatus,
+	removeSocketStatus,
+	api_disconnectSocket,
 } from "/src/helpers/communication";
 import state, {
 	IPlayState,
@@ -35,9 +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,
@@ -64,14 +86,37 @@ export const evaluatingData = reactive({
 	isComplete: false,
 	/**  */
 	isDisabledPlayMusic: false,
+	/** socket异常状态弹窗 */
+	socketErrorPop: false,
+	/** 异常提示 */
+	errorContents: '',
+	/** socket异常状态弹窗的状态值 */
+	socketErrorStatus: 0,
+	/** 延迟检测,socket状态异常 */
+	delayCheckSocketError: false,
+	/** 异常状态,不生成评测记录,不调用保存接口 */
+	isErrorState: false,
+	/** accompanyError,错误类型 */
+	accompanyErrorType: '',	
 });
 
 /** 点击开始评测按钮 */
-export const handleStartEvaluat = () => {
+export const handleStartEvaluat = async () => {
 	if (state.modeType === "evaluating") {
 		handleCancelEvaluat();
 	} else {
-		handleStopPlay();
+		if (state.platform !== 'PC') {
+			// 评测前先检查APP端的websocket状态
+			const res = await api_checkSocketStatus();
+			if (res?.content?.status === "connected") {
+				handleStopPlay();
+			} else {
+				// socket未连接
+				// evaluatingData.socketErrorPop = true
+			}
+		} else {
+			handleStopPlay();
+		}
 	}
 	state.modeType = state.modeType === "evaluating" ? "practise" : "evaluating";
 	if (state.modeType !== "evaluating") {
@@ -80,6 +125,29 @@ export const handleStartEvaluat = () => {
 	}
 };
 
+/** 开始评测 & 延迟检测开始按钮 */
+export const startCheckDelay = async () => {
+	// 评测前先检查APP端的websocket状态
+	const res = await api_checkSocketStatus();
+	if (res?.content?.status === "connected") {
+		// 
+		return new Promise((resolve) => {
+			resolve({checked: true})
+		});
+	} else {
+		/** 
+		 * socket未连接,记录此时的时间,以便于和收到socket成功链接,进行对比,对比时间小于500ms时,则连接中的状态默认显示500ms持续时间
+		 * 
+		 * */ 
+		socketStartTime = +new Date()
+		evaluatingData.socketErrorPop = true
+		evaluatingData.socketErrorStatus = 1
+		return new Promise((resolve) => {
+			resolve({checked: false})
+		});
+	}
+}
+
 const check_currentTime = () => {
 	let preTime = 0;
 	// 选段评测模式
@@ -118,7 +186,7 @@ export const sendEvaluatingOffsetTime = async (currentTime: number) => {
 };
 
 /** 检测耳机 */
-const checkUseEarphone = async () => {
+export const checkUseEarphone = async () => {
 	const res = await getEarphone();
 	return res?.content?.checkIsWired || false;
 };
@@ -249,7 +317,7 @@ export const handleStartBegin = async (preTimes?: number) => {
 		return;
 	}
 	if (res?.content?.reson) {
-		showToast(res.content.reson);
+		showToast(res.content?.des);
 		evaluatingData.startBegin = false;
 		return;
 	}
@@ -259,7 +327,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";
@@ -269,6 +337,7 @@ export const handleStartBegin = async (preTimes?: number) => {
 		}
 		onPlay();
 	}
+	if (evaluatingData.isErrorState) return
 	//开始录音
 	await api_startRecording({
 		accompanimentState: state.setting.enableAccompaniment ? 1 : 0,
@@ -337,21 +406,21 @@ const recordStartTimePoint = async (res?: IPostMessage) => {
 export const handleEndEvaluat = (isComplete = false) => {
 	// 没有开始评测 , 不是评测模式 , 不评分
 	if (!evaluatingData.startBegin || state.modeType !== "evaluating") return;
-	evaluatingData.startBegin = false;
 	// 结束录音
-	api_stopRecording();
+	// api_stopRecording();
 	// 结束评测
-	setTimeout(() => {
-		endEvaluating({
-			musicScoreId: state.examSongId,
-		});
-	}, 500);
+	endEvaluating({
+		musicScoreId: state.examSongId,
+	});
 	showLoadingToast({
 		message: "评分中",
 		duration: 0,
 		overlay: true,
 		overlayClass: styles.scoreMode,
 	});
+	setTimeout(() => {
+		evaluatingData.startBegin = false;
+	}, 500);
 	evaluatingData.isComplete = isComplete;
 	// 如果开启了摄像头, 结束录制视频
 	if (state.setting.camera) {
@@ -388,6 +457,14 @@ export const handleCancelEvaluat = () => {
 	cancelEvaluating();
 	// 停止播放
 	handleStopPlay();
+	endEvaluating({
+		musicScoreId: state.examSongId,
+	});
+	// 如果开启了摄像头, 结束录制视频
+	if (state.setting.camera) {
+		console.log("结束录制视频");
+		api_endCapture();
+	}
 };
 
 /** 查看报告 */
@@ -405,7 +482,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;
@@ -430,6 +507,79 @@ const handleComplexButton = (res?: IPostMessage) => {
 	}
 };
 
+// 检测到APP发送的异常信息
+const handleAccompanyError = (res?: IPostMessage) => {
+	console.log('异常信息返回', res)
+	if (res?.content) {
+		const { type, reson } = res.content;
+		switch (type) {
+			case "enterBackground":
+				// App退到后台
+			case "playError":
+				// 播放异常
+			case "socketError":
+				// socket连接断开,评测中,则取消评测
+				// 延迟检测中
+				if (evaluatingData.soundEffectMode) {
+					evaluatingData.socketErrorStatus = 0
+					evaluatingData.delayCheckSocketError = true
+					evaluatingData.socketErrorPop = type === "socketError" ? true : false
+					evaluatingData.accompanyErrorType = type
+					// api_checkSocketStatus()
+					return
+				}
+				// 评测中
+				if (state.modeType === "evaluating" && evaluatingData.startBegin) {
+					handleCancelEvaluat();
+				}
+				if (tickData.show) {
+					tickData.tickEnd = true
+					tickData.show = false
+				}
+				evaluatingData.socketErrorStatus = 0
+				evaluatingData.socketErrorPop = type === "socketError" ? true : false
+				evaluatingData.isErrorState = true
+				evaluatingData.accompanyErrorType = type
+				resetPlaybackToStart();
+				break;	
+			case "recordError":
+				// 录音异常
+				break;											
+			default:
+				break;
+		}
+	}
+};
+
+// 监测socket状态,是否已经成功连接
+const handleSocketStatus = (res?: IPostMessage) => {
+	if (res?.content?.status === "connected") {
+		const currentTime = +new Date()
+		evaluatingData.delayCheckSocketError = false
+		const diffTime = currentTime - socketStartTime
+		if (diffTime < 1000) {
+			const remainingTime = 1000 - diffTime
+			console.log(remainingTime,99999)
+			setTimeout(() => {
+				evaluatingData.socketErrorStatus = 2
+			}, remainingTime);
+		}
+	}
+}
+
+// 评测出现异常,再试一次
+const hanldeConfirmPop = async () => {
+	api_checkSocketStatus();
+	evaluatingData.socketErrorStatus = 1
+	socketStartTime = +new Date()
+}
+
+// 关闭异常弹窗
+const hanldeClosePop = () => {
+	evaluatingData.socketErrorPop = false
+	evaluatingData.socketErrorStatus = 0
+}
+
 export default defineComponent({
 	name: "evaluating",
 	setup() {
@@ -471,10 +621,20 @@ export default defineComponent({
 
 		watch(pageVisibility, (value) => {
 			if (value == "hidden" && evaluatingData.startBegin) {
-				handleEndBegin();
+				// handleEndBegin();
 			}
 		});
-
+		watch(
+			() => evaluatingData.socketErrorStatus,
+			() => {
+				if (evaluatingData.socketErrorStatus === 2) {
+					setTimeout(() => {
+						evaluatingData.socketErrorPop = false
+						// evaluatingData.socketErrorStatus = 0
+					}, 1000);
+				}
+			}
+		);
 		onMounted(() => {
 			resetPlaybackToStart();
 			hanlde_record();
@@ -491,6 +651,8 @@ export default defineComponent({
 			sendResult(handleScoreResult);
 			hideComplexButton(handleComplexButton, true);
 			api_recordStartTime(recordStartTimePoint);
+			addAccompanyError(handleAccompanyError);
+			addSocketStatus(handleSocketStatus);
 			// 不是选段模式评测, 就清空已选段
 			if (!state.isSelectMeasureMode) {
 				clearSelection();
@@ -505,8 +667,28 @@ export default defineComponent({
 			hideComplexButton(() => {}, false);
 			api_remove_recordStartTime(recordStartTimePoint);
 			handle_reduction();
+			removeAccompanyError(handleAccompanyError);
+			removeSocketStatus(handleSocketStatus);
+			api_disconnectSocket();
 			console.log("卸载评测模块成功");
 		});
-		return () => <div></div>;
+		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}
+						onClose={hanldeClosePop} 
+					/>
+				</Popup>
+
+			</div>
+		);
 	},
 });

+ 130 - 9
src/view/fingering/fingering-config.ts

@@ -21,6 +21,8 @@ export type IFingering = {
   code?: string;
   /** 是否有替指 */
   hasTizhi?: boolean;
+  /** 乐器code匹配的id */
+  id?: number;
 };
 
 type ITypeContent = {
@@ -226,6 +228,110 @@ export const mappingVoicePart = (id: number | string, soruce: "GYM" | "COLEXIU"
   return 0;
 };
 
+/** 映射声部指法,单曲根据声部codeId,合奏曲根据分轨track  */
+export const matchVoicePart = (id: number | string, type: "SINGLE" | "CONCERT"): number => {
+  if (type === "SINGLE") {
+    const subject: { [_key: string | number]: any } = {
+      33: "pan-flute",
+      34: "ocarina",
+      35: "hulusi-flute",
+      37: "piccolo",
+      36: "melodica",
+      38: "baroque-recorder",
+      1: 2,
+      5: 5,
+      3: 4,
+      11: 12,
+      13: 14,
+      12: 13,
+      14: 15,
+      16: 17,
+    };
+    return subject[id];
+  } else {
+    let code = id;
+    const subject: { [_key: string | number]: any } = {
+      Piccolo: "piccolo",
+      Flute: 2,
+      "Flute 1": 2,
+      "Flute 2": 2,
+      Oboe: 1,
+      "Clarinet in Bb": 4,
+      "Clarinet in Bb 1": 4,
+      "Clarinet in Bb 2": 4,
+      "Alto Clarinet in Eb": 4,
+      "Bass Clarinet in Bb": 4,
+      Bassoon: 1,
+      "Alto Saxophone": 5,
+      "Tenor Saxophone": 5,
+      "Baritone Saxophone": 5,
+      "Trumpet in Bb 1": 12,
+      "Trumpet in Bb 2": 12,
+      "Horn in F": 13,
+      "Horn in F 1": 13,
+      "Horn in F 2": 13,
+      "Trombone 1": 14,
+      "Trombone 2": 14,
+      "Trombone 3": 14,
+      Euphonium: 15,
+      Tuba: 17,
+      Chimes: 1,
+      Bells: 1,
+      Xylophone: 1,
+      "Snare Drum": 1,
+      "Bass Drum": 1,
+      Triangle: 1,
+      "Suspended Cymbal": 1,
+      "Crash Cymbals": 1,
+      "Concert Toms": 1,
+      Timpani: 1,
+      flute: 2,
+      oboe: 4,
+      clarinet: 4,
+      trombone: 14,
+      tuba: 17,
+      trumpet: 12,
+      horn: 13,
+      altosaxophone: 6,
+      tenorsaxophone: 6,
+      saxophone: 6,
+      upbasshorn: 15,
+      melodica: 137,
+      hulusiFlute: 136,
+      panflute: 135,
+      recorder: 120,
+      ukulele: 130,
+      mouthorgan: 140,
+      piano: 150,
+      4: "piccolo",
+      3: "hulusi-flute",
+      1: "pan-flute",
+      2: "ocarina",
+      5: "melodica",
+      26: 12,
+      tenorrecorder: "piccolo",
+      woodwind: "hulusi-flute",
+      panpipes: "pan-flute",
+      ocarina: "ocarina",
+      nai: "melodica",
+	    BaroqueRecorder: 'baroque-recorder',
+    };
+    let _track;
+    if (typeof code === "string") {
+      for (let sKey in subject) {
+        if (sKey === code) {
+          _track = subject[sKey];
+          break;
+        }
+      }
+    } else {
+      _track = subject.code;
+    }
+    return _track;
+  }
+  return 0;
+}
+
 /** 声部的指法配置信息 */
 export const subjectFingering = (subjectId: number | string): IFingering => {
   switch (subjectId) {
@@ -235,6 +341,7 @@ export const subjectFingering = (subjectId: number | string): IFingering => {
         direction: "transverse",
         height: "1.6rem",
         hasTizhi: true,
+        id: 1,
       };
     case 4: // 单簧管
       return {
@@ -242,6 +349,7 @@ export const subjectFingering = (subjectId: number | string): IFingering => {
         direction: "vertical",
         width: "3rem",
         hasTizhi: true,
+        id: 3,
       };
     case 5: // 萨克斯
     case 6: // 中音萨克斯
@@ -250,6 +358,7 @@ export const subjectFingering = (subjectId: number | string): IFingering => {
         direction: "vertical",
         width: "4.34rem",
         hasTizhi: true,
+        id: 5,
       };
     case 12: // 小号
       return {
@@ -257,6 +366,7 @@ export const subjectFingering = (subjectId: number | string): IFingering => {
         direction: "transverse",
         height: "1.6rem",
         hasTizhi: false,
+        id: 11,
       };
     case 13: // 圆号
       return {
@@ -264,6 +374,7 @@ export const subjectFingering = (subjectId: number | string): IFingering => {
         direction: "vertical",
         width: "4.98rem",
         hasTizhi: false,
+        id: 12,
       };
     case 14: // 长号
       return {
@@ -271,6 +382,7 @@ export const subjectFingering = (subjectId: number | string): IFingering => {
         direction: "transverse",
         height: "1.6rem",
         hasTizhi: false,
+        id: 13,
       };
     case 15: // 上低音号
       return {
@@ -278,6 +390,7 @@ export const subjectFingering = (subjectId: number | string): IFingering => {
         direction: "vertical",
         width: "4.34rem",
         hasTizhi: false,
+        id: 14,
       };
     case 17: // 大号
       return {
@@ -285,6 +398,7 @@ export const subjectFingering = (subjectId: number | string): IFingering => {
         direction: "vertical",
         width: "4.34rem",
         hasTizhi: false,
+        id: 16,
       };
     case 120: // 短笛
       return {
@@ -293,6 +407,7 @@ export const subjectFingering = (subjectId: number | string): IFingering => {
         width: "3rem",
         orientation: 1,
         hasTizhi: true,
+        id: 2,
       };
     case "piccolo": // 德式竖笛
       return {
@@ -302,6 +417,7 @@ export const subjectFingering = (subjectId: number | string): IFingering => {
         orientation: 1,
         code: "竖笛",
         hasTizhi: true,
+        id: 37,
       };
     case "hulusi-flute": // 葫芦丝
       return {
@@ -311,6 +427,7 @@ export const subjectFingering = (subjectId: number | string): IFingering => {
         orientation: 1,
         code: "葫芦丝",
         hasTizhi: false,
+        id: 35,
       };
     case "pan-flute": // 排箫
       return {
@@ -321,6 +438,7 @@ export const subjectFingering = (subjectId: number | string): IFingering => {
         orientation: 0,
         code: "排箫",
         hasTizhi: false,
+        id: 33,
       };
     case "ocarina": // 陶笛
       return {
@@ -331,6 +449,7 @@ export const subjectFingering = (subjectId: number | string): IFingering => {
         orientation: 0,
         code: "陶笛",
         hasTizhi: false,
+        id: 34,
       };
     case "melodica": // 口风琴
       return {
@@ -340,16 +459,18 @@ 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 {};
   }

+ 35 - 10
src/view/follow-practice/index.tsx

@@ -1,9 +1,13 @@
 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";
 
 export const followData = reactive({
 	list: [] as any, // 频率列表
@@ -29,7 +33,7 @@ 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();
@@ -65,17 +69,38 @@ const onClear = () => {
 };
 
 /** 开始跟练 */
-export const handleFollowStart = () => {
-	onClear();
-	followData.start = true;
-	followData.index = 0;
-	followData.list = [];
-	resetPlaybackToStart();
-	openToggleRecord(true);
-	getNoteIndex();
+export const handleFollowStart = async () => {
+	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();		
+	}
 };
 /** 结束跟练 */
 export const handleFollowEnd = () => {
+	onClear();
 	followData.start = false;
 	openToggleRecord(false);
 	followData.index = 0;

+ 9 - 4
src/view/music-score/index.tsx

@@ -7,9 +7,10 @@ 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"
 
 export const musicRenderTypeKey = "musicRenderType";
-
+let osmd: any = null;
 const musicData = reactive({
 	showSelection: false, // 可以加载点击浮层
 	isRenderLoading: true,
@@ -24,11 +25,12 @@ export const resetMusicScore = () => {
 };
 
 /** 重新渲染曲谱 */
-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;
 };
@@ -58,7 +60,7 @@ export default defineComponent({
 		const getXML = async () => {
 			const res = await fetch(state.xmlUrl).then((response) => response.text());
 			const xml = formatXML(res);
-			musicData.score = onlyVisible(xml, state.partIndex);
+			musicData.score = state.isCombineRender ? xml : onlyVisible(xml, state.partIndex);
 			if (state.gradualTimes) {
 				state.gradual = getGradualLengthByXml(xml);
 			}
@@ -66,7 +68,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,
@@ -100,6 +103,8 @@ export default defineComponent({
 			osmd.render();
 			// console.log("🚀 ~ osmd:", osmd)
 			emit("rendered", osmd);
+			resetFormate();
+			resetGivenFormate();
 			musicData.showSelection = true;
 		};
 		/** 获取渲染容器的宽度 */

+ 22 - 17
src/view/plugins/move-music-score/index.tsx

@@ -147,16 +147,17 @@ export const filterMoveData = async () => {
 		}
 		extStyleConfigJson[moveData.partIndex] = list;
 		console.log("🚀 ~ extStyleConfigJson", extStyleConfigJson)
-		// const res = await request.post("/sysMusicScore/updateExtStyleConfigJson", {
-		// 	data: {
-		// 		sysMusicScoreId: examSongId,
-		// 		extStyleConfigJson: JSON.stringify(extStyleConfigJson),
-		// 	},
-		// });
-		// if (res && res.code == 200) {
-		// 	showToast("保存成功");
-		// }
-		// clearActiveModel();
+		const res = await request.post("/musicSheet/img", {
+			requestType: "json",
+			data: {
+				id: examSongId,
+				extStyleConfigJson: JSON.stringify(extStyleConfigJson),
+			}
+		});
+		if (res && res.code == 200) {
+			showToast("保存成功");
+		}
+		clearActiveModel();
 	}
 };
 
@@ -377,7 +378,7 @@ const handleUndo = () => {
 };
 
 /** 根据移动数据渲染 */
-const renderForMoveData = () => {
+export const renderForMoveData = () => {
 	if (state.extStyleConfigJson) {
 		try {
 			extStyleConfigJson = JSON.parse(state.extStyleConfigJson);
@@ -385,8 +386,12 @@ const renderForMoveData = () => {
 			extStyleConfigJson = {};
 		}
 	}
-
-	if (!extStyleConfigJson || !extStyleConfigJson?.[moveData.partIndex]) return;
+	if (!extStyleConfigJson || !extStyleConfigJson?.[moveData.partIndex]){
+		initSvgId();
+		return
+	} else {
+		initSvgId();
+	}
 	const list = extStyleConfigJson?.[moveData.partIndex];
 	if (list && Array.isArray(list)) {
 		console.log("🚀 ~ list", list);
@@ -413,10 +418,10 @@ export default defineComponent({
 		const isOpen = query.isMove === "1" ? true : false;
 		console.log("🚀 ~ isOpen:", isOpen);
 		onMounted(() => {
-			if (isOpen) {
-				initSvgId();
-			}
-			renderForMoveData();
+			// if (isOpen) {
+			// 	initSvgId();
+			// }
+			// renderForMoveData();
 			const toolBox = document.getElementById("toolBox");
 			toolBox && document.body.appendChild(toolBox);
 		});

+ 13 - 4
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传递
@@ -29,7 +31,7 @@ export default defineComponent({
     const selValues = ref([partIndex.value]);
     const myPicker = ref();
     onMounted(() => {
-			console.log(myPicker.value)
+			// console.log(myPicker.value,99999,selValues.value,props.partIndex)
 		});
     return () => (
       <div class={styles.container}>
@@ -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>

+ 22 - 4
src/view/plugins/toggleMusicSheet/index.tsx

@@ -4,7 +4,7 @@ import { Icon, Popup } from 'vant'
 import ChoosePartName from './choosePartName'
 import state, { togglePlay } from "/src/state";
 import qs from 'query-string'
-import { getInstrumentName } from "/src/constant/instruments";
+import { getInstrumentName, sortMusical } from "/src/constant/instruments";
 import { getQuery } from "/src/utils/queryString";
 
 export const toggleMusicSheet = reactive({
@@ -22,13 +22,31 @@ export default defineComponent({
     const partListNames = computed(() => {
       let partList = 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 {
           text: item + (instrumentName ? `(${instrumentName})` : ''),
           value: index,
+          sortId,
+          canselect
         }
-      }).filter(Boolean)
+      }).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
+        console.log(3333,idx)
+        return idx
+      } else {
+        return 0
+      }
     })
 
     const switchMusic = (index: number) => {
@@ -63,7 +81,7 @@ export default defineComponent({
     return () => (
       <Popup class={styles.popup} v-model:show={toggleMusicSheet.show}>
         <ChoosePartName
-          partIndex={state.partIndex}
+          partIndex={trackIdx.value || 0}
           partListNames={partListNames.value}
           onClose={(value) => {
             console.log("🚀 ~ value:", value)

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

@@ -102,7 +102,9 @@
     opacity: var(--corsor-opacity);
     transform: translate(4PX, -50%);
 }
-
+.eyeLine {
+    background-color: rgb(255, 159, 88);
+}
 .lineStaff {
     width: 14PX;
 }
@@ -137,19 +139,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 {

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

@@ -5,13 +5,14 @@ import { metronomeData } from "/src/helpers/metronome";
 import { evaluatingData } from "../evaluating";
 import { leveByScoreMeasureIcons } from "../evaluating/evaluatResult";
 import { Icon } from "vant";
-import MoveMusicScore from "../plugins/move-music-score";
+import MoveMusicScore, { moveData, renderForMoveData } from "../plugins/move-music-score";
 import { useRoute } from "vue-router";
 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;
@@ -202,6 +208,11 @@ export default defineComponent({
 		});
 		onMounted(() => {
 			calcNoteData();
+			// 初始化谱面可移动的元素位置
+			try {
+			moveData.partIndex = query['part-index'] as string || '0'
+			renderForMoveData()
+			} catch (error) {}
 		});
 		return () => (
 			<div
@@ -237,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)}
@@ -246,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,7 +304,7 @@ export default defineComponent({
 				})}
 
 				{/* 移动模块 */}
-				{/* {query.isMove == "1" && <MoveMusicScore />} */}
+				{query.isMove == "1" && <MoveMusicScore />}
 			</div>
 		);
 	},

+ 40 - 3
src/view/tick/index.tsx

@@ -1,11 +1,13 @@
-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";
 
-const tickData = reactive({
+const browserInfo = browser();
+export const tickData = reactive({
 	list: [] as number[],
 	len: 0,
 	tickEnd: false,
@@ -18,7 +20,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) {
@@ -32,6 +34,25 @@ const handlePlay = (i: number, source: Howl | null) => {
 	});
 };
 
+// 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 +70,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,
 		});
@@ -61,6 +85,7 @@ export const handleStartTick = async () => {
 		// 提前结束, 直接放回false
 		if (tickData.tickEnd) return false;
 		const source = i === 0 ? tickData.source1 : i === tickData.len ? null : tickData.source2;
+		// const source = i === 0 ? audioData.tick : i === tickData.len ? null : audioData.tock;
 		await handlePlay(i, source)
 	}
 	tickData.show = false;
@@ -74,6 +99,18 @@ export default defineComponent({
 		const handleClose = () => {
 			tickData.tickEnd = true
 		};
+		onMounted(() => {
+			Promise.all([createAudio(tockAndTick.tick), createAudio(tockAndTick.tock)]).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}>

+ 32 - 0
src/view/transfer-to-img/index.module.less

@@ -0,0 +1,32 @@
+.skeleton {
+    position: fixed;
+    left: 0;
+    top: 0;
+    width: 100vw;
+    height: 100vh;
+    padding: 20px 30px;
+    background-color: #fff;
+    z-index: 1000;
+    --van-skeleton-paragraph-height: .8rem;
+}
+
+.detail {
+    width: 100vw;
+    height: 100vh;
+    overflow: hidden;
+    overflow-y: auto;
+    --header-height: 62px;
+    background: var(--container-background);
+
+    .container {
+        margin: 0 10px;
+        border-radius: 10px;
+    }
+    :global{
+        #musicAndSelection{
+            overflow: initial !important;
+            height: initial !important;
+            max-height: initial !important;
+        }
+    }
+}

+ 123 - 0
src/view/transfer-to-img/index.tsx

@@ -0,0 +1,123 @@
+import { defineComponent, nextTick, onMounted, reactive } from "vue";
+import state, { EnumMusicRenderType } from "/src/state";
+import MusicScore from "../music-score";
+import styles from "./index.module.less";
+import { getQuery } from "/src/utils/queryString";
+import { closeToast, showLoadingToast } from "vant";
+import { svg2canvas } from "/src/utils/svg2canvas";
+
+export default defineComponent({
+	name: "transfer-to-img",
+	setup() {
+		const query: any = getQuery();
+		const productRenderType = "productRenderType"
+		const detailData = reactive({
+			isLoading: true,
+			isProductLoading: false,
+			step: 0,
+			product: [
+				{
+					state: false,
+					name: "五线谱",
+					type: EnumMusicRenderType.staff,
+					base64: "" as any,
+				},
+				{
+					state: false,
+					name: "首调",
+					type: EnumMusicRenderType.firstTone,
+					base64: "" as any,
+				},
+				{
+					state: false,
+					name: "固定调",
+					type: EnumMusicRenderType.fixedTone,
+					base64: "" as any,
+				},
+			],
+		});
+
+		onMounted(() => {
+			(window as any).appName = "colexiu";
+			state.xmlUrl = decodeURIComponent(query.xmlUrl);
+			//课堂乐器,默认简谱
+			sessionStorage.setItem(productRenderType, detailData.product[detailData.step].type);
+
+			showLoadingToast({ message: "生成中", duration: 0 });
+			setTimeout(() => {
+				detailData.isLoading = false;
+			}, 500);
+		});
+
+		/** 渲染完成 */
+		const handleRendered = async () => {
+			
+			detailData.product[detailData.step].state = true;
+			detailData.product[detailData.step].base64 = await downPng();
+
+			try {
+				console.log(JSON.parse(JSON.stringify(detailData.product)), detailData.step);
+			} catch (error) {}
+
+			detailData.step += 1;
+
+			if (detailData.step !== detailData.product.length) {
+				//课堂乐器,默认简谱
+				sessionStorage.setItem(productRenderType, detailData.product[detailData.step].type);
+				nextTick(() => {
+					detailData.isLoading = true;
+					setTimeout(() => {
+						detailData.isLoading = false;
+					}, 500);
+				});
+				return;
+			}
+
+			closeToast();
+			console.log(detailData.product,123456);
+			window.parent?.postMessage(
+				{
+					api: "webApi_renderSvg",
+					product: JSON.stringify(detailData.product),
+				},
+				"*"
+			);
+		};
+		const downPng = () => {
+			return new Promise((resolve) => {
+				setTimeout(async () => {
+					try {
+						const svg: any = document.getElementById("osmdSvgPage1")?.cloneNode(true);
+						if (!svg) {
+							resolve("");
+							return;
+						}
+						const cw = svg.width.animVal.value;
+						const ch = svg.height.animVal.value;
+						const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+						rect.setAttribute("x", "0");
+						rect.setAttribute("y", "0");
+						rect.setAttribute("width", `${cw * 2}`);
+						rect.setAttribute("height", `${ch * 2}`);
+						rect.setAttribute("fill", "#fff");
+						svg.prepend(rect);
+						const _canvas = svg2canvas(svg.outerHTML);
+						const base64 = _canvas.toDataURL("image/png", 1);
+						resolve(base64);
+					} catch (error) {
+						resolve("");
+					}
+				}, 500);
+			});
+		};
+
+		return () => (
+			<div class={styles.detail}>
+				<div id="scrollContainer" class={[styles.container, "hideCursor"]}>
+					{/* 曲谱渲染 */}
+					{!detailData.isLoading && <MusicScore renderTypeKey={productRenderType} showSelection={false} onRendered={handleRendered} />}
+				</div>
+			</div>
+		);
+	},
+});

+ 5 - 1
vite.config.ts

@@ -65,7 +65,11 @@ export default defineConfig({
 			},
 			"^/instrument/.*": {
 				// target: "https://kt.colexiu.com",
-				target: "https://test.lexiaoya.cn",
+				// target: "https://test.lexiaoya.cn",
+				// target: "https://dev.kt.colexiu.com",
+				// target: "https://dev.resource.colexiu.com", // 内容平台开发环境
+				// target: "https://test.resource.colexiu.com",
+				target: "https://test.kt.colexiu.com",
 				changeOrigin: true,
 				rewrite: (path) => path.replace(/^\/instrument/, ""),
 			},

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini