Browse Source

Merge branch 'feature-1.8.7' into ktyq-online-1.8.7

TIANYONG 1 năm trước cách đây
mục cha
commit
4f1115b169
60 tập tin đã thay đổi với 1697 bổ sung181 xóa
  1. 1 1
      .gitignore
  2. 5 0
      public/flexible.js
  3. 5 0
      src/helpers/communication.ts
  4. 48 3
      src/helpers/formateMusic.ts
  5. 1 0
      src/page-colexiu/detail/index.tsx
  6. 6 5
      src/page-instrument/App.tsx
  7. 1 0
      src/page-instrument/component/mode-type-mode/index.tsx
  8. 3 0
      src/page-instrument/component/the-comfirm/index.module.less
  9. 3 1
      src/page-instrument/component/the-comfirm/index.tsx
  10. BIN
      src/page-instrument/custom-plugins/guide-page/images/pc_teacherBottom1.png
  11. BIN
      src/page-instrument/custom-plugins/guide-page/images/pc_teacherBottom2.png
  12. BIN
      src/page-instrument/custom-plugins/guide-page/images/pc_teacherBottom3.png
  13. BIN
      src/page-instrument/custom-plugins/guide-page/images/pc_teacherBottom4.png
  14. BIN
      src/page-instrument/custom-plugins/guide-page/images/pc_teacherBottom5.png
  15. BIN
      src/page-instrument/custom-plugins/guide-page/images/pc_teacherBottom6.png
  16. 57 15
      src/page-instrument/custom-plugins/guide-page/student-bottom.tsx
  17. 18 15
      src/page-instrument/custom-plugins/guide-page/student-top.tsx
  18. 52 9
      src/page-instrument/custom-plugins/guide-page/teacher-bootom.tsx
  19. 140 15
      src/page-instrument/custom-plugins/guide-page/teacher-top.tsx
  20. 24 0
      src/page-instrument/custom-plugins/helper-model/recommendation/index.module.less
  21. 3 1
      src/page-instrument/custom-plugins/helper-model/recommendation/index.tsx
  22. 8 0
      src/page-instrument/follow-model/index.module.less
  23. 4 3
      src/page-instrument/follow-model/index.tsx
  24. BIN
      src/page-instrument/header-top/image/pc_end_icon.png
  25. BIN
      src/page-instrument/header-top/image/pc_icon_pausebtn.png
  26. BIN
      src/page-instrument/header-top/image/pc_icon_playbtn.png
  27. BIN
      src/page-instrument/header-top/image/pc_icon_resetbtn.png
  28. 35 5
      src/page-instrument/header-top/index.module.less
  29. 98 32
      src/page-instrument/header-top/index.tsx
  30. 34 3
      src/page-instrument/header-top/music-type/index.tsx
  31. 19 1
      src/page-instrument/header-top/settting/index.module.less
  32. 34 4
      src/page-instrument/header-top/settting/index.tsx
  33. 3 0
      src/page-instrument/header-top/title/index.module.less
  34. 2 1
      src/page-instrument/header-top/title/index.tsx
  35. 95 10
      src/page-instrument/view-detail/index.module.less
  36. 86 23
      src/page-instrument/view-detail/index.tsx
  37. 8 1
      src/page-instrument/view-figner/index.module.less
  38. 2 2
      src/page-instrument/view-figner/index.tsx
  39. 248 12
      src/state.ts
  40. 58 0
      src/style.css
  41. 1 1
      src/view/evaluating/index.tsx
  42. 22 4
      src/view/music-score/index.module.less
  43. 15 4
      src/view/music-score/index.tsx
  44. 155 0
      src/view/music-score/testCheck.tsx
  45. 44 0
      src/view/plugins/toggleMusicSheet/choosePartName/index.module.less
  46. 5 3
      src/view/plugins/toggleMusicSheet/choosePartName/index.tsx
  47. 3 0
      src/view/plugins/toggleMusicSheet/index.module.less
  48. 32 4
      src/view/plugins/toggleMusicSheet/index.tsx
  49. 59 0
      src/view/plugins/useDrag/dragbom.tsx
  50. BIN
      src/view/plugins/useDrag/img/left.png
  51. BIN
      src/view/plugins/useDrag/img/modalDragBg.png
  52. BIN
      src/view/plugins/useDrag/img/modalDragBg2.png
  53. BIN
      src/view/plugins/useDrag/img/modalDragBgLeft.png
  54. BIN
      src/view/plugins/useDrag/img/modalDragBgRight.png
  55. BIN
      src/view/plugins/useDrag/img/modalDragDone.png
  56. BIN
      src/view/plugins/useDrag/img/right.png
  57. 85 0
      src/view/plugins/useDrag/index.module.less
  58. 163 0
      src/view/plugins/useDrag/index.ts
  59. 6 0
      src/view/selection/index.module.less
  60. 6 3
      src/view/selection/index.tsx

+ 1 - 1
.gitignore

@@ -21,4 +21,4 @@ dist-ssr
 *.ntvs*
 *.njsproj
 *.sln
-*.sw?
+*.sw?

+ 5 - 0
public/flexible.js

@@ -6,6 +6,11 @@
     b / i > 640 && (b = 640 * i);
     b / i < 375 && (b = 375 * i);
     var c = b / 10;
+    // 老师端,fontsize计算
+    // if (window.location.search.includes('platform=pc') || window.location.search.includes('platform=PC')) {
+    //   c = width / 21.7;
+    // }
+    // console.log('fontSize:',c,'屏幕宽度:',width,'老师端:',window.location.search.includes('platform=pc'))
     f.style.fontSize = c + "px", k.rem = a.rem = c
     window.fontSize = c
   }

+ 5 - 0
src/helpers/communication.ts

@@ -473,4 +473,9 @@ export const api_midiMicDelay = (content: any) => {
 		api: "proxyServiceMessage",
 		content,
 	});
+};
+
+/** 监听老师端,上课页面,功能按钮方向 */
+export const addImagePos = (callback: any) => {
+	listenerMessage("imagePos", callback);
 };

+ 48 - 3
src/helpers/formateMusic.ts

@@ -613,7 +613,7 @@ export const formatZoom = (num = 1) => {
 /** 格式化曲谱
  * 1.全休止符的小节,没有音符默认加个全休止符
  */
-export const formatXML = (xml: string): string => {
+export const formatXML = (xml: string, xmlUrl?: string): string => {
 	if (!xml) return "";
 	const xmlParse = new DOMParser().parseFromString(xml, "text/xml");
 	const measures = Array.from(xmlParse.getElementsByTagName("measure"));
@@ -621,6 +621,10 @@ export const formatXML = (xml: string): string => {
 	compatibleXmlPitchVoice(xmlParse);
 	// 处理重复小节信息
 	parseXmlToRepeat(repeats)
+	// 解析处理evxml
+	if (state.isEvxml) {
+		analyzeEvxml(xmlParse, xmlUrl);
+	}
 	// const words: any = xmlParse.getElementsByTagName("words");
 	// for (const word of words) {
 	// 	if (word && word.textContent?.trim() === "筒音作5") {
@@ -644,6 +648,18 @@ export const formatXML = (xml: string): string => {
 		  speed = Number(measure.getElementsByTagName('per-minute')[0]?.textContent)
 		}
 		const divisions = parseInt(measure.getElementsByTagName("divisions")[0]?.textContent || "256");
+		// 如果note节点里面有space节点,并且没有duration节点,代表这是一个空白节点,需要删除
+		if (measure.getElementsByTagName("note").length && state.isEvxml) {
+			const noteList = Array.from(measure.getElementsByTagName("note")) || [];
+			noteList.forEach((note: any) => {
+				// if (note.getElementsByTagName("space").length && !note.getElementsByTagName("duration").length) {
+				// 	measure.removeChild(note);
+				// }
+				if (!note.getElementsByTagName("duration").length || (note.getElementsByTagName("duration").length && note.getElementsByTagName("duration")[0]?.textContent == 0)) {
+					measure.removeChild(note);
+				}
+			});
+		}
 		if (measure.getElementsByTagName("note").length === 0) {
 			const forwardTimeElement = measure.getElementsByTagName("forward")[0]?.getElementsByTagName("duration")[0];
 			if (forwardTimeElement) {
@@ -661,6 +677,9 @@ export const formatXML = (xml: string): string => {
 		}
 	}
 	// 如果曲谱详情接口没有返回速度,则取xml第一小节的速度,如果取不到,则取默认速度:100
+	if (!speed || speed == -1) {
+		speed = 100
+	}
 	if (!state.originSpeed) {
 		state.originSpeed = state.speed = speed || 100
 	}
@@ -822,6 +841,7 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 	// 是否是变速的曲子
 	const hasVaryingSpeed = _notes.some((item: any) => item.measuresTempoInBPM !== _notes[0].measuresTempoInBPM)
 	console.log('变速曲子',hasVaryingSpeed)
+	// let voicesBBox: any = null;
 	for (let { note, iterator, currentTime, isDouble, isMutileSubject } of _notes) {
 		if (note) {
 			if (preMeasureNumber != note?.sourceMeasure?.MeasureNumberXML) {
@@ -1028,6 +1048,7 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 				} else {
 					if (difftime > 0) {
 						fixtime += difftime;
+						state.fixtime = fixtime;
 					}
 				}
 				// 管乐迷 diff获取不准确时, 弱起补齐
@@ -1035,6 +1056,13 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 					// difftime = iterator.currentTimeStamp.realValue * formatBeatUnit(beatUnit) * (60 / beatSpeed);
 					// fixtime += difftime;
 				}
+
+				// 如果是evxml,fixtime取读取xml的值
+				if (state.isEvxml) {
+					fixtime = state.evXmlBeginTime ? state.evXmlBeginTime : fixtime
+					state.fixtime = fixtime
+				}
+				console.log('节拍器时间',fixtime,state.evXmlBeginTime)
 			}
 			let stave = activeVerticalMeasureList[0]?.stave;
 
@@ -1052,6 +1080,7 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 			// console.log(note.tie)
 			// console.log('👀看看endtime', duration, relaEndtime, fixtime, i)
 			// console.log('频率',note?.pitch?.frequency,i)
+
 			const nodeDetail = {
 				isStaccato: note.voiceEntry.isStaccato(),
 				isRestFlag: note.isRestFlag,
@@ -1094,7 +1123,8 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 				noteLength: 1,
 				osdmContext: osmd,
 				speedbeatUnit: beatUnit,
-				multipleRestMeasures: multipleRestMeasures,
+				multipleRestMeasures: multipleRestMeasures, // 当前合并小节的索引,从1开始到当前的totalMultipleRestMeasures结束,
+				totalMultipleRestMeasures, // 当前小节总的合并小节数
 				measureSpeed,  // 小节速度
 				maxNoteNum: note.maxNoteNum, // 当前小节音符最多的分轨的音符数量
 			};
@@ -1184,7 +1214,7 @@ const parseXmlToRepeat = (repeats: any) => {
 		}
 	}
 	state.repeatInfo = repeatInfo
-	console.log('重播',repeatInfo)
+	// console.log('重播',repeatInfo)
 }
 
 // 校验当前选段是否满足重播条件
@@ -1214,6 +1244,21 @@ export const verifyCanRepeat = (startNum: number, endNum: number) => {
 	}
 }
 
+// 计算evxml的起始播放时间
+const analyzeEvxml = (xmlParse: any, xmlUrl?: string) => {
+	// xml拍号数
+	const xmlNum = xmlParse.getElementsByTagName("timegap")[0]?.getElementsByTagName("values")[0]?.getElementsByTagName("item")[0]?.getAttribute('num');
+	// 第一个音符的起始时间
+	const firstNoteBeginTime = xmlParse.getElementsByTagName("times")[0]?.getElementsByTagName("time")[0]?.getAttribute('begin');
+	state.evXmlBeginTime = firstNoteBeginTime ? firstNoteBeginTime / 1000 : xmlNum ? 60 / state.originSpeed * xmlNum : 0;
+	const hasTimeGap = xmlParse.getElementsByTagName("timegap").length > 0;
+	const hasTimes = xmlParse.getElementsByTagName("times").length > 0;
+	// if (!hasTimeGap && !hasTimes) {
+	// 	state.noTimes.push(xmlUrl)
+	// }
+	console.log('🚀 ~ evxml解析','有timegap:',hasTimeGap,'有times:',hasTimes)
+}
+
 /**
  * 兼容处理xml声部移调
  * 打谱软件可能会自动处理移调,这类型的xml就不用通过程序移调了

+ 1 - 0
src/page-colexiu/detail/index.tsx

@@ -98,6 +98,7 @@ export default defineComponent({
 			state.midiUrl = data.midiUrl;
 			state.parentCategoriesId = data.musicTag;
 			state.playMode = data.audioType === "MP3" ? "MP3" : "MIDI";
+			// state.originSpeed = state.speed = parseFloat(data.playSpeed) || 0;
 			state.originSpeed = state.speed = data.playSpeed;
 			state.track = data.track;
 			state.enableNotation = data.notation ? true : false;

+ 6 - 5
src/page-instrument/App.tsx

@@ -70,20 +70,21 @@ export default defineComponent({
       setBehaviorId(getRandomKey());
     });
 
+    // 老师端云教练,上下键切资源
     const onKeyBoard = (e: KeyboardEvent) => {
-      if (e.code === "ArrowLeft") {
+      if (e.code === "ArrowUp") {
         window.parent.postMessage(
           {
             api: "documentBodyKeyup",
-            code: "ArrowLeft",
+            code: "ArrowUp",
           },
           "*"
         );
-      } else if (e.code === "ArrowRight") {
+      } else if (e.code === "ArrowDown") {
         window.parent.postMessage(
           {
             api: "documentBodyKeyup",
-            code: "ArrowRight",
+            code: "ArrowDown",
           },
           "*"
         );
@@ -97,7 +98,7 @@ export default defineComponent({
 
       // 禁用右键菜单
       document.addEventListener("contextmenu", function (event) {
-        event.preventDefault();
+        // event.preventDefault();
       });
       // 禁用浏览器快捷键
       document.addEventListener("keydown", function (event) {

+ 1 - 0
src/page-instrument/component/mode-type-mode/index.tsx

@@ -11,6 +11,7 @@ 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",
 

+ 3 - 0
src/page-instrument/component/the-comfirm/index.module.less

@@ -6,6 +6,9 @@
     background-color: #fff;
     border-radius: 18px;
     min-width: 244px;
+    &.pcFraction {
+        border-radius: 16PX;
+    }
 }
 
 .title {

+ 3 - 1
src/page-instrument/component/the-comfirm/index.tsx

@@ -3,6 +3,7 @@ import styles from "./index.module.less";
 import icon_title from '../../evaluat-model/evaluat-audio/icon_title.svg'
 import icon_cancel from '../../evaluat-model/evaluat-audio/icon_cancel.svg'
 import icon_confirm from '../../evaluat-model/evaluat-audio/icon_confirm.svg'
+import state, { IPlatform } from "/src/state";
 
 export default defineComponent({
 	name: "evaluat-audio",
@@ -15,7 +16,8 @@ export default defineComponent({
 	emits: ["close"],
 	setup(props, { emit }) {
 		return () => (
-			<div class={styles.fraction}>
+			<div class={[styles.fraction, state.platform === IPlatform.PC && styles.pcFraction]}>
+				{ state.platform === IPlatform.PC && <div class={'top_drag'}></div> }
 				<div class={styles.title}>
 					<img src={icon_title} />
 				</div>

BIN
src/page-instrument/custom-plugins/guide-page/images/pc_teacherBottom1.png


BIN
src/page-instrument/custom-plugins/guide-page/images/pc_teacherBottom2.png


BIN
src/page-instrument/custom-plugins/guide-page/images/pc_teacherBottom3.png


BIN
src/page-instrument/custom-plugins/guide-page/images/pc_teacherBottom4.png


BIN
src/page-instrument/custom-plugins/guide-page/images/pc_teacherBottom5.png


BIN
src/page-instrument/custom-plugins/guide-page/images/pc_teacherBottom6.png


+ 57 - 15
src/page-instrument/custom-plugins/guide-page/student-bottom.tsx

@@ -5,6 +5,7 @@ import styles from "./index.module.less";
 import { getImage } from "./images";
 import { getQuery } from "/src/utils/queryString";
 import { getGuidance, setGuidance } from "./api";
+import state, { IPlatform } from "/src/state";
 
 export default defineComponent({
 	name: "studentB-guide",
@@ -13,7 +14,46 @@ export default defineComponent({
 		const data = reactive({
 			box: {},
 			show: false,
-			steps: [
+			steps: state.platform === IPlatform.PC ? [
+				{
+					ele: "",
+					eleRect: {} as DOMRect,
+					img: getImage("studentB1.png"),
+					handStyle: {
+						top: "-1.39rem",
+						left: "1.7rem",
+						transform: "rotate(180deg)",
+					},
+					imgStyle: {
+						top: "-5.01rem",
+						width: "6.48rem",
+						height: "3.01rem",
+						left: "1.2rem",
+					},
+					btnsStyle: {
+						top: "-1.61rem",
+						left: "3.2rem",
+					},
+				},
+				{
+					ele: "",
+					img: getImage("studentB2.png"),
+					handStyle: {
+						top: "-1.39rem",
+						left: "1.5rem",
+						transform: "rotate(180deg)",
+					},
+					imgStyle: {
+						top: "-5.01rem",
+						width: "6.48rem",
+						height: "3.01rem",
+					},
+					btnsStyle: {
+						top: "-1.61rem",
+						left: "2.3rem",
+					},
+				},
+			] : [
 				{
 					ele: "",
 					eleRect: {} as DOMRect,
@@ -85,21 +125,23 @@ export default defineComponent({
 		const guideInfo = ref({} as any)
 		const getAllGuidance = async()=>{
 		  try{
-		  const res = await getGuidance({guideTag:'guideInfo'})
-		  if(res.data){
-			guideInfo.value = JSON.parse(res.data?.guideValue) || null
-		  }else{
-			guideInfo.value = {}
-		  }
-	  
-	  
-		  if (guideInfo.value && guideInfo.value.studentB) {
-			tipShow.value = false;
-		  } else {
-			tipShow.value = true;
-		  }
+			if (state.guideInfo) {
+				guideInfo.value = state.guideInfo
+			} else {
+				const res = await getGuidance({guideTag:'guideInfo'})
+				if(res.data){
+				  guideInfo.value = JSON.parse(res.data?.guideValue) || null
+				}else{
+				  guideInfo.value = {}
+				}
+			}
+			if (guideInfo.value && guideInfo.value.studentB) {
+				tipShow.value = false;
+			} else {
+				tipShow.value = true;
+			}
 		  }catch(e){
-		  console.log(e)
+		  	console.log(e)
 		  }
 		  // const guideInfo = localStorage.getItem('teacher-guideInfo');
 	  

+ 18 - 15
src/page-instrument/custom-plugins/guide-page/student-top.tsx

@@ -7,6 +7,7 @@ import { useRoute } from "vue-router";
 import { getQuery } from "/src/utils/queryString";
 import { getGuidance, setGuidance } from "./api";
 import { headTopData } from "/src/page-instrument/header-top/index";
+import state from "/src/state";
 
 export default defineComponent({
   name: "studnetT-guide",
@@ -181,21 +182,23 @@ export default defineComponent({
   const guideInfo = ref({} as any)
   const getAllGuidance = async()=>{
     try{
-    const res = await getGuidance({guideTag:'guideInfo'})
-    if(res.data){
-      guideInfo.value = JSON.parse(res.data?.guideValue) || null
-    }else{
-      guideInfo.value = {}
-    }
-
-
-    if (guideInfo.value && guideInfo.value.studnetT) {
-      tipShow.value = false;
-    } else {
-      tipShow.value = headTopData.modeType !== "init" ? true : false;
-    }
-    }catch(e){
-    console.log(e)
+			if (state.guideInfo) {
+				guideInfo.value = state.guideInfo
+			} else {
+				const res = await getGuidance({guideTag:'guideInfo'})
+				if(res.data){
+				  guideInfo.value = JSON.parse(res.data?.guideValue) || null
+				}else{
+				  guideInfo.value = {}
+				}
+			}
+      if (guideInfo.value && guideInfo.value.studnetT) {
+        tipShow.value = false;
+      } else {
+        tipShow.value = headTopData.modeType !== "init" ? true : false;
+      }
+    } catch(e) {
+      console.log(e)
     }
     // const guideInfo = localStorage.getItem('teacher-guideInfo');
 

+ 52 - 9
src/page-instrument/custom-plugins/guide-page/teacher-bootom.tsx

@@ -6,6 +6,7 @@ import { getImage } from "./images";
 import { getQuery } from "/src/utils/queryString";
 import {getGuidance,setGuidance} from './api'
 import { headTopData } from "/src/page-instrument/header-top/index";
+import state, { IPlatform } from "/src/state";
 
 export default defineComponent({
 	name: "aiTeacher-guide",
@@ -14,7 +15,46 @@ export default defineComponent({
 		const data = reactive({
 			box: {},
 			show: false,
-			steps: [
+			steps: state.platform === IPlatform.PC ? [
+				{
+					img: getImage("aiTeacher2.png"),
+					eleRect: {
+						top: "-3rem",
+					},
+					imgStyle: {
+						left: "-0.7rem",
+						width: "6.48rem",
+						height: "3.01rem",
+					},
+					btnsStyle: {
+						bottom: ".9rem",
+						left: ".9rem",
+						transform: "scale(.83)",
+					},
+					boxStyle: {
+						borderRadius: "40px",
+					},
+				},
+				{
+					img: getImage("aiTeacher3.png"),
+					eleRect: {
+						top: "-3rem",
+					},
+					imgStyle: {
+						left: "-0.7rem",
+						width: "6.48rem",
+						height: "3rem",
+					},
+					btnsStyle: {
+						bottom: ".9rem",
+						left: ".8rem",
+						transform: "scale(.83)",
+					},
+					boxStyle: {
+						borderRadius: "40px",
+					},
+				}
+			] : [
 				{
 					eleRect: {
 						left: "4.5rem",
@@ -110,14 +150,17 @@ export default defineComponent({
 		const guideInfo = ref({} as any)
 		const getAllGuidance = async()=>{
 		  try{
-			const res = await getGuidance({guideTag:'guideInfo'})
-			if(res.data){
-			  guideInfo.value = JSON.parse(res.data?.guideValue) || null
-			}else{
-			  guideInfo.value = {}
+			if (state.guideInfo) {
+				guideInfo.value = state.guideInfo
+			} else {
+				const res = await getGuidance({guideTag:'guideInfo'})
+				if(res.data){
+				  guideInfo.value = JSON.parse(res.data?.guideValue) || null
+				}else{
+				  guideInfo.value = {}
+				}
 			}
 	
-	
 			if (guideInfo.value && guideInfo.value.teacherBottom) {
 			  tipShow.value = false;
 			} else {
@@ -138,7 +181,7 @@ export default defineComponent({
 		// } else {
 		// 	tipShow.value = true;
 		// }
-		const steps = ["modeType-box", "modeType-0", "modeType-1", "modeType-2"];
+		const steps = state.platform === IPlatform.PC ? ["modeType-0", "modeType-1"] : ["modeType-box", "modeType-0", "modeType-1", "modeType-2"];
 		const getStepELe = () => {
 			console.log(steps[data.step]);
 			const ele: HTMLElement = document.getElementById(steps[data.step])!;
@@ -164,7 +207,7 @@ export default defineComponent({
 			getStepELe();
 		});
 		const handleNext = () => {
-			if (data.step >= 3) {
+			if (data.step >= 3 || (state.platform === IPlatform.PC && data.step >= 1)) {
 				endGuide();
 				return;
 			}

+ 140 - 15
src/page-instrument/custom-plugins/guide-page/teacher-top.tsx

@@ -5,6 +5,8 @@ import styles from "./index.module.less";
 import { getImage } from "./images";
 import { getQuery } from "/src/utils/queryString";
 import {getGuidance,setGuidance} from './api'
+import state, { IPlatform } from "/src/state";
+
 export default defineComponent({
   name: "teacherTop-guide",
   emits: ["close"],
@@ -12,7 +14,128 @@ export default defineComponent({
     const data = reactive({
       box: {},
       show: false,
-      steps: [
+      steps: state.platform === IPlatform.PC ? [
+        {
+          ele: "",
+          eleRect: {} as DOMRect,
+          img: getImage("pc_teacherBottom1.png"),
+          handStyle: {
+            top: "0.91rem",
+          },
+          imgStyle: {
+            bottom: "2.5rem",
+            width:'3.63rem',
+            height:'2.28rem',
+            left:'-0.45rem'
+          },
+          btnsStyle: {
+            bottom: "2.72rem",
+            left:'1.1rem',
+            transform: "scale(.83)",
+
+          },
+        },
+        {
+          ele: "",
+          img: getImage("pc_teacherBottom2.png"),
+          handStyle: {
+            top: "-1.39rem",
+            left:'0.15rem',
+            transform: 'rotate(180deg)'
+          },
+          imgStyle: {
+            bottom: "2.5rem",
+            width:'4.20rem',
+            height:'2.28rem',
+            left:'-0.45rem'
+          },
+          btnsStyle: {
+            bottom: "2.72rem",
+            left:'1.1rem',
+            transform: "scale(.83)",
+          },
+        },
+        {
+          ele: "",
+          img: getImage("pc_teacherBottom3.png"),
+          handStyle: {
+            top: "-1.39rem",
+            left:'0.17rem',
+            transform: 'rotate(180deg)'
+          },
+          imgStyle: {
+            bottom: "2.5rem",
+            width:'5.15rem',
+            height:'2.28rem',
+            left:'-0.45rem'
+          },
+          btnsStyle: {
+            bottom: "2.72rem",
+            left:'1.1rem',
+            transform: "scale(.83)",
+          },
+        },
+        {
+          ele: "",
+          img: getImage("pc_teacherBottom4.png"),
+          handStyle: {
+            top: "-1.39rem",
+            left:'1.4rem',
+            transform: 'rotate(180deg)'
+          },
+          imgStyle: {
+            bottom: "2.5rem",
+            width:'4.39rem',
+            height:'2.28rem',
+            left:'-0.45rem'
+          },
+          btnsStyle: {
+            bottom: "2.72rem",
+            left:'1.1rem',
+            transform: "scale(.83)",
+          },
+        },
+        {
+          ele: "",
+          img: getImage("pc_teacherBottom5.png"),
+          handStyle: {
+            top: "-1.39rem",
+            left:'1.4rem',
+            transform: 'rotate(180deg)'
+          },
+          imgStyle: {
+            bottom: "2.5rem",
+            width:'4.58rem',
+            height:'2.28rem',
+            left:'-0.45rem'
+          },
+          btnsStyle: {
+            bottom: "2.72rem",
+            left:'1.1rem',
+            transform: "scale(.83)",
+          },
+        },
+        {
+          ele: "",
+          img: getImage("pc_teacherBottom6.png"),
+          handStyle: {
+            top: "-1.39rem",
+            left:'1.4rem',
+            transform: 'rotate(180deg)'
+          },
+          imgStyle: {
+            bottom: "2.5rem",
+            width:'4.01rem',
+            height:'2.28rem',
+            left:'-3.4rem'
+          },
+          btnsStyle: {
+            bottom: "2.72rem",
+            left:'-2.1rem',
+            transform: "scale(.83)",
+          },
+        },
+      ] : [
         {
           ele: "",
           eleRect: {} as DOMRect,
@@ -147,21 +270,23 @@ export default defineComponent({
   const guideInfo = ref({} as any)
   const getAllGuidance = async()=>{
     try{
-    const res = await getGuidance({guideTag:'guideInfo'})
-    if(res.data){
-      guideInfo.value = JSON.parse(res.data?.guideValue) || null
-    }else{
-      guideInfo.value = {}
-    }
-
-
-    if (guideInfo.value && guideInfo.value.teacherTop) {
-      tipShow.value = false;
-    } else {
-      tipShow.value = true;
-    }
+			if (state.guideInfo) {
+				guideInfo.value = state.guideInfo
+			} else {
+				const res = await getGuidance({guideTag:'guideInfo'})
+				if(res.data){
+				  guideInfo.value = JSON.parse(res.data?.guideValue) || null
+				}else{
+				  guideInfo.value = {}
+				}
+			}
+      if (guideInfo.value && guideInfo.value.teacherTop) {
+        tipShow.value = false;
+      } else {
+        tipShow.value = true;
+      }
     }catch(e){
-    console.log(e)
+      console.log(e)
     }
     // const guideInfo = localStorage.getItem('teacher-guideInfo');
 

+ 24 - 0
src/page-instrument/custom-plugins/helper-model/recommendation/index.module.less

@@ -52,6 +52,17 @@
             padding: 9px;
         }
     }
+    &.pcContent {
+        :global {
+            .van-field{
+                font-size: 16Px;
+                line-height: 20Px;
+            }
+            .van-cell__title {
+                font-size: 18Px;
+            }
+        }
+    }
 }
 
 .tags {
@@ -79,10 +90,23 @@
             pointer-events: none;
         }
     }
+    &.pcTags {
+        >span {
+            font-size: 15PX;
+        }
+    }
 }
 .btn{
     display: block;
     height: 36px;
     font-size: 13px;
     margin: 0 auto;
+}
+
+.pcContent {
+    .tags {
+        >span {
+            font-size: 16PX;
+        }
+    }
 }

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

@@ -5,6 +5,7 @@ import { Button, Cell, Field, Tab, Tabs, showToast } from "vant";
 import iconSubmit from "../icons/icon-submit.svg";
 import { sysSuggestionAdd, getSuggestionList } from "/src/page-instrument/api";
 import { storeData } from "/src/store";
+import state, { IPlatform } from "/src/state";
 
 export default defineComponent({
 	name: "recommendation",
@@ -60,7 +61,8 @@ export default defineComponent({
 			getTypeList();
 		});
 		return () => (
-			<div class={styles.content}>
+			<div class={[styles.content, state.platform === IPlatform.PC && styles.pcContent]}>
+				{ state.platform === IPlatform.PC && <div class={'top_drag'}></div> }
 				<Tabs lineHeight={0} color="#1A1A1A">
 					<Tab title="意见反馈">
 						<Cell border={false} title="请选择问题类型" />

+ 8 - 0
src/page-instrument/follow-model/index.module.less

@@ -38,6 +38,14 @@
   }
 }
 
+.pcEndBtn {
+  width: 36px;
+  height: 36px;
+  left: 46px;
+  bottom: 12px !important;
+  margin-left: initial;
+}
+
 .noteState {
   position: fixed;
   bottom: 0;

+ 4 - 3
src/page-instrument/follow-model/index.tsx

@@ -4,7 +4,8 @@ import icons from "./icons.json";
 import { followData, handleFollowEnd, handleFollowStart } from "/src/view/follow-practice";
 import { Popup } from "vant";
 import Microphone from "./microphone";
-import state from "/src/state";
+import state, { IPlatform } from "/src/state";
+import PcEndIcon from "../header-top/image/pc_end_icon.png"
 
 export default defineComponent({
 	name: "follow-model",
@@ -25,9 +26,9 @@ export default defineComponent({
 				</Transition>
 				<Transition name="pop-center">
 					{followData.start && (
-						<div class={[styles.endBtn, state.fingeringInfo?.name && state.fingeringInfo.direction == 'transverse' && state.setting.displayFingering ? styles.top : styles.bottom]} key="end">
+						<div class={[styles.endBtn, state.fingeringInfo?.name && state.fingeringInfo.direction == 'transverse' && state.setting.displayFingering ? styles.top : styles.bottom, state.platform === IPlatform.PC && styles.pcEndBtn]} key="end">
 							<img
-								src={icons.end}
+								src={state.platform === IPlatform.PC ? PcEndIcon : icons.end}
 								onClick={() => {
 									handleFollowEnd();
 								}}

BIN
src/page-instrument/header-top/image/pc_end_icon.png


BIN
src/page-instrument/header-top/image/pc_icon_pausebtn.png


BIN
src/page-instrument/header-top/image/pc_icon_playbtn.png


BIN
src/page-instrument/header-top/image/pc_icon_resetbtn.png


+ 35 - 5
src/page-instrument/header-top/index.module.less

@@ -11,6 +11,7 @@
     animation: headerDown .3s .5s ease-in-out forwards;
 
     &.headRightTop {
+        transform: translateY(100%);
         transition: margin-top .2s ease;
         margin-top: 0;
     }
@@ -60,8 +61,10 @@
     align-items: center;
     margin-left: auto;
     height: 100%;
-
-
+}
+.pcHeadRight {
+    width: 100%;
+    justify-content: center;
 }
 
 .btn {
@@ -139,6 +142,17 @@
             }
         }          
     }     
+    &.pcBtn {
+        .arrowIcon {
+            top: initial;
+            bottom: 68PX;
+            transform: translateX(-8PX) rotate(180deg);
+        }
+        .botton-tips {
+            top: initial;
+            bottom: 76PX;
+        }
+    }
 }
 
 .setBtn {
@@ -180,12 +194,18 @@
         }
     }
 
-    &.playButton {
-        left: 32px !important;
+    &.playLeftButton {
+        left: 46px !important;
         right: auto !important;
         bottom: 12px !important;
     }
 
+    &.playRightButton {
+        right: 46px !important;
+        left: auto !important;
+        bottom: 12px !important;
+    }
+
     &.playButtonHide {
         transition: bottom .2s ease;
         bottom: 60px !important;
@@ -205,12 +225,18 @@
         height: 36px;
     }
 
-    &.pauseButton {
+    &.pauseLeftButton {
         left: 88px !important;
         right: auto !important;
         bottom: 12px !important;
     }
 
+    &.pauseRightButton {
+        right: 88px !important;
+        left: auto !important;
+        bottom: 12px !important;
+    }
+
     &.playButtonHide {
         transition: bottom .2s ease;
         bottom: 60px !important;
@@ -227,4 +253,8 @@
             pointer-events: auto;
         }
     }
+}
+
+.pcTransPop {
+    z-index: 999 !important;
 }

+ 98 - 32
src/page-instrument/header-top/index.tsx

@@ -1,4 +1,4 @@
-import { Transition, computed, defineComponent, onMounted, onUnmounted, reactive, ref, watch } from "vue";
+import { Transition, computed, defineComponent, onMounted, onUnmounted, reactive, ref, watch, toRef } from "vue";
 import styles from "./index.module.less";
 
 import iconBack from "./image/icon-back.svg";
@@ -24,6 +24,9 @@ import store from "store";
 import "../component/the-modal-tip/index.module.less";
 import { metronomeData } from "../../helpers/metronome";
 import { toggleMusicSheet } from "/src/view/plugins/toggleMusicSheet"
+import useDrag from "/src/view/plugins/useDrag/index";
+import Dragbom from "/src/view/plugins/useDrag/dragbom";
+import { getGuidance, setGuidance } from "../custom-plugins/guide-page/api";
 
 /** 头部数据和方法 */
 export const headTopData = reactive({
@@ -286,16 +289,44 @@ export default defineComponent({
 
     /** 课件播放 */
     const changePlay = (res: any) => {
+      // console.log('监听上课页面message',res)
       if (res?.data?.api === "setPlayState") {
-        togglePlay("paused");
+        togglePlay("paused","courseware");
       }
 
       // 菜单状态
       if ((state.platform === IPlatform.PC && res?.data?.api) === "attendClassBarStatus") {
-        state.attendHideMenu = res?.data?.hideMenu;
+        // state.attendHideMenu = res?.data?.hideMenu;
+      }
+
+      // 上课页面,按钮方向
+      if (res?.data?.api === "imagePos") {
+        if (res?.data.data) {
+          state.playBtnDirection = res.data.data === 'right' ? 'right' : 'left';
+          // if (state.fingeringInfo.direction === "vertical" && state.setting.displayFingering) {
+          //   state.musicScoreBtnDirection = state.playBtnDirection === 'right' ? 'left' : 'right';
+          // } else {
+          //   state.musicScoreBtnDirection = state.playBtnDirection;
+          // }
+          state.musicScoreBtnDirection = state.playBtnDirection;
+        }
       }
     };
 
+    const parentClassName = "settingBoxClass_drag";
+    const userId = storeData.user?.id ? String(storeData.user?.id) : '';
+    const positionInfo = state.platform !== IPlatform.PC ? {
+      styleDrag: { value: null }
+    } : useDrag(
+      [
+        `${parentClassName} .top_drag`,
+        `${parentClassName} .bom_drag`
+      ],
+      parentClassName,
+      toRef(headTopData, 'settingMode'),
+      userId
+    )
+
     onMounted(() => {
       getQueryModelSetModelType();
       window.addEventListener("message", changePlay);
@@ -316,6 +347,34 @@ export default defineComponent({
       store.set("musicscoresetting", state.setting);
     });
 
+    // 获取引导页信息
+		const getAllGuidance = async()=>{
+      let guideInfo: any = null;
+		  try{
+        const res = await getGuidance({guideTag:'guideInfo'})
+        if(res.data){
+          guideInfo = JSON.parse(res.data?.guideValue) || null
+        }else{
+          guideInfo = {}
+        }
+        state.guideInfo = guideInfo;
+		  }catch(e){
+		    console.log(e)
+		  }
+	  
+		}
+		getAllGuidance()    
+
+    // 完成拖动弹窗引导页
+    const handleGuide = async () => {
+      state.guideInfo.teacherDrag = true;
+			try{
+				const res = await setGuidance({guideTag:'guideInfo',guideValue:JSON.stringify(state.guideInfo)})
+      }catch(e){
+        console.log(e)
+      }   
+    }
+
     return () => (
       <>
         <div
@@ -336,10 +395,10 @@ export default defineComponent({
           <div class={[styles.back, "headTopBackBtn", !headTopData.showBack && styles.hidenBack]} onClick={handleBack}>
             <img src={iconBack} />
           </div>
-          {query.iscurseplay === "play" ? null : <Title class="pcTitle" text={state.examSongName} rightView={false} />}
+          {(query.iscurseplay === "play" || state.platform === IPlatform.PC) ? null : <Title class="pcTitle" text={state.examSongName} rightView={false} />}
 
           <div
-            class={[styles.headRight]}
+            class={[styles.headRight, state.platform === IPlatform.PC && styles.pcHeadRight]}
             onClick={(e: Event) => {
               e.stopPropagation();
             }}
@@ -358,25 +417,30 @@ export default defineComponent({
               <span>模式</span>
             </div>
 
-            <div class={[styles.btn]} onClick={() => {
-              // 切换光标模式
-              let mode = metronomeData.cursorMode
-              if (['follow'].includes(state.modeType)) {
-                mode = metronomeData.cursorMode === 1 ? 3 : 1
-              } else {
-                mode = metronomeData.cursorMode === 3 ? 1 : metronomeData.cursorMode + 1
-              }              
-              metronomeData.cursorMode = mode
-            }}>
-              <img class={styles.iconBtn} src={headImg(metronomeData.cursorMode === 1 ? 'cursor-icon-1.svg' : metronomeData.cursorMode === 2 ? 'cursor-icon-2.svg' : metronomeData.cursorMode === 3 ? 'cursor-icon-3.svg' : '')} />
-              <span class={styles.iconContent}>
-                {metronomeData.cursorMode === 1 ? '音符指针' : metronomeData.cursorMode === 2 ? '节拍指针' : metronomeData.cursorMode === 3 ? '关闭指针' : ''}
-                {metronomeData.cursorTips && <>
-                    <i class={styles.arrowIcon}></i>
-                    <div class={[styles['botton-tips'],metronomeData.cursorMode === 3 ? styles.tipSpec : '']}>{metronomeData.cursorTips}</div>
-                </>}
-              </span>
-            </div>
+            {/* 一行谱模式,暂不支持节拍指针 */}
+            {
+              !state.isSingleLine ? 
+              <div class={[styles.btn, state.platform === IPlatform.PC ? styles.pcBtn : ""]} onClick={() => {
+                // 切换光标模式
+                let mode = metronomeData.cursorMode
+                if (['follow'].includes(state.modeType)) {
+                  mode = metronomeData.cursorMode === 1 ? 3 : 1
+                } else {
+                  mode = metronomeData.cursorMode === 3 ? 1 : metronomeData.cursorMode + 1
+                }              
+                metronomeData.cursorMode = mode
+              }}>
+                <img class={styles.iconBtn} src={headImg(metronomeData.cursorMode === 1 ? 'cursor-icon-1.svg' : metronomeData.cursorMode === 2 ? 'cursor-icon-2.svg' : metronomeData.cursorMode === 3 ? 'cursor-icon-3.svg' : '')} />
+                <span class={styles.iconContent}>
+                  {metronomeData.cursorMode === 1 ? '音符指针' : metronomeData.cursorMode === 2 ? '节拍指针' : metronomeData.cursorMode === 3 ? '关闭指针' : ''}
+                  {metronomeData.cursorTips && <>
+                      <i class={styles.arrowIcon}></i>
+                      <div class={[styles['botton-tips'],metronomeData.cursorMode === 3 ? styles.tipSpec : '']}>{metronomeData.cursorTips}</div>
+                  </>}
+                </span>
+              </div> : null         
+            }  
+
             {state.musicRendered && !query.lessonTrainingId && !query.questionId && state.isConcert && (
               <div class={[styles.btn, (state.playState === "play" && fingeringBtn.value.disabled) && styles.disabled]} 
                 onClick={() => {
@@ -431,7 +495,7 @@ export default defineComponent({
               <span>指法</span>
             </div>
 
-            <Popover trigger="manual" v-model:show={headData.speedShow} placement="bottom" overlay={false}>
+            <Popover trigger="manual" v-model:show={headData.speedShow} placement={state.platform === IPlatform.PC ? "top" : "bottom"} overlay={false} offset={state.platform === IPlatform.PC ? [8,40] : [0,8]}>
               {{
                 reference: () => (
                   <div
@@ -454,7 +518,7 @@ export default defineComponent({
             </Popover>
             {
               state.enableNotation ? 
-              <Popover trigger="manual" v-model:show={headData.musicTypeShow} placement="bottom-end" overlay={false}>
+              <Popover trigger="manual" v-model:show={headData.musicTypeShow} class={state.platform === IPlatform.PC && styles.pcTransPop} placement={state.platform === IPlatform.PC ? "top-end" : "bottom-end"} overlay={false} offset={state.platform === IPlatform.PC ? [0,40] : [0,8]}>
                 {{
                   reference: () => (
                     <div
@@ -485,12 +549,12 @@ export default defineComponent({
         <div
           id="studnetT-7"
           style={{ display: playBtn.value.display ? "" : "none" }}
-          class={[styles.btn, styles.playBtn, playBtn.value.disabled && styles.disabled, state.platform === IPlatform.PC && styles.playButton, state.platform === IPlatform.PC && !state.attendHideMenu && styles.playButtonHide]}
+          class={[styles.btn, styles.playBtn, playBtn.value.disabled && styles.disabled, state.platform === IPlatform.PC && state.musicScoreBtnDirection === 'left' ? styles.playLeftButton : state.platform === IPlatform.PC && state.musicScoreBtnDirection === 'right' ? styles.playRightButton : '', state.platform === IPlatform.PC && !state.attendHideMenu && styles.playButtonHide]}
           onClick={() => togglePlay()}
         >
           <div class={styles.btnWrap}>
-            <img style={{ display: state.playState === "play" ? "none" : "" }} class={styles.iconBtn} src={headImg("icon_play.svg")} />
-            <img style={{ display: state.playState === "play" ? "" : "none" }} class={styles.iconBtn} src={headImg("icon_pause.svg")} />
+            <img style={{ display: state.playState === "play" ? "none" : "" }} class={styles.iconBtn} src={headImg(state.platform === IPlatform.PC ? "pc_icon_playbtn.png" : "icon_play.svg")} />
+            <img style={{ display: state.playState === "play" ? "" : "none" }} class={styles.iconBtn} src={headImg(state.platform === IPlatform.PC ? "pc_icon_pausebtn.png" : "icon_pause.svg")} />
             <Circle style={{ opacity: state.playState === "play" ? 1 : 0 }} class={styles.progress} stroke-width={80} currentRate={state.playProgress} rate={100} color="#FFC830" />
           </div>
         </div>
@@ -499,14 +563,16 @@ export default defineComponent({
         <div
           id="tips-step-9"
           style={{ display: resetBtn.value.display ? "" : "none" }}
-          class={[styles.btn, styles.resetBtn, resetBtn.value.disabled && styles.disabled, state.platform === IPlatform.PC && styles.pauseButton, state.platform === IPlatform.PC && !state.attendHideMenu && styles.playButtonHide]}
+          class={[styles.btn, styles.resetBtn, resetBtn.value.disabled && styles.disabled, 
+            state.platform === IPlatform.PC && state.musicScoreBtnDirection === 'left' ? styles.pauseLeftButton : state.platform === IPlatform.PC && state.musicScoreBtnDirection === 'right' ? styles.pauseRightButton : '', state.platform === IPlatform.PC && !state.attendHideMenu && styles.playButtonHide]}
           onClick={() => handleResetPlay()}
         >
-          <img class={styles.iconBtn} src={headImg("icon_resetbtn.svg")} />
+          <img class={styles.iconBtn} src={headImg(state.platform === IPlatform.PC ? "pc_icon_resetbtn.png" : "icon_resetbtn.svg")} />
         </div>
 
-        <Popup v-model:show={headTopData.settingMode} class="popup-custom van-scale center-closeBtn" transition="van-scale" teleport="body" closeable>
+        <Popup v-model:show={headTopData.settingMode} class="popup-custom van-scale center-closeBtn settingBoxClass_drag" transition="van-scale" teleport="body" closeable style={positionInfo.styleDrag.value}>
           <Settting />
+          { state.platform === IPlatform.PC && <Dragbom showGuide={!state.guideInfo?.teacherDrag} onGuideDone={handleGuide}  /> }
         </Popup>
 
         {/* 模式切换 */}

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

@@ -1,12 +1,17 @@
-import { defineComponent, reactive, ref, watch } from "vue";
+import { defineComponent, reactive, ref, watch, toRef } from "vue";
 import { headImg } from "../image";
 import { useClickAway } from "@vant/use";
 import { headData } from "..";
 import styles from "./index.module.less";
-import state from "/src/state";
+import state, { IPlatform } from "/src/state";
 import { Popup } from "@varlet/ui";
 import TheComfirm from "../../component/the-comfirm";
 import { musicRenderTypeKey, resetRenderMusicScore } from "/src/view/music-score";
+import useDrag from "/src/view/plugins/useDrag/index";
+import Dragbom from "/src/view/plugins/useDrag/dragbom";
+import { storeData } from "/src/store";
+import { setGuidance } from "../../custom-plugins/guide-page/api";
+
 export default defineComponent({
 	name: "musicType",
 	setup(props) {
@@ -37,6 +42,31 @@ export default defineComponent({
 			}
 			musicTypeData.show = false
 		}
+
+		// 完成拖动弹窗引导页
+		const handleGuide = async () => {
+			state.guideInfo.teacherDrag = true;
+			try{
+					const res = await setGuidance({guideTag:'guideInfo',guideValue:JSON.stringify(state.guideInfo)})
+			}catch(e){
+				console.log(e)
+			}   
+	  	}
+
+		const parentClassName = "transBoxClass_drag";
+		const userId = storeData.user?.id ? String(storeData.user?.id) : '';
+		const positionInfo = state.platform !== IPlatform.PC ? {
+		  styleDrag: { value: null }
+		} : useDrag(
+		  [
+			`${parentClassName} .top_drag`,
+			`${parentClassName} .bom_drag`
+		  ],
+		  parentClassName,
+		  toRef(musicTypeData, 'show'),
+		  userId
+		)
+
 		return () => (
 			<>
 				<div ref={musicTypeRef}>
@@ -53,8 +83,9 @@ export default defineComponent({
 						<div>固定调</div>
 					</div>
 				</div>
-				<Popup teleport="body" closeOnClickOverlay={false} defaultStyle={false} v-model:show={musicTypeData.show}>
+				<Popup teleport="body" closeOnClickOverlay={true} defaultStyle={false} v-model:show={musicTypeData.show} class="transBoxClass_drag" style={positionInfo.styleDrag.value}>
 					<TheComfirm tip="设置成功,是否立即重新加载?" onClose={handleResult} />
+					{ state.platform === IPlatform.PC && <Dragbom showGuide={!state.guideInfo?.teacherDrag} onGuideDone={handleGuide}  /> }
 				</Popup>
 			</>
 		);

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

@@ -5,7 +5,7 @@
 .content {
     position: relative;
     overflow: hidden;
-    border-radius: 18px;
+    border-radius: 16PX;
     width: 300px;
     height: 86vh;
     background-color: #fff;
@@ -74,6 +74,24 @@
             font-size: 12px;
         }
     }
+
+    &.pcContent {
+        height: 230px;
+        .pcDragTop {
+            position: absolute;
+            left: 0;
+            top: 0;
+            width: 100%;
+            height: 20px;
+            z-index: 1;
+            cursor: move;
+        }
+        :global {
+            .van-tab__panel {
+                height: 180px;
+            } 
+        }
+    }
 }
 
 .noticebar {

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

@@ -1,4 +1,4 @@
-import { defineComponent, nextTick, reactive, watch } from "vue";
+import { defineComponent, nextTick, reactive, watch, toRef } from "vue";
 import styles from "./index.module.less";
 import iconClose from "../image/close2.svg";
 import {
@@ -30,6 +30,10 @@ import Recommendation from "../../custom-plugins/helper-model/recommendation";
 import { svg2canvas } from "/src/utils/svg2canvas";
 import { getQuery } from "/src/utils/queryString";
 import { browser } from "/src/utils";
+import { storeData } from "/src/store";
+import useDrag from "/src/view/plugins/useDrag/index";
+import Dragbom from "/src/view/plugins/useDrag/dragbom";
+import { setGuidance } from "/src/page-instrument/custom-plugins/guide-page/api";
 
 export default defineComponent({
 	name: "header-settting",
@@ -98,10 +102,34 @@ export default defineComponent({
 			}
 			state.setting.frequency = currentFrequency >= 0 ? currentFrequency : 0
 		}
-		
+
+		const parentClassName = "recommenBoxClass_drag";
+		const userId = storeData.user?.id ? String(storeData.user?.id) : '';
+		const positionInfo = state.platform !== IPlatform.PC ? {
+			styleDrag: { value: null }
+		  } : useDrag(
+		  [
+			`${parentClassName} .top_drag`,
+			`${parentClassName} .bom_drag`
+		  ],
+		  parentClassName,
+		  toRef(helperData, 'recommendationShow'),
+		  userId
+		)
+	    // 完成拖动弹窗引导页
+		const handleGuide = async () => {
+			state.guideInfo.teacherDrag = true;
+				  try{
+					  const res = await setGuidance({guideTag:'guideInfo',guideValue:JSON.stringify(state.guideInfo)})
+			}catch(e){
+			  console.log(e)
+			}   
+		}
+
 		return () => (
 			<div class={styles["header-settting"]}>
-				<div class={styles.content}>
+				<div class={[styles.content, state.platform === IPlatform.PC && styles.pcContent]}>
+				{ state.platform === IPlatform.PC && <div class={'top_drag'}></div> }
 					<Tabs border animated swipeable>
 						<Tab title="全局设置">
 							<NoticeBar
@@ -282,16 +310,18 @@ export default defineComponent({
 				</Popup>
 				<Popup
 					v-model:show={helperData.recommendationShow}
-					class="popup-custom van-scale center-closeBtn"
+					class="popup-custom van-scale center-closeBtn recommenBoxClass_drag"
 					transition="van-scale"
 					teleport="body"
 					closeable
+					style={positionInfo.styleDrag.value}
 				>
 					<Recommendation
 						onClose={() => {
 							helperData.recommendationShow = false;
 						}}
 					/>
+					{ state.platform === IPlatform.PC && <Dragbom showGuide={!state.guideInfo?.teacherDrag} /> }
 				</Popup>
 			</div>
 		);

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

@@ -11,6 +11,9 @@
     flex: 1;
     padding: 0;
   }
+  &.pcContainer {
+    width: 500px;
+  }
 }
 
 .icon {

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

@@ -1,6 +1,7 @@
 import { defineComponent } from 'vue'
 import { NoticeBar } from 'vant'
 import styles from './index.module.less'
+import state, { IPlatform } from "/src/state";
 
 import MusicIcon from '../image/music.png'
 import ArrowIcon from '../image/arrow.svg'
@@ -22,7 +23,7 @@ export default defineComponent({
   },
   render() {
     return (
-      <div class={styles.container}>
+      <div class={[styles.container, state.platform === IPlatform.PC && styles.pcContainer]}>
         <NoticeBar
           text={this.text}
           color="#000"

+ 95 - 10
src/page-instrument/view-detail/index.module.less

@@ -15,18 +15,45 @@
     height: 100vh;
     overflow: hidden;
     --header-height: 62px;
+    --pc-header-height: 54px;
     background: var(--container-background);
 
     .headHeight {
         position: relative;
         width: 100%;
         height: var(--header-height);
-        transition: margin .3s;
+        transition: all .3s;
+        // bottom: 0;
         z-index: 10;
 
         &.headHide {
             margin-top: calc(0Px - var(--header-height));
         }
+
+        &.pcHeadHideBottom {
+            margin-bottom: calc(-var(--header-height));
+        }
+    }
+
+    .pcHead {
+        height: var(--pc-header-height);
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        transform: translateY(-100%);
+        animation: headerDown .3s .5s ease-in-out forwards;
+        transition: all .3s;
+        &.pcHeadHide {
+            margin-top: calc(0Px - var(--pc-header-height));
+        }
+        &.pcHeadHideBottom {
+            bottom: calc(var(--header-height));
+        }
+        :global {
+            .van-notice-bar__wrap {
+                justify-content: center;
+            }
+        }
     }
 
     .container {
@@ -38,11 +65,17 @@
         transition: height .2s;
         transition: padding-bottom .2s;
         overflow: hidden;
+        //overflow-x: scroll;
+    }
+    .pcContainer {
+        height: calc(100vh - var(--header-height) - var(--pc-header-height));
+        margin: 0 24px;
     }
 }
 
+
 :global {
-    #cursorImg-0 {
+    #cursorImg-0, #cursor-copy {
         width: 2PX !important;
         min-height: 58PX;
         height: 58PX;
@@ -52,24 +85,28 @@
         background-color: rgba(25, 140, 254, 0.7);
         opacity: var(--corsor-opacity);
         //transform: translateX(10PX);
+        z-index: 1 !important;
     }
 
     .staff {
-        #cursorImg-0 {
+        #cursorImg-0, #cursor-copy {
             width: 14Px;
             transform: translateX(11Px);
         }
+        #cursor-copy {
+            transform: translate(0, -34%);
+        }
     }
 
     .jianpuTone {
-        #cursorImg-0 {
+        #cursorImg-0, #cursor-copy {
             width: 18Px;
             transform: translateX(6.3Px) !important;
         }
     }
 
     .eyeProtection {
-        #cursorImg-0 {
+        #cursorImg-0, #cursor-copy {
             background-color: rgb(255, 159, 88);
         }
     }
@@ -81,7 +118,7 @@
 
 .xiaomi {
     :global {
-        #cursorImg-0 {
+        #cursorImg-0, #cursor-copy {
             height: 58PX !important;
             min-height: auto !important;
         }
@@ -90,21 +127,21 @@
 
 .PC {
     :global {
-        #cursorImg-0 {
+        #cursorImg-0, #cursor-copy {
             margin-top: -18PX;
             min-height: 94Px;
             border-radius: 10Px;
         }
 
         .staff {
-            #cursorImg-0 {
+            #cursorImg-0, #cursor-copy {
                 width: 35Px;
                 transform: translateX(21Px) !important;
             }
         }
 
         .jianpuTone {
-            #cursorImg-0 {
+            #cursorImg-0, #cursor-copy {
                 width: 29Px;
                 transform: translateX(13Px) !important;
             }
@@ -147,7 +184,7 @@
         #osmdCanvasPage1 {
             padding-bottom: 0 !important;
         }
-        #cursorImg-0 {
+        #cursorImg-0, #cursor-copy {
             opacity: 0 !important;
         }
     }
@@ -159,4 +196,52 @@
     visibility: hidden;
     background: transparent;
     opacity: 0;
+}
+
+.singleLineDetail {
+    :global {
+        #cursorImg-0 {
+            display: none;
+        }
+        .staveBox {
+            display: none !important;
+        }
+        .cursorAnimate {
+            animation: cnimate 1s ease-in-out infinite;
+        }
+        .leftNoteBg {
+            position: sticky;
+            background: rgba(0, 0, 0, 0.3);
+            ::before {
+                content: "";
+                position: absolute;
+                left: 0;
+                top: 0;
+                width: 200px;
+                height: 100px;
+                background: rgba(0,0,0,0.4);
+                z-index: 999;
+                display: block;
+            }
+        }
+        #cursor-copy {
+            &::after {
+                content: "";
+                position: sticky;
+                left: 0;
+                top: 0;
+                width: 200px;
+                height: 200px;
+                background: rgba(0,0,0,0.4);
+                z-index: 999;
+                display: block;
+            }
+        }        
+    }
+}
+
+@keyframes headerDown {
+    100% {
+        transform: translateY(0%);
+    }
 }

+ 86 - 23
src/page-instrument/view-detail/index.tsx

@@ -2,10 +2,11 @@ import { Popup, Skeleton } from "vant";
 import { computed, defineComponent, nextTick, onBeforeMount, onBeforeUnmount, onMounted, reactive, Transition, watch, watchEffect, defineAsyncComponent } from "vue";
 import { formateTimes } from "../../helpers/formateMusic";
 import Metronome, { metronomeData } from "../../helpers/metronome";
-import state, { EnumMusicRenderType, evaluatCreateMusicPlayer, handleSetSpeed, IAudioState, IPlatform, isRhythmicExercises, resetPlaybackToStart, togglePlay, getMusicDetail } from "/src/state";
+import state, { EnumMusicRenderType, evaluatCreateMusicPlayer, handleSetSpeed, IAudioState, IPlatform, isRhythmicExercises, resetPlaybackToStart, togglePlay, getMusicDetail, calculateDistance, createFixedCursor, addNoteBBox } from "/src/state";
 import { browser, setGlobalData } from "../../utils";
 import AudioList from "../../view/audio-list";
 import MusicScore, { resetMusicScore } from "../../view/music-score";
+import TestCheck from "/src/view/music-score/testCheck";
 import { sysMusicScoreAccompanimentQueryPage } from "../api";
 import EvaluatModel from "../evaluat-model";
 import HeaderTop from "../header-top";
@@ -32,7 +33,7 @@ import { usePageVisibility } from "@vant/use";
 import { initMidi } from "/src/helpers/midiPlay"
 import TheAudio from "/src/components/the-audio"
 import tickWav from "/src/assets/tick.wav";
-
+import Title from "../header-top/title";
 
 const DelayCheck = defineAsyncComponent(() =>
   import('/src/page-instrument/evaluat-model/delay-check')
@@ -135,6 +136,8 @@ export default defineComponent({
       if (state.isPreView) {
         state.zoom = 0.65
       }
+      state.isSingleLine = query.isSingleLine; // 一行谱模式
+      state.moveType = query.moveType == '2' ? 'uniform' : 'smooth'; // 一行谱平移模式
       // Promise.all([sysMusicScoreAccompanimentQueryPage(id)]).then((values) => {
       //   getMusicInfo(values[0]);
       // });
@@ -165,6 +168,11 @@ export default defineComponent({
       setCustomGradual();
 			setCustomNoteRealValue();
       state.times = formateTimes(osmd);
+      // 音符添加位置信息bbox
+      addNoteBBox(state.times);
+      // 一行谱,创建固定的音符指针
+      createFixedCursor();
+      calculateDistance();
       // state.times = resetFrequency(state.times);
       state.times = setNoteHalfTone(state.times);
       console.log("🚀 ~ state.times:", state.times, state.subjectId, state);
@@ -188,7 +196,14 @@ export default defineComponent({
         handleInitTick(beatLengthInMilliseconds, osmd?.Sheet?.SheetPlaybackSetting?.Rhythm?.Numerator || 4);
       // }
       // api_cloudLoading();
-
+      state.playBtnDirection = query.imagePos === 'right' ? 'right' : 'left';
+      state.isAttendClass = (query.imagePos === 'left' || query.imagePos === 'right') ? true : false;
+      // if (state.fingeringInfo.direction === "vertical" && state.setting.displayFingering) {
+      //   state.musicScoreBtnDirection = state.playBtnDirection === 'right' ? 'left' : 'right';
+      // } else {
+      //   state.musicScoreBtnDirection = state.playBtnDirection;
+      // }
+      state.musicScoreBtnDirection = state.playBtnDirection;
       state.musicRendered = true;
 
       evaluatCreateMusicPlayer();
@@ -210,18 +225,37 @@ export default defineComponent({
             },
           };
         } else {
-          return {
-            container: {
-              paddingRight: state.fingeringInfo.width,
-            },
-            fingerBox: {
-              position: "absolute",
-              width: state.fingeringInfo.width,
-              height: "100%",
-              right: 0,
-              top: 0,
-            },
-          };
+          console.log('指法',state.playBtnDirection,state.platform)
+          // 老师端,竖向指法,需要根据功能按钮方向进行设置
+          if (state.platform === IPlatform.PC) {
+            return {
+              container: {
+                paddingRight: state.playBtnDirection === "right" ? "initial" : state.fingeringInfo.width,
+                paddingLeft: state.playBtnDirection === "right" ? state.fingeringInfo.width : "initial",
+              },
+              fingerBox: {
+                position: "absolute",
+                width: state.fingeringInfo.width,
+                height: "100%",
+                right: state.playBtnDirection === "right" ? "initial" : 0,
+                left: state.playBtnDirection === "right" ? 0 : "initial",
+                top: 0,
+              },
+            };
+          } else {
+            return {
+              container: {
+                paddingRight: state.fingeringInfo.width,
+              },
+              fingerBox: {
+                position: "absolute",
+                width: state.fingeringInfo.width,
+                height: "100%",
+                right: 0,
+                top: 0,
+              },
+            };
+          }
         }
       }
       return {
@@ -235,6 +269,13 @@ export default defineComponent({
       () => state.setting.displayFingering,
       () => {
         if (state.fingeringInfo.direction === "vertical") {
+          
+          // if (state.setting.displayFingering) {
+          //   state.musicScoreBtnDirection = state.playBtnDirection === 'left' ? 'right' : 'left'
+          // } else {
+          //   state.musicScoreBtnDirection = state.playBtnDirection
+          // }
+          state.musicScoreBtnDirection = state.playBtnDirection
           nextTick(() => {
             resetMusicScore();
           });
@@ -261,9 +302,10 @@ export default defineComponent({
     watch(
       () => state.playState,
       () => {
-        if (state.platform != IPlatform.PC) {
-          detailData.headerHide = state.playState === "play" ? true : false;
-        }
+        // if (state.platform != IPlatform.PC) {
+        //   detailData.headerHide = state.playState === "play" ? true : false;
+        // }
+        detailData.headerHide = state.playState === "play" ? true : false;
         sendParentMessage(state.playState);
       }
     );
@@ -333,9 +375,10 @@ export default defineComponent({
       detailData.fingerPreView = false;
       detailData.fingerPreViewGuide = false;
     };
+    console.log(1111222,state.zoom)
     return () => (
       <div
-        class={[styles.detail, state.setting.eyeProtection && "eyeProtection", state.platform === IPlatform.PC && styles.PC, state.isPreView && styles.preViewDetail]}
+        class={[styles.detail, state.setting.eyeProtection && "eyeProtection", (state.platform === IPlatform.PC && state.zoom > 0.8) && styles.PC, state.isPreView && styles.preViewDetail, state.isSingleLine && styles.singleLineDetail]}
         style={{
           paddingLeft: detailData.paddingLeft,
           background: state.setting.camera ? `rgba(${state.setting.eyeProtection ? "253,244,229" : "255,255,255"} ,${state.setting.cameraOpacity / 100}) !important` : "",
@@ -348,17 +391,28 @@ export default defineComponent({
             </div>
           )}
         </Transition>
+        {/** 学生端头部标题&功能按钮 */}
         {
-          !state.isPreView && 
+          (!state.isPreView && state.platform !== IPlatform.PC) && 
           <div class={[styles.headHeight, detailData.headerHide && styles.headHide]}>{state.musicRendered && <HeaderTop />}</div>
         }
+        {/** 老师端标题 */}
+        {
+          state.platform === IPlatform.PC && 
+          <div class={[styles.pcHead, detailData.headerHide && styles.pcHeadHide]}>
+            <Title text={state.examSongName} rightView={false} />
+          </div>
+        }
         <div
           id="scrollContainer"
           style={{ ...fingerConfig.value.container, height: detailData.headerHide ? "100vh" : "" }}
-          class={[styles.container, !state.setting.displayCursor && "hideCursor", browsInfo.xiaomi && styles.xiaomi]}
+          class={[styles.container, !state.setting.displayCursor && "hideCursor", browsInfo.xiaomi && styles.xiaomi, state.platform === IPlatform.PC && styles.pcContainer]}
           onClick={(e: Event) => {
             e.stopPropagation();
-            if (state.playState === "play" && state.platform != IPlatform.PC) {
+            // if (state.playState === "play" && state.platform != IPlatform.PC) {
+            //   detailData.headerHide = !detailData.headerHide;
+            // }
+            if (state.playState === "play") {
               detailData.headerHide = !detailData.headerHide;
             }
           }}
@@ -371,6 +425,11 @@ export default defineComponent({
             />
           }
 
+          {/* {
+            state.musicRendered && 
+             <TestCheck />
+          } */}
+
           {/* 指法 */}
           {state.setting.displayFingering && state.fingeringInfo?.name && !state.isPreView && state.isShowFingering && (
             <div style={{ ...fingerConfig.value.fingerBox }}>
@@ -383,7 +442,11 @@ export default defineComponent({
             </div>
           )}
         </div>
-
+        {/** 老师端底部功能按钮 */}
+        {
+          (!state.isPreView && state.platform === IPlatform.PC) && 
+          <div class={[styles.headHeight, detailData.headerHide && styles.pcHeadHideBottom]}>{state.musicRendered && <HeaderTop />}</div>
+        }
         {/* 节拍器,跟练需要播放系统节拍器,所以不需要判断needTick状态 */}
         {/* {state.needTick && <Tick />} */}
         <Tick />

+ 8 - 1
src/page-instrument/view-figner/index.module.less

@@ -1136,7 +1136,14 @@
         .dot {
             display: inline-block;
             position: absolute;
-            top: 0;
+            left: 50%;
+            transform: translateX(-50%);
+            &.topDot {
+                top: 0;
+            }
+            &.bottomDot {
+                top: initial;
+            }
         }
 
         .noteName {

+ 2 - 2
src/page-instrument/view-figner/index.tsx

@@ -1565,14 +1565,14 @@ export default defineComponent({
                             <div>
                               全按作
                               <div class={[styles.noteKey, styles.noteKeyBtn]}>
-                                {data.activeTone.step > 0 ? <span class={styles.dot}></span> : null}
+                                {data.activeTone.step > 0 ? <span class={[styles.topDot, styles.dot]}></span> : null}
                                 <span class={styles.dot}></span>
 
                                 <div class={styles.noteName}>
                                   <sup>{data.activeTone.mark && (data.activeTone.mark === "rise" ? "#" : "b")}</sup>
                                   {data.activeTone.key}
                                 </div>
-                                {data.activeTone.step < 0 ? <span class={styles.dot}></span> : null}
+                                {data.activeTone.step < 0 ? <span class={[styles.bottomDot, styles.dot]}></span> : null}
                               </div>
                             </div>
                             <img src={icons.icon_arrow} />

+ 248 - 12
src/state.ts

@@ -374,8 +374,10 @@ const state = reactive({
   },
   /** 后台设置的基准评测频率 */
   baseFrequency: 440,
-  /** mp3节拍器的时间,统计拍数、速度计算得出 */
+  /** mp3节拍器的时间,统计拍数、速度计算得出,evxml通过读取xml元素获取 */
   fixtime: 0,
+  /** evxml等待播放的时间 */
+  evXmlBeginTime: 0,
   /** 指法信息 */
   fingeringInfo: {} as IFingering,
   /** 滚动容器的ID */
@@ -442,6 +444,31 @@ const state = reactive({
   midiSectionStart: 0,
   /** 音频文件是否加载完成 */
   audioDone: false,
+  /** 谱面svgdom节点 */
+  osmdSvgDom: null as any,
+  /** 滚动容器dom */
+  osdmScrollDom: null as any,
+  /** 光标dom */
+  cursorDom: null as any,
+  fistNoteLeft: 0,
+  /** 是否为单行谱渲染模式 */
+  isSingleLine: false,
+  /** 首尾音符的间距 */
+  noteDistance: 0,
+  /** 一行谱运动模式,平滑移动、匀速移动 */
+  moveType: "smooth" as "smooth" | "uniform",
+  /** 是否是evxml */
+  isEvxml: false,
+  noTimes: [] as any,
+  attendHideMenu: true,
+  /** 老师端:功能按钮布局方向 */
+  playBtnDirection: "left" as "left" | "right",
+  /** 云教练按钮方向,如果有指法并且是竖向的指法,为了防止播放按钮把指法挡住,此时云教练播放按钮方向应该取反 */
+  musicScoreBtnDirection: "right" as "left" | "right",
+  /** 是否在老师端上课页面 */
+  isAttendClass: false,
+  /** 引导页信息 */
+  guideInfo: null as any,
 });
 const browserInfo = browser();
 let offset_duration = 0;
@@ -521,7 +548,7 @@ const handlePlaying = () => {
   const duration = getAudioDuration();
   state.playProgress = (currentTime / duration) * 100;
   let item = getNote(currentTime);
-  // console.log(11111,currentTime,duration,state.playSource, item.i)
+  // console.log(11111,currentTime,duration,state.playSource, item)
   // console.log(item.i,item.noteId,item.measureSpeed)
   // 练习模式下,实时刷新小节速度
   if (item && state.modeType === "practise" && state.playState === "play" && item.measureSpeed && item.measureSpeed !== state.playIngSpeed) {
@@ -588,6 +615,15 @@ const handlePlaying = () => {
   //   metronomeData.metro?.sound(currentTime);
   // }
   metronomeData.metro?.sound(currentTime);
+  // 一行谱,需要滚动小节
+  if (state.isSingleLine) {
+    if (state.moveType === 'smooth') {
+      smoothMoveSvgDom();
+    } else {
+      uniformMoveSvgDom();
+    }
+  }
+  
 };
 /** 跳转到指定音符开始播放 */
 export const skipNotePlay = async (itemIndex: number, isStart = false) => {
@@ -598,7 +634,8 @@ export const skipNotePlay = async (itemIndex: number, isStart = false) => {
   }
   if (item) {
     setAudioCurrentTime(itemTime, itemIndex);
-    gotoNext(item);
+    // 一行谱,点击音符,或者播放完成,需要跳转音符位置
+    gotoNext(item, true);
     metronomeData.metro?.sound(itemTime);
     if (state.isAppPlay) {
       await api_cloudSetCurrentTime({
@@ -615,10 +652,10 @@ export const skipNotePlay = async (itemIndex: number, isStart = false) => {
  * 切换曲谱播放状态
  * @param playState 可选: 默认 undefined, 需要切换的状态 play:播放, paused: 暂停
  */
-export const togglePlay = async (playState?: "play" | "paused") => {
+export const togglePlay = async (playState?: "play" | "paused", sourceType?: string) => {
   // 如果mp3资源还在加载中,给出提示
   if (!state.isAppPlay && !state.audioDone) {
-    showToast('音频资源加载中,请稍后')
+    if (sourceType !== 'courseware') showToast('音频资源加载中,请稍后')
     return
   }
   // midi播放
@@ -731,8 +768,11 @@ const setCursorPosition = (note: any, cursor: any) => {
     });
   }
 };
-/** 跳转到下一个音符 */
-export const gotoNext = (note: any) => {
+/** 
+ * 跳转到下一个音符
+ * 一行谱,点击音符,或者播放完成,需要跳转音符位置,增加参数skipNote
+ **/
+export const gotoNext = (note: any, skipNote?: boolean) => {
   // console.log(33333333333,state.activeNoteIndex,note.i)
   const num = note.i;
 
@@ -744,7 +784,6 @@ export const gotoNext = (note: any) => {
     }
     return;
   }
-  
   const osmd = state.osmd;
   let prev = state.activeNoteIndex;
   state.activeNoteIndex = num;
@@ -765,7 +804,10 @@ export const gotoNext = (note: any) => {
   } catch (error) {
     console.log(error);
   }
-
+  // 一行谱,需要滚动小节
+  if (state.isSingleLine) {
+    moveSvgDom(skipNote);
+  }
   scrollViewNote();
 };
 /** 获取指定音符 */
@@ -1067,6 +1109,7 @@ const setState = (data: any, index: number) => {
   }
   state.gradualTimes = state.extConfigJson.gradualTimes;
   state.repeatedBeats = state.extConfigJson.repeatedBeats || 0;
+  state.isEvxml = state.extConfigJson.isEvxml == 1 ? true : false;
   // 曲子包含节拍器,就不开启节拍器
   state.needTick = data.isUseSystemBeat && data.isPlayBeat ? true : false;
   // state.isOpenMetronome = data.isUseSystemBeat ? false : true;
@@ -1081,7 +1124,8 @@ const setState = (data: any, index: number) => {
   state.musicSheetCategoriesId = data.musicCategoryId;
   state.bizMusicCategoryId = data.bizMusicCategoryId
   state.playMode = data.playMode === "MP3" ? "MP3" : "MIDI";
-  state.originSpeed = state.speed = data.playSpeed;
+  state.originSpeed = state.speed = parseFloat(data.playSpeed) || 0;
+  // state.originSpeed = state.speed = data.playSpeed;
   // state.playIngSpeed = data.playSpeed;
   const track = data.code || data.track;
   state.track = track ? track.replace(/ /g, "").toLocaleLowerCase() : "";
@@ -1140,8 +1184,8 @@ const setState = (data: any, index: number) => {
   state.platform = query.platform?.toLocaleUpperCase() || "";
   if (state.platform === IPlatform.PC) {
     state.zoom = query.zoom || 1.5;
+    state.enableEvaluation = false;
   }
-
   /**
    * 默认渲染什么谱面类型 & 能否转谱逻辑
    * 渲染类型:首先取url参数musicRenderType,没有该参数则取musicalInstruments字段匹配的当前分轨的defaultScore,没有匹配到则取默认值('firstTone')
@@ -1205,4 +1249,196 @@ export const followBeatPaly = () => {
       followBeatPaly();
     }
   });
-};
+};
+
+// 音符添加bbox
+export const addNoteBBox = (list: any[]) => {
+  let voicesBBox: any = null;
+  for (let i = 0; i < list.length; i++) {
+    const note = list[i];
+    const { svgElement, multipleRestMeasures, totalMultipleRestMeasures, stave } = note;
+    /**
+     * 兼容合并休止小节没有音符的情况,将合并的小节宽度等分,音符位置就在等分的位置
+     */
+    let bbox: any = null;
+    if (svgElement?.attrs.id) {
+      // @ts-ignore
+      bbox = document.getElementById(`vf-${svgElement?.attrs?.id}`)?.getBBox();
+      bbox = {
+        x: bbox?.x * state.zoom,
+        y: bbox?.y * state.zoom,
+        width: bbox?.width * state.zoom,
+        height: bbox?.height * state.zoom,
+      }
+    } else {
+      // @ts-ignore
+      let currentVoicesBBox: any = document.getElementById(`${stave?.attrs?.id}`)?.nextSibling?.getBBox();
+      const svgBodyBBox: any = document.getElementById('musicAndSelection')?.getBoundingClientRect();
+      if (!currentVoicesBBox && multipleRestMeasures <= totalMultipleRestMeasures) {
+        currentVoicesBBox = voicesBBox;
+      }
+      let nextIndex = i + 1;
+      while (!list[nextIndex]?.id && nextIndex < list.length) {
+        nextIndex += 1;
+      }
+      // 休止小节的开头和下一个音符之间的间距
+      let multipleWidth: any = currentVoicesBBox?.width * state.zoom;
+      if (list[nextIndex]?.id) {
+        // @ts-ignore
+        multipleWidth = document.getElementById(`${list[nextIndex]?.stave?.attrs?.id}`)?.getBBox()?.x * state.zoom - currentVoicesBBox?.x * state.zoom;
+      }
+      const ratioWidth = multipleWidth / totalMultipleRestMeasures || 0;
+      bbox = currentVoicesBBox ? {
+        bottom: currentVoicesBBox.bottom,
+        height: 30,
+        left: currentVoicesBBox.x * state.zoom + ratioWidth * (multipleRestMeasures - 1),
+        right: currentVoicesBBox.y,
+        top: currentVoicesBBox.top,
+        width: 1,
+        x: currentVoicesBBox.x * state.zoom + ratioWidth * (multipleRestMeasures - 1),
+        y: currentVoicesBBox.y,
+        svgBodyLeft: svgBodyBBox?.x,
+      } : null;
+      voicesBBox = currentVoicesBBox;
+    }
+    note.bbox = bbox;
+  }
+
+}
+
+// 一行谱模式,创建固定的光标
+export const createFixedCursor = () => {
+  if (!state.isSingleLine) return;
+  const svg: any = document.getElementById("osmdSvgPage1");
+  state.osmdSvgDom = svg;
+  const scrollDom = document.getElementById("musicAndSelection");
+  const cursorDom = document.getElementById("cursorImg-0");
+  state.fistNoteLeft = cursorDom?.getBoundingClientRect()?.left || 0;
+  state.osdmScrollDom = scrollDom;
+  state.cursorDom = cursorDom;
+  let copyCursor: any = cursorDom?.cloneNode(true);
+  if (copyCursor) {
+    copyCursor.setAttribute('id','cursor-copy');
+    copyCursor.style.position = 'sticky';
+    copyCursor.style.zIndex = '2';
+    // if (!state.times[0]?.id) {
+    //   copyCursor.style.left = state.times[0]?.bbox?.x + state.times[0]?.bbox?.width / 3 + 'px';
+    // }
+    copyCursor.style.left = state.times[0]?.bbox?.x + state.times[0]?.bbox?.width / 2 - 1 + 'px';
+    copyCursor.style.height = parseFloat(copyCursor.style.height) * 3 + 'px';
+    copyCursor.style.opacity = state.moveType === 'uniform' ? 0 : 1;
+    // copyCursor.style.background = 'red';
+    copyCursor && scrollDom?.appendChild(copyCursor);
+
+    // 创建左侧背景dom
+    // @ts-ignore
+    const firstMeasureBBox: any = document.querySelector('.vf-measure')?.getBBox();
+    const leftDom = document.createElement("div");
+    leftDom.style.width = state.times[0]?.bbox?.x - firstMeasureBBox?.x * state.zoom + 'px';
+    leftDom.style.height = firstMeasureBBox?.height * state.zoom + 'px';
+    leftDom.style.left = firstMeasureBBox?.x * state.zoom + 'px';
+    leftDom.style.transform = 'translateY(20px)';
+    leftDom.classList.add('leftNoteBg');
+    // scrollDom?.appendChild(leftDom);
+  }
+}
+
+/** 计算首尾音符的间距 */
+export const calculateDistance = () => {
+  const firstNoteBBox = state.times[0]?.bbox;
+  const lastNoteBBox = state.times.last()?.bbox;
+  if (firstNoteBBox && lastNoteBBox) {
+    const noteDistance = lastNoteBBox.x - firstNoteBBox.x + lastNoteBBox.width / 2 - firstNoteBBox.width / 2 - 1;
+    console.log('首尾间距', noteDistance)
+    state.noteDistance = noteDistance || 0;
+  }
+}
+
+/** 跳动svgdom */
+export const moveSvgDom = (skipNote?: boolean) => {
+  // const cursorLeft = state.cursorDom?.getBoundingClientRect()?.left || 0;
+  // const leftValue = parseFloat(state.cursorDom.style.left) - 47 - 20;
+  // console.log(cursorLeft,leftValue,'光标位置')
+  // state.osmdSvgDom.style.transform = `translateX(${-leftValue}px)`;
+  // state.osdmScrollDom.scrollLeft = leftValue
+  // console.log('当前音符',state.activeNoteIndex)
+  state.times.forEach((item: any, idx: number) => {
+    const svgEl = document.getElementById(`vf-${state.times[idx]?.svgElement?.attrs?.id}`)
+    const stemEl = document.getElementById(`vf-${state.times[idx]?.svgElement?.attrs?.id}-stem`)
+    if ((item.i === state.activeNoteIndex || item.id === state.times[state.activeNoteIndex].id) && item.svgElement) {
+      svgEl?.classList.add('noteActive')
+      stemEl?.classList.add('noteActive')
+    } else {
+      svgEl?.classList.remove('noteActive')
+      stemEl?.classList.remove('noteActive')
+    }
+  })
+  // document.getElementById('cursor-copy')?.classList.add('cursorAnimate');
+
+  /**
+   * 计算需要移动的距离
+   * 当前选中的音符和第一个音符之间的间距
+   */
+  if (skipNote) {
+    const distance = state.times[state.activeNoteIndex].bbox?.x - state.times[0].bbox?.x + state.times[state.activeNoteIndex].bbox?.width / 2 - state.times[0].bbox?.width / 2;
+    state.osdmScrollDom.scrollTo({
+      left: distance,
+      behavior: "smooth",
+    });
+  }
+}
+
+/** 平滑移动svgdom */
+export const smoothMoveSvgDom = () => {
+  const currentTime = getAudioCurrentTime();
+  const matchNoteIdx = state.times.findIndex((item: any) => Math.abs(item.time - currentTime) * 1000 < 100 )
+  // if (matchNoteIdx >= 0) {
+  //   console.log('匹配',matchNoteIdx,currentTime)
+  // }
+  
+  if (currentTime <= state.fixtime) return;
+  if (currentTime > state.times.last()?.time) return; 
+  // console.log('跳转音符',currentTime)
+  const currentBBox = state.times[state.activeNoteIndex]?.bbox;
+  let nextIndex = state.activeNoteIndex + 1;
+  let nextBBox = state.times[nextIndex]?.bbox;
+  while (!nextBBox && nextIndex < state.times.length) {
+    nextIndex += 1;
+    nextBBox = state.times[nextIndex]?.bbox;
+  }
+  // 下一个音符和当前播放音符之间的间距
+  let noteDistance = nextBBox?.x - state.times[state.activeNoteIndex].bbox?.x + nextBBox?.width / 4 - state.times[state.activeNoteIndex].bbox?.width / 4 || 0
+  if (noteDistance) {
+    // 当前的音符和下一个音符之间的时值
+    const noteDuration = state.times[nextIndex].time - state.times[state.activeNoteIndex]?.time;
+    // 当前时值在该区间的占比
+    const playProgress = (currentTime - state.times[state.activeNoteIndex]?.time) / noteDuration;
+    // 如果当前播放的音符是休止小节的,实际没有音符
+    // if (!state.times[state.activeNoteIndex]?.id && state.times[state.activeNoteIndex]?.multipleRestMeasures && state.times[state.activeNoteIndex+1].id) {
+    //   noteDistance = noteDistance - state.times[state.activeNoteIndex]?.bbox?.svgBodyLeft;
+    // }
+    const distance = noteDistance * playProgress;
+    
+    // 上一个音符和第一个音符的间距
+    let preDistance = state.times[state.activeNoteIndex].bbox?.x - state.times[0].bbox?.x + state.times[state.activeNoteIndex].bbox?.width / 4;
+
+    // console.log(state.activeNoteIndex,'滑动', distance, preDistance,  state.osdmScrollDom.scrollLeft, noteDistance,  nextIndex, currentTime, noteDuration )
+    // console.log('当前音符',state.activeNoteIndex,'距离',noteDistance,'比例',playProgress,'上一个距离',preDistance,'时值',currentTime, noteDuration)
+    //console.log('滑动','音符:',state.activeNoteIndex,'小节:', state.activeMeasureIndex)
+    state.osdmScrollDom.scrollLeft = distance + preDistance;
+  } else {
+    const playProgress = (currentTime - state.times[0]?.time) / state.times.last()?.time
+    const distance = state.noteDistance * playProgress;
+    state.osdmScrollDom.scrollLeft = distance;
+  }
+}
+
+// 匀速平移
+export const uniformMoveSvgDom = () => {
+  const currentTime = getAudioCurrentTime();
+  if (currentTime <= state.fixtime) return;
+  if (currentTime > state.times.last()?.time) return; 
+  const playProgress = (currentTime - state.fixtime) / state.times.last()?.time;
+  const distance = playProgress * state.noteDistance || 0;
+  state.osdmScrollDom.scrollLeft = distance;
+}

+ 58 - 0
src/style.css

@@ -81,6 +81,16 @@ body{
   color           : var(--van-primary-color);
 }
 
+.top_drag {
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 20px;
+  z-index: 1;
+  cursor: move;
+}
+
 /* 动画右滑进入 */
 .v-slide-right-enter-from,
 .v-slide-right-leave-to {
@@ -122,4 +132,52 @@ body{
   animation-duration       : 1.5s;
   animation-name           : guideKeyframes;
   animation-iteration-count: infinite;
+}
+
+@keyframes cnimate{
+  0%{
+      opacity: 0;
+  }
+  50%{
+      opacity: 0.5;
+  }
+  100%{
+      opacity: 1;
+  }
+}
+
+@keyframes noteAnimate{
+  0%{
+      scale: 0.4;
+  }
+  10%{
+    scale: 0.6;
+  }
+  20%{
+    scale: 0.8;
+  }
+  30%{
+    scale: 1;
+  }
+  40%{
+    scale: 1.2;
+  }
+  50%{
+      opacity: 1.4;
+  }
+  60%{
+      opacity: 1.6;
+  }
+  70%{
+    opacity: 1.4;
+  }
+  80%{
+    opacity: 1.2;
+  }  
+  90%{
+      opacity: 1.1;
+  }  
+  100%{
+      opacity: 1;
+  }
 }

+ 1 - 1
src/view/evaluating/index.tsx

@@ -106,7 +106,7 @@ export const evaluatingData = reactive({
 	isAudioPlayEnd: false,
 	preloadJson: true, // 预加载延迟检测的资源
 	jsonLoading: false, // 延迟检测的资源加载中状态
-	jsonLoadDone: false, // 延迟检测的动画dom加载完成
+	jsonLoadDone: false, // 延迟检测的动画dom加载完成状态
 });
 
 const sendOffsetTime = async (offsetTime: number) => {

+ 22 - 4
src/view/music-score/index.module.less

@@ -1,11 +1,13 @@
 :global {
     #musicAndSelection {
         position: relative;
-        overflow-x: hidden;
+        //overflow-x: hidden;
         overflow-y: auto;
+        overflow-x: scroll;
         height: 100%;
         max-height: 100vh;
         transform: translateY(-5%);
+        transition: all 0.5s;
         &::-webkit-scrollbar {
             width: 0;
             display: none;
@@ -15,19 +17,35 @@
             transform-origin: left top;
         }
         svg{
-            overflow: visible;
+            // overflow: visible;
         }
         #osmdCanvasPage1{
             position: absolute !important;
             left: 0;
             top: 0;
+            >svg {
+                // background: antiquewhite;
+                transition: all 0.5s;
+            }
         }
     }
+    .noteActive {
+        path {
+            fill: #FF2B29;
+            stroke: #FF2B29;
+        }
+        transform-box: fill-box;
+        transform-origin: center;
+        animation: noteAnimate 0.3s linear;
+        // transform: scale(2);
+        // transition: all 0.3s;
+    }
 }
 .inGradualRange{
    :global{
         #cursorImg-0{
-            opacity: 0 !important;
+            // opacity: 0 !important;
         }
    } 
-}
+}
+

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

@@ -2,7 +2,7 @@ import { computed, defineComponent, onMounted, reactive, ref } from "vue";
 import { formatXML, onlyVisible } from "../../helpers/formateMusic";
 // // @ts-ignore
 import { OpenSheetMusicDisplay } from "/osmd-extended/src";
-import state, { EnumMusicRenderType } from "/src/state";
+import state, { EnumMusicRenderType, IPlatform } from "/src/state";
 import Selection from "../selection";
 import styles from "./index.module.less";
 import queryString from "query-string";
@@ -89,6 +89,7 @@ export default defineComponent({
 				state.gradual = getGradualLengthByXml(xml);
 			}
 		};
+
 		const init = async () => {
 			const container = document.getElementById("musicAndSelection");
 			if (!container || !musicData.score) return;
@@ -102,6 +103,10 @@ export default defineComponent({
 				drawPartNames: props.showPartNames, // 是否渲染声轨名称
 				drawComposer: false, // 渲染作者
 				defaultColorMusic: props.musicColor, // 颜色
+				renderSingleHorizontalStaffline: state.isSingleLine ? true : false,
+				autoGenerateMultipleRestMeasuresFromRestMeasures: state.isSingleLine ? false : true, // 连续休止小节是否合并显示
+				// darkMode: true, // 暗黑模式
+				// pageFormat: 'A4_P',
 				// autoBeam: true,
 				// drawMetronomeMarks: false,
 				// drawLyricist: false,
@@ -109,11 +114,17 @@ export default defineComponent({
 				
 			});
 			// osmd.EngravingRules.CompactMode = true // 紧凑模式
-			osmd.EngravingRules.PageRightMargin = 2;
-			osmd.EngravingRules.PageTopMargin = 10;
+			osmd.EngravingRules.PageRightMargin = state.isSingleLine ? (window.innerWidth+200)/10 : 2;
+			osmd.EngravingRules.FixedMeasureWidth = state.isSingleLine ? true : false; // 是否固定小节的宽度(小节同一宽度渲染)
+			osmd.EngravingRules.PageTopMargin = state.platform === IPlatform.PC ? 9 : 10; // 老师端顶部间距
 			osmd.EngravingRules.PageTopMarginNarrow = 3;
 			osmd.EngravingRules.PageLeftMargin = 2;
-			osmd.EngravingRules.PageBottomMargin = 2;
+			// 老师端上课页面,左右两边有功能按钮,所以左右边距需要加大
+			// if (state.isAttendClass && state.platform === IPlatform.PC) {
+			// 	osmd.EngravingRules.PageLeftMargin = 7;
+			// 	osmd.EngravingRules.PageRightMargin = 7;
+			// }
+			osmd.EngravingRules.PageBottomMargin = state.platform === IPlatform.PC ? 1 : 2;
 			osmd.EngravingRules.DYMusicScoreType =
 				state.musicRenderType === EnumMusicRenderType.staff ? "staff" : "jianpu";
 			// 如果为固定调,需要加入全局

+ 155 - 0
src/view/music-score/testCheck.tsx

@@ -0,0 +1,155 @@
+import { computed, defineComponent, onMounted, reactive, ref } from "vue";
+import { formatXML, onlyVisible } from "../../helpers/formateMusic";
+// // @ts-ignore
+import { OpenSheetMusicDisplay } from "/osmd-extended/src";
+import state, { EnumMusicRenderType } from "/src/state";
+import Selection from "../selection";
+import styles from "./index.module.less";
+import queryString from "query-string";
+import { getGradualLengthByXml } from "/src/helpers/calcSpeed";
+import { resetFormate, resetGivenFormate, setGlobalMusicSheet } from "/src/helpers/customMusicScore"
+import { setGlobalData } from "/src/utils";
+
+export const musicRenderTypeKey = "musicRenderType";
+let osmd: any = null;
+const musicData = reactive({
+	showSelection: false, // 可以加载点击浮层
+	isRenderLoading: true,
+	score: "",
+	containerWidth: 0,
+});
+
+/** 重新计算曲谱渲染比例 */
+export const resetMusicScore = () => {
+	const contaienrWidth = document.getElementById("musicAndSelection")?.offsetWidth || 625;
+	state.musicZoom = contaienrWidth / musicData.containerWidth;
+	
+};
+
+/** 重新渲染曲谱 */
+export const resetRenderMusicScore = (type?: string) => {
+	const search = queryString.parse(location.search);
+	const newSearch = queryString.stringify({
+		...search,
+		_t: Date.now(),
+		musicRenderType: type
+	});
+	location.search = "?" + newSearch;
+};
+
+export default defineComponent({
+	name: "music-score",
+	emits: ["rendered"],
+	props: {
+		/** 是否渲染选择框 */
+		showSelection: {
+			type: Boolean,
+			default: true,
+		},
+		renderTypeKey: {
+			type: String,
+			default: "",
+		},
+		musicColor: {
+			type: String,
+			default: "",
+		},
+		/** 是否渲染声轨名称 */
+		showPartNames: {
+			type: Boolean,
+			default: false,
+		},
+	},
+	setup(props, { emit }) {
+        // 解析报错的xml
+        const errorList: any = [];
+        const checkRender = async (list?: any) => {
+            let errorNum = 0;
+            for (let i = 0; i < list.length; i++) {
+                const item = list[i];
+                try {
+                    await getXML(item.evxml_file_url);
+                    await init(i);
+                } catch (error) {
+                    errorNum += 1;
+                    errorList.push(item.evxml_file_url);
+                    console.log('🚀 ~ evxml解析报错:',`第${i}个xml`,error,'总错误数:',errorNum)
+                }
+            }
+            console.log('🚀 ~ evxml循环完成','没有times和timegap的:',state.noTimes,'解析报错的xml:',errorList)
+        }
+		/** 设置 曲谱模式,五线谱还是简谱 */
+		const setRenderType = () => {
+			const musicRenderType: any = sessionStorage.getItem(props.renderTypeKey || musicRenderTypeKey);
+			if (musicRenderType in EnumMusicRenderType) {
+				state.musicRenderType = musicRenderType;
+			}
+		};
+		const getXML = async (evxml: any) => {
+			const res = await fetch(evxml).then((response) => response?.text());
+            if (res) {
+                const xml = formatXML(res, evxml);
+                musicData.score = state.isCombineRender ? xml : onlyVisible(xml, state.partIndex);
+                if (state.gradualTimes) {
+                    state.gradual = getGradualLengthByXml(xml);
+                }
+            }
+		};
+
+		const init = async (idx: any) => {
+			const container = document.getElementById("testCheck");
+			if (!container || !musicData.score) return;
+			setGlobalMusicSheet();
+			osmd = new OpenSheetMusicDisplay(container, {
+				drawTitle: false,
+				drawSubtitle: false,
+				// drawMeasureNumbers: false,
+				autoResize: false,
+				followCursor: false,
+				drawPartNames: props.showPartNames, // 是否渲染声轨名称
+				drawComposer: false, // 渲染作者
+				defaultColorMusic: props.musicColor, // 颜色
+				renderSingleHorizontalStaffline: state.isSingleLine ? true : false,
+				autoGenerateMultipleRestMeasuresFromRestMeasures: state.isSingleLine ? false : true, // 连续休止小节是否合并显示
+				// darkMode: true, // 暗黑模式
+				// pageFormat: 'A4_P',
+				// autoBeam: true,
+				// drawMetronomeMarks: false,
+				// drawLyricist: false,
+				// ...this.opotions,
+				
+			});
+			// osmd.EngravingRules.CompactMode = true // 紧凑模式
+			osmd.EngravingRules.PageRightMargin = state.isSingleLine ? (window.innerWidth+200)/10 : 2;
+			osmd.EngravingRules.FixedMeasureWidth = state.isSingleLine ? true : false; // 是否固定小节的宽度(小节同一宽度渲染)
+			osmd.EngravingRules.PageTopMargin = 10;
+			osmd.EngravingRules.PageTopMarginNarrow = 3;
+			osmd.EngravingRules.PageLeftMargin = 2;
+			osmd.EngravingRules.PageBottomMargin = 2;
+			osmd.EngravingRules.DYMusicScoreType = "staff";
+			osmd.EngravingRules.DYMusicScoreId = state.examSongId || ''
+			await osmd.load(musicData.score);
+			osmd.zoom = state.zoom;
+			osmd.render();
+			console.log("🚀 ~ evxml渲染完:", `第${idx}个xml`)
+            container.innerHTML = "";
+			
+		};
+		/** 获取渲染容器的宽度 */
+		const getContainerWidth = () => {
+			musicData.containerWidth = 625;
+			// console.log(musicData.containerWidth)
+		};
+		onMounted(async () => {
+			getContainerWidth();
+            state.isEvxml = true
+            // checkRender(xmlList2);
+		});
+		return () => (
+			<div
+				id="testCheck"
+			>
+			</div>
+		);
+	},
+});

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

@@ -63,6 +63,16 @@
         //height: 80% !important;
       }
     }
+    &.pcPicker {
+      :global {
+        .van-ellipsis {
+          font-size: 20PX;
+        }
+        .van-picker-column__item {
+          height: 60px;
+        }
+      }
+    }
   }
 
   .button {
@@ -72,4 +82,38 @@
     position: relative;
     z-index: 9;
   }
+  &.pcContainer {
+    width: 500PX;
+    height: 380PX;
+    border-radius: 16PX;
+    .button {
+      width: 40%;
+      margin-bottom: 30PX;
+    }
+    .title {
+      font-size: 20PX;
+    }
+    .closeIcon {
+      margin: initial !important;
+      top: -6PX;
+    }
+    .top {
+      padding-left: 30PX;
+      &::before { 
+        left: 20PX;
+      }
+    }
+    :global {
+      .van-button__content {
+        font-size: 20PX;
+      }
+    }
+  }
+}
+.pcPartTop {
+  z-index: 9999;
+  height: 12px;
+  &.pcPartTopZIndex {
+    z-index: 1;
+  }
 }

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

@@ -1,6 +1,7 @@
 import { PropType, computed, defineComponent, ref, toRefs, onMounted } from 'vue'
 import { Picker, Button, Icon } from 'vant'
 import styles from './index.module.less'
+import state, { IPlatform } from "/src/state";
 
 export default defineComponent({
   name: 'choosePartName',
@@ -36,7 +37,8 @@ export default defineComponent({
 			// console.log(myPicker.value,99999,selValues.value,props.partIndex)
 		});
     return () => (
-      <div class={styles.container}>
+      <div class={[styles.container, state.platform === IPlatform.PC && styles.pcContainer]}>
+        { state.platform === IPlatform.PC && <div class={[!state.guideInfo?.teacherDrag && styles.pcPartTopZIndex ,styles.pcPartTop,'top_drag']}></div> }
         <div class={styles.top}>
           <div class={styles.title}>请选择您练习的乐器</div>
           {/* <Icon name="cross" size={24} onClick={() => emit('close')} /> */}
@@ -44,7 +46,7 @@ export default defineComponent({
         </div>
         <Picker
           ref={myPicker}
-          class={styles.picker}
+          class={[styles.picker, state.platform === IPlatform.PC && styles.pcPicker]}
           defaultIndex={props.partIndex}
           v-model={selValues.value}
           showToolbar={false}
@@ -57,7 +59,7 @@ export default defineComponent({
           }}
         />
         <Button class={styles.button} type="primary" round block onClick={() => {
-            console.log(1111,selectIndex.value)
+            // console.log(1111,selectIndex.value)
             if (partIndexChanged.value) {
               emit('close', selectIndex.value)
             } else {

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

@@ -12,4 +12,7 @@
 .popup{
     border-radius: 4px;
     overflow: hidden;
+}
+.pcPartPop {
+    overflow: initial !important;
 }

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

@@ -1,11 +1,15 @@
-import { computed, defineComponent, reactive } from 'vue'
+import { computed, defineComponent, reactive, toRef } from 'vue'
 import styles from './index.module.less'
 import { Icon, Popup } from 'vant'
 import ChoosePartName from './choosePartName'
-import state, { togglePlay } from "/src/state";
+import state, { togglePlay, IPlatform } from "/src/state";
 import qs from 'query-string'
 import { getInstrumentName, sortMusical } from "/src/constant/instruments";
 import { getQuery } from "/src/utils/queryString";
+import useDrag from "/src/view/plugins/useDrag/index";
+import Dragbom from "/src/view/plugins/useDrag/dragbom";
+import { setGuidance } from "/src/page-instrument/custom-plugins/guide-page/api";
+import { storeData } from "/src/store";
 
 export const toggleMusicSheet = reactive({
   show: false,
@@ -21,7 +25,6 @@ export default defineComponent({
 
     const partListNames = computed(() => {
       let partList = state.partListNames || []
-      console.log(777777,state.partListNames)
       partList = partList.filter((item: any) => !item?.toLocaleUpperCase()?.includes('COMMON'))
       const arr =  partList.map((item: any, index: number) => {
         // 该声轨能否被选
@@ -79,8 +82,32 @@ export default defineComponent({
       location.href = _url
     }
 
+    const parentClassName = "switchBoxClass_drag";
+    const userId = storeData.user?.id ? String(storeData.user?.id) : '';
+    const positionInfo = state.platform !== IPlatform.PC ? {
+      styleDrag: { value: null }
+    } : useDrag(
+      [
+        `${parentClassName} .top_drag`,
+        `${parentClassName} .bom_drag`
+      ],
+      parentClassName,
+      toRef(toggleMusicSheet, 'show'),
+      userId
+    )
+
+    // 完成拖动弹窗引导页
+    const handleGuide = async () => {
+      state.guideInfo.teacherDrag = true;
+			try{
+				const res = await setGuidance({guideTag:'guideInfo',guideValue:JSON.stringify(state.guideInfo)})
+      }catch(e){
+        console.log(e)
+      }   
+    }
+
     return () => (
-      <Popup class={styles.popup} v-model:show={toggleMusicSheet.show}>
+      <Popup class={styles.popup} v-model:show={toggleMusicSheet.show} class={[state.platform === IPlatform.PC && styles.pcPartPop ,"switchBoxClass_drag"]} style={positionInfo.styleDrag.value}>
         <ChoosePartName
           partIndex={trackIdx.value || 0}
           partListNames={partListNames.value}
@@ -92,6 +119,7 @@ export default defineComponent({
             }
           }}
         />
+        { state.platform === IPlatform.PC && <Dragbom showGuide={!state.guideInfo?.teacherDrag} onGuideDone={handleGuide}  /> }
       </Popup>
     )
   },

+ 59 - 0
src/view/plugins/useDrag/dragbom.tsx

@@ -0,0 +1,59 @@
+import { defineComponent, computed, reactive, onMounted, nextTick } from 'vue';
+import styles from './index.module.less';
+// 底部拖动区域
+export default defineComponent({
+  name: 'dragBom',
+  emits: ["guideDone"],
+	props: {
+		/** 是否显示引导 */
+		showGuide: {
+			type: Boolean,
+			default: false,
+		},
+	},
+  setup(props, { emit }) {
+    const data = reactive({
+      guidePos: "bottom" as "bottom" | "left" | "right",
+    });
+
+    const initGuidePos = () => {
+      const pageHeight = document.documentElement.clientHeight || document.body.clientHeight;
+      const pageWidth = document.documentElement.clientWidth || document.body.clientWidth;
+      const guideHeight = document.querySelector('.bom_guide')?.clientHeight || 0;
+      const guideWidth = document.querySelector('.bom_guide')?.clientWidth || 0;
+      const dragBBox = document.querySelector('.bom_drag')?.getBoundingClientRect();
+      const dragTop = dragBBox?.top || 0;
+      const dragLeft = dragBBox?.left || 0;
+      // 引导页出现在下边
+      if (pageHeight - dragTop > guideHeight) {
+        data.guidePos = "bottom"
+      } else {
+        // 引导页出现在左边or右边
+        data.guidePos = dragLeft > guideWidth ? "left" : "right"
+      }
+    }
+    onMounted(() => {
+      nextTick(() => {
+        setTimeout(() => {
+          initGuidePos();
+        }, 0);
+      });
+    });
+    return () => (
+      <>
+        <div class={[styles.dragBom, 'bom_drag']}>
+          <div class={styles.box}></div>
+          <div class={[styles.box, styles.right]}></div>
+        </div>
+        {
+          props.showGuide && 
+          <div class={[styles.guide, data.guidePos === "left" && styles.guideLeft, data.guidePos === "right" && styles.guideRight, 'bom_guide']} onClick={() => emit("guideDone")}>
+            <div class={styles.guideBg}></div>
+            <div class={styles.guideDone}></div>
+          </div>          
+        }
+
+      </>
+    );
+  }
+});

BIN
src/view/plugins/useDrag/img/left.png


BIN
src/view/plugins/useDrag/img/modalDragBg.png


BIN
src/view/plugins/useDrag/img/modalDragBg2.png


BIN
src/view/plugins/useDrag/img/modalDragBgLeft.png


BIN
src/view/plugins/useDrag/img/modalDragBgRight.png


BIN
src/view/plugins/useDrag/img/modalDragDone.png


BIN
src/view/plugins/useDrag/img/right.png


+ 85 - 0
src/view/plugins/useDrag/index.module.less

@@ -0,0 +1,85 @@
+.dragBom {
+  width: 100%;
+  height: 10px;
+  display: flex;
+  justify-content: space-between;
+  // border-radius: 0 0 8px 8px;
+  //overflow: hidden;
+  position: absolute;
+  bottom: 0;
+  .box {
+    position: relative;
+    bottom: 10px;
+    width: 20px;
+    height: 20px;
+    background: url('./img/left.png') no-repeat;
+    background-size: 100% 100%;
+    border-bottom-left-radius: 16Px;
+    &.right {
+      background: url('./img/right.png') no-repeat;
+      background-size: 100% 100%;
+      border-bottom-right-radius: 16Px;
+    }
+  }
+}
+.guide {
+  position: absolute;
+  left: 0;
+  top: calc(100% - 10px);
+  &::before {
+    content: "";
+    display: block;
+    position: fixed;
+    left: -100vw;
+    top: -100vh;
+    z-index: 9;
+    width: 200vw;
+    height: 200vh;
+    background: rgba(0,0,0,0.2);
+  }
+  .guideBg {
+    position: relative;
+    z-index: 99;
+    width: 200px;
+    height: 102px;
+    background: url('./img/modalDragBg.png') no-repeat;
+    background-size: 100% 100%;
+  }
+  .guideDone {
+    position: absolute;
+    z-index: 99;
+    left: 34.6%;
+    top: 72.2%;
+    width: 50px;
+    height: 20px;
+    background: url('./img/modalDragDone.png') no-repeat;
+    background-size: 100% 100%;
+    cursor: pointer;
+  }
+  &.guideTop {
+    top: initial;
+    bottom: 2px;
+    .guideBg {
+      background: url('./img/modalDragBg2.png') no-repeat;
+      background-size: 100% 100%;
+    }
+  }
+  &.guideLeft {
+    top: initial;
+    left: -180px;
+    bottom: -5px;
+    .guideBg {
+      background: url('./img/modalDragBgLeft.png') no-repeat;
+      background-size: 100% 100%;
+    }
+  }
+  &.guideRight {
+    top: initial;
+    left: calc(100% - 12px);
+    bottom: -5px;
+    .guideBg {
+      background: url('./img/modalDragBgRight.png') no-repeat;
+      background-size: 100% 100%;
+    }
+  }
+}

+ 163 - 0
src/view/plugins/useDrag/index.ts

@@ -0,0 +1,163 @@
+// 弹窗拖动
+import { ref, Ref, watch, nextTick, computed } from 'vue';
+
+type posType = {
+  top: number;
+  left: number;
+};
+
+/**
+ * @params classList  可拖动地方的class值,也为唯一值
+ * @params boxClass  容器class值必须为唯一值,这个class和useid拼接 作为缓存主键
+ * @params dragShow  弹窗是否显示
+ * @params userId    当前用户id
+ */
+export default function useDrag(
+  classList: string[],
+  boxClass: string,
+  dragShow: Ref<boolean>,
+  userId: string
+) {
+  const pos = ref<posType>({
+    top: -1, // -1 为初始值 代表没有缓存 默认居中
+    left: -1
+  });
+  const useIdDargClass = userId + boxClass;
+  watch(dragShow, () => {
+    if (dragShow.value) {
+      // 初始化pos值
+      initPos();
+      window.addEventListener('resize', refreshPos);
+      nextTick(() => {
+        const boxClassDom = document.querySelector(
+          `.${boxClass}`
+        ) as HTMLElement;
+        if (!boxClassDom) {
+          return;
+        }
+        classList.map((className: string) => {
+          const classDom = document.querySelector(
+            `.${className}`
+          ) as HTMLElement;
+          if (classDom) {
+            classDom.style.cursor = 'move';
+            drag(classDom, boxClassDom, pos);
+          }
+        });
+      });
+    } else {
+      window.removeEventListener('resize', refreshPos);
+      setCachePos(useIdDargClass, pos.value);
+    }
+  });
+  const styleDrag = computed(() => {
+    // 没有设置拖动的时候保持原本的
+    return pos.value.left === -1 && pos.value.top === -1
+      ? {}
+      : {
+          position: 'fixed',
+          left: `${pos.value.left}px`,
+          top: `${pos.value.top}px`,
+          transform: 'initial',
+          transformOrigin: 'initial',
+          margin: 'initial',
+          transition: 'initial'
+        };
+  });
+  function initPos() {
+    const posCache = getCachePos(useIdDargClass);
+    // 有缓存 用缓存的值,没有缓存用默认
+    if (posCache) {
+      pos.value = posCache;
+      nextTick(() => {
+        refreshPos();
+      });
+    }
+  }
+  function refreshPos() {
+    if (pos.value.left === -1 && pos.value.top === -1) {
+      return;
+    }
+    const boxClassDom = document.querySelector(`.${boxClass}`) as HTMLElement;
+    if (!boxClassDom) return;
+    const parentElementRect = boxClassDom.getBoundingClientRect();
+    const clientWidth = document.documentElement.clientWidth;
+    const clientHeight = document.documentElement.clientHeight;
+    const { top, left } = pos.value;
+    const maxLeft = clientWidth - parentElementRect.width;
+    const maxTop = clientHeight - parentElementRect.height;
+    let moveX = left;
+    let moveY = top;
+    const minLeft = 0;
+    const minTop = 0;
+    moveX = moveX < minLeft ? minLeft : moveX > maxLeft ? maxLeft : moveX;
+    moveY = moveY < minTop ? minTop : moveY > maxTop ? maxTop : moveY;
+    pos.value = {
+      top: moveY,
+      left: moveX
+    };
+  }
+  return {
+    pos,
+    styleDrag
+  };
+}
+
+// 拖动
+function drag(el: HTMLElement, parentElement: HTMLElement, pos: Ref<posType>) {
+  function mousedown(e: MouseEvent) {
+    const parentElementRect = parentElement.getBoundingClientRect();
+    const downX = e.clientX;
+    const downY = e.clientY;
+    const clientWidth = document.documentElement.clientWidth;
+    const clientHeight = document.documentElement.clientHeight;
+    const maxLeft = clientWidth - parentElementRect.width;
+    const maxTop = clientHeight - parentElementRect.height;
+    const minLeft = 0;
+    const minTop = 0;
+    function onMousemove(e: MouseEvent) {
+      let moveX = parentElementRect.left + (e.clientX - downX);
+      let moveY = parentElementRect.top + (e.clientY - downY);
+      moveX = moveX < minLeft ? minLeft : moveX > maxLeft ? maxLeft : moveX;
+      moveY = moveY < minTop ? minTop : moveY > maxTop ? maxTop : moveY;
+      pos.value = {
+        top: moveY,
+        left: moveX
+      };
+    }
+    function onMouseup() {
+      document.removeEventListener('mousemove', onMousemove);
+      document.removeEventListener('mouseup', onMouseup);
+    }
+    document.addEventListener('mousemove', onMousemove);
+    document.addEventListener('mouseup', onMouseup);
+  }
+  el.addEventListener('mousedown', mousedown);
+}
+
+// 缓存
+const localStorageName = 'dragCachePos';
+function getCachePos(useIdDargClass: string): null | undefined | posType {
+  const localCachePos = localStorage.getItem(localStorageName);
+  if (localCachePos) {
+    try {
+      return JSON.parse(localCachePos)[useIdDargClass];
+    } catch {
+      return null;
+    }
+  }
+  return null;
+}
+function setCachePos(useIdDargClass: string, pos: posType) {
+  const localCachePos = localStorage.getItem(localStorageName);
+  let cachePosObj: Record<string, any> = {};
+  if (localCachePos) {
+    try {
+      cachePosObj = JSON.parse(localCachePos);
+    } catch {
+      //
+    }
+  }
+  cachePosObj[useIdDargClass] = pos;
+  localStorage.setItem(localStorageName, JSON.stringify(cachePosObj));
+}

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

@@ -21,6 +21,12 @@
     background-color: var(--active-stave-box) !important;
 }
 
+.singleLineSelection {
+    .staveBox {
+        opacity: 0;
+    }
+}
+
 .leftStaveBox {
     background-color: var(--active-stave-box);
 

+ 6 - 3
src/view/selection/index.tsx

@@ -151,7 +151,7 @@ const calcNoteData = () => {
 			}
 		}
 	}
-	console.log("🚀 ~ selectData.notes:", selectData.notes, selectData.staves);
+	// console.log("🚀 ~ selectData.notes:", selectData.notes, selectData.staves);
 };
 
 /** 重新计算 */
@@ -230,7 +230,10 @@ export default defineComponent({
 		return () => (
 			<div
 				id="selectionBox"
-				class={styles.selectionContainer}
+				class={[
+					styles.selectionContainer,
+					!state.sectionStatus && state.isSingleLine ? styles.singleLineSelection : ''
+				]}
 				onClick={(e: Event) => e.stopPropagation()}
 			>
 				{selectData.staves.map((item: any) => {
@@ -260,7 +263,7 @@ export default defineComponent({
 										styles.position,
 										showClass.value(item),
 										scoreItem ? `scoreItemLeve${scoreItem.leve}` : "",
-										state.platform === IPlatform.PC ? styles.linePC : '',
+										(state.platform === IPlatform.PC && state.zoom > 0.8) ? styles.linePC : '',
 									]}
 									style={item.staveBox}
 									onClick={() => handleSelection(item)}