index.tsx 20 KB

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