index.tsx 17 KB

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