Pārlūkot izejas kodu

Merge branch 'iteration-beat-one' into online

lex 1 gadu atpakaļ
vecāks
revīzija
2e0f5aeaaa
100 mainītis faili ar 4907 papildinājumiem un 1380 dzēšanām
  1. BIN
      public/soundfonts/baroque-recorder/A5.mp3
  2. BIN
      public/soundfonts/baroque-recorder/A6.mp3
  3. BIN
      public/soundfonts/baroque-recorder/Ab5.mp3
  4. BIN
      public/soundfonts/baroque-recorder/Ab6.mp3
  5. BIN
      public/soundfonts/baroque-recorder/B5.mp3
  6. BIN
      public/soundfonts/baroque-recorder/B6.mp3
  7. BIN
      public/soundfonts/baroque-recorder/Bb5.mp3
  8. BIN
      public/soundfonts/baroque-recorder/Bb6.mp3
  9. BIN
      public/soundfonts/baroque-recorder/C5.mp3
  10. BIN
      public/soundfonts/baroque-recorder/C6.mp3
  11. BIN
      public/soundfonts/baroque-recorder/C7.mp3
  12. BIN
      public/soundfonts/baroque-recorder/D5.mp3
  13. BIN
      public/soundfonts/baroque-recorder/D6.mp3
  14. BIN
      public/soundfonts/baroque-recorder/D7.mp3
  15. BIN
      public/soundfonts/baroque-recorder/Db5.mp3
  16. BIN
      public/soundfonts/baroque-recorder/Db6.mp3
  17. BIN
      public/soundfonts/baroque-recorder/Db7.mp3
  18. BIN
      public/soundfonts/baroque-recorder/E5.mp3
  19. BIN
      public/soundfonts/baroque-recorder/E6.mp3
  20. BIN
      public/soundfonts/baroque-recorder/Eb5.mp3
  21. BIN
      public/soundfonts/baroque-recorder/Eb6.mp3
  22. BIN
      public/soundfonts/baroque-recorder/F5.mp3
  23. BIN
      public/soundfonts/baroque-recorder/F6.mp3
  24. BIN
      public/soundfonts/baroque-recorder/G5.mp3
  25. BIN
      public/soundfonts/baroque-recorder/G6.mp3
  26. BIN
      public/soundfonts/baroque-recorder/Gb5.mp3
  27. BIN
      public/soundfonts/baroque-recorder/Gb6.mp3
  28. BIN
      public/soundfonts/baroque-recorder/Gb7.mp3
  29. 25 15
      src/page-instrument/api.ts
  30. 37 34
      src/page-instrument/router.ts
  31. 74 82
      src/page-instrument/view-detail/index.tsx
  32. 19 0
      src/page-instrument/view-figner-listen/guide/detail-guide.tsx
  33. 107 0
      src/page-instrument/view-figner-listen/guide/finger-guide.tsx
  34. 69 0
      src/page-instrument/view-figner-listen/guide/guide-index.tsx
  35. BIN
      src/page-instrument/view-figner-listen/guide/image/icon_detail.png
  36. 1 0
      src/page-instrument/view-figner-listen/guide/image/icons.json
  37. 135 0
      src/page-instrument/view-figner-listen/guide/index.module.less
  38. BIN
      src/page-instrument/view-figner-listen/image/icon_action_cancel.png
  39. BIN
      src/page-instrument/view-figner-listen/image/icon_action_confirm.png
  40. BIN
      src/page-instrument/view-figner-listen/image/icon_bg_t.png
  41. BIN
      src/page-instrument/view-figner-listen/image/icon_bg_v.png
  42. BIN
      src/page-instrument/view-figner-listen/image/icon_btn_1.png
  43. BIN
      src/page-instrument/view-figner-listen/image/icon_btn_2.png
  44. BIN
      src/page-instrument/view-figner-listen/image/icon_btn_3.png
  45. BIN
      src/page-instrument/view-figner-listen/image/icon_btn_4.png
  46. BIN
      src/page-instrument/view-figner-listen/image/icon_btn_green_sub.png
  47. BIN
      src/page-instrument/view-figner-listen/image/icon_btn_red_sub.png
  48. BIN
      src/page-instrument/view-figner-listen/image/icon_loading_head.png
  49. BIN
      src/page-instrument/view-figner-listen/image/icon_loading_img.png
  50. BIN
      src/page-instrument/view-figner-listen/image/icon_popup_book.png
  51. BIN
      src/page-instrument/view-figner-listen/image/icon_popup_book_h.png
  52. BIN
      src/page-instrument/view-figner-listen/image/icon_popup_book_v.png
  53. BIN
      src/page-instrument/view-figner-listen/image/icon_popup_top.png
  54. BIN
      src/page-instrument/view-figner-listen/image/icon_shadow_left.png
  55. BIN
      src/page-instrument/view-figner-listen/image/icon_shadow_r.png
  56. BIN
      src/page-instrument/view-figner-listen/image/icon_shuo_h.png
  57. BIN
      src/page-instrument/view-figner-listen/image/icon_shuo_v.png
  58. BIN
      src/page-instrument/view-figner-listen/image/icon_trans_bg.png
  59. BIN
      src/page-instrument/view-figner-listen/image/icon_trans_bg_hu.png
  60. 3 0
      src/page-instrument/view-figner-listen/image/icons.json
  61. 1037 0
      src/page-instrument/view-figner-listen/index.module.less
  62. 1060 0
      src/page-instrument/view-figner-listen/index.tsx
  63. BIN
      src/page-instrument/view-figner-listen/lsy.ttf
  64. BIN
      src/page-instrument/view-figner/image/icon_btn_1.png
  65. BIN
      src/page-instrument/view-figner/image/icon_btn_2.png
  66. BIN
      src/page-instrument/view-figner/image/icon_btn_3.png
  67. BIN
      src/page-instrument/view-figner/image/icon_btn_4.png
  68. BIN
      src/page-instrument/view-figner/image/icon_btn_green_sub.png
  69. BIN
      src/page-instrument/view-figner/image/icon_btn_red_sub.png
  70. 2 2
      src/page-instrument/view-figner/image/icons.json
  71. BIN
      src/page-instrument/view-figner/image/tips1.png
  72. BIN
      src/page-instrument/view-figner/image/tips2.png
  73. BIN
      src/page-instrument/view-figner/image/tips3.png
  74. BIN
      src/page-instrument/view-figner/image/tips4.png
  75. 187 17
      src/page-instrument/view-figner/index.module.less
  76. 1232 687
      src/page-instrument/view-figner/index.tsx
  77. 40 39
      src/store.ts
  78. 300 0
      src/view/figner-preview/index.ts
  79. 552 504
      src/view/fingering/fingering-config.ts
  80. BIN
      src/view/fingering/fingering-img/baroque-recorder/1.png
  81. BIN
      src/view/fingering/fingering-img/baroque-recorder/11.png
  82. BIN
      src/view/fingering/fingering-img/baroque-recorder/2.png
  83. BIN
      src/view/fingering/fingering-img/baroque-recorder/3.png
  84. BIN
      src/view/fingering/fingering-img/baroque-recorder/33.png
  85. BIN
      src/view/fingering/fingering-img/baroque-recorder/4.png
  86. BIN
      src/view/fingering/fingering-img/baroque-recorder/5.png
  87. BIN
      src/view/fingering/fingering-img/baroque-recorder/6.png
  88. BIN
      src/view/fingering/fingering-img/baroque-recorder/7.png
  89. BIN
      src/view/fingering/fingering-img/baroque-recorder/77.png
  90. BIN
      src/view/fingering/fingering-img/baroque-recorder/8.png
  91. BIN
      src/view/fingering/fingering-img/baroque-recorder/88.png
  92. BIN
      src/view/fingering/fingering-img/baroque-recorder/full.png
  93. 13 0
      src/view/fingering/fingering-img/baroque-recorder/index.json
  94. BIN
      src/view/fingering/fingering-img/baroque-recorder/英式.png
  95. 13 0
      src/view/fingering/fingering-img/baroque-recorder1/index.json
  96. BIN
      src/view/fingering/fingering-img/baroque-recorder2/full.png
  97. 1 0
      src/view/fingering/fingering-img/baroque-recorder2/index.json
  98. 0 0
      src/view/fingering/fingering-img/hulusi-flute/index.json
  99. 0 0
      src/view/fingering/fingering-img/hulusi-flute1/index.json
  100. 0 0
      src/view/fingering/fingering-img/melodica/index.json

BIN
public/soundfonts/baroque-recorder/A5.mp3


BIN
public/soundfonts/baroque-recorder/A6.mp3


BIN
public/soundfonts/baroque-recorder/Ab5.mp3


BIN
public/soundfonts/baroque-recorder/Ab6.mp3


BIN
public/soundfonts/baroque-recorder/B5.mp3


BIN
public/soundfonts/baroque-recorder/B6.mp3


BIN
public/soundfonts/baroque-recorder/Bb5.mp3


BIN
public/soundfonts/baroque-recorder/Bb6.mp3


BIN
public/soundfonts/baroque-recorder/C5.mp3


BIN
public/soundfonts/baroque-recorder/C6.mp3


BIN
public/soundfonts/baroque-recorder/C7.mp3


BIN
public/soundfonts/baroque-recorder/D5.mp3


BIN
public/soundfonts/baroque-recorder/D6.mp3


BIN
public/soundfonts/baroque-recorder/D7.mp3


BIN
public/soundfonts/baroque-recorder/Db5.mp3


BIN
public/soundfonts/baroque-recorder/Db6.mp3


BIN
public/soundfonts/baroque-recorder/Db7.mp3


BIN
public/soundfonts/baroque-recorder/E5.mp3


BIN
public/soundfonts/baroque-recorder/E6.mp3


BIN
public/soundfonts/baroque-recorder/Eb5.mp3


BIN
public/soundfonts/baroque-recorder/Eb6.mp3


BIN
public/soundfonts/baroque-recorder/F5.mp3


BIN
public/soundfonts/baroque-recorder/F6.mp3


BIN
public/soundfonts/baroque-recorder/G5.mp3


BIN
public/soundfonts/baroque-recorder/G6.mp3


BIN
public/soundfonts/baroque-recorder/Gb5.mp3


BIN
public/soundfonts/baroque-recorder/Gb6.mp3


BIN
public/soundfonts/baroque-recorder/Gb7.mp3


+ 25 - 15
src/page-instrument/api.ts

@@ -2,47 +2,57 @@ import request from "../utils/request";
 
 /** 获取用户信息 */
 export const studentQueryUserInfo = async () => {
-	return await request.get(`/user/getUserInfo`);
+  return await request.get(`/user/getUserInfo`);
 };
 
 /** 获取曲谱详情 */
 export const sysMusicScoreAccompanimentQueryPage = (sysMusicScoreId: string) => {
-	return request.get("/musicSheet/detail/" + sysMusicScoreId);
+  return request.get("/musicSheet/detail/" + sysMusicScoreId);
 };
 
 /** 新增练习记录(包含评测) */
 export const api_musicPracticeRecordSave = (data: any) => {
-	return request.post("/musicPracticeRecord/save", { requestType: "json", data });
+  return request.post("/musicPracticeRecord/save", { requestType: "json", data });
 };
 /** 添加作业记录 */
 export const api_lessonTrainingSubmitTraining = (data: any) => {
-	return request.post("/lessonTraining/submitTraining", { requestType: "json", data });
+  return request.post("/lessonTraining/submitTraining", { requestType: "json", data });
 };
 /** 获取作业详情 */
 export const api_lessonTrainingTrainingStudentDetail = (id: any) => {
-	return request.get(`/lessonTraining/trainingContentStudentDetail?id=${id}`);
+  return request.get(`/lessonTraining/trainingContentStudentDetail?id=${id}`);
 };
 /** 上传评测视频 */
 export const api_musicPracticeRecordVideoUpload = (data: any) => {
-	return request.post(`/musicPracticeRecord/videoUpload`, {
-		data,
-		requestType: "json",
-	});
+  return request.post(`/musicPracticeRecord/videoUpload`, {
+    data,
+    requestType: "json",
+  });
 };
 
 /** 提交意见反馈 */
 export const sysSuggestionAdd = (data: any) => {
-	return request.post("/sysSuggestion/save", { data, requestType: "json" });
+  return request.post("/sysSuggestion/save", { data, requestType: "json" });
 };
 
 /** 获取评测报告 */
 export const api_musicPracticeRecordDetail = (recordId: string) => {
-	return request.get("/musicPracticeRecord/detail/" + recordId);
+  return request.get("/musicPracticeRecord/detail/" + recordId);
 };
 /** 获取曲谱列表 */
 export const api_musicSheetPage = (data: any) => {
-	return request.post("/musicSheet/page", {
-		data,
-		requestType: "json",
-	});
+  return request.post("/musicSheet/page", {
+    data,
+    requestType: "json",
+  });
+};
+
+/**
+ * 获取声部列表
+ */
+export const getSubjectList = (data: any) => {
+  return request.post("/subject/page", {
+    data,
+    requestType: "json",
+  });
 };

+ 37 - 34
src/page-instrument/router.ts

@@ -3,46 +3,49 @@ import Home from "./view-detail/index";
 import Notfind from "../view/notfind";
 
 const routes: RouteRecordRaw[] = [
-	{
-		path: "/",
-		component: Home,
-	},
-	{
-		path: "/product-img",
-		component: () => import("./view-product-img/index"),
-	},
-	{
-		path: "/evaluat-report",
-		component: () => import("./view-evaluat-report/index"),
-	},
-	{
-		path: "/preview",
-		component: () => import("./view-preview/index"),
-	},
-	{
-		path: "/view-figner",
-		component: () => import("./view-figner/index"),
-	},
-	{
-		path: "/:pathMatch(.*)*",
-		component: Notfind,
-		meta: {
-			title: "404 Not Fund",
-		},
-	},
-	
+  {
+    path: "/",
+    component: Home,
+  },
+  {
+    path: "/product-img",
+    component: () => import("./view-product-img/index"),
+  },
+  {
+    path: "/evaluat-report",
+    component: () => import("./view-evaluat-report/index"),
+  },
+  {
+    path: "/preview",
+    component: () => import("./view-preview/index"),
+  },
+  {
+    path: "/view-figner",
+    component: () => import("./view-figner/index"),
+  },
+  {
+    path: "/view-figner-listen",
+    component: () => import("./view-figner-listen/index"),
+  },
+  {
+    path: "/:pathMatch(.*)*",
+    component: Notfind,
+    meta: {
+      title: "404 Not Fund",
+    },
+  },
 ];
 
 const router = createRouter({
-	history: createWebHashHistory(),
-	routes,
+  history: createWebHashHistory(),
+  routes,
 });
 
 router.beforeEach((to, from, next) => {
-	if (to.meta.title) {
-		document.title = to.meta.title as string;
-	}
-	next();
+  if (to.meta.title) {
+    document.title = to.meta.title as string;
+  }
+  next();
 });
 
 export default router;

+ 74 - 82
src/page-instrument/view-detail/index.tsx

@@ -26,23 +26,23 @@ import TheMusicList from "../component/the-music-list";
 import { storeData } from "/src/store";
 import ViewFigner from "../view-figner";
 import { recalculateNoteData } from "/src/view/selection";
-import ToggleMusicSheet from "/src/view/plugins/toggleMusicSheet"
+import ToggleMusicSheet from "/src/view/plugins/toggleMusicSheet";
 
 /**
  * 特殊教材分类id
  */
-export const classids = [1, 2, 6, 7, 8, 9, 3, 10, 11, 12, 13, 4, 14, 15, 16, 17, 30, 31, 35, 36, 46, 108];  // 大雅金唐, 竖笛教程, 声部训练展开的分类ID
+export const classids = [1, 2, 6, 7, 8, 9, 3, 10, 11, 12, 13, 4, 14, 15, 16, 17, 30, 31, 35, 36, 46, 108]; // 大雅金唐, 竖笛教程, 声部训练展开的分类ID
 
 const calcCeilFrequency = (frequency: number) => {
-  if (frequency) return frequency * 1000 * 2 / 1000;
-  return 0
+  if (frequency) return (frequency * 1000 * 2) / 1000;
+  return 0;
 };
 /** 需要处理频率的乐器
  */
 const resetFrequency = (list: any[]) => {
   const instrumentNames = ["ocarina", "pan-flute", "piccolo", "hulusi-flute"];
   if (!state.fingeringInfo?.name || !instrumentNames.includes(state.fingeringInfo.name)) return list;
-  console.log(state.subjectId, state.fingeringInfo.name, instrumentNames)
+  console.log(state.subjectId, state.fingeringInfo.name, instrumentNames);
   for (let i = 0; i < list.length; i++) {
     if (list[i].prevFrequency) list[i].prevFrequency = calcCeilFrequency(list[i].prevFrequency);
     if (list[i].frequency) list[i].frequency = calcCeilFrequency(list[i].frequency);
@@ -55,7 +55,7 @@ const resetFrequency = (list: any[]) => {
  * 乐器指法处理
  */
 const setNoteHalfTone = (list: any[]) => {
-  const instrumentNames = ["hulusi-flute"] // ["ocarina", "pan-flute", "piccolo"];
+  const instrumentNames = ["hulusi-flute"]; // ["ocarina", "pan-flute", "piccolo"];
   if (!state.fingeringInfo?.name || !instrumentNames.includes(state.fingeringInfo.name)) return list;
   for (let i = 0; i < list.length; i++) {
     const note = list[i];
@@ -76,6 +76,7 @@ export default defineComponent({
       paddingLeft: "",
       headerHide: false,
       fingerPreView: false,
+      fingerPreViewAnimation: false,
       orientation: 0,
       fingerPreViewGuide: false,
     });
@@ -90,6 +91,7 @@ export default defineComponent({
       }
     };
     onBeforeMount(() => {
+      console.time("渲染加载耗时");
       api_keepScreenLongLight();
       getAPPData();
       api_setStatusBarVisibility();
@@ -124,43 +126,43 @@ export default defineComponent({
       detailData.isLoading = false;
     };
 
-		const setState = (data: any, index: number) => {
-			state.appName = "COLEXIU";
-			state.detailId = data.id;
-			state.xmlUrl = data.xmlFileUrl;
-			state.partIndex = index;
-			state.trackId = data.track;
+    const setState = (data: any, index: number) => {
+      state.appName = "COLEXIU";
+      state.detailId = data.id;
+      state.xmlUrl = data.xmlFileUrl;
+      state.partIndex = index;
+      state.trackId = data.track;
       state.subjectId = data.musicSubject;
-			state.categoriesId = data.categoriesId;
-			state.categoriesName = data.musicTagNames;
-			state.enableEvaluation = data.canEvaluate ? true : false;
-			state.examSongId = data.id + "";
-			state.examSongName = data.musicSheetName;
-			state.coverImg = data.titleImg ?? "";
-			// 解析扩展字段
-			if (data.extConfigJson) {
-				try {
-					state.extConfigJson = JSON.parse(data.extConfigJson as string);
-				} catch (error) {
-					console.error("解析扩展字段错误:", error);
-				}
-			}
-			state.isOpenMetronome = data.mp3Type === "MP3_METRONOME" ? true : false;
-			// 曲子包含节拍器,就不开启节拍器
-			state.needTick = data.mp3Type === "MP3_METRONOME" ? false : true;
-			state.isShowFingering = data.showFingering ? true : false;
-			state.music = data.music;
-			state.accompany = data.accompany;
-			state.midiUrl = data.midiUrl;
-			state.parentCategoriesId = data.musicTag;
-			state.musicSheetCategoriesId = data.musicSheetCategoriesId;
-			state.playMode = data.audioType === "MP3" ? "MP3" : "MIDI";
-			state.originSpeed = state.speed = data.playSpeed;
-			const track = data.code || data.track;
-			state.track = track ? track.replace(/ /g, "").toLocaleLowerCase() : "";
-			state.enableNotation = data.notation ? true : false;
+      state.categoriesId = data.categoriesId;
+      state.categoriesName = data.musicTagNames;
+      state.enableEvaluation = data.canEvaluate ? true : false;
+      state.examSongId = data.id + "";
+      state.examSongName = data.musicSheetName;
+      state.coverImg = data.titleImg ?? "";
+      // 解析扩展字段
+      if (data.extConfigJson) {
+        try {
+          state.extConfigJson = JSON.parse(data.extConfigJson as string);
+        } catch (error) {
+          console.error("解析扩展字段错误:", error);
+        }
+      }
+      state.isOpenMetronome = data.mp3Type === "MP3_METRONOME" ? true : false;
+      // 曲子包含节拍器,就不开启节拍器
+      state.needTick = data.mp3Type === "MP3_METRONOME" ? false : true;
+      state.isShowFingering = data.showFingering ? true : false;
+      state.music = data.music;
+      state.accompany = data.accompany;
+      state.midiUrl = data.midiUrl;
+      state.parentCategoriesId = data.musicTag;
+      state.musicSheetCategoriesId = data.musicSheetCategoriesId;
+      state.playMode = data.audioType === "MP3" ? "MP3" : "MIDI";
+      state.originSpeed = state.speed = data.playSpeed;
+      const track = data.code || data.track;
+      state.track = track ? track.replace(/ /g, "").toLocaleLowerCase() : "";
+      state.enableNotation = data.notation ? true : false;
       // 是否是合奏,先根据background判断
-      state.isConcert = data.background?.length > 1
+      state.isConcert = data.background?.length > 1;
 
       // console.log("🚀 ~ state.subjectId:", state.subjectId, state.track as any , state.subjectId)
       // 是否打击乐
@@ -171,7 +173,7 @@ export default defineComponent({
       // 	isRhythmicExercises();
       state.isPercussion = isRhythmicExercises();
       // 设置是否特殊曲谱, 是特殊曲谱取反(不理解之前的思考逻辑), 使用后台设置的速度
-      state.isSpecialBookCategory = !classids.includes(data.musicSheetCategoriesId) 
+      state.isSpecialBookCategory = !classids.includes(data.musicSheetCategoriesId);
 
       // 设置指法
       const code = state.isConcert ? mappingVoicePart(state.trackId, "ENSEMBLE") : mappingVoicePart(state.subjectId, "INSTRUMENT");
@@ -191,7 +193,7 @@ export default defineComponent({
 
       //课堂乐器, 渲染类型: 五线谱, 简谱
       state.musicRenderType = query.musicRenderType || EnumMusicRenderType.firstTone;
-      console.log('state对象', state)
+      console.log("state对象", state);
     };
 
     const setCustom = () => {
@@ -244,6 +246,7 @@ export default defineComponent({
       resetPlaybackToStart();
 
       pushAppMusic();
+      console.timeEnd("渲染加载耗时");
     };
     /** 指法配置 */
     const fingerConfig = computed<any>(() => {
@@ -373,11 +376,7 @@ export default defineComponent({
         class={[styles.detail, state.setting.eyeProtection && "eyeProtection", state.platform === IPlatform.PC && styles.PC]}
         style={{
           paddingLeft: detailData.paddingLeft,
-          background: state.setting.camera
-          ? `rgba(${state.setting.eyeProtection ? '253,244,229' : '255,255,255'} ,${
-            state.setting.cameraOpacity / 100
-            }) !important`
-          : '',
+          background: state.setting.camera ? `rgba(${state.setting.eyeProtection ? "253,244,229" : "255,255,255"} ,${state.setting.cameraOpacity / 100}) !important` : "",
         }}
       >
         <Transition name="van-fade">
@@ -390,9 +389,7 @@ export default defineComponent({
         <div class={[styles.headHeight, detailData.headerHide && styles.headHide]}>{state.musicRendered && <HeaderTop />}</div>
         <div
           id="scrollContainer"
-          style={{ ...fingerConfig.value.container, 
-            height: detailData.headerHide ? "100vh" : "",
-          }}
+          style={{ ...fingerConfig.value.container, height: detailData.headerHide ? "100vh" : "" }}
           class={[styles.container, !state.setting.displayCursor && "hideCursor", browsInfo.xiaomi && styles.xiaomi]}
           onClick={(e: Event) => {
             e.stopPropagation();
@@ -407,15 +404,11 @@ export default defineComponent({
           {/* 指法 */}
           {state.setting.displayFingering && state.fingeringInfo?.name && (
             <div style={{ ...fingerConfig.value.fingerBox }}>
-              <Fingering 
-                   style={{
-                    background: state.setting.camera
-                      ? `rgba(${state.setting.eyeProtection ? '253,244,229' : '255,255,255'} ,${
-                          state.setting.cameraOpacity / 100
-                        })`
-                      : '',
-                  }}              
-                onOpen={() => handleOpenFignerView()} 
+              <Fingering
+                style={{
+                  background: state.setting.camera ? `rgba(${state.setting.eyeProtection ? "253,244,229" : "255,255,255"} ,${state.setting.cameraOpacity / 100})` : "",
+                }}
+                onOpen={() => handleOpenFignerView()}
               />
             </div>
           )}
@@ -444,9 +437,7 @@ export default defineComponent({
         )}
 
         {/* 切换曲谱 */}
-        {!query.lessonTrainingId && !query.questionId && state.isConcert && (
-          <ToggleMusicSheet />
-        )}
+        {!query.lessonTrainingId && !query.questionId && state.isConcert && <ToggleMusicSheet />}
 
         {state.musicRendered && (
           <>
@@ -459,23 +450,24 @@ export default defineComponent({
           </>
         )}
 
-				<Popup
-					zIndex={5050}
-					teleport="body"
-					v-model:show={detailData.fingerPreView}
-					position="bottom"
-					onOpened={() => {
-						detailData.fingerPreViewGuide = true;
-					}}
-				>
-					<ViewFigner
-						show={detailData.fingerPreViewGuide}
-						subject={state.fingeringInfo.name}
-						isComponent={true}
-						onClose={handleCloseFignerView}
-					/>
-				</Popup>
-			</div>
-		);
-	},
+        <Popup
+          zIndex={5050}
+          teleport="body"
+          v-model:show={detailData.fingerPreView}
+          position="bottom"
+          onClosed={() => {
+            detailData.fingerPreViewAnimation = false;
+          }}
+          onOpen={() => {
+            detailData.fingerPreViewAnimation = true;
+          }}
+          onOpened={() => {
+            detailData.fingerPreViewGuide = true;
+          }}
+        >
+          {detailData.fingerPreViewAnimation && <ViewFigner show={detailData.fingerPreViewGuide} subject={state.fingeringInfo.name} isComponent={true} onClose={handleCloseFignerView} />}
+        </Popup>
+      </div>
+    );
+  },
 });

+ 19 - 0
src/page-instrument/view-figner-listen/guide/detail-guide.tsx

@@ -0,0 +1,19 @@
+import { defineComponent, onMounted, ref } from "vue";
+import styles from "./index.module.less";
+import { Icon } from "vant";
+
+export default defineComponent({
+	name: "DetailGuide",
+	emits: ["close"],
+	setup(props, { emit }) {
+		const detailRef = ref();
+		return () => (
+			<div ref={detailRef} class={styles.detail}>
+				<div class={styles.btn} onClick={() => emit("close", true)}>
+					不再提醒
+				</div>
+				<Icon class={styles.close} name="cross" onClick={() => emit("close")} />
+			</div>
+		);
+	},
+});

+ 107 - 0
src/page-instrument/view-figner-listen/guide/finger-guide.tsx

@@ -0,0 +1,107 @@
+import { defineComponent, onMounted, reactive, ref } from "vue";
+import styles from "./index.module.less";
+import { Button, Icon, Popup } from "vant";
+import icons from "./image/icons.json";
+import state from "/src/state";
+import { getQuery } from "/src/utils/queryString";
+
+export default defineComponent({
+	name: "DetailGuide",
+	emits: ["close"],
+	setup(props, { emit }) {
+		const query = getQuery();
+		const data = reactive({
+			box: {},
+			show: true,
+			steps: [
+				{
+					className: "boxItem1",
+					des: `快点击下排按钮听听${state.fingeringInfo.code}的声音吧,按钮可以滑动哦~`,
+					img: icons.icon_cursor_1,
+				},
+				{
+					className: "boxItem2",
+					des: "这里可以切换音调,查看不同音调的指法~",
+					img: icons.icon_cursor_2,
+				},
+				{
+					className: "boxItem3",
+					des: "可以通过手势放大缩小乐器哦~",
+					img: icons.icon_cursor_3,
+				},
+			],
+			step: 0,
+		});
+		const steps = ["finger-note-0", "finger-note-1", "finger-note-2"];
+		const getStepELe = () => {
+			const ele: HTMLElement = document.getElementById(steps[data.step])!;
+			console.log(data.step, ele);
+			if (ele) {
+				const eleRect = ele.getBoundingClientRect();
+				const increment = data.step === 2 ? eleRect.width : 0;
+				data.box = {
+					left: eleRect.x - increment + "px",
+					top: eleRect.y + "px",
+					width: (data.step === 2 ? 0 : eleRect.width) + "px",
+					height: (data.step === 2 ? 0 : eleRect.height) + "px",
+				};
+			} else {
+				handleNext();
+			}
+		};
+		onMounted(() => {
+			getStepELe();
+		});
+
+		const handleNext = () => {
+			if (data.step >= 2) {
+				endGuide();
+				return;
+			}
+			data.step = data.step + 1;
+			getStepELe();
+		};
+
+		const endGuide = () => {
+			emit("close", true);
+		};
+		return () => (
+			<Popup
+				zIndex={5051}
+				teleport="body"
+				overlay={false}
+				closeOnClickOverlay={false}
+				class={["popup-custom", styles.fingerGuide]}
+				v-model:show={data.show}
+			>
+				<div class={styles.content} onClick={() => handleNext()}>
+					<div class={styles.box} style={data.box}>
+						{data.steps.map((item, index) => (
+							<div style={{ display: index === data.step ? "" : "none" }} class={styles[item.className]}>
+								<img src={item.img} />
+							</div>
+						))}
+					</div>
+					<div onClick={(e: Event) => e.stopPropagation()}>
+						{data.steps.map((item, index) => (
+							<div style={{ display: index === data.step ? "" : "none" }} class={[styles.item, ['Woodwind', 'Tenor Recorder'].includes(query.code) && styles.itemScale]}>
+								<div class={styles.title}>
+									<img src={icons.guide_2} />
+									<div class={styles.des} style={{fontSize: index === 2 ? '0.34667rem' : ''}}>
+										{item.des}
+									</div>
+								</div>
+								<div class={styles.icon}>
+									<img src={icons.guide_1} />
+								</div>
+								<Button class={styles.btn} round type="primary" onClick={() => handleNext()}>
+									我知道了
+								</Button>
+							</div>
+						))}
+					</div>
+				</div>
+			</Popup>
+		);
+	},
+});

+ 69 - 0
src/page-instrument/view-figner-listen/guide/guide-index.tsx

@@ -0,0 +1,69 @@
+import { PropType, defineComponent, onMounted, reactive } from "vue";
+import DetailGuide from "./detail-guide";
+import FingerGuide from "./finger-guide";
+import { getQuery } from "/src/utils/queryString";
+
+type guideType = "detail" | "finger";
+export default defineComponent({
+	name: "guide-index",
+	props: {
+		list: {
+			type: Array as PropType<guideType[]>,
+			default: "",
+		},
+		showGuide: {
+			type: Boolean,
+			default: true,
+		}
+	},
+	setup(props) {
+        const query = getQuery();
+		const detailGuideKey = "detailGuideKey";
+		const fingerGuideKey = "fingerGuideKey";
+		const data = reactive({
+			list: props.list,
+			detailShow: false,
+			fingerShow: false,
+		});
+		const init = () => {
+            if (props.showGuide && !query.showGuide) return;
+			if (data.list.includes("detail")) {
+				if (localStorage.getItem(detailGuideKey)) return;
+				setTimeout(() => {
+					data.detailShow = true;
+				}, 300)
+			}
+			if (data.list.includes("finger")) {
+				if (localStorage.getItem(fingerGuideKey)) return;
+				data.fingerShow = true;
+			}
+		};
+		onMounted(() => {
+			init();
+		});
+		return () => (
+			<>
+				{data.detailShow && (
+					<DetailGuide
+						onClose={(val) => {
+							if (val) {
+								localStorage.setItem(detailGuideKey, "1");
+							}
+							data.detailShow = false;
+						}}
+					/>
+				)}
+				{data.fingerShow && (
+					<FingerGuide
+						onClose={(val) => {
+							if (val) {
+								localStorage.setItem(fingerGuideKey, "1");
+							}
+							data.fingerShow = false;
+						}}
+					/>
+				)}
+			</>
+		);
+	},
+});

BIN
src/page-instrument/view-figner-listen/guide/image/icon_detail.png


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 0
src/page-instrument/view-figner-listen/guide/image/icons.json


+ 135 - 0
src/page-instrument/view-figner-listen/guide/index.module.less

@@ -0,0 +1,135 @@
+.detail {
+    position: fixed;
+    left: 50%;
+    bottom: 90px;
+    transform: translateX(-50%);
+    width: 357px;
+    height: 47px;
+    background: url('./image/icon_detail.png') no-repeat;
+    background-size: 100%;
+    display: flex;
+    justify-content: flex-end;
+    padding: 14px 10px 0 0;
+
+    .btn {
+        width: 55px;
+        height: 22px;
+        background: linear-gradient(180deg, #44C9FF 0%, #259CFE 100%);
+        border-radius: 12px;
+        color: #FFFFFF;
+        font-size: 11px;
+        line-height: 22px;
+        text-align: center;
+    }
+
+    .close {
+        color: rgba(255, 255, 255, .55);
+        font-size: 14px;
+        padding: 0 8px;
+        padding-top: 6px;
+    }
+}
+
+.fingerGuide {
+    width: 100%;
+    height: 100%;
+
+    .content {
+        width: 100%;
+        height: 100%;
+    }
+
+    .box {
+        position: fixed;
+        box-shadow: rgba(33, 33, 33, 0.8) 0px 0px 0px 5000px;
+        transition: all 0.25s;
+        border-radius: 8px;
+    }
+    .boxItem1{
+        position: absolute;
+        left: -32px;
+        top: -55px;
+        img{
+            width: 94px;
+        }
+    }
+    .boxItem2{
+        position: absolute;
+        left: -12px;
+        top: -40px;
+        img{
+            width: 42px;
+        }
+    }
+    .boxItem3{
+        position: absolute;
+        left: 20px;
+        top: 0;
+        img{
+            width: 83px;
+        }
+    }
+
+    .item {
+        position: absolute;
+        right: 50px;
+        bottom: 0;
+
+        .title {
+            width: 182px;
+
+            img {
+                width: 100%;
+                height: 67px;
+                display: block;
+            }
+
+            .des {
+                position: absolute;
+                left: 0;
+                top: 0;
+                padding: 8px 8px 0 10px;
+                font-size: 12px;
+                color: #FFFFFF;
+                line-height: 19px;
+            }
+        }
+
+        .icon {
+            width: 88px;
+            height: 99px;
+            margin-left: auto;
+            margin-right: -39px;
+            margin-top: -17px;
+
+            img {
+                width: 100%;
+                height: 100%;
+                object-fit: contain;
+                display: block;
+            }
+        }
+
+        .btn {
+            position: absolute;
+            left: 0;
+            bottom: 45px;
+            width: 83px;
+            height: 27px;
+            line-height: 27px;
+            background: linear-gradient(180deg, #FFF385 0%, #FFC036 100%);
+            border-radius: 13px;
+            border: 1px solid #FFF9DA;
+            font-size: 13px;
+            font-weight: 500;
+            color: #131415;
+            padding: 0;
+        }
+
+        &.itemScale{
+            transform: scale(.8);
+            transform-origin: right bottom;
+            right: 35px;
+        }
+    }
+}

BIN
src/page-instrument/view-figner-listen/image/icon_action_cancel.png


BIN
src/page-instrument/view-figner-listen/image/icon_action_confirm.png


BIN
src/page-instrument/view-figner-listen/image/icon_bg_t.png


BIN
src/page-instrument/view-figner-listen/image/icon_bg_v.png


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


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


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


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


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


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


BIN
src/page-instrument/view-figner-listen/image/icon_loading_head.png


BIN
src/page-instrument/view-figner-listen/image/icon_loading_img.png


BIN
src/page-instrument/view-figner-listen/image/icon_popup_book.png


BIN
src/page-instrument/view-figner-listen/image/icon_popup_book_h.png


BIN
src/page-instrument/view-figner-listen/image/icon_popup_book_v.png


BIN
src/page-instrument/view-figner-listen/image/icon_popup_top.png


BIN
src/page-instrument/view-figner-listen/image/icon_shadow_left.png


BIN
src/page-instrument/view-figner-listen/image/icon_shadow_r.png


BIN
src/page-instrument/view-figner-listen/image/icon_shuo_h.png


BIN
src/page-instrument/view-figner-listen/image/icon_shuo_v.png


BIN
src/page-instrument/view-figner-listen/image/icon_trans_bg.png


BIN
src/page-instrument/view-figner-listen/image/icon_trans_bg_hu.png


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 3 - 0
src/page-instrument/view-figner-listen/image/icons.json


+ 1037 - 0
src/page-instrument/view-figner-listen/index.module.less

@@ -0,0 +1,1037 @@
+@font-face {
+    font-family: 'jianzhu';
+    src: url('./lsy.ttf');
+    font-weight: normal;
+    font-size: normal;
+}
+
+.fingerBox {
+    position: relative;
+    display: flex;
+    flex-direction: column;
+    width: 100vw;
+    height: 100vh;
+    background: rgba(215, 205, 199, 1);
+    user-select: none;
+
+    &.fingerRight {
+        background: url('./image/icon_bg_t.png') no-repeat;
+        background-size: cover;
+
+        .fingerContent {
+            flex-direction: row;
+
+        }
+
+        .backBtn {
+            padding: 26px 17px 26px 29px;
+        }
+
+
+        .tips {
+            width: 43%;
+            border-radius: 18px 0px 0px 18px;
+            padding: 8px;
+            background-image: url('./image/icon_shuo_h.png');
+
+            &::before {
+                content: '';
+                position: absolute;
+                left: 8px;
+                top: 8px;
+                right: 8px;
+                bottom: 8px;
+                border-radius: 15px;
+                border: 1px solid rgba(240, 234, 230, 1);
+                pointer-events: none;
+            }
+
+            &.tipHidden {
+                margin-right: -43%;
+            }
+
+            .tipContentbox {
+                padding: 6px 8px 8px 8px;
+                border-radius: 16px;
+            }
+
+            .tipContent {
+                border-radius: 16px;
+                padding: 14px 8px 4px 8px;
+            }
+        }
+    }
+
+    &.fingerBottom {
+        background: url('./image/icon_bg_v.png') no-repeat;
+        background-size: cover;
+
+        .fingerContent {
+            flex-direction: column;
+        }
+
+
+        .tips {
+            height: 280px;
+            border-radius: 18px 18px 0 0;
+            padding: 8px 8px 0 8px;
+            background-image: url('./image/icon_shuo_v.png');
+
+            &::before {
+                content: '';
+                position: absolute;
+                left: 8px;
+                top: 8px;
+                right: 8px;
+                bottom: 0;
+                border-radius: 15px;
+                border: 1px solid rgba(240, 234, 230, 1);
+                pointer-events: none;
+            }
+
+            &.tipHidden {
+                margin-bottom: -280px;
+            }
+
+            .tipContentbox {
+                padding: 6px 8px 0 8px;
+                border-radius: 16px 16px 0 0;
+            }
+
+            .tipContent {
+                border-radius: 16px 16px 0 0;
+                border-bottom: transparent;
+                padding: 14px 4px 4px 4px;
+            }
+        }
+    }
+}
+
+.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;
+    left: 0;
+    top: 0;
+    right: 0;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    pointer-events: none;
+    padding-right: 18px;
+    padding-top: env(safe-area-inset-top);
+    z-index: 5;
+
+    .backBtn {
+        position: relative;
+        padding: 12px 12px 12px 18px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        border: none;
+        background: none;
+        cursor: pointer;
+        pointer-events: auto;
+
+        img {
+            height: 34px;
+        }
+    }
+
+    .left {
+        display: flex;
+        align-items: center;
+    }
+
+    .baseBtn {
+        width: 60px;
+        height: 45px;
+        background: rgba(255, 255, 255, .48);
+        border-radius: 10px;
+        font-size: 12px;
+        font-weight: 400;
+        color: #616161;
+        line-height: 17px;
+        text-align: center;
+        cursor: pointer;
+        display: flex;
+        flex-direction: column;
+        justify-content: space-evenly;
+        align-items: center;
+        pointer-events: auto;
+
+        img {
+            width: 18px;
+            height: 18px;
+        }
+
+        &:active {
+            opacity: .8;
+        }
+    }
+
+    .rightBtn {
+        display: flex;
+
+        .baseBtn {
+            margin: 0 4px;
+        }
+    }
+}
+
+
+.fingerContent {
+    flex: 1;
+    overflow: hidden;
+    display: flex;
+
+    .wrapFinger {
+        flex: 1;
+        overflow: hidden;
+        display: flex;
+        flex-direction: column;
+    }
+
+    .boxFinger {
+        flex: 1;
+        padding-top: 50px;
+        overflow: hidden;
+    }
+
+
+}
+
+.tips {
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+    position: relative;
+    z-index: 11;
+    flex-shrink: 0;
+    transition: all .3s;
+    // background-color: rgba(190, 166, 140, 1);
+    background-size: 100% 100%;
+    background-repeat: no-repeat;
+    overflow: hidden;
+
+    .tipTitle {
+        position: relative;
+        height: 48px;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        flex-shrink: 0;
+        padding-bottom: 8px;
+
+        :global {
+            .van-button:active:before {
+                opacity: 0 !important;
+            }
+        }
+    }
+
+    .tipTitleName {
+        position: relative;
+        font-weight: 600;
+        z-index: 1;
+        color: #fff;
+        font-size: 16px;
+        text-shadow: 0px 1px 2px #6D4825;
+
+        &::before {
+            content: '';
+            position: absolute;
+            left: -40px;
+            top: 50%;
+            transform: translateY(-50%);
+            width: 25px;
+            height: 1px;
+            background: #fff;
+            z-index: -1;
+        }
+
+        &::after {
+            content: '';
+            position: absolute;
+            right: -40px;
+            top: 50%;
+            transform: translateY(-50%);
+            width: 25px;
+            height: 1px;
+            background: #fff;
+            z-index: -1;
+        }
+    }
+
+    .tipImg {
+        position: absolute;
+        top: 53px;
+        left: 8px;
+        right: 8px;
+
+        img {
+            position: absolute;
+            width: 100%;
+            height: 30px;
+            display: block;
+        }
+    }
+
+    .tipClose {
+        position: absolute;
+        right: 0;
+        top: 0;
+        height: 100%;
+        border: none;
+        background: transparent;
+        border-radius: 0;
+    }
+
+    .iconBook {
+        position: absolute;
+        top: 48px;
+        left: 37px;
+        right: 37px;
+        height: 26px;
+        pointer-events: none;
+        background: url('./image/icon_popup_book.png');
+        background-size: contain;
+        z-index: 1;
+    }
+
+    .tipContentbox {
+        position: relative;
+        flex: 1;
+        background: #FFFFFF;
+        display: flex;
+        overflow: hidden;
+
+        &::before {
+            content: '';
+            position: absolute;
+            top: 0;
+            left: 0;
+            right: 0;
+            padding-bottom: 45%;
+            background: url('./image/icon_popup_top.png') no-repeat;
+            background-size: 100%;
+            opacity: .4;
+            pointer-events: none;
+            z-index: 1;
+        }
+
+        &::after {
+            content: '';
+            position: absolute;
+            top: 6.5px;
+            left: 16px;
+            right: 16px;
+            height: 10px;
+            background: #fff;
+            border-radius: 5px;
+            pointer-events: none;
+            border-top: 1.5px solid rgba(180, 165, 154, 1);
+        }
+    }
+
+    .tipContent {
+        flex: 1;
+        border: 1.5px solid rgba(180, 165, 154, 1);
+        color: rgba(68, 59, 59, 1);
+        font-size: 12px;
+        overflow: hidden;
+        overflow-y: auto;
+    }
+}
+
+.tipItem {
+    display: flex;
+    line-height: 18px;
+    margin-bottom: 8px;
+    background: linear-gradient(180deg, #FFFFFF 0%, #F3F4F7 100%);
+    border-radius: 5px;
+    padding: 10px;
+
+    .iconWrap {
+        display: flex;
+        align-items: center;
+        height: 18px;
+        margin-right: 6px;
+    }
+
+    .tipItemIcon {
+        width: 16px;
+        height: 16px;
+        background: linear-gradient(180deg, rgba(224, 180, 135, 1) 0%, rgba(195, 164, 134, 1) 100%);
+        font-weight: 600;
+        line-height: 16px;
+        color: #FFFFFF;
+        text-shadow: 0px 1px 1px #C8946D;
+        border-radius: 50%;
+        text-align: center;
+
+    }
+}
+
+.notes {
+    position: relative;
+    display: flex;
+    justify-content: center;
+    align-items: flex-start;
+    height: 65px;
+    flex-shrink: 0;
+
+    &.paddingLeft {
+        padding-left: 20Px;
+    }
+
+    .noteContent {
+        display: flex;
+        position: relative;
+        max-width: calc(100% - 92px);
+        border-radius: 25px;
+        background: rgba(255, 255, 255, 0.5);
+        border: 1px solid rgba(255, 255, 255, 0.6);
+        overflow: hidden;
+
+        &.noteContentWrap {
+            &::before {
+                content: '';
+                position: absolute;
+                left: 0;
+                top: 0;
+                height: 100%;
+                width: 23px;
+                background: url('./image/icon_shadow_left.png') no-repeat;
+                background-size: 100% 100%;
+                z-index: 10;
+                pointer-events: none;
+            }
+
+            &::after {
+                content: '';
+                position: absolute;
+                right: 0;
+                top: 0;
+                height: 100%;
+                width: 23px;
+                background: url('./image/icon_shadow_r.png') no-repeat;
+                background-size: 100% 100%;
+                z-index: 10;
+                pointer-events: none;
+            }
+        }
+
+    }
+
+    .noteBox {
+        display: flex;
+        overflow-y: hidden;
+        overflow-x: auto;
+        border-radius: 0 25px 25px 0;
+
+        &::-webkit-scrollbar {
+            width: 0;
+            display: none;
+        }
+    }
+
+    .noteBtn {
+        background: transparent;
+        color: rgba(112, 99, 88, .41);
+        border: 0;
+        padding: 0;
+        font-size: 22px;
+        height: 46px;
+
+        :global {
+            .van-icon {
+                font-weight: bold;
+            }
+        }
+
+        &.disabled {
+            color: rgba(69, 143, 177, .32);
+        }
+
+        &::before {
+            opacity: 0 !important;
+        }
+    }
+}
+
+
+.note {
+    position: relative;
+    margin: 0 2.5Px;
+    width: 46px;
+    height: 46px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    flex-shrink: 0;
+    padding: 6px;
+    z-index: 2;
+
+    img {
+        position: absolute;
+        left: 0;
+        top: 0;
+        width: 100%;
+        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 {
+    position: relative;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    z-index: 1;
+    font-size: 16px;
+    font-family: 'jianzhu';
+    font-weight: normal;
+    color: #616161;
+    line-height: 1;
+    height: 100%;
+
+    &.keyActive {
+        color: #FFF9EC;
+    }
+
+    .dot {
+        width: 3px;
+        height: 3px;
+        border-radius: 50%;
+        background-color: currentColor;
+    }
+
+    .dot+.dot {
+        margin: 2px 0;
+    }
+
+    .noteName {
+        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%;
+        font-size: 12px;
+    }
+}
+
+.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%;
+    height: 100%;
+    display: flex;
+    justify-content: space-evenly;
+    align-items: center;
+    padding: 0 10px 8px 10px;
+}
+
+.loading {
+    position: absolute;
+    left: 0;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    z-index: 100;
+    background: rgba(0, 0, 0, .6);
+}
+
+.loadingWrap {
+    position: relative;
+    width: 295px;
+    padding: 21px 17px;
+    background: rgba(135, 135, 135, .72);
+    border-radius: 24px;
+
+    .loadingIcon {
+        position: absolute;
+        left: 50%;
+        top: -35px;
+        transform: translateX(-50%);
+        width: 216px;
+    }
+
+    .loadingTip {
+        position: absolute;
+        left: 50%;
+        transform: translateX(-50%);
+        bottom: -35px;
+        color: #fff;
+        font-size: 13px;
+        font-weight: 400px;
+    }
+
+    :global {
+        .van-progress {
+            height: 7px;
+        }
+
+        .van-progress__portion {
+            background: linear-gradient(180deg, #3CD6F9 0%, #1CACF1 100%);
+            border: 1px solid rgba(255, 255, 255, .5);
+        }
+
+        .van-progress__pivot {
+            top: 0;
+            color: transparent;
+            background-color: transparent;
+            width: 35px;
+            height: 37px;
+            background-image: url('./image/icon_loading_head.png');
+            background-repeat: no-repeat;
+            background-size: 100% 100%;
+        }
+    }
+}
+
+
+.imgs {
+    position: relative;
+    width: 84%;
+    height: 100%;
+    pointer-events: none;
+
+    &>img {
+        position: absolute;
+        left: 50%;
+        top: 50%;
+        transform: translate(-50%, -50%);
+        display: block;
+        max-width: 100%;
+        max-height: 100%;
+    }
+}
+
+.tizhi {
+    position: absolute;
+    top: 20%;
+    left: 0;
+    width: 30px;
+    height: 30px;
+    text-align: center;
+    line-height: 30px;
+    border-radius: 100%;
+    background-color: #6F99CA;
+    color: #fff;
+    box-shadow: 0 0 10px rgba(0, 0, 0, .05);
+    font-size: 12Px;
+    opacity: 0;
+    pointer-events: none;
+
+    &:active {
+        opacity: .8;
+    }
+}
+
+.canDisplay {
+    opacity: 1;
+    pointer-events: auto;
+}
+
+.disabled {
+    opacity: .5;
+    pointer-events: none;
+}
+
+.toggleBtn {
+    position: fixed;
+    right: 0;
+    top: 50%;
+    transform: translateY(-50%);
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    width: 34px;
+    height: 60px;
+    background: url('./image/icon_trans_bg.png') no-repeat;
+    background-size: 100%;
+    font-size: 14px;
+    color: #616161;
+    line-height: 14px;
+    font-weight: 600;
+    padding-left: 10px;
+    text-align: center;
+    cursor: pointer;
+    z-index: 12;
+
+    &:active {
+        opacity: .8;
+    }
+
+    img {
+        width: 9px;
+        height: 5px;
+        margin-top: 3px;
+    }
+}
+
+.toggleBtnhulusi {
+    height: auto;
+    padding: 14px 0 15px 12px;
+    background-image: url('./image/icon_trans_bg_hu.png');
+    background-size: 100% 100%;
+}
+
+.tones {
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+    position: relative;
+    z-index: 12;
+    transition: all .3s;
+    overflow: hidden;
+    // background-color: rgba(190, 166, 140, 1);
+    background-size: 100% 100%;
+    background-repeat: no-repeat;
+
+    .toneTitle {
+        position: relative;
+        height: 45px;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        flex-shrink: 0;
+
+        :global {
+            .van-button:active:before {
+                opacity: 0 !important;
+            }
+        }
+    }
+
+    .tipTitleName {
+        position: relative;
+        font-weight: 500;
+        font-size: 15px;
+        z-index: 1;
+        color: #fff;
+        font-size: 16px;
+        text-shadow: 0px 1px 2px #6D4825;
+
+        &::before {
+            content: '';
+            position: absolute;
+            left: -40px;
+            top: 50%;
+            transform: translateY(-50%);
+            width: 25px;
+            height: 1px;
+            background: #fff;
+            z-index: -1;
+        }
+
+        &::after {
+            content: '';
+            position: absolute;
+            right: -40px;
+            top: 50%;
+            transform: translateY(-50%);
+            width: 25px;
+            height: 1px;
+            background: #fff;
+            z-index: -1;
+        }
+    }
+
+    .tipClose {
+        position: absolute;
+        right: 0;
+        top: 0;
+        height: 100%;
+        border: none;
+        background: transparent;
+        border-radius: 0;
+    }
+
+    .tipContentbox {
+        position: relative;
+        flex: 1;
+        background: #FFFFFF;
+        display: flex;
+        flex-direction: column;
+        overflow: hidden;
+
+        &::before {
+            content: '';
+            position: absolute;
+            top: 0;
+            left: 0;
+            right: 0;
+            padding-bottom: 45%;
+            background: url('./image/icon_popup_top.png') no-repeat;
+            background-size: 100%;
+            opacity: .4;
+            pointer-events: none;
+        }
+    }
+
+    .tipWrap {
+        flex: 1;
+        overflow: hidden;
+    }
+
+    .tipContent {
+        flex: 1;
+        display: flex;
+        flex-direction: column;
+        border: 1.5px solid rgba(180, 165, 154, 1);
+        color: rgba(68, 59, 59, 1);
+        font-size: 12px;
+
+    }
+
+    .toneAction {
+        border-top: 1px solid #EBEBEB;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        padding: 16px 0;
+
+        img {
+            width: 45%;
+            max-width: 128px;
+            margin: 0 6px;
+
+            &:active {
+                opacity: .85;
+            }
+        }
+    }
+
+    .toneContent {
+        display: flex;
+        flex-wrap: wrap;
+        --van-button-primary-background: rgba(174, 137, 103, 1);
+        --van-button-primary-border-color: rgba(174, 137, 103, 1);
+
+        :global {
+            .van-space-item {
+                width: calc(100% / 4);
+                padding: 6px 2px;
+            }
+
+            .van-button {
+                position: relative;
+                font-size: 13px;
+                width: 100%;
+                height: 0;
+                padding: 0;
+                padding-bottom: 40.5%;
+                flex-shrink: 0;
+
+
+                &::before {
+                    display: none;
+                }
+
+                .van-button__content {
+                    position: absolute;
+                    left: 0;
+                    top: 0;
+                    width: 100%;
+                }
+
+                &:active {
+                    opacity: .8;
+                }
+            }
+
+            .van-button--primary {
+                --van-button-plain-background: RGBA(255, 246, 231, 1);
+            }
+        }
+
+        :global(.van-button--primary) {
+            .dot {
+                background: var(--van-button-primary-background) !important;
+            }
+        }
+
+        .hulusiNoteKey {
+            color: inherit;
+            text-shadow: none;
+        }
+    }
+}
+
+:global(.van-popup--right.tonePopup) {
+    width: 43%;
+    height: 100%;
+    border-radius: 18px 0px 0px 18px;
+    background-color: transparent;
+
+    .tones {
+        padding: 8px;
+        background-image: url('./image/icon_shuo_h.png');
+
+        &::before {
+            content: '';
+            position: absolute;
+            left: 8px;
+            top: 8px;
+            right: 8px;
+            bottom: 8px;
+            border-radius: 15px;
+            border: 1px solid rgba(240, 234, 230, 1);
+            pointer-events: none;
+        }
+
+        .tipContentbox {
+            padding: 6px 8px 8px 8px;
+            border-radius: 16px;
+        }
+
+        .tipContent {
+            border-radius: 16px;
+            padding: 4px 8px;
+        }
+    }
+
+    .hulusiBtn {
+        font-size: 10px;
+        text-wrap: nowrap;
+    }
+}
+
+:global(.van-popup--bottom.tonePopup) {
+    display: flex;
+    flex-direction: column;
+    min-height: 238px;
+    border-radius: 18px 18px 0 0;
+    background-color: transparent;
+
+    .tones {
+        flex: 1;
+        padding: 8px 8px 0 8px;
+        background-image: url('./image/icon_shuo_v.png');
+
+        &::before {
+            content: '';
+            position: absolute;
+            left: 8px;
+            top: 8px;
+            right: 8px;
+            bottom: 0;
+            border-radius: 15px;
+            border: 1px solid rgba(240, 234, 230, 1);
+            pointer-events: none;
+        }
+
+        .tipContentbox {
+            padding: 6px 8px 0 8px;
+            border-radius: 16px 16px 0 0;
+        }
+
+        .tipContent {
+            border-radius: 16px 16px 0 0;
+            border-bottom: transparent;
+            padding: 4px 8px;
+        }
+    }
+
+    .toneContent {
+        margin-top: auto;
+
+        :global {
+            .van-space-item {
+                max-width: 100px;
+            }
+        }
+    }
+}

+ 1060 - 0
src/page-instrument/view-figner-listen/index.tsx

@@ -0,0 +1,1060 @@
+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, 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 Hammer from "hammerjs";
+import { Button, Icon, Loading, Popover, Popup, Progress, Space } from "vant";
+// import GuideIndex from "./guide/guide-index";
+import qs from "query-string";
+import { getQuery } from "/src/utils/queryString";
+import { browser } from "/src/utils";
+import { usePageVisibility } from "@vant/use";
+import { watch } from "vue";
+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 = query.subjectCode || mappingVoicePart(storeData.user?.subjectId, "INSTRUMENT");
+    const subject = code || "pan-flute";
+
+    // 设置屏幕方向
+    api_setRequestedOrientation(["hulusi-flute", "piccolo"].includes(subject) ? 1 : 0);
+
+    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",
+          className: styles.selected,
+        },
+        {
+          text: "听音模式",
+          value: "listenMode",
+          className: "",
+        },
+      ],
+      fingeringMode: "fingeringMode" as "fingeringMode" | "listenMode", // 模式
+      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;
+      }
+    };
+    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);
+    };
+
+    const selectSubjectType = (subject: string) => {
+      data.subjects.forEach((item: any) => {
+        if (item.value === subject) {
+          item.className = styles.selected;
+        } else {
+          item.className = "";
+        }
+      });
+    };
+
+    onBeforeMount(() => {
+      getNotes();
+
+      if (["pan-flute", "ocarina"].includes(data.subject)) {
+        data.viewIndex = 1;
+      }
+
+      selectSubjectType(data.subject);
+      // 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 playStatus = reactive({
+      gamut: false, // 是否播放音阶
+      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 step = 7;
+        for (let i = 0; i < notes.length; i++) {
+          if (!playStatus.gamut) return false;
+
+          if (i % 8 === 0 && i !== 0) {
+            scrollNoteBox("right");
+            step = notes[i].step;
+          }
+          await gaumtPlay(notes[i]);
+        }
+
+        // // 处理播放到最后一个
+        setTimeout(() => {
+          playStatus.gamut = false;
+          gaumntPause();
+        }, 667);
+      }
+    };
+    const gaumtPlay = (note: any, status?: boolean) => {
+      return new Promise((resolve) => {
+        setTimeout(() => {
+          if (playStatus.gamut || status) {
+            noteClick(note);
+          }
+
+          resolve(note);
+        }, 667);
+      });
+    };
+    const gaumntPause = () => {
+      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,
+      /** 0: 未答,1: 答对,2: 答错 */
+      userAnswerStatus: 0 as 0 | 1 | 2, // 用户回答状态
+      userAnswer: {} as any, // 用户答的数据
+    });
+    const onActionPlay = async () => {
+      if (playAction.listenLock) 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);
+          gaumntPause();
+          playAction.listenLock = false;
+        } else {
+          onListenMode();
+        }
+      }
+    };
+
+    // 指法模式
+    const fingeringPlay = (note: any, timer = 1500) => {
+      return new Promise((resolve) => {
+        noteClick(note);
+        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;
+    };
+    // 听音模式
+    const onListenMode = async () => {
+      playAction.listenModeStatus = true; // 是否开始听音
+      playAction.listenLock = true; // 锁
+      let randomIndex = Math.floor(Math.random() * data.notes.length);
+      playAction.exampleAnser = data.notes[randomIndex];
+      data.realKey = playAction.exampleAnser.realKey;
+      scrollAnswer();
+      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]);
+        gaumntPause();
+        playAction.listenLock = false;
+      }, 1000);
+    };
+
+    // 显示答案
+    const onShowAnswer = async () => {
+      if (data.fingeringMode === "fingeringMode") {
+        playAction.showAnswerLoading = true;
+        scrollAnswer();
+        ressetMode();
+      } else if (data.fingeringMode === "listenMode") {
+        if (playAction.listenLock) return;
+        playAction.showAnswerLoading = true;
+        scrollAnswer(playAction.standardAnswer.realKey);
+        await fingeringPlay(playAction.standardAnswer);
+        ressetMode(true, 0);
+      }
+    };
+    // 滚动到对应答案位置
+    const scrollAnswer = (realKey?: any) => {
+      const index = data.notes.findIndex((item: any) => item.realKey === realKey || data.realKey);
+      const activeDom = document.querySelectorAll(".note-class")[index] as any;
+      if (activeDom) {
+        const aWidth = activeDom.offsetWidth;
+        const width = noteBoxRef.value.offsetWidth - aWidth;
+        const wLeft = noteBoxRef.value.offsetLeft;
+        const aLeft = Math.max(activeDom?.offsetLeft - aWidth, 0);
+
+        console.log(aLeft - wLeft - width / 2);
+        (noteBoxRef.value as unknown as HTMLElement).scroll({
+          left: aLeft - wLeft - width / 2,
+          top: 0,
+          behavior: "smooth",
+        });
+      }
+    };
+
+    const ressetMode = (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;
+          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: "2.6rem",
+          paddingBottom: ".6rem",
+        };
+      } else if (data.subject === "piccolo") {
+        return {
+          paddingTop: "3.5rem",
+          paddingBottom: ".6rem",
+        };
+      } else if (data.subject === "pan-flute") {
+        return {
+          paddingTop: "0",
+          paddingBottom: "0",
+        };
+      } else if (data.subject === "ocarina") {
+        return {
+          paddingTop: "1rem",
+          paddingBottom: "0",
+        };
+      } else if (data.subject === "melodica") {
+        return {
+          paddingTop: "2.8rem",
+          paddingBottom: "1rem",
+        };
+      } 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 = "";
+      data.fingeringModeList.forEach((item: any) => {
+        if (item.value === data.fingeringMode) {
+          text = item.text;
+        }
+      });
+      return text;
+    });
+
+    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,
+      };
+    };
+
+    const relactionObj = computed(() => {
+      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 {
+        rs,
+        canTizhi,
+      };
+    });
+    return () => {
+      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) => {
+                  //
+                  selectSubjectType(val.value);
+                  const _url =
+                    location.origin +
+                    location.pathname +
+                    "#/view-figner-listen?" +
+                    qs.stringify({
+                      ...query,
+                      _t: new Date().valueOf(),
+                      subjectCode: val.value,
+                    });
+                  location.href = _url;
+                  window.location.reload();
+                }}
+              >
+                {{
+                  reference: () => (
+                    <div
+                      class={styles.baseBtn}
+                      onClick={() => {
+                        //
+                      }}
+                    >
+                      <img src={icons.icon_change_instrument} />
+                      <span>切换乐器</span>
+                    </div>
+                  ),
+                }}
+              </Popover>
+            </div>
+            <div class={styles.rightBtn}>
+              <Popover
+                placement="bottom"
+                class={styles.popoverContainer}
+                actions={data.fingeringModeList}
+                onSelect={(val: any) => {
+                  //
+                  if (playAction.listenLock) return;
+                  data.fingeringModeList.forEach((item: any) => {
+                    if (item.value === val.value) {
+                      item.className = styles.selected;
+                    } else {
+                      item.className = styles.normal;
+                    }
+                  });
+                  data.fingeringMode = val.value;
+
+                  // 重置
+                  ressetMode(true, 0);
+                }}
+              >
+                {{
+                  reference: () => (
+                    <div class={styles.baseBtn}>
+                      <img src={icons.icon_mode} />
+                      <span>{modeText.value}</span>
+                    </div>
+                  ),
+                }}
+              </Popover>
+
+              <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?.full1} />
+                    {relactionObj.value.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, relactionObj.value.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 draggable={false} class={styles.note} onClick={noteChangeShow}>
+                    <img draggable={false} src={icons.icon_btn_orange} />
+                    <div class={[styles.noteKey]}>
+                      <div class={[styles.noteName, styles.noteFixed]}>{data.noteType === "all" ? "全按键" : "C调"}</div>
+                      <span class={styles.dotFixed}></span>
+                    </div>
+                  </div>
+                  <div ref={noteBoxRef} id="noteBox1" class={styles.noteBox}>
+                    {data.notes.map((note: any, index: number) => (
+                      <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 (playStatus.action) {
+                            playAction.userAnswer = note;
+                            // 判断用户答题
+                            playAction.userAnswerStatus = note.realKey === playAction.standardAnswer.realKey ? 1 : 2;
+                            if (data.fingeringMode === "listenMode") {
+                              playAction.listenLock = true;
+                              data.realKey = note.realKey;
+                              await fingeringPlay(note, 1000);
+                              ressetMode(note.realKey === playAction.standardAnswer.realKey ? true : false, 0);
+                              data.realKey = 0;
+                              playAction.listenLock = false;
+                            } else {
+                              ressetMode(note.realKey === playAction.standardAnswer.realKey ? true : 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 ? note.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 ? note.steps.map((n: any) => <span class={styles.dot}></span>) : null}
+                        </div>
+                      </div>
+                    ))}
+                  </div>
+                </div>
+                <Button class={styles.noteBtn} onClick={() => scrollNoteBox("right")}>
+                  <Icon name="arrow" />
+                </Button>
+              </div>
+              <div class={styles.optionBtns}>
+                {/* , styles.disabled */}
+                <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 && (
+            <>
+              {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>
+      );
+    };
+  },
+});

BIN
src/page-instrument/view-figner-listen/lsy.ttf


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


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 2 - 2
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


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

@@ -14,6 +14,10 @@
     background: rgba(215, 205, 199, 1);
     user-select: none;
 
+    .hiddens {
+        display: none;
+    }
+
     &.fingerRight {
         background: url('./image/icon_bg_t.png') no-repeat;
         background-size: cover;
@@ -23,11 +27,10 @@
 
         }
 
-        .backBtn{
-            padding: 26px 17px 26px 29px;
+        .backBtn {
+            padding: 26px 5px 26px 18px;
         }
 
-
         .tips {
             width: 43%;
             border-radius: 18px 0px 0px 18px;
@@ -107,6 +110,54 @@
     }
 }
 
+.popoverContainer {
+    --van-popover-action-height: 32px;
+    --van-popover-action-font-size: 14px;
+    --van-popover-radius: 12px;
+    --van-popover-action-width: 85px;
+    z-index: 9999 !important;
+    padding: 6Px 0;
+    background-color: #fff;
+    box-shadow: 0 0.05333rem 0.32rem rgba(50, 50, 51, .12);
+    margin-top: 9px !important;
+    border-radius: 8px;
+    color: #999;
+
+    :global {
+        .van-popover__content {
+            max-height: 200px;
+            box-shadow: none;
+            overflow-y: auto;
+            overflow-x: hidden;
+
+            &::-webkit-scrollbar {
+                width: 4px;
+            }
+
+            &::-webkit-scrollbar-thumb {
+                border-radius: 12px;
+                background: rgba(0, 0, 0, 0.2);
+
+                visibility: hidden;
+            }
+
+            &::-webkit-scrollbar-track {
+                border-radius: 0;
+                background: rgba(0, 0, 0, 0.1);
+                visibility: hidden;
+            }
+        }
+
+        .van-popover__action {
+            padding: 0 9px;
+        }
+    }
+
+    .selected {
+        color: #1CACF1;
+        font-weight: 600;
+    }
+}
 
 .head {
     position: absolute;
@@ -140,10 +191,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 +278,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 +442,7 @@
     }
 
     .noteContent {
+        display: flex;
         position: relative;
         max-width: calc(100% - 92px);
         border-radius: 25px;
@@ -391,6 +450,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 +484,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 +529,36 @@
             opacity: 0 !important;
         }
     }
+
+    .tipsT {
+        position: absolute;
+        z-index: 99;
+        top: -40px;
+    }
+
+    .playTips {
+        width: 107px;
+        height: 28px;
+        background: url('./image/tips1.png') no-repeat center center / contain;
+    }
+
+    .playTips2 {
+        width: 147px;
+        height: 28px;
+        background: url('./image/tips4.png') no-repeat center center / contain;
+    }
+
+    .playError {
+        width: 94px;
+        height: 28px;
+        background: url('./image/tips3.png') no-repeat center center / contain;
+    }
+
+    .playSuccess {
+        width: 94px;
+        height: 28px;
+        background: url('./image/tips2.png') no-repeat center center / contain;
+    }
 }
 
 
@@ -480,7 +582,19 @@
         height: 100%;
     }
 
+    .showAnswer {
+        width: 20px;
+        height: 20px;
+        background: url('./image/icon_btn_green_sub.png') no-repeat center / contain;
+        position: absolute;
+        top: 2px;
+        left: -2px;
+        z-index: 99;
 
+        &.errorAnswer {
+            background: url('./image/icon_btn_red_sub.png') no-repeat center / contain;
+        }
+    }
 }
 
 .noteKey {
@@ -516,6 +630,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 +651,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 +866,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 +939,8 @@
             pointer-events: none;
         }
     }
-    .tipWrap{
+
+    .tipWrap {
         flex: 1;
         overflow: hidden;
     }
@@ -786,7 +952,7 @@
         border: 1.5px solid rgba(180, 165, 154, 1);
         color: rgba(68, 59, 59, 1);
         font-size: 12px;
-        
+
     }
 
     .toneAction {
@@ -795,11 +961,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 +993,7 @@
                 padding: 0;
                 padding-bottom: 40.5%;
                 flex-shrink: 0;
-                
+
 
                 &::before {
                     display: none;
@@ -842,7 +1010,8 @@
                     opacity: .8;
                 }
             }
-            .van-button--primary{
+
+            .van-button--primary {
                 --van-button-plain-background: RGBA(255, 246, 231, 1);
             }
         }
@@ -892,7 +1061,8 @@
             padding: 4px 8px;
         }
     }
-    .hulusiBtn{
+
+    .hulusiBtn {
         font-size: 10px;
         text-wrap: nowrap;
     }

+ 1232 - 687
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, showToast } from "vant";
 import GuideIndex from "./guide/guide-index";
 import { getQuery } from "/src/utils/queryString";
 import { browser } from "/src/utils";
@@ -37,666 +15,1233 @@ import { usePageVisibility } from "@vant/use";
 import { watch } from "vue";
 import icon_loading_img from "./image/icon_loading_img.png";
 import state, { IPlatform } from "/src/state";
+import { getSubjectList } from "../api";
 
 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) => {
-			// 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;
-				}
-			}
-			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: [] as any,
+      fingeringModeList: [
+        {
+          text: "指法模式",
+          value: "fingeringMode",
+          icon: icons.icon_click,
+        },
+        {
+          text: "听音模式",
+          value: "listenMode",
+          icon: icons.icon_listen,
+        },
+        {
+          text: "音阶模式",
+          value: "scaleMode",
+          icon: icons.icon_mode,
+        },
+      ],
+      fingeringMode: query.type || ("scaleMode" as "fingeringMode" | "listenMode" | "scaleMode"), // 模式
+      noteType: "all" as "#c" | "all", // 音调
+      loadingDom: false, // 切换乐器时需要重置
+    });
+    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, reject) => {
+        const noteAudio = new Howl({
+          src: url,
+          loop: true,
+          onload: () => {
+            resolve(noteAudio);
+          },
+          onloaderror: () => {
+            reject(new Error(`加载音频失败`));
+          },
+        });
+      });
+    };
+    const getSounFonts = async () => {
+      const pathname = /(192|localhost)/.test(location.origin) ? "/" : location.pathname;
+      data.loadingSoundFonts = true;
+      try {
+        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;
+      } catch (e: any) {
+        //
+        showToast(e.message);
+      }
+      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") {
+        if (["pan-flute", "ocarina"].includes(data.subject)) {
+          data.viewIndex = 1;
+        } else {
+          data.viewIndex = 0;
+        }
+        const o: any = {
+          "pan-flute": 2,
+          ocarina: 2,
+          piccolo: 2,
+          "hulusi-flute": 2,
+          "baroque-recorder": 2,
+        };
+        data.viewTotal = o[data.subject] || 1;
+        data.fingeringMode = "listenMode";
+      } else if (data.fingeringMode === "listenMode") {
+        data.fingeringMode = "fingeringMode";
+      } else if (data.fingeringMode === "fingeringMode") {
+        data.fingeringMode = "scaleMode";
+        data.viewIndex = 0;
+        data.noteType = "all";
+      }
+      resetMode(true, 0);
+      setTimeout(() => {
+        __init(false);
+      }, 100);
+    };
+
+    const __init = async (loadSong = true) => {
+      data.loadingDom = true;
+      getNotes();
+
+      selectSubjectType(data.subject);
+
+      if (data.fingeringMode === "fingeringMode") {
+        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,
+        "baroque-recorder": 2,
+      };
+      data.viewTotal = o[data.subject] || 1;
+      getFingeringData();
+      getHeadTop();
+      if (loadSong) {
+        await getSounFonts();
+      }
+
+      data.loadingDom = false;
+    };
+
+    // 获取声部
+    const getSubjects = async () => {
+      try {
+        const subjects = await getSubjectList({
+          enableFlag: true,
+          delFlag: 0,
+          page: 1,
+          rows: 999,
+        });
+        const rows = subjects.data.rows || [];
+        rows.forEach((row: any) => {
+          data.subjects.push({
+            text: row.name,
+            value: mappingVoicePart(row.code, "INSTRUMENT"),
+            className: "",
+          });
+        });
+        console.log(data.subjects, "subjects");
+      } catch {
+        //
+      }
+    };
+
+    onBeforeMount(async () => {
+      if (browser().isApp) {
+        state.platform = "APP" as IPlatform;
+      } else {
+        state.platform = query.platform?.toLocaleUpperCase() || "";
+      }
+
+      if (state.platform === IPlatform.PC) {
+        document.title = "听音练习";
+      }
+      await getSubjects();
+      __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];
+      if (data.noteAudio) {
+        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) {
+        console.log(1, query);
+        if (query.matchMedia == 1) {
+          // 老师端,首页
+          window.parent.postMessage(
+            {
+              api: "iframe_exit",
+            },
+            "*"
+          );
+          return;
+        } else {
+          window.close();
+          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();
+          gaumntPause();
+        }
+      }
+    );
+    /** 课件播放 */
+    const changePlay = (res: any) => {
+      if (res?.data?.api === "setPlayState") {
+        handleStop();
+        gaumntPause();
+      }
+    };
+
+    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;
+      gaumntPause();
+      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);
+      document.title = "Ai学练";
+    });
+
+    const containerBox = computed(() => {
+      if (state.platform === IPlatform.PC || query.modelType) {
+        return {
+          paddingTop: "1rem",
+          paddingBottom: "",
+        };
+      }
+      if (data.fingeringMode === "scaleMode") {
+        if (data.subject === "hulusi-flute") {
+          return {
+            paddingTop: "3.1rem",
+            paddingBottom: ".8rem",
+          };
+        } else if (data.subject === "piccolo" || data.subject === "baroque-recorder") {
+          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: "",
+          };
+        }
+      } else {
+        if (data.subject === "hulusi-flute") {
+          return {
+            paddingTop: "3.1rem",
+            paddingBottom: "0rem",
+          };
+        } else if (data.subject === "piccolo" || data.subject === "baroque-recorder") {
+          return {
+            paddingTop: "3rem",
+            paddingBottom: ".5rem",
+          };
+        } else if (data.subject === "pan-flute") {
+          return {
+            paddingTop: "0",
+            paddingBottom: "0",
+          };
+        } else if (data.subject === "ocarina") {
+          return {
+            paddingTop: "1rem",
+            paddingBottom: "0",
+          };
+        } else if (data.subject === "melodica") {
+          return {
+            paddingTop: "2.8rem",
+            paddingBottom: "0.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", "baroque-recorder"].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>
+
+              <div class={styles.baseBtn} onClick={onChangeFingeringModel}>
+                <img src={modeText.value.icon} />
+                <span>{modeText.value.text}</span>
+              </div>
+              <Popover
+                placement="bottom"
+                class={styles.popoverContainer}
+                actions={data.subjects}
+                onUpdate:show={() => {
+                  // 播放音阶时不能切换
+                  if (playStatus.gamut) return;
+                  // 开始答题不能切换
+                  if (playStatus.action) return;
+                }}
+                onSelect={(val: any) => {
+                  if (data.subject === val.value) return;
+                  data.subject = val.value;
+                  data.viewIndex = 0;
+                  data.loadingDom = true;
+                  fingerData.fingeringInfo = subjectFingering(data.subject);
+                  resetElement();
+                  resetMode(true, 0);
+                  api_setRequestedOrientation(orientationDirection.value);
+                  // 设置屏幕方向
+                  setTimeout(() => {
+                    data.paddingTop = "";
+                    data.paddingLeft = "";
+                    __init();
+                  }, 100);
+                }}
+              >
+                {{
+                  reference: () => (
+                    <div
+                      class={styles.baseBtn}
+                      onClick={(e) => {
+                        //
+                        // 播放音阶时不能切换
+                        if (playStatus.gamut) {
+                          e.stopPropagation();
+                          e.preventDefault();
+                        }
+                        // 开始答题不能切换
+                        // if (playStatus.action) return;
+                        if (playAction.listenLock) {
+                          e.stopPropagation();
+                          e.preventDefault();
+                        }
+                      }}
+                    >
+                      <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={() => 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>}
+
+                {((data.noteType !== "#c" && (orientationDirection.value === 0 || (orientationDirection.value === 1 && state.platform === IPlatform.PC))) || (orientationDirection.value === 1 && state.platform === IPlatform.APP)) && (
+                  <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.exampleAnser.realKey === note.realKey) ||
+                                  (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>
+                {((data.noteType !== "#c" && (orientationDirection.value === 0 || (orientationDirection.value === 1 && state.platform === IPlatform.PC))) || (orientationDirection.value === 1 && state.platform === IPlatform.APP)) && (
+                  <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.loadingDom ? styles.hiddens : "", 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>
+      );
+    };
+  },
 });

+ 40 - 39
src/store.ts

@@ -1,23 +1,24 @@
 import { reactive } from "vue";
 
 type IUser = {
-	username?: string;
-	nickname?: string;
-	/** 真实姓名 */
-	realName?: string;
-	/** 会员结束时间 */
-	membershipEndTime?: string;
-	tenantId?: number;
-	/** 声部名称 */
-	subjectNames?: string;
-	subjectName?: string;
-	/** 头像 */
-	avatar?: string;
-	memberRankSettingId?: number;
-	id?: string | number;
-	clientType?: "BACKEND" | "SCHOOL" | "TEACHER" | "STUDENT";
-	/** 是否是VIP */
-	vipMember?: boolean;
+  username?: string;
+  nickname?: string;
+  /** 真实姓名 */
+  realName?: string;
+  /** 会员结束时间 */
+  membershipEndTime?: string;
+  tenantId?: number;
+  /** 声部名称 */
+  subjectId?: any;
+  subjectNames?: string;
+  subjectName?: string;
+  /** 头像 */
+  avatar?: string;
+  memberRankSettingId?: number;
+  id?: string | number;
+  clientType?: "BACKEND" | "SCHOOL" | "TEACHER" | "STUDENT";
+  /** 是否是VIP */
+  vipMember?: boolean;
 };
 type IStatus = "init" | "login" | "logout" | "error";
 type IPlatformType = "STUDENT" | "TEACHER" | "WEB" | "";
@@ -25,41 +26,41 @@ type IPlatformApi = "/api-student" | "/api-teacher" | "/api-web" | "/api-backend
 type IProxy = "" | "/gym" | "/colexiu" | "/orchestra" | "/instrument";
 
 export interface IStoreData {
-	platformType: IPlatformType;
-	platformApi: IPlatformApi;
-	proxy: IProxy;
-	isApp: boolean;
+  platformType: IPlatformType;
+  platformApi: IPlatformApi;
+  proxy: IProxy;
+  isApp: boolean;
 }
 
 export const storeData = reactive({
-	status: "init" as IStatus,
-	/** 用户信息 */
-	user: {} as IUser,
-	/** 端口 */
-	platformType: "STUDENT" as IPlatformType,
-	/** api地址前缀 */
-	platformApi: "/api-student" as IPlatformApi,
-	/** 开发模式api前缀 */
-	proxy: "" as IProxy,
-	/** 是否在APP中 */
-	isApp: false,
+  status: "init" as IStatus,
+  /** 用户信息 */
+  user: {} as IUser,
+  /** 端口 */
+  platformType: "STUDENT" as IPlatformType,
+  /** api地址前缀 */
+  platformApi: "/api-student" as IPlatformApi,
+  /** 开发模式api前缀 */
+  proxy: "" as IProxy,
+  /** 是否在APP中 */
+  isApp: false,
 });
 
 /** 初始化 */
 export const setStoreData = (data: IStoreData) => {
-	Object.assign(storeData, data);
+  Object.assign(storeData, data);
 };
 
 /** 设置用户信息 */
 export const setUserInfo = (user: IUser) => {
-	storeData.status = "login";
-	storeData.user = user || {};
+  storeData.status = "login";
+  storeData.user = user || {};
 };
 export const setLogout = () => {
-	storeData.status = "logout";
-	storeData.user = {};
+  storeData.status = "logout";
+  storeData.user = {};
 };
 export const setLoginError = () => {
-	storeData.status = "error";
-	storeData.user = {};
+  storeData.status = "error";
+  storeData.user = {};
 };

+ 300 - 0
src/view/figner-preview/index.ts

@@ -5010,4 +5010,304 @@ export const FIGNER_INSTRUMENT_DATA: { [_: string]: IFIGNER_INSTRUMENT_DATA } =
 			},
 		],
 	},
+	"baroque-recorder": {
+		tips: [
+			{
+				key: 0,
+				name: "拿竖笛的方法",
+				octave: 0,
+				step: 0,
+				realKey: 0,
+				realName:
+					"把竖笛拿起来,然后放在嘴边,把它轻轻地放在你的唇间,并用你的手指拿稳。记住要把你的左手放在上边,有一个孔的背面应该正对着你,不要咬吹口或是让它碰到牙。",
+			},
+			{
+				key: 0,
+				name: "吹竖笛的力度",
+				octave: 0,
+				step: 0,
+				realKey: 0,
+				realName:
+					"向竖笛吹气,轻一点吹想象你在吹泡泡一样,控制气流的稳定并轻柔地吹奏,尝试用膈肌呼吸并确保你吹得均匀,这可以使声音稳定持续。",
+			},
+			{
+				key: 0,
+				name: "学习呼吸方法",
+				octave: 0,
+				step: 0,
+				realKey: 0,
+				realName:
+					"吹竖笛时,气息的控制是很关键的,气息分为缓吹法和急吹法,吸气要从鼻子和嘴角吸气,吸到胸部和腰部,小腹微微向里收,以使演奏有气息支持。",
+			},
+			{
+				key: 0,
+				name: "勤练习指法",
+				octave: 0,
+				step: 0,
+				realKey: 0,
+				realName:
+					"指法图是用来表示竖笛上的单音的,后背的孔叫0孔,从上到下依次为一孔、二孔、三孔、四孔、五孔、六孔、七孔,要吹奏简单的练习曲,必须要牢记单音的指法。",
+			},
+			{
+				key: 0,
+				name: "学习一些演奏技法",
+				octave: 0,
+				step: 0,
+				realKey: 0,
+				realName:
+					"单吐是用舌尖顶住上牙的牙根,用气息轻轻地把舌尖冲开,待声音发出后舌尖有弹性地返回到原来的位置,这时舌尖就像一个通气阀门,如果我们让它发出声音就像发“嘟”音的感觉。在有连线的地方只有第一个音采用单吐的技法,后面的音不再做吐音,只接前面“嘟”音的尾音发出“呜”音,整个连线里面的音就像“嘟呜”的。",
+			},
+		],
+		tones: [
+			{
+				key: 1,
+				name: "C",
+				octave: 5,
+				step: 0,
+				realKey: 0,
+				realName: "",	
+			}
+		],
+		list: [
+			{
+				key: 1,
+				name: "C",
+				octave: 5,
+				step: 0,
+				realKey: 60,
+				realName: "C5",
+			},
+			{
+				key: 2,
+				name: "D",
+				octave: 5,
+				step: 0,
+				realKey: 61,
+				mark: "fall",
+				realName: "Db5",
+			},
+			{
+				key: 2,
+				name: "D",
+				octave: 5,
+				step: 0,
+				realKey: 62,
+				realName: "D5",
+			},
+			{
+				key: 3,
+				name: "E",
+				octave: 5,
+				step: 0,
+				realKey: 63,
+				mark: "fall",
+				realName: "Eb5",
+			},
+			{
+				key: 3,
+				name: "E",
+				octave: 5,
+				step: 0,
+				realKey: 64,
+				realName: "E5",
+			},
+			{
+				key: 4,
+				name: "F",
+				octave: 5,
+				step: 0,
+				realKey: 65,
+				realName: "F5",
+			},
+			{
+				key: 5,
+				name: "G",
+				octave: 5,
+				step: 0,
+				realKey: 66,
+				mark: "fall",
+				realName: "Gb5",
+			},
+			{
+				key: 5,
+				name: "G",
+				octave: 5,
+				step: 0,
+				realKey: 67,
+				realName: "G5",
+			},
+			{
+				key: 6,
+				name: "A",
+				octave: 5,
+				step: 0,
+				realKey: 68,
+				mark: "fall",
+				realName: "Ab5",
+			},
+			{
+				key: 6,
+				name: "A",
+				octave: 5,
+				step: 0,
+				realKey: 69,
+				realName: "A5",
+			},
+			{
+				key: 7,
+				name: "B",
+				octave: 5,
+				step: 0,
+				realKey: 70,
+				mark: "fall",
+				realName: "Bb5",
+			},
+			{
+				key: 7,
+				name: "B",
+				octave: 5,
+				step: 0,
+				realKey: 71,
+				realName: "B5",
+			},
+			{
+				key: 1,
+				name: "C",
+				octave: 6,
+				step: 1,
+				realKey: 72,
+				realName: "C6",
+			},
+			{
+				key: 2,
+				name: "D",
+				octave: 6,
+				step: 1,
+				realKey: 73,
+				mark: "fall",
+				realName: "Db6",
+			},
+			{
+				key: 2,
+				name: "D",
+				octave: 6,
+				step: 1,
+				realKey: 74,
+				realName: "D6",
+			},
+			{
+				key: 3,
+				name: "E",
+				octave: 6,
+				step: 1,
+				realKey: 75,
+				mark: "fall",
+				realName: "Eb6",
+			},
+			{
+				key: 3,
+				name: "E",
+				octave: 6,
+				step: 1,
+				realKey: 76,
+				realName: "E6",
+			},
+			{
+				key: 4,
+				name: "F",
+				octave: 6,
+				step: 1,
+				realKey: 77,
+				realName: "F6",
+			},
+			{
+				key: 5,
+				name: "G",
+				octave: 6,
+				step: 1,
+				realKey: 78,
+				mark: "fall",
+				realName: "Gb6",
+			},
+			{
+				key: 5,
+				name: "G",
+				octave: 6,
+				step: 1,
+				realKey: 79,
+				realName: "G6",
+			},
+			{
+				key: 6,
+				name: "A",
+				octave: 6,
+				step: 1,
+				realKey: 80,
+				mark: "fall",
+				realName: "Ab6",
+			},
+			{
+				key: 6,
+				name: "A",
+				octave: 6,
+				step: 1,
+				realKey: 81,
+				realName: "A6",
+			},
+			{
+				key: 7,
+				name: "B",
+				octave: 6,
+				step: 1,
+				realKey: 82,
+				mark: "fall",
+				realName: "Bb6",
+			},
+			{
+				key: 7,
+				name: "B",
+				octave: 6,
+				step: 1,
+				realKey: 83,
+				realName: "B6",
+			},
+			{
+				key: 1,
+				name: "C",
+				octave: 7,
+				step: 2,
+				realKey: 84,
+				realName: "C7",
+			},
+			{
+				key: 2,
+				name: "D",
+				octave: 7,
+				step: 2,
+				realKey: 85,
+				mark: "fall",
+				realName: "Db7",
+			},
+			{
+				key: 2,
+				name: "D",
+				octave: 7,
+				step: 2,
+				realKey: 86,
+				realName: "D7",
+			},
+			{
+				key: 3,
+				name: "D",
+				octave: 7,
+				step: 2,
+				realKey: 87,
+				mark: "fall",
+				realName: "Gb7",
+			},
+		],
+	},
 };
+
+
+export const FIGNER_INSTRUMENT_REALKEY = [60, 62, 64, 65, 67, 69, 71, 72];

+ 552 - 504
src/view/fingering/fingering-config.ts

@@ -2,524 +2,572 @@ import { CSSProperties } from "vue";
 import relationships from "./fingering-relationships";
 
 export type ITypeFingering = {
-	json: any;
-	relationship: any;
-	height?: number | string;
-	width?: number | string;
-	maxWidth?: number;
-	styles?: CSSProperties;
+  json: any;
+  relationship: any;
+  height?: number | string;
+  width?: number | string;
+  maxWidth?: number;
+  styles?: CSSProperties;
 } | null;
 export type IFingering = {
-	name?: IVocals;
-	direction?: "vertical" | "transverse";
-	width?: string;
-	height?: string;
-	/** 禁用替指 */
-	disabledFinger?: boolean;
-	/** 横竖屏 0:横屏 1: 竖屏 */
-	orientation?: number;
-	code?: string;
-	/** 是否有替指 */
-	hasTizhi?: boolean;
+  name?: IVocals;
+  direction?: "vertical" | "transverse";
+  width?: string;
+  height?: string;
+  /** 禁用替指 */
+  disabledFinger?: boolean;
+  /** 横竖屏 0:横屏 1: 竖屏 */
+  orientation?: number;
+  code?: string;
+  /** 是否有替指 */
+  hasTizhi?: boolean;
 };
 
 type ITypeContent = {
-	[key: string | number]: IFingering;
+  [key: string | number]: IFingering;
 };
 
 export type IVocals =
-	| "flute"
-	| "clarinet"
-	| "saxophone"
-	| "trumpet"
-	| "horn"
-	| "trombone"
-	| "up-bass-horn"
-	| "small-drum"
-	| "tuba"
-	| "piccolo"
-	| "piccolo1"
-	| "piccolo2"
-	| "hulusi-flute"
-	| "hulusi-flute1"
-	| "hulusi-flute2"
-	| "pan-flute"
-	| "pan-flute1"
-	| "pan-flute2"
-	| "pan-flute3"
-	| "pan-flute4"
-	| "ocarina"
-	| "ocarina1"
-	| "ocarina2"
-	| "melodica";
+  | "flute"
+  | "clarinet"
+  | "saxophone"
+  | "trumpet"
+  | "horn"
+  | "trombone"
+  | "up-bass-horn"
+  | "small-drum"
+  | "tuba"
+  | "piccolo"
+  | "piccolo1"
+  | "piccolo2"
+  | "hulusi-flute"
+  | "hulusi-flute1"
+  | "hulusi-flute2"
+  | "pan-flute"
+  | "pan-flute1"
+  | "pan-flute2"
+  | "pan-flute3"
+  | "pan-flute4"
+  | "ocarina"
+  | "ocarina1"
+  | "ocarina2"
+  | "melodica"
+  | "melodica1"
+  | "baroque-recorder"
+  | "baroque-recorder1"
+  | "baroque-recorder2";
 
 /** 映射声部ID */
-export const mappingVoicePart = (
-	id: number | string,
-	soruce: "GYM" | "COLEXIU" | "ORCHESTRA" | "INSTRUMENT" | "ENSEMBLE"
-): number => {
-	if (soruce === "GYM") {
-		return Number(id);
-	} else if (soruce === "COLEXIU") {
-		const subject: { [_key: string | number]: number } = {
-			Flute: 2,
-			Clarinet: 4,
-			Trombone: 14,
-			Tuba: 17,
-			Trumpet: 12,
-			Horn: 13,
-			AltoSaxophone: 6,
-			TenorSaxophone: 6,
-			Saxophone: 6,
-			UpBassHorn: 15,
-			Melodica: 137,
-			HulusiFlute: 136,
-			PanFlute: 135,
-			Ocarina: 134,
-			Recorder: 120,
-			Ukulele: 130,
-			Mouthorgan: 140,
-			Piano: 150,
-		};
-		return subject[id];
-	} else if (soruce === "ORCHESTRA") {
-		const subject: { [_key: string | number]: number } = {
-			1: 23,
-			2: 2,
-			3: 5,
-			4: 4,
-			5: 12,
-			6: 14,
-			7: 13,
-			8: 15,
-			9: 17,
-		};
-		return subject[id];
-	} else if (soruce === "INSTRUMENT") {
-		let code = id;
-		if (typeof code === "string") {
-			code = code.toLocaleLowerCase().replace(/ /g, "");
-		}
-		const subject: { [_key: string | number]: any } = {
-			flute: 2,
-			clarinet: 4,
-			trombone: 14,
-			tuba: 17,
-			trumpet: 12,
-			horn: 13,
-			altosaxophone: 6,
-			tenorsaxophone: 6,
-			saxophone: 6,
-			upbasshorn: 15,
-			melodica: 137,
-			hulusiFlute: 136,
-			panflute: 135,
-			recorder: 120,
-			ukulele: 130,
-			mouthorgan: 140,
-			piano: 150,
-			4: "piccolo",
-			3: "hulusi-flute",
-			1: "pan-flute",
-			2: "ocarina",
-			5: "melodica",
-			23: 2,
-			24: 6,
-			25: 4,
-			26: 12,
-			27: 14,
-			28: 13,
-			29: 15,
-			30: 17,
-			tenorrecorder: "piccolo",
-			woodwind: "hulusi-flute",
-			panpipes: "pan-flute",
-			ocarina: "ocarina",
-			nai: "melodica",
-		};
-		return subject[code] || 0;
-	} else if (soruce === "ENSEMBLE") {
-		let code = id;
-		const subject: { [_key: string | number]: any } = {
-			"Piccolo": "piccolo",
-			"Flute": 2,
-			"Flute 1": 2,
-			"Flute 2": 2,
-			"Oboe": 1,
-			"Clarinet in Bb 1": 4,
-			"Clarinet in Bb 2": 4,
-			"Alto Clarinet in Eb": 4,
-			"Bass Clarinet in Bb": 4,
-			"Bassoon": 1,
-			"Alto Saxophone": 5,
-			"Tenor Saxophone": 5,
-			"Baritone Saxophone": 5,
-			"Trumpet in Bb 1": 12,
-			"Trumpet in Bb 2": 12,
-			"Horn in F": 13,
-			"Horn in F 1": 13,
-			"Horn in F 2": 13,
-			"Trombone 1": 14,
-			"Trombone 2": 14,
-			"Trombone 3": 14,
-			"Euphonium": 15,
-			"Tuba": 17,
-			"Chimes": 1,
-			"Bells": 1,
-			"Xylophone": 1,
-			"Snare Drum": 1,
-			"Bass Drum": 1,
-			"Triangle": 1,
-			"Suspended Cymbal": 1,
-			"Crash Cymbals": 1,
-			"Concert Toms": 1,
-			"Timpani": 1,
-			flute: 2,
-			oboe: 4,
-			clarinet: 4,
-			trombone: 14,
-			tuba: 17,
-			trumpet: 12,
-			horn: 13,
-			altosaxophone: 6,
-			tenorsaxophone: 6,
-			saxophone: 6,
-			upbasshorn: 15,
-			melodica: 137,
-			hulusiFlute: 136,
-			panflute: 135,
-			recorder: 120,
-			ukulele: 130,
-			mouthorgan: 140,
-			piano: 150,
-			4: "piccolo",
-			3: "hulusi-flute",
-			1: "pan-flute",
-			2: "ocarina",
-			5: "melodica",
-			26: 12,
-			tenorrecorder: "piccolo",
-			woodwind: "hulusi-flute",
-			panpipes: "pan-flute",
-			ocarina: "ocarina",
-			nai: "melodica",
-		};
-		let _track;
-		if (typeof code === 'string') {
-			for (let sKey in subject) {
-				if(sKey === code) {
-					_track = subject[sKey]
-					break;
-				}
-			}
-		} else {
-			_track = subject.code
-		}
-		return _track;
-	}
-	return 0;
+export const mappingVoicePart = (id: number | string, soruce: "GYM" | "COLEXIU" | "ORCHESTRA" | "INSTRUMENT" | "ENSEMBLE"): number => {
+  if (soruce === "GYM") {
+    return Number(id);
+  } else if (soruce === "COLEXIU") {
+    const subject: { [_key: string | number]: number } = {
+      Flute: 2,
+      Clarinet: 4,
+      Trombone: 14,
+      Tuba: 17,
+      Trumpet: 12,
+      Horn: 13,
+      AltoSaxophone: 6,
+      TenorSaxophone: 6,
+      Saxophone: 6,
+      UpBassHorn: 15,
+      Melodica: 137,
+      HulusiFlute: 136,
+      PanFlute: 135,
+      Ocarina: 134,
+      Recorder: 120,
+      Ukulele: 130,
+      Mouthorgan: 140,
+      Piano: 150,
+    };
+    return subject[id];
+  } else if (soruce === "ORCHESTRA") {
+    const subject: { [_key: string | number]: number } = {
+      1: 23,
+      2: 2,
+      3: 5,
+      4: 4,
+      5: 12,
+      6: 14,
+      7: 13,
+      8: 15,
+      9: 17,
+    };
+    return subject[id];
+  } else if (soruce === "INSTRUMENT") {
+    let code = id;
+    if (typeof code === "string") {
+      code = code.toLocaleLowerCase().replace(/ /g, "");
+    }
+    console.log(code, "1212");
+    const subject: { [_key: string | number]: any } = {
+      flute: 2,
+      clarinet: 4,
+      trombone: 14,
+      tuba: 17,
+      trumpet: 12,
+      horn: 13,
+      altosaxophone: 6,
+      tenorsaxophone: 6,
+      saxophone: 6,
+      upbasshorn: 15,
+      melodica: 137,
+      hulusiFlute: 136,
+      panflute: 135,
+      recorder: 120,
+      ukulele: 130,
+      mouthorgan: 140,
+      piano: 150,
+	  baroquerecorder: 'baroque-recorder',
+      4: "piccolo",
+      3: "hulusi-flute",
+      1: "pan-flute",
+      2: "ocarina",
+      5: "melodica",
+      23: 2,
+      24: 6,
+      25: 4,
+      26: 12,
+      27: 14,
+      28: 13,
+      29: 15,
+      30: 17,
+      tenorrecorder: "piccolo",
+      woodwind: "hulusi-flute",
+      panpipes: "pan-flute",
+      ocarina: "ocarina",
+      nai: "melodica",
+      15: "baroque-recorder",
+    };
+    return subject[code] || 0;
+  } else if (soruce === "ENSEMBLE") {
+    let code = id;
+    const subject: { [_key: string | number]: any } = {
+      Piccolo: "piccolo",
+      Flute: 2,
+      "Flute 1": 2,
+      "Flute 2": 2,
+      Oboe: 1,
+      "Clarinet in Bb 1": 4,
+      "Clarinet in Bb 2": 4,
+      "Alto Clarinet in Eb": 4,
+      "Bass Clarinet in Bb": 4,
+      Bassoon: 1,
+      "Alto Saxophone": 5,
+      "Tenor Saxophone": 5,
+      "Baritone Saxophone": 5,
+      "Trumpet in Bb 1": 12,
+      "Trumpet in Bb 2": 12,
+      "Horn in F": 13,
+      "Horn in F 1": 13,
+      "Horn in F 2": 13,
+      "Trombone 1": 14,
+      "Trombone 2": 14,
+      "Trombone 3": 14,
+      Euphonium: 15,
+      Tuba: 17,
+      Chimes: 1,
+      Bells: 1,
+      Xylophone: 1,
+      "Snare Drum": 1,
+      "Bass Drum": 1,
+      Triangle: 1,
+      "Suspended Cymbal": 1,
+      "Crash Cymbals": 1,
+      "Concert Toms": 1,
+      Timpani: 1,
+      flute: 2,
+      oboe: 4,
+      clarinet: 4,
+      trombone: 14,
+      tuba: 17,
+      trumpet: 12,
+      horn: 13,
+      altosaxophone: 6,
+      tenorsaxophone: 6,
+      saxophone: 6,
+      upbasshorn: 15,
+      melodica: 137,
+      hulusiFlute: 136,
+      panflute: 135,
+      recorder: 120,
+      ukulele: 130,
+      mouthorgan: 140,
+      piano: 150,
+      4: "piccolo",
+      3: "hulusi-flute",
+      1: "pan-flute",
+      2: "ocarina",
+      5: "melodica",
+      26: 12,
+      tenorrecorder: "piccolo",
+      woodwind: "hulusi-flute",
+      panpipes: "pan-flute",
+      ocarina: "ocarina",
+      nai: "melodica",
+	  BaroqueRecorder: 'baroque-recorder',
+    };
+    let _track;
+    if (typeof code === "string") {
+      for (let sKey in subject) {
+        if (sKey === code) {
+          _track = subject[sKey];
+          break;
+        }
+      }
+    } else {
+      _track = subject.code;
+    }
+    return _track;
+  }
+  return 0;
 };
 
 /** 声部的指法配置信息 */
 export const subjectFingering = (subjectId: number | string): IFingering => {
-	switch (subjectId) {
-		case 2: // 长笛
-			return {
-				name: "flute",
-				direction: "transverse",
-				height: "1.6rem",
-				hasTizhi: true,
-			};
-		case 4: // 单簧管
-			return {
-				name: "clarinet",
-				direction: "vertical",
-				width: "3rem",
-				hasTizhi: true,
-			};
-		case 5: // 萨克斯
-		case 6: // 中音萨克斯
-			return {
-				name: "saxophone",
-				direction: "vertical",
-				width: "4.34rem",
-				hasTizhi: true,
-			};
-		case 12: // 小号
-			return {
-				name: "trumpet",
-				direction: "transverse",
-				height: "1.6rem",
-				hasTizhi: false,
-			};
-		case 13: // 圆号
-			return {
-				name: "horn",
-				direction: "vertical",
-				width: "4.98rem",
-				hasTizhi: false,
-			};
-		case 14: // 长号
-			return {
-				name: "trombone",
-				direction: "transverse",
-				height: "1.6rem",
-				hasTizhi: false,
-			};
-		case 15: // 上低音号
-			return {
-				name: "up-bass-horn",
-				direction: "vertical",
-				width: "4.34rem",
-				hasTizhi: false,
-			};
-		case 17: // 大号
-			return {
-				name: "tuba",
-				direction: "vertical",
-				width: "4.34rem",
-				hasTizhi: false,
-			};
-		case 120: // 短笛
-			return {
-				name: "piccolo",
-				direction: "vertical",
-				width: "3rem",
-				orientation: 1,
-				hasTizhi: true,
-			};
-		case "piccolo": // 竖笛
-			return {
-				name: "piccolo",
-				direction: "vertical",
-				width: "3rem",
-				orientation: 1,
-				code: "竖笛",
-				hasTizhi: true,
-			};
-		case "hulusi-flute": // 葫芦丝
-			return {
-				name: "hulusi-flute",
-				direction: "vertical",
-				width: "3rem",
-				orientation: 1,
-				code: "葫芦丝",
-				hasTizhi: false,
-			};
-		case "pan-flute": // 排箫
-			return {
-				name: "pan-flute",
-				direction: "transverse",
-				height: "2rem",
-				disabledFinger: true,
-				orientation: 0,
-				code: "排箫",
-				hasTizhi: false,
-			};
-		case "ocarina": // 陶笛
-			return {
-				name: "ocarina",
-				direction: "vertical",
-				width: "3rem",
-				disabledFinger: true,
-				orientation: 0,
-				code: "陶笛",
-				hasTizhi: false,
-			};
-		case "melodica": // 口风琴
-			return {
-				name: "melodica",
-				direction: "transverse",
-				height: "2rem",
-				orientation: 0,
-				code: "口风琴",
-				hasTizhi: false,
-			};
-		default:
-			return {};
-	}
+  switch (subjectId) {
+    case 2: // 长笛
+      return {
+        name: "flute",
+        direction: "transverse",
+        height: "1.6rem",
+        hasTizhi: true,
+      };
+    case 4: // 单簧管
+      return {
+        name: "clarinet",
+        direction: "vertical",
+        width: "3rem",
+        hasTizhi: true,
+      };
+    case 5: // 萨克斯
+    case 6: // 中音萨克斯
+      return {
+        name: "saxophone",
+        direction: "vertical",
+        width: "4.34rem",
+        hasTizhi: true,
+      };
+    case 12: // 小号
+      return {
+        name: "trumpet",
+        direction: "transverse",
+        height: "1.6rem",
+        hasTizhi: false,
+      };
+    case 13: // 圆号
+      return {
+        name: "horn",
+        direction: "vertical",
+        width: "4.98rem",
+        hasTizhi: false,
+      };
+    case 14: // 长号
+      return {
+        name: "trombone",
+        direction: "transverse",
+        height: "1.6rem",
+        hasTizhi: false,
+      };
+    case 15: // 上低音号
+      return {
+        name: "up-bass-horn",
+        direction: "vertical",
+        width: "4.34rem",
+        hasTizhi: false,
+      };
+    case 17: // 大号
+      return {
+        name: "tuba",
+        direction: "vertical",
+        width: "4.34rem",
+        hasTizhi: false,
+      };
+    case 120: // 短笛
+      return {
+        name: "piccolo",
+        direction: "vertical",
+        width: "3rem",
+        orientation: 1,
+        hasTizhi: true,
+      };
+    case "piccolo": // 德式竖笛
+      return {
+        name: "piccolo",
+        direction: "vertical",
+        width: "3rem",
+        orientation: 1,
+        code: "竖笛",
+        hasTizhi: true,
+      };
+    case "hulusi-flute": // 葫芦丝
+      return {
+        name: "hulusi-flute",
+        direction: "vertical",
+        width: "3rem",
+        orientation: 1,
+        code: "葫芦丝",
+        hasTizhi: false,
+      };
+    case "pan-flute": // 排箫
+      return {
+        name: "pan-flute",
+        direction: "transverse",
+        height: "2rem",
+        disabledFinger: true,
+        orientation: 0,
+        code: "排箫",
+        hasTizhi: false,
+      };
+    case "ocarina": // 陶笛
+      return {
+        name: "ocarina",
+        direction: "vertical",
+        width: "3rem",
+        disabledFinger: true,
+        orientation: 0,
+        code: "陶笛",
+        hasTizhi: false,
+      };
+    case "melodica": // 口风琴
+      return {
+        name: "melodica",
+        direction: "transverse",
+        height: "2rem",
+        orientation: 0,
+        code: "口风琴",
+        hasTizhi: false,
+      };
+	case "baroque-recorder": // 英式竖笛
+	  return {
+		name: "baroque-recorder",
+		direction: "vertical",
+		width: "3rem",
+		orientation: 1,
+		code: "竖笛",
+		hasTizhi: true,
+	  };	  
+    default:
+      return {};
+  }
 };
 
 export const getFingeringConfig = async (type: IVocals | undefined): Promise<ITypeFingering> => {
-	switch (type) {
-		case "flute":
-			const flute = await import(`./fingering-img/flute/index.json`);
-			return {
-				json: flute.default,
-				relationship: relationships.flute,
-				height: "60px",
-				styles: {},
-			};
-		case "clarinet":
-			const clarinet = await import(`./fingering-img/clarinet/index.json`);
-			return {
-				json: clarinet.default,
-				relationship: relationships.clarinet,
-				styles: {
-					marginLeft: ".4rem",
-					marginRight: ".7rem",
-				},
-			};
-		case "trumpet":
-			const trumpet = await import(`./fingering-img/trumpet/index.json`);
-			return {
-				json: trumpet.default,
-				relationship: relationships.trumpet,
-				// maxWidth: 150,
-			};
-		case "horn":
-			const horn = await import(`./fingering-img/horn/index.json`);
-			return {
-				json: horn.default,
-				relationship: relationships.horn,
-				height: "212px",
-				width: "252px",
-			};
-		case "tuba":
-			const tuba = await import(`./fingering-img/tuba/index.json`);
-			return {
-				json: tuba.default,
-				relationship: relationships.tuba,
-			};
-		case "piccolo":
-			const piccolo = await import(`./fingering-img/piccolo/index.json`);
-			return {
-				json: piccolo.default,
-				relationship: relationships.piccolo,
-			};
-		case "piccolo1":
-			const piccolo1 = await import(`./fingering-img/piccolo1/index.json`);
-			return {
-				json: piccolo1.default,
-				relationship: relationships.piccolo,
-			};
-		case "piccolo2":
-			const piccolo2 = await import(`./fingering-img/piccolo2/index.json`);
-			return {
-				json: piccolo2.default,
-				relationship: relationships.piccolo,
-			};
-		case "up-bass-horn":
-			const upBassHorn = await import(`./fingering-img/up-bass-horn/index.json`);
-			return {
-				json: upBassHorn.default,
-				relationship: relationships["up-bass-horn"],
-			};
-		case "trombone":
-			const trombone = await import(`./fingering-img/trombone/index.json`);
-			return {
-				json: trombone.default,
-				relationship: relationships["trombone"],
-			};
-		case "saxophone":
-			const saxophone = await import(`./fingering-img/saxophone/index.json`);
-			return {
-				json: saxophone.default,
-				relationship: relationships["saxophone"],
-				styles: {
-					marginLeft: ".2rem",
-					marginRight: ".3rem",
-				},
-			};
-		case "small-drum":
-			const smallDrum = await import(`./fingering-img/small-drum/index.json`);
-			return {
-				json: smallDrum.default,
-				relationship: relationships["up-bass-horn"],
-				width: "180px",
-			};
-		case "hulusi-flute":
-			const hulusi = await import(`./fingering-img/hulusi-flute/index.json`);
-			return {
-				json: hulusi.default,
-				relationship: relationships.hulusi,
-				// width: '180px',
-				styles: {
-					marginLeft: ".6rem",
-					marginRight: ".7rem",
-				},
-			};
-		case "hulusi-flute1":
-			const hulusi1 = await import(`./fingering-img/hulusi-flute1/index.json`);
-			return {
-				json: hulusi1.default,
-				relationship: relationships.hulusi,
-				// width: '180px',
-				styles: {
-					marginLeft: ".6rem",
-					marginRight: ".7rem",
-				},
-			};
-		case "hulusi-flute2":
-			const hulusi2 = await import(`./fingering-img/hulusi-flute2/index.json`);
-			return {
-				json: hulusi2.default,
-				relationship: relationships.hulusi,
-				// width: '180px',
-				styles: {
-					marginLeft: ".6rem",
-					marginRight: ".7rem",
-				},
-			};
-		case "pan-flute":
-			const pan = await import(`./fingering-img/pan-flute/index.json`);
-			return {
-				json: pan.default,
-				relationship: relationships.pan,
-			};
-		case "pan-flute1":
-			const pan1 = await import(`./fingering-img/pan-flute1/index.json`);
-			return {
-				json: pan1.default,
-				relationship: relationships.pan,
-			};
-		case "pan-flute2":
-			const pan2 = await import(`./fingering-img/pan-flute2/index.json`);
-			return {
-				json: pan2.default,
-				relationship: relationships.pan,
-			};
-		case "ocarina":
-			const ocarina = await import(`./fingering-img/ocarina/index.json`);
-			return {
-				json: ocarina.default,
-				relationship: relationships.ocarina,
-				width: "180px",
-				styles: {
-					marginTop: "auto",
-				},
-			};
-		case "ocarina1":
-			const ocarina1 = await import(`./fingering-img/ocarina1/index.json`);
-			return {
-				json: ocarina1.default,
-				relationship: relationships.ocarina,
-				width: "180px",
-				styles: {
-					marginTop: "auto",
-				},
-			};
-		case "ocarina2":
-			const ocarina2 = await import(`./fingering-img/ocarina2/index.json`);
-			return {
-				json: ocarina2.default,
-				relationship: relationships.ocarina,
-				width: "180px",
-				styles: {
-					marginTop: "auto",
-				},
-			};
-		case "melodica":
-			const melodica = await import(`./fingering-img/melodica/index.json`);
-			return {
-				json: melodica.default,
-				relationship: relationships.melodica,
-				height: "80px",
-				styles: {
-					marginTop: "auto",
-				},
-			};
-		default:
-			return null;
-	}
-};
+  switch (type) {
+    case "flute":
+      const flute = await import(`./fingering-img/flute/index.json`);
+      return {
+        json: flute.default,
+        relationship: relationships.flute,
+        height: "60px",
+        styles: {},
+      };
+    case "clarinet":
+      const clarinet = await import(`./fingering-img/clarinet/index.json`);
+      return {
+        json: clarinet.default,
+        relationship: relationships.clarinet,
+        styles: {
+          marginLeft: ".4rem",
+          marginRight: ".7rem",
+        },
+      };
+    case "trumpet":
+      const trumpet = await import(`./fingering-img/trumpet/index.json`);
+      return {
+        json: trumpet.default,
+        relationship: relationships.trumpet,
+        // maxWidth: 150,
+      };
+    case "horn":
+      const horn = await import(`./fingering-img/horn/index.json`);
+      return {
+        json: horn.default,
+        relationship: relationships.horn,
+        height: "212px",
+        width: "252px",
+      };
+    case "tuba":
+      const tuba = await import(`./fingering-img/tuba/index.json`);
+      return {
+        json: tuba.default,
+        relationship: relationships.tuba,
+      };
+    case "piccolo":
+      const piccolo = await import(`./fingering-img/piccolo/index.json`);
+      return {
+        json: piccolo.default,
+        relationship: relationships.piccolo,
+      };
+    case "piccolo1":
+      const piccolo1 = await import(`./fingering-img/piccolo1/index.json`);
+      return {
+        json: piccolo1.default,
+        relationship: relationships.piccolo,
+      };
+    case "piccolo2":
+      const piccolo2 = await import(`./fingering-img/piccolo2/index.json`);
+      return {
+        json: piccolo2.default,
+        relationship: relationships.piccolo,
+      };
+    case "up-bass-horn":
+      const upBassHorn = await import(`./fingering-img/up-bass-horn/index.json`);
+      return {
+        json: upBassHorn.default,
+        relationship: relationships["up-bass-horn"],
+      };
+    case "trombone":
+      const trombone = await import(`./fingering-img/trombone/index.json`);
+      return {
+        json: trombone.default,
+        relationship: relationships["trombone"],
+      };
+    case "saxophone":
+      const saxophone = await import(`./fingering-img/saxophone/index.json`);
+      return {
+        json: saxophone.default,
+        relationship: relationships["saxophone"],
+        styles: {
+          marginLeft: ".2rem",
+          marginRight: ".3rem",
+        },
+      };
+    case "small-drum":
+      const smallDrum = await import(`./fingering-img/small-drum/index.json`);
+      return {
+        json: smallDrum.default,
+        relationship: relationships["up-bass-horn"],
+        width: "180px",
+      };
+    case "hulusi-flute":
+      const hulusi = await import(`./fingering-img/hulusi-flute/index.json`);
+      return {
+        json: hulusi.default,
+        relationship: relationships.hulusi,
+        // width: '180px',
+        styles: {
+          marginLeft: ".6rem",
+          marginRight: ".7rem",
+        },
+      };
+    case "hulusi-flute1":
+      const hulusi1 = await import(`./fingering-img/hulusi-flute1/index.json`);
+      return {
+        json: hulusi1.default,
+        relationship: relationships.hulusi,
+        // width: '180px',
+        styles: {
+          marginLeft: ".6rem",
+          marginRight: ".7rem",
+        },
+      };
+    case "hulusi-flute2":
+      const hulusi2 = await import(`./fingering-img/hulusi-flute2/index.json`);
+      return {
+        json: hulusi2.default,
+        relationship: relationships.hulusi,
+        // width: '180px',
+        styles: {
+          marginLeft: ".6rem",
+          marginRight: ".7rem",
+        },
+      };
+    case "pan-flute":
+      const pan = await import(`./fingering-img/pan-flute/index.json`);
+      return {
+        json: pan.default,
+        relationship: relationships.pan,
+      };
+    case "pan-flute1":
+      const pan1 = await import(`./fingering-img/pan-flute1/index.json`);
+      return {
+        json: pan1.default,
+        relationship: relationships.pan,
+      };
+    case "pan-flute2":
+      const pan2 = await import(`./fingering-img/pan-flute2/index.json`);
+      return {
+        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 {
+        json: ocarina.default,
+        relationship: relationships.ocarina,
+        width: "180px",
+        styles: {
+          marginTop: "auto",
+        },
+      };
+    case "ocarina1":
+      const ocarina1 = await import(`./fingering-img/ocarina1/index.json`);
+      return {
+        json: ocarina1.default,
+        relationship: relationships.ocarina,
+        width: "180px",
+        styles: {
+          marginTop: "auto",
+        },
+      };
+    case "ocarina2":
+      const ocarina2 = await import(`./fingering-img/ocarina2/index.json`);
+      return {
+        json: ocarina2.default,
+        relationship: relationships.ocarina,
+        width: "180px",
+        styles: {
+          marginTop: "auto",
+        },
+      };
+    case "melodica":
+      const melodica = await import(`./fingering-img/melodica/index.json`);
+      return {
+        json: melodica.default,
+        relationship: relationships.melodica,
+        height: "80px",
+        styles: {
+          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",
+        },
+      };
+	case "baroque-recorder":
+		const baroqueRecorder = await import(`./fingering-img/baroque-recorder/index.json`);
+		return {
+			json: baroqueRecorder.default,
+			relationship: relationships.baroqueRecorder,
+		};  	
+	case "baroque-recorder1":
+		const baroqueRecorder1 = await import(`./fingering-img/baroque-recorder1/index.json`);
+		return {
+			json: baroqueRecorder1.default,
+			relationship: relationships.baroqueRecorder,
+		};	
+	case "baroque-recorder2":
+		const baroqueRecorder2 = await import(`./fingering-img/baroque-recorder2/index.json`);
+		return {
+			json: baroqueRecorder2.default,
+			relationship: relationships.baroqueRecorder,
+		};			  
+    default:
+      return null;
+  }
+};

BIN
src/view/fingering/fingering-img/baroque-recorder/1.png


BIN
src/view/fingering/fingering-img/baroque-recorder/11.png


BIN
src/view/fingering/fingering-img/baroque-recorder/2.png


BIN
src/view/fingering/fingering-img/baroque-recorder/3.png


BIN
src/view/fingering/fingering-img/baroque-recorder/33.png


BIN
src/view/fingering/fingering-img/baroque-recorder/4.png


BIN
src/view/fingering/fingering-img/baroque-recorder/5.png


BIN
src/view/fingering/fingering-img/baroque-recorder/6.png


BIN
src/view/fingering/fingering-img/baroque-recorder/7.png


BIN
src/view/fingering/fingering-img/baroque-recorder/77.png


BIN
src/view/fingering/fingering-img/baroque-recorder/8.png


BIN
src/view/fingering/fingering-img/baroque-recorder/88.png


BIN
src/view/fingering/fingering-img/baroque-recorder/full.png


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 13 - 0
src/view/fingering/fingering-img/baroque-recorder/index.json


BIN
src/view/fingering/fingering-img/baroque-recorder/英式.png


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 13 - 0
src/view/fingering/fingering-img/baroque-recorder1/index.json


BIN
src/view/fingering/fingering-img/baroque-recorder2/full.png


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 0
src/view/fingering/fingering-img/baroque-recorder2/index.json


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
src/view/fingering/fingering-img/hulusi-flute/index.json


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
src/view/fingering/fingering-img/hulusi-flute1/index.json


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
src/view/fingering/fingering-img/melodica/index.json


Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels