Browse Source

feat: 新版评测报告修改

TIANYONG 2 months ago
parent
commit
c9d0781a3e

+ 1 - 1
osmd-extended

@@ -1 +1 @@
-Subproject commit 9cd9602f73d1392390f938a1d99db257ef062bb1
+Subproject commit 647c6d5387e5d379740089499eaa32643e0d802f

+ 3 - 2
src/page-instrument/evaluat-model/index.tsx

@@ -318,8 +318,9 @@ export default defineComponent({
           isStaccato: item?.voiceEntry?.isStaccato ? true : false, // 是否是重音
           frequencyList: item.frequencyList, // 如果是和弦音符,需要添加多个音符的频率,用于评测
           timeSignature,
-          noteKey,
-          measureDuration
+          // noteKey,
+          measureDuration,
+          firstMeasureNumber: state.firstMeasureNumber
         };
         datas.push(data);
       }

BIN
src/page-instrument/report-new/image/videobg.png


+ 10 - 1
src/page-instrument/report-new/index.module.less

@@ -256,7 +256,16 @@ body {
     .stLine {
         position: absolute;
         width: 1PX;
-        background: rgba(120, 211, 205, 0.58);
+        // background: rgba(120, 211, 205, 0.58);
+        border-right: 1px dashed;
+        /* 利用 repeating-linear-gradient 自定义 dash 长度 */
+        border-image: repeating-linear-gradient(
+            180deg,
+            rgba(120, 211, 205, 0.3) 0,
+            rgba(120, 211, 205, 0.3) 4px,
+            transparent 4px,
+            transparent 6px
+        ) 1;
     }
 }
 

+ 152 - 78
src/page-instrument/report-new/index.tsx

@@ -1,20 +1,23 @@
-import { Skeleton, Switch } from "vant";
-import { defineComponent, onBeforeMount, onBeforeUnmount, onMounted, reactive, Transition, watch, ref } from "vue";
-import state, { isRhythmicExercises, getMusicDetail, EnumMusicRenderType } from "../../state";
-import MusicScore from "../../view/music-score";
+import { Skeleton, Switch, Popup } from "vant";
+import { defineComponent, onBeforeMount, onBeforeUnmount, onMounted, onUnmounted, reactive, Transition, watch, ref, computed, nextTick } from "vue";
+import state from "../../state";
 import styles from "./index.module.less";
 import { getQuery } from "/src/utils/queryString";
 import backIcon from "./image/back_icon.png";
 import videoIcon from "./image/video_icon.png";
 import { api_musicPracticeRecordDetail } from "../api";
 import { buildFreqTable, frequencyToNote, calculateNoteRange, getAbsoluteIndex } from "/src/utils/frequency"
-
+import "plyr/dist/plyr.css";
+import Plyr from "plyr";
+import { browser } from "/src/utils";
+import videobg from "./image/videobg.png";
+import { api_back } from "/src/helpers/communication";
 
 export default defineComponent({
   name: "music-list",
   setup() {
     const query: any = getQuery();
-
+    const browserInfo = browser();
     // 设定可视化参数
     interface VisualConfig {
         msPerPixel: number; // 1 像素代表多少毫秒
@@ -29,10 +32,22 @@ export default defineComponent({
         startTime: number;
         endTime: number;
     }
+    const level: any = {
+        BEGINNER: "入门级",
+        ADVANCED: "进阶级",
+        PERFORMER: "大师级",
+    };    
     const scoreData = reactive({
         videoFilePath: "", // 回放视频路径
+        intonation: 0, // 音准
+        cadence: 0, // 节奏
+        integrity: 0, // 完整度
+        score: 0,
+        speed: 0,
+        musicSheetName: '',
+        evaluationStandard: '' as any, // evaluationStandard:("评测标准 节奏 AMPLITUDE 音准 FREQUENCY 分贝 DECIBELS")
+        heardLevel: "",        
         openRealPitch: false,
-        xmlInfo: {} as any,
         xmlNotes: [],
         xmlMaxFrequency: 0, // 实际的最高音(xml和用户演奏的取高值)
         xmlMinFrequency: 0, // 实际的最低音(xml和用户演奏的取低值)
@@ -43,6 +58,12 @@ export default defineComponent({
         baseNoteList: [] as any, // xml标准音符
     });
 
+    const shareData = reactive({
+        show: false,
+        isInitPlyr: false,
+        _plrl: null as any,
+    });
+
     // 根据音符构造小节信息
     const createMeasureList = (notes: Array<any>) => {
         let measureList = [];
@@ -59,7 +80,8 @@ export default defineComponent({
         let preMeasureTimeSignature = null;
         for (let index = 0; index < notes.length; index++) {
             const note = notes[index];
-            const { measureRenderIndex, frequencyList, frequency, dontEvaluating, duration, timeSignature = {numerator: 2, denominator: 4} } = note;
+            const timeSignature = note.timeSignature ? JSON.parse(note.timeSignature) : {numerator: 2, denominator: 4};
+            const { measureRenderIndex, frequencyList, frequency, dontEvaluating, duration, measureDuration } = note;
             startTime = endTime;
             endTime = endTime + duration / 1000
             xmlMaxFrequency = Math.max(...frequencyList,xmlMaxFrequency)
@@ -82,7 +104,7 @@ export default defineComponent({
                     timeSignature,
                     timeSignatureDiff: true,
                     noteList,
-                    measureDuration: 2
+                    measureDuration: parseFloat(measureDuration)
                 })
             } else {
                 if (preMeasureIndex === measureRenderIndex) {
@@ -113,7 +135,7 @@ export default defineComponent({
                         timeSignature: timeSignature,
                         timeSignatureDiff: currentTimeSignature !== preMeasureTimeSignature ? true: false,
                         noteList,
-                        measureDuration: 2
+                        measureDuration: parseFloat(measureDuration)
                     })
                     preMeasureTimeSignature = currentTimeSignature
                 }
@@ -145,8 +167,7 @@ export default defineComponent({
         let resultData = {} as any;
         try {
           resultData = eval('(' + res?.data?.scoreData + ')');
-          scoreData.xmlInfo = eval('(' + JSON.parse(res.data.scoreData)?.musicalNotesPlayStats?.musicXmlBasicInfo + ')') || {}
-          scoreData.xmlNotes = scoreData.xmlInfo.musicXmlInfos || []
+          scoreData.xmlNotes = JSON.parse(res.data.scoreData)?.musicalNotesPlayStats?.musicXmlBasicInfo || []
           scoreData.useMinFrequency = resultData?.musicalNotesPlayStats?.timeChunkAnalysisList?.find((item: any) => item.baseFrequency > 0).baseFrequency
           resultData?.musicalNotesPlayStats?.timeChunkAnalysisList.forEach((item: any) => {
             const { baseFrequency, startTime, endTime } = item
@@ -160,37 +181,45 @@ export default defineComponent({
                 })
             }
           })
+          scoreData.heardLevel = res.data?.heardLevel;
+          scoreData.cadence = res.data?.cadence;
+          scoreData.integrity = res.data?.integrity;
+          scoreData.intonation = res.data?.intonation;
+          scoreData.score = res.data?.score;
+          scoreData.speed = res.data?.speed;
+          scoreData.videoFilePath = res.data?.videoFilePath || res.data?.recordFilePath;          
+          scoreData.musicSheetName = res.data?.musicSheetName;
+          scoreData.evaluationStandard = res.data?.evaluationStandard;
           createMeasureList(scoreData.xmlNotes)
           console.log(11111,resultData,scoreData)
           drawTable()
         } catch (error) {
           console.error("解析评测结果:", error);
         }
-        scoreData.videoFilePath = res.data?.videoFilePath || res.data?.recordFilePath;
     }
   
-  // 创建网格
-  function createGrid(container: HTMLElement, config: VisualConfig) {
+    // 创建网格
+    function createGrid(container: HTMLElement, config: VisualConfig) {
     const { topNoteIndex, bottomNoteIndex, semitoneHeight, msPerPixel } = config;
     const numNotes = topNoteIndex - bottomNoteIndex + 1;
     const containerHeight = numNotes * semitoneHeight;
-  
+
     container.style.height = `${containerHeight}px`;
-  
+
     for (let i = bottomNoteIndex; i <= topNoteIndex; i++) {
-      const line = document.createElement("div");
-      line.className = "note-line";
-      line.style.top = `${(topNoteIndex - i) * semitoneHeight}px`;
-      container.appendChild(line);
+        const line = document.createElement("div");
+        line.className = "note-line";
+        line.style.top = `${(topNoteIndex - i) * semitoneHeight}px`;
+        container.appendChild(line);
     }
-  
+
     // 画时间刻度
     for (let t = 0; t <= 5000; t += 1000) {
-      const timeMarker = document.createElement("div");
-      timeMarker.className = "time-marker";
-      timeMarker.style.left = `${t / msPerPixel}px`;
-      timeMarker.innerText = `${t / 1000}s`;
-      // container.appendChild(timeMarker);
+        const timeMarker = document.createElement("div");
+        timeMarker.className = "time-marker";
+        timeMarker.style.left = `${t / msPerPixel}px`;
+        timeMarker.innerText = `${t / 1000}s`;
+        // container.appendChild(timeMarker);
     }
 
     // 绘制小节序号和拍号
@@ -228,64 +257,87 @@ export default defineComponent({
     container.appendChild(measureDom);
     container.appendChild(lineDom);
 
-  }
-  
-  // 渲染音符
-  function renderNotes(container: HTMLElement, noteEvents: NoteEvent[], config: VisualConfig, className: string) {
+    }
+
+    // 渲染音符
+    function renderNotes(container: HTMLElement, noteEvents: NoteEvent[], config: VisualConfig, className: string) {
     noteEvents.forEach((event) => {
-      const { note, octave } = frequencyToNote(event.freq, config.baseA4);
-      const absIndex = getAbsoluteIndex(note, octave);
-  
-      if (absIndex < config.bottomNoteIndex || absIndex > config.topNoteIndex) return;
-  
-      const noteDiv = document.createElement("div");
-      noteDiv.className = className;
-      if (className === 'note') {
+        const { note, octave } = frequencyToNote(event.freq, config.baseA4);
+        const absIndex = getAbsoluteIndex(note, octave);
+
+        if (absIndex < config.bottomNoteIndex || absIndex > config.topNoteIndex) return;
+
+        const noteDiv = document.createElement("div");
+        noteDiv.className = className;
+        if (className === 'note') {
         noteDiv.innerHTML = `<span>${note}${octave}</span>`
-      }
-      // noteDiv.innerText = `${note}${octave}`;
-  
-      // 计算位置
-      const xStart = event.startTime / config.msPerPixel;
-      const xEnd = event.endTime / config.msPerPixel;
-      noteDiv.style.left = `${xStart}px`;
-      noteDiv.style.width = `${xEnd - xStart}px`;
-      noteDiv.style.top = `${(config.topNoteIndex - absIndex) * config.semitoneHeight}px`;
-  
-      container.appendChild(noteDiv);
+        }
+        // noteDiv.innerText = `${note}${octave}`;
+
+        // 计算位置
+        const xStart = event.startTime / config.msPerPixel;
+        const xEnd = event.endTime / config.msPerPixel;
+        noteDiv.style.left = `${xStart}px`;
+        noteDiv.style.width = `${xEnd - xStart}px`;
+        noteDiv.style.top = `${(config.topNoteIndex - absIndex) * config.semitoneHeight}px`;
+
+        container.appendChild(noteDiv);
     });
-  }
+    }
   
-  // 主函数
-  const drawTable = () => {
+    // 主函数
+    const drawTable = () => {
     const container = document.getElementById("visualizer") as HTMLElement;
     if (!container) return;
     const realFreqRange = calculateNoteRange(scoreData.xmlMinFrequency, scoreData.xmlMaxFrequency)
     const config: VisualConfig = {
-      msPerPixel: 10,
-      semitoneHeight: 30,
-      baseA4: 442,
-      topNoteIndex: realFreqRange.topNoteIndex + 3,
-      bottomNoteIndex: realFreqRange.bottomNoteIndex - 3,
+        msPerPixel: 10,
+        semitoneHeight: 30,
+        baseA4: 442,
+        topNoteIndex: realFreqRange.topNoteIndex + 3,
+        bottomNoteIndex: realFreqRange.bottomNoteIndex - 3,
     };
-  
+
     container.innerHTML = "";
     createGrid(container, config);
-  
+
     const noteEvents: NoteEvent[] = [
-      { freq: 261.63, startTime: 0, endTime: 500 },
-      { freq: 293.66, startTime: 500, endTime: 1000 },
-      { freq: 468.0, startTime: 1200, endTime: 1800 },
-      { freq: 523.25, startTime: 1800, endTime: 2500 },
+        { freq: 261.63, startTime: 0, endTime: 500 },
+        { freq: 293.66, startTime: 500, endTime: 1000 },
+        { freq: 468.0, startTime: 1200, endTime: 1800 },
+        { freq: 523.25, startTime: 1800, endTime: 2500 },
     ];
     // 绘制标准音符
     renderNotes(container, scoreData.baseNoteList, config, "note");
     // 绘制实际用户演奏音符
     renderNotes(container, scoreData.timeChunkAnalysisList, config, "realNote");
-  }
-
-  
+    }
 
+    /** 返回 */
+    const handleBack = () => {
+        api_back();
+    };
+    // 资源类型
+    const mediaType = computed((): "audio" | "video" => {
+      const subfix = (scoreData.videoFilePath || "").split(".").pop();
+      if (subfix === "wav" || subfix === "mp3" || subfix === "m4a") {
+        return "audio";
+      }
+      return "video";
+    }); 
+    const isPad =  navigator?.userAgent?.includes("UAWEIVRD-W09") || browserInfo?.iPad || browserInfo.isTablet;  
+    const openAudioAndVideo = () => {
+        shareData.show = true;
+        if (shareData.isInitPlyr) return;
+        nextTick(() => {
+            const id = mediaType.value === "audio" ? "#audioSrc" : "#videoSrc";
+            shareData._plrl = new Plyr(id, {
+                controls: ["play-large", "play", "progress", "current-time", "duration"],
+                fullscreen: { enabled: false },
+            });
+            shareData.isInitPlyr = true;
+        });
+    };
 
     onBeforeMount(() => {
 
@@ -308,22 +360,24 @@ export default defineComponent({
 
         
     });
-
+    onUnmounted(()=>{
+      shareData._plrl?.destroy()
+    })
 
     return () => (
       <div class={styles.reportDetail}>     
         <div class={styles.reportHead}>
-            <img class={styles.backIcon} src={backIcon} />
+            <img class={styles.backIcon} src={backIcon} onClick={handleBack} />
             <div class={styles.content}>
                 <div class={styles.title}>
-                    <span class={styles.titleName}>天空之城</span>
-                    <span class={styles.titleLevel}>入门级|速度90</span>
+                    <span class={styles.titleName}>{scoreData.musicSheetName}</span>
+                    <span class={styles.titleLevel}>{level[scoreData.heardLevel]}|速度{Math.floor(scoreData.speed)}</span>
                 </div>
                 <div class={styles.score}>
-                    <span class={styles.total}>总分: 78</span>
-                    <span>|音准: 67</span>
-                    <span>|节奏: 87</span>
-                    <span>|完整度: 87</span>
+                    <span class={styles.total}>总分: {scoreData.score}</span>
+                    <span>|音准: {scoreData.intonation}</span>
+                    <span>|节奏: {scoreData.cadence}</span>
+                    <span>|完整度: {scoreData.integrity}</span>
                 </div>
             </div>
             <div class={styles.right}>
@@ -340,8 +394,10 @@ export default defineComponent({
                     onChange={ async (value) => {
                         // 
                     }}
-                ></Switch>                
-                <img class={styles.videoIcon} src={videoIcon} />
+                ></Switch>   
+                {
+                    mediaType.value === 'video' && <img class={styles.videoIcon} src={videoIcon} onClick={openAudioAndVideo} />
+                }   
             </div>
         </div>
 
@@ -385,8 +441,26 @@ export default defineComponent({
             <div id="visualizer" class={styles.rcTable}></div>
         </div> */}
 
+        <Popup
+            teleport="body"
+            class={["popup-custom", "van-scale", styles.popup]}
+            transition="van-scale"
+            v-model:show={shareData.show}
+            closeable
+            onClose={() => {
+              shareData._plrl?.pause();
+            }}
+          >
+            <div class={styles.playerBoxContent}>
+              <div class={[styles.playerBoxCon, isPad && styles.padPlayerBox]}>
+                <div class={[styles.playerBox, mediaType.value === "video" && styles.videoPlayerBox]}>
+                    <video id="videoSrc" class={styles.videoBox} src={scoreData.videoFilePath} data-poster={videobg} preload="metadata" playsinline />
+                </div>
+              </div>
+            </div>
+        </Popup>
+
       </div>
-    
     );
   },
 });

+ 1 - 1
src/utils/frequency.ts

@@ -5,7 +5,7 @@ type FrequencyTable = {
     };
 };
 
-const noteNames = ["c", "#c", "d", "#d", "e", "f", "#f", "g", "#g", "a", "#a", "b"];  
+const noteNames = ["c", "♯c", "d", "♯d", "e", "f", "♯f", "g", "♯g", "a", "♯a", "b"];  
   
 /**
  * 根据给定音名、八度和基准 A4 频率计算音符频率