index.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. import { useToggle } from '@vant/use'
  2. import { Button } from 'vant'
  3. import { defineComponent, nextTick, onMounted, onUnmounted, reactive, ref, Teleport, Transition, watch } from 'vue'
  4. import { useClientType, useOriginSearch } from '../../uses'
  5. import styles from './index.module.less'
  6. import { IPostMessage, listenerMessage, postMessage, removeListenerMessage } from '/src/helpers/native-message'
  7. import request from '/src/helpers/request'
  8. import { getPlatform, getRequestHostname } from '/src/helpers/utils'
  9. import { setStepIndex } from '/src/pages/detail/helpers'
  10. import state, { refreshView, setCurrentTime } from '/src/pages/detail/runtime'
  11. import detailState from '/src/pages/detail/state'
  12. import iconFollwBtn from './icons/icon-follwBtn.png'
  13. import { unitTestData } from '/src/subpages/colexiu/unitTest/index'
  14. import { formatPitch } from '/src/subpages/colexiu/buttons/evaluating'
  15. // 显示或隐藏播放按钮
  16. const togglePlayer = (show: boolean = false) => {
  17. let globalPlayer: HTMLElement = document.querySelector('#globalPlayer')!
  18. if (globalPlayer) {
  19. globalPlayer.style.display = show ? '' : 'none'
  20. }
  21. }
  22. export const data = reactive({
  23. list: [] as any, // 频率列表
  24. index: 0,
  25. start: false,
  26. times: [] as any[], // 音符列表
  27. endIndex: 0, // 如果为选段结束的小节
  28. })
  29. const [isOpen, changeOpen] = useToggle(true)
  30. const noteFrequency = ref(0)
  31. const audioFrequency = ref(0)
  32. const followTime = ref(0)
  33. // 切换录音
  34. const openToggleRecord = (open: boolean = true) => {
  35. postMessage({
  36. api: 'cloudToggleFollow',
  37. content: {
  38. state: open ? 'start' : 'end',
  39. },
  40. })
  41. // 记录跟练时长
  42. if (open) {
  43. followTime.value = Date.now()
  44. } else {
  45. const playTime = Date.now() - followTime.value
  46. if (followTime.value !== 0 && playTime > 0) {
  47. followTime.value = 0
  48. // 已有全局记录时长,不用单独记录了
  49. // updatePlayTime(playTime / 1000)
  50. }
  51. }
  52. }
  53. const initBehaviorId = '' + new Date().valueOf()
  54. /**
  55. * 记录时长
  56. */
  57. async function updatePlayTime(time: number) {
  58. const search = useOriginSearch()
  59. const behaviorId = sessionStorage.getItem('behaviorId') || search.behaviorId || initBehaviorId
  60. const prefix = getRequestHostname()
  61. const seearchid = useOriginSearch().id as string
  62. const id = location.hash.split('?')[0].split('/').pop() || seearchid || ''
  63. try {
  64. const res = await request.post('/musicPracticeRecord/save', {
  65. prefix: prefix,
  66. data: {
  67. musicSheetId: id,
  68. sysMusicScoreId: id,
  69. feature: search.feature,
  70. playTime: time,
  71. deviceType: getPlatform(),
  72. behaviorId,
  73. },
  74. })
  75. } catch (err) {}
  76. }
  77. // 清除音符状态
  78. const onClear = () => {
  79. detailState.times.forEach((item) => {
  80. const note: HTMLElement = document.querySelector(`div[data-vf=vf${item.id}]`)!
  81. if (note) {
  82. note.classList.remove('follow-up', 'follow-down', 'follow-error', 'follow-success')
  83. }
  84. const _note: HTMLElement = document.getElementById(`vf-${item.id}`)!
  85. if (_note) {
  86. _note.classList.remove('follow-up', 'follow-down')
  87. }
  88. })
  89. }
  90. /** 获取默认开始的音符 */
  91. const getDefaultIndex = () => {
  92. if (unitTestData.isSelectMeasureMode) {
  93. data.endIndex = detailState.times.findIndex(
  94. (n: any) => n.NoteToGraphicalNoteObjectId == detailState.section[1].NoteToGraphicalNoteObjectId
  95. )
  96. const index = detailState.times.findIndex(
  97. (n: any) => n.NoteToGraphicalNoteObjectId == detailState.section[0].NoteToGraphicalNoteObjectId
  98. )
  99. return index > -1 ? index : 0
  100. }
  101. return 0
  102. }
  103. // 开始
  104. const handleStart = () => {
  105. onClear()
  106. data.start = true
  107. openToggleRecord(true)
  108. data.index = getDefaultIndex()
  109. data.list = []
  110. setStepIndex(state.osmd, data.index)
  111. // state.osmd.cursor.reset()
  112. getNoteIndex()
  113. refreshView()
  114. }
  115. // 结束
  116. const handleEnd = () => {
  117. data.start = false
  118. openToggleRecord(false)
  119. data.index = getDefaultIndex()
  120. setStepIndex(state.osmd, data.index)
  121. // state.osmd.cursor.reset()
  122. getNoteIndex()
  123. }
  124. // 下一个
  125. const next = () => {
  126. if (state.osmd.product) {
  127. state.osmd.cursor.setPosition(detailState.times[data.index].cursorBox)
  128. } else {
  129. state.osmd.cursor.next()
  130. }
  131. refreshView()
  132. }
  133. // 获取当前音符
  134. const getNoteIndex = (): any => {
  135. const item = detailState.times[data.index]
  136. if (!item.frequency) {
  137. data.index = data.index + 1
  138. next()
  139. return getNoteIndex()
  140. }
  141. noteFrequency.value = item.frequency
  142. detailState.fixedKey = item.realKey
  143. return {
  144. // ...item,
  145. id: item.id,
  146. min: item.frequency - (item.frequency - item.noteElement.pitch.prevFrequency) * 0.1,
  147. max: item.frequency + (item.noteElement.pitch.nextFrequency - item.frequency) * 0.1,
  148. duration: item.duration,
  149. baseFrequency: formatPitch(item.noteElement.pitch.frequency),
  150. }
  151. }
  152. let checking = false
  153. const onFollowTime = (evt?: IPostMessage) => {
  154. if (unitTestData.isSelectMeasureMode && data.index >= data.endIndex) {
  155. handleEnd()
  156. return
  157. }
  158. const frequency: number = evt?.content?.frequency
  159. audioFrequency.value = frequency
  160. data.list.push(frequency)
  161. checked()
  162. }
  163. const checked = () => {
  164. if (checking) return
  165. checking = true
  166. const item = getNoteIndex()
  167. for (let i = 0; i < data.list.length; i++) {
  168. const frequency = data.list[i]
  169. if (frequency > item.min && frequency < item.max) {
  170. console.log(item.min, frequency, item.max)
  171. next()
  172. data.index += 1
  173. data.list = data.list.slice(i + 1)
  174. setColor(item, '', true)
  175. checking = false
  176. return
  177. }
  178. }
  179. setColor(item, audioFrequency.value > item.baseFrequency ? 'follow-up' : 'follow-down')
  180. checking = false
  181. }
  182. const setColor = (item: any, state: 'follow-up' | 'follow-down' | '', isRight = false) => {
  183. const note: HTMLElement = document.querySelector(`div[data-vf=vf${item.id}]`)!
  184. if (note) {
  185. note.classList.remove('follow-up', 'follow-down', 'follow-error', 'follow-success')
  186. if (isRight) {
  187. note.classList.add('follow-success')
  188. } else {
  189. note.classList.add('follow-error', state)
  190. }
  191. }
  192. const _note: HTMLElement = document.getElementById(`vf-${item.id}`)!
  193. if (_note) {
  194. _note.classList.remove('follow-up', 'follow-down')
  195. state && _note.classList.add(state)
  196. }
  197. }
  198. export default defineComponent({
  199. name: 'follow',
  200. setup(props, { expose }) {
  201. onMounted(() => {
  202. togglePlayer()
  203. listenerMessage('cloudFollowTime', onFollowTime)
  204. })
  205. onUnmounted(() => {
  206. removeListenerMessage('cloudFollowTime', onFollowTime)
  207. togglePlayer(true)
  208. onClear()
  209. setStepIndex(state.osmd, 0)
  210. })
  211. expose({
  212. data,
  213. handleEnd,
  214. })
  215. return () => (
  216. <Teleport to="#colexiu-detail-music-sheet">
  217. <div class={styles.follow}>
  218. <Transition name="start" duration={300}>
  219. {!data.start && (
  220. <Button
  221. style={{
  222. backgroundImage: `url(${iconFollwBtn})`,
  223. marginLeft: detailState.isSpecialShapedScreen ? `${detailState.notchHeight / 4}px` : '',
  224. }}
  225. class={[styles.button, styles.start, styles.followBtn]}
  226. onClick={() => handleStart()}
  227. ></Button>
  228. )}
  229. </Transition>
  230. <div style={{ display: data.start ? "" : "none" }} class={styles.noteState}>
  231. <span style={{ background: "#977CFF" }} class={styles.dot}></span>
  232. <span>低</span>
  233. <span style={{ background: "rgb(255, 0, 0)" }} class={styles.dot}></span>
  234. <span>高</span>
  235. </div>
  236. {/* <div class={styles.title}>
  237. <span>音符频率: {noteFrequency.value.toFixed(2)}</span>
  238. <span style={{ color: 'red', marginLeft: '10px' }}>拾音频率: {audioFrequency.value.toFixed(2)}</span>
  239. <span style={{ marginLeft: '10px' }}>拾音长度: {data.list.length}</span>
  240. </div> */}
  241. </div>
  242. </Teleport>
  243. )
  244. },
  245. })