index.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589
  1. import { defineComponent, Directive, Ref, ref, Transition, Teleport, nextTick, computed, onMounted } from 'vue'
  2. import { Button, Cell, CellGroup, Dialog, Divider, Popover, Slider, Switch } from 'vant'
  3. import ButtonIcon from './icon'
  4. import runtime, * as RuntimeUtils from '/src/pages/detail/runtime'
  5. import Speed from '/src/pages/detail/speed'
  6. import detailState from '/src/pages/detail/state'
  7. import SettingState from '/src/pages/detail/setting-state'
  8. import appState from '/src/state'
  9. import FloatWraper from './float-wraper'
  10. import Evaluating, { evaluatStopPlay } from './evaluating'
  11. import iconTitle from '../popups/evaluating/icons/title.svg'
  12. import iconCancel from '../popups/evaluating/icons/cancel.svg'
  13. import iconConfirm from '../popups/evaluating/icons/confirm.svg'
  14. import { useClientType, useMenu, useOriginSearch, useReload } from '../uses'
  15. import { permissionPopup } from '../popups/permission/permission'
  16. import { open as openMusicList } from '../music-list'
  17. import { postMessage } from '/src/helpers/native-message'
  18. import Popups from '../popups'
  19. import Setting from '../popups/setting'
  20. import evastyles from '../popups/evaluating/index.module.less'
  21. import ModelWraper from './model-wraper'
  22. import Follow from '../popups/follow'
  23. import { switchProps } from '../popups/setting/evaluat'
  24. import iconBack from './icons/icon-back.png'
  25. import iconFollowEndBtn from '../popups/follow/icons/icon-followEndBtn.png'
  26. import iconEvaluatingEnd from './icons/icon-evaluatingEnd.png'
  27. import iconCameraOff from './icons/icon-camera-off.png'
  28. import iconCameraOn from './icons/icon-camera-on.png'
  29. import iconToggle from './icons/icon_toggle.png'
  30. import store from 'store'
  31. import styles from './index.module.less'
  32. import { sendBackRecordTotalTime } from '../App'
  33. import { unitTestData } from '../unitTest'
  34. import { toggleMusicSheet } from '../plugins/toggleMusicSheet'
  35. import classNames from 'classnames'
  36. import Metronome, { metronomeData } from '/src/helpers/metronome'
  37. import { getAllNodes } from '/src/pages/detail/helpers'
  38. import { submitMaxScore } from '/src/subpages/colexiu/buttons/evaluating'
  39. export const confirmShow: Ref<boolean> = ref(false)
  40. /**评测开始按钮状态 */
  41. export const startButtonShow = ref(true)
  42. export const evaluatingRef: Ref<any> = ref({})
  43. export const settingPopup: Ref<any> = ref(null)
  44. export const suggestPopup: Ref<any> = ref(null)
  45. export const followRef = ref<any>(null)
  46. let openSuggestPopupFn = () => {}
  47. export const openSuggestPopup = () => {
  48. openSuggestPopupFn()
  49. }
  50. /**
  51. * 前置验证是否在APP中并且已经付费
  52. * @param cb 回调函数 {status} 验证状态
  53. * @returns
  54. */
  55. const beforeCheck = (cb: (status: boolean) => void) => {
  56. const search = useOriginSearch()
  57. const setting = (search.setting || {}) as any
  58. const chargeType = detailState.activeDetail?.paymentType
  59. const orderStatus = detailState.activeDetail?.orderStatus
  60. const play = detailState.activeDetail?.play
  61. const membershipDays = appState.user?.membershipDays || 0
  62. if (useClientType() === 'web' || play || setting.feeType === 'FREE') {
  63. return cb(true)
  64. }
  65. if (
  66. chargeType?.includes('VIP') &&
  67. chargeType?.includes('CHARGE') &&
  68. !(membershipDays > 0) &&
  69. orderStatus !== 'PAID'
  70. ) {
  71. permissionPopup.active = 'memberAndDemand'
  72. permissionPopup.show = true
  73. return cb(false)
  74. }
  75. if (chargeType === 'VIP' && !(membershipDays > 0)) {
  76. permissionPopup.active = 'member'
  77. permissionPopup.show = true
  78. return cb(false)
  79. }
  80. if (chargeType === 'CHARGE' && orderStatus !== 'PAID') {
  81. permissionPopup.active = 'demand'
  82. permissionPopup.show = true
  83. return cb(false)
  84. }
  85. cb(true)
  86. }
  87. const back: () => void = () => {
  88. // 如果是乐教通,点击返回按钮,需要关闭当前窗口
  89. if (window.location.href.includes('isYjt')) {
  90. window.parent.postMessage(
  91. {
  92. api: "api_YjtClose"
  93. },
  94. "*"
  95. );
  96. return
  97. }
  98. submitMaxScore()
  99. sendBackRecordTotalTime()
  100. postMessage({
  101. api: 'back',
  102. })
  103. }
  104. export type IModelType = 'practice' | 'evaluation' | 'follow' | 'init'
  105. export const modelType = ref<IModelType>('init')
  106. export const onChangeModelType = (type: IModelType) => {
  107. runtime.initShow = true;
  108. if (type === modelType.value) return
  109. // 跟练模式,光标只有音符模式,无节拍模式
  110. if (type === 'follow' && metronomeData.cursorMode === 2) {
  111. metronomeData.cursorMode = 1
  112. }
  113. if (type === 'evaluation') {
  114. // RuntimeUtils.changeSpeed(detailState.activeDetail?.originalSpeed, false)
  115. // 评测模式
  116. runtime.evaluatingStatus = true
  117. modelType.value = type
  118. } else {
  119. const speeds = store.get('speeds') || {}
  120. const search = useOriginSearch()
  121. const speed = speeds[search.id as any]
  122. // 还原速度
  123. if (speed) {
  124. RuntimeUtils.changeSpeed(speeds[search.id as any])
  125. }
  126. }
  127. nextTick(() => {
  128. modelType.value = type
  129. })
  130. }
  131. export default defineComponent({
  132. name: 'Colexiu-Buttons',
  133. props: {
  134. onSetMusicScoreType: {
  135. type: Object,
  136. default: (n: any) => {},
  137. },
  138. },
  139. emits: ['setMusicScoreType'],
  140. setup(props, { emit }) {
  141. try {
  142. detailState.times = getAllNodes(runtime.osmd)
  143. // console.log('state.times', detailState.times)
  144. } catch (error) {
  145. console.log(error)
  146. }
  147. const search = useOriginSearch()
  148. const speedRef = ref()
  149. const [show] = useMenu()
  150. const camera = ref(false)
  151. //根据路由传参设置模式
  152. const useRouteSetModelType = () => {
  153. // 课后练习,只能选择练习模式
  154. const modelType: IModelType = search.lessonTrainingId ? 'practice' : search.modelType as IModelType
  155. if (modelType && modelType != 'evaluation') {
  156. onChangeModelType(modelType)
  157. }
  158. }
  159. onMounted(() => {
  160. useRouteSetModelType()
  161. })
  162. // 固定调
  163. const musicTypeShow = ref(false)
  164. const musicAction = ref('')
  165. const onSelect = (action: any) => {
  166. musicAction.value = action.text
  167. confirmShow.value = true
  168. }
  169. const hanldeSelect = () => {
  170. if (musicAction.value === '五线谱') {
  171. // if (SettingState.sett.type == 'staff') return
  172. SettingState.sett.type = 'staff'
  173. } else if (musicAction.value === '简谱') {
  174. // if (SettingState.sett.type === 'jianpu' && !SettingState.sett.keySignature) return
  175. SettingState.sett.type = 'jianpu'
  176. SettingState.sett.keySignature = false
  177. } else if (musicAction.value === '固定调') {
  178. // if (SettingState.sett.type === 'jianpu' && SettingState.sett.keySignature) return
  179. SettingState.sett.type = 'jianpu'
  180. SettingState.sett.keySignature = true
  181. }
  182. sessionStorage.setItem('notation', SettingState.sett.type)
  183. }
  184. const musicType = (type: string) => {
  185. if (type === 'staff') {
  186. return SettingState.sett.type === type
  187. } else if (type === 'shoudiao') {
  188. return SettingState.sett.type === 'jianpu' && !SettingState.sett.keySignature
  189. } else if (type === 'guding') {
  190. return SettingState.sett.type === 'jianpu' && SettingState.sett.keySignature
  191. }
  192. }
  193. return () => {
  194. const changeModeIsDisabled =
  195. (detailState.activeDetail?.isAppPlay
  196. ? detailState.activeDetail?.midiUrl === ''
  197. : runtime.isFirstPlay || runtime.audiosInstance?.length == 1) ||
  198. runtime.evaluatingStatus ||
  199. (detailState.activeDetail?.isAppPlay && detailState.midiPlayIniting)
  200. return (
  201. <div
  202. onClick={(e: Event) => e.stopPropagation()}
  203. class={[styles.container, show.value ? '' : styles.outUp]}
  204. style={search.headerHeight ? { height: '1rem', paddingTop: '0.25rem' } : ''}
  205. >
  206. <div class={styles.leftButton}>
  207. {(search?.modelType && !search.unitId) ? null : <img class={styles.backbtn} src={iconBack} onClick={back} />}
  208. {
  209. search.isHideBack === 'false' ? <img class={styles.backbtn} src={iconBack} onClick={back} /> : null
  210. }
  211. <div class={styles.titleWrap}>
  212. <div class={styles.title}>{detailState.activeDetail?.musicSheetName}</div>
  213. {search.albumName && <div class={styles.album}>{search.albumName}</div>}
  214. </div>
  215. </div>
  216. <div class={styles.centerButton}>
  217. <Transition name="finish">
  218. {!startButtonShow.value && modelType.value === 'evaluation' && (
  219. <Button
  220. class={[styles.button, styles.finish]}
  221. onClick={() => {
  222. evaluatingRef.value?.playerStop?.()
  223. }}
  224. >
  225. <img style={{ width: '100%', display: 'block' }} src={iconEvaluatingEnd} />
  226. </Button>
  227. )}
  228. </Transition>
  229. <Transition name="finish">
  230. {followRef?.value?.data.start && (
  231. <Button
  232. class={[styles.button, styles.finish, styles.followEndBtn]}
  233. onClick={() => {
  234. followRef.value?.handleEnd?.()
  235. }}
  236. >
  237. <img style={{ width: '100%', display: 'block' }} src={iconFollowEndBtn} />
  238. </Button>
  239. )}
  240. </Transition>
  241. </div>
  242. <div class={[styles.moreButton]} style={{ opacity: detailState.initRendered ? 1 : 0 }}>
  243. {!search?.modelType && modelType.value !== 'init' && !detailState.frozenMode && !detailState.isLessonTrain && (
  244. <Button
  245. data-step="m0"
  246. class={[styles.button, styles.hasText]}
  247. disabled={(runtime.evaluatingStatus && !startButtonShow.value) || followRef.value?.data.start}
  248. onClick={() => {
  249. // 不是课后训练选段和单元测验选段,切换模式去除选段
  250. if (!unitTestData.isSelectMeasureMode && detailState.sectionStatus) {
  251. RuntimeUtils.clearSectionStatus()
  252. }
  253. if (modelType.value === 'practice') {
  254. // 当前为练习模式,需要停止播放
  255. RuntimeUtils.resetPlayStatus()
  256. RuntimeUtils.setCurrentTime(0)
  257. }
  258. if (modelType.value === 'evaluation') {
  259. runtime.evaluatingStatus = false
  260. evaluatStopPlay()
  261. }
  262. RuntimeUtils.resetBaseRate();
  263. modelType.value = 'init'
  264. }}
  265. >
  266. <ButtonIcon
  267. key="modelType"
  268. name={
  269. ['init', 'practice'].includes(modelType.value)
  270. ? 'modelType'
  271. : ['follow'].includes(modelType.value)
  272. ? 'modelType1'
  273. : 'modelType2'
  274. }
  275. />
  276. <span>模式</span>
  277. </Button>
  278. )}
  279. <>
  280. <Button
  281. class={classNames(styles.button, styles.hasText, styles.minPadding)}
  282. disabled={detailState.isCombineRender}
  283. onClick={() => {
  284. // 切换光标模式
  285. let mode = metronomeData.cursorMode
  286. if (['follow'].includes(modelType.value)) {
  287. mode = metronomeData.cursorMode === 1 ? 3 : 1
  288. } else {
  289. mode = metronomeData.cursorMode === 3 ? 1 : metronomeData.cursorMode + 1
  290. }
  291. metronomeData.cursorMode = mode
  292. }}
  293. >
  294. <ButtonIcon key="modelType" name={metronomeData.cursorMode === 1 ? 'cursor-icon-1' : metronomeData.cursorMode === 2 ? 'cursor-icon-2' : metronomeData.cursorMode === 3 ? 'cursor-icon-3' : ''} />
  295. <span class={styles.iconContent}>
  296. {metronomeData.cursorMode === 1 ? '音符指针' : metronomeData.cursorMode === 2 ? '节拍指针' : metronomeData.cursorMode === 3 ? '关闭指针' : ''}
  297. {metronomeData.cursorTips && <>
  298. <i class={styles.arrowIcon}></i>
  299. <div class={classNames(styles['botton-tips'],metronomeData.cursorMode === 3 ? styles.tipSpec : '')}>{metronomeData.cursorTips}</div>
  300. </>}
  301. </span>
  302. </Button>
  303. </>
  304. {detailState.initRendered && !search.lessonTrainingId && !search.questionId && detailState.activeDetail?.musicSheetType == 'CONCERT' && (
  305. <Button
  306. class={[styles.button, styles.hasText]}
  307. onClick={() => {
  308. toggleMusicSheet.toggle(true)
  309. }}
  310. disabled={
  311. (runtime.evaluatingStatus && !startButtonShow.value) ||
  312. runtime.playState === 'play' ||
  313. followRef.value?.data.start
  314. }
  315. >
  316. <img src={iconToggle} />
  317. <span>声轨</span>
  318. </Button>
  319. )}
  320. {['practice', 'evaluation'].includes(modelType.value) && (
  321. <>
  322. {
  323. modelType.value === 'practice' ?
  324. <Button
  325. data-step="m1"
  326. class={[styles.button, styles.hasText]}
  327. onClick={() => RuntimeUtils.changeMode(runtime.mode === 'background' ? 'music' : 'background')}
  328. disabled={changeModeIsDisabled}
  329. >
  330. <ButtonIcon key="music" name={runtime.mode === 'music' ? 'music' : 'accompaniment'} />
  331. <span>{runtime.mode === 'background' ? '伴奏' : '原声'}</span>
  332. </Button> : null
  333. }
  334. {/* 如果为单元测试和课后训练 */}
  335. {unitTestData.isSelectMeasureMode ? null : (
  336. <Button
  337. data-step="m2"
  338. class={[styles.button, styles.hasText]}
  339. onClick={RuntimeUtils.sectionChange}
  340. disabled={runtime.playState === 'play'}
  341. >
  342. <ButtonIcon
  343. key="section"
  344. name={
  345. 'section' +
  346. (detailState.section.length && detailState.section.length <= 2
  347. ? detailState.section.length
  348. : '')
  349. }
  350. />
  351. <span>选段</span>
  352. </Button>
  353. )}
  354. {
  355. modelType.value === 'practice' ?
  356. <Button
  357. data-step="m3"
  358. class={[styles.button, styles.hasText]}
  359. disabled={runtime.playState === 'play'}
  360. onClick={() => {
  361. SettingState.sett.fingering = !SettingState.sett.fingering
  362. RuntimeUtils.event.emit('settingFingeringChange')
  363. }}
  364. >
  365. <ButtonIcon key="music" name={SettingState.sett.fingering ? 'fingeringOn' : 'fingeringOff'} />
  366. <span>指法</span>
  367. </Button> : null
  368. }
  369. </>
  370. )}
  371. {modelType.value === 'evaluation' && (
  372. <>
  373. <Popover
  374. v-model:show={camera.value}
  375. overlay={false}
  376. placement="bottom-end"
  377. class="cameraPopover"
  378. show-arrow={false}
  379. vSlots={{
  380. reference: () => (
  381. <div
  382. onClick={(e: Event) => {
  383. if (!startButtonShow.value) e.stopPropagation()
  384. }}
  385. >
  386. <Button class={[styles.button, styles.hasText]} disabled={!startButtonShow.value}>
  387. <img src={SettingState.sett.camera ? iconCameraOn : iconCameraOff} />
  388. <span>摄像头</span>
  389. </Button>
  390. </div>
  391. ),
  392. }}
  393. >
  394. <CellGroup border={false}>
  395. {
  396. <Cell center title="摄像头">
  397. <div style="display:flex;justify-content: flex-end;">
  398. <Switch v-model={SettingState.sett.camera} {...switchProps}>
  399. off
  400. </Switch>
  401. </div>
  402. </Cell>
  403. }
  404. {SettingState.sett.camera && (
  405. <Cell class="cameraOpacity" center title="透明度">
  406. <Slider
  407. style={{ width: '90%' }}
  408. min={0}
  409. max={100}
  410. v-model:modelValue={SettingState.sett.opacity}
  411. v-slots={{
  412. button: () => <div class={styles.slider}>{SettingState.sett.opacity}</div>,
  413. }}
  414. ></Slider>
  415. </Cell>
  416. )}
  417. </CellGroup>
  418. </Popover>
  419. <Evaluating ref={evaluatingRef} />
  420. </>
  421. )}
  422. {['practice', 'evaluation'].includes(modelType.value) && !search.lessonTrainingId && (
  423. <Popover
  424. trigger="manual"
  425. overlay={false}
  426. placement="bottom"
  427. class={styles.popover}
  428. show={show.value && runtime.speedShow && !(runtime.playState === 'play')}
  429. // @ts-ignore
  430. onUpdate:show={(show: boolean) => (runtime.speedShow = show)}
  431. vSlots={{
  432. reference: () => (
  433. <Button
  434. data-step="m4"
  435. class={[styles.button, styles.hasText, styles.speedButton]}
  436. // disabled={runtime.evaluatingStatus || runtime.playState === 'play'}
  437. disabled={runtime.playState === 'play'}
  438. onClick={() => {
  439. speedRef.value?.refUpdateSpeed(runtime.playIngSpeed || runtime.speed)
  440. runtime.speedShow = !runtime.speedShow
  441. }}
  442. >
  443. <ButtonIcon name="speed" />
  444. <span>速度</span>
  445. <span class={styles.label}>{runtime.playIngSpeed || runtime.speed}</span>
  446. </Button>
  447. ),
  448. }}
  449. >
  450. <Speed
  451. ref={speedRef}
  452. updateSpeed={(speed: number) => {
  453. runtime.speed = speed
  454. runtime.playIngSpeed = speed
  455. }}
  456. changed={RuntimeUtils.changeSpeed}
  457. mode={runtime.mode}
  458. changeMode={RuntimeUtils.changeMode}
  459. lib={{ speed: runtime.playIngSpeed || runtime.speed }}
  460. class={styles.speed}
  461. />
  462. </Popover>
  463. )}
  464. {detailState.activeDetail?.notation ? (
  465. <Popover
  466. class={styles.toggleMusicType}
  467. placement="bottom-end"
  468. show={musicTypeShow.value}
  469. // @ts-ignore
  470. onUpdate:show={(val: boolean) => {
  471. if (
  472. runtime.playState === 'play' ||
  473. (runtime.evaluatingStatus && !startButtonShow.value) ||
  474. followRef.value?.data.start
  475. ) {
  476. } else {
  477. musicTypeShow.value = val
  478. }
  479. }}
  480. >
  481. {{
  482. reference: () => (
  483. <Button
  484. disabled={
  485. runtime.playState === 'play' ||
  486. (runtime.evaluatingStatus && !startButtonShow.value) ||
  487. followRef.value?.data.start
  488. }
  489. class={[styles.button, styles.hasText, styles.speedButton]}
  490. >
  491. <ButtonIcon name="icon-zhuanpu" />
  492. <span>{musicType('staff') ? '转简谱' : '转五线谱'}</span>
  493. </Button>
  494. ),
  495. default: () => (
  496. <>
  497. <div role="menuitem" class="van-popover__action" onClick={() => onSelect({ text: '五线谱' })}>
  498. <ButtonIcon key="type" name={musicType('staff') ? 'icon-staff-active' : 'icon-staff'} />
  499. <div class={['action-text', musicType('staff') && 'action-active']}>五线谱</div>
  500. </div>
  501. <div role="menuitem" class="van-popover__action" onClick={() => onSelect({ text: '简谱' })}>
  502. <ButtonIcon key="type" name={musicType('shoudiao') ? 'shuodiao-active' : 'shuodiao'} />
  503. <div class={['action-text', musicType('shoudiao') && 'action-active']}>首调</div>
  504. </div>
  505. <div role="menuitem" class="van-popover__action" onClick={() => onSelect({ text: '固定调' })}>
  506. <ButtonIcon key="type" name={musicType('guding') ? 'guding-active' : 'guding'} />
  507. <div class={['action-text', musicType('guding') && 'action-active']}>固定调</div>
  508. </div>
  509. </>
  510. ),
  511. }}
  512. </Popover>
  513. ) : null}
  514. {detailState.initRendered && (
  515. <>
  516. <Button
  517. class={[styles.button, styles.hasText]}
  518. onClick={() => {
  519. settingPopup.value?.onShow()
  520. }}
  521. disabled={
  522. (runtime.evaluatingStatus && !startButtonShow.value) ||
  523. runtime.playState === 'play' ||
  524. followRef.value?.data.start
  525. }
  526. >
  527. <ButtonIcon name="setting" />
  528. <span>设置</span>
  529. </Button>
  530. <Popups
  531. ref={settingPopup}
  532. style={{
  533. borderRadius: '8px',
  534. }}
  535. >
  536. <Setting active={modelType.value == 'practice' ? '2' : modelType.value == 'evaluation' ? '3' : '1'} />
  537. </Popups>
  538. </>
  539. )}
  540. </div>
  541. <FloatWraper />
  542. <Dialog.Component
  543. teleport="body"
  544. class={evastyles.confirm}
  545. style={{
  546. overflow: 'initial',
  547. }}
  548. vSlots={{
  549. title: () => <img class={evastyles.iconTitle} src={iconTitle} />,
  550. footer: () => (
  551. <div class={evastyles.footer}>
  552. <img src={iconCancel} onClick={() => (confirmShow.value = false)} />
  553. <img
  554. src={iconConfirm}
  555. onClick={() => {
  556. hanldeSelect()
  557. useReload()
  558. }}
  559. />
  560. </div>
  561. ),
  562. }}
  563. v-model:show={confirmShow.value}
  564. message={'设置成功,是否立即重新加载?'}
  565. />
  566. </div>
  567. )
  568. }
  569. },
  570. })