index.tsx 20 KB

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