index.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. import { PropType, computed, defineComponent, nextTick, onBeforeMount, onMounted, reactive } from "vue";
  2. import styles from "./index.module.less";
  3. import icons from "./image/icons.json";
  4. import { FIGNER_INSTRUMENT_DATA, IFIGNER_INSTRUMENT_Note } from "/src/view/figner-preview";
  5. import {
  6. ITypeFingering,
  7. IVocals,
  8. getFingeringConfig,
  9. subjectFingering,
  10. } from "/src/view/fingering/fingering-config";
  11. import { Howl } from "howler";
  12. import { storeData } from "/src/store";
  13. import { api_back } from "/src/helpers/communication";
  14. import Hammer from "hammerjs";
  15. import { Button, Icon, Popup, Space } from "vant";
  16. import GuideIndex from "./guide/guide-index";
  17. import { getQuery } from "/src/utils/queryString";
  18. export default defineComponent({
  19. name: "viewFigner",
  20. emits: ["close"],
  21. props: {
  22. isComponent: {
  23. type: Boolean,
  24. default: false,
  25. },
  26. subject: {
  27. type: String as PropType<IVocals>,
  28. default: "",
  29. },
  30. },
  31. setup(props, { emit }) {
  32. const query = getQuery();
  33. const subject = props.isComponent ? props.subject || "pan-flute" : query.code || "pan-flute";
  34. const data = reactive({
  35. loading: true,
  36. subject: subject,
  37. realKey: 0,
  38. notes: [] as IFIGNER_INSTRUMENT_Note[],
  39. tones: [] as IFIGNER_INSTRUMENT_Note[],
  40. activeTone: {} as IFIGNER_INSTRUMENT_Note,
  41. activeToneName: "",
  42. soundFonts: {} as any,
  43. viewIndex: 0,
  44. noteAudio: null as unknown as Howl,
  45. transform: {
  46. scale: 1,
  47. x: 0,
  48. y: 0,
  49. startScale: 1,
  50. startX: 0,
  51. startY: 0,
  52. transition: "",
  53. },
  54. tipShow: false,
  55. tips: [] as IFIGNER_INSTRUMENT_Note[],
  56. tnoteShow: false,
  57. });
  58. const fingerData = reactive({
  59. relationshipIndex: 0,
  60. subject: null as unknown as ITypeFingering,
  61. fingeringInfo: subjectFingering(data.subject),
  62. });
  63. const getNotes = () => {
  64. const fignerData = FIGNER_INSTRUMENT_DATA[data.subject as keyof typeof FIGNER_INSTRUMENT_DATA];
  65. if (fignerData) {
  66. data.tones = fignerData.tones || [];
  67. if (data.tones.length) {
  68. data.activeTone = data.tones[0];
  69. }
  70. data.tips = fignerData.tips || [];
  71. setNotes();
  72. setTimeout(() => {
  73. data.loading = false;
  74. }, 600);
  75. }
  76. };
  77. const setNotes = () => {
  78. const fignerData = FIGNER_INSTRUMENT_DATA[data.subject as keyof typeof FIGNER_INSTRUMENT_DATA];
  79. if (fignerData) {
  80. data.notes = fignerData[`list${data.activeTone.realName || ""}`];
  81. }
  82. };
  83. const getFingeringData = async () => {
  84. const subject: any = data.subject + (data.viewIndex === 0 ? "" : data.viewIndex);
  85. // console.log("🚀 ~ subject:", subject);
  86. fingerData.subject = await getFingeringConfig(subject);
  87. };
  88. const getSounFonts = () => {
  89. const pathname = /(192|localhost)/.test(location.origin) ? "/" : location.pathname;
  90. for (let i = 0; i < data.notes.length; i++) {
  91. const note = data.notes[i];
  92. // console.log("🚀 ~ note:", i)
  93. let url = `${pathname}soundfonts/${data.subject}/`;
  94. url += note.realName;
  95. url += ".mp3";
  96. const noteAudio = new Howl({
  97. src: url,
  98. loop: true,
  99. });
  100. data.soundFonts[note.realKey] = noteAudio;
  101. }
  102. // console.log("🚀 ~ data.soundFonts:", data.soundFonts);
  103. };
  104. onBeforeMount(() => {
  105. getNotes();
  106. getFingeringData();
  107. getSounFonts();
  108. });
  109. const noteClick = (item: IFIGNER_INSTRUMENT_Note) => {
  110. if (data.noteAudio) {
  111. data.noteAudio.stop();
  112. if (data.realKey === item.realKey) {
  113. data.realKey = 0;
  114. data.noteAudio = null as unknown as Howl;
  115. return;
  116. }
  117. }
  118. data.realKey = item.realKey;
  119. data.noteAudio = data.soundFonts[item.realKey];
  120. data.noteAudio.play();
  121. };
  122. /** 返回 */
  123. const handleBack = () => {
  124. if (data.noteAudio) {
  125. data.noteAudio.stop();
  126. data.realKey = 0;
  127. data.noteAudio = null as unknown as Howl;
  128. }
  129. if (props.isComponent) {
  130. console.log("关闭");
  131. emit("close");
  132. return;
  133. }
  134. // 不在APP中,
  135. if (!storeData.isApp) {
  136. window.close();
  137. return;
  138. }
  139. api_back();
  140. };
  141. onMounted(() => {
  142. loadElement();
  143. });
  144. const loadElement = () => {
  145. const fingeringContainer = document.getElementById("fingeringContainer");
  146. // console.log("🚀 ~ fingeringContainer:", fingeringContainer);
  147. const mc = new Hammer.Manager(fingeringContainer as HTMLElement);
  148. mc.add(new Hammer.Pan({ threshold: 0, pointers: 0 }));
  149. mc.add(new Hammer.Pinch({ threshold: 0 })).recognizeWith([mc.get("pan")]);
  150. // mc.get("pan").set({ direction: Hammer.DIRECTION_ALL });
  151. // mc.get("pinch").set({ enable: true });
  152. mc.on("panstart pinchstart", function (ev) {
  153. data.transform.transition = "";
  154. });
  155. mc.on("panmove pinchmove", function (ev) {
  156. if (ev.type === "pinchmove") {
  157. // console.log("🚀 ~ ev:", ev.type, ev.scale, ev.deltaX, ev.deltaY);
  158. data.transform.scale = ev.scale * data.transform.startScale;
  159. data.transform.x = data.transform.startX + ev.deltaX;
  160. data.transform.y = data.transform.startY + ev.deltaY;
  161. }
  162. if (ev.type === "panmove") {
  163. // console.log("🚀 ~ ev:", ev.type, ev.deltaX, ev.deltaY);
  164. data.transform.x = data.transform.startX + ev.deltaX;
  165. data.transform.y = data.transform.startY + ev.deltaY;
  166. }
  167. });
  168. //
  169. mc.on("hammer.input", function (ev) {
  170. // console.log("🚀 ~ ev:", ev.type, ev.isFinal);
  171. if (ev.isFinal) {
  172. data.transform.startScale = data.transform.scale;
  173. data.transform.startX = data.transform.x;
  174. data.transform.startY = data.transform.y;
  175. }
  176. });
  177. };
  178. const resetElement = () => {
  179. data.transform.transition = "all 0.3s";
  180. nextTick(() => {
  181. data.transform.scale = 1;
  182. data.transform.x = 0;
  183. data.transform.y = 0;
  184. data.transform.startScale = 1;
  185. data.transform.startX = 0;
  186. data.transform.startY = 0;
  187. });
  188. };
  189. return () => {
  190. const relationship = fingerData.subject?.relationship?.[data.realKey] || [];
  191. const rs: number[] = Array.isArray(relationship[1])
  192. ? relationship[fingerData.relationshipIndex]
  193. : relationship;
  194. const canTizhi = Array.isArray(relationship[1]);
  195. return (
  196. <div class={styles.fingerBox}>
  197. <div class={styles.head}>
  198. <div class={styles.left}>
  199. <button
  200. class={[styles.backBtn, data.subject === "pan-flute" && styles.backRight]}
  201. onClick={() => handleBack()}
  202. >
  203. <img src={icons.icon_back} />
  204. </button>
  205. {data.subject === "pan-flute" && (
  206. <div
  207. class={styles.baseBtn}
  208. onClick={() => {
  209. data.viewIndex++;
  210. if (data.viewIndex > 2) {
  211. data.viewIndex = 0;
  212. }
  213. getFingeringData();
  214. }}
  215. >
  216. 切换视图
  217. </div>
  218. )}
  219. </div>
  220. <div class={styles.rightBtn}>
  221. <div class={[styles.item]} onClick={() => resetElement()}>
  222. <img src={icons.icon_2_0} />
  223. <span>还原</span>
  224. </div>
  225. <div
  226. class={[styles.item]}
  227. onClick={() => {
  228. resetElement();
  229. data.tipShow = !data.tipShow;
  230. }}
  231. >
  232. <img src={icons.icon_2_1} />
  233. <span>使用说明</span>
  234. </div>
  235. </div>
  236. </div>
  237. <div
  238. class={[
  239. styles.fingerContent,
  240. !query.modelType && fingerData.fingeringInfo.orientation === 1 ? styles.fingerBottom : styles.fingerRight,
  241. ]}
  242. >
  243. <div class={styles.wrapFinger}>
  244. <div id="fingeringContainer" class={styles.boxFinger}>
  245. <div
  246. style={{
  247. transform: `translate3d(${data.transform.x}px,${data.transform.y}px,0px) scale(${data.transform.scale})`,
  248. transition: data.transform.transition,
  249. }}
  250. class={[styles.fingeringContainer]}
  251. >
  252. <div class={styles.imgs}>
  253. <img src={fingerData.subject?.json?.full} />
  254. {rs.map((key: number | string, index: number) => {
  255. const nk: string =
  256. typeof key === "string" ? key.replace("active-", "") : String(key);
  257. return <img data-index={nk} src={fingerData.subject?.json?.[nk]} />;
  258. })}
  259. </div>
  260. <div
  261. id="finger-note-2"
  262. class={[styles.tizhi, canTizhi && styles.canDisplay]}
  263. onClick={() =>
  264. (fingerData.relationshipIndex = fingerData.relationshipIndex === 0 ? 1 : 0)
  265. }
  266. >
  267. 替指
  268. </div>
  269. </div>
  270. </div>
  271. <div class={styles.notes}>
  272. <Button class={styles.noteBtn}>
  273. <Icon name="arrow-left" />
  274. </Button>
  275. <div class={styles.noteContent}>
  276. <div class={styles.noteBox}>
  277. {data.notes.map((note: IFIGNER_INSTRUMENT_Note, index: number) => {
  278. const steps = new Array(Math.abs(note.step)).fill(1);
  279. return (
  280. <div
  281. id={index == 0 ? "finger-note-0" : ""}
  282. draggable={false}
  283. class={styles.note}
  284. onClick={() => noteClick(note)}
  285. >
  286. {data.realKey === note.realKey ? (
  287. <img draggable={false} src={icons.icon_btn_ylow} />
  288. ) : (
  289. <img draggable={false} src={icons.icon_btn_blue} />
  290. )}
  291. <div
  292. class={[styles.noteKey, data.realKey === note.realKey && styles.keyActive]}
  293. >
  294. {note.step > 0 ? steps.map((n) => <span class={styles.dot}></span>) : null}
  295. <div class={styles.noteName}>
  296. <sup>{note.mark && (note.mark === "rise" ? "#" : "b")}</sup>
  297. {note.key}
  298. </div>
  299. {note.step < 0 ? steps.map((n) => <span class={styles.dot}></span>) : null}
  300. </div>
  301. </div>
  302. );
  303. })}
  304. </div>
  305. </div>
  306. <Button class={styles.noteBtn}>
  307. <Icon name="arrow" />
  308. </Button>
  309. </div>
  310. </div>
  311. <div class={[styles.tips, data.tipShow ? "" : styles.tipHidden]}>
  312. <div class={styles.tipTitle}>
  313. <div class={styles.tipTitleName}>{fingerData.fingeringInfo.code}使用说明</div>
  314. <Button class={styles.tipClose} onClick={() => (data.tipShow = false)}>
  315. <Icon name="cross" color="#999" />
  316. </Button>
  317. </div>
  318. <div class={styles.tipContent}>
  319. {data.tips.map((tip, tipIndex) => (
  320. <div class={styles.tipItem}>
  321. <div class={styles.iconWrap}>
  322. <div class={styles.tipItemIcon}>{tipIndex + 1}</div>
  323. </div>
  324. <div>
  325. {tip.name}: {tip.realName}
  326. </div>
  327. </div>
  328. ))}
  329. </div>
  330. </div>
  331. </div>
  332. {!!data.tones.length && (
  333. <>
  334. {fingerData.fingeringInfo.name == "hulusi-flute" ? (
  335. <div
  336. id="finger-note-1"
  337. class={[styles.toggleBtn, styles.toggleBtnhulusi]}
  338. onClick={() => (data.tnoteShow = true)}
  339. >
  340. <div>
  341. 全按作
  342. <div class={[styles.noteKey, styles.hulusiNoteKey]}>
  343. {data.activeTone.step > 0 ? <span class={styles.dot}></span> : null}
  344. <div class={styles.noteName}>
  345. <sup>
  346. {data.activeTone.mark && (data.activeTone.mark === "rise" ? "#" : "b")}
  347. </sup>
  348. {data.activeTone.key}
  349. </div>
  350. {data.activeTone.step < 0 ? <span class={styles.dot}></span> : null}
  351. </div>
  352. </div>
  353. <img src={icons.icon_arrow} />
  354. </div>
  355. ) : (
  356. <div id="finger-note-1" class={styles.toggleBtn} onClick={() => (data.tnoteShow = true)}>
  357. <div>
  358. <sup>{data.activeTone.mark && (data.activeTone.mark === "rise" ? "#" : "b")}</sup>
  359. {data.activeTone.name}
  360. </div>
  361. <img src={icons.icon_arrow} />
  362. </div>
  363. )}
  364. </>
  365. )}
  366. <Popup
  367. class="tonePopup"
  368. v-model:show={data.tnoteShow}
  369. position={!query.modelType && fingerData.fingeringInfo.orientation === 1 ? "bottom" : "right"}
  370. >
  371. <div class={styles.tones}>
  372. <div class={styles.toneTitle}>
  373. <div class={styles.tipTitleName}>移调</div>
  374. <Button class={styles.tipClose} onClick={() => (data.tnoteShow = false)}>
  375. <Icon name="cross" color="#999" />
  376. </Button>
  377. </div>
  378. <div style={{ flex: 1, overflow: "hidden" }}>
  379. <Space size={0} class={styles.toneContent}>
  380. {data.tones.map((tone: IFIGNER_INSTRUMENT_Note) => {
  381. const steps = new Array(Math.abs(tone.step)).fill(1);
  382. return (
  383. <Button
  384. round
  385. plain
  386. type={data.activeTone.realName === tone.realName ? "primary" : "default"}
  387. onClick={() => {
  388. data.activeTone = tone;
  389. setNotes();
  390. }}
  391. >
  392. {fingerData.fingeringInfo.name == "hulusi-flute" ? (
  393. <div style={{ display: "flex", alignItems: "center" }}>
  394. 全按作
  395. <div class={[styles.noteKey, styles.hulusiNoteKey]}>
  396. {tone.step > 0 ? <span class={styles.dot}></span> : null}
  397. <div class={styles.noteName}>
  398. <sup>{tone.mark && (tone.mark === "rise" ? "#" : "b")}</sup>
  399. {tone.key}
  400. </div>
  401. {tone.step < 0 ? <span class={styles.dot}></span> : null}
  402. </div>
  403. </div>
  404. ) : (
  405. <div class={styles.noteName}>
  406. <sup>{tone.mark && (tone.mark === "rise" ? "#" : "b")}</sup>
  407. {tone.name}
  408. </div>
  409. )}
  410. </Button>
  411. );
  412. })}
  413. </Space>
  414. </div>
  415. <Space size={0} class={styles.toneAction}>
  416. <Button type="primary" round plain onClick={() => (data.tnoteShow = false)}>
  417. 取消
  418. </Button>
  419. <Button type="primary" round onClick={() => (data.tnoteShow = false)}>
  420. 确定
  421. </Button>
  422. </Space>
  423. </div>
  424. </Popup>
  425. {!data.loading && <GuideIndex list={["finger"]} />}
  426. </div>
  427. );
  428. };
  429. },
  430. });