index.tsx 45 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036
  1. import { Transition, computed, defineComponent, onMounted, onUnmounted, reactive, ref, watch, toRef, ComputedRef, nextTick, defineAsyncComponent } from "vue";
  2. import styles from "./index.module.less";
  3. import iconBack from "./image/icon-back.png";
  4. import listImg from "./image/list.png";
  5. import iconMode from "./image/mode.png";
  6. import { headImg } from "./image";
  7. import { Badge, Circle, Popover, Popup, showConfirmDialog, showToast, NoticeBar } from "vant";
  8. import Speed from "./speed";
  9. import { evaluatingData, handleStartEvaluat } from "/src/view/evaluating";
  10. import Settting from "./settting";
  11. import state, { IPlatform, handleChangeSection, handleResetPlay, handleRessetState, togglePlay, IPlayState, refreshMusicSvg, EnumMusicRenderType, resetSettings, handleGuide, resetCursorPosition } from "/src/state";
  12. import { audioData, getAudioCurrentTime } from "/src/view/audio-list";
  13. import { followData, toggleFollow } from "/src/view/follow-practice";
  14. import { api_back } from "/src/helpers/communication";
  15. import MusicType from "./music-type";
  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. import useDrag from "/src/view/plugins/useDrag/index";
  27. import Dragbom from "/src/view/plugins/useDrag/dragbom";
  28. import { getGuidance, setGuidance } from "../custom-plugins/guide-page/api";
  29. // import ModeView from "./modeView";
  30. import { smoothAnimationState } from "../view-detail/smoothAnimation";
  31. import { isMusicList, musicListShow } from "../component/the-music-list";
  32. import { EvaluatingDriver, FollowDriver, PractiseDriver } from "../custom-plugins/guide-driver";
  33. import { fingerRef } from "/src/page-instrument/view-detail/index"
  34. import { handleLoadBeatMusic } from "/src/view/audio-list"
  35. const ModeView = defineAsyncComponent(() =>
  36. import('./modeView')
  37. )
  38. /** 头部数据和方法 */
  39. export const headTopData = reactive({
  40. /** 模式 */
  41. modeType: "" as "init" | "show",
  42. /** 显示返回按钮 */
  43. showBack: true,
  44. /** 设置弹窗 */
  45. settingMode: false,
  46. /** 切换模式 */
  47. handleChangeModeType(value: "practise" | "follow" | "evaluating") {
  48. // 后台设置为不能评测
  49. if (value === "evaluating" && !state.enableEvaluation) return;
  50. // 打击乐&节奏练习不支持跟练模式
  51. if (value === "follow" && state.isPercussion) return;
  52. // 跟练模式,光标只有音符模式,无节拍模式
  53. if (value === "follow" && metronomeData.cursorMode === 2) {
  54. metronomeData.cursorMode = 1;
  55. }
  56. if (value === "practise") {
  57. // 切回当前的时值
  58. const isModeChange = modeChangeHandleTimes("play", "music")
  59. // 没有切换的时候 不处理下面的
  60. if (isModeChange) {
  61. try {
  62. metronomeData.metro.calculation(state.times);
  63. } catch (error) {}
  64. console.log("重新之后的times", state.times, state.fixtime);
  65. }
  66. // state.playIngSpeed = state.speed
  67. }
  68. if (value === "evaluating") {
  69. // 如果延迟检测资源还在加载中,给出提示
  70. if (!evaluatingData.jsonLoadDone) {
  71. evaluatingData.jsonLoading = true;
  72. state.audioDone && showToast("资源加载中,请稍后"); //音频资源加载完之后才提示
  73. return;
  74. }
  75. // 如果是pc端, 评测模式暂不可用
  76. if (state.platform === IPlatform.PC) {
  77. showConfirmDialog({
  78. className: "modalTip",
  79. title: "温馨提示",
  80. message: "该功能暂未开放,敬请期待!",
  81. showCancelButton: false,
  82. });
  83. return;
  84. }
  85. /* 当前是唱名的模式 */
  86. if(state.playSource === "mingSong") {
  87. const isModeChange = modeChangeHandleTimes("play","mingSong","play","music")
  88. // 没有切换的时候 不处理下面的
  89. if (isModeChange) {
  90. try {
  91. metronomeData.metro.calculation(state.times);
  92. } catch (error) {}
  93. console.log("重新之后的times", state.times, state.fixtime);
  94. }
  95. }
  96. // 评测模式,只有一行谱模式
  97. // if (!state.isSingleLine) {
  98. // state.isSingleLine = true;
  99. // refreshMusicSvg();
  100. // }
  101. smoothAnimationState.isShow.value = false; // 隐藏旋律线
  102. state.playIngSpeed = state.originSpeed;
  103. handleStartEvaluat();
  104. resetCursorPosition()
  105. // 开发模式,把此处打开
  106. // state.modeType = "evaluating";
  107. // evaluatingData.rendered = true;
  108. // evaluatingData.soundEffectMode = true;
  109. } else if (value === "follow") {
  110. // 跟练模式,只有一行谱模式
  111. if (!state.isSingleLine) {
  112. state.isSingleLine = true;
  113. refreshMusicSvg();
  114. }
  115. smoothAnimationState.isShow.value = false;
  116. toggleFollow();
  117. }
  118. headTopData.modeType = "show";
  119. },
  120. // 改变模式之前的状态
  121. oldPlayType: "play",
  122. // 记录切换模式前的状态
  123. oldModeType: "practise" as "practise" | "follow" | "evaluating",
  124. });
  125. export const headData = reactive({
  126. speedShow: false,
  127. musicTypeShow: false,
  128. });
  129. let resetBtn: ComputedRef<{
  130. display: boolean;
  131. disabled: boolean;
  132. }>;
  133. // 点击切换的时候才触发提醒
  134. let isClickMode = false;
  135. /**
  136. * 处理模式切换
  137. * @param oldPlayType 没改变之前的播放模式
  138. * @param oldPlaySource 没改变之前的播放类型
  139. * @param isforceReset 是否强制刷新播放状态 模式times时值改变时候也刷新
  140. */
  141. export async function handlerModeChange(oldPlayType: "play" | "sing", oldPlaySource: IPlayState, isforceReset?: boolean) {
  142. const isModeChange = modeChangeHandleTimes(oldPlayType, oldPlaySource);
  143. // 没有切换的时候 不处理下面的
  144. if (isModeChange) {
  145. try {
  146. metronomeData.metro.calculation(state.times);
  147. } catch (error) {}
  148. console.log("重新之后的times", state.times, state.fixtime);
  149. }
  150. if (isModeChange || isforceReset) {
  151. // 重置播放状态
  152. handleRessetState();
  153. // 隐藏重播按钮
  154. resetBtn && (resetBtn.value.display = false);
  155. }
  156. // 节拍器音频加载
  157. await handleLoadBeatMusic()
  158. // 当模式改变的时候 放在这里是因为需要等谱面加载完成之后再提示(点击按钮模式切换才提示)
  159. if (isClickMode) {
  160. showToast({
  161. message: state.playType === "play" ? "已切换为演奏场景" : "已切换为演唱场景",
  162. position: "top",
  163. className: "selectionToast",
  164. });
  165. isClickMode = false;
  166. }
  167. }
  168. // 模式切换之后重新给times赋值
  169. function modeChangeHandleTimes(oldPlayType: "play" | "sing", oldPlaySource: IPlayState, nowPlayType?:"play" | "sing", nowPlaySource?: IPlayState) {
  170. const playType = nowPlayType || state.playType;
  171. const playSource = nowPlaySource || state.playSource;
  172. const { notBeatFixtime, xmlMp3BeatFixTime, difftime } = state.times[0];
  173. const { isOpenMetronome, isSingOpenMetronome } = state;
  174. // 因为演奏加了唱名,所以往跟练模式切换和评测模式切换的时候,刷新谱面的时候需要更新时值,这时候调用handleRessetState 会回到练习模式,这里是
  175. if(state.modeType === "follow" || state.modeType === "evaluating") {
  176. return false
  177. }
  178. // 当相同时候也不处理
  179. if(oldPlayType === playType && oldPlaySource === playSource) {
  180. return false
  181. }
  182. // 原声向伴奏和伴奏向原声不处理 范唱向伴唱和伴唱向范唱切不处理
  183. if((oldPlaySource==="music"&&playSource==="background")||(oldPlaySource==="background"&&playSource==="music")){
  184. return false
  185. }
  186. // 唱名到唱名时候不处理
  187. if(oldPlaySource === "mingSong" && playSource === "mingSong"){
  188. return false
  189. }
  190. // 原生伴奏向范唱伴唱,范唱伴唱向原生伴奏时候,isSingOpenMetronome和isOpenMetronome相等时候不处理
  191. if(["music","background"].includes(oldPlaySource)&&["music","background"].includes(playSource)&&isOpenMetronome===isSingOpenMetronome){
  192. return false
  193. }
  194. // 当切为 原生伴奏,或者范唱伴唱 时候
  195. if(playSource==="music" || playSource ==="background"){
  196. if(playType === "play"){
  197. // 有节拍器
  198. if (isOpenMetronome) {
  199. state.fixtime = notBeatFixtime + xmlMp3BeatFixTime;
  200. state.times.map((item) => {
  201. item.time = item.notBeatTime + xmlMp3BeatFixTime;
  202. item.endtime = item.notBeatEndTime + xmlMp3BeatFixTime;
  203. item.fixtime = notBeatFixtime + xmlMp3BeatFixTime;
  204. });
  205. return true;
  206. } else {
  207. state.fixtime = notBeatFixtime;
  208. state.times.map((item) => {
  209. item.time = item.notBeatTime;
  210. item.endtime = item.notBeatEndTime;
  211. item.fixtime = notBeatFixtime;
  212. });
  213. return true;
  214. }
  215. }
  216. if(playType==="sing"){
  217. // 有节拍器
  218. if (isSingOpenMetronome) {
  219. state.fixtime = notBeatFixtime + xmlMp3BeatFixTime;
  220. state.times.map((item) => {
  221. item.time = item.notBeatTime + xmlMp3BeatFixTime;
  222. item.endtime = item.notBeatEndTime + xmlMp3BeatFixTime;
  223. item.fixtime = notBeatFixtime + xmlMp3BeatFixTime;
  224. });
  225. return true;
  226. } else {
  227. state.fixtime = notBeatFixtime;
  228. state.times.map((item) => {
  229. item.time = item.notBeatTime;
  230. item.endtime = item.notBeatEndTime;
  231. item.fixtime = notBeatFixtime;
  232. });
  233. return true;
  234. }
  235. }
  236. }
  237. // 当切为唱名时候
  238. if(playSource==="mingSong"){
  239. // 唱名文件也要加上弱起时间 他们制作曲子加了弱起时间 注意这修改了之后给总控平台的时值也需要改
  240. state.fixtime = difftime;
  241. state.times.map((item) => {
  242. item.time = item.xmlNoteTime + difftime;
  243. item.endtime = item.xmlNoteEndTime + difftime;
  244. item.fixtime = difftime;
  245. });
  246. return true;
  247. }
  248. return false;
  249. }
  250. export default defineComponent({
  251. name: "header-top",
  252. emits: ["close"],
  253. setup(props, { emit }) {
  254. const query = getQuery();
  255. // 是否显示引导
  256. const showGuide = ref(false);
  257. const showStudentGuide = ref(false);
  258. const showWebGuide = ref(true);
  259. let displayFingeringCache = false; // 指法缓存
  260. /** 设置按钮 */
  261. const settingBtn = computed(() => {
  262. // 音频播放中 禁用
  263. if (state.playState === "play") return { display: true, disabled: true };
  264. // 评测开始 禁用, 跟练开始 禁用
  265. if (evaluatingData.startBegin || followData.start) return { display: true, disabled: true };
  266. return {
  267. display: true,
  268. disabled: false,
  269. };
  270. });
  271. /** 转谱按钮 */
  272. const converBtn = computed(() => {
  273. // 音频播放中 禁用
  274. if (state.playState === "play") return { display: true, disabled: true };
  275. // 评测开始 禁用
  276. if (evaluatingData.startBegin || followData.start) return { display: true, disabled: true };
  277. return {
  278. disabled: false,
  279. display: true,
  280. };
  281. });
  282. /** 速度按钮 */
  283. const speedBtn = computed(() => {
  284. // 选择模式, 跟练模式 不显示
  285. //if (headTopData.modeType !== "show" || state.modeType === "follow") return { display: false, disabled: true };
  286. if (state.modeType === "follow") return { display: false, disabled: true };
  287. // 评测模式, 音频播放中 禁用
  288. if (state.modeType === "evaluating" || state.playState === "play") return { display: true, disabled: true };
  289. return {
  290. disabled: false,
  291. display: true,
  292. };
  293. });
  294. /** 节拍器按钮 */
  295. const metronomeBtn = computed(() => {
  296. // 选择模式 不显示
  297. //if (headTopData.modeType !== "show") return { display: false, disabled: true };
  298. // 音频播放中 禁用
  299. if (state.playState === "play") return { display: true, disabled: true };
  300. return {
  301. disabled: false,
  302. display: true,
  303. };
  304. });
  305. /** 指法按钮 */
  306. const fingeringBtn = computed(() => {
  307. // 后台设置不显示指法
  308. if (!state.isShowFingering) return { display: true, disabled: true };
  309. // 没有指法 选择模式 评测模式 跟练模式 不显示
  310. //if (headTopData.modeType !== "show" || !state.fingeringInfo.name || ["evaluating", "follow"].includes(state.modeType)) return { display: false, disabled: true };
  311. if (!state.fingeringInfo.name || ["evaluating", "follow"].includes(state.modeType)) return { display: false, disabled: true };
  312. // 音频播放中 禁用
  313. if (state.playState === "play") return { display: true, disabled: true };
  314. return {
  315. disabled: false,
  316. display: true,
  317. };
  318. });
  319. /** 摄像头按钮 */
  320. const cameraBtn = computed(() => {
  321. // 选择模式 不显示
  322. if (headTopData.modeType !== "show" || state.modeType !== "evaluating") return { display: false, disabled: true };
  323. // 音频播放中 禁用
  324. if (state.playState === "play") return { display: true, disabled: true };
  325. return {
  326. disabled: false,
  327. display: true,
  328. };
  329. });
  330. /** 选段按钮 */
  331. const selectBtn = computed(() => {
  332. // 选择模式 不显示
  333. //if (headTopData.modeType !== "show" || ["follow"].includes(state.modeType)) return { display: false, disabled: true };
  334. if (["follow"].includes(state.modeType)) return { display: false, disabled: true };
  335. // 音频播放中 禁用
  336. if (state.playState === "play" || state.isHomeWork) return { display: true, disabled: true };
  337. return {
  338. disabled: false,
  339. display: true,
  340. };
  341. });
  342. /** 原声按钮 */
  343. const originBtn = computed(() => {
  344. // 没有音源不显示
  345. if (state.noMusicSource) return { display: false, disabled: false };
  346. // 选择模式,跟练模式 不显示
  347. //if (headTopData.modeType !== "show" || state.modeType === "follow") return { display: false, disabled: false };
  348. if (state.modeType === "follow") return { display: false, disabled: false };
  349. // 评测开始 禁用
  350. if (state.modeType === "evaluating") return { display: false, disabled: true };
  351. // 总谱渲染在播放过程中 不能切换
  352. if(state.isCombineRender && state.playState === "play") return { display: true, disabled: true }
  353. if (!state.isAppPlay) {
  354. // 播放过程中不能切换
  355. if (state.playState === "play") {
  356. return { display: true, disabled: true };
  357. }
  358. if (state.playType === "play") {
  359. // 原声, 伴奏 少一个,就不能切换
  360. if (state.music && state.accompany) return { display: true, disabled: false };
  361. } else {
  362. // 范唱
  363. let index = 0;
  364. state.fanSong && index++;
  365. state.banSong && index++;
  366. state.mingSong && index++;
  367. if (index > 1) {
  368. return { display: true, disabled: false };
  369. }
  370. }
  371. }
  372. return {
  373. disabled: true,
  374. display: true,
  375. };
  376. });
  377. /** 播放类型按钮 */
  378. const playTypeBtn = computed(() => {
  379. // 管乐迷没有演唱模式,
  380. return {
  381. disabled: false,
  382. display: false,
  383. }
  384. // 选择模式,跟练模式,评测模式 不显示
  385. //if (headTopData.modeType !== "show" || state.modeType === "follow" || state.modeType === "evaluating" || state.isHomeWork) return { display: false, disabled: false };
  386. if (state.modeType === "follow" || state.modeType === "evaluating" || state.isHomeWork) return { display: false, disabled: false };
  387. if (!state.isAppPlay) {
  388. let index = 0;
  389. state.music && index++;
  390. state.accompany && index++;
  391. let songIndex = 0;
  392. state.fanSong && songIndex++;
  393. state.banSong && songIndex++;
  394. state.mingSong && songIndex++;
  395. // 演唱和演奏 都有数据的时间不禁用
  396. if (songIndex > 0 && index > 0) {
  397. // 音频播放中 禁用
  398. if (state.playState === "play") {
  399. return { display: true, disabled: true };
  400. }
  401. return { display: true, disabled: false };
  402. }
  403. }
  404. return {
  405. disabled: false,
  406. display: false,
  407. };
  408. });
  409. /** 模式切换按钮 */
  410. const toggleBtn = computed(() => {
  411. // 老师端,打击乐&节奏练习不显示
  412. if (state.isPercussion && state.platform === IPlatform.PC) return { display: false, disabled: false };
  413. if(state.isCombineRender) return { display: false, disabled: false };
  414. // 没有音源不显示
  415. if (state.noMusicSource) return { display: false, disabled: false };
  416. // 不是演奏模式 影藏
  417. if (state.playType !== "play") return { display: false, disabled: false };
  418. // 选择模式, url设置模式 不显示
  419. if (headTopData.modeType !== "show" || !headTopData.showBack) return { display: false, disabled: false };
  420. // 跟练开始, 评测开始 播放开始 隐藏
  421. if (state.playState == "play" || followData.start || evaluatingData.startBegin) return { display: true, disabled: true };
  422. // 课件播放,上课页面潜入云教练,不显示模式切换
  423. if (query.hideMode == 1) return { display: false, disabled: false };
  424. return {
  425. display: true,
  426. disabled: false,
  427. };
  428. });
  429. /** 播放按钮 */
  430. const playBtn = computed(() => {
  431. // 没有音源不显示
  432. if (state.noMusicSource) return { display: false, disabled: false };
  433. // 选择模式 不显示
  434. if (headTopData.modeType !== "show") return { display: false, disabled: false };
  435. // 评测模式 不显示,跟练模式 不显示
  436. if (["evaluating", "follow"].includes(state.modeType)) return { display: false, disabled: true };
  437. // midi音频未初始化完成不可点击
  438. if (state.isAppPlay && state.midiPlayIniting) return { display: true, disabled: true };
  439. return {
  440. display: true,
  441. disabled: false,
  442. };
  443. });
  444. /** 重播按钮 */
  445. resetBtn = computed(() => {
  446. // 没有音源不显示
  447. if (state.noMusicSource) return { display: false, disabled: false };
  448. // 选择模式 不显示
  449. if (headTopData.modeType !== "show") return { display: false, disabled: false };
  450. // 评测模式 不显示,跟练模式 不显示
  451. if (["evaluating", "follow"].includes(state.modeType)) return { display: false, disabled: true };
  452. // 播放状态 不显示
  453. if (state.playState === "play") return { display: false, disabled: true };
  454. // 播放进度为0 不显示
  455. const currentTime = getAudioCurrentTime();
  456. // midi音频未初始化完成不可点击
  457. if (state.isAppPlay && state.midiPlayIniting) return { display: false, disabled: true };
  458. if (!currentTime) return { display: false, disabled: true };
  459. return {
  460. display: true,
  461. disabled: false,
  462. };
  463. });
  464. /** 重置按钮 */
  465. const restoreBtn = computed(() => {
  466. // 播放中 禁用
  467. if (state.playState === "play" || evaluatingData.startBegin || followData.start || state.isHomeWork) return { display: true, disabled: true };
  468. return {
  469. disabled: false,
  470. display: true,
  471. };
  472. });
  473. const browInfo = browser();
  474. /** 返回 */
  475. const handleBack = () => {
  476. // 如果是乐教通,点击返回按钮,需要关闭当前窗口
  477. if (query["isYjt"] == "1") {
  478. window.parent.postMessage(
  479. {
  480. api: "api_YjtClose"
  481. },
  482. "*"
  483. );
  484. return
  485. }
  486. // 练习作业,完整练习才记录练习次数
  487. // HANDLE_WORK_ADD();
  488. // 不在APP中,
  489. if (!storeData.isApp) {
  490. window.parent.postMessage(
  491. {
  492. api: "back",
  493. },
  494. "*"
  495. );
  496. window.close();
  497. return;
  498. }
  499. if ((browInfo.iPhone || browInfo.ios) && state.isHomeWork) {
  500. setTimeout(() => {
  501. api_back();
  502. }, 550);
  503. return;
  504. }
  505. api_back();
  506. };
  507. /** 根据参数设置模式 */
  508. const getQueryModelSetModelType = () => {
  509. /** 作业模式 start, 如果为作业模式不处理,让作业模块处理 */
  510. if (state.isHomeWork) {
  511. return;
  512. }
  513. /** 作业模式 end */
  514. if (state.defaultModeType == 1) {
  515. headTopData.handleChangeModeType("practise");
  516. // if (state.platform === IPlatform.PC || state.isPreView) {
  517. // headTopData.showBack = false;
  518. // }
  519. if (state.isPreView) {
  520. headTopData.showBack = false;
  521. }
  522. } else {
  523. if (query.modelType) {
  524. if (query.modelType === "practise") {
  525. headTopData.handleChangeModeType("practise");
  526. } else if (query.modelType === "evaluating") {
  527. headTopData.handleChangeModeType("evaluating");
  528. }
  529. headTopData.showBack = false;
  530. } else {
  531. setTimeout(() => {
  532. headTopData.modeType = "init";
  533. }, 500);
  534. }
  535. }
  536. };
  537. /** 课件播放 */
  538. const changePlay = (res: any) => {
  539. // console.log('监听上课页面message',res)
  540. if (res?.data?.api === "setPlayState") {
  541. togglePlay("paused", true);
  542. }
  543. if(res?.data?.api === 'togglePlayState') {
  544. // if(state.playState === "play") {
  545. // togglePlay("paused");
  546. // }
  547. // if(state.playState === 'paused') {
  548. // togglePlay("play");
  549. // }
  550. console.log('togglePlayState', state.playState)
  551. togglePlay(state.playState === "play" ? "paused" : "play");
  552. }
  553. // 上课页面,按钮方向
  554. if (res?.data?.api === "imagePos") {
  555. if (res?.data.data) {
  556. state.playBtnDirection = res.data.data === "right" ? "right" : "left";
  557. // if (state.fingeringInfo.direction === "vertical" && state.setting.displayFingering) {
  558. // state.musicScoreBtnDirection = state.playBtnDirection === 'right' ? 'left' : 'right';
  559. // } else {
  560. // state.musicScoreBtnDirection = state.playBtnDirection;
  561. // }
  562. state.musicScoreBtnDirection = state.playBtnDirection;
  563. }
  564. }
  565. };
  566. const parentClassName = "settingBoxClass_drag";
  567. const userId = storeData.user?.id ? String(storeData.user?.id) : "";
  568. const positionInfo =
  569. state.platform !== IPlatform.PC
  570. ? {
  571. styleDrag: { value: null },
  572. }
  573. : useDrag([`${parentClassName} .top_draging`, `${parentClassName} .bom_drag`], parentClassName, toRef(headTopData, "settingMode"), userId);
  574. const speedClassName = "speedBoxClass_drag";
  575. const speedInfo =
  576. state.platform !== IPlatform.PC
  577. ? {
  578. styleDrag: { value: null },
  579. }
  580. : useDrag([`${speedClassName} .top_draging`, `${speedClassName} .bom_drag`], speedClassName, toRef(headData, "speedShow"), userId);
  581. onMounted(() => {
  582. getQueryModelSetModelType();
  583. window.addEventListener("message", changePlay);
  584. if (state.platform === IPlatform.PC) {
  585. showGuide.value = true;
  586. } else {
  587. showStudentGuide.value = true;
  588. }
  589. if (query.showWebGuide === "false") {
  590. showWebGuide.value = false;
  591. }
  592. document.addEventListener("keydown", (e: KeyboardEvent) => {
  593. if (e.code === "Tab") {
  594. e.stopPropagation();
  595. e.preventDefault();
  596. // onStartPlayState();
  597. togglePlay(state.playState === "play" ? "paused" : "play");
  598. }
  599. });
  600. });
  601. onUnmounted(() => {
  602. window.removeEventListener("message", changePlay);
  603. });
  604. const noticeBarWidth = ref<number>();
  605. watch(
  606. () => smoothAnimationState.isShow.value,
  607. () => {
  608. // NoticeBar能不能滚动
  609. if ((smoothAnimationState.isShow.value || state.isCombineRender) && isMusicList.value) {
  610. nextTick(() => {
  611. const widthCon = (document.querySelector("#noticeBarRollDom .van-notice-bar__content") as any)?.offsetWidth || undefined;
  612. noticeBarWidth.value = widthCon;
  613. });
  614. }
  615. },
  616. { immediate: true }
  617. );
  618. // 设置改变触发
  619. watch(state.setting, () => {
  620. console.log(state.setting, "state.setting");
  621. store.set("musicscoresetting", state.setting);
  622. });
  623. const isPad = navigator?.userAgent?.includes("UAWEIVRD-W09") || browInfo?.iPad || browInfo.isTablet;
  624. return () => (
  625. <>
  626. <div
  627. class={[styles.headerTop, state.platform === IPlatform.PC && state.musicScoreBtnDirection === "left" ? styles.headerTopRight : ""]}
  628. onClick={(e: Event) => {
  629. e.stopPropagation();
  630. if (state.platform === IPlatform.PC) {
  631. // 显示隐藏菜单
  632. window.parent.postMessage(
  633. {
  634. api: "onAttendToggleMenu",
  635. },
  636. "*"
  637. );
  638. }
  639. }}
  640. >
  641. {/* 返回和标题 */}
  642. {
  643. <div id="noticeBarRollDom" class={[styles.headTopLeftBox, (state.playState == "play" || followData.practiceStart || evaluatingData.startBegin) && styles.headTopLeftHide]}>
  644. {
  645. !query.isMove && !query.isHideBack && <img src={iconBack} class={["headTopBackBtn", styles.img, !headTopData.showBack && styles.hidenBack]} onClick={handleBack} />
  646. }
  647. {smoothAnimationState.isShow.value || state.isCombineRender ? (
  648. <div
  649. style={
  650. noticeBarWidth.value
  651. ? {
  652. "--noticeBarWidth": noticeBarWidth.value + "px",
  653. }
  654. : {}
  655. }
  656. class={[styles.title, state.isCbsView && styles.blackTitle, "headeTopTitleBtn", isPad && styles.isIpad]}
  657. onClick={() => {
  658. isMusicList.value && (musicListShow.value = true);
  659. }}
  660. >
  661. {isMusicList.value && <div class={[styles.symbolNote, "driver-8"]}></div>}
  662. <NoticeBar text={state.examSongName} background="none" />
  663. </div>
  664. ) : (
  665. isMusicList.value && (
  666. <img
  667. src={listImg}
  668. class={[styles.img, styles.listImg, "driver-8"]}
  669. onClick={() => {
  670. musicListShow.value = true;
  671. }}
  672. />
  673. )
  674. )}
  675. </div>
  676. }
  677. {/* 模式提醒 */}
  678. {/* {state.modeType === "practise" && (
  679. <div class={[styles.modeWarn, "practiseModeWarn", state.platform === IPlatform.PC && state.musicScoreBtnDirection === "left" ? styles.modeWarnRight : ""]}>
  680. <img src={state.playType === "play" ? headImg("perform1.png") : headImg("sing1.png")} />
  681. <div>{state.playType === "play" ? "演奏场景" : "演唱场景"}</div>
  682. </div>
  683. )} */}
  684. {/* 功能按钮 */}
  685. <div
  686. class={[styles.headRight]}
  687. onClick={(e: Event) => {
  688. e.stopPropagation();
  689. }}
  690. >
  691. {/* 模式切换 */}
  692. {
  693. <div
  694. id={state.platform === IPlatform.PC ? "teacherTop-0" : "studnetT-0"}
  695. style={{ display: toggleBtn.value.display ? "" : "none"}}
  696. class={["driver-9", styles.btn, toggleBtn.value.disabled && styles.disabled, styles.modeType]}
  697. onClick={() => {
  698. headTopData.oldModeType = state.modeType;
  699. handleRessetState();
  700. headTopData.modeType = "init";
  701. }}
  702. >
  703. <img class={styles.iconBtn} src={iconMode} />
  704. <span>{state.modeType === "practise" ? "练习模式" : state.modeType === "follow" ? "跟练模式" : state.modeType === "evaluating" ? "评测模式" : ""}</span>
  705. </div>
  706. }
  707. {/* 一行谱模式,暂不支持节拍指针 */}
  708. {/* {(
  709. <div
  710. class={[styles.btn, state.platform === IPlatform.PC ? styles.pcBtn : ""]}
  711. onClick={() => {
  712. // 切换光标模式
  713. let mode = metronomeData.cursorMode;
  714. if (["follow"].includes(state.modeType)) {
  715. mode = metronomeData.cursorMode === 1 ? 3 : 1;
  716. } else {
  717. mode = metronomeData.cursorMode === 1 ? 3 : metronomeData.cursorMode === 2 ? 1 : 2;
  718. }
  719. metronomeData.cursorMode = mode;
  720. }}
  721. >
  722. <img class={styles.iconBtn} src={headImg(metronomeData.cursorMode === 1 ? "cursor_icon1.png" : metronomeData.cursorMode === 2 ? "cursor_icon2.png" : metronomeData.cursorMode === 3 ? "cursor_icon3.png" : "")} />
  723. <span class={styles.iconContent}>
  724. {metronomeData.cursorMode === 1 ? "音符指针" : metronomeData.cursorMode === 2 ? "节拍指针" : metronomeData.cursorMode === 3 ? "关闭指针" : ""}
  725. {metronomeData.cursorTips && (
  726. <div class={[styles["botton-tips"], metronomeData.cursorMode === 3 ? styles.tipSpec : ""]}>{metronomeData.cursorTips}</div>
  727. )}
  728. </span>
  729. </div>
  730. )} */}
  731. <div
  732. style={{ display: playTypeBtn.value.display ? "" : "none" }}
  733. class={["driver-2", styles.btn, playTypeBtn.value.disabled && styles.disabled, styles.playType]}
  734. onClick={() => {
  735. const oldPlayType = state.playType;
  736. headTopData.oldPlayType = oldPlayType;
  737. const oldPlaySource = state.playSource;
  738. if (state.playType === "play") {
  739. state.playType = "sing";
  740. state.playSource = state.fanSong ? "music" : state.banSong ? "background" : "mingSong";
  741. } else {
  742. state.playType = "play";
  743. state.playSource = state.music ? "music" : "background";
  744. }
  745. isClickMode = true;
  746. // 有指法并且显示指法的时候 切换到演唱模式 需要影藏指法
  747. let isRefresh = false;
  748. if (state.isShowFingering && state.fingeringInfo.name && (state.setting.displayFingering || displayFingeringCache)) {
  749. if (state.playType === "sing") {
  750. state.setting.displayFingering = false;
  751. displayFingeringCache = true;
  752. } else {
  753. state.setting.displayFingering = displayFingeringCache;
  754. displayFingeringCache = false;
  755. }
  756. // 如果是竖屏指法和一行谱的时候 改变指法值的时候state 会调用刷新 refreshMusicSvg 所以下面不调用
  757. if (state.fingeringInfo.direction === "vertical" && !state.isSingleLine) {
  758. isRefresh = true;
  759. }
  760. }
  761. // 有歌词的时候,切换播放模式,需要重新渲染谱面 指法不刷新谱面的时候
  762. if (state.xmlHasLyric && !isRefresh) {
  763. refreshMusicSvg();
  764. } else if (!isRefresh) {
  765. handlerModeChange(oldPlayType, oldPlaySource, true);
  766. }
  767. }}
  768. >
  769. <img style={{ display: state.playType === "play" ? "" : "none" }} class={styles.iconBtn} src={headImg(`perform.png`)} />
  770. <img style={{ display: state.playType === "play" ? "none" : "" }} class={styles.iconBtn} src={headImg(`sing.png`)} />
  771. <span>{state.playType === "play" ? "演奏" : "演唱"}</span>
  772. </div>
  773. <div
  774. id={state.platform === IPlatform.PC ? "teacherTop-1" : "studnetT-1"}
  775. style={{ display: originBtn.value.display ? "" : "none" }}
  776. class={["driver-3", styles.btn, originBtn.value.disabled && styles.disabled, state.playType === "play" ? styles.playSource : styles.songSource]}
  777. onClick={async () => {
  778. const oldPlayType = state.playType;
  779. const oldPlaySource = state.playSource;
  780. if (state.playType === "play") {
  781. if (state.playSource === "music") {
  782. state.playSource = state.accompany ? "background" : "mingSong";
  783. } else if (state.playSource === "background") {
  784. state.playSource = state.mingSong ? "mingSong" : "music";
  785. } else {
  786. state.playSource = state.music ? "music" : "background";
  787. }
  788. } else {
  789. if (state.playSource === "music") {
  790. state.playSource = state.banSong ? "background" : "mingSong";
  791. } else if (state.playSource === "background") {
  792. state.playSource = state.mingSong ? "mingSong" : "music";
  793. } else {
  794. state.playSource = state.fanSong ? "music" : "background";
  795. }
  796. }
  797. await handlerModeChange(oldPlayType, oldPlaySource);
  798. // 总谱 并且开启了单个声轨音频时候
  799. if(state.isCombineRender && state.playSource === "background") {
  800. audioData.combineIndex = -1
  801. state.music = ""
  802. }
  803. showToast({
  804. message: state.playType === "play" ? (state.playSource === "music" ? "已切换为原声" : state.playSource === "background" ? "已切换为伴奏" : "已切换为唱名") : state.playSource === "music" ? "已切换为范唱" : state.playSource === "background" ? "已切换为伴唱" : "已切换为唱名",
  805. position: "top",
  806. className: "selectionToast",
  807. });
  808. }}
  809. >
  810. <img style={{ display: state.playSource === "music" ? "" : "none" }} class={styles.iconBtn} src={state.playType === "play" ? headImg(`music.png`) : headImg(`music1.png`)} />
  811. <img style={{ display: state.playSource === "background" ? "" : "none" }} class={styles.iconBtn} src={state.playType === "play" ? headImg(`background.png`) : headImg(`background1.png`)} />
  812. <img style={{ display: state.playSource === "mingSong" ? "" : "none" }} class={styles.iconBtn} src={headImg(`ming_icon.png`)} />
  813. <span>{state.playSource === "music" ? (state.playType === "play" ? "原声" : "范唱") : state.playSource === "background" ? (state.playType === "play" ? "伴奏" : "伴唱") : "唱名"}</span>
  814. </div>
  815. <div
  816. id={state.platform === IPlatform.PC ? "teacherTop-2" : "studnetT-2"}
  817. style={{ display: selectBtn.value.display ? "" : "none" }}
  818. class={["driver-4", styles.btn, selectBtn.value.disabled && styles.disabled, styles.section, state.sectionStatus && styles.isSection]}
  819. onClick={() => handleChangeSection()}
  820. >
  821. <img style={{ display: state.section.length === 0 ? "" : "none" }} class={styles.iconBtn} src={headImg(`section0.png`)} />
  822. <img style={{ display: state.section.length === 1 ? "" : "none" }} class={styles.iconBtn} src={headImg(`section1.png`)} />
  823. <img style={{ display: state.section.length === 2 ? "" : "none" }} class={styles.iconBtn} src={headImg(`section2.png`)} />
  824. <span>选段</span>
  825. </div>
  826. {
  827. <>
  828. <div
  829. style={{ display: metronomeBtn.value.display ? "" : "none" }}
  830. class={["driver-5", styles.btn, styles.metronomeBtn, metronomeBtn.value.disabled && styles.disabled, headData.speedShow && styles.isSpeed, styles.speed]}
  831. onClick={async () => {
  832. headData.speedShow = !headData.speedShow;
  833. }}
  834. >
  835. <img style={{ display: metronomeData.disable ? "block" : "none" }} class={styles.iconBtn} src={headImg("tickon.png")} />
  836. <img style={{ display: !metronomeData.disable ? "block" : "none" }} class={styles.iconBtn} src={headImg("tickoff.png")} />
  837. <span style={{ whiteSpace: "nowrap" }}>节拍</span>
  838. <div class={styles.speedCon}>
  839. <img src={headImg(`${state.speedIcon}.png`)} />
  840. <div>{Math.floor(state.speed)}</div>
  841. </div>
  842. </div>
  843. {
  844. <Popup v-model:show={headData.speedShow} class="popup-custom van-scale center-closeBtn speedBoxClass_drag" transition="van-scale" teleport="body" style={speedInfo.styleDrag.value} overlay-style={{ background: "rgba(0, 0, 0, 0.7)" }}>
  845. <Speed />
  846. {state.platform === IPlatform.PC && <Dragbom showGuide={!state.guideInfo?.teacherDrag} onGuideDone={handleGuide} />}
  847. </Popup>
  848. }
  849. </>
  850. }
  851. {/* {state.enableNotation ? (
  852. <Popover trigger="manual" v-model:show={headData.musicTypeShow} class={state.platform === IPlatform.PC && styles.pcTransPop} placement={state.platform === IPlatform.PC ? "top-end" : "bottom-end"} overlay={false} offset={state.platform === IPlatform.PC ? [0, 40] : [0, 8]}>
  853. {{
  854. reference: () => (
  855. <div
  856. id={state.platform === IPlatform.PC ? "teacherTop-5" : "studnetT-5"}
  857. style={{ display: converBtn.value.display ? "" : "none" }}
  858. class={[styles.btn, converBtn.value.disabled && styles.disabled]}
  859. onClick={(e: Event) => {
  860. e.stopPropagation();
  861. headData.musicTypeShow = !headData.musicTypeShow;
  862. }}
  863. >
  864. <img class={styles.iconBtn} src={headImg("icon_zhuanpu.svg")} />
  865. <span>{state.musicRenderType === "staff" ? "转简谱" : "转五线谱"}</span>
  866. </div>
  867. ),
  868. default: () => <MusicType />,
  869. }}
  870. </Popover>
  871. ) : null} */}
  872. {state.musicRendered && !query.lessonTrainingId && !query.questionId && state.isConcert && (
  873. <div
  874. class={[styles.btn, state.playState === "play" && fingeringBtn.value.disabled && styles.disabled, toggleMusicSheet.show && styles.isMusicSheet, styles.musicSheet, "driver-10"]}
  875. onClick={() => {
  876. toggleMusicSheet.toggle(true);
  877. }}
  878. >
  879. <img class={styles.iconBtn} src={headImg(`shenggui.png`)} />
  880. <span>声部</span>
  881. </div>
  882. )}
  883. {
  884. <div
  885. class={[styles.btn, restoreBtn.value.disabled && styles.disabled, "driver-5-1"]}
  886. onClick={() => resetSettings()}
  887. >
  888. <img class={styles.iconBtn} src={headImg("reset.png")} />
  889. <span>重置</span>
  890. </div>
  891. }
  892. <div
  893. id={state.platform === IPlatform.PC ? "teacherTop-6" : "studnetT-6"}
  894. style={{ display: settingBtn.value.display ? "" : "none" }}
  895. class={["driver-6", styles.btn, settingBtn.value.disabled && styles.disabled, headTopData.settingMode && styles.isSettingMode, styles.settingMode]}
  896. onClick={() => (headTopData.settingMode = true)}
  897. >
  898. <img class={styles.iconBtn} src={headImg("icon_menu.png")} />
  899. <span>设置</span>
  900. </div>
  901. </div>
  902. </div>
  903. {/** 指法点击区域 */}
  904. {
  905. state.fingeringInfo.direction === "transverse" && state.setting.displayFingering ?
  906. <div class={styles.headerMid} onClick={() => {
  907. fingerRef.value?.doubeClick()
  908. }}></div> : null
  909. }
  910. {/* 播放按钮 */}
  911. <div
  912. id="studnetT-7"
  913. style={{ display: playBtn.value.display ? "" : "none" }}
  914. class={[
  915. // 引导使用的类
  916. "driver-1",
  917. styles.playBtn,
  918. playBtn.value.disabled && styles.disabled,
  919. state.platform === IPlatform.PC && state.musicScoreBtnDirection === "left" ? styles.playLeftButton : state.platform === IPlatform.PC && state.musicScoreBtnDirection === "right" ? styles.playRightButton : "",
  920. ]}
  921. onClick={() => {
  922. // C调能播放唱名,非C调时,只有谱面类型是首调时,才能播放唱名
  923. if (!state.isCTone && state.playSource === 'mingSong') {
  924. const notPlayDesc = state.musicRenderType === EnumMusicRenderType.staff ? '该曲目的五线谱目前还不支持播放唱名' : state.musicRenderType === EnumMusicRenderType.fixedTone ? '该曲目的固定调目前还不支持播放唱名' : '';
  925. if (notPlayDesc) {
  926. showToast({
  927. message: notPlayDesc,
  928. position: "top",
  929. className: "selectionToast",
  930. });
  931. return
  932. }
  933. }
  934. togglePlay(state.playState === "play" ? "paused" : "play")
  935. }}
  936. >
  937. <div class={styles.btnWrap}>
  938. <img style={{ display: state.playState === "play" ? "none" : "" }} class={styles.iconBtn} src={headImg("icon_play.png")} />
  939. <img style={{ display: state.playState === "play" ? "" : "none" }} class={styles.iconBtn} src={headImg("icon_pause.png")} />
  940. <Circle style={{ opacity: state.playState === "play" ? 1 : 0 }} class={styles.progress} stroke-width={60} stroke-linecap={"square"} currentRate={state.playProgress} rate={100} color="#FFED78" layer-color="rgba(255,255,255,0.5)" />
  941. </div>
  942. </div>
  943. {/* 重播按钮 */}
  944. <div
  945. id="tips-step-9"
  946. style={{ display: resetBtn.value.display ? "" : "none" }}
  947. class={[styles.resetBtn, resetBtn.value.disabled && styles.disabled, state.platform === IPlatform.PC && state.musicScoreBtnDirection === "left" ? styles.pauseLeftButton : state.platform === IPlatform.PC && state.musicScoreBtnDirection === "right" ? styles.pauseRightButton : ""]}
  948. onClick={() => handleResetPlay()}
  949. >
  950. <img class={styles.iconBtn} src={headImg("icon_reset.png")} />
  951. </div>
  952. <Popup v-model:show={headTopData.settingMode} class="popup-custom van-scale center-closeBtn settingBoxClass_drag" transition="van-scale" teleport="body" style={positionInfo.styleDrag.value} overlay-style={{ background: "rgba(0, 0, 0, 0.7)" }}>
  953. <Settting />
  954. {state.platform === IPlatform.PC && <Dragbom showGuide={!state.guideInfo?.teacherDrag} onGuideDone={handleGuide} />}
  955. </Popup>
  956. {/* 模式切换 */}
  957. {/* <ModeTypeMode /> */}
  958. <ModeView></ModeView>
  959. {/* isAllBtns */}
  960. {/* {isAllBtns.value && !query.isCbs && showGuideIndex.value && <TeacherTop></TeacherTop>}
  961. {isAllBtnsStudent.value && !query.isCbs && showGuideIndex.value && <StudentTop></StudentTop>} */}
  962. {/* 练习模式功能引导 加载音频完成 不是会员 */}
  963. {state.modeType === "practise" && headTopData.modeType !== "init" && !query.isCbs && state.audioDone && !state.isLoading && showWebGuide.value && (
  964. <PractiseDriver
  965. statusAll={{
  966. playBtnStatus: playBtn.value.display,
  967. subjectStatus: state.musicRendered && !query.lessonTrainingId && !query.questionId && state.isConcert,
  968. modelTypeStatus: toggleBtn.value.display,
  969. playType: playTypeBtn.value.display,
  970. originPlayType: state.playType === "play" ? true : false,
  971. originBtnStatus: originBtn.value.display,
  972. backTitle: !(state.playState == "play" || followData.start || evaluatingData.startBegin) && isMusicList.value,
  973. titleType: smoothAnimationState.isShow.value ? "TEXT" : isMusicList.value ? "IMG" : "NONE",
  974. showSwitchList: isMusicList.value && !state.isHomeWork && !query.isHideMusicList,
  975. }}
  976. />
  977. )}
  978. {/* 跟练模式功能引导 加载音频完成 不是会员 */}
  979. {state.modeType === "follow" && headTopData.modeType !== "init" && !query.isCbs && state.audioDone && !state.isLoading && showWebGuide.value && (
  980. <FollowDriver
  981. statusAll={{
  982. subjectStatus: state.musicRendered && !query.lessonTrainingId && !query.questionId && state.isConcert,
  983. }}
  984. />
  985. )}
  986. {/* 评测模式功能引导 加载音频完成 不是会员 */}
  987. {state.modeType === "evaluating" && headTopData.modeType !== "init" && !evaluatingData.earphoneMode && !query.isCbs && state.audioDone && !state.isLoading && evaluatingData.websocketState && !evaluatingData.startBegin && evaluatingData.checkEnd && showWebGuide.value && (
  988. <EvaluatingDriver
  989. statusAll={{
  990. subjectStatus: state.musicRendered && !query.lessonTrainingId && !query.questionId && state.isConcert,
  991. }}
  992. />
  993. )}
  994. </>
  995. );
  996. },
  997. });