Bläddra i källkod

Merge branch 'feature-tianyong' into klx-online

TIANYONG 4 månader sedan
förälder
incheckning
79b3fa7fd6

+ 2 - 0
src/helpers/calcSpeed.ts

@@ -123,7 +123,9 @@ export type GradualItem = {
  */
 export const getGradualLengthByXml = (xml: string) => {
 	const firstPartXml = onlyVisible(xml, 0, 'calc')
+	//console.time('解析xml 耗时2')
 	const xmlParse = new DOMParser().parseFromString(firstPartXml, "text/xml");
+	//console.timeEnd('解析xml 耗时2')
 	const measures = Array.from(xmlParse.querySelectorAll("measure"));
 	const notes = Array.from(xmlParse.querySelectorAll("note"));
 	const words = Array.from(xmlParse.querySelectorAll("words"));

+ 25 - 7
src/helpers/formateMusic.ts

@@ -15,6 +15,7 @@ import {
 } from "/osmd-extended/src";
 import { GradualChange, speedInfo } from "./calcSpeed";
 import { beatUnitTo, speedBeatTo } from "/src/helpers/beatConfig"
+import { xmlDocRef } from "/src/view/music-score"
 
 const browserInfo = browser();
 dayjs.extend(duration);
@@ -250,13 +251,16 @@ export type CustomInfo = {
 };
 
 /** 从xml中获取自定义信息,并删除多余的字符串 */
-export const getCustomInfo = (xml: string): CustomInfo => {
+export const getCustomInfo = (xml: string, resourceType?: string): CustomInfo => {
 	const data = {
 		showSpeed: true,
 		parsedXML: xml,
 	};
-	const xmlParse = new DOMParser().parseFromString(xml, "text/xml");
-	const words: any = xmlParse.getElementsByTagName("words");
+	//console.time('解析xml 耗时3')
+	// const xmlParse = new DOMParser().parseFromString(xml, "text/xml");
+	const xmlParse = xmlDocRef.value && resourceType === 'init' ? xmlDocRef.value : new DOMParser().parseFromString(xml, "text/xml");
+	//console.timeEnd('解析xml 耗时3')
+	const words: any = xmlParse?.getElementsByTagName("words");
 	for (const word of words) {
 		if (word && word.textContent?.trim() === "隐藏速度") {
 			data.showSpeed = false;
@@ -365,7 +369,10 @@ export const onlyVisible = (xml: string, partIndex: number, resourceType?: strin
 	if (!xml) return "";
 	// console.log('原始xml')
 	const detailId = state.cbsExamSongId + "";
-	const xmlParse = new DOMParser().parseFromString(xml, "text/xml");
+	//console.time('解析xml 耗时4')
+	// const xmlParse = new DOMParser().parseFromString(xml, "text/xml");
+	const xmlParse = xmlDocRef.value && !resourceType ? xmlDocRef.value : new DOMParser().parseFromString(xml, "text/xml");
+	//console.timeEnd('解析xml 耗时4')
 	const partList = xmlParse.getElementsByTagName("part-list")?.[0]?.getElementsByTagName("score-part") || [];
 	const partListNames = Array.from(partList).map((item) => item.getElementsByTagName("part-name")?.[0]?.textContent?.trim() || "");
 	const parts: any = xmlParse.getElementsByTagName("part");
@@ -535,7 +542,9 @@ export const onlyVisible2 = (xml: string): string => {
 	if (!xml) return "";
 	// console.log('原始xml')
 	//const detailId = state.cbsExamSongId + "";
+	console.time('解析xml 耗时5')
 	const xmlParse = new DOMParser().parseFromString(xml, "text/xml");
+	console.timeEnd('解析xml 耗时5')
 	const partList = xmlParse.getElementsByTagName("part-list")?.[0]?.getElementsByTagName("score-part") || [];
 	//const partListNames = Array.from(partList).map((item) => item.getElementsByTagName("part-name")?.[0]?.textContent?.trim() || "");
 	//state.partListNames = partListNames;
@@ -628,7 +637,9 @@ export const formatZoom = (num = 1) => {
 /** 妙极客多分轨的曲子,可能没有part-name标签,需要手动加上该标签 */
 export const xmlAddPartName = (xml: string) => {
 	if (!xml) return "";
+	console.time('解析xml 耗时')
 	const xmlParse = new DOMParser().parseFromString(xml, "text/xml");
+	console.timeEnd('解析xml 耗时')
 	const scoreParts = Array.from(xmlParse.getElementsByTagName("score-part"));
 	for (const scorePart of scoreParts) {
 		if (scorePart.getElementsByTagName("part-name").length === 0) {
@@ -642,15 +653,19 @@ export const xmlAddPartName = (xml: string) => {
 			scorePart.getElementsByTagName("part-name")[0].textContent = scorePart.getAttribute("id") || "";
 		}
 	}
+	xmlDocRef.value = xmlParse;
 	return new XMLSerializer().serializeToString(xmlParse);
 }
 
 /** 格式化曲谱
  * 1.全休止符的小节,没有音符默认加个全休止符
  */
-export const formatXML = (xml: string, xmlUrl?: string): string => {
+export const formatXML = (xml: string, xmlUrl?: string, resourceType?: string): string => {
 	if (!xml) return "";
-	const xmlParse = new DOMParser().parseFromString(xml, "text/xml");
+	//console.time('解析xml 耗时7')
+	// const xmlParse = new DOMParser().parseFromString(xml, "text/xml");
+	const xmlParse = xmlDocRef.value && resourceType === 'init' ? xmlDocRef.value : new DOMParser().parseFromString(xml, "text/xml");
+	//console.timeEnd('解析xml 耗时7')
 
 	// 声调
 	const fifths = xmlParse.getElementsByTagName("fifths");
@@ -741,6 +756,7 @@ export const formatXML = (xml: string, xmlUrl?: string): string => {
 	// 前面小节的拍子
 	let preBeats: number = 4;
 	let preBeatType: number = 4;
+	let baseDivisions: number = 256;
 	// 小节中如果没有节点默认为休止符
 	for (const measure of measures) {
 		if (beats === -1 && measure.getElementsByTagName("beats").length) {
@@ -757,7 +773,8 @@ export const formatXML = (xml: string, xmlUrl?: string): string => {
 		const currentBeatType = measure.getElementsByTagName("beat-type").length ? measure.getElementsByTagName("beat-type")[0]?.textContent : preBeatType;
 		preBeats = Number(currentBeats);
 		preBeatType = Number(currentBeatType);
-		const divisions = parseInt(measure.getElementsByTagName("divisions")[0]?.textContent || "256");
+		const divisions = parseInt(measure.getElementsByTagName("divisions")[0]?.textContent || String(baseDivisions));
+		baseDivisions = divisions
 		// 如果note节点里面有space节点,并且没有duration节点,代表这是一个空白节点,需要删除
 		if (measure.getElementsByTagName("note").length && state.isEvxml) {
 			const noteList = Array.from(measure.getElementsByTagName("note")) || [];
@@ -1392,6 +1409,7 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 				isStaccato: note.voiceEntry.isStaccato(),
 				isRestFlag: note.isRestFlag,
 				noteId: note.NoteToGraphicalNoteObjectId,
+				// noteId: note.NoteToGraphicalNoteObjectId || `restNote${note.sourceMeasure.MeasureNumberXML}`,
 				measureListIndex: note.sourceMeasure.measureListIndex,
 				MeasureNumberXML: note.sourceMeasure.MeasureNumberXML, // 当前的小节数,(从1开始)
 				_noteLength: _noteLength,

+ 50 - 0
src/page-instrument/App.tsx

@@ -126,6 +126,56 @@ export default defineComponent({
       //     event.preventDefault();
       //   }
       // });
+      
+      /**
+       * DNS 解析耗时: domainLookupEnd - domainLookupStart
+       * TCP 连接耗时: connectEnd - connectStart
+       * SSL 安全连接耗时: connectEnd - secureConnectionStart
+       * 网络请求耗时 (TTFB): responseStart - requestStart
+       *数据传输耗时: responseEnd - responseStart
+        *DOM 解析耗时: domInteractive - responseEnd
+        *资源加载耗时: loadEventStart - domContentLoadedEventEnd
+        *First Byte时间: responseStart - domainLookupStart
+        *白屏时间: responseEnd - fetchStart
+        *首次可交互时间: domInteractive - fetchStart
+        *DOM Ready 时间: domContentLoadEventEnd - fetchStart
+        *页面完全加载时间: loadEventStart - fetchStart
+        *http 头部大小: transferSize - encodedBodySize
+        *重定向次数:performance.navigation.redirectCount
+        *重定向耗时: redirectEnd - redirectStart
+       */
+      window.onload = function() {
+        console.log('加载完成')
+        let timing: any = performance.getEntriesByType('navigation')[0] || {};
+        const { domainLookupEnd, domainLookupStart, connectEnd, connectStart, responseStart,
+          requestStart, responseEnd, domInteractive, loadEventStart, domContentLoadedEventEnd,
+          fetchStart, secureConnectionStart, transferSize, encodedBodySize,
+          redirectEnd, redirectStart
+        } = timing
+        //console.log(timing.domInteractive);
+        //console.log(timing.fetchStart);
+        let diff = timing.domInteractive - timing.fetchStart;
+        //console.log("TTI: " + diff);
+        const timeInfo = [
+          { '类型': 'DNS 解析耗时', '耗时': domainLookupEnd - domainLookupStart},
+          { '类型': 'TCP 连接耗时', '耗时': connectEnd - connectStart},
+          { '类型': 'SSL 安全连接耗时', '耗时': connectEnd - secureConnectionStart},
+          { '类型': '网络请求耗时', '耗时': responseStart - requestStart},
+          { '类型': '数据传输耗时', '耗时': responseEnd - responseStart},
+          { '类型': 'DOM 解析耗时', '耗时': domInteractive - responseEnd},
+          { '类型': '资源加载耗时', '耗时': loadEventStart - domContentLoadedEventEnd},
+          { '类型': 'First Byte时间', '耗时': responseStart - domainLookupStart},
+          { '类型': '白屏时间', '耗时': responseEnd - fetchStart},
+          { '类型': '首次可交互时间', '耗时': domInteractive - fetchStart},
+          { '类型': 'DOM Ready 时间', '耗时': domContentLoadedEventEnd - fetchStart},
+          { '类型': '页面完全加载时间', '耗时': loadEventStart - fetchStart},
+          { '类型': 'http 头部大小', '耗时': transferSize - encodedBodySize},
+          { '类型': '重定向次数', '耗时': performance.navigation.redirectCount},
+          { '类型': '重定向耗时', '耗时': redirectEnd - redirectStart},
+        ];
+        console.table(timeInfo);
+        
+      };      
     });
 
     onUnmounted(() => {

+ 22 - 1
src/page-instrument/evaluat-model/index.tsx

@@ -178,6 +178,8 @@ export default defineComponent({
       let skip = false;
       const datas = [];
       let selectTimes = state.times;
+      // 选段评测前面小节的listen、play标识
+      let preLyricsContent = ''
       let unitTestIdx = 0;
       let preTime = 0;
       let preTimes = [];
@@ -218,7 +220,22 @@ export default defineComponent({
       if (state.section.length === 2 && firstNoteTime === 0 && state.section[0]?.MeasureNumberXML === state.firstMeasureNumber + 1 && state.times[0].fixtime) {
         actualBeatLength  = actualBeatLength + Math.round((state.times[0].fixtime * 1000) / 1)
       }
-
+      // 找到选段评测,开始小节前面最近的是play或者listen的小节
+      if (preTimes.length) {
+        for (let index = preTimes.length-1; index >= 0; index--) {
+          const item = preTimes[index]
+          const note = getNoteByMeasuresSlursStart(item)
+          if (note.formatLyricsEntries.contains('Play') || note.formatLyricsEntries.contains('Play...')) {
+            preLyricsContent = 'Play'
+            break
+          }
+          if (note.formatLyricsEntries.contains('Listen')) {
+            preLyricsContent = 'Listen'
+            break
+          }
+        }
+        preLyricsContent = preLyricsContent ? preLyricsContent : 'Play'
+      }
       for (let index = 0; index < selectTimes.length; index++) {
         const item = selectTimes[index];
         const note = getNoteByMeasuresSlursStart(item);
@@ -231,6 +248,10 @@ export default defineComponent({
         const end = difftime + (item.sourceRelaEndtime || item.relaEndtime) - starTime;
         const isStaccato = note.noteElement.voiceEntry.isStaccato();
         const noteRate = isStaccato ? 0.5 : 1;
+        // 如果选段评测,开始小节没有注脚,则取前面最近的小节的注脚
+        if (index == 0 && !note.formatLyricsEntries.length) {
+          ListenMode = preLyricsContent === 'Play' ? false : preLyricsContent === 'Listen' ? true : false
+        }        
         if (note.formatLyricsEntries.contains("Play") || note.formatLyricsEntries.contains("Play...")) {
           ListenMode = false;
         }

+ 23 - 3
src/page-instrument/view-detail/index.tsx

@@ -119,6 +119,7 @@ export default defineComponent({
         }
       }
     };
+    console.time('加载过程')
     onBeforeMount(async () => {
       // console.time("渲染加载耗时");
       api_keepScreenLongLight();
@@ -215,10 +216,29 @@ export default defineComponent({
 
     /** 渲染完成 */
     const handleRendered = (osmd: any) => {
+      state.isLoading = false
       api_cloudLoading();
       console.timeEnd("渲染加载耗时");
+      console.timeLog('加载过程','谱面渲染完成')
+
       detailData.skeletonLoading = false;
       state.osmd = osmd;
+      // 预览模式不需要往下执行
+      if (state.isPreView) {
+        // 酷乐秀曲谱详情页,需要下载A4尺寸的图片
+        setTimeout(() => {
+          if (query.downPng === 'A4' && state.partIndex != 999) {
+            const imgList = getSvgPngToSize(state.osmd)
+            console.log('A4', imgList)
+            window.parent.postMessage({
+              api: 'musicStaffRender',
+              loading: false,
+              osmdImg: imgList
+            }, '*');
+          }
+        }, 100);
+      return;
+    }
       // 没有设置速度使用读取的速度
       if (state.originSpeed === 0) {
         state.originSpeed = state.speed = (osmd as any).bpm || osmd.Sheet.userStartTempoInBPM || 100;
@@ -620,9 +640,9 @@ export default defineComponent({
             </div>
           )}
         </div>
-        {/* 曲目渲染完成,再去下载mp3资源 */}
-        {!detailData.isLoading && !detailData.skeletonLoading && <AudioList />}
-
+        {/* 曲目渲染时,同步下载mp3资源 */}
+        {!detailData.isLoading && <AudioList />}
+        {/* {!detailData.isLoading && !detailData.skeletonLoading && <AudioList />} */}
         {/* {!detailData.isLoading && <TheAudio src={tickWav} />} */}
 
         {/* 预加载延迟检测组建 */}

+ 6 - 0
src/page-instrument/view-evaluat-report/index.tsx

@@ -366,6 +366,12 @@ export default defineComponent({
       // @ts-ignore
       const startMeasureNum = detailData.musicalNotesPlayStats?.[0]?.measureRenderIndex, endMeasureNum = detailData.musicalNotesPlayStats?.last()?.measureRenderIndex;
       allNote.value = allNote.value.filter((item: any) => (item.MeasureNumberXML >= startMeasureNum+1 && item.MeasureNumberXML <= endMeasureNum+1))
+      // 从0开始的曲子,MeasureNumberXML也是从0开始,需要兼容处理
+      // if (state.firstMeasureNumber === 0) {
+      //   allNote.value = allNote.value.filter((item: any) => (item.MeasureNumberXML >= startMeasureNum && item.MeasureNumberXML <= endMeasureNum))
+      // } else {
+      //   allNote.value = allNote.value.filter((item: any) => (item.MeasureNumberXML >= startMeasureNum+1 && item.MeasureNumberXML <= endMeasureNum+1))
+      // }
       // @ts-ignore
       const beams = Array.from(new Set(document.getElementsByClassName("vf-beam")));
       beams.forEach((item: any) => {

+ 36 - 8
src/state.ts

@@ -17,13 +17,14 @@ import { followData, skipNotePractice } from "/src/view/follow-practice/index"
 import { changeSongSourceByBeat } from "/src/view/audio-list"
 import { moveSmoothAnimation, smoothAnimationState, moveSmoothAnimationByPlayTime, moveTranslateXNum, destroySmoothAnimation, calcClientWidth } from "/src/page-instrument/view-detail/smoothAnimation"
 import { storeData } from "/src/store";
-import { downloadXmlStr } from "./view/music-score"
+import { downloadXmlStr, xmlDocRef } from "./view/music-score"
 import { musicScoreRef, headerColumnHide } from "/src/page-instrument/view-detail/index"
 import { headTopData } from "/src/page-instrument/header-top/index";
 import { api_lessonTrainingTrainingStudentDetail } from "/src/page-instrument/api"
 import { undoData, moveData } from "/src/view/plugins/move-music-score"
 import { HANDLE_WORK_ADD } from "/src/page-instrument/custom-plugins/work-index";
 import { speedBeatTo, unitImgs } from "/src/helpers/beatConfig"
+import IndexedDBService from "/src/utils/indexedDB";
 import { musicalInstrumentCodeInfo, instruments, fixInstrumentNameCode } from "/src/constant/instruments";
 
 const query: any = getQuery();
@@ -352,7 +353,7 @@ const state = reactive({
   // 加载条
   isLoading: true,
   /** 加载中的文案 */
-  loadingText: '音频资源加载中,请稍后…',
+  loadingText: '资源加载中,请稍后…',
   /** 是否是简单的单行谱模式页面 */
   isSimplePage: false, 
   /** xml的速度和后台设置的速度,计算出的基础音频播放倍率 */
@@ -398,6 +399,8 @@ const state = reactive({
   isWebAudit: false,
   /** 是否是单声轨多声部的声轨 */
   isSingleMutliTrack: false,
+  /** 是否是来源于缓存的xml */
+  xmlFromStore: false,
 });
 const browserInfo = browser();
 let offset_duration = 0;
@@ -664,7 +667,7 @@ export const skipNotePlay = async (itemIndex: number, isStart = false, handType?
 export const togglePlay = async (playState: "play" | "paused", isForceCLoseToast?:boolean) => {
   // 如果mp3资源还在加载中,给出提示
   if (!state.isAppPlay && !state.audioDone) {
-    if (!isForceCLoseToast) showToast('音频资源加载中,请稍后')
+    if (!isForceCLoseToast) showToast('资源加载中,请稍后')
     return
   }
   // 播放之前  当为评测模式和不为MIDI时候按  是否禁用节拍器  切换音源
@@ -1282,6 +1285,25 @@ const initInstrumentCode = async () => {
   // console.log('声轨codes',instruments)
 }
 
+// 判断有没有xml缓存,有则直接使用
+const queryMusicXml = async (id: string, xmlUr: string) => {
+  let xmlString = ''
+  const dbService = new IndexedDBService("MyDatabase", "MyStore");
+  console.time('缓存获取xml')
+  const storeXmlData: any = await dbService.get(id).then((data) => data );
+  if (storeXmlData && storeXmlData.xmlString) {
+    xmlString = storeXmlData && storeXmlData.xmlString
+    state.xmlFromStore = true;
+    console.timeEnd('缓存获取xml')
+    // 使用完后删除数据
+    dbService.delete(id)
+  } else {
+    state.xmlFromStore = false;
+    xmlString = await fetch(xmlUr).then((response) => response.text());
+  }
+  return xmlString;
+}
+
 const getMusicInfo = async (res: any) => {
   try {
     await initInstrumentCode()
@@ -1292,10 +1314,11 @@ const getMusicInfo = async (res: any) => {
   state.isScoreRender = res.data?.isScoreRender
   // 是否默认显示总谱
   state.defaultScoreRender = res.data?.defaultScoreRender
-  /* 获取声轨列表 */
-  let xmlString = await fetch(res.data.xmlFileUrl).then((response) => response.text());
+  // let xmlString = await fetch(res.data.xmlFileUrl).then((response) => response.text());
+  let xmlString: string = await queryMusicXml(res.data.bizId + "", res.data.xmlFileUrl);
   xmlString = xmlAddPartName(xmlString);
   downloadXmlStr.value = xmlString //给musice-score 赋值xmlString 以免加载2次
+    /* 获取声轨列表 */
   const tracks = xmlToTracks(xmlString) //获取声轨列表
   // 是否显示节拍器  (管乐迷 默认显示节拍器)
   state.isMixBeat = res.data?.isMixBeat  
@@ -1355,9 +1378,14 @@ const getMusicInfo = async (res: any) => {
 };
 //获取xml中的音轨数据
 function xmlToTracks(xmlString: string) {
-  const xmlParse = new DOMParser().parseFromString(xmlString, "text/xml");
-  const partNames = Array.from(xmlParse.getElementsByTagName('part-name'));
-  return partNames.reduce((arr: string[], item) => {
+  //console.time('domparse')
+  // console.time('解析xml 耗时1')
+  // const xmlParse = new DOMParser().parseFromString(xmlString, "text/xml");
+  const xmlParse = xmlDocRef.value;
+  // console.timeEnd('解析xml 耗时1')
+  //console.timeEnd('domparse')
+  const partNames = xmlParse ? Array.from(xmlParse.getElementsByTagName('part-name')) : [];
+  return partNames.reduce((arr: string[], item: any) => {
     const textContent = item?.textContent?.trim()
     if (textContent?.toLocaleLowerCase() === "common") {
       (window as any).HasCommonTrack = true;

+ 117 - 0
src/utils/indexedDB.ts

@@ -0,0 +1,117 @@
+interface DataInfo {
+    id: number | string; // 主键
+    xmlString: string;
+    xmlDoc: Document;
+}
+
+export default class IndexedDBService<T> {
+    private dbName: string;
+    private storeName: string;
+    private version: number;
+  
+    constructor(dbName: string, storeName: string, version: number = 1) {
+      this.dbName = dbName;
+      this.storeName = storeName;
+      this.version = version;
+    }
+  
+    // 初始化数据库
+    private async init(): Promise<IDBDatabase> {
+      return new Promise((resolve, reject) => {
+        const request = indexedDB.open(this.dbName, this.version);
+  
+        // 数据库第一次创建或版本号升级时触发
+        request.onupgradeneeded = () => {
+          const db = request.result;
+          // 创建对象存储(类似于表)
+          if (!db.objectStoreNames.contains(this.storeName)) {
+            db.createObjectStore(this.storeName, { keyPath: "id" });
+          }
+        };
+  
+        // 返回数据库实例
+        request.onsuccess = () => resolve(request.result);
+        // 捕获错误
+        request.onerror = () => reject(request.error);
+      });
+    }
+  
+    // 添加或者更新数据
+    async save(data: T): Promise<void> {
+      const db = await this.init();
+      return new Promise((resolve, reject) => {
+        const transaction = db.transaction(this.storeName, "readwrite");
+        const store = transaction.objectStore(this.storeName);
+        const request = store.put(data);
+  
+        request.onsuccess = () => resolve();
+        request.onerror = () => reject(request.error);
+      });
+    }
+  
+    // 获取某条数据
+    async get(id: number | string): Promise<T | undefined> {
+      const db = await this.init();
+      return new Promise((resolve, reject) => {
+        const transaction = db.transaction(this.storeName, "readonly");
+        const store = transaction.objectStore(this.storeName);
+        const request = store.get(id);
+  
+        request.onsuccess = () => resolve(request.result as T);
+        request.onerror = () => reject(request.error);
+      });
+    }
+  
+    // 删除数据
+    async delete(id: number | string): Promise<void> {
+      const db = await this.init();
+      return new Promise((resolve, reject) => {
+        const transaction = db.transaction(this.storeName, "readwrite");
+        const store = transaction.objectStore(this.storeName);
+        const request = store.delete(id);
+  
+        request.onsuccess = () => resolve();
+        request.onerror = () => reject(request.error);
+      });
+    }
+  
+    // 查询所有数据
+    async getAll(): Promise<T[]> {
+      const db = await this.init();
+      return new Promise((resolve, reject) => {
+        const transaction = db.transaction(this.storeName, "readonly");
+        const store = transaction.objectStore(this.storeName);
+        const request = store.getAll();
+  
+        request.onsuccess = () => resolve(request.result as T[]);
+        request.onerror = () => reject(request.error);
+      });
+    }
+
+    // 清空某个 Object Store 中的所有数据
+    async clearAllData(): Promise<void> {
+      const db = await this.init();
+    
+      return new Promise((resolve, reject) => {
+        const transaction = db.transaction(this.storeName, "readwrite");
+        const store = transaction.objectStore(this.storeName);
+    
+        const request = store.clear(); // 清空数据
+    
+        request.onsuccess = () => resolve();
+        request.onerror = () => reject(request.error);
+      });
+    }
+
+    // 删除整个数据库
+    async deleteDatabase(): Promise<void> {
+      return new Promise((resolve, reject) => {
+        const request = indexedDB.deleteDatabase(this.dbName); // 删除整个数据库
+    
+        request.onsuccess = () => resolve();
+        request.onerror = () => reject(request.error);
+      });
+    }
+}
+  
+  

+ 15 - 5
src/view/audio-list/index.tsx

@@ -214,7 +214,8 @@ const createAudio = (src?: string): Promise<HTMLAudioElement | null> => {
 		return Promise.resolve(null)
 	}
 	return new Promise((resolve) => {
-		const a = new Audio(src + '?v=' + Date.now());
+		// const a = new Audio(src + '?v=' + Date.now());
+		const a = new Audio(src);
 		a.onloadedmetadata = () => {
 			resolve(a);
 		};
@@ -381,7 +382,7 @@ export async function changeCombineAudio (combineIndex: number){
 		}
 		return
 	}
-	state.loadingText = "音频资源加载中,请稍后…";
+	state.loadingText = "资源加载中,请稍后…";
 	state.isLoading = true;
 	const musicUrl = audioData.combineMusics[combineIndex]
 	// 有就拿缓存,没有就加载
@@ -476,8 +477,15 @@ export default defineComponent({
 		 * #11046
 		 * 声音与圆点消失的节点不一致,可能原因是部分安卓手机没有立即播放,所以需要等待有音频进度返回时再播放节拍器
 		 * mp3节拍器的圆点动画
+		 * 
+		 * #12291
+		 * 如果是选段评测,并且开始小节不是第一个小节,不需要播放节拍器的圆点动画
 		 */
 		const tickAnimate = (time: number) => {
+			if (state.section.length && state.section[0]?.MeasureNumberXML !== state.firstMeasureNumber) {
+				evaluatingData.needPlayTick = false;
+				return;
+			}			
 			if (storeData.isApp && state.modeType === 'evaluating' && evaluatingData.needPlayTick && time > 0) {
 				evaluatingData.needPlayTick = false;
 				handleStartTick()
@@ -552,7 +560,7 @@ export default defineComponent({
 				return
 			}			
 			if (state.playMode !== "MIDI") {
-				console.time("音频加载时间123")
+				console.time("音频加载时")
 				// 处理音源
 				const [music, accompany, fanSong, banSong, mingSong, mingSongGirl] = await loadAudio()
 				audioData.backgroundEle = accompany;
@@ -645,8 +653,9 @@ export default defineComponent({
 				changeMingSongType()
 
 				state.audioDone = true;
-				state.isLoading = false
-				console.timeEnd("音频加载时间123")
+				// state.isLoading = false
+				console.timeEnd("音频加载耗时")
+				console.timeLog('加载过程','音频加载完成')
 				console.log("音频数据:",audioData)
 				api_playProgress(progress);
 			} else {
@@ -659,6 +668,7 @@ export default defineComponent({
 				// 监听midi播放结束
 				api_cloudplayed(midiPlayEnd);
 			}
+			console.timeEnd('加载过程')
 		});
 		onUnmounted(() => {
 			api_remove_cloudplayed(midiPlayEnd);

+ 8 - 3
src/view/evaluating/index.tsx

@@ -118,6 +118,7 @@ export const evaluatingData = reactive({
   tipErjiShow: false, // 评测提示弹窗
   onceErjiPopShow: false, // 是否已经提示过耳机弹窗,重新进入评测页面,重置该状态为false,手动关掉耳机弹窗,改变该状态为true,本次评测都不在提示耳机状态弹窗
   needCheckErjiStatus: true, // 点击评测模式进入评测模块的需要检测耳机状态,通过返回按钮进入评测模块的,不检测耳机状态
+  evaluatResultLoading: false, // 评测结果处理中
 });
 
 const sendOffsetTime = async (offsetTime: number) => {
@@ -346,6 +347,8 @@ const handleScoreResult = (res?: IPostMessage) => {
           evaluatingData.hideResultModal = true;
         }
         evaluatingData.resulstMode = evaluatingData.isErrorState ? false : true;
+        evaluatingData.startBegin = false;
+        evaluatingData.evaluatResultLoading = false;
       }, 200);
       evaluatingData.resultData = {
         ...body,
@@ -508,11 +511,12 @@ const recordStartTimePoint = async (res?: IPostMessage) => {
  * @returns
  */
 export const handleEndEvaluat = (isComplete = false, endType?: string) => {
-  // 没有开始评测 , 不是评测模式 , 不评分
-  if (!evaluatingData.startBegin || state.modeType !== "evaluating") return;
+  // 没有开始评测 , 不是评测模式 , 不评分;evaluatResultLoading:评测结果处理中,避免重复结束
+  if (!evaluatingData.startBegin || state.modeType !== "evaluating" || evaluatingData.evaluatResultLoading) return;
   // 结束录音
   // api_stopRecording();
   // 结束评测
+  evaluatingData.evaluatResultLoading = true
   console.log("评测结束1");
   endEvaluating({
     musicScoreId: state.examSongId,
@@ -535,7 +539,7 @@ export const handleEndEvaluat = (isComplete = false, endType?: string) => {
     }
   }
   setTimeout(() => {
-    evaluatingData.startBegin = false;
+    // evaluatingData.startBegin = false;
     if (endType === 'selfCancel') {
       // 重置播放倍率
       const item: any = (state.sectionStatus && state.section.length === 2) ? state.sectionFirst || state.section[0] : state.times[0];
@@ -640,6 +644,7 @@ export const handleViewReport = (key: "recordId" | "recordIdStr", type: "gym" |
     statusBarTextColor: false,
     isOpenLight: true,
     c_orientation: 0,
+    showLoadingAnim: true
   });
 };
 

+ 23 - 4
src/view/music-score/index.tsx

@@ -35,6 +35,9 @@ export const resetRenderMusicScore = (type?: string) => {
 // 下载后的xml
 export const downloadXmlStr = ref("")
 
+// xml的document对象
+export const xmlDocRef = ref<Document | null>(null);
+
 export default defineComponent({
 	name: "music-score",
 	emits: ["rendered"],
@@ -69,18 +72,34 @@ export default defineComponent({
 				state.musicRenderType = musicRenderType;
 			}
 		};
-		const getXML = async () => {
+		const getXML = async (cbType?: string) => {
 			// 当有下载的xml的时候直接使用,否则需要下载
 			if(!downloadXmlStr.value){
 				downloadXmlStr.value = await fetch(state.xmlUrl).then((response) => response.text())
 			}
+			console.time('增删改查xml')
+			// if (state.xmlFromStore) {
+			// 	musicData.score = state.isCombineRender ? downloadXmlStr.value : onlyVisible(downloadXmlStr.value, state.partIndex);
+			// 	if (state.gradualTimes) {
+			// 		state.gradual = getGradualLengthByXml(downloadXmlStr.value);
+			// 	}
+			// } else {
+			// 	const xmlStr = downloadXmlStr.value;
+			// 	const parseXmlInfo = getCustomInfo(xmlStr, cbType);
+			// 	const xml = formatXML(parseXmlInfo.parsedXML, '', cbType);
+			// 	musicData.score = state.isCombineRender ? xml : onlyVisible(xml, state.partIndex);
+			// 	if (state.gradualTimes) {
+			// 		state.gradual = getGradualLengthByXml(xml);
+			// 	}
+			// }
 			const xmlStr = downloadXmlStr.value;
-			const parseXmlInfo = getCustomInfo(xmlStr);
-			const xml = formatXML(parseXmlInfo.parsedXML);
+			const parseXmlInfo = getCustomInfo(xmlStr, cbType);
+			const xml = formatXML(parseXmlInfo.parsedXML, '', cbType);
 			musicData.score = state.isCombineRender ? xml : onlyVisible(xml, state.partIndex);
 			if (state.gradualTimes) {
 				state.gradual = getGradualLengthByXml(xml);
 			}
+			console.timeEnd('增删改查xml')
 		};
 
 		const init = async () => {
@@ -191,7 +210,7 @@ export default defineComponent({
 		let horizontalDragScroll:HorizontalDragScroll | null
 		onMounted(async () => {
 			//setRenderType();
-			await getXML();
+			await getXML('init');
 			await init();
 			// pc 端支持 拖动滚动
 			if(state.platform === "PC" || query.isCbs){

+ 12 - 1
src/view/plugins/toggleMusicSheet/index.tsx

@@ -11,6 +11,8 @@ import Dragbom from "/src/view/plugins/useDrag/dragbom";
 import { setGuidance } from "/src/page-instrument/custom-plugins/guide-page/api";
 import { storeData } from "/src/store";
 import { api_closeCamera } from "/src/helpers/communication";
+import { xmlDocRef, downloadXmlStr } from "/src/view/music-score"
+import IndexedDBService from "/src/utils/indexedDB";
 
 export const toggleMusicSheet = reactive({
   show: false,
@@ -52,7 +54,15 @@ export default defineComponent({
       }
     })
 
-    const switchMusic = (partIndexs: number[]) => {
+    // 切换的时候存储处理过后的xml
+    const storeXmlData = async () => {
+      const dbService = new IndexedDBService("MyDatabase", "MyStore");
+      await dbService.save({ id: state.examSongId, xmlString: downloadXmlStr.value })
+        .then(() => dbService.get(state.examSongId))
+        .then((data) => console.log("数据:", data));
+    }
+
+    const switchMusic = async (partIndexs: number[]) => {
       const index = partIndexs.join(",") as any
       // 暂停播放
       togglePlay("paused");
@@ -85,6 +95,7 @@ export default defineComponent({
       if (state.setting.camera) {
         api_closeCamera();
       }
+      await storeXmlData()
       location.href = _url
     }
 

+ 4 - 1
src/view/selection/index.tsx

@@ -277,11 +277,14 @@ export default defineComponent({
 			}
 			return []
 		})
-
+		console.time('dom挂载')
 		onMounted(() => {
+			console.timeEnd('dom挂载')
 			selectData.notes = [];
 			selectData.staves = [];
+			console.time('添加dom时间')
 			calcNoteData();
+			console.timeEnd('添加dom时间')
 			const img: HTMLElement = document.querySelector('#cursorImg-0')!
 			if (metronomeData.cursorMode === 2){
 				img.classList.add('lineHide')

+ 29 - 0
vite.config.ts

@@ -14,6 +14,16 @@ export default defineConfig({
   // assetsInclude: ['**/*.html'],
   plugins: [
     // mkcert(), // 本地https
+    // viteCompression({
+    //   algorithm: "gzip", // 指定压缩算法为gzip,[ 'gzip' , 'brotliCompress' ,'deflate' , 'deflateRaw']
+    //   ext: ".gz", // 指定压缩后的文件扩展名为.gz
+    //   threshold: 10240, // 仅对文件大小大于threshold的文件进行压缩,默认为10KB
+    //   deleteOriginFile: false, // 是否删除原始文件,默认为false
+    //   filter: /\.(js|css|json|html|ico|svg)(\?.*)?$/i, // 匹配要压缩的文件的正则表达式,默认为匹配.js、.css、.json、.html、.ico和.svg文件
+    //   compressionOptions: { level: 9 }, // 指定gzip压缩级别,默认为9(最高级别)
+    //   verbose: true, //是否在控制台输出压缩结果
+    //   disable: false, //是否禁用插件
+    // }),    
     legacy({
       targets: ["Chrome 63"],
       additionalLegacyPolyfills: ["regenerator-runtime/runtime"],
@@ -35,11 +45,30 @@ export default defineConfig({
     },
   },
   build: {
+    minify: 'terser', // 启用 terser 压缩  
+    terserOptions: {  
+        compress: {  
+            // pure_funcs: ['console.log'], // 只删除 console.log 
+            //drop_console: true, // 删除所有 console
+            drop_debugger: true, // 删除 debugger  
+        }  
+    },    
     rollupOptions: {
       input: {
         instrument: resolve(__dirname, "instrument.html"),
       },
       output: {
+        // 最小化拆分包
+        manualChunks(id) {
+          if (id.includes("osmd-extended")) {
+            // 通过拆分包的方式将所有来自osmd-extended的模块打包到单独的chunk中
+            return id
+              .toString()
+              .split("osmd-extended/")[1]
+              .split("/")[0]
+              .toString();
+          }
+        },        
         chunkFileNames: "js/[name]-[hash].js", // 引入文件名的名称
         entryFileNames: "js/[name]-[hash].js", // 包的入口文件名称
         assetFileNames: "[ext]/[name]-[hash].[ext]", // 资源文件像 字体,图片等