index.tsx 18 KB

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