index.tsx 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212
  1. import { PropType, computed, defineComponent, nextTick, onBeforeMount, onMounted, onUnmounted, reactive, ref } from "vue";
  2. import styles from "./index.module.less";
  3. import icons from "./image/icons.json";
  4. import { FIGNER_INSTRUMENT_DATA, FIGNER_INSTRUMENT_REALKEY, IFIGNER_INSTRUMENT_Note } from "/src/view/figner-preview";
  5. import { ITypeFingering, IVocals, getFingeringConfig, mappingVoicePart, subjectFingering } from "/src/view/fingering/fingering-config";
  6. import { Howl } from "howler";
  7. import { storeData } from "/src/store";
  8. import { api_back, api_cloudLoading, api_setRequestedOrientation, api_setStatusBarVisibility, isSpecialShapedScreen } from "/src/helpers/communication";
  9. import Hammer from "hammerjs";
  10. import { Button, Icon, Loading, Popover, Popup, Progress, Space, showToast } from "vant";
  11. import GuideIndex from "./guide/guide-index";
  12. import { getQuery } from "/src/utils/queryString";
  13. import { browser } from "/src/utils";
  14. import { usePageVisibility } from "@vant/use";
  15. import { watch } from "vue";
  16. import icon_loading_img from "./image/icon_loading_img.png";
  17. import state, { IPlatform } from "/src/state";
  18. import { getSubjectList } from "../api";
  19. export default defineComponent({
  20. name: "viewFigner",
  21. emits: ["close"],
  22. props: {
  23. show: {
  24. type: Boolean,
  25. default: true,
  26. },
  27. isComponent: {
  28. type: Boolean,
  29. default: false,
  30. },
  31. subject: {
  32. type: String as PropType<IVocals>,
  33. default: "",
  34. },
  35. },
  36. setup(props, { emit }) {
  37. const query = getQuery();
  38. const browsInfo = browser();
  39. const code = mappingVoicePart(query.code, "INSTRUMENT");
  40. const subject = props.isComponent ? props.subject || "pan-flute" : code || "pan-flute";
  41. const data = reactive({
  42. loading: true,
  43. subject: subject as any,
  44. realKey: 0,
  45. notes: [] as IFIGNER_INSTRUMENT_Note[],
  46. tones: [] as IFIGNER_INSTRUMENT_Note[],
  47. activeTone: {} as IFIGNER_INSTRUMENT_Note,
  48. popupActiveTone: {} as IFIGNER_INSTRUMENT_Note,
  49. activeToneName: "",
  50. soundFonts: {} as any,
  51. viewIndex: 0,
  52. viewTotal: 1,
  53. noteAudio: null as unknown as Howl,
  54. transform: {
  55. scale: 1,
  56. x: 0,
  57. y: 0,
  58. startScale: 1,
  59. startX: 0,
  60. startY: 0,
  61. transition: "",
  62. },
  63. tipShow: false,
  64. tips: [] as IFIGNER_INSTRUMENT_Note[],
  65. tnoteShow: false,
  66. loadingSoundFonts: true,
  67. loadingSoundProgress: 0,
  68. huaweiPad: navigator?.userAgent?.includes("UAWEIVRD-W09") ? true : false,
  69. paddingTop: "",
  70. paddingLeft: "",
  71. subjects: [] as any,
  72. fingeringModeList: [
  73. {
  74. text: "指法模式",
  75. value: "fingeringMode",
  76. icon: icons.icon_click,
  77. },
  78. {
  79. text: "听音模式",
  80. value: "listenMode",
  81. icon: icons.icon_listen,
  82. },
  83. {
  84. text: "音阶模式",
  85. value: "scaleMode",
  86. icon: icons.icon_mode,
  87. },
  88. ],
  89. fingeringMode: query.type || ("scaleMode" as "fingeringMode" | "listenMode" | "scaleMode"), // 模式
  90. noteType: "all" as "#c" | "all", // 音调
  91. loadingDom: false, // 切换乐器时需要重置
  92. });
  93. const fingerData = reactive({
  94. relationshipIndex: 0,
  95. subject: null as unknown as ITypeFingering,
  96. fingeringInfo: subjectFingering(data.subject),
  97. });
  98. if (!props.isComponent) {
  99. state.fingeringInfo = fingerData.fingeringInfo;
  100. }
  101. const getAPPData = async (type: "top" | "left") => {
  102. const screenData = await isSpecialShapedScreen();
  103. if (screenData?.content) {
  104. // console.log("🚀 ~ screenData:", screenData.content);
  105. const { isSpecialShapedScreen, notchHeight } = screenData.content;
  106. if (isSpecialShapedScreen) {
  107. if (type === "top") {
  108. data.paddingTop = 25 + "px";
  109. }
  110. if (type === "left") {
  111. data.paddingLeft = 25 + "px";
  112. }
  113. }
  114. }
  115. };
  116. const getHeadTop = () => {
  117. if (!browsInfo.ios && fingerData.fingeringInfo.orientation === 1) {
  118. getAPPData("top");
  119. }
  120. if (!browsInfo.ios && fingerData.fingeringInfo.orientation === 0) {
  121. getAPPData("left");
  122. }
  123. };
  124. const getNotes = () => {
  125. const fignerData = FIGNER_INSTRUMENT_DATA[data.subject as keyof typeof FIGNER_INSTRUMENT_DATA];
  126. if (fignerData) {
  127. data.tones = fignerData.tones || [];
  128. if (data.tones.length) {
  129. data.activeTone = data.tones[0];
  130. data.popupActiveTone = data.tones[0];
  131. }
  132. data.tips = fignerData.tips || [];
  133. setNotes();
  134. setTimeout(() => {
  135. data.loading = false;
  136. }, 600);
  137. }
  138. };
  139. const setNotes = () => {
  140. const fignerData = FIGNER_INSTRUMENT_DATA[data.subject as keyof typeof FIGNER_INSTRUMENT_DATA];
  141. if (fignerData) {
  142. const tempNotes = fignerData[`list${data.activeTone.realName || ""}`];
  143. const appendNote: any = [];
  144. tempNotes.forEach((note: any) => {
  145. note.steps = new Array(Math.abs(note.step)).fill(1);
  146. if (FIGNER_INSTRUMENT_REALKEY.includes(note.realKey)) {
  147. appendNote.push(note);
  148. }
  149. });
  150. // 判断是音符状态
  151. data.notes = data.noteType === "#c" ? appendNote : tempNotes;
  152. // data.notes = fignerData[`list${data.activeTone.realName || ""}`];
  153. }
  154. };
  155. const getFingeringData = async () => {
  156. const subject: any = data.subject + (data.viewIndex === 0 ? "" : data.viewIndex);
  157. console.log("🚀 ~ subject:", subject);
  158. fingerData.subject = await getFingeringConfig(subject);
  159. };
  160. const createAudio = (url: string) => {
  161. return new Promise((resolve, reject) => {
  162. const noteAudio = new Howl({
  163. src: url,
  164. loop: true,
  165. onload: () => {
  166. resolve(noteAudio);
  167. },
  168. onloaderror: () => {
  169. reject(new Error(`加载音频失败`));
  170. },
  171. });
  172. });
  173. };
  174. const getSounFonts = async () => {
  175. const pathname = /(192|localhost)/.test(location.origin) ? "/" : location.pathname;
  176. data.loadingSoundFonts = true;
  177. try {
  178. data.loadingSoundProgress = 0;
  179. for (let i = 0; i < data.notes.length; i++) {
  180. const note = data.notes[i];
  181. // console.log("🚀 ~ note:", i);
  182. let url = `${pathname}soundfonts/${data.subject}/`;
  183. url += note.realName;
  184. url += ".mp3";
  185. data.soundFonts[note.realKey] = await createAudio(url);
  186. data.loadingSoundProgress = Math.floor(((i + 1) / data.notes.length) * 100);
  187. }
  188. data.loadingSoundProgress = 100;
  189. } catch (e: any) {
  190. //
  191. showToast(e.message);
  192. }
  193. api_cloudLoading();
  194. data.loadingSoundFonts = false;
  195. };
  196. const selectSubjectType = (subject: string) => {
  197. data.subjects.forEach((item: any) => {
  198. if (item.value === subject) {
  199. item.className = styles.selected;
  200. } else {
  201. item.className = "";
  202. }
  203. });
  204. };
  205. // 切换当前模式
  206. const onChangeFingeringModel = () => {
  207. //
  208. if (playAction.listenLock) return;
  209. if (playAction.showAnswerLoading) return;
  210. if (data.fingeringMode === "scaleMode") {
  211. if (["pan-flute", "ocarina"].includes(data.subject)) {
  212. data.viewIndex = 1;
  213. } else {
  214. data.viewIndex = 0;
  215. }
  216. const o: any = {
  217. "pan-flute": 2,
  218. ocarina: 2,
  219. piccolo: 2,
  220. "hulusi-flute": 2,
  221. "baroque-recorder": 2,
  222. };
  223. data.viewTotal = o[data.subject] || 1;
  224. data.fingeringMode = "listenMode";
  225. } else if (data.fingeringMode === "listenMode") {
  226. data.fingeringMode = "fingeringMode";
  227. } else if (data.fingeringMode === "fingeringMode") {
  228. data.fingeringMode = "scaleMode";
  229. data.viewIndex = 0;
  230. data.noteType = "all";
  231. }
  232. resetMode(true, 0);
  233. __init(false);
  234. };
  235. const __init = async (loadSong = true) => {
  236. data.loadingDom = true;
  237. getNotes();
  238. selectSubjectType(data.subject);
  239. if (data.fingeringMode === "fingeringMode") {
  240. if (data.subject === "pan-flute") {
  241. data.viewIndex = 3;
  242. } else if (["pan-flute", "ocarina", "melodica"].includes(data.subject)) {
  243. data.viewIndex = 1;
  244. }
  245. } else {
  246. if (["pan-flute", "ocarina"].includes(data.subject)) {
  247. data.viewIndex = 1;
  248. }
  249. }
  250. const o: any = {
  251. "pan-flute": 2,
  252. ocarina: 2,
  253. piccolo: 2,
  254. "hulusi-flute": 2,
  255. "baroque-recorder": 2,
  256. };
  257. data.viewTotal = o[data.subject] || 1;
  258. getFingeringData();
  259. getHeadTop();
  260. if (loadSong) {
  261. await getSounFonts();
  262. }
  263. data.loadingDom = false;
  264. };
  265. // 获取声部
  266. const getSubjects = async () => {
  267. try {
  268. const subjects = await getSubjectList({
  269. enableFlag: true,
  270. delFlag: 0,
  271. page: 1,
  272. rows: 999,
  273. });
  274. const rows = subjects.data.rows || [];
  275. rows.forEach((row: any) => {
  276. data.subjects.push({
  277. text: row.name,
  278. value: mappingVoicePart(row.code, "INSTRUMENT"),
  279. className: "",
  280. });
  281. });
  282. console.log(data.subjects, "subjects");
  283. } catch {
  284. //
  285. }
  286. };
  287. onBeforeMount(async () => {
  288. if (state.platform === IPlatform.PC) {
  289. document.title = "听音练习";
  290. }
  291. state.platform = query.platform?.toLocaleUpperCase() || "";
  292. await getSubjects();
  293. __init();
  294. });
  295. /**
  296. * 播放音频
  297. * @param item 音频节点
  298. * @param showNote 是否显示对应的指法
  299. * @returns
  300. */
  301. const noteClick = (item: IFIGNER_INSTRUMENT_Note, showNote = true) => {
  302. // console.log('音高', item.realKey)
  303. if (data.noteAudio) {
  304. data.noteAudio.stop();
  305. if (data.realKey === item.realKey) {
  306. data.realKey = 0;
  307. data.noteAudio = null as unknown as Howl;
  308. return;
  309. }
  310. }
  311. if (showNote) {
  312. data.realKey = item.realKey;
  313. }
  314. data.noteAudio = data.soundFonts[item.realKey];
  315. data.noteAudio.play();
  316. };
  317. const handleStop = () => {
  318. if (data.noteAudio) {
  319. data.noteAudio.stop();
  320. data.realKey = 0;
  321. data.noteAudio = null as unknown as Howl;
  322. }
  323. };
  324. /** 返回 */
  325. const handleBack = () => {
  326. // platform: query.platform,
  327. handleStop();
  328. if (props.isComponent) {
  329. console.log("关闭");
  330. emit("close");
  331. return;
  332. } else if (state.platform === IPlatform.PC) {
  333. // 老师端,首页
  334. window.parent.postMessage(
  335. {
  336. api: "iframe_exit",
  337. },
  338. "*"
  339. );
  340. return;
  341. // if (fingerData.fingeringInfo.orientation === 0) {
  342. // api_setRequestedOrientation(1);
  343. // }
  344. }
  345. // 不在APP中,
  346. if (!storeData.isApp) {
  347. window.close();
  348. return;
  349. }
  350. api_back();
  351. };
  352. onMounted(() => {
  353. loadElement();
  354. api_setStatusBarVisibility();
  355. });
  356. const loadElement = () => {
  357. const fingeringContainer = document.getElementById("fingeringContainer");
  358. // console.log("🚀 ~ fingeringContainer:", fingeringContainer);
  359. const mc = new Hammer.Manager(fingeringContainer as HTMLElement);
  360. mc.add(new Hammer.Pan({ threshold: 0, pointers: 0 }));
  361. mc.add(new Hammer.Pinch({ threshold: 0 })).recognizeWith([mc.get("pan")]);
  362. // mc.get("pan").set({ direction: Hammer.DIRECTION_ALL });
  363. // mc.get("pinch").set({ enable: true });
  364. mc.on("panstart pinchstart", function (ev) {
  365. data.transform.transition = "";
  366. });
  367. mc.on("panmove pinchmove", function (ev) {
  368. if (ev.type === "pinchmove") {
  369. // console.log("🚀 ~ ev:", ev.type, ev.scale, ev.deltaX, ev.deltaY);
  370. data.transform.scale = ev.scale * data.transform.startScale;
  371. data.transform.x = data.transform.startX + ev.deltaX;
  372. data.transform.y = data.transform.startY + ev.deltaY;
  373. }
  374. if (ev.type === "panmove") {
  375. // console.log("🚀 ~ ev:", ev.type, ev.deltaX, ev.deltaY);
  376. data.transform.x = data.transform.startX + ev.deltaX;
  377. data.transform.y = data.transform.startY + ev.deltaY;
  378. }
  379. });
  380. //
  381. mc.on("hammer.input", function (ev) {
  382. // console.log("🚀 ~ ev:", ev.type, ev.isFinal);
  383. if (ev.isFinal) {
  384. data.transform.startScale = data.transform.scale;
  385. data.transform.startX = data.transform.x;
  386. data.transform.startY = data.transform.y;
  387. }
  388. });
  389. };
  390. const resetElement = () => {
  391. data.transform.transition = "all 0.3s";
  392. nextTick(() => {
  393. data.transform.scale = 1;
  394. data.transform.x = 0;
  395. data.transform.y = 0;
  396. data.transform.startScale = 1;
  397. data.transform.startX = 0;
  398. data.transform.startY = 0;
  399. });
  400. };
  401. const pageVisible = usePageVisibility();
  402. watch(
  403. () => pageVisible.value,
  404. (val) => {
  405. if (val === "hidden") {
  406. console.log("页面隐藏停止播放");
  407. handleStop();
  408. }
  409. }
  410. );
  411. /** 课件播放 */
  412. const changePlay = (res: any) => {
  413. if (res?.data?.api === "setPlayState") {
  414. handleStop();
  415. }
  416. };
  417. const noteBoxRef = ref();
  418. const scrollNoteBox = (type: "left" | "right") => {
  419. const width = noteBoxRef.value.offsetWidth / 2;
  420. (noteBoxRef.value as unknown as HTMLElement).scrollBy({
  421. left: type === "left" ? -width : width,
  422. behavior: "smooth",
  423. });
  424. };
  425. const playStatus = reactive({
  426. gamut: false, // 是否播放音阶
  427. gamutTimer: null as any, // 播放音阶定时器
  428. answer: false, // 是否显示答案
  429. action: false, // 是否开始播放
  430. });
  431. /** 音符切换 */
  432. const noteChangeShow = () => {
  433. // 播放音阶时不能切换
  434. if (playStatus.gamut) return;
  435. // 开始答题不能切换
  436. if (playStatus.action) return;
  437. gaumntPause();
  438. if (data.noteType === "all") {
  439. data.noteType = "#c";
  440. } else {
  441. data.noteType = "all";
  442. }
  443. getNotes();
  444. };
  445. // 开始播放音阶
  446. const onGamutPlayOrPause = async () => {
  447. if (playStatus.gamut) {
  448. playStatus.gamut = false;
  449. gaumntPause();
  450. } else {
  451. // 不管当前显示在哪个音老师滚动到开始位置
  452. (noteBoxRef.value as unknown as HTMLElement).scroll({
  453. left: 0,
  454. top: 0,
  455. behavior: "smooth",
  456. });
  457. playStatus.gamut = true;
  458. const notes = data.notes;
  459. let scrollCount = 0;
  460. for (let i = 0; i < notes.length; i++) {
  461. if (!playStatus.gamut) return false;
  462. const activeDom = document.querySelectorAll(".note-class")[i] as any;
  463. if (activeDom.offsetLeft >= noteBoxRef.value.offsetWidth + (noteBoxRef.value.offsetWidth / 2) * scrollCount - activeDom.offsetWidth) {
  464. scrollNoteBox("right");
  465. scrollCount++;
  466. }
  467. await gaumtPlay(notes[i]);
  468. }
  469. // // 处理播放到最后一个
  470. setTimeout(() => {
  471. playStatus.gamut = false;
  472. gaumntPause();
  473. }, 667);
  474. }
  475. };
  476. const gaumtPlay = (note: any, status?: boolean) => {
  477. return new Promise((resolve) => {
  478. playStatus.gamutTimer = setTimeout(() => {
  479. if (playStatus.gamut || status) {
  480. noteClick(note);
  481. }
  482. resolve(note);
  483. }, 667);
  484. });
  485. };
  486. const gaumntPause = () => {
  487. clearTimeout(playStatus.gamutTimer);
  488. if (data.noteAudio) {
  489. data.noteAudio.stop();
  490. data.realKey = 0;
  491. data.noteAudio = null as unknown as Howl;
  492. }
  493. };
  494. /** 开始播放 */
  495. const playAction = reactive({
  496. exampleAnser: {} as any, // 示例声音
  497. standardAnswer: {} as any, // 标准答案key
  498. showAnswerLoading: false, // 显示按答案中
  499. listenModeStatus: false, // 是否开始了模式
  500. listenLock: false,
  501. listenTipsStatus: false, // 开始播放状态
  502. /** 0: 未答,1: 答对,2: 答错 */
  503. userAnswerStatus: 0 as 0 | 1 | 2, // 用户回答状态
  504. userAnswer: {} as any, // 用户答的数据
  505. });
  506. const onActionPlay = async () => {
  507. if (playAction.listenLock) return;
  508. if (playAction.showAnswerLoading) return;
  509. playStatus.action = true;
  510. playStatus.answer = true;
  511. // 先暂停播放声音
  512. gaumntPause();
  513. if (data.fingeringMode === "fingeringMode") {
  514. onFingeringMode();
  515. } else if (data.fingeringMode === "listenMode") {
  516. if (playAction.listenModeStatus) {
  517. playAction.listenLock = true;
  518. await fingeringPlay(playAction.standardAnswer, 1500, false);
  519. gaumntPause();
  520. playAction.listenLock = false;
  521. } else {
  522. onListenMode();
  523. }
  524. }
  525. };
  526. // 指法模式
  527. const fingeringPlay = (note: any, timer = 1500, showNote = true) => {
  528. return new Promise((resolve) => {
  529. noteClick(note, showNote);
  530. setTimeout(() => {
  531. resolve(note);
  532. }, timer);
  533. });
  534. };
  535. const onFingeringMode = () => {
  536. const randomIndex = Math.floor(Math.random() * data.notes.length);
  537. playAction.standardAnswer = data.notes[randomIndex];
  538. data.realKey = data.notes[randomIndex].realKey;
  539. if (playAction.listenModeStatus) {
  540. return;
  541. }
  542. playAction.listenModeStatus = true; // 是否开始听音
  543. playAction.listenLock = true; // 锁
  544. playAction.listenTipsStatus = true;
  545. setTimeout(() => {
  546. playAction.listenTipsStatus = false;
  547. playAction.listenLock = false; // 锁
  548. }, 2000);
  549. };
  550. // 听音模式
  551. const onListenMode = async () => {
  552. playAction.listenModeStatus = true; // 是否开始听音
  553. playAction.listenLock = true; // 锁
  554. playAction.listenTipsStatus = true;
  555. // 设置并保存示例数据
  556. let randomIndex = Math.floor(Math.random() * data.notes.length);
  557. playAction.exampleAnser = data.notes[randomIndex];
  558. data.realKey = playAction.exampleAnser.realKey;
  559. scrollAnswer(playAction.exampleAnser.realKey);
  560. await fingeringPlay(playAction.exampleAnser);
  561. data.realKey = 0;
  562. playAction.exampleAnser = {};
  563. gaumntPause();
  564. setTimeout(async () => {
  565. // 设置答题数据
  566. randomIndex = Math.floor(Math.random() * data.notes.length);
  567. playAction.standardAnswer = data.notes[randomIndex];
  568. await fingeringPlay(data.notes[randomIndex], 1500, false);
  569. gaumntPause();
  570. playAction.listenLock = false;
  571. playAction.listenTipsStatus = false;
  572. }, 1000);
  573. };
  574. // 显示答案
  575. const onShowAnswer = async () => {
  576. if (playAction.listenLock) return;
  577. playAction.showAnswerLoading = true;
  578. scrollAnswer(playAction.standardAnswer.realKey);
  579. await fingeringPlay(playAction.standardAnswer);
  580. resetMode(true, 0);
  581. // }
  582. };
  583. // 滚动到对应答案位置
  584. const scrollAnswer = (realKey?: any) => {
  585. const tempRealKey = realKey || data.realKey;
  586. const index = data.notes.findIndex((item: any) => item.realKey === tempRealKey);
  587. const activeDom = document.querySelectorAll(".note-class")[index] as any;
  588. if (activeDom) {
  589. const aWidth = activeDom.offsetWidth;
  590. const width = noteBoxRef.value.offsetWidth;
  591. const aLeft = Math.max(activeDom?.offsetLeft - aWidth, 0);
  592. (noteBoxRef.value as unknown as HTMLElement).scroll({
  593. left: Math.max(aLeft - width / 2, 0),
  594. top: 0,
  595. behavior: "smooth",
  596. });
  597. }
  598. };
  599. /**
  600. * 重置播放状态
  601. * @param status 是否全部重置
  602. * @param timer 延时时长(默认2s)
  603. */
  604. const resetMode = (status = true, timer = 2000) => {
  605. // 2秒钟后重置
  606. setTimeout(() => {
  607. gaumntPause();
  608. if (status) {
  609. playAction.standardAnswer = {};
  610. playAction.showAnswerLoading = false;
  611. playAction.userAnswerStatus = 0;
  612. playAction.userAnswer = {};
  613. playAction.listenModeStatus = false;
  614. playStatus.action = false;
  615. playStatus.answer = false;
  616. playStatus.gamut = false;
  617. data.realKey = 0;
  618. } else {
  619. playAction.userAnswerStatus = 0;
  620. playAction.userAnswer = {};
  621. }
  622. }, timer);
  623. };
  624. /** 滚轮缩放 */
  625. const handleWheel = (e: WheelEvent) => {
  626. e.preventDefault();
  627. if (e.deltaY > 0) {
  628. data.transform.scale -= 0.1;
  629. if (data.transform.scale <= 0.5) {
  630. data.transform.scale = 0.5;
  631. }
  632. } else {
  633. data.transform.scale += 0.1;
  634. if (data.transform.scale >= 2) {
  635. data.transform.scale = 2;
  636. }
  637. }
  638. };
  639. onMounted(() => {
  640. window.addEventListener("message", changePlay);
  641. const fingeringContainer = document.getElementById("fingeringContainer");
  642. fingeringContainer?.addEventListener("wheel", handleWheel);
  643. });
  644. onUnmounted(() => {
  645. window.removeEventListener("message", changePlay);
  646. const fingeringContainer = document.getElementById("fingeringContainer");
  647. fingeringContainer?.removeEventListener("wheel", handleWheel);
  648. document.title = "Ai学练";
  649. });
  650. const containerBox = computed(() => {
  651. if (state.platform === IPlatform.PC || query.modelType) {
  652. return {
  653. paddingTop: "1rem",
  654. paddingBottom: "",
  655. };
  656. }
  657. if (data.fingeringMode === "scaleMode") {
  658. if (data.subject === "hulusi-flute") {
  659. return {
  660. paddingTop: "3.1rem",
  661. paddingBottom: ".8rem",
  662. };
  663. } else if (data.subject === "piccolo") {
  664. return {
  665. paddingTop: "4rem",
  666. paddingBottom: ".8rem",
  667. };
  668. } else if (data.subject === "pan-flute") {
  669. return {
  670. paddingTop: "0",
  671. paddingBottom: "0",
  672. };
  673. } else if (data.subject === "ocarina") {
  674. return {
  675. paddingTop: "1.2rem",
  676. paddingBottom: "0",
  677. };
  678. } else if (data.subject === "melodica") {
  679. return {
  680. paddingTop: "2.8rem",
  681. paddingBottom: "1.8rem",
  682. };
  683. } else {
  684. return {
  685. paddingTop: "",
  686. paddingBottom: "",
  687. };
  688. }
  689. } else {
  690. if (data.subject === "hulusi-flute") {
  691. return {
  692. paddingTop: "3.1rem",
  693. paddingBottom: "0rem",
  694. };
  695. } else if (data.subject === "piccolo") {
  696. return {
  697. paddingTop: "3rem",
  698. paddingBottom: ".5rem",
  699. };
  700. } else if (data.subject === "pan-flute") {
  701. return {
  702. paddingTop: "0",
  703. paddingBottom: "0",
  704. };
  705. } else if (data.subject === "ocarina") {
  706. return {
  707. paddingTop: "1rem",
  708. paddingBottom: "0",
  709. };
  710. } else if (data.subject === "melodica") {
  711. return {
  712. paddingTop: "2.8rem",
  713. paddingBottom: "0.8rem",
  714. };
  715. } else {
  716. return {
  717. paddingTop: "",
  718. paddingBottom: "",
  719. };
  720. }
  721. }
  722. });
  723. const listenText = computed(() => {
  724. if (data.fingeringMode === "fingeringMode") {
  725. if (playStatus.action) {
  726. return "换一换";
  727. } else {
  728. return "开始练习";
  729. }
  730. } else if (data.fingeringMode === "listenMode") {
  731. if (playStatus.action) {
  732. return "再听一遍";
  733. } else {
  734. return "开始听音";
  735. }
  736. }
  737. return "开始听音";
  738. });
  739. const modeText = computed(() => {
  740. let text = "";
  741. let icon = icons.icon_mode;
  742. data.fingeringModeList.forEach((item: any) => {
  743. if (item.value === data.fingeringMode) {
  744. text = item.text;
  745. icon = item.icon;
  746. }
  747. });
  748. return {
  749. text,
  750. icon,
  751. };
  752. });
  753. // 屏幕方向 0 竖,1 横
  754. const orientationDirection = computed(() => {
  755. return ["hulusi-flute", "piccolo"].includes(data.subject) ? 1 : 0;
  756. });
  757. const resultImg = (note: any) => {
  758. if (data.realKey === note.realKey && !playStatus.action) {
  759. return {
  760. icon: icons.icon_btn_ylow,
  761. status: false,
  762. };
  763. } else if (playAction.exampleAnser.realKey === note.realKey) {
  764. return {
  765. icon: icons.icon_btn_ylow,
  766. status: false,
  767. };
  768. } else if (playAction.standardAnswer.realKey === note.realKey) {
  769. // 没有开始答题
  770. if (!playStatus.action) {
  771. return {
  772. icon: icons.icon_btn_ylow,
  773. status: false,
  774. };
  775. }
  776. // 显示答案中
  777. if (playAction.showAnswerLoading) {
  778. return {
  779. icon: icons.icon_btn_green,
  780. status: true,
  781. };
  782. }
  783. // 用户答对
  784. if (playAction.userAnswerStatus === 1) {
  785. return {
  786. icon: icons.icon_btn_green,
  787. status: true,
  788. };
  789. }
  790. } else {
  791. // 用户答错
  792. if (playAction.userAnswerStatus === 2 && playAction.userAnswer.realKey === note.realKey) {
  793. return {
  794. icon: icons.icon_btn_red,
  795. status: true,
  796. };
  797. }
  798. }
  799. return {
  800. icon: icons.icon_btn_blue,
  801. status: true,
  802. };
  803. };
  804. return () => {
  805. const relationship = fingerData.subject?.relationship?.[data.realKey] || [];
  806. const rs: number[] = Array.isArray(relationship[1]) ? relationship[fingerData.relationshipIndex] : relationship;
  807. const canTizhi = Array.isArray(relationship[1]);
  808. return (
  809. <div class={[styles.fingerBox, state.platform !== IPlatform.PC && !query.modelType && fingerData.fingeringInfo.orientation === 1 ? styles.fingerBottom : styles.fingerRight]}>
  810. <div
  811. class={styles.head}
  812. style={{
  813. paddingTop: data.paddingTop ? data.paddingTop : "",
  814. paddingLeft: data.paddingLeft ? data.paddingLeft : "",
  815. }}
  816. >
  817. <div class={styles.left}>
  818. <button class={[styles.backBtn]} onClick={() => handleBack()}>
  819. <img src={icons.icon_back} />
  820. </button>
  821. <div class={styles.baseBtn} onClick={onChangeFingeringModel}>
  822. <img src={modeText.value.icon} />
  823. <span>{modeText.value.text}</span>
  824. </div>
  825. <Popover
  826. placement="bottom"
  827. class={styles.popoverContainer}
  828. actions={data.subjects}
  829. onSelect={(val: any) => {
  830. if (data.subject === val.value) return;
  831. data.subject = val.value;
  832. data.viewIndex = 0;
  833. data.loadingDom = true;
  834. fingerData.fingeringInfo = subjectFingering(data.subject);
  835. console.log(fingerData.fingeringInfo);
  836. resetElement();
  837. resetMode(true, 0);
  838. api_setRequestedOrientation(orientationDirection.value);
  839. // 设置屏幕方向
  840. setTimeout(() => {
  841. __init();
  842. }, 100);
  843. }}
  844. >
  845. {{
  846. reference: () => (
  847. <div
  848. class={styles.baseBtn}
  849. onClick={() => {
  850. //
  851. }}
  852. >
  853. <img src={icons.icon_change_instrument} />
  854. <span>切换乐器</span>
  855. </div>
  856. ),
  857. }}
  858. </Popover>
  859. {data.subject !== "melodica" && data.fingeringMode === "scaleMode" && (
  860. <div
  861. class={styles.baseBtn}
  862. onClick={() => {
  863. data.viewIndex++;
  864. if (data.viewIndex > data.viewTotal) {
  865. if (["pan-flute", "ocarina"].includes(data.subject)) {
  866. data.viewIndex = 1;
  867. } else {
  868. data.viewIndex = 0;
  869. }
  870. }
  871. getFingeringData();
  872. }}
  873. >
  874. <img src={icons.icon_toggle} />
  875. <span>切换视图</span>
  876. </div>
  877. )}
  878. </div>
  879. <div class={styles.rightBtn}>
  880. <div class={styles.baseBtn} onClick={() => resetElement()}>
  881. <img src={icons.icon_2_0} />
  882. <span>还原</span>
  883. </div>
  884. <div
  885. class={styles.baseBtn}
  886. onClick={() => {
  887. resetElement();
  888. data.tipShow = !data.tipShow;
  889. }}
  890. >
  891. <img src={icons.icon_2_1} />
  892. <span>使用说明</span>
  893. </div>
  894. </div>
  895. </div>
  896. <div class={styles.fingerContent}>
  897. <div class={styles.wrapFinger}>
  898. <div
  899. id="fingeringContainer"
  900. class={styles.boxFinger}
  901. style={{
  902. paddingTop: containerBox.value.paddingTop,
  903. paddingBottom: containerBox.value.paddingBottom,
  904. }}
  905. >
  906. <div
  907. style={{
  908. transform: `translate3d(${data.transform.x}px,${data.transform.y}px,0px) scale(${data.transform.scale})`,
  909. transition: data.transform.transition,
  910. }}
  911. class={[styles.fingeringContainer]}
  912. >
  913. <div class={styles.imgs}>
  914. <img src={data.fingeringMode === "scaleMode" ? fingerData.subject?.json?.full : fingerData.subject?.json?.full1} />
  915. {rs.map((key: number | string, index: number) => {
  916. const nk: string = typeof key === "string" ? key.replace("active-", "") : String(key);
  917. return <img data-index={nk} src={fingerData.subject?.json?.[nk]} />;
  918. })}
  919. <div style={{ left: data.viewIndex == 2 ? "0" : "64%" }} class={[styles.tizhi, canTizhi && styles.canDisplay]} onClick={() => (fingerData.relationshipIndex = fingerData.relationshipIndex === 0 ? 1 : 0)}>
  920. 替指
  921. </div>
  922. <div id="finger-note-2" style={{ left: "50%", transform: "translateX(-50%)" }} class={styles.tizhi} onClick={() => (fingerData.relationshipIndex = fingerData.relationshipIndex === 0 ? 1 : 0)}></div>
  923. </div>
  924. </div>
  925. </div>
  926. <div
  927. class={styles.notes}
  928. style={{
  929. paddingLeft: data.paddingLeft ? data.paddingLeft : "",
  930. }}
  931. >
  932. {playAction.listenTipsStatus && <div class={[styles.tipsT, data.fingeringMode === "fingeringMode" ? styles.playTips2 : styles.playTips]}></div>}
  933. {playAction.userAnswerStatus === 1 && <div class={[styles.tipsT, styles.playSuccess]}></div>}
  934. {playAction.userAnswerStatus === 2 && <div class={[styles.tipsT, styles.playError]}></div>}
  935. {data.noteType !== "#c" && orientationDirection.value === 0 && (
  936. <Button class={styles.noteBtn} onClick={() => scrollNoteBox("left")}>
  937. <Icon name="arrow-left" />
  938. </Button>
  939. )}
  940. <div class={[styles.noteContent, data.fingeringMode !== "scaleMode" && orientationDirection.value === 0 && styles.noteContentOther, browsInfo.ios ? "" : styles.noteContentWrap, data.huaweiPad && styles.huaweiPad]}>
  941. {/* 判断是否为音阶模式 */}
  942. {data.fingeringMode !== "scaleMode" && (
  943. <div draggable={false} class={styles.note} onClick={noteChangeShow}>
  944. <img draggable={false} src={data.noteType === "all" ? icons.icon_btn_orange : icons.icon_btn_orange2} />
  945. </div>
  946. )}
  947. {/* [styles.noteContent, browsInfo.ios ? "" : styles.noteContentWrap, data.huaweiPad && styles.huaweiPad] */}
  948. <div class={styles.lastNoteContent}>
  949. <div ref={noteBoxRef} class={styles.noteBox}>
  950. {data.notes.map((note: IFIGNER_INSTRUMENT_Note, index: number) => {
  951. const steps = new Array(Math.abs(note.step)).fill(1);
  952. return (
  953. <div
  954. id={index == 0 ? "finger-note-0" : ""}
  955. draggable={false}
  956. class={[styles.note, "note-class"]}
  957. key={note.realKey}
  958. onClick={async () => {
  959. // 判断是否在播放音阶
  960. if (playStatus.gamut) return;
  961. if (playAction.listenLock) return;
  962. if (playAction.showAnswerLoading) return;
  963. if (playStatus.action) {
  964. playAction.userAnswer = note;
  965. // 判断用户答题
  966. const userResult = note.realKey === playAction.standardAnswer.realKey ? 1 : 2;
  967. playAction.userAnswerStatus = userResult;
  968. playAction.listenLock = true;
  969. data.realKey = note.realKey;
  970. await fingeringPlay(note, 1000);
  971. resetMode(userResult === 1 ? true : false, 0);
  972. data.realKey = 0;
  973. // 如果是指法模式显示完之后要还原
  974. if (data.fingeringMode === "fingeringMode" && userResult === 2) {
  975. // 延迟显示,因为重置的时候有一个异步操作
  976. setTimeout(() => {
  977. data.realKey = playAction.standardAnswer.realKey;
  978. }, 10);
  979. }
  980. playAction.listenLock = false;
  981. } else {
  982. noteClick(note);
  983. }
  984. }}
  985. >
  986. <img draggable={false} src={resultImg(note).icon} />
  987. {playStatus.action && ((playAction.showAnswerLoading && playAction.standardAnswer.realKey === note.realKey) || (playAction.userAnswerStatus === 1 && playAction.userAnswer.realKey === note.realKey)) ? <span class={styles.showAnswer}></span> : ""}
  988. {playStatus.action && playAction.userAnswerStatus === 2 && playAction.userAnswer.realKey === note.realKey ? <span class={[styles.showAnswer, styles.errorAnswer]}></span> : ""}
  989. <div
  990. class={[
  991. styles.noteKey,
  992. ((data.realKey === note.realKey && !playStatus.action) ||
  993. (playStatus.action && playAction.exampleAnser.realKey === note.realKey) ||
  994. (playStatus.action && ((playAction.showAnswerLoading && playAction.standardAnswer.realKey === note.realKey) || (playAction.userAnswerStatus === 1 && playAction.userAnswer.realKey === note.realKey))) ||
  995. (playStatus.action && playAction.userAnswerStatus === 2 && playAction.userAnswer.realKey === note.realKey)) &&
  996. styles.keyActive,
  997. ]}
  998. >
  999. {/* 显示对应的点 */}
  1000. {note.step > 0 ? steps.map((n: any) => <span class={styles.dot}></span>) : null}
  1001. <div class={styles.noteName}>
  1002. <sup>{note.mark && (note.mark === "rise" ? "#" : "b")}</sup>
  1003. {note.key}
  1004. </div>
  1005. {/* 显示对应的点 */}
  1006. {note.step < 0 ? steps.map((n: any) => <span class={styles.dot}></span>) : null}
  1007. </div>
  1008. </div>
  1009. );
  1010. })}
  1011. </div>
  1012. </div>
  1013. </div>
  1014. {data.noteType !== "#c" && orientationDirection.value === 0 && (
  1015. <Button class={styles.noteBtn} onClick={() => scrollNoteBox("right")}>
  1016. <Icon name="arrow" />
  1017. </Button>
  1018. )}
  1019. </div>
  1020. {data.fingeringMode !== "scaleMode" && (
  1021. <div class={styles.optionBtns}>
  1022. <Button class={[styles.oBtn, styles.gamut, playStatus.action && styles.disabled]} round onClick={onGamutPlayOrPause}>
  1023. {playStatus.gamut ? "暂停" : "播放音阶"}
  1024. </Button>
  1025. <Button class={[styles.oBtn, styles.play, playStatus.gamut && styles.disabled]} round onClick={onActionPlay}>
  1026. {listenText.value}
  1027. </Button>
  1028. <Button class={[styles.oBtn, styles.success, !playStatus.answer && styles.disabled]} round onClick={onShowAnswer}>
  1029. 显示答案
  1030. </Button>
  1031. </div>
  1032. )}
  1033. </div>
  1034. <div class={[styles.tips, data.loadingDom ? styles.hiddens : "", data.tipShow ? "" : styles.tipHidden]}>
  1035. <div class={styles.tipTitle}>
  1036. <div class={styles.tipTitleName}>{fingerData.fingeringInfo.code}使用说明</div>
  1037. <Button class={styles.tipClose} onClick={() => (data.tipShow = false)}>
  1038. <Icon name="cross" size={19} color="#fff" />
  1039. </Button>
  1040. </div>
  1041. <div class={styles.iconBook}></div>
  1042. <div class={styles.tipContentbox}>
  1043. <div class={styles.tipContent}>
  1044. {data.tips.map((tip, tipIndex) => (
  1045. <div class={styles.tipItem}>
  1046. <div class={styles.iconWrap}>
  1047. <div class={styles.tipItemIcon}>{tipIndex + 1}</div>
  1048. </div>
  1049. <div>
  1050. {tip.name}: {tip.realName}
  1051. </div>
  1052. </div>
  1053. ))}
  1054. </div>
  1055. </div>
  1056. </div>
  1057. {data.loadingSoundFonts && (
  1058. <div class={styles.loading}>
  1059. <div class={styles.loadingWrap}>
  1060. <img class={styles.loadingIcon} src={icon_loading_img} />
  1061. <Progress percentage={data.loadingSoundProgress} />
  1062. <div class={styles.loadingTip}>加载中,请稍后…</div>
  1063. </div>
  1064. </div>
  1065. )}
  1066. </div>
  1067. {!!data.tones.length && data.fingeringMode === "scaleMode" && (
  1068. <>
  1069. {fingerData.fingeringInfo.name == "hulusi-flute" ? (
  1070. <div id="finger-note-1" class={[styles.toggleBtn, styles.toggleBtnhulusi]} onClick={() => (data.tnoteShow = true)}>
  1071. <div>
  1072. 全按作
  1073. <div class={[styles.noteKey]}>
  1074. {data.activeTone.step > 0 ? <span class={styles.dot}></span> : null}
  1075. <div class={styles.noteName}>
  1076. <sup>{data.activeTone.mark && (data.activeTone.mark === "rise" ? "#" : "b")}</sup>
  1077. {data.activeTone.key}
  1078. </div>
  1079. {data.activeTone.step < 0 ? <span class={styles.dot}></span> : null}
  1080. </div>
  1081. </div>
  1082. <img src={icons.icon_arrow} />
  1083. </div>
  1084. ) : (
  1085. <div id="finger-note-1" class={styles.toggleBtn} onClick={() => (data.tnoteShow = true)}>
  1086. <div style={{ marginTop: "-4px" }}>
  1087. <sup>{data.activeTone.mark && (data.activeTone.mark === "rise" ? "#" : "b")}</sup>
  1088. {data.activeTone.name}
  1089. </div>
  1090. <img src={icons.icon_arrow} />
  1091. </div>
  1092. )}
  1093. </>
  1094. )}
  1095. <Popup class="tonePopup" v-model:show={data.tnoteShow} position={state.platform !== IPlatform.PC && !query.modelType && fingerData.fingeringInfo.orientation === 1 ? "bottom" : "right"}>
  1096. <div class={styles.tones}>
  1097. <div class={styles.toneTitle}>
  1098. <div class={styles.tipTitleName}>移调</div>
  1099. <Button class={styles.tipClose} onClick={() => (data.tnoteShow = false)}>
  1100. <Icon name="cross" size={19} color="#fff" />
  1101. </Button>
  1102. </div>
  1103. <div class={styles.tipContentbox}>
  1104. <div class={styles.tipContent}>
  1105. <div class={styles.tipWrap}>
  1106. <Space size={0} class={styles.toneContent}>
  1107. {data.tones.map((tone: IFIGNER_INSTRUMENT_Note) => {
  1108. const steps = new Array(Math.abs(tone.step)).fill(1);
  1109. return (
  1110. <Button
  1111. class={[fingerData.fingeringInfo.name == "hulusi-flute" && styles.hulusiBtn]}
  1112. round
  1113. plain
  1114. type={data.popupActiveTone.realName === tone.realName ? "primary" : "default"}
  1115. onClick={() => {
  1116. data.popupActiveTone = tone;
  1117. setNotes();
  1118. }}
  1119. >
  1120. {fingerData.fingeringInfo.name == "hulusi-flute" ? (
  1121. <div style={{ display: "flex", alignItems: "center" }}>
  1122. 全按作
  1123. <div class={[styles.noteKey, styles.hulusiNoteKey]}>
  1124. {tone.step > 0 ? <span class={styles.dot}></span> : null}
  1125. <div class={styles.noteName} style={{ fontSize: "0.25rem" }}>
  1126. <sup>{tone.mark && (tone.mark === "rise" ? "#" : "b")}</sup>
  1127. {tone.key}
  1128. </div>
  1129. {tone.step < 0 ? <span class={styles.dot}></span> : null}
  1130. </div>
  1131. </div>
  1132. ) : (
  1133. <div class={styles.noteName}>
  1134. <sup>{tone.mark && (tone.mark === "rise" ? "#" : "b")}</sup>
  1135. {tone.name}
  1136. </div>
  1137. )}
  1138. </Button>
  1139. );
  1140. })}
  1141. </Space>
  1142. </div>
  1143. <div class={styles.toneAction}>
  1144. <img onClick={() => (data.tnoteShow = false)} src={icons.icon_action_cancel} />
  1145. <img
  1146. onClick={() => {
  1147. data.activeTone = data.popupActiveTone;
  1148. setNotes();
  1149. data.tnoteShow = false;
  1150. }}
  1151. src={icons.icon_action_confirm}
  1152. />
  1153. </div>
  1154. </div>
  1155. </div>
  1156. </div>
  1157. </Popup>
  1158. {props.show && !data.loading && !data.loadingSoundFonts && <GuideIndex showGuide={false} list={["finger"]} />}
  1159. </div>
  1160. );
  1161. };
  1162. },
  1163. });