index.tsx 12 KB

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