Browse Source

更新听音练习

lex 1 year ago
parent
commit
e3c1837b7e

BIN
src/page-instrument/view-figner/image/icon_btn_1.png


BIN
src/page-instrument/view-figner/image/icon_btn_2.png


BIN
src/page-instrument/view-figner/image/icon_btn_3.png


BIN
src/page-instrument/view-figner/image/icon_btn_4.png


BIN
src/page-instrument/view-figner/image/icon_btn_green_sub.png


BIN
src/page-instrument/view-figner/image/icon_btn_red_sub.png


File diff suppressed because it is too large
+ 0 - 0
src/page-instrument/view-figner/image/icons.json


BIN
src/page-instrument/view-figner/image/tips1.png


BIN
src/page-instrument/view-figner/image/tips2.png


BIN
src/page-instrument/view-figner/image/tips3.png


BIN
src/page-instrument/view-figner/image/tips4.png


+ 158 - 17
src/page-instrument/view-figner/index.module.less

@@ -23,11 +23,10 @@
 
         }
 
-        .backBtn{
-            padding: 26px 17px 26px 29px;
+        .backBtn {
+            padding: 26px 5px 26px 18px;
         }
 
-
         .tips {
             width: 43%;
             border-radius: 18px 0px 0px 18px;
@@ -107,6 +106,30 @@
     }
 }
 
+.popoverContainer {
+    --van-popover-action-height: 34px;
+    --van-popover-action-font-size: 14px;
+    --van-popover-radius: 12px;
+    --van-popover-action-width: 85px;
+    margin-top: 9px !important;
+    color: #999;
+
+    :global {
+        .van-popover__content {
+            max-height: 200px;
+            overflow-y: auto;
+        }
+
+        .van-popover__action {
+            padding: 0 9px;
+        }
+    }
+
+    .selected {
+        color: #1CACF1;
+        font-weight: 600;
+    }
+}
 
 .head {
     position: absolute;
@@ -140,10 +163,16 @@
     .left {
         display: flex;
         align-items: center;
+
+        .baseBtn {
+            margin: 0 4px;
+        }
     }
 
     .baseBtn {
-        width: 60px;
+        // width: 60px;
+        // height: 45px;
+        width: 54px;
         height: 45px;
         background: rgba(255, 255, 255, .48);
         border-radius: 10px;
@@ -221,8 +250,9 @@
         align-items: center;
         flex-shrink: 0;
         padding-bottom: 8px;
-        :global{
-            .van-button:active:before{
+
+        :global {
+            .van-button:active:before {
                 opacity: 0 !important;
             }
         }
@@ -384,6 +414,7 @@
     }
 
     .noteContent {
+        display: flex;
         position: relative;
         max-width: calc(100% - 92px);
         border-radius: 25px;
@@ -391,6 +422,10 @@
         border: 1px solid rgba(255, 255, 255, 0.6);
         overflow: hidden;
 
+        &.noteContentOther {
+            max-width: calc(100% - 92px - 52px - 5Px);
+        }
+
         &.noteContentWrap {
             &::before {
                 content: '';
@@ -421,18 +456,27 @@
 
     }
 
+    .lastNoteContent {
+        display: flex;
+        position: relative;
+        max-width: calc(100%);
+        border-radius: 0 25px 25px 0;
+        // background: rgba(255, 255, 255, 0.5);
+        // border: 1px solid rgba(255, 255, 255, 0.6);
+        overflow: hidden;
+    }
+
     .noteBox {
         display: flex;
         overflow-y: hidden;
         overflow-x: auto;
-        border-radius: 25px;
+        border-radius: 0 25px 25px 0;
+        z-index: 9;
 
         &::-webkit-scrollbar {
             width: 0;
             display: none;
         }
-
-
     }
 
     .noteBtn {
@@ -457,6 +501,36 @@
             opacity: 0 !important;
         }
     }
+
+    .tipsT {
+        position: absolute;
+        z-index: 99;
+        top: -40px;
+    }
+
+    .playTips {
+        width: 91px;
+        height: 23px;
+        background: url('./image/tips1.png') no-repeat center center / contain;
+    }
+
+    .playTips2 {
+        width: 122px;
+        height: 23px;
+        background: url('./image/tips4.png') no-repeat center center / contain;
+    }
+
+    .playError {
+        width: 83px;
+        height: 23px;
+        background: url('./image/tips3.png') no-repeat center center / contain;
+    }
+
+    .playSuccess {
+        width: 83px;
+        height: 23px;
+        background: url('./image/tips2.png') no-repeat center center / contain;
+    }
 }
 
 
@@ -480,7 +554,18 @@
         height: 100%;
     }
 
+    .showAnswer {
+        width: 20px;
+        height: 20px;
+        background: url('./image/icon_btn_green_sub.png') no-repeat center / contain;
+        position: absolute;
+        bottom: 2px;
+        right: -2px;
 
+        &.errorAnswer {
+            background: url('./image/icon_btn_red_sub.png') no-repeat center / contain;
+        }
+    }
 }
 
 .noteKey {
@@ -516,6 +601,20 @@
         position: relative;
     }
 
+    // .noteFixed {
+    //     font-size: 12px;
+    //     color: #FFFFFF;
+    //     font-weight: 600;
+    //     padding-bottom: 5px;
+    //     transform: scale(0.8);
+    //     white-space: nowrap;
+    // }
+
+    .dotFixed {
+        width: 5px;
+        height: 5px;
+    }
+
     .mark {
         position: absolute;
         left: -80%;
@@ -523,6 +622,42 @@
     }
 }
 
+.optionBtns {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    padding-bottom: 21px;
+
+    .oBtn {
+        width: 104px;
+        height: 46px;
+        border: none;
+        font-weight: 600;
+        font-size: 15px;
+        color: #fff;
+        cursor: pointer;
+        margin: 0 3px;
+
+        &.gamut {
+            background: url('./image/icon_btn_3.png') no-repeat center / contain;
+        }
+
+        &.play {
+            background: url('./image/icon_btn_2.png') no-repeat center / contain;
+        }
+
+        &.success {
+            background: url('./image/icon_btn_4.png') no-repeat center / contain;
+        }
+
+        &.disabled {
+            background: url('./image/icon_btn_1.png') no-repeat center / contain;
+            color: #616161;
+            cursor: not-allowed;
+        }
+    }
+}
+
 .fingeringContainer {
     position: relative;
     width: 100%;
@@ -702,8 +837,9 @@
         justify-content: center;
         align-items: center;
         flex-shrink: 0;
-        :global{
-            .van-button:active:before{
+
+        :global {
+            .van-button:active:before {
                 opacity: 0 !important;
             }
         }
@@ -774,7 +910,8 @@
             pointer-events: none;
         }
     }
-    .tipWrap{
+
+    .tipWrap {
         flex: 1;
         overflow: hidden;
     }
@@ -786,7 +923,7 @@
         border: 1.5px solid rgba(180, 165, 154, 1);
         color: rgba(68, 59, 59, 1);
         font-size: 12px;
-        
+
     }
 
     .toneAction {
@@ -795,11 +932,13 @@
         justify-content: center;
         align-items: center;
         padding: 16px 0;
+
         img {
             width: 45%;
             max-width: 128px;
             margin: 0 6px;
-            &:active{
+
+            &:active {
                 opacity: .85;
             }
         }
@@ -825,7 +964,7 @@
                 padding: 0;
                 padding-bottom: 40.5%;
                 flex-shrink: 0;
-                
+
 
                 &::before {
                     display: none;
@@ -842,7 +981,8 @@
                     opacity: .8;
                 }
             }
-            .van-button--primary{
+
+            .van-button--primary {
                 --van-button-plain-background: RGBA(255, 246, 231, 1);
             }
         }
@@ -892,7 +1032,8 @@
             padding: 4px 8px;
         }
     }
-    .hulusiBtn{
+
+    .hulusiBtn {
         font-size: 10px;
         text-wrap: nowrap;
     }

+ 1119 - 686
src/page-instrument/view-figner/index.tsx

@@ -1,35 +1,13 @@
-import {
-	PropType,
-	computed,
-	defineComponent,
-	nextTick,
-	onBeforeMount,
-	onMounted,
-	onUnmounted,
-	reactive,
-	ref,
-} from "vue";
+import { PropType, computed, defineComponent, nextTick, onBeforeMount, onMounted, onUnmounted, reactive, ref } from "vue";
 import styles from "./index.module.less";
 import icons from "./image/icons.json";
-import { FIGNER_INSTRUMENT_DATA, IFIGNER_INSTRUMENT_Note } from "/src/view/figner-preview";
-import {
-	ITypeFingering,
-	IVocals,
-	getFingeringConfig,
-	mappingVoicePart,
-	subjectFingering,
-} from "/src/view/fingering/fingering-config";
+import { FIGNER_INSTRUMENT_DATA, FIGNER_INSTRUMENT_REALKEY, IFIGNER_INSTRUMENT_Note } from "/src/view/figner-preview";
+import { ITypeFingering, IVocals, getFingeringConfig, mappingVoicePart, subjectFingering } from "/src/view/fingering/fingering-config";
 import { Howl } from "howler";
 import { storeData } from "/src/store";
-import {
-	api_back,
-	api_cloudLoading,
-	api_setRequestedOrientation,
-	api_setStatusBarVisibility,
-	isSpecialShapedScreen,
-} from "/src/helpers/communication";
+import { api_back, api_cloudLoading, api_setRequestedOrientation, api_setStatusBarVisibility, isSpecialShapedScreen } from "/src/helpers/communication";
 import Hammer from "hammerjs";
-import { Button, Icon, Loading, Popup, Progress, Space } from "vant";
+import { Button, Icon, Loading, Popover, Popup, Progress, Space } from "vant";
 import GuideIndex from "./guide/guide-index";
 import { getQuery } from "/src/utils/queryString";
 import { browser } from "/src/utils";
@@ -39,663 +17,1118 @@ import icon_loading_img from "./image/icon_loading_img.png";
 import state, { IPlatform } from "/src/state";
 
 export default defineComponent({
-	name: "viewFigner",
-	emits: ["close"],
-	props: {
-		show: {
-			type: Boolean,
-			default: true,
-		},
-		isComponent: {
-			type: Boolean,
-			default: false,
-		},
-		subject: {
-			type: String as PropType<IVocals>,
-			default: "",
-		},
-	},
-	setup(props, { emit }) {
-		const query = getQuery();
-		const browsInfo = browser();
-		const code = mappingVoicePart(query.code, "INSTRUMENT");
-		const subject = props.isComponent ? props.subject || "pan-flute" : code || "pan-flute";
-		const data = reactive({
-			loading: true,
-			subject: subject as any,
-			realKey: 0,
-			notes: [] as IFIGNER_INSTRUMENT_Note[],
-			tones: [] as IFIGNER_INSTRUMENT_Note[],
-			activeTone: {} as IFIGNER_INSTRUMENT_Note,
-			popupActiveTone: {} as IFIGNER_INSTRUMENT_Note,
-			activeToneName: "",
-			soundFonts: {} as any,
-			viewIndex: 0,
-			viewTotal: 1,
-			noteAudio: null as unknown as Howl,
-			transform: {
-				scale: 1,
-				x: 0,
-				y: 0,
-				startScale: 1,
-				startX: 0,
-				startY: 0,
-				transition: "",
-			},
-			tipShow: false,
-			tips: [] as IFIGNER_INSTRUMENT_Note[],
-
-			tnoteShow: false,
-			loadingSoundFonts: true,
-			loadingSoundProgress: 0,
-
-			huaweiPad: navigator?.userAgent?.includes("UAWEIVRD-W09") ? true : false,
-			paddingTop: "",
-			paddingLeft: "",
-		});
-		const fingerData = reactive({
-			relationshipIndex: 0,
-			subject: null as unknown as ITypeFingering,
-			fingeringInfo: subjectFingering(data.subject),
-		});
-		if (!props.isComponent) {
-			state.fingeringInfo = fingerData.fingeringInfo;
-		}
-
-		const getAPPData = async (type: "top" | "left") => {
-			const screenData = await isSpecialShapedScreen();
-			if (screenData?.content) {
-				// console.log("🚀 ~ screenData:", screenData.content);
-				const { isSpecialShapedScreen, notchHeight } = screenData.content;
-				if (isSpecialShapedScreen) {
-					if (type === "top") {
-						data.paddingTop = 25 + "px";
-					}
-					if (type === "left") {
-						data.paddingLeft = 25 + "px";
-					}
-				}
-			}
-		};
-
-		const getHeadTop = () => {
-			if (!browsInfo.ios && fingerData.fingeringInfo.orientation === 1) {
-				getAPPData("top");
-			}
-			if (!browsInfo.ios && fingerData.fingeringInfo.orientation === 0) {
-				getAPPData("left");
-			}
-		}
-
-		const getNotes = () => {
-			const fignerData = FIGNER_INSTRUMENT_DATA[data.subject as keyof typeof FIGNER_INSTRUMENT_DATA];
-			if (fignerData) {
-				data.tones = fignerData.tones || [];
-				if (data.tones.length) {
-					data.activeTone = data.tones[0];
-					data.popupActiveTone = data.tones[0];
-				}
-				data.tips = fignerData.tips || [];
-				setNotes();
-				setTimeout(() => {
-					data.loading = false;
-				}, 600);
-			}
-		};
-		const setNotes = () => {
-			const fignerData = FIGNER_INSTRUMENT_DATA[data.subject as keyof typeof FIGNER_INSTRUMENT_DATA];
-			if (fignerData) {
-				data.notes = fignerData[`list${data.activeTone.realName || ""}`];
-			}
-		};
-		const getFingeringData = async () => {
-			const subject: any = data.subject + (data.viewIndex === 0 ? "" : data.viewIndex);
-			console.log("🚀 ~ subject:", subject);
-			fingerData.subject = await getFingeringConfig(subject);
-		};
-		const createAudio = (url: string) => {
-			return new Promise((resolve) => {
-				const noteAudio = new Howl({
-					src: url,
-					loop: true,
-					onload: () => {
-						resolve(noteAudio);
-					},
-				});
-			});
-		};
-		const getSounFonts = async () => {
-			const pathname = /(192|localhost)/.test(location.origin) ? "/" : location.pathname;
-			data.loadingSoundFonts = true;
-			data.loadingSoundProgress = 0;
-			for (let i = 0; i < data.notes.length; i++) {
-				const note = data.notes[i];
-				// console.log("🚀 ~ note:", i);
-				let url = `${pathname}soundfonts/${data.subject}/`;
-				url += note.realName;
-				url += ".mp3";
-				data.soundFonts[note.realKey] = await createAudio(url);
-				data.loadingSoundProgress = Math.floor(((i + 1) / data.notes.length) * 100);
-			}
-			data.loadingSoundProgress = 100;
-			api_cloudLoading();
-			data.loadingSoundFonts = false;
-			// console.log("🚀 ~ data.soundFonts:", data.soundFonts);
-		};
-		onBeforeMount(() => {
-			getNotes();
-			if (["pan-flute", "ocarina"].includes(data.subject)) {
-				data.viewIndex = 1;
-			}
-			const o: any = {
-				"pan-flute": 2,
-				ocarina: 2,
-				piccolo: 2,
-				"hulusi-flute": 2,
-			};
-			data.viewTotal = o[data.subject] || 1;
-			getFingeringData();
-			getSounFonts();
-			getHeadTop();
-		});
-
-		const noteClick = (item: IFIGNER_INSTRUMENT_Note) => {
-			if (data.noteAudio) {
-				data.noteAudio.stop();
-				if (data.realKey === item.realKey) {
-					data.realKey = 0;
-					data.noteAudio = null as unknown as Howl;
-					return;
-				}
-			}
-			data.realKey = item.realKey;
-			data.noteAudio = data.soundFonts[item.realKey];
-			data.noteAudio.play();
-		};
-		const handleStop = () => {
-			if (data.noteAudio) {
-				data.noteAudio.stop();
-				data.realKey = 0;
-				data.noteAudio = null as unknown as Howl;
-			}
-		};
-
-		/** 返回 */
-		const handleBack = () => {
-			handleStop();
-			if (props.isComponent) {
-				console.log("关闭");
-				emit("close");
-				return;
-			} else {
-				// if (fingerData.fingeringInfo.orientation === 0) {
-				// 	api_setRequestedOrientation(1);
-				// }
-			}
-			// 不在APP中,
-			if (!storeData.isApp) {
-				window.close();
-				return;
-			}
-			api_back();
-		};
-
-		onMounted(() => {
-			loadElement();
-			api_setStatusBarVisibility();
-		});
-		const loadElement = () => {
-			const fingeringContainer = document.getElementById("fingeringContainer");
-			// console.log("🚀 ~ fingeringContainer:", fingeringContainer);
-			const mc = new Hammer.Manager(fingeringContainer as HTMLElement);
-			mc.add(new Hammer.Pan({ threshold: 0, pointers: 0 }));
-			mc.add(new Hammer.Pinch({ threshold: 0 })).recognizeWith([mc.get("pan")]);
-			// mc.get("pan").set({ direction: Hammer.DIRECTION_ALL });
-			// mc.get("pinch").set({ enable: true });
-			mc.on("panstart pinchstart", function (ev) {
-				data.transform.transition = "";
-			});
-			mc.on("panmove pinchmove", function (ev) {
-				if (ev.type === "pinchmove") {
-					// console.log("🚀 ~ ev:", ev.type, ev.scale, ev.deltaX, ev.deltaY);
-					data.transform.scale = ev.scale * data.transform.startScale;
-					data.transform.x = data.transform.startX + ev.deltaX;
-					data.transform.y = data.transform.startY + ev.deltaY;
-				}
-				if (ev.type === "panmove") {
-					// console.log("🚀 ~ ev:", ev.type, ev.deltaX, ev.deltaY);
-					data.transform.x = data.transform.startX + ev.deltaX;
-					data.transform.y = data.transform.startY + ev.deltaY;
-				}
-			});
-			//
-			mc.on("hammer.input", function (ev) {
-				// console.log("🚀 ~ ev:", ev.type, ev.isFinal);
-				if (ev.isFinal) {
-					data.transform.startScale = data.transform.scale;
-					data.transform.startX = data.transform.x;
-					data.transform.startY = data.transform.y;
-				}
-			});
-		};
-		const resetElement = () => {
-			data.transform.transition = "all 0.3s";
-			nextTick(() => {
-				data.transform.scale = 1;
-				data.transform.x = 0;
-				data.transform.y = 0;
-				data.transform.startScale = 1;
-				data.transform.startX = 0;
-				data.transform.startY = 0;
-			});
-		};
-
-		const pageVisible = usePageVisibility();
-		watch(
-			() => pageVisible.value,
-			(val) => {
-				if (val === "hidden") {
-					console.log("页面隐藏停止播放");
-					handleStop();
-				}
-			}
-		);
-		/** 课件播放 */
-		const changePlay = (res: any) => {
-			if (res?.data?.api === "setPlayState") {
-				handleStop();
-			}
-		};
-
-		const noteBoxRef = ref();
-		const scrollNoteBox = (type: "left" | "right") => {
-			const width = noteBoxRef.value.offsetWidth / 2;
-			(noteBoxRef.value as unknown as HTMLElement).scrollBy({
-				left: type === "left" ? -width : width,
-				behavior: "smooth",
-			});
-		};
-
-		/** 滚轮缩放 */
-		const handleWheel = (e: WheelEvent) => {
-			e.preventDefault();
-			if (e.deltaY > 0) {
-				data.transform.scale -= 0.1;
-				if (data.transform.scale <= 0.5) {
-					data.transform.scale = 0.5;
-				}
-			} else {
-				data.transform.scale += 0.1;
-				if (data.transform.scale >= 2) {
-					data.transform.scale = 2;
-				}
-			}
-		};
-
-		onMounted(() => {
-			window.addEventListener("message", changePlay);
-			const fingeringContainer = document.getElementById("fingeringContainer");
-			fingeringContainer?.addEventListener("wheel", handleWheel);
-		});
-
-		onUnmounted(() => {
-			window.removeEventListener("message", changePlay);
-			const fingeringContainer = document.getElementById("fingeringContainer");
-			fingeringContainer?.removeEventListener("wheel", handleWheel);
-		});
-
-		const containerBox = computed(() => {
-			if (state.platform === IPlatform.PC || query.modelType) {
-				return {
-					paddingTop: "1rem",
-					paddingBottom: "",
-				};
-			}
-			if (data.subject === "hulusi-flute") {
-				return {
-					paddingTop: "3.1rem",
-					paddingBottom: ".8rem",
-				};
-			} else if (data.subject === "piccolo") {
-				return {
-					paddingTop: "4rem",
-					paddingBottom: ".8rem",
-				};
-			} else if (data.subject === "pan-flute") {
-				return {
-					paddingTop: "0",
-					paddingBottom: "0",
-				};
-			} else if (data.subject === "ocarina") {
-				return {
-					paddingTop: "1.2rem",
-					paddingBottom: "0",
-				};
-			} else if (data.subject === "melodica") {
-				return {
-					paddingTop: "2.8rem",
-					paddingBottom: "1.8rem",
-				};
-			} else {
-				return {
-					paddingTop: "",
-					paddingBottom: "",
-				};
-			}
-		});
-		return () => {
-			const relationship = fingerData.subject?.relationship?.[data.realKey] || [];
-			const rs: number[] = Array.isArray(relationship[1])
-				? relationship[fingerData.relationshipIndex]
-				: relationship;
-			const canTizhi = Array.isArray(relationship[1]);
-			return (
-				<div
-					class={[
-						styles.fingerBox,
-						state.platform !== IPlatform.PC &&
-						!query.modelType &&
-						fingerData.fingeringInfo.orientation === 1
-							? styles.fingerBottom
-							: styles.fingerRight,
-					]}
-				>
-					<div
-						class={styles.head}
-						style={{
-							paddingTop: data.paddingTop ? data.paddingTop : "",
-							paddingLeft: data.paddingLeft ? data.paddingLeft : "",
-						}}
-					>
-						<div class={styles.left}>
-							<button class={[styles.backBtn]} onClick={() => handleBack()}>
-								<img src={icons.icon_back} />
-							</button>
-							{data.subject !== "melodica" && (
-								<div
-									class={styles.baseBtn}
-									onClick={() => {
-										data.viewIndex++;
-										if (data.viewIndex > data.viewTotal) {
-											if (["pan-flute", "ocarina"].includes(data.subject)) {
-												data.viewIndex = 1;
-											} else {
-												data.viewIndex = 0;
-											}
-										}
-										getFingeringData();
-									}}
-								>
-									<img src={icons.icon_toggle} />
-									<span>切换视图</span>
-								</div>
-							)}
-						</div>
-						<div class={styles.rightBtn}>
-							<div class={styles.baseBtn} onClick={() => resetElement()}>
-								<img src={icons.icon_2_0} />
-								<span>还原</span>
-							</div>
-							<div
-								class={styles.baseBtn}
-								onClick={() => {
-									resetElement();
-									data.tipShow = !data.tipShow;
-								}}
-							>
-								<img src={icons.icon_2_1} />
-								<span>使用说明</span>
-							</div>
-						</div>
-					</div>
-					<div class={styles.fingerContent}>
-						<div class={styles.wrapFinger}>
-							<div
-								id="fingeringContainer"
-								class={styles.boxFinger}
-								style={{
-									paddingTop: containerBox.value.paddingTop,
-									paddingBottom: containerBox.value.paddingBottom,
-								}}
-							>
-								<div
-									style={{
-										transform: `translate3d(${data.transform.x}px,${data.transform.y}px,0px) scale(${data.transform.scale})`,
-										transition: data.transform.transition,
-									}}
-									class={[styles.fingeringContainer]}
-								>
-									<div class={styles.imgs}>
-										<img src={fingerData.subject?.json?.full} />
-										{rs.map((key: number | string, index: number) => {
-											const nk: string =
-												typeof key === "string" ? key.replace("active-", "") : String(key);
-											return <img data-index={nk} src={fingerData.subject?.json?.[nk]} />;
-										})}
-										<div
-											style={{ left: data.viewIndex == 2 ? "0" : "64%" }}
-											class={[styles.tizhi, canTizhi && styles.canDisplay]}
-											onClick={() =>
-												(fingerData.relationshipIndex = fingerData.relationshipIndex === 0 ? 1 : 0)
-											}
-										>
-											替指
-										</div>
-										<div
-											id="finger-note-2"
-											style={{ left: "50%", transform: "translateX(-50%)" }}
-											class={styles.tizhi}
-											onClick={() =>
-												(fingerData.relationshipIndex = fingerData.relationshipIndex === 0 ? 1 : 0)
-											}
-										></div>
-									</div>
-								</div>
-							</div>
-							<div
-								class={styles.notes}
-								style={{
-									paddingLeft: data.paddingLeft ? data.paddingLeft : "",
-								}}
-							>
-								<Button class={styles.noteBtn} onClick={() => scrollNoteBox("left")}>
-									<Icon name="arrow-left" />
-								</Button>
-								<div
-									class={[
-										styles.noteContent,
-										browsInfo.ios ? "" : styles.noteContentWrap,
-										data.huaweiPad && styles.huaweiPad,
-									]}
-								>
-									<div ref={noteBoxRef} class={styles.noteBox}>
-										{data.notes.map((note: IFIGNER_INSTRUMENT_Note, index: number) => {
-											const steps = new Array(Math.abs(note.step)).fill(1);
-											return (
-												<div
-													id={index == 0 ? "finger-note-0" : ""}
-													draggable={false}
-													class={styles.note}
-													onClick={() => noteClick(note)}
-												>
-													{data.realKey === note.realKey ? (
-														<img draggable={false} src={icons.icon_btn_ylow} />
-													) : (
-														<img draggable={false} src={icons.icon_btn_blue} />
-													)}
-
-													<div
-														class={[styles.noteKey, data.realKey === note.realKey && styles.keyActive]}
-													>
-														{note.step > 0 ? steps.map((n) => <span class={styles.dot}></span>) : null}
-
-														<div class={styles.noteName}>
-															<sup>{note.mark && (note.mark === "rise" ? "#" : "b")}</sup>
-															{note.key}
-														</div>
-														{note.step < 0 ? steps.map((n) => <span class={styles.dot}></span>) : null}
-													</div>
-												</div>
-											);
-										})}
-									</div>
-								</div>
-								<Button class={styles.noteBtn} onClick={() => scrollNoteBox("right")}>
-									<Icon name="arrow" />
-								</Button>
-							</div>
-						</div>
-						<div class={[styles.tips, data.tipShow ? "" : styles.tipHidden]}>
-							<div class={styles.tipTitle}>
-								<div class={styles.tipTitleName}>{fingerData.fingeringInfo.code}使用说明</div>
-								<Button class={styles.tipClose} onClick={() => (data.tipShow = false)}>
-									<Icon name="cross" size={19} color="#fff" />
-								</Button>
-							</div>
-							<div class={styles.iconBook}></div>
-							<div class={styles.tipContentbox}>
-								<div class={styles.tipContent}>
-									{data.tips.map((tip, tipIndex) => (
-										<div class={styles.tipItem}>
-											<div class={styles.iconWrap}>
-												<div class={styles.tipItemIcon}>{tipIndex + 1}</div>
-											</div>
-											<div>
-												{tip.name}: {tip.realName}
-											</div>
-										</div>
-									))}
-								</div>
-							</div>
-						</div>
-						{data.loadingSoundFonts && (
-							<div class={styles.loading}>
-								<div class={styles.loadingWrap}>
-									<img class={styles.loadingIcon} src={icon_loading_img} />
-									<Progress percentage={data.loadingSoundProgress} />
-									<div class={styles.loadingTip}>加载中,请稍后…</div>
-								</div>
-							</div>
-						)}
-					</div>
-					{!!data.tones.length && (
-						<>
-							{fingerData.fingeringInfo.name == "hulusi-flute" ? (
-								<div
-									id="finger-note-1"
-									class={[styles.toggleBtn, styles.toggleBtnhulusi]}
-									onClick={() => (data.tnoteShow = true)}
-								>
-									<div>
-										全按作
-										<div class={[styles.noteKey]}>
-											{data.activeTone.step > 0 ? <span class={styles.dot}></span> : null}
-
-											<div class={styles.noteName}>
-												<sup>
-													{data.activeTone.mark && (data.activeTone.mark === "rise" ? "#" : "b")}
-												</sup>
-												{data.activeTone.key}
-											</div>
-											{data.activeTone.step < 0 ? <span class={styles.dot}></span> : null}
-										</div>
-									</div>
-									<img src={icons.icon_arrow} />
-								</div>
-							) : (
-								<div id="finger-note-1" class={styles.toggleBtn} onClick={() => (data.tnoteShow = true)}>
-									<div style={{ marginTop: "-4px" }}>
-										<sup>{data.activeTone.mark && (data.activeTone.mark === "rise" ? "#" : "b")}</sup>
-										{data.activeTone.name}
-									</div>
-									调
-									<img src={icons.icon_arrow} />
-								</div>
-							)}
-						</>
-					)}
-
-					<Popup
-						class="tonePopup"
-						v-model:show={data.tnoteShow}
-						position={
-							state.platform !== IPlatform.PC &&
-							!query.modelType &&
-							fingerData.fingeringInfo.orientation === 1
-								? "bottom"
-								: "right"
-						}
-					>
-						<div class={styles.tones}>
-							<div class={styles.toneTitle}>
-								<div class={styles.tipTitleName}>移调</div>
-								<Button class={styles.tipClose} onClick={() => (data.tnoteShow = false)}>
-									<Icon name="cross" size={19} color="#fff" />
-								</Button>
-							</div>
-							<div class={styles.tipContentbox}>
-								<div class={styles.tipContent}>
-									<div class={styles.tipWrap}>
-										<Space size={0} class={styles.toneContent}>
-											{data.tones.map((tone: IFIGNER_INSTRUMENT_Note) => {
-												const steps = new Array(Math.abs(tone.step)).fill(1);
-												return (
-													<Button
-														class={[fingerData.fingeringInfo.name == "hulusi-flute" && styles.hulusiBtn]}
-														round
-														plain
-														type={
-															data.popupActiveTone.realName === tone.realName ? "primary" : "default"
-														}
-														onClick={() => {
-															data.popupActiveTone = tone;
-															setNotes();
-														}}
-													>
-														{fingerData.fingeringInfo.name == "hulusi-flute" ? (
-															<div style={{ display: "flex", alignItems: "center" }}>
-																全按作
-																<div class={[styles.noteKey, styles.hulusiNoteKey]}>
-																	{tone.step > 0 ? <span class={styles.dot}></span> : null}
-																	<div class={styles.noteName} style={{ fontSize: "0.25rem" }}>
-																		<sup>{tone.mark && (tone.mark === "rise" ? "#" : "b")}</sup>
-																		{tone.key}
-																	</div>
-																	{tone.step < 0 ? <span class={styles.dot}></span> : null}
-																</div>
-															</div>
-														) : (
-															<div class={styles.noteName}>
-																<sup>{tone.mark && (tone.mark === "rise" ? "#" : "b")}</sup>
-																{tone.name}
-															</div>
-														)}
-													</Button>
-												);
-											})}
-										</Space>
-									</div>
-									<div class={styles.toneAction}>
-										<img onClick={() => (data.tnoteShow = false)} src={icons.icon_action_cancel} />
-										<img
-											onClick={() => {
-												data.activeTone = data.popupActiveTone;
-												setNotes();
-												data.tnoteShow = false;
-											}}
-											src={icons.icon_action_confirm}
-										/>
-									</div>
-								</div>
-							</div>
-						</div>
-					</Popup>
-
-					{props.show && !data.loading && !data.loadingSoundFonts && (
-						<GuideIndex showGuide={false} list={["finger"]} />
-					)}
-				</div>
-			);
-		};
-	},
+  name: "viewFigner",
+  emits: ["close"],
+  props: {
+    show: {
+      type: Boolean,
+      default: true,
+    },
+    isComponent: {
+      type: Boolean,
+      default: false,
+    },
+    subject: {
+      type: String as PropType<IVocals>,
+      default: "",
+    },
+  },
+  setup(props, { emit }) {
+    const query = getQuery();
+    const browsInfo = browser();
+    const code = mappingVoicePart(query.code, "INSTRUMENT");
+    const subject = props.isComponent ? props.subject || "pan-flute" : code || "pan-flute";
+    const data = reactive({
+      loading: true,
+      subject: subject as any,
+      realKey: 0,
+      notes: [] as IFIGNER_INSTRUMENT_Note[],
+      tones: [] as IFIGNER_INSTRUMENT_Note[],
+      activeTone: {} as IFIGNER_INSTRUMENT_Note,
+      popupActiveTone: {} as IFIGNER_INSTRUMENT_Note,
+      activeToneName: "",
+      soundFonts: {} as any,
+      viewIndex: 0,
+      viewTotal: 1,
+      noteAudio: null as unknown as Howl,
+      transform: {
+        scale: 1,
+        x: 0,
+        y: 0,
+        startScale: 1,
+        startX: 0,
+        startY: 0,
+        transition: "",
+      },
+      tipShow: false,
+      tips: [] as IFIGNER_INSTRUMENT_Note[],
+
+      tnoteShow: false,
+      loadingSoundFonts: true,
+      loadingSoundProgress: 0,
+
+      huaweiPad: navigator?.userAgent?.includes("UAWEIVRD-W09") ? true : false,
+      paddingTop: "",
+      paddingLeft: "",
+      subjects: [
+        {
+          text: "排箫",
+          value: "pan-flute",
+          className: "",
+        },
+        {
+          text: "陶笛",
+          value: "ocarina",
+          className: "",
+        },
+        {
+          text: "葫芦丝",
+          value: "hulusi-flute",
+          className: "",
+        },
+        {
+          text: "竖笛",
+          value: "piccolo",
+          className: "",
+        },
+        {
+          text: "口风琴",
+          value: "melodica",
+          className: "",
+        },
+      ],
+      fingeringModeList: [
+        {
+          text: "指法模式",
+          value: "fingeringMode",
+          icon: icons.icon_click,
+        },
+        {
+          text: "听音模式",
+          value: "listenMode",
+          icon: icons.icon_listen,
+        },
+        {
+          text: "音阶模式",
+          value: "scaleMode",
+          icon: icons.icon_mode,
+        },
+      ],
+      fingeringMode: query.listenMode || ("scaleMode" as "fingeringMode" | "listenMode" | "scaleMode"), // 模式
+      noteType: "all" as "#c" | "all", // 音调
+    });
+    const fingerData = reactive({
+      relationshipIndex: 0,
+      subject: null as unknown as ITypeFingering,
+      fingeringInfo: subjectFingering(data.subject),
+    });
+    if (!props.isComponent) {
+      state.fingeringInfo = fingerData.fingeringInfo;
+    }
+
+    const getAPPData = async (type: "top" | "left") => {
+      const screenData = await isSpecialShapedScreen();
+      if (screenData?.content) {
+        // console.log("🚀 ~ screenData:", screenData.content);
+        const { isSpecialShapedScreen, notchHeight } = screenData.content;
+        if (isSpecialShapedScreen) {
+          if (type === "top") {
+            data.paddingTop = 25 + "px";
+          }
+          if (type === "left") {
+            data.paddingLeft = 25 + "px";
+          }
+        }
+      }
+    };
+
+    const getHeadTop = () => {
+      if (!browsInfo.ios && fingerData.fingeringInfo.orientation === 1) {
+        getAPPData("top");
+      }
+      if (!browsInfo.ios && fingerData.fingeringInfo.orientation === 0) {
+        getAPPData("left");
+      }
+    };
+
+    const getNotes = () => {
+      const fignerData = FIGNER_INSTRUMENT_DATA[data.subject as keyof typeof FIGNER_INSTRUMENT_DATA];
+      if (fignerData) {
+        data.tones = fignerData.tones || [];
+        if (data.tones.length) {
+          data.activeTone = data.tones[0];
+          data.popupActiveTone = data.tones[0];
+        }
+        data.tips = fignerData.tips || [];
+        setNotes();
+        setTimeout(() => {
+          data.loading = false;
+        }, 600);
+      }
+    };
+    const setNotes = () => {
+      const fignerData = FIGNER_INSTRUMENT_DATA[data.subject as keyof typeof FIGNER_INSTRUMENT_DATA];
+      if (fignerData) {
+        const tempNotes = fignerData[`list${data.activeTone.realName || ""}`];
+        const appendNote: any = [];
+        tempNotes.forEach((note: any) => {
+          note.steps = new Array(Math.abs(note.step)).fill(1);
+          if (FIGNER_INSTRUMENT_REALKEY.includes(note.realKey)) {
+            appendNote.push(note);
+          }
+        });
+        // 判断是音符状态
+        data.notes = data.noteType === "#c" ? appendNote : tempNotes;
+        // data.notes = fignerData[`list${data.activeTone.realName || ""}`];
+      }
+    };
+    const getFingeringData = async () => {
+      const subject: any = data.subject + (data.viewIndex === 0 ? "" : data.viewIndex);
+      console.log("🚀 ~ subject:", subject);
+      fingerData.subject = await getFingeringConfig(subject);
+    };
+    const createAudio = (url: string) => {
+      return new Promise((resolve) => {
+        const noteAudio = new Howl({
+          src: url,
+          loop: true,
+          onload: () => {
+            resolve(noteAudio);
+          },
+        });
+      });
+    };
+    const getSounFonts = async () => {
+      const pathname = /(192|localhost)/.test(location.origin) ? "/" : location.pathname;
+      data.loadingSoundFonts = true;
+      data.loadingSoundProgress = 0;
+      for (let i = 0; i < data.notes.length; i++) {
+        const note = data.notes[i];
+        // console.log("🚀 ~ note:", i);
+        let url = `${pathname}soundfonts/${data.subject}/`;
+        url += note.realName;
+        url += ".mp3";
+        data.soundFonts[note.realKey] = await createAudio(url);
+        data.loadingSoundProgress = Math.floor(((i + 1) / data.notes.length) * 100);
+      }
+      data.loadingSoundProgress = 100;
+      api_cloudLoading();
+      data.loadingSoundFonts = false;
+    };
+
+    const selectSubjectType = (subject: string) => {
+      data.subjects.forEach((item: any) => {
+        if (item.value === subject) {
+          item.className = styles.selected;
+        } else {
+          item.className = "";
+        }
+      });
+    };
+
+    // 切换当前模式
+    const onChangeFingeringModel = () => {
+      //
+      if (playAction.listenLock) return;
+      if (playAction.showAnswerLoading) return;
+      if (data.fingeringMode === "scaleMode") {
+        data.fingeringMode = "listenMode";
+      } else if (data.fingeringMode === "listenMode") {
+        data.fingeringMode = "fingeringMode";
+      } else if (data.fingeringMode === "fingeringMode") {
+        data.fingeringMode = "scaleMode";
+      }
+      resetMode(true, 0);
+      __init();
+    };
+
+    const __init = () => {
+      getNotes();
+
+      selectSubjectType(data.subject);
+
+      if (data.fingeringMode === "fingeringMode") {
+        console.log(data.subject, "data.subject");
+        if (data.subject === "pan-flute") {
+          data.viewIndex = 3;
+        } else if (["pan-flute", "ocarina", "melodica"].includes(data.subject)) {
+          data.viewIndex = 1;
+        }
+      } else {
+        if (["pan-flute", "ocarina"].includes(data.subject)) {
+          data.viewIndex = 1;
+        }
+      }
+
+      const o: any = {
+        "pan-flute": 2,
+        ocarina: 2,
+        piccolo: 2,
+        "hulusi-flute": 2,
+      };
+      data.viewTotal = o[data.subject] || 1;
+      getFingeringData();
+      getSounFonts();
+      getHeadTop();
+    };
+
+    onBeforeMount(() => {
+      state.platform = query.platform?.toLocaleUpperCase() || "";
+      __init();
+    });
+
+    /**
+     * 播放音频
+     * @param item 音频节点
+     * @param showNote 是否显示对应的指法
+     * @returns
+     */
+    const noteClick = (item: IFIGNER_INSTRUMENT_Note, showNote = true) => {
+      // console.log('音高', item.realKey)
+      if (data.noteAudio) {
+        data.noteAudio.stop();
+        if (data.realKey === item.realKey) {
+          data.realKey = 0;
+          data.noteAudio = null as unknown as Howl;
+          return;
+        }
+      }
+      if (showNote) {
+        data.realKey = item.realKey;
+      }
+      data.noteAudio = data.soundFonts[item.realKey];
+      data.noteAudio.play();
+    };
+    const handleStop = () => {
+      if (data.noteAudio) {
+        data.noteAudio.stop();
+        data.realKey = 0;
+        data.noteAudio = null as unknown as Howl;
+      }
+    };
+
+    /** 返回 */
+    const handleBack = () => {
+      // platform: query.platform,
+      handleStop();
+      if (props.isComponent) {
+        console.log("关闭");
+        emit("close");
+        return;
+      } else if (state.platform === IPlatform.PC) {
+        // 老师端,首页
+        window.parent.postMessage(
+          {
+            api: "iframe_exit",
+          },
+          "*"
+        );
+        return;
+        // if (fingerData.fingeringInfo.orientation === 0) {
+        // 	api_setRequestedOrientation(1);
+        // }
+      }
+      // 不在APP中,
+      if (!storeData.isApp) {
+        window.close();
+        return;
+      }
+      api_back();
+    };
+
+    onMounted(() => {
+      loadElement();
+      api_setStatusBarVisibility();
+    });
+    const loadElement = () => {
+      const fingeringContainer = document.getElementById("fingeringContainer");
+      // console.log("🚀 ~ fingeringContainer:", fingeringContainer);
+      const mc = new Hammer.Manager(fingeringContainer as HTMLElement);
+      mc.add(new Hammer.Pan({ threshold: 0, pointers: 0 }));
+      mc.add(new Hammer.Pinch({ threshold: 0 })).recognizeWith([mc.get("pan")]);
+      // mc.get("pan").set({ direction: Hammer.DIRECTION_ALL });
+      // mc.get("pinch").set({ enable: true });
+      mc.on("panstart pinchstart", function (ev) {
+        data.transform.transition = "";
+      });
+      mc.on("panmove pinchmove", function (ev) {
+        if (ev.type === "pinchmove") {
+          // console.log("🚀 ~ ev:", ev.type, ev.scale, ev.deltaX, ev.deltaY);
+          data.transform.scale = ev.scale * data.transform.startScale;
+          data.transform.x = data.transform.startX + ev.deltaX;
+          data.transform.y = data.transform.startY + ev.deltaY;
+        }
+        if (ev.type === "panmove") {
+          // console.log("🚀 ~ ev:", ev.type, ev.deltaX, ev.deltaY);
+          data.transform.x = data.transform.startX + ev.deltaX;
+          data.transform.y = data.transform.startY + ev.deltaY;
+        }
+      });
+      //
+      mc.on("hammer.input", function (ev) {
+        // console.log("🚀 ~ ev:", ev.type, ev.isFinal);
+        if (ev.isFinal) {
+          data.transform.startScale = data.transform.scale;
+          data.transform.startX = data.transform.x;
+          data.transform.startY = data.transform.y;
+        }
+      });
+    };
+    const resetElement = () => {
+      data.transform.transition = "all 0.3s";
+      nextTick(() => {
+        data.transform.scale = 1;
+        data.transform.x = 0;
+        data.transform.y = 0;
+        data.transform.startScale = 1;
+        data.transform.startX = 0;
+        data.transform.startY = 0;
+      });
+    };
+
+    const pageVisible = usePageVisibility();
+    watch(
+      () => pageVisible.value,
+      (val) => {
+        if (val === "hidden") {
+          console.log("页面隐藏停止播放");
+          handleStop();
+        }
+      }
+    );
+    /** 课件播放 */
+    const changePlay = (res: any) => {
+      if (res?.data?.api === "setPlayState") {
+        handleStop();
+      }
+    };
+
+    const noteBoxRef = ref();
+    const scrollNoteBox = (type: "left" | "right") => {
+      const width = noteBoxRef.value.offsetWidth / 2;
+      (noteBoxRef.value as unknown as HTMLElement).scrollBy({
+        left: type === "left" ? -width : width,
+        behavior: "smooth",
+      });
+    };
+
+    const playStatus = reactive({
+      gamut: false, // 是否播放音阶
+      gamutTimer: null as any, // 播放音阶定时器
+      answer: false, // 是否显示答案
+      action: false, // 是否开始播放
+    });
+
+    /** 音符切换 */
+    const noteChangeShow = () => {
+      // 播放音阶时不能切换
+      if (playStatus.gamut) return;
+      // 开始答题不能切换
+      if (playStatus.action) return;
+      if (data.noteType === "all") {
+        data.noteType = "#c";
+      } else {
+        data.noteType = "all";
+      }
+      getNotes();
+    };
+
+    // 开始播放音阶
+    const onGamutPlayOrPause = async () => {
+      if (playStatus.gamut) {
+        playStatus.gamut = false;
+        gaumntPause();
+      } else {
+        // 不管当前显示在哪个音老师滚动到开始位置
+        (noteBoxRef.value as unknown as HTMLElement).scroll({
+          left: 0,
+          top: 0,
+          behavior: "smooth",
+        });
+        playStatus.gamut = true;
+        const notes = data.notes;
+        let scrollCount = 0;
+        for (let i = 0; i < notes.length; i++) {
+          if (!playStatus.gamut) return false;
+          const activeDom = document.querySelectorAll(".note-class")[i] as any;
+          if (activeDom.offsetLeft >= noteBoxRef.value.offsetWidth + (noteBoxRef.value.offsetWidth / 2) * scrollCount - activeDom.offsetWidth) {
+            scrollNoteBox("right");
+            scrollCount++;
+          }
+          await gaumtPlay(notes[i]);
+        }
+
+        // // 处理播放到最后一个
+        setTimeout(() => {
+          playStatus.gamut = false;
+          gaumntPause();
+        }, 667);
+      }
+    };
+    const gaumtPlay = (note: any, status?: boolean) => {
+      return new Promise((resolve) => {
+        playStatus.gamutTimer = setTimeout(() => {
+          if (playStatus.gamut || status) {
+            noteClick(note);
+          }
+          resolve(note);
+        }, 667);
+      });
+    };
+    const gaumntPause = () => {
+      clearTimeout(playStatus.gamutTimer);
+      if (data.noteAudio) {
+        data.noteAudio.stop();
+        data.realKey = 0;
+        data.noteAudio = null as unknown as Howl;
+      }
+    };
+
+    /** 开始播放 */
+    const playAction = reactive({
+      exampleAnser: {} as any, // 示例声音
+      standardAnswer: {} as any, // 标准答案key
+      showAnswerLoading: false, // 显示按答案中
+      listenModeStatus: false, // 是否开始了模式
+      listenLock: false,
+      listenTipsStatus: false, // 开始播放状态
+      /** 0: 未答,1: 答对,2: 答错 */
+      userAnswerStatus: 0 as 0 | 1 | 2, // 用户回答状态
+      userAnswer: {} as any, // 用户答的数据
+    });
+    const onActionPlay = async () => {
+      if (playAction.listenLock) return;
+      if (playAction.showAnswerLoading) return;
+      playStatus.action = true;
+      playStatus.answer = true;
+      // 先暂停播放声音
+      gaumntPause();
+      if (data.fingeringMode === "fingeringMode") {
+        onFingeringMode();
+      } else if (data.fingeringMode === "listenMode") {
+        if (playAction.listenModeStatus) {
+          playAction.listenLock = true;
+          await fingeringPlay(playAction.standardAnswer, 1500, false);
+          gaumntPause();
+          playAction.listenLock = false;
+        } else {
+          onListenMode();
+        }
+      }
+    };
+
+    // 指法模式
+    const fingeringPlay = (note: any, timer = 1500, showNote = true) => {
+      return new Promise((resolve) => {
+        noteClick(note, showNote);
+        setTimeout(() => {
+          resolve(note);
+        }, timer);
+      });
+    };
+    const onFingeringMode = () => {
+      const randomIndex = Math.floor(Math.random() * data.notes.length);
+      playAction.standardAnswer = data.notes[randomIndex];
+      data.realKey = data.notes[randomIndex].realKey;
+      if (playAction.listenModeStatus) {
+        return;
+      }
+      playAction.listenModeStatus = true; // 是否开始听音
+      playAction.listenLock = true; // 锁
+      playAction.listenTipsStatus = true;
+      setTimeout(() => {
+        playAction.listenTipsStatus = false;
+        playAction.listenLock = false; // 锁
+      }, 2000);
+    };
+    // 听音模式
+    const onListenMode = async () => {
+      playAction.listenModeStatus = true; // 是否开始听音
+      playAction.listenLock = true; // 锁
+      playAction.listenTipsStatus = true;
+      // 设置并保存示例数据
+      let randomIndex = Math.floor(Math.random() * data.notes.length);
+      playAction.exampleAnser = data.notes[randomIndex];
+      data.realKey = playAction.exampleAnser.realKey;
+      scrollAnswer(playAction.exampleAnser.realKey);
+      await fingeringPlay(playAction.exampleAnser);
+      data.realKey = 0;
+      playAction.exampleAnser = {};
+      gaumntPause();
+      setTimeout(async () => {
+        // 设置答题数据
+        randomIndex = Math.floor(Math.random() * data.notes.length);
+        playAction.standardAnswer = data.notes[randomIndex];
+        await fingeringPlay(data.notes[randomIndex], 1500, false);
+        gaumntPause();
+        playAction.listenLock = false;
+        playAction.listenTipsStatus = false;
+      }, 1000);
+    };
+
+    // 显示答案
+    const onShowAnswer = async () => {
+      if (playAction.listenLock) return;
+      playAction.showAnswerLoading = true;
+      scrollAnswer(playAction.standardAnswer.realKey);
+      await fingeringPlay(playAction.standardAnswer);
+      resetMode(true, 0);
+      // }
+    };
+    // 滚动到对应答案位置
+    const scrollAnswer = (realKey?: any) => {
+      const tempRealKey = realKey || data.realKey;
+      const index = data.notes.findIndex((item: any) => item.realKey === tempRealKey);
+      const activeDom = document.querySelectorAll(".note-class")[index] as any;
+      if (activeDom) {
+        const aWidth = activeDom.offsetWidth;
+        const width = noteBoxRef.value.offsetWidth;
+        const aLeft = Math.max(activeDom?.offsetLeft - aWidth, 0);
+        (noteBoxRef.value as unknown as HTMLElement).scroll({
+          left: Math.max(aLeft - width / 2, 0),
+          top: 0,
+          behavior: "smooth",
+        });
+      }
+    };
+
+    /**
+     * 重置播放状态
+     * @param status 是否全部重置
+     * @param timer 延时时长(默认2s)
+     */
+    const resetMode = (status = true, timer = 2000) => {
+      // 2秒钟后重置
+      setTimeout(() => {
+        gaumntPause();
+        if (status) {
+          playAction.standardAnswer = {};
+          playAction.showAnswerLoading = false;
+          playAction.userAnswerStatus = 0;
+          playAction.userAnswer = {};
+          playAction.listenModeStatus = false;
+          playStatus.action = false;
+          playStatus.answer = false;
+          playStatus.gamut = false;
+          data.realKey = 0;
+        } else {
+          playAction.userAnswerStatus = 0;
+          playAction.userAnswer = {};
+        }
+      }, timer);
+    };
+
+    /** 滚轮缩放 */
+    const handleWheel = (e: WheelEvent) => {
+      e.preventDefault();
+      if (e.deltaY > 0) {
+        data.transform.scale -= 0.1;
+        if (data.transform.scale <= 0.5) {
+          data.transform.scale = 0.5;
+        }
+      } else {
+        data.transform.scale += 0.1;
+        if (data.transform.scale >= 2) {
+          data.transform.scale = 2;
+        }
+      }
+    };
+
+    onMounted(() => {
+      window.addEventListener("message", changePlay);
+      const fingeringContainer = document.getElementById("fingeringContainer");
+      fingeringContainer?.addEventListener("wheel", handleWheel);
+    });
+
+    onUnmounted(() => {
+      window.removeEventListener("message", changePlay);
+      const fingeringContainer = document.getElementById("fingeringContainer");
+      fingeringContainer?.removeEventListener("wheel", handleWheel);
+    });
+
+    const containerBox = computed(() => {
+      if (state.platform === IPlatform.PC || query.modelType) {
+        return {
+          paddingTop: "1rem",
+          paddingBottom: "",
+        };
+      }
+      if (data.subject === "hulusi-flute") {
+        return {
+          paddingTop: "3.1rem",
+          paddingBottom: ".8rem",
+        };
+      } else if (data.subject === "piccolo") {
+        return {
+          paddingTop: "4rem",
+          paddingBottom: ".8rem",
+        };
+      } else if (data.subject === "pan-flute") {
+        return {
+          paddingTop: "0",
+          paddingBottom: "0",
+        };
+      } else if (data.subject === "ocarina") {
+        return {
+          paddingTop: "1.2rem",
+          paddingBottom: "0",
+        };
+      } else if (data.subject === "melodica") {
+        return {
+          paddingTop: "2.8rem",
+          paddingBottom: "1.8rem",
+        };
+      } else {
+        return {
+          paddingTop: "",
+          paddingBottom: "",
+        };
+      }
+    });
+
+    const listenText = computed(() => {
+      if (data.fingeringMode === "fingeringMode") {
+        if (playStatus.action) {
+          return "换一换";
+        } else {
+          return "开始练习";
+        }
+      } else if (data.fingeringMode === "listenMode") {
+        if (playStatus.action) {
+          return "再听一遍";
+        } else {
+          return "开始听音";
+        }
+      }
+      return "开始听音";
+    });
+
+    const modeText = computed(() => {
+      let text = "";
+      let icon = icons.icon_mode;
+      data.fingeringModeList.forEach((item: any) => {
+        if (item.value === data.fingeringMode) {
+          text = item.text;
+          icon = item.icon;
+        }
+      });
+      return {
+        text,
+        icon,
+      };
+    });
+
+    // 屏幕方向 0 竖,1 横
+    const orientationDirection = computed(() => {
+      return ["hulusi-flute", "piccolo"].includes(data.subject) ? 1 : 0;
+    });
+
+    const resultImg = (note: any) => {
+      if (data.realKey === note.realKey && !playStatus.action) {
+        return {
+          icon: icons.icon_btn_ylow,
+          status: false,
+        };
+      } else if (playAction.exampleAnser.realKey === note.realKey) {
+        return {
+          icon: icons.icon_btn_ylow,
+          status: false,
+        };
+      } else if (playAction.standardAnswer.realKey === note.realKey) {
+        // 没有开始答题
+        if (!playStatus.action) {
+          return {
+            icon: icons.icon_btn_ylow,
+            status: false,
+          };
+        }
+        // 显示答案中
+        if (playAction.showAnswerLoading) {
+          return {
+            icon: icons.icon_btn_green,
+            status: true,
+          };
+        }
+        // 用户答对
+        if (playAction.userAnswerStatus === 1) {
+          return {
+            icon: icons.icon_btn_green,
+            status: true,
+          };
+        }
+      } else {
+        // 用户答错
+        if (playAction.userAnswerStatus === 2 && playAction.userAnswer.realKey === note.realKey) {
+          return {
+            icon: icons.icon_btn_red,
+            status: true,
+          };
+        }
+      }
+      return {
+        icon: icons.icon_btn_blue,
+        status: true,
+      };
+    };
+    return () => {
+      const relationship = fingerData.subject?.relationship?.[data.realKey] || [];
+      const rs: number[] = Array.isArray(relationship[1]) ? relationship[fingerData.relationshipIndex] : relationship;
+      const canTizhi = Array.isArray(relationship[1]);
+      return (
+        <div class={[styles.fingerBox, state.platform !== IPlatform.PC && !query.modelType && fingerData.fingeringInfo.orientation === 1 ? styles.fingerBottom : styles.fingerRight]}>
+          <div
+            class={styles.head}
+            style={{
+              paddingTop: data.paddingTop ? data.paddingTop : "",
+              paddingLeft: data.paddingLeft ? data.paddingLeft : "",
+            }}
+          >
+            <div class={styles.left}>
+              <button class={[styles.backBtn]} onClick={() => handleBack()}>
+                <img src={icons.icon_back} />
+              </button>
+              <Popover
+                placement="bottom"
+                class={styles.popoverContainer}
+                actions={data.subjects}
+                onSelect={(val: any) => {
+                  if (data.subject === val.value) return;
+                  data.subject = val.value;
+                  data.viewIndex = 0;
+                  resetElement();
+                  resetMode(true, 0);
+                  // 设置屏幕方向
+                  __init();
+                  setTimeout(() => {
+                    api_setRequestedOrientation(orientationDirection.value);
+                  }, 100);
+                }}
+              >
+                {{
+                  reference: () => (
+                    <div
+                      class={styles.baseBtn}
+                      onClick={() => {
+                        //
+                      }}
+                    >
+                      <img src={icons.icon_change_instrument} />
+                      <span>切换乐器</span>
+                    </div>
+                  ),
+                }}
+              </Popover>
+
+              {data.subject !== "melodica" && data.fingeringMode === "scaleMode" && (
+                <div
+                  class={styles.baseBtn}
+                  onClick={() => {
+                    data.viewIndex++;
+                    if (data.viewIndex > data.viewTotal) {
+                      if (["pan-flute", "ocarina"].includes(data.subject)) {
+                        data.viewIndex = 1;
+                      } else {
+                        data.viewIndex = 0;
+                      }
+                    }
+                    getFingeringData();
+                  }}
+                >
+                  <img src={icons.icon_toggle} />
+                  <span>切换视图</span>
+                </div>
+              )}
+            </div>
+            <div class={styles.rightBtn}>
+              <div class={styles.baseBtn} onClick={onChangeFingeringModel}>
+                <img src={modeText.value.icon} />
+                <span>{modeText.value.text}</span>
+              </div>
+
+              <div class={styles.baseBtn} onClick={() => resetElement()}>
+                <img src={icons.icon_2_0} />
+                <span>还原</span>
+              </div>
+              <div
+                class={styles.baseBtn}
+                onClick={() => {
+                  resetElement();
+                  data.tipShow = !data.tipShow;
+                }}
+              >
+                <img src={icons.icon_2_1} />
+                <span>使用说明</span>
+              </div>
+            </div>
+          </div>
+          <div class={styles.fingerContent}>
+            <div class={styles.wrapFinger}>
+              <div
+                id="fingeringContainer"
+                class={styles.boxFinger}
+                style={{
+                  paddingTop: containerBox.value.paddingTop,
+                  paddingBottom: containerBox.value.paddingBottom,
+                }}
+              >
+                <div
+                  style={{
+                    transform: `translate3d(${data.transform.x}px,${data.transform.y}px,0px) scale(${data.transform.scale})`,
+                    transition: data.transform.transition,
+                  }}
+                  class={[styles.fingeringContainer]}
+                >
+                  <div class={styles.imgs}>
+                    <img src={data.fingeringMode === "scaleMode" ? fingerData.subject?.json?.full : fingerData.subject?.json?.full1} />
+                    {rs.map((key: number | string, index: number) => {
+                      const nk: string = typeof key === "string" ? key.replace("active-", "") : String(key);
+                      return <img data-index={nk} src={fingerData.subject?.json?.[nk]} />;
+                    })}
+                    <div style={{ left: data.viewIndex == 2 ? "0" : "64%" }} class={[styles.tizhi, canTizhi && styles.canDisplay]} onClick={() => (fingerData.relationshipIndex = fingerData.relationshipIndex === 0 ? 1 : 0)}>
+                      替指
+                    </div>
+                    <div id="finger-note-2" style={{ left: "50%", transform: "translateX(-50%)" }} class={styles.tizhi} onClick={() => (fingerData.relationshipIndex = fingerData.relationshipIndex === 0 ? 1 : 0)}></div>
+                  </div>
+                </div>
+              </div>
+              <div
+                class={styles.notes}
+                style={{
+                  paddingLeft: data.paddingLeft ? data.paddingLeft : "",
+                }}
+              >
+                {playAction.listenTipsStatus && <div class={[styles.tipsT, data.fingeringMode === "fingeringMode" ? styles.playTips2 : styles.playTips]}></div>}
+                {playAction.userAnswerStatus === 1 && <div class={[styles.tipsT, styles.playSuccess]}></div>}
+                {playAction.userAnswerStatus === 2 && <div class={[styles.tipsT, styles.playError]}></div>}
+                <Button class={styles.noteBtn} onClick={() => scrollNoteBox("left")}>
+                  <Icon name="arrow-left" />
+                </Button>
+                <div class={[styles.noteContent, data.fingeringMode !== "scaleMode" && orientationDirection.value === 0 && styles.noteContentOther, browsInfo.ios ? "" : styles.noteContentWrap, data.huaweiPad && styles.huaweiPad]}>
+                  {/* 判断是否为音阶模式 */}
+                  {data.fingeringMode !== "scaleMode" && (
+                    <div draggable={false} class={styles.note} onClick={noteChangeShow}>
+                      <img draggable={false} src={data.noteType === "all" ? icons.icon_btn_orange : icons.icon_btn_orange2} />
+                    </div>
+                  )}
+
+                  {/* [styles.noteContent, browsInfo.ios ? "" : styles.noteContentWrap, data.huaweiPad && styles.huaweiPad] */}
+                  <div class={styles.lastNoteContent}>
+                    <div ref={noteBoxRef} class={styles.noteBox}>
+                      {data.notes.map((note: IFIGNER_INSTRUMENT_Note, index: number) => {
+                        const steps = new Array(Math.abs(note.step)).fill(1);
+                        return (
+                          <div
+                            id={index == 0 ? "finger-note-0" : ""}
+                            draggable={false}
+                            class={[styles.note, "note-class"]}
+                            key={note.realKey}
+                            onClick={async () => {
+                              // 判断是否在播放音阶
+                              if (playStatus.gamut) return;
+                              if (playAction.listenLock) return;
+                              if (playAction.showAnswerLoading) return;
+                              if (playStatus.action) {
+                                playAction.userAnswer = note;
+                                // 判断用户答题
+                                const userResult = note.realKey === playAction.standardAnswer.realKey ? 1 : 2;
+                                playAction.userAnswerStatus = userResult;
+                                playAction.listenLock = true;
+                                data.realKey = note.realKey;
+                                await fingeringPlay(note, 1000);
+                                resetMode(userResult === 1 ? true : false, 0);
+                                data.realKey = 0;
+
+                                // 如果是指法模式显示完之后要还原
+                                if (data.fingeringMode === "fingeringMode" && userResult === 2) {
+                                  // 延迟显示,因为重置的时候有一个异步操作
+                                  setTimeout(() => {
+                                    data.realKey = playAction.standardAnswer.realKey;
+                                  }, 10);
+                                }
+                                playAction.listenLock = false;
+                              } else {
+                                noteClick(note);
+                              }
+                            }}
+                          >
+                            <img draggable={false} src={resultImg(note).icon} />
+
+                            {playStatus.action && ((playAction.showAnswerLoading && playAction.standardAnswer.realKey === note.realKey) || (playAction.userAnswerStatus === 1 && playAction.userAnswer.realKey === note.realKey)) ? <span class={styles.showAnswer}></span> : ""}
+                            {playStatus.action && playAction.userAnswerStatus === 2 && playAction.userAnswer.realKey === note.realKey ? <span class={[styles.showAnswer, styles.errorAnswer]}></span> : ""}
+                            <div
+                              class={[
+                                styles.noteKey,
+                                ((data.realKey === note.realKey && !playStatus.action) ||
+                                  (playStatus.action && ((playAction.showAnswerLoading && playAction.standardAnswer.realKey === note.realKey) || (playAction.userAnswerStatus === 1 && playAction.userAnswer.realKey === note.realKey))) ||
+                                  (playStatus.action && playAction.userAnswerStatus === 2 && playAction.userAnswer.realKey === note.realKey)) &&
+                                  styles.keyActive,
+                              ]}
+                            >
+                              {/* 显示对应的点 */}
+                              {note.step > 0 ? steps.map((n: any) => <span class={styles.dot}></span>) : null}
+
+                              <div class={styles.noteName}>
+                                <sup>{note.mark && (note.mark === "rise" ? "#" : "b")}</sup>
+                                {note.key}
+                              </div>
+                              {/* 显示对应的点 */}
+                              {note.step < 0 ? steps.map((n: any) => <span class={styles.dot}></span>) : null}
+                            </div>
+                          </div>
+                        );
+                      })}
+                    </div>
+                  </div>
+                </div>
+                <Button class={styles.noteBtn} onClick={() => scrollNoteBox("right")}>
+                  <Icon name="arrow" />
+                </Button>
+              </div>
+              {data.fingeringMode !== "scaleMode" && (
+                <div class={styles.optionBtns}>
+                  <Button class={[styles.oBtn, styles.gamut, playStatus.action && styles.disabled]} round onClick={onGamutPlayOrPause}>
+                    {playStatus.gamut ? "暂停" : "播放音阶"}
+                  </Button>
+                  <Button class={[styles.oBtn, styles.play, playStatus.gamut && styles.disabled]} round onClick={onActionPlay}>
+                    {listenText.value}
+                  </Button>
+                  <Button class={[styles.oBtn, styles.success, !playStatus.answer && styles.disabled]} round onClick={onShowAnswer}>
+                    显示答案
+                  </Button>
+                </div>
+              )}
+            </div>
+            <div class={[styles.tips, data.tipShow ? "" : styles.tipHidden]}>
+              <div class={styles.tipTitle}>
+                <div class={styles.tipTitleName}>{fingerData.fingeringInfo.code}使用说明</div>
+                <Button class={styles.tipClose} onClick={() => (data.tipShow = false)}>
+                  <Icon name="cross" size={19} color="#fff" />
+                </Button>
+              </div>
+              <div class={styles.iconBook}></div>
+              <div class={styles.tipContentbox}>
+                <div class={styles.tipContent}>
+                  {data.tips.map((tip, tipIndex) => (
+                    <div class={styles.tipItem}>
+                      <div class={styles.iconWrap}>
+                        <div class={styles.tipItemIcon}>{tipIndex + 1}</div>
+                      </div>
+                      <div>
+                        {tip.name}: {tip.realName}
+                      </div>
+                    </div>
+                  ))}
+                </div>
+              </div>
+            </div>
+            {data.loadingSoundFonts && (
+              <div class={styles.loading}>
+                <div class={styles.loadingWrap}>
+                  <img class={styles.loadingIcon} src={icon_loading_img} />
+                  <Progress percentage={data.loadingSoundProgress} />
+                  <div class={styles.loadingTip}>加载中,请稍后…</div>
+                </div>
+              </div>
+            )}
+          </div>
+          {!!data.tones.length && data.fingeringMode === "scaleMode" && (
+            <>
+              {fingerData.fingeringInfo.name == "hulusi-flute" ? (
+                <div id="finger-note-1" class={[styles.toggleBtn, styles.toggleBtnhulusi]} onClick={() => (data.tnoteShow = true)}>
+                  <div>
+                    全按作
+                    <div class={[styles.noteKey]}>
+                      {data.activeTone.step > 0 ? <span class={styles.dot}></span> : null}
+
+                      <div class={styles.noteName}>
+                        <sup>{data.activeTone.mark && (data.activeTone.mark === "rise" ? "#" : "b")}</sup>
+                        {data.activeTone.key}
+                      </div>
+                      {data.activeTone.step < 0 ? <span class={styles.dot}></span> : null}
+                    </div>
+                  </div>
+                  <img src={icons.icon_arrow} />
+                </div>
+              ) : (
+                <div id="finger-note-1" class={styles.toggleBtn} onClick={() => (data.tnoteShow = true)}>
+                  <div style={{ marginTop: "-4px" }}>
+                    <sup>{data.activeTone.mark && (data.activeTone.mark === "rise" ? "#" : "b")}</sup>
+                    {data.activeTone.name}
+                  </div>
+                  调
+                  <img src={icons.icon_arrow} />
+                </div>
+              )}
+            </>
+          )}
+
+          <Popup class="tonePopup" v-model:show={data.tnoteShow} position={state.platform !== IPlatform.PC && !query.modelType && fingerData.fingeringInfo.orientation === 1 ? "bottom" : "right"}>
+            <div class={styles.tones}>
+              <div class={styles.toneTitle}>
+                <div class={styles.tipTitleName}>移调</div>
+                <Button class={styles.tipClose} onClick={() => (data.tnoteShow = false)}>
+                  <Icon name="cross" size={19} color="#fff" />
+                </Button>
+              </div>
+              <div class={styles.tipContentbox}>
+                <div class={styles.tipContent}>
+                  <div class={styles.tipWrap}>
+                    <Space size={0} class={styles.toneContent}>
+                      {data.tones.map((tone: IFIGNER_INSTRUMENT_Note) => {
+                        const steps = new Array(Math.abs(tone.step)).fill(1);
+                        return (
+                          <Button
+                            class={[fingerData.fingeringInfo.name == "hulusi-flute" && styles.hulusiBtn]}
+                            round
+                            plain
+                            type={data.popupActiveTone.realName === tone.realName ? "primary" : "default"}
+                            onClick={() => {
+                              data.popupActiveTone = tone;
+                              setNotes();
+                            }}
+                          >
+                            {fingerData.fingeringInfo.name == "hulusi-flute" ? (
+                              <div style={{ display: "flex", alignItems: "center" }}>
+                                全按作
+                                <div class={[styles.noteKey, styles.hulusiNoteKey]}>
+                                  {tone.step > 0 ? <span class={styles.dot}></span> : null}
+                                  <div class={styles.noteName} style={{ fontSize: "0.25rem" }}>
+                                    <sup>{tone.mark && (tone.mark === "rise" ? "#" : "b")}</sup>
+                                    {tone.key}
+                                  </div>
+                                  {tone.step < 0 ? <span class={styles.dot}></span> : null}
+                                </div>
+                              </div>
+                            ) : (
+                              <div class={styles.noteName}>
+                                <sup>{tone.mark && (tone.mark === "rise" ? "#" : "b")}</sup>
+                                {tone.name}
+                              </div>
+                            )}
+                          </Button>
+                        );
+                      })}
+                    </Space>
+                  </div>
+                  <div class={styles.toneAction}>
+                    <img onClick={() => (data.tnoteShow = false)} src={icons.icon_action_cancel} />
+                    <img
+                      onClick={() => {
+                        data.activeTone = data.popupActiveTone;
+                        setNotes();
+                        data.tnoteShow = false;
+                      }}
+                      src={icons.icon_action_confirm}
+                    />
+                  </div>
+                </div>
+              </div>
+            </div>
+          </Popup>
+
+          {props.show && !data.loading && !data.loadingSoundFonts && <GuideIndex showGuide={false} list={["finger"]} />}
+        </div>
+      );
+    };
+  },
 });

+ 18 - 1
src/view/fingering/fingering-config.ts

@@ -51,7 +51,8 @@ export type IVocals =
   | "ocarina"
   | "ocarina1"
   | "ocarina2"
-  | "melodica";
+  | "melodica"
+  | "melodica1";
 
 /** 映射声部ID */
 export const mappingVoicePart = (id: number | string, soruce: "GYM" | "COLEXIU" | "ORCHESTRA" | "INSTRUMENT"): number => {
@@ -390,6 +391,12 @@ export const getFingeringConfig = async (type: IVocals | undefined): Promise<ITy
         json: pan2.default,
         relationship: relationships.pan,
       };
+    case "pan-flute3":
+      const pan3 = await import(`./fingering-img/pan-flute3/index.json`);
+      return {
+        json: pan3.default,
+        relationship: relationships.pan,
+      };
     case "ocarina":
       const ocarina = await import(`./fingering-img/ocarina/index.json`);
       return {
@@ -430,6 +437,16 @@ export const getFingeringConfig = async (type: IVocals | undefined): Promise<ITy
           marginTop: "auto",
         },
       };
+    case "melodica1":
+      const melodica1 = await import(`./fingering-img/melodica1/index.json`);
+      return {
+        json: melodica1.default,
+        relationship: relationships.melodica,
+        height: "80px",
+        styles: {
+          marginTop: "auto",
+        },
+      };
     default:
       return null;
   }

File diff suppressed because it is too large
+ 0 - 0
src/view/fingering/fingering-img/hulusi-flute/index.json


File diff suppressed because it is too large
+ 1 - 0
src/view/fingering/fingering-img/melodica1/index.json


File diff suppressed because it is too large
+ 1 - 0
src/view/fingering/fingering-img/pan-flute3/index.json


Some files were not shown because too many files changed in this diff