liushengqiang 1 year ago
parent
commit
79d40bf3c0
57 changed files with 1110 additions and 471 deletions
  1. 40 0
      image2code.js
  2. 11 0
      package-lock.json
  3. 2 2
      package.json
  4. 61 0
      src/helpers/communication.ts
  5. 12 0
      src/helpers/eventemitter.ts
  6. 22 2
      src/helpers/formateMusic.ts
  7. 0 382
      src/helpers/multiple-audio.ts
  8. 3 4
      src/page-gym/App.tsx
  9. 20 7
      src/page-gym/detail/index.tsx
  10. 54 0
      src/page-gym/evaluat-model/earphone/index.module.less
  11. 23 0
      src/page-gym/evaluat-model/earphone/index.tsx
  12. BIN
      src/page-gym/evaluat-model/icons/1.png
  13. BIN
      src/page-gym/evaluat-model/icons/2.png
  14. BIN
      src/page-gym/evaluat-model/icons/3.png
  15. BIN
      src/page-gym/evaluat-model/icons/4.png
  16. BIN
      src/page-gym/evaluat-model/icons/5.png
  17. 14 0
      src/page-gym/evaluat-model/icons/arrow-left-background.svg
  18. BIN
      src/page-gym/evaluat-model/icons/bad.png
  19. 15 0
      src/page-gym/evaluat-model/icons/close.svg
  20. 15 0
      src/page-gym/evaluat-model/icons/close2.svg
  21. BIN
      src/page-gym/evaluat-model/icons/erji.png
  22. BIN
      src/page-gym/evaluat-model/icons/good.png
  23. BIN
      src/page-gym/evaluat-model/icons/great.png
  24. 1 0
      src/page-gym/evaluat-model/icons/index.json
  25. BIN
      src/page-gym/evaluat-model/icons/left-bg.png
  26. BIN
      src/page-gym/evaluat-model/icons/perfect.png
  27. BIN
      src/page-gym/evaluat-model/icons/title.png
  28. 27 0
      src/page-gym/evaluat-model/index.module.less
  29. 206 0
      src/page-gym/evaluat-model/index.tsx
  30. 54 0
      src/page-gym/evaluat-model/sound-effect/data.ts
  31. BIN
      src/page-gym/evaluat-model/sound-effect/icons/bg-note.png
  32. BIN
      src/page-gym/evaluat-model/sound-effect/icons/bg.png
  33. BIN
      src/page-gym/evaluat-model/sound-effect/icons/child.png
  34. BIN
      src/page-gym/evaluat-model/sound-effect/icons/content-bg.png
  35. BIN
      src/page-gym/evaluat-model/sound-effect/icons/dot-active.png
  36. BIN
      src/page-gym/evaluat-model/sound-effect/icons/dot-error.png
  37. BIN
      src/page-gym/evaluat-model/sound-effect/icons/dot.png
  38. 11 0
      src/page-gym/evaluat-model/sound-effect/icons/icon-sound_120.svg
  39. 11 0
      src/page-gym/evaluat-model/sound-effect/icons/icon-sound_12_4.svg
  40. 10 0
      src/page-gym/evaluat-model/sound-effect/icons/icon-sound_13.svg
  41. 11 0
      src/page-gym/evaluat-model/sound-effect/icons/icon-sound_14_15.svg
  42. 11 0
      src/page-gym/evaluat-model/sound-effect/icons/icon-sound_5_6.svg
  43. 11 0
      src/page-gym/evaluat-model/sound-effect/icons/icon-sound_default.svg
  44. BIN
      src/page-gym/evaluat-model/sound-effect/icons/notes.png
  45. 98 0
      src/page-gym/evaluat-model/sound-effect/index.module.less
  46. 84 0
      src/page-gym/evaluat-model/sound-effect/index.tsx
  47. 3 2
      src/page-gym/main.ts
  48. 21 5
      src/state.ts
  49. 2 1
      src/store.ts
  50. 5 5
      src/utils/index.ts
  51. 77 60
      src/utils/native-message.ts
  52. 163 0
      src/view/evaluating/index.tsx
  53. 0 1
      src/view/music-score/index.tsx
  54. 5 0
      src/view/selection/index.module.less
  55. 5 0
      src/view/selection/index.tsx
  56. 1 0
      src/view/selection/scoreIcon.json
  57. 1 0
      vite.config.ts

+ 40 - 0
image2code.js

@@ -0,0 +1,40 @@
+const fs = require('fs')
+const path = require('path')
+
+// 指法文件夹位置
+// const filesDir = path.join(__dirname, './fingering')
+// const filesDir = path.join(__dirname, './pages/detail')
+const filesDir = path.join(__dirname, './src/page-gym/evaluat-model/icons')
+console.log("🚀 ~ filesDir:", filesDir, path.join(filesDir, 'index.json'))
+
+// 需要处理的文件后缀
+const suffixs = ['png', 'svg']
+
+const files = fs.readdirSync(path.resolve(filesDir))
+// console.log("🚀 ~ files:", files)
+
+;(async function() {
+  let i = 0
+  const exportJson = {}
+  for (const file of files) {
+    const suffix = file.slice(file.lastIndexOf('.') + 1)
+    // console.log("🚀 ~ suffix:", suffix)
+    if (!suffixs.includes(suffix)) continue;
+    const dirFullPath = path.join(filesDir, file)
+    // console.log("🚀 ~ dirFullPath:", dirFullPath)
+    fs.stat(dirFullPath, (err, stat) => {
+      if (!err && !stat.isDirectory()) {
+        const fileNames = file.split('.')
+        const fileBuffer =  fs.readFileSync(dirFullPath)
+        console.log("🚀 ~ fileBuffer:", fileNames[0])
+        const fileType = suffix === 'svg' ? 'svg+xml' : suffix
+        const str = `data:image/${fileType};base64,` + Buffer.from(fileBuffer, 'binary').toString('base64')
+        exportJson[fileNames[0]] = str
+        
+        fs.writeFileSync(path.join(filesDir, 'index.json'), JSON.stringify(exportJson, null, 2))
+      }
+    })
+    // if (i === 0) break;
+  }
+  
+})()

+ 11 - 0
package-lock.json

@@ -12,6 +12,7 @@
         "clean-deep": "^3.4.0",
         "consola": "^2.15.3",
         "dayjs": "^1.11.7",
+        "eventemitter3": "^5.0.0",
         "howler": "^2.2.3",
         "lodash": "^4.17.21",
         "query-string": "^8.1.0",
@@ -3049,6 +3050,11 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/eventemitter3": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.0.tgz",
+      "integrity": "sha512-riuVbElZZNXLeLEoprfNYoDSwTBRR44X3mnhdI1YcnENpWTCsTTVZ2zFuqQcpoyqPQIUXdiPEU0ECAq0KQRaHg=="
+    },
     "node_modules/fast-glob": {
       "version": "3.2.12",
       "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
@@ -6629,6 +6635,11 @@
       "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
       "dev": true
     },
+    "eventemitter3": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.0.tgz",
+      "integrity": "sha512-riuVbElZZNXLeLEoprfNYoDSwTBRR44X3mnhdI1YcnENpWTCsTTVZ2zFuqQcpoyqPQIUXdiPEU0ECAq0KQRaHg=="
+    },
     "fast-glob": {
       "version": "3.2.12",
       "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",

+ 2 - 2
package.json

@@ -2,9 +2,8 @@
   "name": "cloud-exercise",
   "private": true,
   "version": "0.0.0",
-  "type": "module",
   "scripts": {
-    "dev": "vite --force",
+    "dev": "vite --host --force",
     "build": "vue-tsc && vite build",
     "preview": "vite preview --open"
   },
@@ -13,6 +12,7 @@
     "clean-deep": "^3.4.0",
     "consola": "^2.15.3",
     "dayjs": "^1.11.7",
+    "eventemitter3": "^5.0.0",
     "howler": "^2.2.3",
     "lodash": "^4.17.21",
     "query-string": "^8.1.0",

+ 61 - 0
src/helpers/communication.ts

@@ -0,0 +1,61 @@
+import { browser } from "../utils";
+import { CallBack, IPostMessage, listenerMessage, postMessage, promisefiyPostMessage } from "../utils/native-message";
+
+let isApp = (): boolean => {
+	const browserInfo = browser();
+	isApp = () => browserInfo.isApp;
+	return isApp();
+};
+/**获取耳机的插入状态 */
+export const getEarphone = (): Promise<IPostMessage | undefined> => {
+	if (!isApp()) return Promise.resolve({} as any);
+	return promisefiyPostMessage({ api: "isWiredHeadsetOn" });
+};
+
+/** 获取异形屏信息 */
+export const isSpecialShapedScreen = (): Promise<IPostMessage | undefined> => {
+	if (!isApp()) return Promise.resolve({} as any);
+	return promisefiyPostMessage({ api: "isSpecialShapedScreen" });
+};
+
+/** 开始录音 */
+export const startSoundCheck = () => {
+	postMessage({
+		api: "startSoundCheck",
+	});
+};
+
+/** 录音返回 */
+export const sendResult = (callback: CallBack) => {
+	listenerMessage("sendResult", callback);
+};
+
+/** 结束录音 */
+export const endSoundCheck = () => {
+	postMessage({
+		api: "endSoundCheck",
+	});
+};
+
+/** 开始评测 */
+export const startEvaluating = (content: any): Promise<IPostMessage | undefined> => {
+   if (!isApp()) return Promise.resolve({} as any);
+	return promisefiyPostMessage({ api: "startEvaluating", content: content });
+};
+/** 结束评测 */
+export const endEvaluating = (content: any): Promise<IPostMessage | undefined> => {
+   if (!isApp()) return Promise.resolve({} as any);
+	return promisefiyPostMessage({ api: "endEvaluating", content: content });
+};
+
+/** 评测开始录音 */
+export const startRecording = (): Promise<IPostMessage | undefined> => {
+   if (!isApp()) return Promise.resolve({} as any);
+	return promisefiyPostMessage({ api: "startRecording" });
+};
+
+/** 和websocket通信 */
+export const proxyServiceMessage = (content: any): Promise<IPostMessage | undefined> => {
+   if (!isApp()) return Promise.resolve({} as any);
+	return promisefiyPostMessage({ api: "proxyServiceMessage", content });
+}

+ 12 - 0
src/helpers/eventemitter.ts

@@ -0,0 +1,12 @@
+import eventemitter from 'eventemitter3'
+
+const _event = new eventemitter()
+
+export enum EventEnum {
+    /** 评测结果返回 */
+    sendResultScore = 'sendResultScore',
+    /** 播放器播放到后面自动结束 */
+    playEnd = 'playEnd',
+}
+
+export default _event

+ 22 - 2
src/helpers/formateMusic.ts

@@ -792,7 +792,7 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 			// 如果有节拍器,需要将节拍器的时间算出来
 			if (i === 0) {
 				fixtime += getFixTime(beatSpeed);
-				console.log("🚀 ~ fixtime:", fixtime, beatSpeed)
+				// console.log("🚀 ~ fixtime:", fixtime, beatSpeed)
 			}
 			// console.log(getTimeByBeatUnit(beatUnit, measureSpeed, iterator.currentMeasure.activeTimeSignature.Denominator))
 			let gradualLength = 0;
@@ -907,15 +907,18 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 			}
 			// console.log(note.tie)
 			const nodeDetail = {
+				isStaccato: note.voiceEntry.isStaccato(),
 				isRestFlag: note.isRestFlag,
 				noteId: note.NoteToGraphicalNoteObjectId,
 				measureListIndex: note.sourceMeasure.measureListIndex,
 				MeasureNumberXML: note.sourceMeasure.MeasureNumberXML,
 				_noteLength: _noteLength,
 				svgElement: svgElement,
+				frequency: note?.pitch?.frequency || -1,
+				nextFrequency: note?.pitch?.nextFrequency || -1,
+				prevFrequency: note?.pitch?.prevFrequency || -1,
 				difftime,
 				octaveOffset: activeVerticalMeasureList[0]?.octaveOffset,
-				frequency: note.pitch?.frequency,
 				speed,
 				beatSpeed,
 				i,
@@ -975,3 +978,20 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 	state.activeMeasureIndex = sortArray[0].MeasureNumberXML
 	return sortArray;
 };
+
+/** 获取小节之间的连音线,仅同音高*/
+export const getNoteByMeasuresSlursStart = (note: any) => {
+	let activeNote = note;
+	let tieNote;
+	if (note.noteElement.tie && note.noteElement.tie.StartNote) {
+		tieNote = note.noteElement.tie.StartNote;
+	}
+	if (activeNote && tieNote && tieNote !== activeNote.noteElement) {
+		for (const note of state.times) {
+			if (tieNote === note.noteElement) {
+				return note;
+			}
+		}
+	}
+	return activeNote;
+};

+ 0 - 382
src/helpers/multiple-audio.ts

@@ -1,382 +0,0 @@
-/** 播放多个音频 */
-export default class MultipleAudio {
-
-  audios: any = {}
-
-  audioList: string[] = []
-
-  length: number = 0
-
-  status: 'init' | 'play' | 'pause' = 'init'
-
-  speed: number = 90
-
-  muted: boolean = false
-
-  audio: null | HTMLAudioElement = null
-
-
-  // group = new Pizzicato.Group([])
-
-  currentTime: number = 0
-
-  duration: number = 0
-
-  timer: any = null
-
-  accelerateRefreshPlayer = () => {
-    if (this.timer) {
-      return
-    }
-    const prevTime = this.currentTime
-    let now = new Date().getTime()
-    this.timer = setInterval(() => {
-      this.currentTime = (new Date().getTime() - now) / 1000 + prevTime
-      // console.log(this.currentTime)
-      this.event.emit('timeupdate', this)
-    }, 10)
-  }
-
-  clearAccelerateRefreshPlayer = () => {
-    clearInterval(this.timer)
-    this.timer = null
-  }
-
-  constructor(list: string[]) {
-    this.setSongs(list)
-    // this.event.on('timeupdate', () => {
-    //   console.log(this.currentTime)
-    // })
-  }
-
-  async setSongs(list: string[]) {
-    this.audioList = list.filter(item => !!item)
-    this.audio = null
-    this.audios = {}
-    const filterReqs = list.filter(item => !!item).map(async url => ({
-      // bolb: await request.get(url, {responseType: 'blob'}),
-      url
-    }))
-    const res = await Promise.all(filterReqs)
-    for (const item of res) {
-      const audio = new Audio(item.url)
-      // audio.controls = true
-      // document.body.append(audio)
-      audio.load()
-      this.audios[item.url] = audio
-      if (!this.audio) {
-        this.audio = audio
-      }
-    }
-    // console.log(filterReqs.length, this.audio)
-    this.length = filterReqs.length
-    if (this.audio) {
-      this.audio.addEventListener('loadedmetadata', evt => {
-        // this.duration = this.audio?.duration || 0
-        // this.event.emit('loadedmetadata', evt, this.audio?.duration)
-        this.setDuration()
-      })
-      this.audio.addEventListener('timeupdate', evt => {
-        this.currentTime = this.audio?.currentTime || 0
-        this.event.emit('timeupdate', evt)
-        let used = false
-        if (this.currentTime === this.duration && !used) {
-          used = true
-          // this.event.emit('ended')
-          // this.audio?.dispatchEvent(endedEvent)
-          // for (const key in this.audios) {
-          //   if (Object.prototype.hasOwnProperty.call(this.audios, key)) {
-          //     const audio = this.audios[key]
-          //     const endedEvent = new Event('ended')
-          //     audio.dispatchEvent(endedEvent)
-          //   }
-          // }
-          // console.log('ended')
-        }
-      })
-      // this.audio.addEventListener('ended', () => {
-      //   this.setCurrentTime(0)
-      //   // this.play()
-      // })
-    }
-    if (list.length) {
-      this.status = this.getStatus()
-    }
-    this.event.on('allWaiting', () => {
-      if (this.hasWaitng()) {
-        this.event.emit('waiting')
-      }
-    })
-    this.event.on('allPlaying', () => {
-      if (!this.hasWaitng()) {
-        this.event.emit('playing')
-      }
-    })
-    this.syncEvent()
-  }
-
-  setDuration(aus?: any) {
-    const audios: HTMLAudioElement[] = Object.values(aus || this.audios || {})
-    if (audios.length) {
-      const times: number[] = []
-      for (const item of audios) {
-        const duration = (item as HTMLAudioElement).duration
-        if (duration > 0) {
-          times.push(duration)
-        }
-      }
-      const num = Math.floor(Math.max(...times) -  Math.min(...times))
-      if (num >= 1){
-        console.log('该教程原音与伴奏时长超过' + num + '秒,请修改后使用')
-        // Dialog.alert({
-        //   message: '该教程原音与伴奏时长超过' + num + '秒,请修改后使用',
-        // })
-      }
-      this.duration = Math.min(...times)
-
-      if (this.duration > 0) {
-        this.event?.emit('loadedmetadata', null, this.duration)
-      }
-    }
-  }
-
-  destroyed() {
-    this.pause()
-    this.event.removeAllListeners()
-    this.audio = null
-    // Object.values(this.audios).map(item => item.remove())
-    this.audios = {}
-  }
-
-  hasWaitng() {
-    let status = false
-    for (const audio of Object.values(this.audios)) {
-      if ((audio as any).dataset.status === 'waiting') {
-        status = true
-        break
-      }
-    }
-    return status
-  }
-
-  syncEvent() {
-    let isEnded = false
-    const play = (evt: Event) => {
-      isEnded = false
-      // console.log('实际延迟', new Date().getTime() - starttime)
-      // console.log('开始触发play事件', new Date().getTime())
-      this.event.emit('play', evt)
-      // console.log(this.audioList[0])
-      if (compareURL((evt.target as HTMLAudioElement)?.src, this.audioList[0])) {
-        playStartTime = new Date().getTime()
-      }
-      // this.play()
-    }
-    const pause = async (evt: Event) => {
-      await this.pause()
-      this.event.emit('pause', evt)
-      if (compareURL((evt.target as HTMLAudioElement)?.src, this.audioList[0])) {
-        const playTime = new Date().getTime() - playStartTime
-        playStartTime = new Date().getTime()
-        this.event.emit('updatePlayTime', playTime / 1000)
-      }
-    }
-    const waiting = (evt: any) => {
-      if (this.status === 'play') {
-        evt.target.dataset.status = 'waiting'
-      }
-      this.event.emit('allWaiting')
-    }
-    const playing = (evt: any) => {
-      evt.target.dataset.status = ''
-      this.event.emit('allPlaying')
-    }
-    const ended = async (evt: Event) => {
-      if (!isEnded) {
-        isEnded = true
-        await this.pause()
-      }
-      for (const key in this.audios) {
-        if (Object.prototype.hasOwnProperty.call(this.audios, key)) {
-          this.event.emit('ended', {
-            target: this.audios[key]
-          })
-          // const audio = this.audios[key]
-          // const endedEvent = new Event('ended')
-          // audio.dispatchEvent(endedEvent)
-        }
-      }
-      // this.event.emit('ended', evt)
-    }
-    // for (const audio of Object.values(this.audios)) {
-    //   audio.removeEventListener('play', play)
-    //   audio.removeEventListener('pause', pause)
-    //   audio.removeEventListener('waiting', waiting)
-    //   audio.removeEventListener('playing', playing)
-    //   audio.removeEventListener('ended', ended)
-    // }
-    for (const audio of Object.values(this.audios)) {
-      (audio as HTMLAudioElement).addEventListener('loadedmetadata', () => this.setDuration(this.audios))
-      ;(audio as HTMLAudioElement).addEventListener('play', play)
-      ;(audio as HTMLAudioElement).addEventListener('pause', pause)
-      ;(audio as HTMLAudioElement).addEventListener('waiting', waiting)
-      ;(audio as HTMLAudioElement).addEventListener('playing', playing)
-      ;(audio as HTMLAudioElement).addEventListener('ended', ended)
-    }
-      // this.audio?.addEventListener('play', play)
-      // this.audio?.addEventListener('pause', pause)
-      // this.audio?.addEventListener('waiting', waiting)
-      // this.audio?.addEventListener('playing', playing)
-      // this.audio?.addEventListener('ended', ended)
-  }
-
-  getStatus() {
-    return !this.audio ? 'init' : this.audio?.paused ? 'pause' : 'play'
-  }
-
-  play(delay?: number) {
-    let plused = false
-    if (this.getStatus() !== 'play') {
-      return new Promise((resolve) => {
-        setTimeout(() => {
-          starttime = new Date().getTime()
-          Object.values(this.audios).map(async (item: any, inedx: number) => {
-            // console.log('play duration', item.duration)
-            await item.play()
-            if (!plused) {
-              plused = true
-              // console.log('starttime', starttime)
-              // console.log('延迟时间', new Date().getTime() - starttime)
-              // runtime.evaluatingFixTime += new Date().getTime() - starttime
-            }
-          })
-          resolve(this.audios)
-          // Promise.all(Object.values(this.audios).map(async (item: any) => await item.play()))
-          //   .then(res => {
-          //     this.status = this.getStatus()
-          //     resolve(res)
-          //     return res
-          //   })
-        }, (delay || 100))
-      })
-    }
-    this.status = this.getStatus()
-    return Promise.resolve()
-  }
-
-  pause() {
-    this.status = this.getStatus()
-    return Promise.all(Object.values(this.audios).map(async (item: any) => await item.pause()))
-    .then(res => {
-      this.status = this.getStatus()
-      return res
-    })
-  }
-
-  setVolume = (none: boolean, cb: () => void) => {
-    let timer = setInterval(() => {
-      Object.values(this.audios).map((item: any) => {
-        if (none) {
-          item.volume -= 0.01
-          if (item.volume <= 0.01) {
-            item.volume = 0
-            clearInterval(timer)
-            cb && cb()
-          }
-        } else {
-          item.volume += 0.01
-          if (item.volume >= 1) {
-            item.volume = 1
-            clearInterval(timer)
-            cb && cb()
-          }
-        }
-        console.log(item.volume)
-      })
-    }, 16.7)
-  }
-
-  setMute(muted: boolean, url?: string) {
-    if (url) {
-      if (this.audios[url]) {
-        // this.setVolume(muted, () => this.audios[url].muted = muted)
-        this.audios[url].muted = muted
-        // this.audios[url].volume = (muted ? 0 : 1)
-      }
-    } else {
-      this.muted = muted
-      // this.setVolume(muted, () => {
-        Object.values(this.audios).map((item: any) => item.muted = muted)
-      // })
-    }
-  }
-
-  setSpeed(speed: number, url?: string) {
-    if (url) {
-      if (this.audios[url]) {
-        this.audios[url].playbackRate = speed
-        // this.audios[url].speed = speed
-      }
-    } else {
-      this.speed = speed
-      // this.group.speed = speed
-      Object.values(this.audios).map((item: any) => {
-        item.playbackRate = speed
-        // console.log(item.getSourceNode().playbackRate.value = speed)
-        return item
-      })
-    }
-  }
-
-
-  setCurrentTime(time: number) {
-    this.currentTime = time
-    // if (this.status === 'play') {
-    //   this.pause()
-    //   this.play()
-    // }
-    Object.values(this.audios).map((item: any) => item.currentTime = time)
-  }
-
-  toggleMute(url?: string) {
-    if (url) {
-      if (this.audios[url]) {
-        this.audios[url].muted = !this.audios[url].muted
-      }
-    } else {
-      Object.values(this.audios).map((item: any) => item.muted = !this.muted)
-      this.muted = !this.muted
-    }
-  }
-
-  togglePlay(delay?: number) {
-    if (this.getStatus() === 'pause') {
-      this.play(delay)
-    } else if (this.getStatus() === 'play') {
-      this.setMute(true)
-      this.pause()
-    }
-  }
-}
-
-export interface IAudios {
-  [key: string]: HTMLAudioElement
-}
-
-export interface IMultipleAudio {
-  audios: IAudios
-  audio: null | HTMLAudioElement
-  status: string
-  muted: boolean
-  speed: number
-  length: number
-  event: EventEmitterType
-  play(): void
-  pause(): void
-  setMute(muted: boolean, url?: string): void
-  setSpeed(speed: number, url?: string): void
-  togglePlay(): void
-  toggleMute(): void
-  setCurrentTime(time: number): void
-  destroyed: () => void
-}

+ 3 - 4
src/page-gym/App.tsx

@@ -3,7 +3,7 @@ import { computed, defineComponent, onBeforeMount } from "vue";
 import { RouterView } from "vue-router";
 import TheError from "../components/The-error";
 import { setUserInfo, storeData } from "../store";
-import { setToken } from "../utils";
+import { getRandomKey, setToken } from "../utils";
 import { getQuery } from "../utils/queryString";
 import Notfind from "../view/notfind";
 import { employeeQueryUserInfo, studentQueryUserInfo, teacherQueryUserInfo } from "./api";
@@ -26,9 +26,7 @@ export default defineComponent({
 		const setUser = async () => {
 			const res = await getUserInfo();
 			const { student } = res?.data || {};
-			setUserInfo({
-				membershipEndTime: student.membershipEndTime,
-			});
+			setUserInfo(student);
 			// console.log("🚀 ~ res:", res);
 		};
 		onBeforeMount(() => {
@@ -37,6 +35,7 @@ export default defineComponent({
 			}
 			setUser();
 			document.getElementById('loading')!.className = ''
+			localStorage.setItem("behaviorId", getRandomKey());
 		});
 
 		const inited = computed(() => {

+ 20 - 7
src/page-gym/detail/index.tsx

@@ -1,5 +1,5 @@
 import { Skeleton } from "vant";
-import { defineComponent, onMounted, reactive, Transition } from "vue";
+import { defineComponent, onBeforeMount, onMounted, reactive, Transition } from "vue";
 import { useRoute } from "vue-router";
 import { formateTimes } from "../../helpers/formateMusic";
 import Metronome, { metronomeData } from "../../helpers/metronome";
@@ -13,6 +13,7 @@ import { sysMusicScoreAccompanimentQueryPage, sysMusicScoreCategoriesQueryTree }
 import EvaluatModel from "../evaluat-model";
 import HeaderTop from "../header-top";
 import styles from "./index.module.less";
+import { isSpecialShapedScreen } from "/src/helpers/communication";
 import Evaluating from "/src/view/evaluating";
 import MeasureSpeed from "/src/view/plugins/measure-speed";
 import Selection from "/src/view/selection";
@@ -28,9 +29,22 @@ export default defineComponent({
 		const detailData = reactive({
 			isLoading: true,
 			svgRendered: false, // 曲谱渲染完成
-			/** 可以加载点击浮层  */
-			showSelection: false,
+			showSelection: false, // 可以加载点击浮层
+			paddingLeft: "",
 		});
+		const getAPPData = async () => {
+			const screenData = await isSpecialShapedScreen();
+			if (screenData?.content) {
+				// console.log("🚀 ~ screenData:", screenData.content);
+				const { isSpecialShapedScreen, notchHeight } = screenData.content;
+				if (isSpecialShapedScreen) {
+					detailData.paddingLeft = 25 + "px";
+				}
+			}
+		};
+		onBeforeMount(() => {
+			getAPPData();
+		})
 		// console.log(route.params, query)
 		/** 获取曲谱数据 */
 		const getMusicInfo = (res: any) => {
@@ -71,6 +85,7 @@ export default defineComponent({
 		};
 
 		const setState = (data: any, index: number) => {
+			state.detailId = data.id;
 			state.xmlUrl = data.xmlUrl;
 			state.partIndex = index;
 			state.subjectId = data.subjectId;
@@ -127,7 +142,7 @@ export default defineComponent({
 			detailData.showSelection = true;
 		};
 		return () => (
-			<div class={styles.detail}>
+			<div class={styles.detail} style={{ paddingLeft: detailData.paddingLeft }}>
 				{!detailData.svgRendered && (
 					<div class={styles.skeleton}>
 						<Skeleton class={styles.skeleton} row={8} />
@@ -151,9 +166,7 @@ export default defineComponent({
 					)}
 					{!detailData.isLoading && <AudioList />}
 				</div>
-				<div class={styles.plugins}>
-					{detailData.svgRendered && <MeasureSpeed />}
-				</div>
+				<div class={styles.plugins}>{detailData.svgRendered && <MeasureSpeed />}</div>
 			</div>
 		);
 	},

+ 54 - 0
src/page-gym/evaluat-model/earphone/index.module.less

@@ -0,0 +1,54 @@
+.fraction {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    color: #fff;
+    background-color: #fff;
+    border-radius: 18px;
+    min-width: 244px;
+}
+
+.title {
+    position: relative;
+    width: 100px;
+    height: 30px;
+    top: -4.5px;
+
+    img {
+        display: block;
+        width: 100%;
+        height: 100%;
+    }
+
+    .titleDes {
+        position: absolute;
+        left: 0;
+        right: 0;
+        bottom: 0;
+        top: 0;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        font-size: 15px;
+    }
+}
+
+.erji {
+    width: 150px;
+}
+
+.tip {
+    font-size: 13px;
+    color: #808080;
+}
+
+.btn {
+    width: 106px;
+    height: 31px;
+    margin: 11px 0 17px 0;
+    line-height: 31px;
+    text-align: center;
+    background: #01C1B5;
+    border-radius: 36px;
+    font-size: 13px;
+}

+ 23 - 0
src/page-gym/evaluat-model/earphone/index.tsx

@@ -0,0 +1,23 @@
+import { defineComponent } from "vue";
+import styles from "./index.module.less";
+import icons from "../icons/index.json";
+
+export default defineComponent({
+	name: "earphone",
+	emits: ["close"],
+	setup(props, { emit }) {
+		return () => (
+			<div class={styles.fraction}>
+				<div class={styles.title}>
+					<img src={icons.title} />
+					<div class={styles.titleDes}>提示</div>
+				</div>
+				<img class={styles.erji} src={icons.erji} />
+				<div class={styles.tip}>请佩戴耳机以保证测评准确率~</div>
+				<div class={styles.btn} onClick={() => emit("close")}>
+					确定
+				</div>
+			</div>
+		);
+	},
+});

BIN
src/page-gym/evaluat-model/icons/1.png


BIN
src/page-gym/evaluat-model/icons/2.png


BIN
src/page-gym/evaluat-model/icons/3.png


BIN
src/page-gym/evaluat-model/icons/4.png


BIN
src/page-gym/evaluat-model/icons/5.png


+ 14 - 0
src/page-gym/evaluat-model/icons/arrow-left-background.svg

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>箭头</title>
+    <g id="云教练2版" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="画板" transform="translate(-21.000000, -19.000000)">
+            <g id="箭头" transform="translate(21.000000, 19.000000)">
+                <circle id="椭圆形" fill="#01C1B5" cx="16" cy="16" r="16"></circle>
+                <g id="编组" transform="translate(16.100000, 16.533333) scale(-1, 1) translate(-16.100000, -16.533333) translate(6.600000, 8.533333)" fill="#FFFFFF" fill-rule="nonzero">
+                    <path d="M11.3783644,15.2991467 L17.9125501,8.76496106 C18.157653,8.51993004 18.2953564,8.1875535 18.2953564,7.84097646 C18.2953564,7.49439943 18.157653,7.16202289 17.9125501,6.91699186 L11.3783644,0.382806222 C11.1333334,0.137703307 10.8009569,-1.22046857e-07 10.4543798,-1.22046857e-07 C10.1078028,-1.22046857e-07 9.77542625,0.137703307 9.53039523,0.382806222 C9.02019826,0.893028709 9.02019826,1.72023574 9.53039523,2.23045823 L13.8337591,6.53445653 L1.30683713,6.53445653 C0.960216585,6.53437236 0.627768546,6.67202957 0.382670797,6.91712731 C0.137573047,7.16222506 -3.85698459e-08,7.4946731 -3.85698459e-08,7.84129365 C-3.85698459e-08,8.1879142 0.137573041,8.52036224 0.382670791,8.76545999 C0.627768541,9.01055774 0.960216582,9.14821494 1.30683713,9.14813078 L13.8337591,9.14813078 L9.53039523,13.4514947 C9.20028733,13.7816026 9.07136551,14.2627453 9.19219339,14.7136811 C9.31302127,15.1646169 9.66524221,15.5168378 10.116178,15.6376657 C10.5671138,15.7584936 11.0482565,15.6295718 11.3783644,15.2994639 L11.3783644,15.2991467 Z" id="路径"></path>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

BIN
src/page-gym/evaluat-model/icons/bad.png


+ 15 - 0
src/page-gym/evaluat-model/icons/close.svg

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 62 (91390) - https://sketch.com -->
+    <title>编组</title>
+    <desc>Created with Sketch.</desc>
+    <g id="智能打分" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="81-100备份" transform="translate(-493.000000, -57.000000)" fill="#DBDBDB" fill-rule="nonzero">
+            <g id="弹窗" transform="translate(143.000000, 40.000000)">
+                <g id="编组" transform="translate(350.000000, 17.000000)">
+                    <path d="M1.05548433,1.07092551 L0.973890205,1.16134969 C0.666012474,1.54828454 0.694001358,2.11481906 1.05785686,2.46909941 L6.537,7.949 L1.05548433,13.4315873 C0.672191496,13.8148801 0.672191496,14.4441272 1.05548433,14.8274201 L1.08636669,14.8583024 L1.17896103,14.9399506 C1.56501142,15.2393273 2.12839072,15.2121112 2.48219949,14.8583024 L7.964,9.376 L13.4470285,14.8583024 C13.8303213,15.2415953 14.4595684,15.2415953 14.8428613,14.8583024 L14.8737436,14.8274201 L14.9553918,14.7348257 C15.2547684,14.3487753 15.2275524,13.785396 14.8737436,13.4315873 L9.391,7.949 L14.8737436,2.46675831 C15.2570364,2.08346548 15.2570364,1.45421835 14.8737436,1.07092551 L14.8428613,1.04004316 L14.7502669,0.958394979 C14.3642165,0.659018325 13.8008372,0.686234384 13.4470285,1.04004316 L7.964,6.53 L2.48231995,1.0401637 C2.09890665,0.65675032 1.46965952,0.65675032 1.08636669,1.04004316 L1.05548433,1.07092551 Z" id="路径"></path>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 15 - 0
src/page-gym/evaluat-model/icons/close2.svg

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 62 (91390) - https://sketch.com -->
+    <title>编组</title>
+    <desc>Created with Sketch.</desc>
+    <g id="智能打分" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="81-100备份" transform="translate(-493.000000, -57.000000)" fill="#01C1B5" fill-rule="nonzero">
+            <g id="弹窗" transform="translate(143.000000, 40.000000)">
+                <g id="编组" transform="translate(350.000000, 17.000000)">
+                    <path d="M1.05548433,1.07092551 L0.973890205,1.16134969 C0.666012474,1.54828454 0.694001358,2.11481906 1.05785686,2.46909941 L6.537,7.949 L1.05548433,13.4315873 C0.672191496,13.8148801 0.672191496,14.4441272 1.05548433,14.8274201 L1.08636669,14.8583024 L1.17896103,14.9399506 C1.56501142,15.2393273 2.12839072,15.2121112 2.48219949,14.8583024 L7.964,9.376 L13.4470285,14.8583024 C13.8303213,15.2415953 14.4595684,15.2415953 14.8428613,14.8583024 L14.8737436,14.8274201 L14.9553918,14.7348257 C15.2547684,14.3487753 15.2275524,13.785396 14.8737436,13.4315873 L9.391,7.949 L14.8737436,2.46675831 C15.2570364,2.08346548 15.2570364,1.45421835 14.8737436,1.07092551 L14.8428613,1.04004316 L14.7502669,0.958394979 C14.3642165,0.659018325 13.8008372,0.686234384 13.4470285,1.04004316 L7.964,6.53 L2.48231995,1.0401637 C2.09890665,0.65675032 1.46965952,0.65675032 1.08636669,1.04004316 L1.05548433,1.07092551 Z" id="路径"></path>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

BIN
src/page-gym/evaluat-model/icons/erji.png


BIN
src/page-gym/evaluat-model/icons/good.png


BIN
src/page-gym/evaluat-model/icons/great.png


File diff suppressed because it is too large
+ 1 - 0
src/page-gym/evaluat-model/icons/index.json


BIN
src/page-gym/evaluat-model/icons/left-bg.png


BIN
src/page-gym/evaluat-model/icons/perfect.png


BIN
src/page-gym/evaluat-model/icons/title.png


+ 27 - 0
src/page-gym/evaluat-model/index.module.less

@@ -0,0 +1,27 @@
+:global {
+    .var-popup--center {
+        overflow: initial;
+    }
+}
+
+.btn {
+    position: fixed;
+    bottom: 33px;
+    left: 50%;
+    transform: translateX(-50%);
+    width: 115px;
+    height: 38px;
+    line-height: 38px;
+    background: var(--van-primary-color);
+    border-radius: 20px;
+    font-size: 16px;
+    font-weight: 500;
+    color: #FFFFFF;
+    text-align: center;
+    z-index: 10;
+}
+.endBtn{
+    display: flex;
+    align-items: center;
+    justify-content: space-evenly;
+}

+ 206 - 0
src/page-gym/evaluat-model/index.tsx

@@ -0,0 +1,206 @@
+import { Popup } from "@varlet/ui";
+import "@varlet/ui/es/popup/style/index";
+import { defineComponent, onBeforeUnmount, onMounted, onUnmounted, reactive, watch } from "vue";
+import {
+	connectWebsocket,
+	evaluatingData,
+	handleEndBegin,
+	handleEndSoundCheck,
+	handlePerformDetection,
+	handleStartBegin,
+} from "/src/view/evaluating";
+import Earphone from "./earphone";
+import styles from "./index.module.less";
+import SoundEffect from "./sound-effect";
+import state from "/src/state";
+import { storeData } from "/src/store";
+import { browser } from "/src/utils";
+import { getNoteByMeasuresSlursStart } from "/src/helpers/formateMusic";
+import { Icon } from "vant";
+import _event, { EventEnum } from "/src/helpers/eventemitter";
+
+// frequency 频率, amplitude 振幅, decibels 分贝
+type TCriteria = "frequency" | "amplitude" | "decibels";
+
+export default defineComponent({
+	name: "evaluat-model",
+	setup() {
+		/**
+		 * 木管(长笛 萨克斯 单簧管)乐器一级的2、3、6测评要放原音音频
+		 * 铜管乐器一级的1a,1b,5,6测评要放原音音频
+		 */
+		const getMusicMode = () => {
+			const muguan = [2, 4, 5, 6];
+			const tongguan = [12, 13, 14, 15, 17];
+			if (muguan.includes(state.subjectId) && (state.examSongName || "").search(/[^\u0000-\u00FF](1-2|1-3|1-6)/gi) > -1) {
+				return "music";
+			}
+			if (tongguan.includes(state.subjectId) && (state.examSongName || "").search(/[^\u0000-\u00FF](1-1-1|1-1-2|1-5|1-6)/gi) > -1) {
+				return "music";
+			}
+			if ([23, 113, 121].includes(state.subjectId)) {
+				return "music";
+			}
+			return "background";
+		};
+		const browserInfo = browser();
+		/** 是否是节奏练习 */
+		const isRhythmicExercises = () => {
+			const examSongName = state.examSongName || "";
+			return examSongName.indexOf("节奏练习") > -1;
+		};
+
+		/** 获取评测标准 */
+		const getEvaluationCriteria = () => {
+			let criteria: TCriteria = "frequency";
+			// 声部打击乐
+			if ([23, 113, 121].includes(state.subjectId)) {
+				criteria = "amplitude";
+			} else if (isRhythmicExercises()) {
+				// 分类为节奏练习
+				criteria = "decibels";
+			}
+			return criteria;
+		};
+
+		/** 生成评测曲谱数据 */
+		const formatTimes = () => {
+			let ListenMode = false;
+			let dontEvaluatingMode = false;
+			let skip = false;
+			const datas = [];
+			for (let index = 0; index < state.times.length; index++) {
+				const item = state.times[index];
+				const note = getNoteByMeasuresSlursStart(item);
+				const rate = state.speed / state.originSpeed;
+				const difftime = item.difftime;
+				const start = difftime + (item.sourceRelativeTime || item.relativeTime);
+				const end = difftime + (item.sourceRelaEndtime || item.relaEndtime);
+				const isStaccato = note.noteElement.voiceEntry.isStaccato();
+				const noteRate = isStaccato ? 0.5 : 1;
+				if (note.formatLyricsEntries.contains("Play") || note.formatLyricsEntries.contains("Play...")) {
+					ListenMode = false;
+				}
+				if (note.formatLyricsEntries.contains("Listen")) {
+					ListenMode = true;
+				}
+				if (note.formatLyricsEntries.contains("纯律结束")) {
+					dontEvaluatingMode = false;
+				}
+				if (note.formatLyricsEntries.contains("纯律")) {
+					dontEvaluatingMode = true;
+				}
+				const nextNote = state.times[index + 1];
+				// console.log("noteinfo", note.noteElement.isRestFlag && !!note.stave && !!nextNote)
+				if (skip && (note.stave || !item.noteElement.isRestFlag || (nextNote && !nextNote.noteElement.isRestFlag))) {
+					skip = false;
+				}
+				if (note.noteElement.isRestFlag && !!note.stave && !!nextNote && nextNote.noteElement.isRestFlag) {
+					skip = true;
+				}
+				// console.log(note.measureOpenIndex, item.measureOpenIndex, note);
+				// console.log("skip", skip)
+				const data = {
+					timeStamp: (start * 1000) / rate,
+					duration: ((end * 1000) / rate - (start * 1000) / rate) * noteRate,
+					frequency: item.frequency,
+					nextFrequency: item.nextFrequency,
+					prevFrequency: item.prevFrequency,
+					// 重复的情况index会自然累加,render的index是谱面渲染的index
+					measureIndex: note.measureOpenIndex,
+					measureRenderIndex: item.measureListIndex,
+					dontEvaluating: ListenMode || dontEvaluatingMode,
+					musicalNotesIndex: item.i,
+					denominator: note.noteElement?.Length.denominator,
+				};
+				datas.push(data);
+			}
+			return datas;
+		};
+		/** 连接websocket */
+		const handleConnect = async () => {
+			const behaviorId = localStorage.getItem("behaviorId") || undefined;
+			const rate = state.speed / state.originSpeed;
+			const content = {
+				musicXmlInfos: formatTimes(),
+				id: state.examSongId,
+				subjectId: state.subjectId,
+				detailId: state.detailId,
+				examSongId: state.examSongId,
+				xmlUrl: state.xmlUrl,
+				partIndex: state.partIndex,
+				behaviorId,
+				tenantId: storeData.user.tenantId,
+				platform: browserInfo.ios ? "IOS" : browserInfo.android ? "ANDROID" : "WEB",
+				clientId: storeData.platformType === "STUDENT" ? "student" : storeData.platformType === "TEACHER" ? "teacher" : "education",
+				speed: state.speed,
+				heardLevel: state.setting.evaluationDifficulty,
+				beatLength: Math.round((state.fixtime * 1000) / rate),
+				campId: sessionStorage.getItem("campId") || "",
+				evaluationCriteria: getEvaluationCriteria(),
+			};
+			const result = await connectWebsocket(content);
+			state.playSource = getMusicMode();
+		};
+
+        /** 评测返回 */
+        const handleResult = (result: any) => {
+            console.log('评测返回2', result.body)
+        }
+		onMounted(() => {
+			handlePerformDetection();
+            _event.on(EventEnum.sendResultScore, handleResult)
+		});
+        onBeforeUnmount(() => {
+            _event.off(EventEnum.sendResultScore)
+        })
+		watch(
+			() => evaluatingData.checkEnd,
+			() => {
+				if (evaluatingData.checkEnd) {
+					console.log("检测结束,连接websocket");
+					handleConnect();
+				}
+			}
+		);
+		return () => (
+			<div>
+				{evaluatingData.websocketState && (
+					<>
+						{!evaluatingData.startBegin && (
+							<div class={styles.btn} onClick={handleStartBegin}>
+								开始演奏
+							</div>
+						)}
+						{evaluatingData.startBegin && (
+							<div class={[styles.btn, styles.endBtn]} onClick={() => handleEndBegin(false)}>
+								<Icon name="success" />
+								<span>结束演奏</span>
+							</div>
+						)}
+					</>
+				)}
+				<Popup closeOnClickOverlay={false} defaultStyle={false} v-model:show={evaluatingData.earphoneMode}>
+					<Earphone
+						onClose={() => {
+							evaluatingData.earphoneMode = false;
+							handlePerformDetection();
+						}}
+					/>
+				</Popup>
+				<Popup closeOnClickOverlay={false} defaultStyle={false} v-model:show={evaluatingData.soundEffectMode}>
+					<SoundEffect
+						onClose={(value: any) => {
+							evaluatingData.soundEffectMode = false;
+							if (value) {
+								state.setting.soundEffect = false;
+							}
+							handleEndSoundCheck();
+							handlePerformDetection();
+						}}
+					/>
+				</Popup>
+			</div>
+		);
+	},
+});

+ 54 - 0
src/page-gym/evaluat-model/sound-effect/data.ts

@@ -0,0 +1,54 @@
+import iconSound_12_4 from "./icons/icon-sound_12_4.svg";
+import iconsound_5_6 from "./icons/icon-sound_5_6.svg";
+import iconsound_13 from "./icons/icon-sound_13.svg";
+import iconsound_14_15 from "./icons/icon-sound_14_15.svg";
+import iconsound_120 from "./icons/icon-sound_120.svg";
+import iconsound_default from "./icons/icon-sound_default.svg";
+export const getScoreData = (subjectId: number) => {
+	// 小号、单簧管
+	if (subjectId == 12 || subjectId == 4) {
+		return {
+			src: iconSound_12_4,
+			text: "",
+			frequency: 525.6295448312027,
+		};
+	}
+	// 萨克斯
+	if (subjectId == 5 || subjectId == 6) {
+		return {
+			src: iconsound_5_6,
+			text: "C",
+			frequency: 525.6295448312027,
+		};
+	}
+	// 圆号
+	if (subjectId == 13) {
+		return {
+			src: iconsound_13,
+			text: "F",
+			frequency: 350.8156324849721,
+		};
+	}
+	// 长号 上低音号
+	if (subjectId == 14 || subjectId == 15) {
+		return {
+			src: iconsound_14_15,
+			text: "S",
+			frequency: 117.07067192670213,
+		};
+	}
+	// 长号 上低音号
+	if (subjectId == 120) {
+		return {
+			src: iconsound_120,
+			text: "A",
+			frequency: 884,
+		};
+	}
+	// 剩余声部
+	return {
+		src: iconsound_default,
+		text: "Bb",
+		frequency: 468.28268770680853,
+	};
+};

BIN
src/page-gym/evaluat-model/sound-effect/icons/bg-note.png


BIN
src/page-gym/evaluat-model/sound-effect/icons/bg.png


BIN
src/page-gym/evaluat-model/sound-effect/icons/child.png


BIN
src/page-gym/evaluat-model/sound-effect/icons/content-bg.png


BIN
src/page-gym/evaluat-model/sound-effect/icons/dot-active.png


BIN
src/page-gym/evaluat-model/sound-effect/icons/dot-error.png


BIN
src/page-gym/evaluat-model/sound-effect/icons/dot.png


File diff suppressed because it is too large
+ 11 - 0
src/page-gym/evaluat-model/sound-effect/icons/icon-sound_120.svg


File diff suppressed because it is too large
+ 11 - 0
src/page-gym/evaluat-model/sound-effect/icons/icon-sound_12_4.svg


File diff suppressed because it is too large
+ 10 - 0
src/page-gym/evaluat-model/sound-effect/icons/icon-sound_13.svg


File diff suppressed because it is too large
+ 11 - 0
src/page-gym/evaluat-model/sound-effect/icons/icon-sound_14_15.svg


File diff suppressed because it is too large
+ 11 - 0
src/page-gym/evaluat-model/sound-effect/icons/icon-sound_5_6.svg


File diff suppressed because it is too large
+ 11 - 0
src/page-gym/evaluat-model/sound-effect/icons/icon-sound_default.svg


BIN
src/page-gym/evaluat-model/sound-effect/icons/notes.png


+ 98 - 0
src/page-gym/evaluat-model/sound-effect/index.module.less

@@ -0,0 +1,98 @@
+.sound-effect {
+  position: relative;
+  width: 100vw;
+  height: 100vh;
+  background-image: url('./icons/bg.png');
+  background-size: 100% 100%;
+  background-repeat: no-repeat;
+  background-position: center;
+
+}
+
+.top {
+  display: flex;
+  justify-content: space-between;
+  padding: 20px 30px 10px 20px;
+}
+
+.back {
+  width: 30px;
+  height: 30px;
+
+  img {
+    width: 100%;
+    height: 100%;
+    display: block;
+  }
+}
+
+.skibtns {
+  z-index: 9999 !important;
+  :global {
+    --van-popover-action-width: 100px;
+    --van-popover-action-font-size: 14px;
+    --van-popover-light-text-color: #999999;
+  }
+}
+
+.rightSkipBtn {
+  font-size: 17px;
+
+  .tran {
+    margin-left: 6px;
+    transform: rotate(90deg);
+  }
+}
+
+.content {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  background-image: url('./icons/bg-note.png');
+  background-repeat: no-repeat;
+  background-size: 94%;
+  background-position: center 30%;
+}
+
+.heiban {
+  position: relative;
+  width: 50%;
+  height: 75vh;
+  background-image: url('./icons/content-bg.png');
+  background-repeat: no-repeat;
+  background-size: contain;
+  background-position: center;
+  max-height: 370PX;
+  min-height: 260PX;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+}
+
+.iconChild {
+  position: absolute;
+  bottom: 0;
+  left: 8%;
+  width: 8vw;
+  max-width: 95PX;
+}
+.scoreContent{
+  text-align: center;
+}
+.tips{
+  text-align: center;
+  color: #fff;
+  font-size: 13px;
+  padding: 4px 0;
+}
+.steps{
+  margin: 0 auto;
+  padding: 10px 0;
+  width: 40%;
+  display: flex;
+  justify-content: space-evenly;
+  & > img {
+    width: 20px;
+    height: 20px;
+  }
+}

+ 84 - 0
src/page-gym/evaluat-model/sound-effect/index.tsx

@@ -0,0 +1,84 @@
+import { defineComponent, reactive, ref, watch } from "vue";
+import { Popover, Icon } from "vant";
+import icons from "../icons/index.json";
+import iconChild from "./icons/child.png";
+import DotIcon from "./icons/dot.png";
+import DotActiveIcon from "./icons/dot-active.png";
+import DotErrorIcon from "./icons/dot-error.png";
+
+import styles from "./index.module.less";
+import state from "/src/state";
+import { evaluatingData } from "/src/view/evaluating";
+import { getScoreData } from "./data";
+
+export default defineComponent({
+	name: "sound-effect",
+	emits: ["close"],
+	setup(props, { emit }) {
+		const scoreData = getScoreData(state.subjectId);
+		const soundEffectData = reactive({
+			step: 0,
+			tips: ["左边红灯表示吹奏的音过低", "吹奏时请保持中间绿灯亮起", "右边红灯表示吹奏的音过高"],
+			time: 1,
+		});
+		watch(
+			() => evaluatingData.soundEffectFrequency,
+			() => {
+        // console.log('吹奏',evaluatingData.soundEffectFrequency , scoreData.frequency)
+				const trend =
+					Math.abs(evaluatingData.soundEffectFrequency - scoreData.frequency) <= 10 ? 1 : evaluatingData.soundEffectFrequency > scoreData.frequency ? 2 : 0;
+				soundEffectData.step = trend;
+				if (trend !== 1) {
+					soundEffectData.time = Date.now();
+				}
+        // 持续时间达到3秒钟,效音成功
+				if (Date.now() - soundEffectData.time > 3000) {
+					// console.log("效音完成");
+          emit('close')
+				}
+			}
+		);
+
+		/** 跳过本次 */
+		const handleSelect = (e: {text: string}) => {
+      if (e.text === '关闭校音'){
+        emit('close', true)
+        return
+      } 
+      emit('close')
+		};
+		return () => (
+			<div class={styles["sound-effect"]}>
+				<div class={styles.top}>
+					<div class={styles.back} onClick={() => emit('close')}>
+						<img src={icons["arrow-left-background"]} />
+					</div>
+					<Popover trigger="click" class={styles.skibtns} actions={[{ text: "跳过本次" }, { text: "关闭校音" }]} onSelect={handleSelect}>
+						{{
+							reference: () => (
+								<div class={styles.rightSkipBtn}>
+									<span>跳过本次</span>
+									<Icon name="play" color="var(--van-primary-color)" class={styles.tran}/>
+								</div>
+							),
+						}}
+					</Popover>
+				</div>
+				<div class={styles.content}>
+					<div class={styles.heiban}>
+						<img class={styles.iconChild} src={iconChild} />
+						<div class={styles.scoreContent}>
+							<img src={scoreData.src} />
+						</div>
+						<div class={styles.tips}>{soundEffectData.tips[soundEffectData.step]}</div>
+						<div class={styles.steps}>
+							<img src={soundEffectData.step === 0 ? DotErrorIcon : DotIcon} />
+							<img src={soundEffectData.step === 1 ? DotActiveIcon : DotIcon} />
+							<img src={soundEffectData.step === 2 ? DotErrorIcon : DotIcon} />
+						</div>
+					</div>
+				</div>
+			</div>
+		);
+	},
+});

+ 3 - 2
src/page-gym/main.ts

@@ -1,7 +1,7 @@
 import 'vant/lib/index.css'
 import { createApp } from 'vue'
 import { getRequestHostname } from '../constant/whiteUrl'
-import { initStore, storeData } from '../store'
+import { initStore } from '../store'
 import '../style.css'
 import App from './App'
 import router from './router'
@@ -14,4 +14,5 @@ initStore({
     proxy: import.meta.env.DEV ? '/gym' : '',
 })
 
-createApp(App).use(router).mount('#app')
+createApp(App).use(router)
+.mount('#app')

+ 21 - 5
src/state.ts

@@ -1,10 +1,17 @@
 import { showToast, Toast } from "vant";
 import { reactive, watchEffect } from "vue";
 import { OpenSheetMusicDisplay } from "../osmd-extended/src";
+import _event, { EventEnum } from "./helpers/eventemitter";
 import { metronomeData } from "./helpers/metronome";
 import { GradualNote, GradualTimes, GradualVersion, IMode } from "./type";
+import { handleEndBegin, sendEvaluatingOffsetTime } from "./view/evaluating";
+
+/** 入门 | 进阶 | 大师 */
+export type IDifficulty = 'BEGINNER' | 'ADVANCED' | 'PERFORMER'
 
 const state = reactive({
+	/** 当前曲谱数据ID, 和曲谱ID不一致 */
+	detailId: '',
 	/** 曲谱资源URL */
 	xmlUrl: "",
 	/** 声部ID */
@@ -51,7 +58,7 @@ const state = reactive({
 	isSpecialBookCategory: false,
 	/** 播放状态 */
 	playState: "paused" as "play" | "paused",
-	/** 原音,伴奏 */
+	/** 播放那个: 原音,伴奏 */
 	playSource: "music" as "music" | "background",
 	/** 播放进度 */
 	playProgress: 0,
@@ -82,7 +89,7 @@ const state = reactive({
 	/** 设置 */
 	setting: {
 		/** 效音提醒 */
-		soundEffect: false,
+		soundEffect: true,
 		/** 护眼模式 */
 		eyeProtection: false,
 		/** 摄像头 */
@@ -96,12 +103,14 @@ const state = reactive({
 		/** 频率 */
 		frequency: '442',
 		/** 评测难度 */
-		evaluationDifficulty: '',
+		evaluationDifficulty: 'ADVANCED' as IDifficulty,
 		/** 保存到相册 */
 		saveToAlbum: false,
 		/** 开启伴奏 */
 		enableAccompaniment: false
 	},
+	/** 节拍器的时间 */
+	fixtime: 0,
 
 	repeatedBeats: 0,
 
@@ -168,12 +177,19 @@ const setStep = () => {
 /** 开始播放 */
 export const onPlay = () => {
 	setStep();
+	if (state.modeType === 'evaluating'){
+		const currentTime = state.songEl?.currentTime || 0;
+		sendEvaluatingOffsetTime(currentTime)
+	}
 };
 /** 播放中事件 */
 export const onTimeupdate = (evt: Event) => {};
 /** 播放完成事件 */
 export const onEnded = () => {
 	handleStopPlay();
+	if (state.modeType === 'evaluating'){
+		handleEndBegin()
+	}
 };
 
 /**
@@ -237,7 +253,7 @@ export const togglePlay = (playState?: "play" | "paused") => {
 	}
 };
 /** 结束播放 */
-const handleStopPlay = () => {
+export const handleStopPlay = () => {
 	state.playState = "paused";
 	state.songEl?.pause();
 	state.backgroundEl?.pause();
@@ -289,7 +305,7 @@ export const getNote = (currentTime: number) => {
 	const len = state.times.length;
 	/** 播放超过了最后一个音符的时间,直接结束 */
 	if (currentTime > times[len - 1].endtime + 1) {
-		handleStopPlay();
+		onEnded();
 		return;
 	}
 	let _item = null as any;

+ 2 - 1
src/store.ts

@@ -1,9 +1,10 @@
 import { reactive } from "vue";
 
 type IUser = {
-	username?: string;
+	username?: string
     /** 会员结束时间 */
     membershipEndTime?: string
+	tenantId?: number
 };
 type IStatus = "init" | "login" | "logout" | "error";
 type IPlatformType = "STUDENT" | "TEACHER" | "WEB";

+ 5 - 5
src/utils/index.ts

@@ -8,11 +8,10 @@ export const browser = () => {
 		webKit: u.indexOf("AppleWebKit") > -1, //苹果、谷歌内核
 		gecko: u.indexOf("Gecko") > -1 && u.indexOf("KHTML") == -1, //火狐内核
 		mobile: !!u.match(/AppleWebKit.*Mobile.*/), //是否为移动终端
-		ios: !!u.match(/Mac OS X/), //ios终端
-		// ios: !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/), //ios终端
-		android: u.indexOf("ORCHESTRAAPPA") > -1 || u.indexOf("Adr") > -1, //android终端
+		ios: !!u.match(/Mac OS X/) || /(iPhone|iPad|iPod|iOS)/i.test(u), //ios终端
+		android: u.indexOf('Android') > -1 || u.indexOf('Adr') > -1,   //判断是否是 android终端
 		iPhone: u.indexOf("ORCHESTRAAPPI") > -1, //是否为iPhone或者QQHD浏览器
-		isApp: u.indexOf("ORCHESTRAAPPI") > -1 || u.indexOf("ORCHESTRAAPPA") > -1,
+		isApp: u.indexOf('DAYAAPPA') > -1 || u.indexOf('DAYAAPPI') > -1 || u.indexOf("ORCHESTRAAPPI") > -1 || u.indexOf("ORCHESTRAAPPA") > -1,
 		isTeacher: u.indexOf("ORCHESTRATEACHER") > -1,
 		isStudent: u.indexOf("ORCHESTRASTUDENT") > -1,
 		isSchool: u.indexOf("ORCHESTRASCHOOL") > -1,
@@ -24,8 +23,9 @@ export const browser = () => {
 		xiaomi: !!u.match(/mi\s/i) || !!u.match(/redmi/i) || !!u.match(/mix/i),
 	};
 };
+/** uuid */
 export const getRandomKey = () => {
-	const key = "" + new Date().getTime() + Math.floor(Math.random() * 1000000);
+	const key = "" + Date.now() + Math.floor(Math.random() * 1000000);
 	return key;
 };
 

+ 77 - 60
src/utils/native-message.ts

@@ -1,8 +1,19 @@
-import { browser, getRandomKey } from "./";
+import { browser, getRandomKey } from '/src/utils'
+
 
 export interface IPostMessage {
-	api: string;
-	content?: any;
+  api: string
+  content?: any
+}
+
+/**
+ * 劫持postMessage
+ */
+
+const originalPostMessage = window.postMessage
+
+window.postMessage = (message: IPostMessage) => {
+  originalPostMessage(message, '*')
 }
 
 /**
@@ -13,75 +24,81 @@ export interface IPostMessage {
  *
  */
 
-type CallBack = (evt?: IPostMessage) => void;
+export type CallBack = (evt?: IPostMessage) => void
 
-const loop = () => {};
+const loop = () => {}
 
-const calls: { [key: string]: CallBack | CallBack[] } = {};
+const calls: { [key: string]: CallBack | CallBack[] } = {}
 
-const browserInfo = browser();
+const browserInfo = browser()
 
 if (browserInfo.isApp) {
-	window.addEventListener("message", (evt) => {
-		try {
-			console.log("app交互接受:", evt.data);
-			const data = evt.data ? (typeof evt.data === "object" ? evt.data : JSON.parse(evt.data)) : {};
-			const uuid = data.content?.uuid || data.uuid;
-			try {
-				if (data.content) {
-					data.content = JSON.parse(data.content);
-				}
-			} catch (error) {
-				//
-			}
-			if (!uuid) {
-				const keys = Object.keys(calls).filter((key) => key.indexOf(data.api) === 0);
-				for (const key of keys) {
-					const callback = calls[key] || loop;
-					typeof callback === "function" && callback(data);
-				}
-				return;
-			}
-			const callId = data.content?.uuid || data.uuid || data.api + data.uuid;
-			const callback = calls[callId] || loop;
-			typeof callback === "function" && callback(data);
-		} catch (error) {
-			console.error("通信消息解析错误", error);
-		}
-	});
+  window.addEventListener('message', evt => {
+    try {
+      const data = evt.data ? typeof evt.data === 'object' ? evt.data : JSON.parse(evt.data) : {}
+      const uuid = data.content?.uuid || data.uuid
+      try {
+        if (data.content) {
+          data.content = JSON.parse(data.content)
+        }
+      } catch (error) {}
+      console.log('h5_接受_api:', data?.api)
+      if (!uuid) {
+        const keys = Object.keys(calls).filter(key => key.indexOf(data.api) === 0)
+        for (const key of keys) {
+          const callback = calls[key] || loop
+          typeof callback === 'function' && callback(data)
+          if (Array.isArray(callback)) {
+            callback.forEach(cb => {
+              typeof cb === 'function' && cb(data)
+            })
+          }
+        }
+        return
+      }
+      const callid = data.content?.uuid || data.uuid || (data.api + data.uuid)
+      const callback = calls[callid] || loop
+      typeof callback === 'function' && callback(data)
+    } catch (error) {
+      console.error('通信消息解析错误', error)
+    }
+  })
 }
 
-const instance: any = (window as any).postMessageInstance;
+const instance: any = (window as any).DAYA || (window as any).webkit?.messageHandlers?.DAYA
 
 export const postMessage = (data: IPostMessage, callback?: CallBack) => {
-	if (browserInfo.isApp) {
-		const uuid = getRandomKey();
-		calls[uuid] = callback || loop;
-		data.content = data.content ? { ...data.content, uuid } : { uuid };
-		console.log("app交互发送:", data);
-		instance.postMessage(JSON.stringify(data));
-	}
-};
+  if (browserInfo.isApp) {
+    const uuid = getRandomKey()
+    calls[uuid] = callback || loop
+    data.content = data.content ? {...data.content, uuid} : {uuid}
+    instance.postMessage(JSON.stringify(data))
+    console.log('h5_请求_api:', data.api)
+  }
+}
 
 export const listenerMessage = (api: string, callback: CallBack) => {
-	if (browserInfo.isApp) {
-		const uuid = api + getRandomKey();
-		calls[uuid] = callback || loop;
-	}
-};
+  if (browserInfo.isApp) {
+    const uuid = api
+    if (!calls[uuid]) {
+      calls[uuid] = []
+    }
+    ;(calls[uuid] as CallBack[]).push(callback || loop)
+  }
+}
 
 export const removeListenerMessage = (api: string, callback: CallBack) => {
-	if (browserInfo.isApp) {
-		const uuid = api;
-		if (Array.isArray(calls[uuid])) {
-			const indexOf = (calls[uuid] as CallBack[]).indexOf(callback);
-			(calls[uuid] as CallBack[]).splice(indexOf, 1);
-		}
-	}
-};
+  if (browserInfo.isApp) {
+    const uuid = api
+    if (Array.isArray(calls[uuid])) {
+      const indexOf = (calls[uuid] as CallBack[]).indexOf(callback)
+      ;(calls[uuid] as CallBack[]).splice(indexOf, 1)
+    }
+  }
+}
 
 export const promisefiyPostMessage = (data: IPostMessage): Promise<IPostMessage | undefined> => {
-	return new Promise((resolve) => {
-		postMessage(data, (res) => resolve(res));
-	});
-};
+  return new Promise((resolve) => {
+    postMessage(data, res => resolve(res))
+  })
+}

+ 163 - 0
src/view/evaluating/index.tsx

@@ -0,0 +1,163 @@
+import { showToast } from "vant";
+import { defineComponent, onBeforeUnmount, onMounted, reactive, ref } from "vue";
+import {
+	endEvaluating,
+	endSoundCheck,
+	getEarphone,
+	proxyServiceMessage,
+	sendResult,
+	startEvaluating,
+	startRecording,
+	startSoundCheck,
+} from "/src/helpers/communication";
+import _event, { EventEnum } from "/src/helpers/eventemitter";
+import state, { handleStopPlay, togglePlay } from "/src/state";
+
+export const evaluatingData = reactive({
+	earphone: false, // 是否插入耳机
+	soundEffect: false, // 是否效音
+	soundEffectFrequency: 0, // 效音频率
+	checkStep: 0, // 执行步骤
+	checkEnd: false, // 检测结束
+	earphoneMode: false, // 耳机弹窗
+	soundEffectMode: false, // 效音弹窗
+	websocketState: false, // websocket连接状态
+	startBegin: false, // 开始
+	backtime: 0, // 延迟时间
+	measureIndex: -1,
+	measureRenderIndex: -1,
+	score: 0, // 评测分数
+});
+
+/** 开始播放发送延迟时间 */
+export const sendEvaluatingOffsetTime = (currentTime: number) => {
+	const nowTime = Date.now();
+	console.log("第一次播放时间", nowTime);
+	console.log("已播放时长: ", currentTime * 1000);
+	console.log("不减掉已播放时间: ", nowTime - evaluatingData.backtime);
+	const delayTime = nowTime - evaluatingData.backtime - currentTime * 1000;
+	console.log("真正播放延迟", delayTime);
+	// 蓝牙耳机延迟一点发送消息确保在录音后面
+	setTimeout(async () => {
+		await proxyServiceMessage({
+			header: {
+				commond: "audioPlayStart",
+				type: "SOUND_COMPARE",
+			},
+			body: {
+				offsetTime: delayTime,
+			},
+		});
+		evaluatingData.backtime = 0;
+	}, 220);
+};
+
+/** 检测耳机 */
+const checkUseEarphone = async () => {
+	const res = await getEarphone();
+	return res?.content?.checkIsWired || false;
+};
+/** 效音提醒 */
+const checkSoundEffect = () => {
+	// console.log("🚀 效音状态状态:")
+	return state.setting.soundEffect;
+};
+
+/**
+ * 开始录音
+ */
+const useSoundEffect = () => {
+	handleEndSoundCheck();
+	sendResult((res) => {
+		if (res?.content) {
+			const { header, body } = res.content;
+			if (header.commond === "checking") {
+				evaluatingData.soundEffectFrequency = body.frequency;
+			}
+		}
+	});
+	startSoundCheck();
+};
+/** 结束录音 */
+export const handleEndSoundCheck = () => {
+	endSoundCheck();
+};
+
+/** 连接websocket */
+export const connectWebsocket = async (content: any) => {
+	evaluatingData.websocketState = false;
+	try {
+		// console.log("🚀 ~ content:", JSON.stringify(content))
+	} catch (error) {}
+	const res = await startEvaluating(content);
+	if (res?.api === "startEvaluating") {
+		evaluatingData.websocketState = true;
+	} else {
+		showToast("请在APP端进行评测");
+	}
+};
+
+/**
+ * 执行检测
+ */
+export const handlePerformDetection = async () => {
+	evaluatingData.checkEnd = false;
+	if (evaluatingData.checkStep === 0) {
+		// 检测耳机
+		const erji = await checkUseEarphone();
+		evaluatingData.earphoneMode = !erji;
+		evaluatingData.checkStep = 1;
+		return;
+	}
+	if (evaluatingData.checkStep === 1) {
+		// 效音
+		// 是否需要开启效音
+		if (checkSoundEffect()) {
+			evaluatingData.soundEffectMode = true;
+			useSoundEffect();
+		}
+		evaluatingData.checkStep = 10;
+		return;
+	}
+	if (evaluatingData.checkStep === 10) {
+		// 连接websocket
+		evaluatingData.checkEnd = true;
+	}
+};
+
+/** 开始评测 */
+export const handleStartBegin = async () => {
+	evaluatingData.startBegin = true;
+	sendResult((res) => {
+		if (res?.content) {
+            // console.log("🚀 ~ 评测返回:", res);
+			const { header } = res.content;
+			if (["measureScore", "overall"].includes(header.commond)) {
+                // console.log('评测返回1', new Date().toLocaleString())
+                _event.emit(EventEnum.sendResultScore, res.content)
+			}
+		}
+	});
+	await startRecording();
+	evaluatingData.backtime = Date.now();
+	togglePlay("play");
+};
+
+/**
+ * 结束评测
+ * @param isEnd 是否是自动播放停止, 默认: false
+ */
+export const handleEndBegin = (isEnd = false) => {
+	evaluatingData.startBegin = false;
+	endEvaluating({
+		musicScoreId: state.examSongId,
+	});
+    if (isEnd) return
+	handleStopPlay();
+};
+export default defineComponent({
+	name: "evaluating",
+	setup() {
+		return () => <div></div>;
+	},
+});

+ 0 - 1
src/view/music-score/index.tsx

@@ -42,7 +42,6 @@ export default defineComponent({
             osmd.EngravingRules.PageTopMargin = 2
             osmd.EngravingRules.PageLeftMargin = 2
             osmd.EngravingRules.PageBottomMargin = 2
-            osmd.EngravingRules.PageBottomMargin = 2
             await osmd.load(musicData.score)
             osmd.zoom = state.zoom
             osmd.render()

+ 5 - 0
src/view/selection/index.module.less

@@ -85,4 +85,9 @@
     .lineHide{
         opacity: 0;
     }
+}
+.scoreItem{
+    position: absolute;
+    right: 0;
+    top: -100%;
 }

+ 5 - 0
src/view/selection/index.tsx

@@ -2,6 +2,7 @@ import { computed, defineComponent, onMounted, reactive } from "vue";
 import state, { handleSelection, skipNotePlay } from "/src/state";
 import styles from "./index.module.less";
 import { metronomeData } from "/src/helpers/metronome";
+// import scoreIocn from './scoreIcon.json'
 
 export default defineComponent({
 	name: "selection",
@@ -108,6 +109,10 @@ export default defineComponent({
 							{item.staveBox && (
 								<div class={[styles.position, showClass.value(item)]} style={item.staveBox} onClick={() => handleSelection(item)}>
 									{!item.isRestFlag && metronomeData.lineShow && item.MeasureNumberXML === metronomeData.activeMetro?.measureNumberXML && <div class={styles.line} style={{ left: metronomeData.activeMetro.left }}></div>}
+									{/* <div class={styles.scoreItem}>
+										<img src={scoreIocn.perfect} />
+										98
+									</div> */}
 								</div>
 							)}
 						</>

File diff suppressed because it is too large
+ 1 - 0
src/view/selection/scoreIcon.json


+ 1 - 0
vite.config.ts

@@ -46,6 +46,7 @@ export default defineConfig({
 		},
 	},
 	server: {
+		cors: true,
 		proxy: {
 			"^/gym/.*": {
 				target: "https://mstutest.dayaedu.com",

Some files were not shown because too many files changed in this diff