index.tsx 20 KB

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