index.tsx 75 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790
  1. import { PropType, computed, defineComponent, nextTick, onBeforeMount, onMounted, onUnmounted, reactive, ref,toRef } 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 { api_musicalInstrumentList, api_subjectList, getSubjectList } from "../api";
  19. import ChangeSubject from "./change-subject";
  20. import { Tabs,Tab } from "vant"
  21. import useDrag from '/src/hooks/useDrag';
  22. import Dragbom from '/src/hooks/useDrag/dragbom';
  23. export default defineComponent({
  24. name: "viewFigner",
  25. emits: ["close"],
  26. props: {
  27. show: {
  28. type: Boolean,
  29. default: true,
  30. },
  31. isComponent: {
  32. type: Boolean,
  33. default: false,
  34. },
  35. subject: {
  36. type: String as PropType<IVocals>,
  37. default: "",
  38. },
  39. },
  40. setup(props, { emit }) {
  41. const query = getQuery();
  42. const browsInfo = browser();
  43. const code = mappingVoicePart(query.code, "INSTRUMENT");
  44. const subject = props.isComponent ? props.subject || "pan-flute" : code || "pan-flute";
  45. const data = reactive({
  46. linkSource: query.linkSource, // 来源,目前只有课件里使用
  47. loading: true,
  48. subject: subject as any,
  49. realKey: 0,
  50. notes: [] as IFIGNER_INSTRUMENT_Note[],
  51. tones: [] as IFIGNER_INSTRUMENT_Note[],
  52. activeTone: {} as IFIGNER_INSTRUMENT_Note,
  53. popupActiveTone: {} as IFIGNER_INSTRUMENT_Note,
  54. activeToneName: "",
  55. soundFonts: {} as any,
  56. viewIndex: 0,
  57. viewTotal: 1,
  58. noteAudio: null as unknown as Howl,
  59. transform: {
  60. scale: 1,
  61. x: 0,
  62. y: 0,
  63. startScale: 1,
  64. startX: 0,
  65. startY: 0,
  66. transition: "",
  67. },
  68. tipShow: false,
  69. tips: [] as IFIGNER_INSTRUMENT_Note[],
  70. tnoteShow: false,
  71. loadingSoundFonts: true,
  72. loadingSoundProgress: 0,
  73. changeSubjectShow: false,
  74. huaweiPad: navigator?.userAgent?.includes("UAWEIVRD-W09") ? true : false,
  75. paddingTop: "",
  76. paddingLeft: "",
  77. subjects: [] as any,
  78. fingeringModeList: [
  79. {
  80. text: "指法模式",
  81. value: "fingeringMode",
  82. icon: icons.icon_click,
  83. },
  84. {
  85. text: "听音模式",
  86. value: "listenMode",
  87. icon: icons.icon_listen,
  88. },
  89. {
  90. text: "音阶模式",
  91. value: "scaleMode",
  92. icon: icons.icon_mode,
  93. },
  94. ],
  95. fingeringMode: query.type || ("scaleMode" as "fingeringMode" | "listenMode" | "scaleMode"), // 模式
  96. noteType: "all" as "#c" | "all", // 音调
  97. loadingDom: false, // 切换乐器时需要重置
  98. loadingImg: false, // 切换模式,加载图片
  99. });
  100. const fingerData = reactive({
  101. relationshipIndex: 0,
  102. subject: null as unknown as ITypeFingering,
  103. fingeringInfo: subjectFingering(data.subject),
  104. });
  105. if (!props.isComponent) {
  106. state.fingeringInfo = fingerData.fingeringInfo;
  107. }
  108. const getAPPData = async (type: "top" | "left") => {
  109. const screenData = await isSpecialShapedScreen();
  110. if (screenData?.content) {
  111. console.log("🚀 ~ screenData:", screenData.content);
  112. const { isSpecialShapedScreen, notchHeight } = screenData.content;
  113. if (isSpecialShapedScreen) {
  114. if (type === "top") {
  115. data.paddingTop = 25 + "px";
  116. }
  117. if (type === "left") {
  118. data.paddingLeft = 25 + "px";
  119. }
  120. }
  121. }
  122. };
  123. const getHeadTop = () => {
  124. if (fingerData.fingeringInfo.orientation === 1) {
  125. getAPPData("top");
  126. }
  127. if (fingerData.fingeringInfo.orientation === 0) {
  128. getAPPData("left");
  129. }
  130. };
  131. const getNotes = () => {
  132. const fignerData = FIGNER_INSTRUMENT_DATA[data.subject as keyof typeof FIGNER_INSTRUMENT_DATA];
  133. if (fignerData) {
  134. data.tones = fignerData.tones || [];
  135. if (data.tones.length) {
  136. data.activeTone = data.tones[0];
  137. data.popupActiveTone = data.tones[0];
  138. }
  139. data.tips = fignerData.tips || [];
  140. setNotes();
  141. setTimeout(() => {
  142. data.loading = false;
  143. }, 600);
  144. }
  145. };
  146. const setNotes = () => {
  147. const fignerData = FIGNER_INSTRUMENT_DATA[data.subject as keyof typeof FIGNER_INSTRUMENT_DATA];
  148. if (fignerData) {
  149. const tempNotes = fignerData[`list${data.activeTone.realName || ""}`];
  150. const appendNote: any = [];
  151. tempNotes.forEach((note: any) => {
  152. note.steps = new Array(Math.abs(note.step)).fill(1);
  153. if (FIGNER_INSTRUMENT_REALKEY.includes(note.realKey)) {
  154. appendNote.push(note);
  155. }
  156. });
  157. // 判断是音符状态
  158. data.notes = data.noteType === "#c" ? appendNote : tempNotes;
  159. // data.notes = fignerData[`list${data.activeTone.realName || ""}`];
  160. }
  161. };
  162. const getFingeringData = async () => {
  163. const subject: any = data.subject + (data.viewIndex === 0 ? "" : data.viewIndex);
  164. console.log("🚀 ~ subject:模式", subject, data.viewIndex, data.fingeringMode);
  165. fingerData.subject = await getFingeringConfig(subject);
  166. };
  167. const createAudio = (url: string) => {
  168. return new Promise((resolve, reject) => {
  169. const noteAudio = new Howl({
  170. src: url,
  171. loop: true,
  172. onload: () => {
  173. resolve(noteAudio);
  174. },
  175. onloaderror: () => {
  176. reject(new Error(`加载音频失败`));
  177. },
  178. });
  179. });
  180. };
  181. const getSounFonts = async () => {
  182. const pathname = /(192|localhost)/.test(location.origin) ? "/" : location.pathname;
  183. data.loadingSoundFonts = true;
  184. try {
  185. data.loadingSoundProgress = 0;
  186. for (let i = 0; i < data.notes.length; i++) {
  187. const note = data.notes[i];
  188. // console.log("🚀 ~ note:", i);
  189. let url = `${pathname}soundfonts/${data.subject}/`;
  190. url += note.realName;
  191. url += ".mp3";
  192. data.soundFonts[note.realKey] = await createAudio(url);
  193. data.loadingSoundProgress = Math.floor(((i + 1) / data.notes.length) * 100);
  194. }
  195. data.loadingSoundProgress = 100;
  196. } catch (e: any) {
  197. //
  198. showToast(e.message);
  199. }
  200. api_cloudLoading();
  201. data.loadingSoundFonts = false;
  202. };
  203. // const selectSubjectType = (subject: string) => {
  204. // data.subjects.forEach((item: any) => {
  205. // if (item.value === subject) {
  206. // item.className = styles.selected;
  207. // } else {
  208. // item.className = "";
  209. // }
  210. // });
  211. // };
  212. // 切换当前模式
  213. const onChangeFingeringModel = (e: any) => {
  214. e.stopPropagation();
  215. //
  216. if (playAction.listenLock) return;
  217. if (playAction.showAnswerLoading) return;
  218. data.loadingImg = true;
  219. if (data.fingeringMode === "scaleMode") {
  220. if (["pan-flute", "ocarina", "whistling"].includes(data.subject)) {
  221. data.viewIndex = 1;
  222. } else {
  223. data.viewIndex = 0;
  224. }
  225. const o: any = {
  226. "pan-flute": 2,
  227. ocarina: 2,
  228. whistling: 2,
  229. piccolo: 2,
  230. "hulusi-flute": 2,
  231. "baroque-recorder": 2,
  232. };
  233. data.viewTotal = o[data.subject] || 1;
  234. data.fingeringMode = "listenMode";
  235. } else if (data.fingeringMode === "listenMode") {
  236. data.fingeringMode = "fingeringMode";
  237. } else if (data.fingeringMode === "fingeringMode") {
  238. data.fingeringMode = "scaleMode";
  239. data.viewIndex = 0;
  240. data.noteType = "all";
  241. }
  242. data.tipShow = false;
  243. resetMode(true, 0);
  244. setTimeout(() => {
  245. __init(false);
  246. }, 100);
  247. };
  248. const __init = async (loadSong = true) => {
  249. data.loadingDom = true;
  250. getNotes();
  251. // selectSubjectType(data.subject);
  252. if (data.fingeringMode === "fingeringMode") {
  253. if (data.subject === "pan-flute") {
  254. data.viewIndex = 3;
  255. } else if (["pan-flute", "ocarina", "melodica", "whistling"].includes(data.subject)) {
  256. data.viewIndex = 1;
  257. }
  258. } else {
  259. if (["pan-flute", "ocarina", "whistling"].includes(data.subject)) {
  260. data.viewIndex = 1;
  261. }
  262. }
  263. const o: any = {
  264. "pan-flute": 2,
  265. ocarina: 2,
  266. whistling: 2,
  267. piccolo: 2,
  268. "hulusi-flute": 2,
  269. "baroque-recorder": 2,
  270. };
  271. data.viewTotal = o[data.subject] || 1;
  272. getFingeringData();
  273. getHeadTop();
  274. if (loadSong) {
  275. await getSounFonts();
  276. }
  277. data.loadingDom = false;
  278. data.loadingImg = false;
  279. };
  280. // 获取声部
  281. const getSubjects = async () => {
  282. try {
  283. // api_subjectList
  284. // const subjects = await api_musicalInstrumentList({
  285. // enableFlag: true,
  286. // });
  287. // const rows = subjects.data || [];
  288. // rows.forEach((row: any) => {
  289. // const tempList: any = {
  290. // text: row.name,
  291. // value: mappingVoicePart(row.code, "INSTRUMENT"), // mappingVoicePart(row.code, "INSTRUMENT"),
  292. // id: row.id,
  293. // };
  294. // data.subjects.push(tempList);
  295. // });
  296. const subjects = await api_subjectList({
  297. enableFlag: true,
  298. delFlag: 0,
  299. page: 1,
  300. rows: 999,
  301. });
  302. const rows = subjects.data || [];
  303. const tempSubjects: any[] = [];
  304. rows.forEach((row: any) => {
  305. const tempList: any = {
  306. text: row.name,
  307. value: "", // mappingVoicePart(row.code, "INSTRUMENT"),
  308. id: row.id,
  309. children: [] as any,
  310. };
  311. if (Array.isArray(row.instruments)) {
  312. row.instruments.forEach((i: any) => {
  313. tempList.children.push({
  314. text: i.name,
  315. id: i.id,
  316. value: mappingVoicePart(i.code, "INSTRUMENT"),
  317. });
  318. });
  319. }
  320. tempSubjects.push(tempList);
  321. // if (row.instruments && row.instruments.length > 0) {
  322. // if (row.instruments.length > 1) {
  323. // row.instruments.forEach((i: any) => {
  324. // tempList.children.push({
  325. // text: i.name,
  326. // id: i.id,
  327. // value: mappingVoicePart(i.code, "INSTRUMENT"),
  328. // });
  329. // });
  330. // } else {
  331. // const singleRow = row.instruments[0];
  332. // if (singleRow.code) {
  333. // tempList.value = mappingVoicePart(singleRow.code, "INSTRUMENT");
  334. // tempList.id = singleRow.id;
  335. // }
  336. // }
  337. // }
  338. // data.subjects.push(tempList);
  339. });
  340. console.log(data.subject, "data.subject");
  341. // tempSubjects.forEach((item: any) => {
  342. // if (item.value === data.subject && item.children?.length > 1) {
  343. // data.subject = item.children[0].value;
  344. // }
  345. // });
  346. data.subjects = tempSubjects;
  347. } catch (e) {
  348. //
  349. console.log(e, "e");
  350. }
  351. };
  352. onBeforeMount(async () => {
  353. if (browser().isApp) {
  354. state.platform = "APP" as IPlatform;
  355. } else {
  356. state.platform = query.platform?.toLocaleUpperCase() || "";
  357. }
  358. if (state.platform === IPlatform.PC) {
  359. document.title = "听音练习";
  360. }
  361. await getSubjects();
  362. __init();
  363. });
  364. /**
  365. * 播放音频
  366. * @param item 音频节点
  367. * @param showNote 是否显示对应的指法
  368. * @returns
  369. */
  370. const noteClick = (item: IFIGNER_INSTRUMENT_Note, showNote = true) => {
  371. // console.log('音高', item.realKey)
  372. if (data.noteAudio) {
  373. data.noteAudio.stop();
  374. if (data.realKey === item.realKey) {
  375. data.realKey = 0;
  376. data.noteAudio = null as unknown as Howl;
  377. return;
  378. }
  379. }
  380. if (showNote) {
  381. data.realKey = item.realKey;
  382. }
  383. console.log("key:", item.realKey, data.soundFonts);
  384. data.noteAudio = data.soundFonts[item.realKey];
  385. if (data.noteAudio) {
  386. data.noteAudio.play();
  387. }
  388. };
  389. const handleStop = () => {
  390. if (data.noteAudio) {
  391. data.noteAudio.stop();
  392. data.realKey = 0;
  393. data.noteAudio = null as unknown as Howl;
  394. }
  395. };
  396. /** 返回 */
  397. const handleBack = () => {
  398. // platform: query.platform,
  399. handleStop();
  400. if (props.isComponent) {
  401. // 返回的时候默认横屏
  402. // api_setRequestedOrientation(0);
  403. emit("close");
  404. return;
  405. } else if (state.platform === IPlatform.PC) {
  406. console.log(1, query);
  407. if (query.matchMedia == 1) {
  408. // 老师端,首页
  409. window.parent.postMessage(
  410. {
  411. api: "iframe_exit",
  412. },
  413. "*"
  414. );
  415. return;
  416. } else {
  417. window.close();
  418. return;
  419. }
  420. // if (fingerData.fingeringInfo.orientation === 0) {
  421. // api_setRequestedOrientation(1);
  422. // }
  423. }
  424. // 不在APP中,
  425. if (!storeData.isApp) {
  426. window.close();
  427. return;
  428. }
  429. api_back();
  430. };
  431. // 排箫,默认0.9显示
  432. const setDefaultScale = () => {
  433. if (data.subject === "pan-flute") {
  434. data.transform.scale = 0.9;
  435. data.transform.startScale = 0.9;
  436. }
  437. };
  438. onMounted(() => {
  439. loadElement();
  440. api_setStatusBarVisibility();
  441. });
  442. const loadElement = () => {
  443. const fingeringContainer = document.getElementById("fingeringContainer");
  444. setDefaultScale();
  445. // console.log("🚀 ~ fingeringContainer:", fingeringContainer);
  446. const mc = new Hammer.Manager(fingeringContainer as HTMLElement);
  447. mc.add(new Hammer.Pan({ threshold: 0, pointers: 0 }));
  448. mc.add(new Hammer.Pinch({ threshold: 0 })).recognizeWith([mc.get("pan")]);
  449. // mc.get("pan").set({ direction: Hammer.DIRECTION_ALL });
  450. // mc.get("pinch").set({ enable: true });
  451. mc.on("panstart pinchstart", function (ev) {
  452. data.transform.transition = "";
  453. });
  454. mc.on("panmove pinchmove", function (ev) {
  455. if (ev.type === "pinchmove") {
  456. // console.log("🚀 ~ ev:", ev.type, ev.scale, ev.deltaX, ev.deltaY);
  457. data.transform.scale = ev.scale * data.transform.startScale;
  458. data.transform.x = data.transform.startX + ev.deltaX;
  459. data.transform.y = data.transform.startY + ev.deltaY;
  460. }
  461. if (ev.type === "panmove") {
  462. // console.log("🚀 ~ ev:", ev.type, ev.deltaX, ev.deltaY);
  463. data.transform.x = data.transform.startX + ev.deltaX;
  464. data.transform.y = data.transform.startY + ev.deltaY;
  465. }
  466. });
  467. //
  468. mc.on("hammer.input", function (ev) {
  469. if (ev.isFinal) {
  470. data.transform.startScale = data.transform.scale;
  471. data.transform.startX = data.transform.x;
  472. data.transform.startY = data.transform.y;
  473. }
  474. });
  475. };
  476. const resetElement = () => {
  477. data.transform.transition = "all 0.3s";
  478. nextTick(() => {
  479. data.transform.scale = data.subject === "pan-flute" ? 0.9 : 1;
  480. data.transform.x = 0;
  481. data.transform.y = 0;
  482. data.transform.startScale = data.subject === "pan-flute" ? 0.9 : 1;
  483. data.transform.startX = 0;
  484. data.transform.startY = 0;
  485. });
  486. };
  487. // 判断乐器是否移动
  488. const instrumentTranstion = computed(() => {
  489. const transform = data.transform;
  490. let scale = 1;
  491. if (data.subject === "pan-flute") {
  492. scale = 0.9;
  493. }
  494. if (transform.scale !== scale || transform.x !== 0 || transform.y !== 0 || transform.startScale !== scale || transform.startX !== 0 || transform.startY !== 0) {
  495. return true;
  496. } else {
  497. return false;
  498. }
  499. });
  500. const pageVisible = usePageVisibility();
  501. watch(
  502. () => pageVisible.value,
  503. (val) => {
  504. if (val === "hidden") {
  505. clearTimeout(playAction.timer);
  506. playAction.listenLock = false;
  507. playAction.listenTipsStatus = false;
  508. playAction.exampleAnser = {};
  509. resetMode(true, 0);
  510. handleStop();
  511. gaumntPause();
  512. }
  513. }
  514. );
  515. /** 课件播放 */
  516. const changePlay = (res: any) => {
  517. if (res?.data?.api === "setPlayState") {
  518. clearTimeout(playAction.timer);
  519. playAction.listenLock = false;
  520. playAction.listenTipsStatus = false;
  521. playAction.exampleAnser = {};
  522. resetMode(true, 0);
  523. handleStop();
  524. gaumntPause();
  525. // 重置乐器
  526. if (res?.data?.data.code) {
  527. data.subject = code;
  528. data.viewIndex = 0;
  529. data.tipShow = false;
  530. data.loadingDom = true;
  531. fingerData.fingeringInfo = subjectFingering(data.subject);
  532. data.activeTone = {} as any;
  533. resetElement();
  534. // 设置屏幕方向
  535. setTimeout(() => {
  536. __init();
  537. }, 100);
  538. }
  539. }
  540. };
  541. const noteBoxRef = ref();
  542. const scrollNoteBox = (type: "left" | "right") => {
  543. const width = noteBoxRef.value.offsetWidth / 2;
  544. (noteBoxRef.value as unknown as HTMLElement).scrollBy({
  545. left: type === "left" ? -width : width,
  546. behavior: "smooth",
  547. });
  548. };
  549. const playStatus = reactive({
  550. gamut: false, // 是否播放音阶
  551. gamutTimer: null as any, // 播放音阶定时器
  552. answer: false, // 是否显示答案
  553. action: false, // 是否开始播放
  554. });
  555. /** 音符切换 */
  556. const noteChangeShow = () => {
  557. if (playStatus.action) {
  558. if (playAction.listenLock) return;
  559. playAction.resetAction = true;
  560. resetMode(true, 0);
  561. }
  562. // // 播放音阶时不能切换
  563. // if (playStatus.gamut) return;
  564. // // 开始答题不能切换
  565. // if (playStatus.action) return;
  566. playStatus.gamut = false;
  567. gaumntPause();
  568. if (data.noteType === "all") {
  569. data.noteType = "#c";
  570. } else {
  571. data.noteType = "all";
  572. }
  573. getNotes();
  574. setTimeout(() => {
  575. playAction.resetAction = false;
  576. }, 2000);
  577. };
  578. // 开始播放音阶
  579. const onGamutPlayOrPause = async () => {
  580. playAction.resetAction = false;
  581. if (playStatus.gamut) {
  582. playStatus.gamut = false;
  583. gaumntPause();
  584. } else {
  585. // 不管当前显示在哪个音老师滚动到开始位置
  586. (noteBoxRef.value as unknown as HTMLElement).scroll({
  587. left: 0,
  588. top: 0,
  589. behavior: "smooth",
  590. });
  591. playStatus.gamut = true;
  592. const notes = data.notes;
  593. let scrollCount = 0;
  594. for (let i = 0; i < notes.length; i++) {
  595. if (!playStatus.gamut) return false;
  596. const activeDom = document.querySelectorAll(".note-class")[i] as any;
  597. if (activeDom.offsetLeft >= noteBoxRef.value.offsetWidth + (noteBoxRef.value.offsetWidth / 2) * scrollCount - activeDom.offsetWidth) {
  598. scrollNoteBox("right");
  599. scrollCount++;
  600. }
  601. await gaumtPlay(notes[i]);
  602. }
  603. // // 处理播放到最后一个
  604. setTimeout(() => {
  605. playStatus.gamut = false;
  606. gaumntPause();
  607. }, 667);
  608. }
  609. };
  610. const gaumtPlay = (note: any, status?: boolean) => {
  611. return new Promise((resolve) => {
  612. playStatus.gamutTimer = setTimeout(() => {
  613. if (playStatus.gamut || status) {
  614. noteClick(note);
  615. }
  616. resolve(note);
  617. }, 667);
  618. });
  619. };
  620. const gaumntPause = () => {
  621. clearTimeout(playStatus.gamutTimer);
  622. if (data.noteAudio) {
  623. data.noteAudio.stop();
  624. data.realKey = 0;
  625. data.noteAudio = null as unknown as Howl;
  626. }
  627. };
  628. /** 开始播放 */
  629. const playAction = reactive({
  630. exampleAnser: {} as any, // 示例声音
  631. standardAnswer: {} as any, // 标准答案key
  632. showAnswerLoading: false, // 显示按答案中
  633. listenModeStatus: false, // 是否开始了模式
  634. listenLock: false,
  635. listenTipsStatus: false, // 开始播放状态
  636. resetAction: false, // 是否重置
  637. /** 0: 未答,1: 答对,2: 答错 */
  638. userAnswerStatus: 0 as 0 | 1 | 2, // 用户回答状态
  639. userAnswer: {} as any, // 用户答的数据
  640. timer: null as any,
  641. });
  642. const onActionPlay = async () => {
  643. playAction.resetAction = false;
  644. if (playAction.listenLock) return;
  645. if (playAction.showAnswerLoading) return;
  646. playStatus.action = true;
  647. playStatus.answer = true;
  648. // 先暂停播放声音
  649. gaumntPause();
  650. if (data.fingeringMode === "fingeringMode") {
  651. onFingeringMode();
  652. } else if (data.fingeringMode === "listenMode") {
  653. if (playAction.listenModeStatus) {
  654. playAction.listenLock = true;
  655. await fingeringPlay(playAction.standardAnswer, 1500, false);
  656. gaumntPause();
  657. playAction.listenLock = false;
  658. } else {
  659. onListenMode();
  660. }
  661. }
  662. };
  663. // 指法模式
  664. const fingeringPlay = (note: any, timer = 1500, showNote = true) => {
  665. return new Promise((resolve) => {
  666. noteClick(note, showNote);
  667. playAction.timer = setTimeout(() => {
  668. resolve(note);
  669. }, timer);
  670. });
  671. };
  672. const onFingeringMode = () => {
  673. const randomIndex = Math.floor(Math.random() * data.notes.length);
  674. playAction.standardAnswer = data.notes[randomIndex];
  675. data.realKey = data.notes[randomIndex].realKey;
  676. if (playAction.listenModeStatus) {
  677. return;
  678. }
  679. playAction.listenModeStatus = true; // 是否开始听音
  680. playAction.listenLock = true; // 锁
  681. playAction.listenTipsStatus = true;
  682. playAction.timer = setTimeout(() => {
  683. playAction.listenTipsStatus = false;
  684. playAction.listenLock = false; // 锁
  685. }, 2000);
  686. };
  687. // 听音模式
  688. const onListenMode = async () => {
  689. playAction.listenModeStatus = true; // 是否开始听音
  690. playAction.listenLock = true; // 锁
  691. playAction.listenTipsStatus = true;
  692. // 设置并保存示例数据
  693. let randomIndex = data.notes.findIndex((item: any) => item.realKey === 67); // Math.floor(Math.random() * data.notes.length);
  694. playAction.exampleAnser = data.notes[randomIndex];
  695. data.realKey = playAction.exampleAnser.realKey;
  696. scrollAnswer(playAction.exampleAnser.realKey);
  697. await fingeringPlay(playAction.exampleAnser);
  698. data.realKey = 0;
  699. playAction.exampleAnser = {};
  700. gaumntPause();
  701. playAction.timer = setTimeout(async () => {
  702. // 设置答题数据
  703. randomIndex = Math.floor(Math.random() * data.notes.length);
  704. playAction.standardAnswer = data.notes[randomIndex];
  705. await fingeringPlay(data.notes[randomIndex], 1500, false);
  706. gaumntPause();
  707. playAction.listenLock = false;
  708. playAction.listenTipsStatus = false;
  709. }, 1000);
  710. };
  711. // 显示答案
  712. const onShowAnswer = async () => {
  713. if (playAction.listenLock) return;
  714. playAction.showAnswerLoading = true;
  715. scrollAnswer(playAction.standardAnswer.realKey);
  716. await fingeringPlay(playAction.standardAnswer);
  717. resetMode(true, 0);
  718. // }
  719. };
  720. // 滚动到对应答案位置
  721. const scrollAnswer = (realKey?: any) => {
  722. const tempRealKey = realKey || data.realKey;
  723. const index = data.notes.findIndex((item: any) => item.realKey === tempRealKey);
  724. const activeDom = document.querySelectorAll(".note-class")[index] as any;
  725. if (activeDom) {
  726. const aWidth = activeDom.offsetWidth;
  727. const width = noteBoxRef.value.offsetWidth;
  728. const aLeft = Math.max(activeDom?.offsetLeft - aWidth, 0);
  729. (noteBoxRef.value as unknown as HTMLElement).scroll({
  730. left: Math.max(aLeft - width / 2, 0),
  731. top: 0,
  732. behavior: "smooth",
  733. });
  734. }
  735. };
  736. /**
  737. * 重置播放状态
  738. * @param status 是否全部重置
  739. * @param timer 延时时长(默认2s)
  740. */
  741. const resetMode = (status = true, timer = 2000) => {
  742. // 2秒钟后重置
  743. setTimeout(() => {
  744. gaumntPause();
  745. if (status) {
  746. playAction.standardAnswer = {};
  747. playAction.showAnswerLoading = false;
  748. playAction.userAnswerStatus = 0;
  749. playAction.userAnswer = {};
  750. playAction.listenModeStatus = false;
  751. playStatus.action = false;
  752. playStatus.answer = false;
  753. playStatus.gamut = false;
  754. data.realKey = 0;
  755. } else {
  756. playAction.userAnswerStatus = 0;
  757. playAction.userAnswer = {};
  758. }
  759. }, timer);
  760. };
  761. /** 滚轮缩放 */
  762. const handleWheel = (e: WheelEvent) => {
  763. e.preventDefault();
  764. if (e.deltaY > 0) {
  765. data.transform.scale -= 0.1;
  766. if (data.transform.scale <= 0.5) {
  767. data.transform.scale = 0.5;
  768. }
  769. } else {
  770. data.transform.scale += 0.1;
  771. if (data.transform.scale >= 2) {
  772. data.transform.scale = 2;
  773. }
  774. }
  775. };
  776. onMounted(() => {
  777. window.addEventListener("message", changePlay);
  778. const fingeringContainer = document.getElementById("fingeringContainer");
  779. fingeringContainer?.addEventListener("wheel", handleWheel);
  780. });
  781. onUnmounted(() => {
  782. window.removeEventListener("message", changePlay);
  783. const fingeringContainer = document.getElementById("fingeringContainer");
  784. fingeringContainer?.removeEventListener("wheel", handleWheel);
  785. document.title = "Ai学练";
  786. });
  787. const containerBox = computed(() => {
  788. if (state.platform === IPlatform.PC || query.modelType) {
  789. return {
  790. paddingTop: "1.3rem",
  791. paddingBottom: "",
  792. };
  793. }
  794. if (data.fingeringMode === "scaleMode") {
  795. if (data.subject === "hulusi-flute") {
  796. return {
  797. paddingTop: "1.3rem",
  798. paddingBottom: ".5rem",
  799. };
  800. } else if (data.subject === "piccolo" || data.subject === "baroque-recorder") {
  801. return {
  802. paddingTop: "1.3rem",
  803. paddingBottom: ".5rem",
  804. };
  805. } else if (data.subject === "pan-flute") {
  806. return {
  807. paddingTop: "1.3rem",
  808. paddingBottom: "0",
  809. };
  810. } else if (data.subject === "ocarina" || data.subject === "whistling") {
  811. return {
  812. paddingTop: "1.3rem",
  813. paddingBottom: "0",
  814. };
  815. } else if (data.subject === "melodica") {
  816. return {
  817. paddingTop: "1.8rem",
  818. paddingBottom: "0.2rem",
  819. };
  820. } else {
  821. return {
  822. paddingTop: "",
  823. paddingBottom: "",
  824. };
  825. }
  826. } else {
  827. if (data.subject === "hulusi-flute") {
  828. return {
  829. paddingTop: "1.3rem",
  830. paddingBottom: "0rem",
  831. };
  832. } else if (data.subject === "piccolo" || data.subject === "baroque-recorder") {
  833. return {
  834. paddingTop: "1.3rem",
  835. paddingBottom: ".5rem",
  836. };
  837. } else if (data.subject === "pan-flute") {
  838. return {
  839. paddingTop: "1.3rem",
  840. paddingBottom: "0",
  841. };
  842. } else if (data.subject === "ocarina" || data.subject === "whistling") {
  843. return {
  844. paddingTop: "1.3rem",
  845. paddingBottom: "0",
  846. };
  847. } else if (data.subject === "melodica") {
  848. return {
  849. paddingTop: "1.8rem",
  850. paddingBottom: "0.2rem",
  851. };
  852. } else {
  853. return {
  854. paddingTop: "",
  855. paddingBottom: "",
  856. };
  857. }
  858. }
  859. });
  860. const listenText = computed(() => {
  861. if (data.fingeringMode === "fingeringMode") {
  862. if (playStatus.action) {
  863. return "换一换";
  864. } else {
  865. return "开始练习";
  866. }
  867. } else if (data.fingeringMode === "listenMode") {
  868. if (playStatus.action) {
  869. return "再听一遍";
  870. } else {
  871. return "开始听音";
  872. }
  873. }
  874. return "开始听音";
  875. });
  876. const modeText = computed(() => {
  877. let text = "";
  878. let icon = icons.icon_mode;
  879. data.fingeringModeList.forEach((item: any) => {
  880. if (item.value === data.fingeringMode) {
  881. text = item.text;
  882. icon = item.icon;
  883. }
  884. });
  885. return {
  886. text,
  887. icon,
  888. };
  889. });
  890. // 屏幕方向 0 竖,1 横
  891. const orientationDirection = computed(() => {
  892. return ["hulusi-flute", "piccolo", "baroque-recorder"].includes(data.subject) ? 1 : 0;
  893. });
  894. const resultImg = (note: any) => {
  895. if (data.realKey === note.realKey && !playStatus.action) {
  896. return {
  897. icon: icons.icon_btn_ylow,
  898. status: false,
  899. };
  900. } else if (playAction.exampleAnser.realKey === note.realKey) {
  901. return {
  902. icon: icons.icon_btn_ylow,
  903. status: false,
  904. };
  905. } else if (playAction.standardAnswer.realKey === note.realKey) {
  906. // 没有开始答题
  907. if (!playStatus.action) {
  908. return {
  909. icon: icons.icon_btn_ylow,
  910. status: false,
  911. };
  912. }
  913. // 显示答案中
  914. if (playAction.showAnswerLoading) {
  915. return {
  916. icon: icons.icon_btn_green,
  917. status: true,
  918. };
  919. }
  920. // 用户答对
  921. if (playAction.userAnswerStatus === 1) {
  922. return {
  923. icon: icons.icon_btn_green,
  924. status: true,
  925. };
  926. }
  927. } else {
  928. // 用户答错
  929. if (playAction.userAnswerStatus === 2 && playAction.userAnswer.realKey === note.realKey) {
  930. return {
  931. icon: icons.icon_btn_red,
  932. status: true,
  933. };
  934. }
  935. }
  936. return {
  937. icon: icons.icon_btn_blue,
  938. status: true,
  939. };
  940. };
  941. const userTabActive=ref<"1"|"2">("1")
  942. const userTabs=[
  943. {
  944. name:"音阶",
  945. value:"1"
  946. },
  947. {
  948. name:"功能",
  949. value:"2"
  950. }
  951. ]
  952. // 切换乐器弹窗
  953. let changeSubjectShowBoxDragData: any;
  954. let changeSubjectShowBoxClass: string;
  955. if (query.platform==="pc") {
  956. changeSubjectShowBoxClass = 'changeSubjectShowBoxClass_drag';
  957. changeSubjectShowBoxDragData = useDrag(
  958. [`${changeSubjectShowBoxClass} .dragTopBox`, `${changeSubjectShowBoxClass} .dragbomBox`],
  959. changeSubjectShowBoxClass,
  960. toRef(data, 'changeSubjectShow'),
  961. storeData.user.id as string
  962. );
  963. }
  964. // 移调弹窗
  965. let tnoteShowBoxDragData: any;
  966. let tnoteShowBoxClass: string;
  967. if (query.platform==="pc") {
  968. tnoteShowBoxClass = 'tnoteShowBoxClass_drag';
  969. tnoteShowBoxDragData = useDrag(
  970. [`${tnoteShowBoxClass} .dragTopBox`, `${tnoteShowBoxClass} .dragbomBox`],
  971. tnoteShowBoxClass,
  972. toRef(data, 'tnoteShow'),
  973. storeData.user.id as string
  974. );
  975. }
  976. return () => {
  977. const relationship = fingerData.subject?.relationship?.[data.realKey] || [];
  978. const rs: number[] = Array.isArray(relationship[1]) ? relationship[fingerData.relationshipIndex] : relationship;
  979. const canTizhi = Array.isArray(relationship[1]);
  980. return (
  981. <div
  982. class={[styles.fingerBox, state.platform !== IPlatform.PC && !query.modelType && fingerData.fingeringInfo.orientation === 1 ? styles.fingerBottom : styles.fingerRight, data.linkSource === "class" ? styles.linkSourceClass : ""]}
  983. onClick={() => {
  984. if (data.linkSource === "class") {
  985. window.parent.postMessage(
  986. {
  987. api: "clickViewFigner",
  988. },
  989. "*"
  990. );
  991. }
  992. }}
  993. >
  994. {/* 老师端过来隐藏 */}
  995. {
  996. query.platform!=='pc'&&(
  997. <div
  998. class={styles.head}
  999. style={{
  1000. paddingTop: data.paddingTop && !browser().ios ? data.paddingTop : "",
  1001. paddingLeft: data.paddingLeft && !browser().ios ? data.paddingLeft : "",
  1002. }}
  1003. >
  1004. <div class={styles.left}>
  1005. <button class={[styles.backBtn]} onClick={() => handleBack()}>
  1006. <img src={icons.icon_back} />
  1007. </button>
  1008. <div
  1009. class={[styles.baseBtn, styles.changeInstrumentBtn]}
  1010. onClick={(e) => {
  1011. e.stopPropagation();
  1012. //
  1013. // 播放音阶时不能切换
  1014. if (playStatus.gamut) {
  1015. return;
  1016. }
  1017. // 开始答题不能切换
  1018. if (playAction.listenLock) {
  1019. return;
  1020. }
  1021. data.changeSubjectShow = true;
  1022. }}
  1023. >
  1024. <img src={icons.icon_change_instrument} />
  1025. <span>切换乐器</span>
  1026. </div>
  1027. <div class={styles.baseBtn} onClick={onChangeFingeringModel}>
  1028. <img src={modeText.value.icon} />
  1029. <span>{modeText.value.text}</span>
  1030. </div>
  1031. {/* */}
  1032. </div>
  1033. {/* */}
  1034. </div>
  1035. )
  1036. }
  1037. <div
  1038. class={styles.fingerContent}
  1039. style={{
  1040. paddingTop: data.paddingTop ? data.paddingTop : "",
  1041. paddingLeft: data.paddingLeft ? data.paddingLeft : "",
  1042. }}
  1043. >
  1044. <div class={styles.wrapFinger}>
  1045. <div
  1046. id="fingeringContainer"
  1047. class={[styles.boxFinger,query.platform==='pc'?styles.pcBoxFinger:""]}
  1048. style={{
  1049. paddingTop: containerBox.value.paddingTop,
  1050. paddingBottom: containerBox.value.paddingBottom,
  1051. }}
  1052. >
  1053. <div
  1054. style={{
  1055. transform: `translate3d(${data.transform.x}px,${data.transform.y}px,0px) scale(${data.transform.scale})`,
  1056. transition: data.transform.transition,
  1057. }}
  1058. class={[styles.fingeringContainer]}
  1059. >
  1060. <div class={styles.imgs}>
  1061. {!data.loadingImg && <img src={data.fingeringMode === "scaleMode" ? fingerData.subject?.json?.full : fingerData.subject?.json?.full1} />}
  1062. {rs.map((key: number | string, index: number) => {
  1063. const nk: string = typeof key === "string" ? key.replace("active-", "") : String(key);
  1064. return <img data-index={nk} src={fingerData.subject?.json?.[nk]} />;
  1065. })}
  1066. <div style={{ left: data.viewIndex == 2 ? "0" : "64%" }} class={[styles.tizhi, canTizhi && styles.canDisplay]} onClick={() => (fingerData.relationshipIndex = fingerData.relationshipIndex === 0 ? 1 : 0)}>
  1067. 替指
  1068. </div>
  1069. <div id="finger-note-2" style={{ left: "50%", transform: "translateX(-50%)" }} class={styles.tizhi} onClick={() => (fingerData.relationshipIndex = fingerData.relationshipIndex === 0 ? 1 : 0)}></div>
  1070. </div>
  1071. </div>
  1072. </div>
  1073. {/* 老师端过来隐藏 */}
  1074. {
  1075. query.platform==='pc'?(
  1076. <div class={styles.userTab}>
  1077. <Tabs v-model:active={userTabActive.value} class={styles.userTabBox}>
  1078. {
  1079. userTabs.map(item=>{
  1080. return <Tab title={item.name} name={item.value}>
  1081. {
  1082. item.value==="1"?(<>
  1083. <div
  1084. class={styles.notes}
  1085. style={{
  1086. paddingLeft: data.paddingLeft ? data.paddingLeft : "",
  1087. }}
  1088. >
  1089. {playAction.listenTipsStatus && <div class={[styles.tipsT, data.fingeringMode === "fingeringMode" ? styles.playTips2 : styles.playTips]}></div>}
  1090. {playAction.userAnswerStatus === 1 && <div class={[styles.tipsT, styles.playSuccess]}></div>}
  1091. {playAction.userAnswerStatus === 2 && <div class={[styles.tipsT, styles.playError]}></div>}
  1092. {playAction.resetAction && <div class={[styles.tipsT, styles.playTips5]}></div>}
  1093. <div class={styles.changeMusBtn} onClick={onChangeFingeringModel}>
  1094. <span>{modeText.value.text}</span>
  1095. </div>
  1096. <div
  1097. class={[styles.noteContent, data.fingeringMode !== "scaleMode" && orientationDirection.value === 0 && styles.noteContentOther, browsInfo.ios ? "" : styles.noteContentWrap, data.huaweiPad && styles.huaweiPad]}
  1098. onClick={(e: any) => {
  1099. e.stopPropagation();
  1100. }}
  1101. >
  1102. {((data.noteType !== "#c" && (orientationDirection.value === 0 || (orientationDirection.value === 1 && state.platform === IPlatform.PC))) || (orientationDirection.value === 1 && state.platform === IPlatform.APP)) && (
  1103. <Button
  1104. class={styles.noteBtn}
  1105. onClick={(e: any) => {
  1106. e.stopPropagation();
  1107. scrollNoteBox("left");
  1108. }}
  1109. >
  1110. <Icon name="arrow-left" />
  1111. </Button>
  1112. )}
  1113. {/* 判断是否为音阶模式 */}
  1114. {data.fingeringMode !== "scaleMode" && (
  1115. <div draggable={false} class={styles.note} onClick={noteChangeShow}>
  1116. <img draggable={false} src={data.noteType === "all" ? icons.icon_btn_orange : icons.icon_btn_orange2} />
  1117. </div>
  1118. )}
  1119. {/* [styles.noteContent, browsInfo.ios ? "" : styles.noteContentWrap, data.huaweiPad && styles.huaweiPad] */}
  1120. <div class={styles.lastNoteContent}>
  1121. <div ref={noteBoxRef} class={styles.noteBox}>
  1122. {data.notes.map((note: IFIGNER_INSTRUMENT_Note, index: number) => {
  1123. const steps = new Array(Math.abs(note.step)).fill(1);
  1124. return (
  1125. <div
  1126. id={index == 0 ? "finger-note-0" : ""}
  1127. draggable={false}
  1128. class={[styles.note, "note-class"]}
  1129. key={note.realKey}
  1130. onClick={async () => {
  1131. // 判断是否在播放音阶
  1132. if (playStatus.gamut) return;
  1133. if (playAction.listenLock) return;
  1134. if (playAction.showAnswerLoading) return;
  1135. if (playStatus.action) {
  1136. playAction.userAnswer = note;
  1137. // 判断用户答题
  1138. const userResult = note.realKey === playAction.standardAnswer.realKey ? 1 : 2;
  1139. playAction.userAnswerStatus = userResult;
  1140. playAction.listenLock = true;
  1141. data.realKey = note.realKey;
  1142. await fingeringPlay(note, 1000);
  1143. resetMode(userResult === 1 ? true : false, 0);
  1144. data.realKey = 0;
  1145. // 如果是指法模式显示完之后要还原
  1146. if (data.fingeringMode === "fingeringMode" && userResult === 2) {
  1147. // 延迟显示,因为重置的时候有一个异步操作
  1148. setTimeout(() => {
  1149. data.realKey = playAction.standardAnswer.realKey;
  1150. }, 10);
  1151. }
  1152. playAction.listenLock = false;
  1153. } else {
  1154. noteClick(note);
  1155. }
  1156. }}
  1157. >
  1158. <img draggable={false} src={resultImg(note).icon} />
  1159. {playStatus.action && ((playAction.showAnswerLoading && playAction.standardAnswer.realKey === note.realKey) || (playAction.userAnswerStatus === 1 && playAction.userAnswer.realKey === note.realKey)) ? <span class={styles.showAnswer}></span> : ""}
  1160. {playStatus.action && playAction.userAnswerStatus === 2 && playAction.userAnswer.realKey === note.realKey ? <span class={[styles.showAnswer, styles.errorAnswer]}></span> : ""}
  1161. <div
  1162. class={[
  1163. styles.noteKey,
  1164. ((data.realKey === note.realKey && !playStatus.action) ||
  1165. (playStatus.action && playAction.exampleAnser.realKey === note.realKey) ||
  1166. (playStatus.action && ((playAction.showAnswerLoading && playAction.standardAnswer.realKey === note.realKey) || (playAction.userAnswerStatus === 1 && playAction.userAnswer.realKey === note.realKey))) ||
  1167. (playStatus.action && playAction.userAnswerStatus === 2 && playAction.userAnswer.realKey === note.realKey)) &&
  1168. styles.keyActive,
  1169. ]}
  1170. >
  1171. {/* 显示对应的点 */}
  1172. {note.step > 0 ? steps.map((n: any) => <span class={styles.dot}></span>) : null}
  1173. <div class={styles.noteName}>
  1174. <sup>{note.mark && (note.mark === "rise" ? "#" : "b")}</sup>
  1175. {note.key}
  1176. </div>
  1177. {/* 显示对应的点 */}
  1178. {note.step < 0 ? steps.map((n: any) => <span class={styles.dot}></span>) : null}
  1179. </div>
  1180. </div>
  1181. );
  1182. })}
  1183. </div>
  1184. </div>
  1185. {((data.noteType !== "#c" && (orientationDirection.value === 0 || (orientationDirection.value === 1 && state.platform === IPlatform.PC))) || (orientationDirection.value === 1 && state.platform === IPlatform.APP)) && (
  1186. <Button
  1187. class={styles.noteBtn}
  1188. onClick={(e: any) => {
  1189. e.stopPropagation();
  1190. scrollNoteBox("right");
  1191. }}
  1192. >
  1193. <Icon name="arrow" />
  1194. </Button>
  1195. )}
  1196. </div>
  1197. </div>
  1198. {data.fingeringMode !== "scaleMode" && (
  1199. <div
  1200. class={styles.optionBtns}
  1201. onClick={(e: any) => {
  1202. e.stopPropagation();
  1203. }}
  1204. >
  1205. <Button class={[styles.oBtn, styles.gamut, playStatus.action && styles.disabled]} round onClick={onGamutPlayOrPause}>
  1206. {playStatus.gamut ? "暂停" : "播放音阶"}
  1207. </Button>
  1208. <Button class={[styles.oBtn, styles.play, playStatus.gamut && styles.disabled]} round onClick={onActionPlay}>
  1209. {listenText.value}
  1210. </Button>
  1211. <Button class={[styles.oBtn, styles.success, !playStatus.answer && styles.disabled]} round onClick={onShowAnswer}>
  1212. 显示答案
  1213. </Button>
  1214. </div>
  1215. )}
  1216. </>):(<>
  1217. <div class={styles.btnBox}>
  1218. <div class={styles.btnCon}>
  1219. <div
  1220. class={[styles.btnGr]}
  1221. onClick={(e) => {
  1222. e.stopPropagation();
  1223. //
  1224. // 播放音阶时不能切换
  1225. if (playStatus.gamut) {
  1226. return;
  1227. }
  1228. // 开始答题不能切换
  1229. if (playAction.listenLock) {
  1230. return;
  1231. }
  1232. data.changeSubjectShow = true;
  1233. }}
  1234. >
  1235. <img src={icons.icon_change_instrument} />
  1236. <span>切换乐器</span>
  1237. </div>
  1238. {data.subject !== "melodica" && data.fingeringMode === "scaleMode" && (
  1239. <div
  1240. class={styles.btnGr}
  1241. onClick={() => {
  1242. data.viewIndex++;
  1243. if (data.viewIndex > data.viewTotal) {
  1244. if (["pan-flute", "ocarina", "whistling"].includes(data.subject)) {
  1245. data.viewIndex = 1;
  1246. } else {
  1247. data.viewIndex = 0;
  1248. }
  1249. }
  1250. getFingeringData();
  1251. }}
  1252. >
  1253. <img src={icons.icon_toggle} />
  1254. <span>视图</span>
  1255. </div>
  1256. )}
  1257. <div
  1258. class={styles.btnGr}
  1259. onClick={() => {
  1260. resetElement();
  1261. data.tipShow = !data.tipShow;
  1262. }}
  1263. >
  1264. <img src={icons.icon_2_1} />
  1265. <span>说明</span>
  1266. </div>
  1267. {!!data.tones.length && data.fingeringMode === "scaleMode" && (
  1268. <>
  1269. {fingerData.fingeringInfo.name == "hulusi-flute" ? (
  1270. <div id="finger-note-1" class={[styles.btnGr, styles.btnGrToggleBtn]} onClick={() => (data.tnoteShow = true)}>
  1271. <div class={styles.nameBox}>
  1272. 全按作
  1273. <div class={[styles.noteKey, styles.noteKeyBtn]}>
  1274. {data.activeTone.step > 0 ? <span class={styles.dot}></span> : null}
  1275. <span class={styles.dot}></span>
  1276. <div class={styles.noteName}>
  1277. <sup>{data.activeTone.mark && (data.activeTone.mark === "rise" ? "#" : "b")}</sup>
  1278. {data.activeTone.key}
  1279. </div>
  1280. {data.activeTone.step < 0 ? <span class={styles.dot}></span> : null}
  1281. </div>
  1282. </div>
  1283. <img class={styles.arrowImg} src={icons.icon_arrow} />
  1284. </div>
  1285. ) : (
  1286. <div id="finger-note-1" class={[styles.btnGr, styles.btnGrToggleBtn]} onClick={() => (data.tnoteShow = true)}>
  1287. <div>
  1288. <div class={styles.name}>
  1289. <div>
  1290. <sup>{data.activeTone.mark && (data.activeTone.mark === "rise" ? "#" : "b")}</sup>
  1291. {data.activeTone.name}
  1292. </div>
  1293. </div>
  1294. <img class={styles.arrowImg} src={icons.icon_arrow} />
  1295. </div>
  1296. </div>
  1297. )}
  1298. </>
  1299. )}
  1300. {
  1301. instrumentTranstion.value &&
  1302. <div class={[styles.btnGr]} onClick={() => resetElement()}>
  1303. <img src={icons.icon_2_0} />
  1304. <span>还原</span>
  1305. </div>
  1306. }
  1307. </div>
  1308. </div>
  1309. </>)
  1310. }
  1311. </Tab>
  1312. })
  1313. }
  1314. </Tabs>
  1315. </div>
  1316. ):
  1317. (
  1318. <>
  1319. <div
  1320. class={styles.notes}
  1321. style={{
  1322. paddingLeft: data.paddingLeft ? data.paddingLeft : "",
  1323. }}
  1324. >
  1325. {playAction.listenTipsStatus && <div class={[styles.tipsT, data.fingeringMode === "fingeringMode" ? styles.playTips2 : styles.playTips]}></div>}
  1326. {playAction.userAnswerStatus === 1 && <div class={[styles.tipsT, styles.playSuccess]}></div>}
  1327. {playAction.userAnswerStatus === 2 && <div class={[styles.tipsT, styles.playError]}></div>}
  1328. {playAction.resetAction && <div class={[styles.tipsT, styles.playTips5]}></div>}
  1329. {((data.noteType !== "#c" && (orientationDirection.value === 0 || (orientationDirection.value === 1 && state.platform === IPlatform.PC))) || (orientationDirection.value === 1 && state.platform === IPlatform.APP)) && (
  1330. <Button
  1331. class={styles.noteBtn}
  1332. onClick={(e: any) => {
  1333. e.stopPropagation();
  1334. scrollNoteBox("left");
  1335. }}
  1336. >
  1337. <Icon name="arrow-left" />
  1338. </Button>
  1339. )}
  1340. <div
  1341. class={[styles.noteContent, data.fingeringMode !== "scaleMode" && orientationDirection.value === 0 && styles.noteContentOther, browsInfo.ios ? "" : styles.noteContentWrap, data.huaweiPad && styles.huaweiPad]}
  1342. onClick={(e: any) => {
  1343. e.stopPropagation();
  1344. }}
  1345. >
  1346. {/* 判断是否为音阶模式 */}
  1347. {data.fingeringMode !== "scaleMode" && (
  1348. <div draggable={false} class={styles.note} onClick={noteChangeShow}>
  1349. <img draggable={false} src={data.noteType === "all" ? icons.icon_btn_orange : icons.icon_btn_orange2} />
  1350. </div>
  1351. )}
  1352. {/* [styles.noteContent, browsInfo.ios ? "" : styles.noteContentWrap, data.huaweiPad && styles.huaweiPad] */}
  1353. <div class={styles.lastNoteContent}>
  1354. <div ref={noteBoxRef} class={styles.noteBox}>
  1355. {data.notes.map((note: IFIGNER_INSTRUMENT_Note, index: number) => {
  1356. const steps = new Array(Math.abs(note.step)).fill(1);
  1357. return (
  1358. <div
  1359. id={index == 0 ? "finger-note-0" : ""}
  1360. draggable={false}
  1361. class={[styles.note, "note-class"]}
  1362. key={note.realKey}
  1363. onClick={async () => {
  1364. // 判断是否在播放音阶
  1365. if (playStatus.gamut) return;
  1366. if (playAction.listenLock) return;
  1367. if (playAction.showAnswerLoading) return;
  1368. if (playStatus.action) {
  1369. playAction.userAnswer = note;
  1370. // 判断用户答题
  1371. const userResult = note.realKey === playAction.standardAnswer.realKey ? 1 : 2;
  1372. playAction.userAnswerStatus = userResult;
  1373. playAction.listenLock = true;
  1374. data.realKey = note.realKey;
  1375. await fingeringPlay(note, 1000);
  1376. resetMode(userResult === 1 ? true : false, 0);
  1377. data.realKey = 0;
  1378. // 如果是指法模式显示完之后要还原
  1379. if (data.fingeringMode === "fingeringMode" && userResult === 2) {
  1380. // 延迟显示,因为重置的时候有一个异步操作
  1381. setTimeout(() => {
  1382. data.realKey = playAction.standardAnswer.realKey;
  1383. }, 10);
  1384. }
  1385. playAction.listenLock = false;
  1386. } else {
  1387. noteClick(note);
  1388. }
  1389. }}
  1390. >
  1391. <img draggable={false} src={resultImg(note).icon} />
  1392. {playStatus.action && ((playAction.showAnswerLoading && playAction.standardAnswer.realKey === note.realKey) || (playAction.userAnswerStatus === 1 && playAction.userAnswer.realKey === note.realKey)) ? <span class={styles.showAnswer}></span> : ""}
  1393. {playStatus.action && playAction.userAnswerStatus === 2 && playAction.userAnswer.realKey === note.realKey ? <span class={[styles.showAnswer, styles.errorAnswer]}></span> : ""}
  1394. <div
  1395. class={[
  1396. styles.noteKey,
  1397. ((data.realKey === note.realKey && !playStatus.action) ||
  1398. (playStatus.action && playAction.exampleAnser.realKey === note.realKey) ||
  1399. (playStatus.action && ((playAction.showAnswerLoading && playAction.standardAnswer.realKey === note.realKey) || (playAction.userAnswerStatus === 1 && playAction.userAnswer.realKey === note.realKey))) ||
  1400. (playStatus.action && playAction.userAnswerStatus === 2 && playAction.userAnswer.realKey === note.realKey)) &&
  1401. styles.keyActive,
  1402. ]}
  1403. >
  1404. {/* 显示对应的点 */}
  1405. {note.step > 0 ? steps.map((n: any) => <span class={styles.dot}></span>) : null}
  1406. <div class={styles.noteName}>
  1407. <sup>{note.mark && (note.mark === "rise" ? "#" : "b")}</sup>
  1408. {note.key}
  1409. </div>
  1410. {/* 显示对应的点 */}
  1411. {note.step < 0 ? steps.map((n: any) => <span class={styles.dot}></span>) : null}
  1412. </div>
  1413. </div>
  1414. );
  1415. })}
  1416. </div>
  1417. </div>
  1418. </div>
  1419. {((data.noteType !== "#c" && (orientationDirection.value === 0 || (orientationDirection.value === 1 && state.platform === IPlatform.PC))) || (orientationDirection.value === 1 && state.platform === IPlatform.APP)) && (
  1420. <Button
  1421. class={styles.noteBtn}
  1422. onClick={(e: any) => {
  1423. e.stopPropagation();
  1424. scrollNoteBox("right");
  1425. }}
  1426. >
  1427. <Icon name="arrow" />
  1428. </Button>
  1429. )}
  1430. </div>
  1431. {data.fingeringMode !== "scaleMode" && (
  1432. <div
  1433. class={styles.optionBtns}
  1434. onClick={(e: any) => {
  1435. e.stopPropagation();
  1436. }}
  1437. >
  1438. <Button class={[styles.oBtn, styles.gamut, playStatus.action && styles.disabled]} round onClick={onGamutPlayOrPause}>
  1439. {playStatus.gamut ? "暂停" : "播放音阶"}
  1440. </Button>
  1441. <Button class={[styles.oBtn, styles.play, playStatus.gamut && styles.disabled]} round onClick={onActionPlay}>
  1442. {listenText.value}
  1443. </Button>
  1444. <Button class={[styles.oBtn, styles.success, !playStatus.answer && styles.disabled]} round onClick={onShowAnswer}>
  1445. 显示答案
  1446. </Button>
  1447. </div>
  1448. )}
  1449. </>
  1450. )
  1451. }
  1452. </div>
  1453. {/* 老师端过来隐藏 */}
  1454. {
  1455. query.platform!=='pc'&&(
  1456. <div
  1457. class={styles.fixedRightBtns}
  1458. style={{
  1459. paddingTop: data.paddingTop ? data.paddingTop : "",
  1460. paddingLeft: data.paddingLeft ? data.paddingLeft : "",
  1461. }}
  1462. onClick={(e: any) => {
  1463. e.stopPropagation();
  1464. }}
  1465. >
  1466. <div class={styles.rightBtn}>
  1467. {data.subject !== "melodica" && data.fingeringMode === "scaleMode" && (
  1468. <div
  1469. class={styles.baseBtn}
  1470. onClick={() => {
  1471. data.viewIndex++;
  1472. if (data.viewIndex > data.viewTotal) {
  1473. if (["pan-flute", "ocarina", "whistling"].includes(data.subject)) {
  1474. data.viewIndex = 1;
  1475. } else {
  1476. data.viewIndex = 0;
  1477. }
  1478. }
  1479. getFingeringData();
  1480. }}
  1481. >
  1482. <img src={icons.icon_toggle} />
  1483. <span>视图</span>
  1484. </div>
  1485. )}
  1486. <div
  1487. class={styles.baseBtn}
  1488. onClick={() => {
  1489. resetElement();
  1490. data.tipShow = !data.tipShow;
  1491. }}
  1492. >
  1493. <img src={icons.icon_2_1} />
  1494. <span>说明</span>
  1495. </div>
  1496. {!!data.tones.length && data.fingeringMode === "scaleMode" && (
  1497. <>
  1498. {fingerData.fingeringInfo.name == "hulusi-flute" ? (
  1499. <div id="finger-note-1" class={[styles.baseBtn, styles.toggleBtnhulusi, styles.active]} onClick={() => (data.tnoteShow = true)}>
  1500. <div>
  1501. 全按作
  1502. <div class={[styles.noteKey, styles.noteKeyBtn]}>
  1503. {data.activeTone.step > 0 ? <span class={styles.dot}></span> : null}
  1504. <span class={styles.dot}></span>
  1505. <div class={styles.noteName}>
  1506. <sup>{data.activeTone.mark && (data.activeTone.mark === "rise" ? "#" : "b")}</sup>
  1507. {data.activeTone.key}
  1508. </div>
  1509. {data.activeTone.step < 0 ? <span class={styles.dot}></span> : null}
  1510. </div>
  1511. </div>
  1512. <img src={icons.icon_arrow} />
  1513. </div>
  1514. ) : (
  1515. <div id="finger-note-1" class={[styles.baseBtn, styles.toggleBtnhulusi2, styles.active]} onClick={() => (data.tnoteShow = true)}>
  1516. <div class={styles.oterhD}>
  1517. <div>
  1518. <div style={{ marginTop: "-4px" }}>
  1519. <sup>{data.activeTone.mark && (data.activeTone.mark === "rise" ? "#" : "b")}</sup>
  1520. {data.activeTone.name}
  1521. </div>
  1522. </div>
  1523. <img src={icons.icon_arrow} />
  1524. </div>
  1525. </div>
  1526. )}
  1527. </>
  1528. )}
  1529. </div>
  1530. <div class={[styles.baseBtn, !instrumentTranstion.value && styles.resetBtn]} style={{ marginTop: "8px" }} onClick={() => resetElement()}>
  1531. <img src={icons.icon_2_0} />
  1532. <span>还原</span>
  1533. </div>
  1534. </div>
  1535. )
  1536. }
  1537. {/* 老师端加上遮罩点击关闭 */}
  1538. {
  1539. query.platform==='pc'&&data.tipShow&&(
  1540. <div class={[styles.tipsOverlay,data.tipShow?styles.tipsOverlayBg:'']} onClick={()=>{data.tipShow=false}}></div>
  1541. )
  1542. }
  1543. <div class={[styles.tips, data.loadingDom ? styles.hiddens : "", data.tipShow ? "" : styles.tipHidden,query.platform==='pc'&&data.tipShow?styles.tipsPcBg:""]}>
  1544. <div class={styles.tipTitle}>
  1545. <div class={styles.tipTitleName}>{fingerData.fingeringInfo.code}使用说明</div>
  1546. <Button
  1547. class={styles.tipClose}
  1548. onClick={(e: any) => {
  1549. e.stopPropagation();
  1550. data.tipShow = false;
  1551. }}
  1552. >
  1553. <Icon name="cross" size={19} color="#fff" />
  1554. </Button>
  1555. </div>
  1556. <div class={styles.iconBook}></div>
  1557. <div class={styles.tipContentbox}>
  1558. <div class={styles.tipContent}>
  1559. {data.tips.map((tip, tipIndex) => (
  1560. <div class={styles.tipItem}>
  1561. <div class={styles.iconWrap}>
  1562. <div class={styles.tipItemIcon}>{tipIndex + 1}</div>
  1563. </div>
  1564. <div>
  1565. {tip.name}: {tip.realName}
  1566. </div>
  1567. </div>
  1568. ))}
  1569. </div>
  1570. </div>
  1571. </div>
  1572. {data.loadingSoundFonts && (
  1573. <div class={styles.loading}>
  1574. <div class={styles.loadingWrap}>
  1575. <img class={styles.loadingIcon} src={icon_loading_img} />
  1576. <Progress percentage={data.loadingSoundProgress} />
  1577. <div class={styles.loadingTip}>加载中,请稍后…</div>
  1578. </div>
  1579. </div>
  1580. )}
  1581. </div>
  1582. <Popup class={["tonePopup",tnoteShowBoxClass]} style={
  1583. query.platform==="pc" ? tnoteShowBoxDragData.styleDrag.value : {}
  1584. }
  1585. v-model:show={data.tnoteShow}
  1586. position={state.platform === IPlatform.PC?"center":!query.modelType && fingerData.fingeringInfo.orientation === 1 ? "bottom" : "right"}>
  1587. <div class={styles.tones}>
  1588. <div class={[styles.toneTitle,"toneTitle_pc"]}>
  1589. <div class={styles.tipTitleName}>移调</div>
  1590. <Button
  1591. class={styles.tipClose}
  1592. onClick={(e: any) => {
  1593. e.stopPropagation();
  1594. data.tnoteShow = false;
  1595. }}
  1596. >
  1597. <Icon name="cross" size={19} color="#fff" />
  1598. </Button>
  1599. </div>
  1600. <div class={[styles.tipContentbox,"tipContentbox_pc"]}>
  1601. <div class={[styles.tipContent,"tipContent_pc"]}>
  1602. <div class={[styles.tipWrap,"tipWrap_pc"]}>
  1603. <Space size={0} class={styles.toneContent}>
  1604. {data.tones.map((tone: IFIGNER_INSTRUMENT_Note) => {
  1605. const steps = new Array(Math.abs(tone.step)).fill(1);
  1606. return (
  1607. <Button
  1608. class={[fingerData.fingeringInfo.name == "hulusi-flute" && styles.hulusiBtn]}
  1609. round
  1610. plain
  1611. type={data.popupActiveTone.realName === tone.realName ? "primary" : "default"}
  1612. onClick={(e: any) => {
  1613. e.stopPropagation();
  1614. data.popupActiveTone = tone;
  1615. setNotes();
  1616. }}
  1617. >
  1618. {fingerData.fingeringInfo.name == "hulusi-flute" ? (
  1619. <div style={{ display: "flex", alignItems: "center" }}>
  1620. 全按作
  1621. <div class={[styles.noteKey, styles.hulusiNoteKey]}>
  1622. {tone.step > 0 ? <span class={styles.dot}></span> : null}
  1623. <div class={styles.noteName} style={{ fontSize: "0.25rem" }}>
  1624. <sup>{tone.mark && (tone.mark === "rise" ? "#" : "b")}</sup>
  1625. {tone.key}
  1626. </div>
  1627. {tone.step < 0 ? <span class={styles.dot}></span> : null}
  1628. </div>
  1629. </div>
  1630. ) : (
  1631. <div class={styles.noteName}>
  1632. <sup>{tone.mark && (tone.mark === "rise" ? "#" : "b")}</sup>
  1633. {tone.name}
  1634. </div>
  1635. )}
  1636. </Button>
  1637. );
  1638. })}
  1639. </Space>
  1640. </div>
  1641. <div class={[styles.toneAction,"toneAction_pc"]}>
  1642. <img
  1643. onClick={(e: any) => {
  1644. e.stopPropagation();
  1645. data.tnoteShow = false;
  1646. }}
  1647. src={icons.icon_action_cancel}
  1648. />
  1649. <img
  1650. onClick={(e: any) => {
  1651. e.stopPropagation();
  1652. data.activeTone = data.popupActiveTone;
  1653. setNotes();
  1654. data.tnoteShow = false;
  1655. }}
  1656. src={icons.icon_action_confirm}
  1657. />
  1658. </div>
  1659. </div>
  1660. </div>
  1661. </div>
  1662. {query.platform==="pc" && <>
  1663. <div class={[styles.dragTopBox,"dragTopBox"]}></div>
  1664. <div class={[styles.dragbomBox,"dragbomBox"]}>
  1665. <Dragbom></Dragbom>
  1666. </div>
  1667. </>}
  1668. </Popup>
  1669. <Popup
  1670. style={
  1671. query.platform==="pc" ? changeSubjectShowBoxDragData.styleDrag.value : {}
  1672. }
  1673. v-model:show={data.changeSubjectShow}
  1674. class={[styles.changeSubjectPopup,changeSubjectShowBoxClass]}
  1675. onClick={(e: any) => {
  1676. e.stopPropagation();
  1677. }}
  1678. >
  1679. <ChangeSubject
  1680. subjectList={data.subjects}
  1681. subject={data.subject}
  1682. onClose={() => (data.changeSubjectShow = false)}
  1683. onConfirm={(code: any) => {
  1684. if (data.subject === code) {
  1685. data.changeSubjectShow = false;
  1686. return;
  1687. }
  1688. const originalSubject = JSON.parse(JSON.stringify(data.subject));
  1689. data.subject = code;
  1690. data.viewIndex = 0;
  1691. data.tipShow = false;
  1692. data.loadingDom = true;
  1693. fingerData.fingeringInfo = subjectFingering(data.subject);
  1694. data.activeTone = {} as any;
  1695. resetElement();
  1696. resetMode(true, 0);
  1697. // api_setRequestedOrientation(orientationDirection.value);
  1698. data.changeSubjectShow = false;
  1699. // 设置屏幕方向
  1700. setTimeout(() => {
  1701. const before = ["hulusi-flute", "piccolo", "baroque-recorder"].includes(originalSubject) ? 0 : 0;
  1702. if (orientationDirection.value !== before) {
  1703. data.paddingTop = "";
  1704. data.paddingLeft = "";
  1705. }
  1706. __init();
  1707. }, 100);
  1708. }}
  1709. />
  1710. {query.platform==="pc" && <>
  1711. <div class={[styles.dragTopBox,"dragTopBox"]}></div>
  1712. <div class={[styles.dragbomBox,"dragbomBox"]}>
  1713. <Dragbom></Dragbom>
  1714. </div>
  1715. </>}
  1716. </Popup>
  1717. {props.show && !data.loading && !data.loadingSoundFonts && <GuideIndex fingeringMode={data.fingeringMode} showGuide={false} list={["finger"]} />}
  1718. </div>
  1719. );
  1720. };
  1721. },
  1722. });