index.tsx 18 KB

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