Ver Fonte

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

TIANYONG há 11 meses atrás
pai
commit
a21a81b239

+ 17 - 0
src/helpers/formateMusic.ts

@@ -640,6 +640,23 @@ export const formatXML = (xml: string, xmlUrl?: string): string => {
 	
 	const xmlParse = new DOMParser().parseFromString(xml, "text/xml");
 
+	// 声调
+	const fifths = xmlParse.getElementsByTagName("fifths");
+	if (fifths && fifths.length) {
+		// 是否是C调
+		state.isCTone = fifths[0].textContent === '0'
+	}
+
+	const endings = Array.from(xmlParse.getElementsByTagName("ending"));
+	for (const ending of endings) {
+		// if (ending.getAttribute('type') === 'stop') {
+		// 	// @ts-ignore
+		// 	ending.parentNode?.removeChild(ending.parentNode?.getElementsByTagName('bar-style')[0])
+		// }
+		// @ts-ignore
+		// ending.parentNode.parentNode?.removeChild(ending.parentNode)
+	}
+
 	const measures = Array.from(xmlParse.getElementsByTagName("measure"));
 	const minutes: any = xmlParse.getElementsByTagName("per-minute");
 	let speeds: any = []

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

@@ -42,17 +42,18 @@ export default defineComponent({
           followData.isBeginMask && <div class={styles.beginMask}></div>
         }        
         <div class={[styles.operatingBtn, state.platform === IPlatform.PC && state.musicScoreBtnDirection === "left" ? styles.operatingLeft : ""]}>
-          {!followData.start && (
+          {(!followData.start && !followData.practiceStart) && (
             <img
               class={[styles.iconBtn, "follow-1"]}
               src={headImg("icon_play.png")}
               onClick={() => {
-                followData.start = true;
+                // followData.start = true;
+                followData.practiceStart = true;
                 handleFollowStart();
               }}
             />
           )}
-          {followData.start && (
+          {(followData.start || followData.practiceStart)&& (
             <>
               <img class={styles.iconBtn} src={headImg("icon_reset.png")} onClick={() => handleFollowEnd()} />
               <img class={styles.iconBtn} src={headImg("submit.png")} onClick={() => handleFollowEnd()} />

+ 6 - 4
src/page-instrument/header-top/index.module.less

@@ -103,8 +103,8 @@
         pointer-events: none;
     }
 }
-
-.modeChangeBox {
+.modeChangeBox{
+    cursor: pointer;
     position: fixed;
     top: 20px;
     right: 30px;
@@ -317,6 +317,7 @@
 }
 
 .playBtn {
+    cursor: pointer;
     position: fixed;
     right: 30px;
     bottom: 12px;
@@ -356,6 +357,7 @@
 }
 
 .resetBtn {
+    cursor: pointer;
     position: fixed;
     right: 100px;
     bottom: 12px;
@@ -447,8 +449,8 @@
                 margin-left: 150px;
             }
         }
-
-        >.modeImg {
+        > .modeImg {
+            cursor: pointer;
             width: calc((100% - 2*40px)/3);
             max-width: 220px;
         }

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

@@ -9,7 +9,7 @@ import { Badge, Circle, Popover, Popup, showConfirmDialog, showToast, NoticeBar
 import Speed from "./speed";
 import { evaluatingData, handleStartEvaluat } from "/src/view/evaluating";
 import Settting from "./settting";
-import state, { IPlatform, handleChangeSection, handleResetPlay, handleRessetState, togglePlay, IPlayState, refreshMusicSvg } from "/src/state";
+import state, { IPlatform, handleChangeSection, handleResetPlay, handleRessetState, togglePlay, IPlayState, refreshMusicSvg, EnumMusicRenderType } from "/src/state";
 import { getAudioCurrentTime } from "/src/view/audio-list";
 import { followData, toggleFollow } from "/src/view/follow-practice";
 import { api_back } from "/src/helpers/communication";
@@ -940,7 +940,21 @@ export default defineComponent({
             playBtn.value.disabled && styles.disabled,
             state.platform === IPlatform.PC && state.musicScoreBtnDirection === "left" ? styles.playLeftButton : state.platform === IPlatform.PC && state.musicScoreBtnDirection === "right" ? styles.playRightButton : "",
           ]}
-          onClick={() => togglePlay(state.playState === "play" ? "paused" : "play")}
+          onClick={() => {
+            // C调能播放唱名,非C调时,只有谱面类型是首调时,才能播放唱名
+            if (!state.isCTone && state.playSource === 'mingSong') {
+              const notPlayDesc = state.musicRenderType === EnumMusicRenderType.staff ? '该曲目的五线谱目前还不支持播放唱名' : state.musicRenderType === EnumMusicRenderType.fixedTone ? '该曲目的固定调目前还不支持播放唱名' : '';
+              if (notPlayDesc) {
+                showToast({
+                  message: notPlayDesc,
+                  position: "top",
+                  className: "selectionToast",
+                });
+                return
+              }
+            }
+            togglePlay(state.playState === "play" ? "paused" : "play")
+          }}
         >
           <div class={styles.btnWrap}>
             <img style={{ display: state.playState === "play" ? "none" : "" }} class={styles.iconBtn} src={headImg("icon_play.png")} />

+ 6 - 2
src/page-instrument/header-top/modeView.tsx

@@ -107,7 +107,9 @@ export default defineComponent({
           src={backImg}
           class={styles.back}
           onClick={() => {
-            smoothAnimationState.isShow.value = state.melodyLine;
+            if(state.isSingleLine){
+              smoothAnimationState.isShow.value = state.melodyLine;
+            }
             // 返回的时候 跳转到之前记录的模式
             if(headTopData.oldModeType !== "practise"){
               // 点击评测模式进入评测模块的需要检测耳机状态,通过返回按钮进入评测模块的,不检测耳机状态
@@ -120,7 +122,9 @@ export default defineComponent({
         <img src={nameImg} class={styles.name} />
         <div class={[styles.modeBox, ((!state.isPercussion && !state.enableEvaluation) || (state.isPercussion && state.enableEvaluation) || (state.isPercussion && !state.enableEvaluation)) && styles.twoModeBox]}>
           <Vue3Lottie ref={modeImgDom1} class={styles.modeImg} animationData={lxMode} autoPlay={false} loop={true} onClick={() => {
-            smoothAnimationState.isShow.value = state.melodyLine;
+            if(state.isSingleLine){
+              smoothAnimationState.isShow.value = state.melodyLine;
+            }
             headTopData.handleChangeModeType("practise")
             } }></Vue3Lottie>
           {!state.isPercussion && <Vue3Lottie ref={modeImgDom2} class={styles.modeImg} animationData={glMode} autoPlay={false} loop={true} onClick={() => headTopData.handleChangeModeType("follow")}></Vue3Lottie>}

BIN
src/page-instrument/view-detail/images/bg2_left_zs.png


+ 29 - 2
src/page-instrument/view-detail/index.module.less

@@ -251,6 +251,14 @@
     }
 }
 
+@keyframes rotate {
+    from {
+        transform: rotate(0deg);
+    }
+    to {
+        transform: rotate(360deg);
+    }
+}
 .loadingPop {
     position: fixed;
     left: 0;
@@ -273,8 +281,27 @@
             color: #999;
         }
     }
-
-    .lottie {
+    .loadingCssBox{
+        width: 27px;
+        height: 27px;
+        display: flex;
+        justify-content: space-between;
+        flex-wrap: wrap;
+        align-content: space-between;
+        margin-bottom: 24px;
+        animation: rotate 1.5s linear infinite;
+        .loadingCssItem{
+            width: 11px;
+            height: 11px;
+            border-radius: 50%;
+            background: #20BDFF;
+            opacity: 0.5;
+            &:nth-child(2){
+                opacity:1;
+            }
+        }
+    }
+    .lottie{
         width: 120px;
     }
 

+ 22 - 1
src/page-instrument/view-detail/index.tsx

@@ -38,6 +38,7 @@ import { initSmoothAnimation } from "./smoothAnimation";
 import EmptyMusic, { isEmptyMusicShow } from "./emptyMusic";
 import { position } from "html2canvas/dist/types/css/property-descriptors/position";
 import Loading from "./loading"
+import LoadingCss from "./loadingCss"
 // import bgJson from "./images/index.json";
 import bg2Left from "./images/bg2_left_zs.png";
 import bg2Right from "./images/bg2_right_zs.png";
@@ -308,6 +309,20 @@ export default defineComponent({
       // pushAppMusic();
       // console.timeEnd("渲染加载耗时");
     };
+    function handleOnRendered(osmd: any) {
+      try{
+        handleRendered(osmd)
+      }catch(err){
+        console.log("webApi_beatTimes",err)
+        window.parent.postMessage(
+          {
+            api: "webApi_beatTimes",
+            data: "-1"
+          },
+          "*"
+        );
+      }
+    }
     /** 指法配置 */
     const fingerConfig = computed<any>(() => {
       if (state.setting.displayFingering && state.fingeringInfo?.name) {
@@ -534,7 +549,12 @@ export default defineComponent({
         >
           {/* 曲谱渲染 */}
           {!detailData.isLoading && (
-            <MusicScore ref={musicScoreRef} musicColor={state.isPreView ? "#000000" : "#FFFFFF"} showPartNames={state.isCombineRender} onRendered={handleRendered}>
+            <MusicScore 
+              ref={musicScoreRef}
+              musicColor={state.isPreView ? '#000000' : '#FFFFFF'}
+              showPartNames={state.isCombineRender}
+              onRendered={handleOnRendered} 
+            > 
               {/* 旋律线关闭时候的 标题和作者 */}
               <AuthorName></AuthorName>
               {/* 节拍器 */}
@@ -602,6 +622,7 @@ export default defineComponent({
           </>
         )}
         <Loading tipText={state.loadingText} />
+        <LoadingCss />
         <Popup
           zIndex={5050}
           teleport="body"

+ 21 - 0
src/page-instrument/view-detail/loadingCss.tsx

@@ -0,0 +1,21 @@
+import { defineComponent, ref} from "vue"
+import styles from "./index.module.less"
+import state from "/src/state"
+
+export const isLoadingCss = ref(false)
+export default defineComponent({
+   name: "loadingCss",
+   setup() {
+      return () => (
+         <div class={[styles.loadingPop, state.isPreView && styles.isPreView]} style={{ display: isLoadingCss.value ? "flex" : "none" }}>
+            <div class={styles.loadingCssBox}>
+                <div class={styles.loadingCssItem}></div>
+                <div class={styles.loadingCssItem}></div>
+                <div class={styles.loadingCssItem}></div>
+                <div class={styles.loadingCssItem}></div>
+            </div>
+            <div class={styles.loadingTip}>正在加载中,请稍等…</div>
+         </div>
+      )
+   }
+})

+ 21 - 6
src/state.ts

@@ -560,6 +560,8 @@ const state = reactive({
   workSectionNeedReset: false,
   /** 旋律线开关 */
   melodyLine: true,
+  /** 是否是C调,切换到唱名时,只有C调所有的谱面类型都可以播放唱名文件;其它调的只有首调可以播放唱名,因为唱名是按照C调制作的,没有其它调的唱名文件 */
+  isCTone: false,
 });
 const browserInfo = browser();
 let offset_duration = 0;
@@ -1891,9 +1893,15 @@ watch(
     // const matchMeasureNum = state.activeMeasureIndex - needReduceMultipleRestNum - 1
     // console.log('选中的小节',matchMeasureNum,'需要减去的小节',needReduceMultipleRestNum,'当前的小节',state.activeMeasureIndex)
     state.vfmeasures.forEach((item: any, idx: number) => {
-      const measureNum = item.getAttribute('data-num') ? Number(item.getAttribute('data-num')) : -1;
-      const nextMeasureNum = state.vfmeasures[idx+1]?.getAttribute('data-num') ? Number(state.vfmeasures[idx+1]?.getAttribute('data-num')) : -1;
-      if (measureNum >= 0 && (measureNum === state.activeMeasureIndex || (measureNum < state.activeMeasureIndex && nextMeasureNum > state.activeMeasureIndex)) || (measureNum < state.activeMeasureIndex && nextMeasureNum == -1) ) {
+      const dataNum = item.getAttribute('data-num')  // 值可能为字符串类型的undefined
+      const measureNum = (dataNum && dataNum !== "undefined") ? Number(dataNum) : -1;
+      let nextDataNum = state.vfmeasures[idx+1]?.getAttribute('data-num')
+      // 当有换行小节,下个小节的nextDataNum是undefined,所以这里需要往后找一个
+      if(!(nextDataNum && nextDataNum !== "undefined")){
+        nextDataNum = state.vfmeasures[idx + 2]?.getAttribute('data-num')
+      }
+      const nextMeasureNum = Number(nextDataNum)
+      if (measureNum >= 0 && (measureNum === state.activeMeasureIndex || (measureNum < state.activeMeasureIndex && nextMeasureNum > state.activeMeasureIndex))) {
         item.querySelector('.vf-custom-bg')?.setAttribute("fill", "#132D4C")
         item.querySelector('.vf-custom-bot')?.setAttribute("fill", "#040D1E")
         // 预备小节
@@ -1943,7 +1951,8 @@ watch(
         rightMeasureNumberXML = state.section[0].MeasureNumberXML
       }
       state.vfmeasures.forEach((item: any, idx: number) => {
-        const measureNum = item.getAttribute('data-num') ? Number(item.getAttribute('data-num')) : 1;
+        const dataNum = item.getAttribute('data-num')  // 值可能为字符串类型的undefined
+        const measureNum = (dataNum && dataNum !== "undefined") ? Number(dataNum) : -1;
         // 小于选中置灰
         if (measureNum < leftMeasureNumberXML) {
           item.querySelector('.vf-custom-bg')?.setAttribute("fill", "rgba(96,159,207,0.5)")
@@ -1963,8 +1972,14 @@ watch(
     }else{
       // 恢复选段前
       state.vfmeasures.forEach((item: any, idx: number) => {
-        const measureNum = item.getAttribute('data-num') ? Number(item.getAttribute('data-num')) : -1;
-        const nextMeasureNum = state.vfmeasures[idx+1]?.getAttribute('data-num') ? Number(state.vfmeasures[idx+1]?.getAttribute('data-num')) : -1;
+        const dataNum = item.getAttribute('data-num')  // 值可能为字符串类型的undefined
+        const measureNum = (dataNum && dataNum !== "undefined") ? Number(dataNum) : -1;
+        let nextDataNum = state.vfmeasures[idx+1]?.getAttribute('data-num')
+        // 当有换行小节,下个小节的nextDataNum是undefined,所以这里需要往后找一个
+        if(!(nextDataNum && nextDataNum !== "undefined")){
+          nextDataNum = state.vfmeasures[idx + 2]?.getAttribute('data-num')
+        }
+        const nextMeasureNum = Number(nextDataNum)
         if (measureNum >= 0 && (measureNum === state.activeMeasureIndex || (measureNum < state.activeMeasureIndex && nextMeasureNum > state.activeMeasureIndex)) ) {
           item.querySelector('.vf-custom-bg')?.setAttribute("fill", "#132D4C")
           item.querySelector('.vf-custom-bot')?.setAttribute("fill", "#040D1E")

+ 26 - 4
src/view/audio-list/index.tsx

@@ -124,9 +124,11 @@ export const setAudioCurrentTime = (time: number, index = 0) => {
 		setMidiCurrentTime(index);
 		return;
 	}
+	if(state.playSource === "mingSong") {
+		audioData.mingSongEle && (audioData.mingSongEle.currentTime = time);
+	}
 	audioData.songEle && (audioData.songEle.currentTime = time);
 	audioData.backgroundEle && (audioData.backgroundEle.currentTime = time);
-	audioData.mingSongEle && (audioData.mingSongEle.currentTime = time);
 	audioData.progress = time;
 };
 
@@ -150,7 +152,13 @@ export const toggleMutePlayAudio = (source: IPlayState, muted: boolean) => {
 /** 切换节拍器音源 */
 export const changeSongSourceByBate = (isDisBate:boolean) => {
 	// isDisBate 为true 切换到不带节拍的,为false 切换到带节拍的
-	const currentTime = audioData.songEle?.currentTime || audioData.backgroundEle?.currentTime || audioData.mingSongEle?.currentTime || audioData.progress || 0
+	let currentTime
+	if(state.playSource === "mingSong"){
+		currentTime = audioData.mingSongEle?.currentTime
+	}else{
+		currentTime = audioData.songEle?.currentTime || audioData.backgroundEle?.currentTime
+	}
+	currentTime || (currentTime = audioData.progress || 0)
 	if (isDisBate) {
 		if(state.playType === "play"){
 			audioData.songEle = audioData.songCollection.songEle
@@ -173,7 +181,10 @@ export const changeSongSourceByBate = (isDisBate:boolean) => {
 	}
 	audioData.songEle && (audioData.songEle.currentTime = currentTime)
 	audioData.backgroundEle && (audioData.backgroundEle.currentTime = currentTime)
-	audioData.mingSongEle && (audioData.mingSongEle.currentTime = currentTime)
+	// 当前是唱名才设置时间
+	if(state.playSource === "mingSong") {
+		audioData.mingSongEle && (audioData.mingSongEle.currentTime = currentTime)
+	}
 	// 设置静音与取消静音
 	if (state.playSource === "music") {
 		audioData.songEle && (audioData.songEle.muted = false);
@@ -239,13 +250,24 @@ export default defineComponent({
 			}
 			return new Promise((resolve) => {
 				const a = new Audio(src + '?v=' + Date.now());
-				a.load();
 				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);
+				}
 			});
 		};
 

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

@@ -37,7 +37,7 @@ export default defineComponent({
 
     const doubeClick = () => {
       // 如果在评测和跟练中,双击指法不跳转
-      if ((state.modeType === 'evaluating' && evaluatingData.startBegin) || (state.modeType === 'follow' && followData.start)) {
+      if ((state.modeType === 'evaluating' && evaluatingData.startBegin) || (state.modeType === 'follow' && followData.start) || state.playState === "play") {
         return;
       }
       const nowTime = Date.now();

+ 7 - 0
src/view/follow-practice/index.tsx

@@ -23,6 +23,7 @@ export const followData = reactive({
 	earphone: false,
 	isBeginMask: false, // 倒计时和系统节拍器时候的遮罩,防止用户点击
 	dontAccredit: true, // 没有开启麦克风权限,不需要调用结束收音的api
+	practiceStart: false,
 });
 
 // 记录跟练时长
@@ -50,6 +51,7 @@ export const toggleFollow = (notCancel = true) => {
 	// 取消跟练
 	if (!notCancel) {
 		followData.start = false;
+		followData.practiceStart = false;
 		// 开启了麦克风授权,才需要调用结束收音
 		if (storeData.isApp && !followData.dontAccredit) {
 			openToggleRecord(false);
@@ -80,6 +82,7 @@ const openToggleRecord = async (open: boolean = true) => {
 		if (!openState && followData.start) {
 			followData.earphone = true;
 			followData.start = false;
+			followData.practiceStart = false;
 		}
 	}
 };
@@ -109,6 +112,7 @@ export const handleFollowStart = async () => {
 	if (res?.content?.reson) {
 		followData.isBeginMask = false
 		followData.start = false;
+		followData.practiceStart = false;
 	} else {
 		followData.dontAccredit = false;
 		// 跟练模式开始前,增加播放系统节拍器
@@ -118,11 +122,13 @@ export const handleFollowStart = async () => {
 		if (!tickend) {
 			followData.isBeginMask = false
 			followData.start = false;
+			followData.practiceStart = false;
 			return false;
 		}
 		onClear();
 		followData.isBeginMask = false
 		followData.start = true;
+		followData.practiceStart = true;
 		followData.index = 0;
 		followData.list = [];
 		initSetPlayRate();
@@ -140,6 +146,7 @@ export const handleFollowStart = async () => {
 export const handleFollowEnd = () => {
 	onClear();
 	followData.start = false;
+	followData.practiceStart = false;
 	openToggleRecord(false);
 	followData.index = 0;
 	console.log("结束");

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

@@ -11,6 +11,7 @@ import { resetFormate, resetGivenFormate, setGlobalMusicSheet, limitSingleSvgPag
 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"
 
 export const musicRenderTypeKey = "musicRenderType";
 let osmd: any = null;
@@ -199,6 +200,7 @@ export default defineComponent({
 		/** 刷新曲谱 */
 		const refreshMusicScore = async () => {
 			console.log('刷新谱面123')
+			isLoadingCss.value = true
 			const container = document.getElementById('musicAndSelection'), svgDom = document.getElementById('osmdCanvasPage1'), selectionBox = document.getElementById('selectionBox');
 			if (container && svgDom) {
 				container?.removeChild(svgDom)
@@ -209,8 +211,6 @@ export default defineComponent({
 			state.osmd.clear();
 			musicData.isRenderLoading = true;
 			musicData.isRefreshLoading = true;
-			state.loadingText = '正在加载中,请稍等…'
-			state.isLoading = true;
 			// 在下一帧再执行,确保出现loading
 			requestAnimationFrame(async ()=>{
 				getContainerWidth();
@@ -219,8 +219,8 @@ export default defineComponent({
 				await init();
 				musicData.isRenderLoading = false;
 				musicData.isRefreshLoading = false;
-				state.isLoading = false;
 				musicData.showSelection = true;
+				isLoadingCss.value = false
 			})
 		}
 		expose({

+ 2 - 1
src/view/selection/index.module.less

@@ -186,7 +186,7 @@
         content: "";
         position: absolute;
         left: 50%;
-        bottom: -9PX;
+        bottom: -8PX;
         transform: translateX(-50%);
         width: 13Px;
         height: 9Px;
@@ -195,6 +195,7 @@
         background-position: center center;
         background-repeat: no-repeat;
         z-index: 2;    
+        opacity: 0.7;
     }
 }
 

+ 2 - 2
vite.config.ts

@@ -76,8 +76,8 @@ export default defineConfig({
         // target: "https://kt.colexiu.com",
         // target: "https://test.lexiaoya.cn",
         // target: "https://kt.colexiu.com",
-        target: "https://test.resource.colexiu.com", // 内容平台开发环境,内容平台开发,需在url链接上加上isCbs=true
-        // target: "https://test.kt.colexiu.com",
+        // target: "https://test.resource.colexiu.com", // 内容平台开发环境,内容平台开发,需在url链接上加上isCbs=true
+        target: "https://test.kt.colexiu.com",
         //target: "https://mec.colexiu.com",
         changeOrigin: true,
         rewrite: (path) => path.replace(/^\/instrument/, ""),