Przeglądaj źródła

Merge branch 'feature-tianyong-newVersion' into ktyq-online-new

TIANYONG 11 miesięcy temu
rodzic
commit
e083c68dc8

+ 1 - 1
osmd-extended

@@ -1 +1 @@
-Subproject commit 9b847e5f475415bbeaba33e736fc5c563a9eead6
+Subproject commit a9aa056f79fb3aa710a6586b2064241a6636bc1c

+ 25 - 8
src/helpers/customMusicScore.ts

@@ -320,11 +320,11 @@ export const resetFormate = () => {
 			.forEach((t) => {
 				// console.log('文本123',t.textContent,'是否在可视区域内',isElementInViewport(t))
 				// D.C循环标记不在可视区域内,需要修复移动其位置信息
-				if (t.textContent?.includes('D.C')) {
-					if (!isElementInViewport(t)) {
-						t.style.transform = `translateX(-40px)`;
-					}
-				}
+				// if (t.textContent?.includes('D.C')) {
+				// 	if (!isElementInViewport(t)) {
+				// 		t.style.transform = `translateX(-40px)`;
+				// 	}
+				// }
 				vfbeams.forEach((curve) => {
 					const result = collisionDetection(t, curve);
 					const prePath: SVGAElement = t?.previousSibling as unknown as SVGAElement;
@@ -595,8 +595,8 @@ export const resetFormate = () => {
 		if (!state.isCreateImg && !state.isPreView) {
 			staves.forEach((stave: any,i: number) => {
 				const list = [
-					Array.from(stave?.getElementsByTagName("text") || []),
 					Array.from(stave?.querySelectorAll(".vf-StaveSection") || []),
+					Array.from(stave?.getElementsByTagName("text") || []),
 					Array.from(stave?.querySelectorAll(".vf-Volta") || []),
 					Array.from(stave?.querySelectorAll(".vf-clef") || []),
 					Array.from(stave?.querySelectorAll(".vf-keysignature") || []),
@@ -625,10 +625,13 @@ export const resetFormate = () => {
 				const customG = `<g>${rect}${rectBottom}</g>`
 				try {
 					if (list.length) {
-						list.forEach((_el: any) => {
+						for(const _el of list) {
+							if (_el?.parentElement?.classList?.contains('vf-StaveSection')) {
+								continue;
+							}
 							stave?.appendChild(_el)
 							_el?.style?.removeProperty("display");
-						});
+						}
 					}
 				} catch (error) {}
 				stave.innerHTML = customG + stave.innerHTML;
@@ -655,6 +658,20 @@ export const resetFormate = () => {
 			// }
 		});
 
+		// 修复D.C、D.S等渲染位置不对的问题
+		const repairWord = ["D.S.", "D.C.", "Fine"];
+		[...vfmeasures].forEach((measure: any) => {
+			const needRepairTexts = measure.querySelectorAll('text').length ? Array.from(measure.querySelectorAll('text'))?.filter((item: any) => repairWord.includes(item?.textContent)) : [];
+			if (needRepairTexts.length) {
+				// 该小节结束位置的x坐标
+				const measureCoordinate = measure?.querySelector('.vf-custom-bg')?.getBBox() || null
+				const measureEndX = measureCoordinate ? measureCoordinate?.x + measureCoordinate?.width - 30 : 0;
+				needRepairTexts.forEach((text: any) => {
+					text?.setAttribute('x', measureEndX)
+				})
+			}
+		});
+
 	}
 	if (!state.isCombineRender && state.isSingleLine) {
 		transSinglePage();

+ 5 - 0
src/helpers/formateMusic.ts

@@ -672,6 +672,11 @@ export const formatXML = (xml: string, xmlUrl?: string): string => {
 		if (minute.textContent && measureSpeed) {
 			speeds.push(Number(measureSpeed))
 		}
+		// if (hasSpeedDot && measureSpeed) {
+		// 	minute.textContent = measureSpeed
+		// 	const dotDom = minute?.parentElement.querySelector('beat-unit-dot')
+		// 	minute?.parentElement?.removeChild(dotDom)
+		// }
 	}
 	speeds = [...new Set(speeds)]
 	const hasVaryingSpeed = speeds.length > 1 ? true : false

+ 2 - 2
src/helpers/metronome.ts

@@ -286,9 +286,9 @@ class Metronome {
 			const measureListIndex = note?.noteElement?.sourceMeasure?.measureListIndex;
 			if (measureNumberXML > -1) {
 				if (measureNumberXML != xmlNumber) {
-					// 弱起的时候 根据音符结尾时间减去音符开头时间,得到的不是正常小节的时间,然后平均分配节拍之后,当前节拍间隔会非常短 这里弱起拿正常
+					// 弱起的时候 根据音符结尾时间减去音符开头时间,得到的不是正常小节的时间,然后平均分配节拍之后,当前节拍间隔会非常短 这里弱起取整个小节的时间
 					let startTime = note.measures[0].time
-					if(i === 0){
+					if(i === 0 && note.measures[0].difftime>0){
 						startTime = note.measures[note.measures.length - 1].endtime - note.measures[0].measureLength
 					}
 					const m = {

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

@@ -690,7 +690,9 @@ export default defineComponent({
           {/* 返回和标题 */}
           {!(state.playState == "play" || followData.start || evaluatingData.startBegin) && (
             <div id="noticeBarRollDom" class={styles.headTopLeftBox}>
-              <img src={iconBack} class={["headTopBackBtn", styles.img, !headTopData.showBack && styles.hidenBack]} onClick={handleBack} />
+              {
+                !query.isMove && <img src={iconBack} class={["headTopBackBtn", styles.img, !headTopData.showBack && styles.hidenBack]} onClick={handleBack} />
+              }
               {smoothAnimationState.isShow.value || state.isCombineRender ? (
                 <div
                   style={

+ 5 - 3
src/page-instrument/header-top/settting/index.tsx

@@ -3,7 +3,7 @@ import styles from "./index.module.less"
 import { headImg } from "../image";
 import { headTopData } from "../index"
 import { Switch, showToast, Field, Popup, Slider } from "vant";
-import state, { refreshMusicSvg, IPlatform } from "/src/state"
+import state, { refreshMusicSvg, IPlatform, checkMoveNoSave } from "/src/state"
 import { api_closeCamera, api_openCamera, api_savePicture } from "/src/helpers/communication";
 import { smoothAnimationState} from "/src/page-instrument/view-detail/smoothAnimation"
 import Recommendation from "../../custom-plugins/helper-model/recommendation";
@@ -208,10 +208,11 @@ export default defineComponent({
                                 <div class={styles.radioBox}>
                                     {
                                         [{name:'单行谱',value:true},{name:'多行谱',value:false}].map(item=>{
-                                            return <div class={ state.isSingleLine===item.value && styles.active } onClick={ ()=>{ 
+                                            return <div class={ state.isSingleLine===item.value && styles.active } onClick={ async ()=>{ 
                                                 if(state.isSingleLine === item.value){
                                                     return
                                                 }
+                                                await checkMoveNoSave();
                                                 headTopData.settingMode = false
                                                 // resetRenderMusicScore(state.musicRenderType)
                                                 const _time = setTimeout(() => {
@@ -233,10 +234,11 @@ export default defineComponent({
                                 <div class={styles.radioBox}>
                                     {
                                         notationList.value.map(item=>{
-                                            return <div class={ state.musicRenderType===item.value && styles.active } onClick={ ()=>{ 
+                                            return <div class={ state.musicRenderType===item.value && styles.active } onClick={ async ()=>{ 
                                                 if(state.musicRenderType === item.value){
                                                     return
                                                 }
+                                                await checkMoveNoSave();
                                                 headTopData.settingMode = false
                                                 // resetRenderMusicScore(state.musicRenderType)
                                                 const _time = setTimeout(() => {

+ 110 - 66
src/state.ts

@@ -1,4 +1,4 @@
-import { closeToast, showToast } from "vant";
+import { closeToast, showToast, showConfirmDialog } from "vant";
 import { nextTick, reactive, watch } from "vue";
 import { OpenSheetMusicDisplay } from "../osmd-extended/src";
 import { metronomeData } from "./helpers/metronome";
@@ -20,6 +20,8 @@ import { storeData } from "/src/store";
 import { downloadXmlStr } from "./view/music-score"
 import { musicScoreRef, headerColumnHide } from "/src/page-instrument/view-detail/index"
 import { headTopData } from "/src/page-instrument/header-top/index";
+import { api_lessonTrainingTrainingStudentDetail } from "/src/page-instrument/api"
+import { undoData, moveData } from "/src/view/plugins/move-music-score"
 
 const query: any = getQuery();
 
@@ -1355,13 +1357,23 @@ const getMusicInfo = async (res: any) => {
   if (state.isEvaluatReport) {
     partIndex = state.partIndex;
   }
+  // 布置作业 取作业的乐器id
+  const workRecord = query.workRecord
+  let workRecordInstrumentId:undefined | string
+  if(workRecord){
+    const res = await api_lessonTrainingTrainingStudentDetail(workRecord);
+    if (res?.code === 200) {
+      workRecordInstrumentId = res.data?.instrumentId
+    }
+  }
   /* 获取声轨列表 */
   let xmlString = await fetch(res.data.xmlFileUrl).then((response) => response.text());
   xmlString = xmlAddPartName(xmlString);
   downloadXmlStr.value = xmlString //给musice-score 赋值xmlString 以免加载2次
   const tracks = xmlToTracks(xmlString) //获取声轨列表
   // 设置音源  track 为当前的声轨 index为当前的
-  const { track, index, musicalInstrumentId } = state.isSimplePage ? { track:tracks[0], index:0, musicalInstrumentId: '' } : initMusicSource(res.data, tracks, partIndex)
+  const { track, index, musicalInstrumentId } = state.isSimplePage ? { track:tracks[0], index:0, musicalInstrumentId: '' } : initMusicSource(res.data, tracks, partIndex, workRecordInstrumentId)
+  // 这里返回的track可能和实际的对不上,所以重新筛选一下
   const realTrack = musicalInstrumentId && res.data?.musicalInstruments?.length ? res.data?.musicalInstruments.find((item: any) => item?.id == musicalInstrumentId)?.code?.split(',')?.[0] : '';
   const musicInfo = {
     ...res.data,
@@ -1383,9 +1395,9 @@ function xmlToTracks(xmlString: string) {
   }, []);
 }
 // 设置音源
-function initMusicSource(data: any, tracks: string[], partIndex: number) {
+function initMusicSource(data: any, tracks: string[], partIndex: number, workRecordInstrumentId?: string) {
   let track:string,index:number, musicalInstrumentId: string
-  const instrumentId = query.instrumentId || storeData.user?.instrumentId
+  const instrumentId = workRecordInstrumentId || query.instrumentId || storeData.user?.instrumentId
   let { musicSheetType, isAllSubject, musicSheetSoundList, musicSheetAccompanimentList } = data
   musicSheetSoundList || (musicSheetSoundList = [])
   musicSheetAccompanimentList || (musicSheetAccompanimentList = [])
@@ -1395,27 +1407,15 @@ function initMusicSource(data: any, tracks: string[], partIndex: number) {
     accompanyObj = musicSheetAccompanimentList.find((item: any) => {
       return item.audioPlayType === "PLAY"
     })
-    // 适用声部(isAllSubject)为true 时候没有乐器只有一个原音;当前用户有乐器就匹配  不然取第一个原音
+    // 是否全声部(isAllSubject)为true 时候没有乐器只有一个原音(比如节奏练习,这个曲子全部乐器都支持);当前用户有乐器就匹配  不然取第一个原音
     musicObj = musicSheetSoundList.find((item: any) => {
-      return (isAllSubject || !instrumentId) ? item.audioPlayType === "PLAY" : (item.audioPlayType === "PLAY" && item.musicalInstrumentId == instrumentId)
+      return isAllSubject ? item.audioPlayType === "PLAY" : (item.audioPlayType === "PLAY" && item.musicalInstrumentId == instrumentId)
     })
-    // 因为可能根据学生的乐器id也找不到曲目所以尝试取第一个
-    if(!musicObj){
+    // 当没有找到原音的时候,并且instrumentId没有值的时候,取默认第一个乐器
+    if(!musicObj && !instrumentId){
       musicObj = musicSheetSoundList.find((item: any) => {
         return item.audioPlayType === "PLAY"
       })
-      // 取第一个时候,不播放演奏文件切换为演唱模式  总控平台排除这个逻辑
-      if(!query.isCbs){
-        state.playType = "sing"
-        if(musicObj) {
-          musicObj.audioFileUrl = null
-          musicObj.audioBeatMixUrl = null
-        }
-        if(accompanyObj) {
-          accompanyObj.audioFileUrl = null
-          accompanyObj.audioBeatMixUrl = null
-        }
-      }
     }
     fanSongObj = musicSheetSoundList.find((item: any) => {
       return item.audioPlayType === "SING"
@@ -1435,14 +1435,16 @@ function initMusicSource(data: any, tracks: string[], partIndex: number) {
         // 总谱渲染
         state.isCombineRender = true
         state.partListNames = tracks
-        // 总谱演唱模式是 范唱,取scoreAudioFileUrl字段
-        fanSongObj = musicSheetAccompanimentList.find((item: any) => {
+        banSongObj = musicSheetAccompanimentList.find((item: any) => {
           return item.audioPlayType === "SING"
         })
+        // 总谱演唱模式是 范唱,取banSongObj 里面的scoreAudioFileUrl字段
         // 先取scoreAudioFileUrl的值 如果 没有就是空
-        if(fanSongObj){
-          fanSongObj.audioFileUrl = fanSongObj.scoreAudioFileUrl
-          fanSongObj.audioBeatMixUrl = fanSongObj.scoreAudioBeatMixUrl
+        if(banSongObj){
+          fanSongObj = {
+            audioFileUrl: banSongObj.scoreAudioFileUrl,
+            audioBeatMixUrl: banSongObj.scoreAudioBeatMixUrl
+          }
         }
         // 总谱演奏模式是 伴奏
         accompanyObj = musicSheetAccompanimentList.find((item: any) => {
@@ -1524,44 +1526,56 @@ const setState = (data: any, index: number) => {
   const localStoragePlayType = localStorage.getItem("musicScorePlayType")
   if(localStoragePlayType) {
     localStorage.removeItem("musicScorePlayType")
-    state.playType = localStoragePlayType as any
-  }
-    // 根据当前文件有没有 设置当前的播放模式
-  if(state.playType === "play"){
-    if(state.music){
-      state.playSource = "music"
-    }else if(state.accompany){
-      state.playSource = "background"
+    const fields = localStoragePlayType.split(',')
+    state.playType = fields[0] as any
+    state.playSource = fields[1] as any
+  }
+  // 根据当前文件有没有 设置当前的播放模式
+  const playObj = {
+    "play_music":"music",
+    "play_background":"accompany",
+    "sing_music":"fanSong",
+    "sing_background":"banSong",
+    "sing_mingSong":"mingSong",
+  } as any
+  // 当前缓存 有值的时候 用这个,没有的时候 走筛选
+  // @ts-ignore
+  if(!state[playObj[`${state.playType}_${state.playSource}`]]){
+    if(state.playType === "play"){
+      if(state.music){
+        state.playSource = "music"
+      }else if(state.accompany){
+        state.playSource = "background"
+      }else{
+        if(state.fanSong){
+          state.playType = "sing"
+          state.playSource = "music"
+        }else if(state.banSong){
+          state.playType = "sing"
+          state.playSource = "background"
+        }else if(state.mingSong){
+          state.playType = "sing"
+          state.playSource = "mingSong"
+        }
+      }
     }else{
       if(state.fanSong){
-        state.playType = "sing"
         state.playSource = "music"
       }else if(state.banSong){
-        state.playType = "sing"
         state.playSource = "background"
       }else if(state.mingSong){
-        state.playType = "sing"
         state.playSource = "mingSong"
-      }
-    }
-  }else{
-    if(state.fanSong){
-      state.playSource = "music"
-    }else if(state.banSong){
-      state.playSource = "background"
-    }else if(state.mingSong){
-      state.playSource = "mingSong"
-    }else{
-      if(state.music){
-        state.playType = "play"
-        state.playSource = "music"
-      }else if(state.accompany){
-        state.playType = "play"
-        state.playSource = "background"
+      }else{
+        if(state.music){
+          state.playType = "play"
+          state.playSource = "music"
+        }else if(state.accompany){
+          state.playType = "play"
+          state.playSource = "background"
+        }
       }
     }
   }
-
   state.appName = "COLEXIU";
   state.detailId = data.bizId;
   state.xmlUrl = data.xmlFileUrl;
@@ -1576,12 +1590,12 @@ const setState = (data: any, index: number) => {
   /**
    * 单曲,指法根据用户当前的乐器来显示,如果没有则取musicSheetSoundList第一个track
    */
-  const currentInstrumentId = query.instrumentId || storeData.user?.instrumentId;
-  let musicalCode = !currentInstrumentId ? data.musicSheetSoundList?.find((item:any)=>{ return item.audioPlayType === "PLAY" })?.track || '' : data.musicSheetSoundList?.find((item: any) => item?.musicalInstrumentId == currentInstrumentId && item.audioPlayType === "PLAY")?.track || '';
-  const pitchSubject = musicalInstrumentCodeInfo.find((n) => n.code.toLocaleLowerCase() === subjectCode.toLocaleLowerCase())
-  const pitchMusical = musicalInstrumentCodeInfo.find((n) => n.code.toLocaleLowerCase() === musicalCode.toLocaleLowerCase())
-  state.subjectCodeId = pitchSubject ? pitchSubject.id : 0
-  state.musicalCodeId = pitchMusical ? pitchMusical.id : 0
+  // const currentInstrumentId = query.instrumentId || storeData.user?.instrumentId;
+  // let musicalCode = !currentInstrumentId ? data.musicSheetSoundList?.find((item:any)=>{ return item.audioPlayType === "PLAY" })?.track || '' : data.musicSheetSoundList?.find((item: any) => item?.musicalInstrumentId == currentInstrumentId && item.audioPlayType === "PLAY")?.track || '';
+  // const pitchSubject = musicalInstrumentCodeInfo.find((n) => n.code.toLocaleLowerCase() === subjectCode.toLocaleLowerCase())
+  // const pitchMusical = musicalInstrumentCodeInfo.find((n) => n.code.toLocaleLowerCase() === musicalCode.toLocaleLowerCase())
+  // state.subjectCodeId = pitchSubject ? pitchSubject.id : 0
+  // state.musicalCodeId = pitchMusical ? pitchMusical.id : 0
   state.categoriesId = data.musicCategoryId;
   state.categoriesName = data.musicTagNames;
   // state.enableEvaluation = data.isEvaluated ? true : false;
@@ -1685,13 +1699,13 @@ const setState = (data: any, index: number) => {
    * 能否转谱:先取isConvertibleScore字段,如果isConvertibleScore为true,则取musicalInstruments字段匹配的当前分轨的transferFlag,都为true则可以转谱
    * 
    */
-  let pitchTrack = null
-  if (state.isConcert) {
-    musicalCode = musicalInstrumentCodeInfo.find((item: any) => item.id === state.musicalCodeId)?.code
-    pitchTrack = data.musicalInstruments?.find((item: any) => item.code?.split(',')[0] === musicalCode)
-  } else {
-    pitchTrack = data.musicalInstruments?.find((item: any) => item.code?.split(',')[0] === musicalCode)
-  }
+  // let pitchTrack = null
+  // if (state.isConcert) {
+  //   musicalCode = musicalInstrumentCodeInfo.find((item: any) => item.id === state.musicalCodeId)?.code
+  //   pitchTrack = data.musicalInstruments?.find((item: any) => item.code?.split(',')[0] === musicalCode)
+  // } else {
+  //   pitchTrack = data.musicalInstruments?.find((item: any) => item.code?.split(',')[0] === musicalCode)
+  // }
   let musicalRenderType = ''
   // if (pitchTrack?.defaultScore) {
   //   musicalRenderType = pitchTrack?.defaultScore === 'STAVE' ? 'staff' : pitchTrack?.defaultScore === 'JIAN' ? 'fixedTone' : pitchTrack?.defaultScore === 'FIRST' ? 'firstTone' : ''
@@ -2086,8 +2100,38 @@ watch(
   }
 )
 
+// 后台编辑谱面模式,切换谱面时,如果有操作没有保存,需要给出提示
+export const checkMoveNoSave = async () => {
+  return new Promise((resolve, reject) => {
+    if (query.isMove) {
+      if (moveData.open && undoData.undoList.length) {
+        showConfirmDialog({
+          className: "noSaveModal",
+          title: "温馨提示",
+          message: "您有新的修改还未保存,切换谱面后本次编辑的内容将不会保存",
+        }).then(() => {
+          moveData.open = false
+          resolve(true)
+        }).catch(() => {
+          return;
+        });
+      } else {
+        moveData.open = false
+        undoData.undoList = []
+        resolve(true)
+      }
+    } else {
+      resolve(true)
+    }
+  });
+
+
+}
+
+
 /** 刷新谱面 */
 export const refreshMusicSvg = () => {
+  moveData.modelList = []
   clearSelection();
   resetBaseRate();
   state.activeMeasureIndex = -1;

+ 4 - 0
src/style.css

@@ -123,6 +123,10 @@ body {
   transition: all 0.3s;
 }
 
+.noSaveModal {
+  transform: scale(0.8) translateY(-50%);
+}
+
 /* 引导动画 */
 @keyframes guideKeyframes {
   0% {

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

@@ -9,10 +9,10 @@ import queryString from "query-string";
 import { getGradualLengthByXml } from "/src/helpers/calcSpeed";
 import { resetFormate, resetGivenFormate, setGlobalMusicSheet, limitSingleSvgPageHeight } from "/src/helpers/customMusicScore"
 import { setGlobalData } from "/src/utils";
-import Loading from "/src/view/audio-list/loading"
 import { storeData } from "/src/store";
 import { isLoadingCss } from "/src/page-instrument/view-detail/loadingCss"
 import HorizontalDragScroll from './HorizontalDragScroll';
+import { getQuery } from "/src/utils/queryString";
 
 export const musicRenderTypeKey = "musicRenderType";
 let osmd: any = null;
@@ -63,6 +63,7 @@ export default defineComponent({
 		},
 	},
 	setup(props, { emit, slots, expose }) {
+		const query: any = getQuery();
 		/** 设置 曲谱模式,五线谱还是简谱 */
 		const setRenderType = () => {
 			const musicRenderType: any = sessionStorage.getItem(props.renderTypeKey || musicRenderTypeKey);
@@ -186,7 +187,7 @@ export default defineComponent({
 			await init();
 			musicData.isRenderLoading = false;
 			// pc 端支持 拖动滚动
-			if(state.platform === "PC"){
+			if(state.platform === "PC" || query.isCbs){
 				const container = document.querySelector('#musicAndSelection') as HTMLElement;
 				horizontalDragScroll = new HorizontalDragScroll(container);
 			}
@@ -252,12 +253,12 @@ export default defineComponent({
 					state.isSingleLine && "singleLineMusicBox",
 					(!state.isCreateImg && !state.isPreView && state.musicRenderType === EnumMusicRenderType.staff) ? "blueMusicXml" : "",
 					state.isSingleLine && state.playState ==="play" && styles.notTouch,
-					!state.isSingleLine && state.platform === "PC" &&  styles.pcCursorGrab
+					!state.isSingleLine && (state.platform === "PC" || query.isCbs) &&  styles.pcCursorGrab
 
 				]}
 			>
 				{slots.default?.()}
-				{props.showSelection && musicData.showSelection && !state.isPreView && !state.isEvaluatReport &&!state.isSimplePage && <Selection />}
+				{props.showSelection && musicData.showSelection && !state.isPreView && !state.isEvaluatReport &&!state.isSimplePage && state.musicRendered && <Selection />}
 			</div>
 		);
 	},

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


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


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


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


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


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


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


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


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


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

@@ -54,4 +54,87 @@
     cursor: pointer;
     transition: all 0.5s;
     transform: rotate(180deg);
+}
+
+.editToolBox {
+    position: fixed;
+    left: 0;
+    top: 0;
+    width: 100%;
+    background: rgba(19, 36, 64, 0.5);
+    z-index: 999999;
+    display: flex;
+    align-items: center;
+    justify-content: flex-end;
+    padding: 14PX 30PX;
+    pointer-events: none;
+    min-height: 58PX;
+    .editItem {
+        display: flex;
+        align-items: center;
+        padding: 5PX 12PX;
+        background: rgba(255,255,255,0.2);
+        border-radius: 20PX;
+        margin-left: 18PX;
+        cursor: pointer;
+        pointer-events: all;
+        &:active {
+            opacity: .5;
+        }
+        img {
+            width: 18PX;
+            height: 18PX;
+            margin-right: 6PX;
+        }
+        span {
+            font-size: 14PX;
+            color: #fff;
+        }
+    }
+    .extraItem {
+        margin-left: 18PX;
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        padding: 5PX 12PX;
+        background: rgba(255,255,255,0.2);
+        border-radius: 20PX;
+        position: relative;
+        width: 76PX;
+        box-sizing: border-box;
+        img {
+            width: 18PX;
+            height: 18PX;
+            cursor: pointer;
+            &:active {
+                opacity: .5;
+            }
+        }
+        &::before {
+            content: "";
+            position: absolute;
+            left: 50%;
+            top: 50%;
+            transform: translate(-50%,-50%);
+            width: 1PX;
+            height: 20PX;
+            background: rgba(255,255,255,0.3);
+            z-index: 1;
+        }
+    }
+    .disabled {
+        opacity: .5;
+        pointer-events: none;
+    }
+}
+
+.itemDisabled {
+    .editItem {
+        opacity: .5;
+        pointer-events: none;
+    }
+    .canEdit {
+        opacity: 1;
+        pointer-events: visible;
+    }
 }

+ 84 - 15
src/view/plugins/move-music-score/index.tsx

@@ -1,5 +1,5 @@
-import { Row, showToast } from "vant";
-import { defineComponent, onMounted, reactive, nextTick, ref } from "vue";
+import { Row, showToast, showConfirmDialog } from "vant";
+import { defineComponent, onMounted, onUnmounted, reactive, nextTick, ref } from "vue";
 import state from "/src/state";
 import request from "/src/utils/request";
 import { getQuery } from "/src/utils/queryString";
@@ -10,6 +10,14 @@ import "@varlet/ui/es/button-group/style";
 import "@varlet/ui/es/switch/style";
 import { storeData } from "/src/store";
 import rightHideIcon from './image/right_hide_icon.png';
+import editIcon from './image/edit.png';
+import editCloseIcon from './image/edit_close.png';
+import editSaveIcon from './image/edit_save.png';
+import editPreIcon from './image/edit_pre.png';
+import editDeleteIcon from './image/edit_delete.png';
+import editResetIcon from './image/edit_reset.png';
+import editReduceIcon from './image/edit_reduce.png';
+import editAddIcon from './image/edit_add.png';
 
 let extStyleConfigJson: any = {};
 const clientWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
@@ -70,9 +78,10 @@ function initSvgId() {
 	if (!svg) return;
 	const vfstavetempo: HTMLElement[] = Array.from(svg.querySelectorAll(".vf-stavetempo"));
 	const vftext: HTMLElement[] = Array.from(svg.querySelectorAll(".vf-text"));
-	const vfstaveSection: HTMLElement[] = []; //Array.from(svg.querySelectorAll(".vf-StaveSection"));
+	const vfstaveSection: HTMLElement[] = Array.from(svg.querySelectorAll(".vf-StaveSection"));
 	const vfRepetition: HTMLElement[] = Array.from(svg.querySelectorAll(".vf-Repetition"));
 	const vflineGroup: HTMLElement[] = Array.from(svg.querySelectorAll(".vf-lineGroup"));
+	// console.log('速度标记',vfstavetempo)
 	let tempIndex = 1;
 	[...vfstavetempo].forEach((ele) => {
 		setEleId(ele, "temp" + tempIndex);
@@ -80,6 +89,7 @@ function initSvgId() {
 	});
 	let textIndex = 1;
 	[...vftext].forEach((ele) => {
+		// console.log(ele.textContent,textIndex)
 		setEleId(ele, "text" + textIndex);
 		textIndex++;
 	});
@@ -112,10 +122,10 @@ function setEleId(ele: HTMLElement, eleId: string) {
 	if (!id) {
 		ele.setAttribute("id", eleId);
 	}
-	createModelBox(ele as any);
+	createModelBox(ele as any, eleId as any);
 }
 
-function createModelBox(ele: SVGAElement) {
+function createModelBox(ele: SVGAElement, eleId?: any) {
 	const musicContainer = document.getElementById("musicAndSelection")?.getBoundingClientRect() || { x: 0, y: 0 };
 	const parentLeft = musicContainer.x || 0;
 	const parentTop = musicContainer.y || 0;
@@ -128,7 +138,7 @@ function createModelBox(ele: SVGAElement) {
 	};
 	const type = ele.getAttribute("class");
 	moveData.modelList.push({
-		id: ele.getAttribute("id"),
+		id: eleId || ele.getAttribute("id"),
 		bbox,
 		type,
 		isMove: false,
@@ -159,6 +169,22 @@ function getBox(ele: SVGAElement) {
 	};
 }
 
+// 切换开关
+const switchMoveState = () => {
+	// 如果编辑过,没有保存,点击取消状态,需要提醒用户是否取消
+	if (moveData.open && undoData.undoList.length) {
+		showConfirmDialog({
+			className: "noSaveModal",
+			title: "温馨提示",
+			message: "您有新的修改还未保存,取消后本次编辑的内容将不会保存",
+		}).then(() => {
+			moveData.open = false
+		});
+	} else {
+		moveData.open = !moveData.open
+	}
+}
+
 // 过滤数据
 export const filterMoveData = async () => {
 	const examSongId = state.examSongId;
@@ -216,6 +242,9 @@ export const filterMoveData = async () => {
 		});
 		if (res && res.code == 200) {
 			showToast("保存成功");
+			undoData.undoList = [];
+			undoData.activeItem = null;
+			state.extStyleConfigJson = JSON.stringify(extStyleConfigJson)
 		}
 		clearActiveModel();
 	}
@@ -462,6 +491,7 @@ const handleUndo = () => {
 
 /** 根据移动数据渲染 */
 export const renderForMoveData = () => {
+	if (state.isSingleLine || state.musicRenderType !== 'staff') return; 
 	if (state.extStyleConfigJson) {
 		try {
 			extStyleConfigJson = JSON.parse(state.extStyleConfigJson);
@@ -514,7 +544,7 @@ export const renderForMoveData = () => {
 					// index = targetIndex + 1
 					// item.id = `text${index}`
 					index = targetIndex
-					item.id = `text${targetIndex+1}`
+					item.id = `text${targetIndex}`
 				}
 				// console.log(66666666,index)
 				if (index > -1) {
@@ -544,14 +574,25 @@ export default defineComponent({
 			// 	initSvgId();
 			// }
 			// renderForMoveData();
+			moveData.modelList = []
 			nextTick(() => initNoteCoord())
+			// const hasToolDom = Array.from(document.body.children)?.some((item: any) => item?.id === 'toolBox')
+			// if (!hasToolDom) {
+			// 	const toolBox = document.getElementById("toolBox");
+			// 	toolBox && document.body.appendChild(toolBox);
+			// }
 			const toolBox = document.getElementById("toolBox");
 			toolBox && document.body.appendChild(toolBox);
 		});
+		onUnmounted(() => {
+			moveData.modelList = []
+			const toolBox = document.getElementById("toolBox");
+			toolBox && document.body.removeChild(toolBox);
+		})
 		return () => (
 			<div class={[moveData.open ? "" : styles.moveDisabled]}>
 				<div id="toolBox">
-					<div class={[styles.toolBox, !showToolBox.value && styles.hideTool]} >
+					{/* <div class={[styles.toolBox, !showToolBox.value && styles.hideTool]} >
 						<Switch v-model={moveData.open} />
 						{moveData.open && (
 							<>
@@ -561,12 +602,6 @@ export default defineComponent({
 										<Button onClick={() => handleAddAndSub('sub')}>减</Button>
 									</ButtonGroup>
 								)}
-								{/* <ButtonGroup size="small">
-									
-									<Button>
-										<Icon name="arrow-down" style={{ transform: "rotate(-90deg)" }} />
-									</Button>
-								</ButtonGroup> */}
 								<Button size="small" onClick={handleUndo} disabled={undoData.undoList.length ? false : true}>
 									<Icon name="arrow-down" style={{ transform: "rotate(90deg)" }} />
 								</Button>
@@ -592,7 +627,41 @@ export default defineComponent({
 							class={[styles.rightHideIcon, !showToolBox.value ? styles.rightIconShow : '']} 
 							src={rightHideIcon}
 							onClick={() => showToolBox.value = true } />
-					}  
+					}   */}	
+					<div class={[styles.editToolBox, !moveData.open && styles.itemDisabled]}>		
+						{
+							state.musicRenderType === 'staff' && !state.isSingleLine && 
+							<>
+								<div class={[styles.editItem, styles.canEdit]} onClick={switchMoveState}>
+									<img src={moveData.open ? editCloseIcon : editIcon} />
+									<span>{moveData.open ? '取消' : '编辑'}</span>
+								</div>
+								<div class={styles.editItem} onClick={filterMoveData}>
+									<img src={editSaveIcon} />
+									<span>保存</span>
+								</div>
+								<div class={[styles.editItem, !undoData.undoList.length && styles.disabled]} onClick={handleUndo}>
+									<img src={editPreIcon} />
+									<span>撤回</span>
+								</div>
+								<div class={[styles.editItem, moveData.activeIndex <= -1 && styles.disabled]} onClick={handleDeleteMoveNote}>
+									<img src={editDeleteIcon} />
+									<span>{moveData.modelList[moveData.activeIndex]?.isDelete ? '回显' : '删除'}</span>
+								</div>
+								<div class={styles.editItem} onClick={resetMoveNote}>
+									<img src={editResetIcon} />
+									<span>重置</span>
+								</div>
+								{
+									moveData.tool.isAddAndSub && 
+									<div class={styles.extraItem}>
+										<img src={editReduceIcon} onClick={() => handleAddAndSub('sub')} />
+										<img src={editAddIcon} onClick={() => handleAddAndSub('add')} />
+									</div>								
+								}		
+							</>						
+						}										
+					</div>		
 				</div>
 				{moveData.modelList.map((item: any, index: number) => {
 					return (

+ 33 - 39
src/view/plugins/toggleMusicSheet/choosePartName/index.tsx

@@ -1,7 +1,7 @@
 import { PropType, computed, defineComponent, ref, toRefs, onMounted, watch, nextTick } from 'vue'
 import { Picker, Button, Icon } from 'vant'
 import styles from './index.module.less'
-import state, { IPlatform } from "/src/state";
+import state, { IPlatform, checkMoveNoSave } from "/src/state";
 import changeName from "./imgs/changeName.png"
 import { headImg } from "/src/page-instrument/header-top/image";
 import { toggleMusicSheet } from "../index"
@@ -11,7 +11,7 @@ export default defineComponent({
   name: 'choosePartName',
   props: {
     partListNames: {
-      type: Array as PropType<string[]>,
+      type: Array as PropType<any[]>,
       default: () => [],
     },
     partIndex: {
@@ -21,37 +21,41 @@ export default defineComponent({
   },
   emits: ['close'],
   setup(props, { emit }) {
-    // #9463 bug,未更换声轨点击确定不应该重新加载,现在会导致切换错误
-    const partIndexChanged = ref(false);
-    const { partListNames, partIndex } = toRefs(props)
-    let idx = partListNames.value.findIndex((item: any) => item.value === partIndex.value);
-    idx = idx > -1 ? idx : 0;
-    const selectIndex = ref(idx);
-    const columns = computed(() => {
-      return partListNames.value
-    })
-    // console.log(1111,partListNames.value, partIndex.value, selectIndex.value, columns.value, 999999)
-    /**
-     * 默认选中的
-     * picker组件,3.x的版本可以使用defaultIndex,4.x的版本只能使用v-model传递
-     * */ 
-    const selValues = ref([partIndex.value]);
+    const selValues = ref([props.partIndex]);
     const myPicker = ref();
-    onMounted(() => {
-			// console.log(myPicker.value,99999,selValues.value,props.partIndex)
-		});
-
     watch(
       () => toggleMusicSheet.show,
       () => {
         if (toggleMusicSheet.show) {
-          selectIndex.value = partIndex.value
-          selValues.value = [partIndex.value]
+          selValues.value = [props.partIndex]
         }
-        //console.log('声轨',selValues.value,partIndex.value,selectIndex.value)
       }
     );
-
+    watch(() => toggleMusicSheet.show, ()=>{
+        // 支持滚轮事件
+        if (toggleMusicSheet.show) {
+          nextTick(() => {
+            myPicker.value.$el.addEventListener('wheel', handleWheel)
+          })
+        } else {
+            myPicker.value.$el.removeEventListener('wheel', handleWheel)
+        }
+      },{immediate:true}
+    )
+    function handleWheel(e: WheelEvent){
+      e.preventDefault()
+      // 先停止 惯性滚动
+      myPicker.value.confirm()
+      const direction = e.deltaY > 0 ? 1 : -1
+      const targetObject = myPicker.value.getSelectedOptions(0)[0]
+      const index = props.partListNames.findIndex(
+          obj => obj == targetObject
+      )
+      const newIndex = index + direction
+      if (newIndex >= 0 && newIndex < props.partListNames.length) {
+        selValues.value = [props.partListNames[newIndex].value]
+      }
+    }
     return () => (
       <div class={[styles.container, state.platform === IPlatform.PC && styles.pcContainer, styles[state.modeType]]}>
         <div class={[styles.head, "top_draging"]}>
@@ -63,27 +67,17 @@ export default defineComponent({
             <Picker
               ref={myPicker}
               class={[styles.picker, state.platform === IPlatform.PC && styles.pcPicker]}
-              defaultIndex={props.partIndex}
               v-model={selValues.value}
               showToolbar={false}
-              columns={columns.value}
+              columns={props.partListNames}
               visible-option-num={5}
               option-height={"1.06666rem"}
-              onChange={(row) => {
-                console.log(1111,'选择的索引', row)
-                if (!partIndexChanged.value) partIndexChanged.value = true
-                selectIndex.value = row.selectedValues[0]
-              }}
             />
-            <img src={ okBtn } class={styles.button} onClick={() => {
+            <img src={ okBtn } class={styles.button} onClick={async () => {
+                await checkMoveNoSave();
                 myPicker.value.confirm()
                 nextTick(()=>{
-                  // console.log(1111,selectIndex.value)
-                  if (partIndexChanged.value) {
-                    emit('close', selectIndex.value)
-                  } else {
-                    emit('close', partIndex.value)
-                  }
+                  emit('close', selValues.value[0])
                 })
               }
             }></img>

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

@@ -37,7 +37,9 @@ export default defineComponent({
           sortId,
           canselect
         }
-      }).filter((item: any) => item.canselect).sort((a: any, b: any) => a.sortId - b.sortId)
+      }).filter((item: any) => item.canselect)
+      // 不需要自定义排序,改为按照xml声轨顺序显示
+      // .sort((a: any, b: any) => a.sortId - b.sortId)
       // 支持总谱渲染的时候 加上总谱字段
       state.isScoreRender && arr.unshift({canselect:true, sortId:999, text: "总谱", value: 999})
       return arr
@@ -70,7 +72,7 @@ export default defineComponent({
         },
       })
       // 存储当前 模式
-      localStorage.setItem("musicScorePlayType", state.playType)
+      localStorage.setItem("musicScorePlayType", `${state.playType},${state.playSource}`)
       const _url =
         location.origin +
         location.pathname +

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

@@ -255,7 +255,10 @@ export default defineComponent({
 			// 初始化谱面可移动的元素位置
 			try {
 			moveData.partIndex = state.partIndex + ""
-			nextTick(() => renderForMoveData())
+			// 速度标记元素和谱面并非同时渲染,初始化可移动元素的时候,需要加个延迟
+			setTimeout(() => {
+				renderForMoveData()
+			}, 0);
 			} catch (error) {}
 		});
 		return () => (