index.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. import { Transition, computed, defineComponent, onMounted, onUnmounted, reactive, ref, watch } from "vue";
  2. import styles from "./index.module.less";
  3. import iconBack from "./image/icon-back.svg";
  4. import Title from "./title";
  5. import { headImg } from "./image";
  6. import { Badge, Circle, Popover, Popup, showConfirmDialog, showToast } from "vant";
  7. import Speed from "./speed";
  8. import { evaluatingData, handleStartEvaluat } from "/src/view/evaluating";
  9. import Settting from "./settting";
  10. import state, { IPlatform, handleChangeSection, handleResetPlay, handleRessetState, togglePlay } from "/src/state";
  11. import { getAudioCurrentTime } from "/src/view/audio-list";
  12. import { followData, toggleFollow } from "/src/view/follow-practice";
  13. import { api_back } from "/src/helpers/communication";
  14. import MusicType from "./music-type";
  15. import ModeTypeMode from "../component/mode-type-mode";
  16. import { getQuery } from "/src/utils/queryString";
  17. import { storeData } from "/src/store";
  18. import TeacherTop from "../custom-plugins/guide-page/teacher-top";
  19. import StudentTop from "../custom-plugins/guide-page/student-top";
  20. import { HANDLE_WORK_ADD } from "../custom-plugins/work-index";
  21. import { browser } from "/src/utils";
  22. import store from "store";
  23. import "../component/the-modal-tip/index.module.less";
  24. import { metronomeData } from "../../helpers/metronome";
  25. import { toggleMusicSheet } from "/src/view/plugins/toggleMusicSheet"
  26. /** 头部数据和方法 */
  27. export const headTopData = reactive({
  28. /** 模式 */
  29. modeType: "" as "init" | "show",
  30. /** 显示返回按钮 */
  31. showBack: true,
  32. /** 设置弹窗 */
  33. settingMode: false,
  34. /** 切换模式 */
  35. handleChangeModeType(value: "practise" | "follow" | "evaluating") {
  36. // 后台设置为不能评测
  37. if (value === 'evaluating' && !state.enableEvaluation) return
  38. // 打击乐&节奏练习不支持跟练模式
  39. if (value === 'follow' && state.isPercussion) return
  40. // 跟练模式,光标只有音符模式,无节拍模式
  41. if (value === 'follow' && metronomeData.cursorMode === 2) {
  42. metronomeData.cursorMode = 1
  43. }
  44. if (value === 'practise') {
  45. // state.playIngSpeed = state.speed
  46. }
  47. if (value === "evaluating") {
  48. // 如果延迟检测资源还在加载中,给出提示
  49. if (!evaluatingData.jsonLoadDone) {
  50. evaluatingData.jsonLoading = true
  51. showToast('资源加载中,请稍后')
  52. return
  53. }
  54. // 如果是pc端, 评测模式暂不可用
  55. if (state.platform === IPlatform.PC) {
  56. showConfirmDialog({
  57. className: "modalTip",
  58. title: "温馨提示",
  59. message: "该功能暂未开放,敬请期待!",
  60. showCancelButton: false,
  61. });
  62. return;
  63. }
  64. state.playIngSpeed = state.originSpeed
  65. handleStartEvaluat();
  66. // 开发模式,把此处打开
  67. // state.modeType = "evaluating"
  68. // evaluatingData.rendered = true;
  69. // evaluatingData.soundEffectMode = true;
  70. } else if (value === "follow") {
  71. toggleFollow();
  72. }
  73. headTopData.modeType = "show";
  74. },
  75. });
  76. export const headData = reactive({
  77. speedShow: false,
  78. musicTypeShow: false,
  79. });
  80. export default defineComponent({
  81. name: "header-top",
  82. emits: ["close"],
  83. setup(props, { emit }) {
  84. const query = getQuery();
  85. // 是否显示引导
  86. const showGuide = ref(false);
  87. const showStudentGuide = ref(false);
  88. /** 设置按钮 */
  89. const settingBtn = computed(() => {
  90. // 音频播放中 禁用
  91. if (state.playState === "play") return { display: true, disabled: true };
  92. // 评测开始 禁用, 跟练开始 禁用
  93. if (evaluatingData.startBegin || followData.start) return { display: true, disabled: true };
  94. return {
  95. display: true,
  96. disabled: false,
  97. };
  98. });
  99. /** 转谱按钮 */
  100. const converBtn = computed(() => {
  101. // 音频播放中 禁用
  102. if (state.playState === "play") return { display: true, disabled: true };
  103. // 评测开始 禁用
  104. if (evaluatingData.startBegin || followData.start) return { display: true, disabled: true };
  105. return {
  106. disabled: false,
  107. display: true,
  108. };
  109. });
  110. /** 速度按钮 */
  111. const speedBtn = computed(() => {
  112. // 选择模式, 跟练模式 不显示
  113. if (headTopData.modeType !== "show" || state.modeType === "follow") return { display: false, disabled: true };
  114. // 评测模式, 音频播放中 禁用
  115. if (state.modeType === "evaluating" || state.playState === "play") return { display: true, disabled: true };
  116. return {
  117. disabled: false,
  118. display: true,
  119. };
  120. });
  121. /** 指法按钮 */
  122. const fingeringBtn = computed(() => {
  123. // 后台设置不显示指法
  124. if (!state.isShowFingering) return { display: true, disabled: true };
  125. // 没有指法 选择模式 评测模式 跟练模式 不显示
  126. if (headTopData.modeType !== "show" || !state.fingeringInfo.name || ["evaluating", "follow"].includes(state.modeType)) return { display: false, disabled: true };
  127. // 音频播放中 禁用
  128. if (state.playState === "play") return { display: true, disabled: true };
  129. return {
  130. disabled: false,
  131. display: true,
  132. };
  133. });
  134. /** 摄像头按钮 */
  135. const cameraBtn = computed(() => {
  136. // 选择模式 不显示
  137. if (headTopData.modeType !== "show" || state.modeType !== "evaluating") return { display: false, disabled: true };
  138. // 音频播放中 禁用
  139. if (state.playState === "play") return { display: true, disabled: true };
  140. return {
  141. disabled: false,
  142. display: true,
  143. };
  144. });
  145. /** 选段按钮 */
  146. const selectBtn = computed(() => {
  147. // 选择模式 不显示
  148. if (headTopData.modeType !== "show" || ["evaluating", "follow"].includes(state.modeType)) return { display: false, disabled: true };
  149. // 音频播放中 禁用
  150. if (state.playState === "play") return { display: true, disabled: true };
  151. return {
  152. disabled: false,
  153. display: true,
  154. };
  155. });
  156. /** 原声按钮 */
  157. const originBtn = computed(() => {
  158. // 选择模式,跟练模式 不显示
  159. if (headTopData.modeType !== "show" || state.modeType === "follow") return { display: false, disabled: false };
  160. // 评测开始 禁用
  161. if (state.modeType === "evaluating") return { display: false, disabled: true };
  162. if (!state.isAppPlay) {
  163. // 原声, 伴奏 少一个,就不能切换
  164. if (!state.music || !state.accompany) return { display: true, disabled: true };
  165. }
  166. return {
  167. disabled: false,
  168. display: true,
  169. };
  170. });
  171. /** 模式切换按钮 */
  172. const toggleBtn = computed(() => {
  173. // 选择模式, url设置模式 不显示
  174. if (headTopData.modeType !== "show" || !headTopData.showBack) return { display: false, disabled: false };
  175. // 跟练开始, 评测开始 禁用
  176. if (followData.start || evaluatingData.startBegin) return { display: true, disabled: true };
  177. return {
  178. display: true,
  179. disabled: false,
  180. };
  181. });
  182. /** 播放按钮 */
  183. const playBtn = computed(() => {
  184. // 选择模式 不显示
  185. if (headTopData.modeType !== "show") return { display: false, disabled: false };
  186. // 评测模式 不显示,跟练模式 不显示
  187. if (["evaluating", "follow"].includes(state.modeType)) return { display: false, disabled: true };
  188. // midi音频未初始化完成不可点击
  189. if (state.isAppPlay && state.midiPlayIniting) return { display: true, disabled: true };
  190. return {
  191. display: true,
  192. disabled: false,
  193. };
  194. });
  195. /** 重播按钮 */
  196. const resetBtn = computed(() => {
  197. // 选择模式 不显示
  198. if (headTopData.modeType !== "show") return { display: false, disabled: false };
  199. // 评测模式 不显示,跟练模式 不显示
  200. if (["evaluating", "follow"].includes(state.modeType)) return { display: false, disabled: true };
  201. // 播放状态 不显示
  202. if (state.playState === "play") return { display: false, disabled: true };
  203. // 播放进度为0 不显示
  204. const currentTime = getAudioCurrentTime();
  205. // midi音频未初始化完成不可点击
  206. if (state.isAppPlay && state.midiPlayIniting) return { display: false, disabled: true };
  207. if (!currentTime) return { display: false, disabled: true };
  208. return {
  209. display: true,
  210. disabled: false,
  211. };
  212. });
  213. const isAllBtns = computed(() => {
  214. const flag = converBtn.value.display && speedBtn.value.display && selectBtn.value.display && originBtn.value.display && toggleBtn.value.display && showGuide.value;
  215. return flag;
  216. });
  217. const isAllBtnsStudent = computed(() => {
  218. const flag = converBtn.value.display && speedBtn.value.display && selectBtn.value.display && originBtn.value.display && toggleBtn.value.display && showStudentGuide.value;
  219. return flag;
  220. });
  221. const browInfo = browser();
  222. /** 返回 */
  223. const handleBack = () => {
  224. HANDLE_WORK_ADD();
  225. // 不在APP中,
  226. if (!storeData.isApp) {
  227. window.close();
  228. return;
  229. }
  230. if ((browInfo.iPhone || browInfo.ios) && query.workRecord) {
  231. setTimeout(() => {
  232. api_back();
  233. }, 550);
  234. return;
  235. }
  236. api_back();
  237. };
  238. /** 根据参数设置模式 */
  239. const getQueryModelSetModelType = () => {
  240. /** 作业模式 start, 如果为作业模式不处理,让作业模块处理 */
  241. if (query.workRecord) {
  242. return;
  243. }
  244. /** 作业模式 end */
  245. if (query.modelType) {
  246. if (query.modelType === "practise") {
  247. headTopData.handleChangeModeType("practise");
  248. } else if (query.modelType === "evaluating") {
  249. headTopData.handleChangeModeType("evaluating");
  250. }
  251. headTopData.showBack = false;
  252. } else {
  253. setTimeout(() => {
  254. headTopData.modeType = "init";
  255. }, 500);
  256. }
  257. };
  258. /** 课件播放 */
  259. const changePlay = (res: any) => {
  260. if (res?.data?.api === "setPlayState") {
  261. togglePlay("paused");
  262. }
  263. // 菜单状态
  264. if ((state.platform === IPlatform.PC && res?.data?.api) === "attendClassBarStatus") {
  265. state.attendHideMenu = res?.data?.hideMenu;
  266. }
  267. };
  268. onMounted(() => {
  269. getQueryModelSetModelType();
  270. window.addEventListener("message", changePlay);
  271. if (state.platform === IPlatform.PC) {
  272. showGuide.value = true;
  273. } else {
  274. showStudentGuide.value = true;
  275. }
  276. });
  277. onUnmounted(() => {
  278. window.removeEventListener("message", changePlay);
  279. });
  280. // 设置改变触发
  281. watch(state.setting, () => {
  282. console.log(state.setting, "state.setting");
  283. store.set("musicscoresetting", state.setting);
  284. });
  285. return () => (
  286. <>
  287. <div
  288. class={[styles.headerTop, state.platform === IPlatform.PC && styles.headRightTop, state.platform === IPlatform.PC && !state.attendHideMenu && styles.headRightTopHide]}
  289. onClick={(e: Event) => {
  290. e.stopPropagation();
  291. if (state.platform === IPlatform.PC) {
  292. // 显示隐藏菜单
  293. window.parent.postMessage(
  294. {
  295. api: "onAttendToggleMenu",
  296. },
  297. "*"
  298. );
  299. }
  300. }}
  301. >
  302. <div class={[styles.back, "headTopBackBtn", !headTopData.showBack && styles.hidenBack]} onClick={handleBack}>
  303. <img src={iconBack} />
  304. </div>
  305. {query.iscurseplay === "play" ? null : <Title class="pcTitle" text={state.examSongName} rightView={false} />}
  306. <div
  307. class={[styles.headRight]}
  308. onClick={(e: Event) => {
  309. e.stopPropagation();
  310. }}
  311. >
  312. <div
  313. id={state.platform === IPlatform.PC ? "teacherTop-0" : "studnetT-0"}
  314. style={{ display: toggleBtn.value.display ? "" : "none" }}
  315. class={[styles.btn, toggleBtn.value.disabled && styles.disabled]}
  316. onClick={() => {
  317. handleRessetState();
  318. headTopData.modeType = "init";
  319. }}
  320. >
  321. <img class={styles.iconBtn} src={headImg(`modeType.svg`)} />
  322. <span>模式</span>
  323. </div>
  324. {/* 一行谱模式,暂不支持节拍指针 */}
  325. {
  326. !state.isSingleLine ?
  327. <div class={[styles.btn]} onClick={() => {
  328. // 切换光标模式
  329. let mode = metronomeData.cursorMode
  330. if (['follow'].includes(state.modeType)) {
  331. mode = metronomeData.cursorMode === 1 ? 3 : 1
  332. } else {
  333. mode = metronomeData.cursorMode === 3 ? 1 : metronomeData.cursorMode + 1
  334. }
  335. metronomeData.cursorMode = mode
  336. }}>
  337. <img class={styles.iconBtn} src={headImg(metronomeData.cursorMode === 1 ? 'cursor-icon-1.svg' : metronomeData.cursorMode === 2 ? 'cursor-icon-2.svg' : metronomeData.cursorMode === 3 ? 'cursor-icon-3.svg' : '')} />
  338. <span class={styles.iconContent}>
  339. {metronomeData.cursorMode === 1 ? '音符指针' : metronomeData.cursorMode === 2 ? '节拍指针' : metronomeData.cursorMode === 3 ? '关闭指针' : ''}
  340. {metronomeData.cursorTips && <>
  341. <i class={styles.arrowIcon}></i>
  342. <div class={[styles['botton-tips'],metronomeData.cursorMode === 3 ? styles.tipSpec : '']}>{metronomeData.cursorTips}</div>
  343. </>}
  344. </span>
  345. </div> : null
  346. }
  347. {state.musicRendered && !query.lessonTrainingId && !query.questionId && state.isConcert && (
  348. <div class={[styles.btn, (state.playState === "play" && fingeringBtn.value.disabled) && styles.disabled]}
  349. onClick={() => {
  350. toggleMusicSheet.toggle(true)
  351. }}>
  352. <img class={styles.iconBtn} src={headImg(`shenggui.svg`)} />
  353. <span>声轨</span>
  354. </div>
  355. )}
  356. <div
  357. id={state.platform === IPlatform.PC ? "teacherTop-1" : "studnetT-1"}
  358. style={{ display: originBtn.value.display ? "" : "none" }}
  359. class={[styles.btn, originBtn.value.disabled && styles.disabled]}
  360. onClick={() => {
  361. state.playSource = state.playSource === "music" ? "background" : "music";
  362. }}
  363. >
  364. <img style={{ display: state.playSource === "music" ? "" : "none" }} class={styles.iconBtn} src={headImg(`music.svg`)} />
  365. <img style={{ display: state.playSource === "music" ? "none" : "" }} class={styles.iconBtn} src={headImg(`background.svg`)} />
  366. <span>{state.playSource === "music" ? "原声" : "伴奏"}</span>
  367. </div>
  368. {
  369. state.modeType !== "evaluating" &&
  370. <div
  371. class={[styles.btn]}
  372. onClick={async () => {
  373. metronomeData.disable = !metronomeData.disable;
  374. metronomeData.metro?.initPlayer();
  375. }}
  376. >
  377. <img style={{ display: metronomeData.disable ? "block" : "none" }} class={styles.iconBtn} src={headImg("tickoff.svg")} />
  378. <img style={{ display: !metronomeData.disable ? "block" : "none" }} class={styles.iconBtn} src={headImg("tickon.svg")} />
  379. <span style={{ whiteSpace: "nowrap" }}>节拍器</span>
  380. </div>
  381. }
  382. <div id={state.platform === IPlatform.PC ? "teacherTop-2" : "studnetT-2"} style={{ display: selectBtn.value.display ? "" : "none" }} class={[styles.btn, selectBtn.value.disabled && styles.disabled]} onClick={() => handleChangeSection()}>
  383. <img style={{ display: state.section.length === 0 ? "" : "none" }} class={styles.iconBtn} src={headImg(`section0.svg`)} />
  384. <img style={{ display: state.section.length === 1 ? "" : "none" }} class={styles.iconBtn} src={headImg(`section1.svg`)} />
  385. <img style={{ display: state.section.length === 2 ? "" : "none" }} class={styles.iconBtn} src={headImg(`section2.svg`)} />
  386. <span>选段</span>
  387. </div>
  388. <div
  389. id={state.platform === IPlatform.PC ? "teacherTop-3" : "studnetT-3"}
  390. style={{ display: fingeringBtn.value.display ? "" : "none" }}
  391. class={[styles.btn, fingeringBtn.value.disabled && styles.disabled]}
  392. onClick={() => {
  393. state.setting.displayFingering = !state.setting.displayFingering;
  394. }}
  395. >
  396. <img style={{ display: state.setting.displayFingering ? "" : "none" }} class={styles.iconBtn} src={headImg(`icon_evaluatingOn.svg`)} />
  397. <img style={{ display: state.setting.displayFingering ? "none" : "" }} class={styles.iconBtn} src={headImg(`icon_evaluatingOff.svg`)} />
  398. <span>指法</span>
  399. </div>
  400. <Popover trigger="manual" v-model:show={headData.speedShow} placement="bottom" overlay={false}>
  401. {{
  402. reference: () => (
  403. <div
  404. id={state.platform === IPlatform.PC ? "teacherTop-4" : "studnetT-4"}
  405. style={{ display: speedBtn.value.display ? "" : "none" }}
  406. class={[styles.btn, speedBtn.value.disabled && styles.disabled]}
  407. onClick={(e: Event) => {
  408. e.stopPropagation();
  409. headData.speedShow = !headData.speedShow;
  410. }}
  411. >
  412. <Badge class={styles.badge} content={state.playState === "play" ? state.playIngSpeed : state.speed}>
  413. <img class={styles.iconBtn} src={headImg("icon_speed.svg")} />
  414. </Badge>
  415. <span>速度</span>
  416. </div>
  417. ),
  418. default: () => <Speed />,
  419. }}
  420. </Popover>
  421. {
  422. state.enableNotation ?
  423. <Popover trigger="manual" v-model:show={headData.musicTypeShow} placement="bottom-end" overlay={false}>
  424. {{
  425. reference: () => (
  426. <div
  427. id={state.platform === IPlatform.PC ? "teacherTop-5" : "studnetT-5"}
  428. style={{ display: converBtn.value.display ? "" : "none" }}
  429. class={[styles.btn, converBtn.value.disabled && styles.disabled]}
  430. onClick={(e: Event) => {
  431. e.stopPropagation();
  432. headData.musicTypeShow = !headData.musicTypeShow;
  433. }}
  434. >
  435. <img class={styles.iconBtn} src={headImg("icon_zhuanpu.svg")} />
  436. <span>{state.musicRenderType === "staff" ? "转简谱" : "转五线谱"}</span>
  437. </div>
  438. ),
  439. default: () => <MusicType />,
  440. }}
  441. </Popover> : null
  442. }
  443. <div id={state.platform === IPlatform.PC ? "teacherTop-6" : "studnetT-6"} style={{ display: settingBtn.value.display ? "" : "none" }} class={[styles.btn, styles.setBtn, settingBtn.value.disabled && styles.disabled]} onClick={() => (headTopData.settingMode = true)}>
  444. <img class={styles.iconBtn} src={headImg("icon_menu.svg")} />
  445. <span>设置</span>
  446. </div>
  447. </div>
  448. </div>
  449. {/* 播放按钮 */}
  450. <div
  451. id="studnetT-7"
  452. style={{ display: playBtn.value.display ? "" : "none" }}
  453. class={[styles.btn, styles.playBtn, playBtn.value.disabled && styles.disabled, state.platform === IPlatform.PC && styles.playButton, state.platform === IPlatform.PC && !state.attendHideMenu && styles.playButtonHide]}
  454. onClick={() => togglePlay()}
  455. >
  456. <div class={styles.btnWrap}>
  457. <img style={{ display: state.playState === "play" ? "none" : "" }} class={styles.iconBtn} src={headImg("icon_play.svg")} />
  458. <img style={{ display: state.playState === "play" ? "" : "none" }} class={styles.iconBtn} src={headImg("icon_pause.svg")} />
  459. <Circle style={{ opacity: state.playState === "play" ? 1 : 0 }} class={styles.progress} stroke-width={80} currentRate={state.playProgress} rate={100} color="#FFC830" />
  460. </div>
  461. </div>
  462. {/* 重播按钮 */}
  463. <div
  464. id="tips-step-9"
  465. style={{ display: resetBtn.value.display ? "" : "none" }}
  466. class={[styles.btn, styles.resetBtn, resetBtn.value.disabled && styles.disabled, state.platform === IPlatform.PC && styles.pauseButton, state.platform === IPlatform.PC && !state.attendHideMenu && styles.playButtonHide]}
  467. onClick={() => handleResetPlay()}
  468. >
  469. <img class={styles.iconBtn} src={headImg("icon_resetbtn.svg")} />
  470. </div>
  471. <Popup v-model:show={headTopData.settingMode} class="popup-custom van-scale center-closeBtn" transition="van-scale" teleport="body" closeable>
  472. <Settting />
  473. </Popup>
  474. {/* 模式切换 */}
  475. <ModeTypeMode />
  476. {/* isAllBtns */}
  477. {isAllBtns.value && !query.isCbs && <TeacherTop></TeacherTop>}
  478. {isAllBtnsStudent.value && !query.isCbs && <StudentTop></StudentTop>}
  479. </>
  480. );
  481. },
  482. });