index.tsx 19 KB

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