Browse Source

多声轨选中指定的声轨渲染,可以指定声轨播放对应的原音

黄琪勇 1 month ago
parent
commit
ac207b8998

+ 4 - 2
src/helpers/formateMusic.ts

@@ -913,7 +913,9 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 	let differFrom = 0;
 	// let testIdx = 0;
 	let repeatIdx = 0; // 循环的次数
-	const firstTrackName = state.canSelectTracks[0] || "";
+	// 当多选声部的时候 ,取选择的第一个声部
+	const firstTrackName = state.combinePartIndexs.length>1 ? state.partListNames[state.combinePartIndexs[0]] : state.canSelectTracks[0] || "";
+	const currentTrackIndex = state.isCombineRender && state.combinePartIndexs.length > 1 ? state.combinePartIndexs[0] : 0;
 	while (!iterator.EndReached) {
 		// console.log({ ...iterator });
 		/** 多声轨合并显示,当前音符的时值取所有声轨中的最小值 */
@@ -1108,7 +1110,7 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 				note.sourceMeasure.verticalMeasureList = note.sourceMeasure?.verticalMeasureList.filter((item: any) => state.canSelectTracks.includes(item?.parentStaff?.parentInstrument.Name?.trim()))
 			}
 
-			activeVerticalMeasureList = [note.sourceMeasure?.verticalMeasureList?.[0]] || [];
+			activeVerticalMeasureList = [note.sourceMeasure?.verticalMeasureList?.[currentTrackIndex]] || [];
 			// 某些情况下,合并显示的妙极客曲子,note.sourceMeasure?.verticalMeasureList可能为空数组
 			if (state.isCombineRender && state.isEvxml && note.sourceMeasure?.verticalMeasureList.length === 0) {
 				activeVerticalMeasureList = osmd.GraphicSheet.MeasureList.find((item: any) => item[0]?.MeasureNumber === note.sourceMeasure.MeasureNumberXML) || [];

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

@@ -10,7 +10,7 @@ import Speed from "./speed";
 import { evaluatingData, handleStartEvaluat } from "/src/view/evaluating";
 import Settting from "./settting";
 import state, { IPlatform, handleChangeSection, handleResetPlay, handleRessetState, togglePlay, IPlayState, refreshMusicSvg, EnumMusicRenderType } from "/src/state";
-import { getAudioCurrentTime } from "/src/view/audio-list";
+import { audioData, getAudioCurrentTime } from "/src/view/audio-list";
 import { followData, toggleFollow } from "/src/view/follow-practice";
 import { api_back } from "/src/helpers/communication";
 import MusicType from "./music-type";
@@ -377,6 +377,8 @@ export default defineComponent({
       if (state.modeType === "follow") return { display: false, disabled: false };
       // 评测开始 禁用
       if (state.modeType === "evaluating") return { display: false, disabled: true };
+      // 总谱渲染在播放过程中 不能切换 
+      if(state.isCombineRender && state.playState === "play") return { display: true, disabled: true } 
       if (!state.isAppPlay) {
         // 播放过程中不能切换
         if (state.playState === "play") {
@@ -916,6 +918,11 @@ export default defineComponent({
                   }
                 }
                 await handlerModeChange(oldPlayType, oldPlaySource);
+                // 总谱 并且开启了单个声轨音频时候
+                if(state.isCombineRender && state.playSource === "background") {
+                  audioData.combineIndex = -1
+                  state.music = ""
+                }
                 showToast({
                   message: state.playType === "play" ? (state.playSource === "music" ? "已切换为原声" : state.playSource === "background" ? "已切换为伴奏" : "已切换为唱名") : state.playSource === "music" ? "已切换为范唱" : state.playSource === "background" ? "已切换为伴唱" : "已切换为唱名",
                   position: "top",

+ 48 - 7
src/state.ts

@@ -371,6 +371,8 @@ const state = reactive({
   track: "",
   /** 当前显示声部索引 */
   partIndex: 0,
+  /** 总谱渲染时候 只显示部分声部的值 */
+  combinePartIndexs:[],
   /** 演奏是否需要节拍器 */
   needTick: false,
   /** 演唱模式是否需要节拍器 */
@@ -1402,9 +1404,30 @@ const getMusicInfo = async (res: any) => {
   state.isScoreRender = res.data?.isScoreRender
   // 是否默认显示总谱
   state.defaultScoreRender = res.data?.defaultScoreRender
+  /* 获取声轨列表 */
+  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) //获取声轨列表
   // 是否显示节拍器
   state.isMixBeat = res.data?.isMixBeat
-  let partIndex = query["part-index"] ? parseInt(query["part-index"]) : -1 // -1为partIndex没有值的时候
+  /* 设置partIndex */
+  let partIndexs = query["part-index"] ? query["part-index"].split(",") : ["-1"] // -1为partIndex没有值的时候
+  // 如果传入的是part-name,需要将part-name转换成part-index
+  if (query["part-name"]) {
+    const partValue = decodeURIComponent(query["part-name"]) || ''
+    let nameIdx = tracks.findIndex((item: any) => item == partValue)
+    partIndexs = [nameIdx]
+  }  
+  partIndexs = partIndexs.map((indexStr:string) => {
+    return parseInt(indexStr)
+  }).sort((a, b) => a - b);
+  let partIndex = partIndexs[0]
+  // 当partIndexs 大于1个的时候,代表用户自己选择了多个声部,用总谱渲染的逻辑
+  if(partIndexs.length > 1){
+    partIndex = 999
+    state.combinePartIndexs = partIndexs
+  }
   // 如果是评测报告,会有默认的分轨index
   if (state.isEvaluatReport) {
     partIndex = state.partIndex;
@@ -1421,11 +1444,12 @@ const getMusicInfo = async (res: any) => {
   // multiTracksSelection 返回为空,默认代表全部分轨
   state.canSelectTracks = res.data.multiTracksSelection === "null" || res.data.multiTracksSelection === "" || res.data.multiTracksSelection === null ? [] : res.data.multiTracksSelection?.split(',');
   state.canSelectTracks = state.canSelectTracks.map((item: any)=>item.trim())
-  /* 获取声轨列表 */
-  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) //获取声轨列表
+  // 如果是多个分轨合并显示的,需要记录下所选分轨的第一个分轨的名字,渲染计算音符位置的时候需要根据第一个分轨找到对应音符的位置
+  if (state.combinePartIndexs.length) {
+    (window as any).DYFirstTrackName = tracks[state.combinePartIndexs[0]] || '';
+  } else {
+    (window as any).DYFirstTrackName = '';
+  }
   // 设置音源  track 为当前的声轨 index为当前的
   const { track, index, musicalInstrumentId } = state.isSimplePage ? { track:tracks[0], index: state.partIndex, musicalInstrumentId: '' } : initMusicSource(res.data, tracks, partIndex, workRecordInstrumentId)
   // 这里返回的track可能和实际的对不上,所以重新筛选一下
@@ -1487,7 +1511,7 @@ function initMusicSource(data: any, tracks: string[], partIndex: number, workRec
   } else {
     /* 合奏 */
     // 支持总谱 并且当前是总谱。partIndex是999时候,或者默认是总谱并且partIndex为-1时候  -1就是partIndex没有值
-    if(state.isScoreRender && (partIndex===999 || (state.defaultScoreRender && partIndex===-1))){
+    if(state.isScoreRender && (partIndex===999 || (state.defaultScoreRender && partIndex===-1)) || state.combinePartIndexs.length > 1){
         // 总谱渲染
         state.isCombineRender = true
         banSongObj = musicSheetAccompanimentList.find((item: any) => {
@@ -1501,6 +1525,23 @@ function initMusicSource(data: any, tracks: string[], partIndex: number, workRec
             audioBeatMixUrl: banSongObj.scoreAudioBeatMixUrl
           }
         }
+        // 总谱 需要播放各个声部的音频
+        if(state.combinePartIndexs.length) {
+          // 当选择多个分轨时候
+          state.combinePartIndexs.map( partI => {
+            const musicSheetSound = musicSheetSoundList.find((item:any)=>{
+              return item.track?.toLowerCase().trim() === tracks[partI]?.toLowerCase().trim()
+            })
+            musicSheetSound?.audioFileUrl && (audioData.combineMusics[partI] = musicSheetSound.audioFileUrl)
+          })
+        }else{
+          tracks.map((itemTrack:any, partI:number) => {
+            const musicSheetSound = musicSheetSoundList.find((item:any)=>{
+              return item.track?.toLowerCase().trim() === itemTrack?.toLowerCase().trim()
+            })
+            musicSheetSound?.audioFileUrl && (audioData.combineMusics[partI] = musicSheetSound.audioFileUrl)
+          })
+        }
         // 总谱演奏模式是 伴奏
         accompanyObj = musicSheetAccompanimentList.find((item: any) => {
           return item.audioPlayType === "PLAY"

+ 144 - 53
src/view/audio-list/index.tsx

@@ -18,6 +18,7 @@ import Crunker from "/src/utils/crunker"
 import tickMp3 from "/src/assets/tick.wav"
 import tockMp3 from "/src/assets/tock.wav"
 import { metronomeData } from "/src/helpers/metronome";
+import { showToast } from "vant"
 
 export const audioData = reactive({
 	songEle: null as HTMLAudioElement | null, // 原生
@@ -44,7 +45,10 @@ export const audioData = reactive({
 		mingSongGirlEle: null as HTMLAudioElement | null,
 		beatMingSongEle: null as HTMLAudioElement | null,
 		beatMingSongGirlEle: null as HTMLAudioElement | null
-	}
+	},
+	combineIndex: -1, // 当前 播放的总谱音频索引
+	combineMusics: {} as Record<string, any>, // 音频 url
+	combineMusicEles:[] as {key:number, value:HTMLAudioElement, beatValue:HTMLAudioElement|null}[] // 存储的音频el 当大于4个时候删除
 });
 const midiRef = ref();
 /** 播放或暂停 */
@@ -207,8 +211,78 @@ export const changeMingSongType = () =>{
 	}
 }
 
-// 处理加载节拍器音频
+const createAudio = (src?: string): Promise<HTMLAudioElement | null> => {
+	if(!src){
+		return Promise.resolve(null)
+	}
+	return new Promise((resolve) => {
+		const a = new Audio(src + '?v=' + Date.now());
+		a.onloadedmetadata = () => {
+			resolve(a);
+		};
+		a.onerror = () => {
+			resolve(null);
+		};
+		// 当未加载 资源之前 切换到其他浏览器标签,浏览器可能会禁止资源加载所以无法触发onloadedmetadata事件,导致一直在加载中,这里做个兼容
+		if (document.visibilityState === 'visible') {
+			a.load();
+		} else {
+			const onVisibilityChange = () => {
+				if (document.visibilityState === 'visible') {
+					document.removeEventListener('visibilitychange', onVisibilityChange);
+					a.load();
+				}
+			};
+			document.addEventListener('visibilitychange', onVisibilityChange);
+		}
+	});
+};
+
+// 合成节拍器资源
 let CrunkerInstance: Crunker
+async function mergeBeatAudio(music?:string){
+	let beatMusic
+	if(!state.isMixBeat) {
+		return beatMusic
+	}
+	if(!music){
+		return beatMusic
+	}
+	console.time("音频合成时间")
+	try{
+		/* 音频合成 */
+		if(!CrunkerInstance){
+			CrunkerInstance = new Crunker()
+		}
+		console.time("音频加载时间")
+		const [musicBuff, tickBuff, tockBuff] = await CrunkerInstance.fetchAudio(music?`${music}?v=${Date.now()}`:undefined, tickMp3, tockMp3)
+		console.timeEnd("音频加载时间")
+		// 计算音频空白时间
+		const silenceDuration = musicBuff&&!state.isEvxml ? CrunkerInstance.calculateSilenceDuration(musicBuff) : 0
+		console.log(`音频空白时间:${silenceDuration}`)
+		const beats:AudioBuffer[] = []
+		const beatsTime:number[] = []
+		metronomeData.metroMeasure.map(measures=>{
+			measures.map((item:any)=>{
+				beats.push(item.isTick?tickBuff!:tockBuff!)
+				beatsTime.push(item.time + silenceDuration) // xml 计算的时候 加上空白的时间
+			})
+		})
+		console.time("音频合并时间")
+		const musicBuffMeg = musicBuff && CrunkerInstance.mergeAudioBuffers([musicBuff,...beats],[0,...beatsTime])
+		console.timeEnd("音频合并时间")
+		console.time("音频audioDom生成时间")
+		beatMusic = musicBuffMeg && CrunkerInstance.exportAudioElement(musicBuffMeg)
+		console.timeEnd("音频audioDom生成时间")
+	}catch(err){
+		console.log(err)
+	}
+	console.timeEnd("音频合成时间")
+	return beatMusic
+}
+
+
+// 处理加载节拍器音频
 export const handleLoadBeatMusic = async () => {
 	if(metronomeData.disable) {
 		return
@@ -251,30 +325,7 @@ export const handleLoadBeatMusic = async () => {
 	}
 	state.loadingText = "音频资源加载中,请稍后…"
 	state.isLoading = true
-	/* 音频合成 */
-	if(!CrunkerInstance){
-		CrunkerInstance = new Crunker()
-	}
-	console.time("音频加载时间")
-	const [audioBuffer, tickBuff, tockBuff] = await CrunkerInstance.fetchAudio(`${currentMusic}?v=${Date.now()}`, tickMp3, tockMp3)
-	console.timeEnd("音频加载时间")
-	// 计算音频空白时间
-	const silenceDuration = audioBuffer&&!state.isEvxml ? CrunkerInstance.calculateSilenceDuration(audioBuffer) : 0
-	console.log(`音频空白时间:${silenceDuration}`)
-	const beats:AudioBuffer[] = []
-    const beatsTime:number[] = []
-	metronomeData.metroMeasure.map(measures=>{
-		measures.map((item:any)=>{
-			beats.push(item.index===0?tickBuff!:tockBuff!)
-			beatsTime.push(item.time+silenceDuration) //不是妙极客的曲子才加上空白
-		})
-	})
-	console.time("音频合并时间")
-	const musicBuffMeg = audioBuffer && CrunkerInstance.mergeAudioBuffers([audioBuffer!,...beats],[0,...beatsTime])
-	console.timeEnd("音频合并时间")
-	console.time("音频audioDom生成时间")
-	const musicAudio = musicBuffMeg && CrunkerInstance.exportAudioElement(musicBuffMeg) as any
-	console.timeEnd("音频audioDom生成时间")
+	const musicAudio = await mergeBeatAudio(currentMusic) as any
 	const playEleObj = {
 		"play_music":"beatSongEle",
 		"play_background":"beatBackgroundEle",
@@ -318,6 +369,73 @@ export const handleLoadBeatMusic = async () => {
 	state.isLoading = false
 }
 
+// 切换对应的声轨,并且配置当前的audio
+export async function changeCombineAudio (combineIndex: number){
+	// 重复点击的时候取消选中 原音
+	if(combineIndex === audioData.combineIndex){
+		audioData.combineIndex = -1
+		state.playSource = "background"
+		state.music = ""
+		// 当没有背景音文件的时候
+		if(!state.accompany) {
+			state.noMusicSource = true
+		}
+		return
+	}
+	state.loadingText = "音频资源加载中,请稍后…";
+	state.isLoading = true;
+	const musicUrl = audioData.combineMusics[combineIndex]
+	// 有就拿缓存,没有就加载
+	const cacheMusicIndex = audioData.combineMusicEles.findIndex(ele => {
+		return ele.key === combineIndex
+	})
+	const cacheMusic = audioData.combineMusicEles[cacheMusicIndex]
+	if(cacheMusic?.value){
+		audioData.songCollection.songEle = cacheMusic.value
+		audioData.songCollection.beatSongEle = cacheMusic.beatValue
+		// 使用缓存之后 当前数据位置向后偏移,删除缓存的时候以使用顺序位置
+		const itemMusic = audioData.combineMusicEles.splice(cacheMusicIndex, 1)
+		audioData.combineMusicEles.push(...itemMusic)
+	}else{
+		const music = await createAudio(musicUrl)
+		const beatMusic = await mergeBeatAudio(musicUrl) as any 
+		// 当没有背景音的时候 需要绑定事件
+		if(!state.accompany){
+			if(music){
+				music.addEventListener("play", onPlay);
+				music.addEventListener("ended", onEnded);
+			}			
+			if(beatMusic){
+				beatMusic.addEventListener("play", onPlay);
+				beatMusic.addEventListener("ended", onEnded);
+			}
+		}
+		audioData.combineMusicEles.push({
+			key: combineIndex,
+			value: music!,
+			beatValue: beatMusic!
+		})
+		// 当大于4个数据的时候 删除掉最前面的数据
+		if(audioData.combineMusicEles.length > 4){
+			audioData.combineMusicEles.splice(0,1)
+		}
+		audioData.songCollection.songEle = music
+		audioData.songCollection.beatSongEle = beatMusic!
+	}
+	audioData.combineIndex = combineIndex
+	state.music = musicUrl
+	state.playSource = "music"
+	// 当没有背景音文件的时候
+	if(!state.accompany) {
+		state.noMusicSource = false
+	}
+	showToast({
+		message:  "已开启原声",
+		position: "top",
+		className: "selectionToast",
+	});
+	state.isLoading = false;
+}
 export default defineComponent({
 	name: "audio-list",
 	setup() {
@@ -352,33 +470,6 @@ export default defineComponent({
 			}
 		);
 
-		const createAudio = (src?: string): Promise<HTMLAudioElement | null> => {
-			if(!src){
-				return Promise.resolve(null)
-			}
-			return new Promise((resolve) => {
-				const a = new Audio(src + '?v=' + Date.now());
-				a.onloadedmetadata = () => {
-					resolve(a);
-				};
-				a.onerror = () => {
-					resolve(null);
-				};
-				// 当未加载 资源之前 切换到其他浏览器标签,浏览器可能会禁止资源加载所以无法触发onloadedmetadata事件,导致一直在加载中,这里做个兼容
-				if (document.visibilityState === 'visible') {
-					a.load();
-				} else {
-					const onVisibilityChange = () => {
-						if (document.visibilityState === 'visible') {
-							document.removeEventListener('visibilitychange', onVisibilityChange);
-							a.load();
-						}
-					};
-					document.addEventListener('visibilitychange', onVisibilityChange);
-				}
-			});
-		};
-
 		/**
 		 * #11046
 		 * 声音与圆点消失的节点不一致,可能原因是部分安卓手机没有立即播放,所以需要等待有音频进度返回时再播放节拍器

BIN
src/view/music-score/combineAudio/imgs/lock.png


BIN
src/view/music-score/combineAudio/imgs/open.png


+ 18 - 0
src/view/music-score/combineAudio/index.module.less

@@ -0,0 +1,18 @@
+.combineAudio {
+   position: absolute;
+   left: 0;
+   top: 0;
+   z-index: 1;
+   .combineAudioImg {
+      position: absolute;
+      z-index: 119;
+      width: 22PX;
+      height: 22PX;
+      padding: 2PX;
+      transform: scale(var(--combineZoom));
+   }
+   &.play .combineAudioImg {
+      pointer-events: none;
+      opacity: 0.4;
+   }
+}

+ 68 - 0
src/view/music-score/combineAudio/index.tsx

@@ -0,0 +1,68 @@
+import { defineComponent, onMounted, ref, computed } from "vue"
+import styles from "./index.module.less"
+import { audioData, changeCombineAudio } from "/src/view/audio-list"
+import openImg from "./imgs/open.png"
+import lockImg from "./imgs/lock.png"
+import state from "/src/state"
+
+export default defineComponent({
+   name: "combineAudio",
+   setup(props, { emit }) {
+      const elementsData = ref<{ index: number; top: number; left: number }[]>([])
+      onMounted(() => {
+         const parent = document.querySelector("#osmdCanvasPage1")
+         const elements = document.querySelectorAll("g[data-trackIdx]")
+         const musicContainerPos = document.getElementById("musicAndSelection")?.getBoundingClientRect() || {
+            top: 0,
+            left: 0
+         }
+         const combineMusicsIndexs = Object.keys(audioData.combineMusics)
+         elements.forEach(element => {
+            const dataTrackIdx = element.getAttribute("data-trackIdx")
+            // 当有 dataTrackIdx 并且有原音的时候 显示
+            if (dataTrackIdx && combineMusicsIndexs.includes(dataTrackIdx)) {
+               const elementRect = element.getBoundingClientRect()
+               const height = elementRect.height
+               let top = elementRect.top + height / 2 - 11 - musicContainerPos.top
+               let left = elementRect.left - 22 - 10 - musicContainerPos.left
+               elementsData.value.push({
+                  index: parseInt(dataTrackIdx),
+                  top: top,
+                  left: left
+               })
+            }
+         })
+      })
+      const combineZoom = computed(() => {
+         let zoom = state.zoom
+         if (zoom < 1) {
+            zoom = 1
+         } else if (zoom > 1.5) {
+            zoom = 1.5
+         }
+         return zoom
+      })
+      return () => (
+         <>
+            <div class={[styles.combineAudio, state.playState === "play" && styles.play]}>
+               {elementsData.value.map(item => {
+                  return (
+                     <img
+                        class={styles.combineAudioImg}
+                        onClick={() => {
+                           changeCombineAudio(item.index)
+                        }}
+                        style={{
+                           top: item.top + "px",
+                           left: item.left - (combineZoom.value - 1) * 22 + "px",
+                           "--combineZoom": combineZoom.value
+                        }}
+                        src={audioData.combineIndex === item.index ? openImg : lockImg}
+                     />
+                  )
+               })}
+            </div>
+         </>
+      )
+   }
+})

+ 6 - 3
src/view/music-score/index.tsx

@@ -13,6 +13,7 @@ import { storeData } from "/src/store";
 import { isLoadingCss } from "/src/page-instrument/view-detail/loadingCss"
 import HorizontalDragScroll from './HorizontalDragScroll';
 import { getQuery } from "/src/utils/queryString";
+import CombineAudio from './combineAudio';
 
 export const musicRenderTypeKey = "musicRenderType";
 let osmd: any = null;
@@ -140,7 +141,7 @@ export default defineComponent({
 				// osmd.EngravingRules.PageTopMargin = state.isPreView ? 1 : 3;
 				osmd.EngravingRules.PageTopMargin = (state.isPreView && state.musicRenderType === EnumMusicRenderType.staff) ? 1 : state.isPreView ? 2 : 3;
 				osmd.EngravingRules.PageTopMarginNarrow = 3;
-				osmd.EngravingRules.PageLeftMargin = 3.6;
+				osmd.EngravingRules.PageLeftMargin = state.isCombineRender ? 8 : 3.6;
 				osmd.EngravingRules.PageRightMargin = 3;
 				osmd.EngravingRules.BreathMarkDistance = 0.1; // 呼吸标记距离音符的位置,百分比
 				osmd.EngravingRules.PageBottomMargin = state.isSingleLine ? 2 : 18;
@@ -169,10 +170,11 @@ export default defineComponent({
 			}
 			// 需要渲染总谱的云教练页面
 			if (!state.isSimplePage && state.isCombineRender) {
+				const canSelectTracks = state.combinePartIndexs.length > 1 ? state.combinePartIndexs.map(partIndex => { return state.partListNames[partIndex] }) : state.canSelectTracks
 				for (let i = 0; i < osmd.Sheet.Instruments.length; i++) {
 					const trackName = state.isEvxml && state.evxmlAddPartName ? osmd.Sheet.Instruments[i].idString || '' : osmd.Sheet.Instruments[i].Name || '';
-					osmd.Sheet.Instruments[i].Visible = state.canSelectTracks.includes(trackName.trim())
-				  }
+					osmd.Sheet.Instruments[i].Visible = canSelectTracks.includes(trackName.trim())
+				}
 			}
 			// 下载图片改为A4纸尺寸,因为会和曲谱标题合成一张图片,所以尺寸的高度需要缩小,1123-1065
 			// if (props.isDownXml) {
@@ -268,6 +270,7 @@ export default defineComponent({
 			>
 				{slots.default?.()}
 				{props.showSelection && musicData.showSelection && !state.isEvaluatReport &&!state.isSimplePage && state.musicRendered && <Selection />}
+				{props.showSelection && musicData.showSelection && state.isCombineRender && state.musicRendered && <CombineAudio></CombineAudio> }
 			</div>
 		);
 	},

BIN
src/view/plugins/toggleMusicSheet/choosePartName/imgs/changeName.png


BIN
src/view/plugins/toggleMusicSheet/choosePartName/imgs/headImg.png


BIN
src/view/plugins/toggleMusicSheet/choosePartName/imgs/headImg1.png


BIN
src/view/plugins/toggleMusicSheet/choosePartName/imgs/headImg2.png


BIN
src/view/plugins/toggleMusicSheet/choosePartName/imgs/resetBtn.png


+ 105 - 85
src/view/plugins/toggleMusicSheet/choosePartName/index.module.less

@@ -1,73 +1,49 @@
 .container {
   &.follow{
     .head{
-        background: url("../../../../page-instrument/header-top/image/headImg1.png") no-repeat;
+        background: url("./imgs/headImg1.png") no-repeat;
         background-size: 100% 100%; 
     }
     .pickerCon{
         background: #ACDDEA;
         box-shadow: 0px 4px 0px 0px #5EA2B9;
-        .pickerBox{
+        .pickerContent{
             background: #E3F3F5;
-            .picker{
-              :global{
-                .van-picker__mask {
-                  background-image:linear-gradient( rgba(227,243,245,0.8), rgba(227,243,245,0.8)),linear-gradient( rgba(227,243,245,0.8), rgba(227,243,245,0.8));
-                }
-                .van-picker__columns{
-                  .van-picker__frame{
-                    border-color: #CFE6EC;
-                  }
-                }
-              }
-            }
+        }
+        .pickerBox .titCon.stickyTit{
+          background-color: #E3F3F5;
         }
     }
-}
-&.evaluating{
-    .head{
-      width: 394px;
-      height: 62px;
-      margin-bottom: -3px;
-      background: url("/src/page-instrument/header-top/image/headImg2.png") no-repeat;
-      background-size: 100% 100%; 
-      .headTit{
-          bottom: 11px;
+  }
+  &.evaluating{
+      .head{
+        width: 600px;
+        height: 62px;
+        margin-bottom: -3px;
+        background: url("./imgs/headImg2.png") no-repeat;
+        background-size: 100% 100%; 
+        .closeImg{
+            top: 2px;
+            right: -26px;
+        }
       }
-      .closeImg{
-          top: 2px;
-          right: -26px;
+      .pickerCon{
+          background: #B0CDFF;
+          box-shadow: 0px 4px 0px 0px #759CE4;
+          .pickerContent{
+              background: #EAF1FB;
+          }
+          .pickerBox .titCon.stickyTit{
+            background-color: #EAF1FB;
+          }
       }
-    }
-    .pickerCon{
-        background: #B0CDFF;
-        box-shadow: 0px 4px 0px 0px #759CE4;
-        .pickerBox{
-            background: #EAF1FB;
-            .picker{
-              :global{
-                .van-picker__mask {
-                  background-image:linear-gradient( rgba(234,241,251,0.8), rgba(234,241,251,0.8)),linear-gradient( rgba(234,241,251,0.8), rgba(234,241,251,0.8));
-                }
-              }
-            }
-        }
-    }
-}
+  }
   .head {
-      background: url("../../../../page-instrument/header-top/image/headImg.png") no-repeat;
+      background: url("./imgs/headImg.png") no-repeat;
       background-size: 100% 100%;
-      width: 372px;
+      width: 576px;
       height: 57px;
-      position: relative;
-      .headTit{
-          position: absolute;
-          bottom: 8px;
-          left: 50%;
-          transform: translateX(-50%);
-          width: 76px;
-          height: 20px;
-      }        
+      position: relative;      
       .closeImg{
           position: absolute;
           top: 0;
@@ -78,53 +54,97 @@
       }
   }
   .pickerCon{
-    width: 354px;
-    height: 284px;
+    width: 560px;
+    height: 288px;
     background: #B0D8FF;
     box-shadow: 0px 4px 0px 0px #7AAEE0;
     border-radius: 0px 0px 24px 24px;
     margin: 0 auto;
     padding: 10px;
-    .pickerBox{
+    .pickerContent{
       width: 100%;
       height: 100%;
       background: #EAF2FB;
       border-radius: 12px;
+      padding: 6px 16px 0;
       overflow: hidden;
     }
-  }
-  .picker {
-    height: 204px;
-    overflow: hidden;
-    background-color: initial;
-    display: flex;
-    align-items: center;
-    :global{
-      .van-picker__mask {
-        background-image:linear-gradient( rgba(234,242,251,0.8), rgba(234,242,251,0.8)),linear-gradient( rgba(234,242,251,0.8), rgba(234,242,251,0.8));
+    .pickerBox{
+      width: 100%;
+      height: calc(100% - 60px);
+      margin-bottom: 10px;
+      display: flex;
+      flex-direction: column;
+      overflow-y: auto;
+      &::-webkit-scrollbar {
+          width: 0;
+          display: none;
       }
-      .van-picker__columns{
-        width: 100%;
-        .van-picker__frame{
-          border-top: 1px solid #D5E0ED;
-          border-bottom: 1px solid #D5E0ED;
+      .titCon{
+        display: flex;
+        align-items: center;
+        padding-top: 10px;
+        &.stickyTit{
+          position: sticky;
+          top: -1px;
+          background-color: #EAF2FB;
+        }
+        .tit{
+          font-weight: 600;
+          font-size: 15px;
+          color: #131415;
+          line-height: 21px;
+        }
+        .tips{
+          margin-left: 6px;
+          font-weight: 400;
+          font-size: 13px;
+          color: #777777;
+          line-height: 1;
         }
       }
-      .van-picker-column__wrapper{
-        padding: 0 16px;
+      .content{
+        display: flex;
+        flex-wrap: wrap;
+        .specialBtn {
+          width: 96px;
+        }
+        .selBtn{
+          height: 34px;
+          line-height: 34px;
+          background: #ffffff;
+          border-radius: 6px;
+          font-weight: 400;
+          font-size: 14px;
+          color: #333333;
+          text-align: center;
+          cursor: pointer;
+          border:1px solid transparent;
+          margin-top: 10px;
+          margin-right: 10px;
+          padding: 0 8px;
+          &.active{
+            background: #EBF8FF;
+            border-color: #1CACF1;
+            color: #1CACF1;
+          }
+        }
       }
-      .van-picker-column__item{
-        font-weight: 600;
-        font-size: 15px;
+    }
+    .btnCon{
+      display: flex;
+      justify-content: center;
+      .btn{
+        width: 118px;
+        height: 39px;
+        cursor: pointer;
+        &:active,&:hover{
+          opacity: 0.8;
+        }
+        & + img{
+          margin-left: 20px;
+        }
       }
     }
   }
-  .button {
-    display: block;
-    margin: 10px auto 0;
-    cursor: pointer;
-    width: 118px;
-    height: 40px;
-    z-index: 9;
-  }
 }

+ 91 - 48
src/view/plugins/toggleMusicSheet/choosePartName/index.tsx

@@ -1,11 +1,13 @@
 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, checkMoveNoSave } from "/src/state";
-import changeName from "./imgs/changeName.png"
+import { showToast } from 'vant'
 import { headImg } from "/src/page-instrument/header-top/image";
 import { toggleMusicSheet } from "../index"
 import okBtn from "./imgs/okBtn.png"
+import cancelBtn from "./imgs/cancelBtn.png"
+import resetBtn from "./imgs/resetBtn.png"
+import { getQuery } from "/src/utils/queryString";
 
 export default defineComponent({
   name: 'choosePartName',
@@ -14,73 +16,114 @@ export default defineComponent({
       type: Array as PropType<any[]>,
       default: () => [],
     },
-    partIndex: {
-      type: Number,
-      default: 0,
+    partIndexs: {
+      type: Array as PropType<number[]>,
+      default: () => []
     },
   },
   emits: ['close'],
   setup(props, { emit }) {
-    const selValues = ref([props.partIndex]);
-    const myPicker = ref();
+    const query: any = getQuery();
+    const selValues = ref([...props.partIndexs]);
     watch(
       () => toggleMusicSheet.show,
       () => {
         if (toggleMusicSheet.show) {
-          selValues.value = [props.partIndex]
+          selValues.value = [...props.partIndexs]
         }
       }
     );
-    watch(() => toggleMusicSheet.show, ()=>{
-        // 支持滚轮事件
-        if (toggleMusicSheet.show) {
-          nextTick(() => {
-            myPicker.value.$el.addEventListener('wheel', handleWheel)
-          })
+    function hanldeSelSheet(value:number, isCombineRender = false){
+      if(isCombineRender){
+        selValues.value = [value]
+      }else{
+        // 总谱切换过来的时候
+        if(selValues.value[0] === 999){
+          selValues.value = []
+        }
+        if (state.modeType !== 'practise') {
+          if (value == selValues.value[0]) {
+            return
+          } else {
+            selValues.value = []
+            selValues.value.push(value)
+          }
         } else {
-            myPicker.value.$el.removeEventListener('wheel', handleWheel)
+          const index = selValues.value.indexOf(value)
+          if(index > -1){
+            if(selValues.value.length > 1){
+              selValues.value.splice(index, 1)
+            }
+          }else{
+            if(selValues.value.length >=4){
+              showToast({
+                position: "top",
+                message: "最多可选4个"
+              });
+              return
+            }
+            selValues.value.push(value)
+          }
         }
-      },{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.container, styles[state.modeType]]}>
         <div class={[styles.head, "top_draging"]}>
-          <img class={styles.headTit} src={changeName} />
           <img class={styles.closeImg} src={headImg("closeImg.png")} onClick={() => emit("close")} />
         </div>
         <div class={styles.pickerCon}>
-          <div class={styles.pickerBox}>
-            <Picker
-              ref={myPicker}
-              class={[styles.picker, state.platform === IPlatform.PC && styles.pcPicker]}
-              v-model={selValues.value}
-              showToolbar={false}
-              columns={props.partListNames}
-              visible-option-num={5}
-              option-height={"1.06666rem"}
-            />
-            <img src={ okBtn } class={styles.button} onClick={async () => {
-                await checkMoveNoSave();
-                myPicker.value.confirm()
-                nextTick(()=>{
-                  emit('close', selValues.value[0])
-                })
+          <div class={styles.pickerContent}>
+            <div class={styles.pickerBox}>
+              {
+                state.isScoreRender &&
+                  <>
+                    {/* <div class={styles.titCon}>
+                      <div class={styles.tit}>选择总谱</div>
+                    </div> */}
+                    <div class={styles.content}>
+                      <div class={[styles.selBtn, styles.specialBtn, selValues.value.includes(999) && styles.active]} onClick={()=>{ hanldeSelSheet(999, true) }}>总谱</div>
+                    </div>
+                  </>
               }
-            }></img>
+              <div class={[styles.titCon, styles.stickyTit]}>
+                <div class={styles.tit}>选择声部</div>
+                {
+                  state.modeType === 'practise' && 
+                  <div class={styles.tips}>(最多可选4个)</div>
+                }
+              </div>
+              <div class={[styles.content]}>
+                {
+                  props.partListNames.map((item: any)=>{
+                    return <div class={[styles.selBtn,selValues.value.includes(item.value) && styles.active]} onClick={()=>{hanldeSelSheet(item.value)}}>{item.text}</div>
+                  })
+                }
+              </div>
+            </div>
+            <div class={styles.btnCon}>
+                <img src={ resetBtn } class={styles.btn} onClick={async () => {
+                    selValues.value = []
+                  }
+                }></img>
+                <img src={ okBtn } class={styles.btn} onClick={async () => {
+                    if (!selValues.value.length) {
+                      showToast({
+                        position: "top",
+                        message: "最少需要选择一个声部"
+                      });
+                      return
+                    }
+                    if (query.isMove) {
+                      await checkMoveNoSave();
+                    }
+                    toggleMusicSheet.show = false
+                    nextTick(()=>{
+                      emit('close', selValues.value)
+                    })
+                  }
+                }></img>
+            </div>
           </div>
         </div>
       </div>

+ 9 - 13
src/view/plugins/toggleMusicSheet/index.tsx

@@ -40,24 +40,19 @@ export default defineComponent({
       }).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
     })
 
-    const trackIdx: any = computed(() => {
-      if (partListNames && partListNames.value.length) {
-        
-        const idx = partListNames.value.find((item: any) => item.value == state.partIndex)?.value || 0
-        console.log(3333,idx)
-        return idx
-      } else {
-        return 0
+    const partIndexs: any = computed(() => {
+      if(state.combinePartIndexs.length > 1){
+        return state.combinePartIndexs
+      }else{
+        return [state.partIndex]
       }
     })
 
-    const switchMusic = (index: number) => {
-      if (state.partIndex === index) return
+    const switchMusic = (partIndexs: number[]) => {
+      const index = partIndexs.join(",") as any
       // 暂停播放
       togglePlay("paused");
       // 销毁播放器
@@ -82,6 +77,7 @@ export default defineComponent({
           behaviorId: sessionStorage.getItem('behaviorId') || '',
           _t: new Date().valueOf(),
           'part-index': index,
+          'part-name': ''
         })
       console.log(_url)
       location.href = _url
@@ -114,7 +110,7 @@ export default defineComponent({
     return () => (
       <Popup v-model:show={toggleMusicSheet.show} class="popup-custom van-scale center-closeBtn switchBoxClass_drag" transition="van-scale" teleport="body" style={positionInfo.styleDrag.value} overlay-style={{background:'rgba(0, 0, 0, 0.7)'}}>
         <ChoosePartName
-          partIndex={trackIdx.value || 0}
+          partIndexs={partIndexs.value}
           partListNames={partListNames.value}
           onClose={(value) => {
             console.log("🚀 ~ value:", value)