index.tsx 19 KB

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