index.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. import { Popup, Snackbar } from "@varlet/ui";
  2. import { Transition, defineComponent, onMounted, reactive, watch } from "vue";
  3. import {
  4. connectWebsocket,
  5. evaluatingData,
  6. handleEndBegin,
  7. handleEndSoundCheck,
  8. handlePerformDetection,
  9. handleStartBegin,
  10. handleStartEvaluat,
  11. handleViewReport,
  12. } from "/src/view/evaluating";
  13. import Earphone from "./earphone";
  14. import styles from "./index.module.less";
  15. import SoundEffect from "./sound-effect";
  16. import state from "/src/state";
  17. import { storeData } from "/src/store";
  18. import { browser } from "/src/utils";
  19. import { getNoteByMeasuresSlursStart } from "/src/helpers/formateMusic";
  20. import EvaluatResult from "./evaluat-result";
  21. import EvaluatAudio from "./evaluat-audio";
  22. import { api_openWebView, api_proxyServiceMessage, api_setCache, api_videoUpdate } from "/src/helpers/communication";
  23. import EvaluatShare from "./evaluat-share";
  24. import { Vue3Lottie } from "vue3-lottie";
  25. import startData from "./data/start.json";
  26. import startingData from "./data/starting.json";
  27. import iconEvaluat from "./icons/evaluating.json";
  28. import { getQuery } from "/src/utils/queryString";
  29. import { showToast } from "vant";
  30. // frequency 频率, amplitude 振幅, decibels 分贝
  31. type TCriteria = "frequency" | "amplitude" | "decibels";
  32. export default defineComponent({
  33. name: "evaluat-model",
  34. setup() {
  35. const query = getQuery();
  36. const evaluatModel = reactive({
  37. tips: true,
  38. evaluatUpdateAudio: false,
  39. shareMode: false,
  40. });
  41. const browserInfo = browser();
  42. /** 是否是节奏练习 */
  43. const isRhythmicExercises = () => {
  44. const examSongName = state.examSongName || "";
  45. return examSongName.indexOf("节奏练习") > -1;
  46. };
  47. /** 获取评测标准 */
  48. const getEvaluationCriteria = () => {
  49. let criteria: TCriteria = "frequency";
  50. // 声部打击乐
  51. if ([23, 113, 121].includes(state.subjectId)) {
  52. criteria = "amplitude";
  53. } else if (isRhythmicExercises()) {
  54. // 分类为节奏练习
  55. criteria = "decibels";
  56. }
  57. return criteria;
  58. };
  59. /** 生成评测曲谱数据 */
  60. const formatTimes = () => {
  61. const difftime = state.times[0]?.difftime || 0;
  62. let starTime = 0;
  63. let ListenMode = false;
  64. let dontEvaluatingMode = false;
  65. let skip = false;
  66. const datas = [];
  67. let times = state.times;
  68. /** 如果为选段模式,评测 */
  69. if (state.isSelectMeasureMode) {
  70. const startIndex = state.times.findIndex((n: any) => n.noteId == state.section[0].noteId);
  71. const endIndex = state.times.findIndex((n: any) => n.noteId == state.section[1].noteId);
  72. times = state.times.filter((n: any, index: number) => {
  73. return index >= startIndex && index <= endIndex;
  74. });
  75. starTime = times[0].sourceRelativeTime || times[0].relativeTime;
  76. }
  77. let measureIndex = -1;
  78. let recordMeasure = -1;
  79. for (let index = 0; index < times.length; index++) {
  80. const item = times[index];
  81. const note = getNoteByMeasuresSlursStart(item);
  82. const rate = state.speed / state.originSpeed;
  83. const start = difftime + (item.sourceRelativeTime || item.relativeTime) - starTime;
  84. const end = difftime + (item.sourceRelaEndtime || item.relaEndtime) - starTime;
  85. const isStaccato = note.noteElement.voiceEntry.isStaccato();
  86. const noteRate = isStaccato ? 0.5 : 1;
  87. if (note.formatLyricsEntries.contains("Play") || note.formatLyricsEntries.contains("Play...")) {
  88. ListenMode = false;
  89. }
  90. if (note.formatLyricsEntries.contains("Listen")) {
  91. ListenMode = true;
  92. }
  93. if (note.formatLyricsEntries.contains("纯律结束")) {
  94. dontEvaluatingMode = false;
  95. }
  96. if (note.formatLyricsEntries.contains("纯律")) {
  97. dontEvaluatingMode = true;
  98. }
  99. const nextNote = state.times[index + 1];
  100. // console.log("noteinfo", note.noteElement.isRestFlag && !!note.stave && !!nextNote)
  101. if (skip && (note.stave || !item.noteElement.isRestFlag || (nextNote && !nextNote.noteElement.isRestFlag))) {
  102. skip = false;
  103. }
  104. if (note.noteElement.isRestFlag && !!note.stave && !!nextNote && nextNote.noteElement.isRestFlag) {
  105. skip = true;
  106. }
  107. // console.log(note.measureOpenIndex, item.measureOpenIndex, note);
  108. // console.log("skip", skip)
  109. if (note.measureOpenIndex != recordMeasure) {
  110. measureIndex++;
  111. recordMeasure = note.measureOpenIndex;
  112. }
  113. const data = {
  114. timeStamp: (start * 1000) / rate,
  115. duration: ((end * 1000) / rate - (start * 1000) / rate) * noteRate,
  116. frequency: item.frequency,
  117. nextFrequency: item.nextFrequency,
  118. prevFrequency: item.prevFrequency,
  119. // 重复的情况index会自然累加,render的index是谱面渲染的index
  120. measureIndex: measureIndex,
  121. measureRenderIndex: item.measureListIndex,
  122. dontEvaluating: ListenMode || dontEvaluatingMode || item.skipMode,
  123. musicalNotesIndex: index,
  124. denominator: note.noteElement?.Length.denominator,
  125. isOrnament: !!note?.voiceEntry?.ornamentContainer,
  126. };
  127. datas.push(data);
  128. }
  129. return datas;
  130. };
  131. /** 生成数据 */
  132. const hanldeCreateData = async () => {
  133. const behaviorId = localStorage.getItem("behaviorId") || undefined;
  134. const rate = state.speed / state.originSpeed;
  135. const content = {
  136. musicXmlInfos: formatTimes(),
  137. subjectId: state.subjectId,
  138. detailId: state.detailId,
  139. examSongId: state.examSongId,
  140. xmlUrl: state.xmlUrl,
  141. partIndex: state.partIndex,
  142. behaviorId,
  143. platform: browserInfo.ios ? "IOS" : browserInfo.android ? "ANDROID" : "WEB",
  144. clientId: storeData.platformType === "STUDENT" ? "student" : storeData.platformType === "TEACHER" ? "teacher" : "education",
  145. hertz: state.setting.frequency,
  146. reactionTimeMs: state.setting.reactionTimeMs,
  147. speed: state.speed,
  148. heardLevel: state.setting.evaluationDifficulty,
  149. beatLength: Math.round((state.fixtime * 1000) / rate),
  150. practiceSource: query.unitId ? "UNIT_TEST" : "PRACTICE",
  151. feature: "EVALUATION",
  152. // evaluationCriteria: getEvaluationCriteria(),
  153. };
  154. await connectWebsocket(content);
  155. // 切换为伴奏
  156. if (state.accompany) {
  157. state.playSource = "background";
  158. }
  159. console.log("连接成功");
  160. };
  161. /** 评测结果按钮处理 */
  162. const handleEvaluatResult = (type: "practise" | "tryagain" | "look" | "share" | "update") => {
  163. if (type === "update") {
  164. // 上传云端
  165. evaluatModel.evaluatUpdateAudio = true;
  166. return;
  167. } else if (type === "share") {
  168. // 分享
  169. evaluatModel.shareMode = true;
  170. return;
  171. } else if (type === "look") {
  172. // 跳转
  173. handleViewReport("recordIdStr", "orchestra");
  174. return;
  175. } else if (type === "practise") {
  176. // 去练习
  177. handleStartEvaluat();
  178. } else if (type === "tryagain") {
  179. // 再来一次
  180. }
  181. evaluatingData.resulstMode = false;
  182. };
  183. /** 上传音视频 */
  184. const hanldeUpdateVideoAndAudio = async (update = false) => {
  185. if (!update) {
  186. evaluatModel.evaluatUpdateAudio = false;
  187. return;
  188. }
  189. if (state.setting.camera && state.setting.saveToAlbum) {
  190. evaluatModel.evaluatUpdateAudio = false;
  191. api_videoUpdate((res: any) => {
  192. if (res) {
  193. if (res?.content?.type === "success") {
  194. handleSaveResult({
  195. recordId: evaluatingData.resultData?.recordIdStr,
  196. filePath: res?.content?.filePath,
  197. });
  198. } else if (res?.content?.type === "error") {
  199. showToast({
  200. message: res.content?.message || "上传失败",
  201. });
  202. }
  203. }
  204. });
  205. return;
  206. }
  207. evaluatModel.evaluatUpdateAudio = false;
  208. handleSaveResult({
  209. recordId: evaluatingData.resultData?.recordId,
  210. });
  211. };
  212. const handleSaveResult = (_body: any) => {
  213. api_proxyServiceMessage({
  214. header: {
  215. commond: "videoUpload",
  216. status: 200,
  217. type: "SOUND_COMPARE",
  218. },
  219. body: _body,
  220. });
  221. showToast({
  222. message: "上传成功",
  223. });
  224. };
  225. onMounted(() => {
  226. handlePerformDetection();
  227. });
  228. watch(
  229. () => evaluatingData.checkEnd,
  230. () => {
  231. if (evaluatingData.checkEnd) {
  232. console.log("检测结束,生成评测数据");
  233. hanldeCreateData();
  234. }
  235. }
  236. );
  237. /** 监听评测结束 */
  238. watch(
  239. () => evaluatingData.resulstMode,
  240. () => {
  241. // 评测结束, 并且完整评测
  242. if (evaluatingData.resulstMode && evaluatingData.isComplete) {
  243. /** 有单元测验时,存储分数缓存 */
  244. api_setCache({
  245. key: "h5-orchestra-unit",
  246. value: JSON.stringify({
  247. musicId: query.id || "",
  248. unitId: query.unitId || "",
  249. questionId: query.questionId || "",
  250. score: evaluatingData.resultData?.score || 0,
  251. }),
  252. });
  253. }
  254. }
  255. );
  256. return () => (
  257. <div>
  258. <Transition name="pop-center">
  259. {evaluatingData.websocketState && !evaluatingData.startBegin && (
  260. <div class={styles.startBtn} onClick={handleStartBegin}>
  261. <img src={iconEvaluat.evaluatingStart} />
  262. </div>
  263. )}
  264. </Transition>
  265. <div style={{ display: !evaluatingData.startBegin ? "" : "none" }} class={styles.dialogueBox} key="start">
  266. <div class={styles.dialogue}>
  267. <img class={styles.dialoguebg} src={iconEvaluat["task-bg"]} />
  268. <div>演奏前请调整好乐器,保证最佳演奏状态。</div>
  269. </div>
  270. <Vue3Lottie class={styles.dialogueIcon} animationData={startData}></Vue3Lottie>
  271. </div>
  272. <div style={{ display: evaluatingData.startBegin ? "" : "none" }} class={styles.dialogueBox} key="start">
  273. <div class={styles.dialogueing}>收音中...</div>
  274. <Vue3Lottie class={styles.dialogueIcon} animationData={startingData}></Vue3Lottie>
  275. </div>
  276. <Popup teleport="body" closeOnClickOverlay={false} defaultStyle={false} v-model:show={evaluatingData.earphoneMode}>
  277. <Earphone
  278. onClose={() => {
  279. evaluatingData.earphoneMode = false;
  280. handlePerformDetection();
  281. }}
  282. />
  283. </Popup>
  284. <Popup teleport="body" closeOnClickOverlay={false} defaultStyle={false} v-model:show={evaluatingData.soundEffectMode}>
  285. <SoundEffect
  286. onClose={(value: any) => {
  287. evaluatingData.soundEffectMode = false;
  288. console.log('设置soundEffect')
  289. if (value) {
  290. state.setting.soundEffect = false;
  291. }
  292. handleEndSoundCheck();
  293. handlePerformDetection();
  294. }}
  295. />
  296. </Popup>
  297. <Popup teleport="body" closeOnClickOverlay={false} defaultStyle={false} v-model:show={evaluatingData.resulstMode}>
  298. <EvaluatResult onClose={handleEvaluatResult} />
  299. </Popup>
  300. <Popup teleport="body" closeOnClickOverlay={false} defaultStyle={false} v-model:show={evaluatModel.evaluatUpdateAudio}>
  301. <EvaluatAudio onClose={hanldeUpdateVideoAndAudio} />
  302. </Popup>
  303. <Popup teleport="body" defaultStyle={false} v-model:show={evaluatModel.shareMode}>
  304. <EvaluatShare onClose={() => (evaluatModel.shareMode = false)} />
  305. </Popup>
  306. </div>
  307. );
  308. },
  309. });