index.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575
  1. import {
  2. PropType,
  3. computed,
  4. defineComponent,
  5. nextTick,
  6. onBeforeMount,
  7. onMounted,
  8. onUnmounted,
  9. reactive,
  10. ref,
  11. } from "vue";
  12. import styles from "./index.module.less";
  13. import icons from "./image/icons.json";
  14. import { FIGNER_INSTRUMENT_DATA, IFIGNER_INSTRUMENT_Note } from "/src/view/figner-preview";
  15. import {
  16. ITypeFingering,
  17. IVocals,
  18. getFingeringConfig,
  19. mappingVoicePart,
  20. subjectFingering,
  21. } from "/src/view/fingering/fingering-config";
  22. import { Howl } from "howler";
  23. import { storeData } from "/src/store";
  24. import { api_back, api_setRequestedOrientation } from "/src/helpers/communication";
  25. import Hammer from "hammerjs";
  26. import { Button, Icon, Loading, Popup, Progress, Space } from "vant";
  27. import GuideIndex from "./guide/guide-index";
  28. import { getQuery } from "/src/utils/queryString";
  29. import { browser } from "/src/utils";
  30. import { usePageVisibility } from "@vant/use";
  31. import { watch } from "vue";
  32. import icon_loading_img from "./image/icon_loading_img.png";
  33. export default defineComponent({
  34. name: "viewFigner",
  35. emits: ["close"],
  36. props: {
  37. isComponent: {
  38. type: Boolean,
  39. default: false,
  40. },
  41. subject: {
  42. type: String as PropType<IVocals>,
  43. default: "",
  44. },
  45. },
  46. setup(props, { emit }) {
  47. const query = getQuery();
  48. const browsInfo = browser();
  49. const code = mappingVoicePart(query.code, "INSTRUMENT");
  50. const subject = props.isComponent ? props.subject || "pan-flute" : code || "pan-flute";
  51. const data = reactive({
  52. loading: true,
  53. subject: subject as any,
  54. realKey: 0,
  55. notes: [] as IFIGNER_INSTRUMENT_Note[],
  56. tones: [] as IFIGNER_INSTRUMENT_Note[],
  57. activeTone: {} as IFIGNER_INSTRUMENT_Note,
  58. popupActiveTone: {} as IFIGNER_INSTRUMENT_Note,
  59. activeToneName: "",
  60. soundFonts: {} as any,
  61. viewIndex: 0,
  62. noteAudio: null as unknown as Howl,
  63. transform: {
  64. scale: 1,
  65. x: 0,
  66. y: 0,
  67. startScale: 1,
  68. startX: 0,
  69. startY: 0,
  70. transition: "",
  71. },
  72. tipShow: false,
  73. tips: [] as IFIGNER_INSTRUMENT_Note[],
  74. tnoteShow: false,
  75. loadingSoundFonts: true,
  76. loadingSoundProgress: 0,
  77. huaweiPad: navigator?.userAgent?.includes("UAWEIVRD-W09") ? true : false,
  78. });
  79. const fingerData = reactive({
  80. relationshipIndex: 0,
  81. subject: null as unknown as ITypeFingering,
  82. fingeringInfo: subjectFingering(data.subject),
  83. });
  84. if (!props.isComponent) {
  85. if (fingerData.fingeringInfo.orientation === 0) {
  86. api_setRequestedOrientation(fingerData.fingeringInfo.orientation);
  87. }
  88. }
  89. const getNotes = () => {
  90. const fignerData = FIGNER_INSTRUMENT_DATA[data.subject as keyof typeof FIGNER_INSTRUMENT_DATA];
  91. if (fignerData) {
  92. data.tones = fignerData.tones || [];
  93. if (data.tones.length) {
  94. data.activeTone = data.tones[0];
  95. data.popupActiveTone = data.tones[0];
  96. }
  97. data.tips = fignerData.tips || [];
  98. setNotes();
  99. setTimeout(() => {
  100. data.loading = false;
  101. }, 600);
  102. }
  103. };
  104. const setNotes = () => {
  105. const fignerData = FIGNER_INSTRUMENT_DATA[data.subject as keyof typeof FIGNER_INSTRUMENT_DATA];
  106. if (fignerData) {
  107. data.notes = fignerData[`list${data.activeTone.realName || ""}`];
  108. }
  109. };
  110. const getFingeringData = async () => {
  111. const subject: any = data.subject + (data.viewIndex === 0 ? "" : data.viewIndex);
  112. console.log("🚀 ~ subject:", subject);
  113. fingerData.subject = await getFingeringConfig(subject);
  114. };
  115. const createAudio = (url: string) => {
  116. return new Promise((resolve) => {
  117. const noteAudio = new Howl({
  118. src: url,
  119. loop: true,
  120. onload: () => {
  121. resolve(noteAudio);
  122. },
  123. });
  124. });
  125. };
  126. const getSounFonts = async () => {
  127. const pathname = /(192|localhost)/.test(location.origin) ? "/" : location.pathname;
  128. data.loadingSoundFonts = true;
  129. data.loadingSoundProgress = 0;
  130. for (let i = 0; i < data.notes.length; i++) {
  131. const note = data.notes[i];
  132. // console.log("🚀 ~ note:", i);
  133. let url = `${pathname}soundfonts/${data.subject}/`;
  134. url += note.realName;
  135. url += ".mp3";
  136. data.soundFonts[note.realKey] = await createAudio(url);
  137. data.loadingSoundProgress = Math.floor(((i + 1) / data.notes.length) * 100);
  138. }
  139. setTimeout(() => {
  140. data.loadingSoundProgress = 100;
  141. data.loadingSoundFonts = false;
  142. }, 500);
  143. // console.log("🚀 ~ data.soundFonts:", data.soundFonts);
  144. };
  145. onBeforeMount(() => {
  146. getNotes();
  147. getFingeringData();
  148. getSounFonts();
  149. });
  150. const noteClick = (item: IFIGNER_INSTRUMENT_Note) => {
  151. if (data.noteAudio) {
  152. data.noteAudio.stop();
  153. if (data.realKey === item.realKey) {
  154. data.realKey = 0;
  155. data.noteAudio = null as unknown as Howl;
  156. return;
  157. }
  158. }
  159. data.realKey = item.realKey;
  160. data.noteAudio = data.soundFonts[item.realKey];
  161. data.noteAudio.play();
  162. };
  163. const handleStop = () => {
  164. if (data.noteAudio) {
  165. data.noteAudio.stop();
  166. data.realKey = 0;
  167. data.noteAudio = null as unknown as Howl;
  168. }
  169. };
  170. /** 返回 */
  171. const handleBack = () => {
  172. handleStop();
  173. if (props.isComponent) {
  174. console.log("关闭");
  175. emit("close");
  176. return;
  177. } else {
  178. if (fingerData.fingeringInfo.orientation === 0) {
  179. api_setRequestedOrientation(1);
  180. }
  181. }
  182. // 不在APP中,
  183. if (!storeData.isApp) {
  184. window.close();
  185. return;
  186. }
  187. api_back();
  188. };
  189. onMounted(() => {
  190. loadElement();
  191. });
  192. const loadElement = () => {
  193. const fingeringContainer = document.getElementById("fingeringContainer");
  194. // console.log("🚀 ~ fingeringContainer:", fingeringContainer);
  195. const mc = new Hammer.Manager(fingeringContainer as HTMLElement);
  196. mc.add(new Hammer.Pan({ threshold: 0, pointers: 0 }));
  197. mc.add(new Hammer.Pinch({ threshold: 0 })).recognizeWith([mc.get("pan")]);
  198. // mc.get("pan").set({ direction: Hammer.DIRECTION_ALL });
  199. // mc.get("pinch").set({ enable: true });
  200. mc.on("panstart pinchstart", function (ev) {
  201. data.transform.transition = "";
  202. });
  203. mc.on("panmove pinchmove", function (ev) {
  204. if (ev.type === "pinchmove") {
  205. // console.log("🚀 ~ ev:", ev.type, ev.scale, ev.deltaX, ev.deltaY);
  206. data.transform.scale = ev.scale * data.transform.startScale;
  207. data.transform.x = data.transform.startX + ev.deltaX;
  208. data.transform.y = data.transform.startY + ev.deltaY;
  209. }
  210. if (ev.type === "panmove") {
  211. // console.log("🚀 ~ ev:", ev.type, ev.deltaX, ev.deltaY);
  212. data.transform.x = data.transform.startX + ev.deltaX;
  213. data.transform.y = data.transform.startY + ev.deltaY;
  214. }
  215. });
  216. //
  217. mc.on("hammer.input", function (ev) {
  218. // console.log("🚀 ~ ev:", ev.type, ev.isFinal);
  219. if (ev.isFinal) {
  220. data.transform.startScale = data.transform.scale;
  221. data.transform.startX = data.transform.x;
  222. data.transform.startY = data.transform.y;
  223. }
  224. });
  225. };
  226. const resetElement = () => {
  227. data.transform.transition = "all 0.3s";
  228. nextTick(() => {
  229. data.transform.scale = 1;
  230. data.transform.x = 0;
  231. data.transform.y = 0;
  232. data.transform.startScale = 1;
  233. data.transform.startX = 0;
  234. data.transform.startY = 0;
  235. });
  236. };
  237. const pageVisible = usePageVisibility();
  238. watch(
  239. () => pageVisible.value,
  240. (val) => {
  241. if (val === "hidden") {
  242. console.log("页面隐藏停止播放");
  243. handleStop();
  244. }
  245. }
  246. );
  247. /** 课件播放 */
  248. const changePlay = (res: any) => {
  249. if (res?.data?.api === "setPlayState") {
  250. handleStop();
  251. }
  252. };
  253. const noteBoxRef = ref();
  254. const scrollNoteBox = (type: "left" | "right") => {
  255. const width = noteBoxRef.value.offsetWidth / 2;
  256. (noteBoxRef.value as unknown as HTMLElement).scrollBy({
  257. left: type === "left" ? -width : width,
  258. behavior: "smooth",
  259. });
  260. };
  261. /** 滚轮缩放 */
  262. const handleWheel = (e: WheelEvent) => {
  263. e.preventDefault();
  264. if (e.deltaY > 0) {
  265. data.transform.scale -= 0.1;
  266. if (data.transform.scale <= 0.5) {
  267. data.transform.scale = 0.5;
  268. }
  269. } else {
  270. data.transform.scale += 0.1;
  271. if (data.transform.scale >= 2) {
  272. data.transform.scale = 2;
  273. }
  274. }
  275. };
  276. onMounted(() => {
  277. window.addEventListener("message", changePlay);
  278. const fingeringContainer = document.getElementById("fingeringContainer");
  279. fingeringContainer?.addEventListener("wheel", handleWheel);
  280. });
  281. onUnmounted(() => {
  282. window.removeEventListener("message", changePlay);
  283. const fingeringContainer = document.getElementById("fingeringContainer");
  284. fingeringContainer?.removeEventListener("wheel", handleWheel);
  285. });
  286. return () => {
  287. const relationship = fingerData.subject?.relationship?.[data.realKey] || [];
  288. const rs: number[] = Array.isArray(relationship[1])
  289. ? relationship[fingerData.relationshipIndex]
  290. : relationship;
  291. const canTizhi = Array.isArray(relationship[1]);
  292. return (
  293. <div
  294. class={[
  295. styles.fingerBox,
  296. !query.modelType && fingerData.fingeringInfo.orientation === 1
  297. ? styles.fingerBottom
  298. : styles.fingerRight,
  299. ]}
  300. >
  301. <div class={[styles.head, styles.backHead]}>
  302. <div class={styles.left}>
  303. <button class={[styles.backBtn]} onClick={() => handleBack()}>
  304. <img src={icons.icon_back} />
  305. </button>
  306. </div>
  307. </div>
  308. <div class={styles.head}>
  309. <div class={styles.left}>
  310. {data.subject === "pan-flute" && (
  311. <div
  312. class={styles.baseBtn}
  313. onClick={() => {
  314. data.viewIndex++;
  315. if (data.viewIndex > 2) {
  316. data.viewIndex = 0;
  317. }
  318. getFingeringData();
  319. }}
  320. >
  321. 切换视图
  322. </div>
  323. )}
  324. </div>
  325. <div class={styles.rightBtn}>
  326. <div class={[styles.item]} onClick={() => resetElement()}>
  327. <img src={icons.icon_2_0} />
  328. <span>还原</span>
  329. </div>
  330. <div
  331. class={[styles.item]}
  332. onClick={() => {
  333. resetElement();
  334. data.tipShow = !data.tipShow;
  335. }}
  336. >
  337. <img src={icons.icon_2_1} />
  338. <span>使用说明</span>
  339. </div>
  340. </div>
  341. </div>
  342. <div class={styles.fingerContent}>
  343. <div class={styles.wrapFinger}>
  344. <div id="fingeringContainer" class={styles.boxFinger}>
  345. <div
  346. style={{
  347. transform: `translate3d(${data.transform.x}px,${data.transform.y}px,0px) scale(${data.transform.scale})`,
  348. transition: data.transform.transition,
  349. }}
  350. class={[styles.fingeringContainer]}
  351. >
  352. <div class={styles.imgs}>
  353. <img src={fingerData.subject?.json?.full} />
  354. {rs.map((key: number | string, index: number) => {
  355. const nk: string =
  356. typeof key === "string" ? key.replace("active-", "") : String(key);
  357. return <img data-index={nk} src={fingerData.subject?.json?.[nk]} />;
  358. })}
  359. <div
  360. id="finger-note-2"
  361. class={[styles.tizhi, canTizhi && styles.canDisplay]}
  362. onClick={() =>
  363. (fingerData.relationshipIndex = fingerData.relationshipIndex === 0 ? 1 : 0)
  364. }
  365. >
  366. 替指
  367. </div>
  368. </div>
  369. </div>
  370. </div>
  371. <div class={styles.notes}>
  372. <Button class={styles.noteBtn} onClick={() => scrollNoteBox("left")}>
  373. <Icon name="arrow-left" />
  374. </Button>
  375. <div
  376. class={[
  377. styles.noteContent,
  378. browsInfo.ios ? "" : styles.noteContentWrap,
  379. data.huaweiPad && styles.huaweiPad,
  380. ]}
  381. >
  382. <div ref={noteBoxRef} class={styles.noteBox}>
  383. {data.notes.map((note: IFIGNER_INSTRUMENT_Note, index: number) => {
  384. const steps = new Array(Math.abs(note.step)).fill(1);
  385. return (
  386. <div
  387. id={index == 0 ? "finger-note-0" : ""}
  388. draggable={false}
  389. class={styles.note}
  390. onClick={() => noteClick(note)}
  391. >
  392. {data.realKey === note.realKey ? (
  393. <img draggable={false} src={icons.icon_btn_ylow} />
  394. ) : (
  395. <img draggable={false} src={icons.icon_btn_blue} />
  396. )}
  397. <div
  398. class={[styles.noteKey, data.realKey === note.realKey && styles.keyActive]}
  399. >
  400. {note.step > 0 ? steps.map((n) => <span class={styles.dot}></span>) : null}
  401. <div class={styles.noteName}>
  402. <sup>{note.mark && (note.mark === "rise" ? "#" : "b")}</sup>
  403. {note.key}
  404. </div>
  405. {note.step < 0 ? steps.map((n) => <span class={styles.dot}></span>) : null}
  406. </div>
  407. </div>
  408. );
  409. })}
  410. </div>
  411. </div>
  412. <Button class={styles.noteBtn} onClick={() => scrollNoteBox("right")}>
  413. <Icon name="arrow" />
  414. </Button>
  415. </div>
  416. </div>
  417. <div class={[styles.tips, data.tipShow ? "" : styles.tipHidden]}>
  418. <div class={styles.tipTitle}>
  419. <div class={styles.tipTitleName}>{fingerData.fingeringInfo.code}使用说明</div>
  420. <Button class={styles.tipClose} onClick={() => (data.tipShow = false)}>
  421. <Icon name="cross" color="#999" />
  422. </Button>
  423. </div>
  424. <div class={styles.tipContent}>
  425. {data.tips.map((tip, tipIndex) => (
  426. <div class={styles.tipItem}>
  427. <div class={styles.iconWrap}>
  428. <div class={styles.tipItemIcon}>{tipIndex + 1}</div>
  429. </div>
  430. <div>
  431. {tip.name}: {tip.realName}
  432. </div>
  433. </div>
  434. ))}
  435. </div>
  436. </div>
  437. {data.loadingSoundFonts && (
  438. <div class={styles.loading}>
  439. <div class={styles.loadingWrap}>
  440. <img class={styles.loadingIcon} src={icon_loading_img} />
  441. <Progress percentage={data.loadingSoundProgress} />
  442. <div class={styles.loadingTip}>加载中,请稍后…</div>
  443. </div>
  444. </div>
  445. )}
  446. </div>
  447. {!!data.tones.length && (
  448. <>
  449. {fingerData.fingeringInfo.name == "hulusi-flute" ? (
  450. <div
  451. id="finger-note-1"
  452. class={[styles.toggleBtn, styles.toggleBtnhulusi]}
  453. onClick={() => (data.tnoteShow = true)}
  454. >
  455. <div>
  456. 全按作
  457. <div class={[styles.noteKey, styles.hulusiNoteKey]}>
  458. {data.activeTone.step > 0 ? <span class={styles.dot}></span> : null}
  459. <div class={styles.noteName}>
  460. <sup>
  461. {data.activeTone.mark && (data.activeTone.mark === "rise" ? "#" : "b")}
  462. </sup>
  463. {data.activeTone.key}
  464. </div>
  465. {data.activeTone.step < 0 ? <span class={styles.dot}></span> : null}
  466. </div>
  467. </div>
  468. <img src={icons.icon_arrow} />
  469. </div>
  470. ) : (
  471. <div id="finger-note-1" class={styles.toggleBtn} onClick={() => (data.tnoteShow = true)}>
  472. <div>
  473. <sup>{data.activeTone.mark && (data.activeTone.mark === "rise" ? "#" : "b")}</sup>
  474. {data.activeTone.name}
  475. </div>
  476. <img src={icons.icon_arrow} />
  477. </div>
  478. )}
  479. </>
  480. )}
  481. <Popup
  482. class="tonePopup"
  483. v-model:show={data.tnoteShow}
  484. position={
  485. !query.modelType && fingerData.fingeringInfo.orientation === 1 ? "bottom" : "right"
  486. }
  487. >
  488. <div class={styles.tones}>
  489. <div class={styles.toneTitle}>
  490. <div class={styles.tipTitleName}>移调</div>
  491. <Button class={styles.tipClose} onClick={() => (data.tnoteShow = false)}>
  492. <Icon name="cross" color="#999" />
  493. </Button>
  494. </div>
  495. <div style={{ flex: 1, overflow: "hidden" }}>
  496. <Space size={0} class={styles.toneContent}>
  497. {data.tones.map((tone: IFIGNER_INSTRUMENT_Note) => {
  498. const steps = new Array(Math.abs(tone.step)).fill(1);
  499. return (
  500. <Button
  501. round
  502. plain
  503. type={data.popupActiveTone.realName === tone.realName ? "primary" : "default"}
  504. onClick={() => {
  505. data.popupActiveTone = tone;
  506. setNotes();
  507. }}
  508. >
  509. {fingerData.fingeringInfo.name == "hulusi-flute" ? (
  510. <div style={{ display: "flex", alignItems: "center" }}>
  511. 全按作
  512. <div class={[styles.noteKey, styles.hulusiNoteKey]}>
  513. {tone.step > 0 ? <span class={styles.dot}></span> : null}
  514. <div class={styles.noteName} style={{ fontSize: "0.25rem" }}>
  515. <sup>{tone.mark && (tone.mark === "rise" ? "#" : "b")}</sup>
  516. {tone.key}
  517. </div>
  518. {tone.step < 0 ? <span class={styles.dot}></span> : null}
  519. </div>
  520. </div>
  521. ) : (
  522. <div class={styles.noteName}>
  523. <sup>{tone.mark && (tone.mark === "rise" ? "#" : "b")}</sup>
  524. {tone.name}
  525. </div>
  526. )}
  527. </Button>
  528. );
  529. })}
  530. </Space>
  531. </div>
  532. <Space size={0} class={styles.toneAction}>
  533. <Button type="primary" round plain onClick={() => (data.tnoteShow = false)}>
  534. 取消
  535. </Button>
  536. <Button
  537. type="primary"
  538. round
  539. onClick={() => {
  540. data.activeTone = data.popupActiveTone;
  541. setNotes();
  542. data.tnoteShow = false;
  543. }}
  544. >
  545. 确定
  546. </Button>
  547. </Space>
  548. </div>
  549. </Popup>
  550. {!data.loading && !data.loadingSoundFonts && <GuideIndex list={["finger"]} />}
  551. </div>
  552. );
  553. };
  554. },
  555. });