TIANYONG 8 mesiacov pred
rodič
commit
9be7fd17cc
93 zmenil súbory, kde vykonal 3542 pridanie a 1197 odobranie
  1. 10 10
      dist/colexiu.html
  2. 9 9
      dist/index.html
  3. 7 7
      dist/instrument.html
  4. 11 11
      dist/orchestra.html
  5. 7 7
      dist/report-share.html
  6. 1 1
      osmd-extended
  7. 4 3
      src/helpers/calcSpeed.ts
  8. 302 15
      src/helpers/customMusicScore.ts
  9. 199 35
      src/helpers/formateMusic.ts
  10. 13 7
      src/helpers/metronome.ts
  11. 26 13
      src/page-instrument/App.tsx
  12. 9 0
      src/page-instrument/api.ts
  13. 9 1
      src/page-instrument/component/authorName/index.module.less
  14. 2 2
      src/page-instrument/component/authorName/index.tsx
  15. 282 0
      src/page-instrument/component/the-music-list/filterList.tsx
  16. BIN
      src/page-instrument/component/the-music-list/imgs/headImg.png
  17. BIN
      src/page-instrument/component/the-music-list/imgs/queding.png
  18. BIN
      src/page-instrument/component/the-music-list/imgs/quxiao.png
  19. BIN
      src/page-instrument/component/the-music-list/imgs/shouqi.png
  20. BIN
      src/page-instrument/component/the-music-list/imgs/sj.png
  21. BIN
      src/page-instrument/component/the-music-list/imgs/xiang.png
  22. BIN
      src/page-instrument/component/the-music-list/imgs/zhankai.png
  23. 300 63
      src/page-instrument/component/the-music-list/index.module.less
  24. 2 2
      src/page-instrument/component/the-music-list/index.tsx
  25. 44 27
      src/page-instrument/component/the-music-list/list.tsx
  26. BIN
      src/page-instrument/custom-plugins/guide-driver/images/practise/d11.png
  27. BIN
      src/page-instrument/custom-plugins/guide-driver/images/practise/d7.png
  28. BIN
      src/page-instrument/custom-plugins/guide-driver/images/report/r2.png
  29. BIN
      src/page-instrument/custom-plugins/guide-driver/images/report/r3.png
  30. 75 12
      src/page-instrument/custom-plugins/guide-driver/index.less
  31. 223 118
      src/page-instrument/custom-plugins/guide-driver/index.tsx
  32. 4 2
      src/page-instrument/custom-plugins/helper-model/recommendation/index.module.less
  33. 1 1
      src/page-instrument/custom-plugins/helper-model/recommendation/index.tsx
  34. 2 1
      src/page-instrument/custom-plugins/work-index/index.tsx
  35. 21 14
      src/page-instrument/evaluat-model/evaluat-result/index.tsx
  36. 11 8
      src/page-instrument/evaluat-model/index.tsx
  37. 4 3
      src/page-instrument/follow-model/index.tsx
  38. 156 84
      src/page-instrument/header-top/index.module.less
  39. 157 89
      src/page-instrument/header-top/index.tsx
  40. 6 2
      src/page-instrument/header-top/modeView.tsx
  41. 3 2
      src/page-instrument/header-top/settting/index.module.less
  42. 27 23
      src/page-instrument/header-top/settting/index.tsx
  43. 2 2
      src/page-instrument/header-top/speed/index.module.less
  44. 13 10
      src/page-instrument/header-top/speed/index.tsx
  45. 1 0
      src/page-instrument/simple-detail/index.tsx
  46. BIN
      src/page-instrument/view-detail/images/bg2_left_zs.png
  47. 159 11
      src/page-instrument/view-detail/index.module.less
  48. 109 78
      src/page-instrument/view-detail/index.tsx
  49. 1 1
      src/page-instrument/view-detail/loading.tsx
  50. 21 0
      src/page-instrument/view-detail/loadingCss.tsx
  51. 7 2
      src/page-instrument/view-detail/smoothAnimation/index.less
  52. 79 10
      src/page-instrument/view-detail/smoothAnimation/index.ts
  53. 5 3
      src/page-instrument/view-evaluat-report/component/share-top/index.module.less
  54. 18 10
      src/page-instrument/view-evaluat-report/component/share-top/index.tsx
  55. 1 1
      src/page-instrument/view-evaluat-report/index.tsx
  56. 24 5
      src/page-instrument/view-figner/change-subject/index.tsx
  57. 4 0
      src/page-instrument/view-figner/index.module.less
  58. 32 26
      src/page-instrument/view-figner/index.tsx
  59. 375 128
      src/state.ts
  60. 4 0
      src/style.css
  61. 28 1
      src/view/abnormal-pop/index.module.less
  62. 2 1
      src/view/abnormal-pop/index.tsx
  63. 45 34
      src/view/audio-list/index.tsx
  64. 3 2
      src/view/evaluating/index.tsx
  65. 8 1
      src/view/fingering/fingering-config.ts
  66. 1 1
      src/view/fingering/index.tsx
  67. 20 10
      src/view/follow-practice/index.tsx
  68. 71 0
      src/view/music-score/HorizontalDragScroll.ts
  69. 19 0
      src/view/music-score/index.module.less
  70. 46 28
      src/view/music-score/index.tsx
  71. BIN
      src/view/plugins/move-music-score/image/edit.png
  72. BIN
      src/view/plugins/move-music-score/image/edit_add.png
  73. BIN
      src/view/plugins/move-music-score/image/edit_close.png
  74. BIN
      src/view/plugins/move-music-score/image/edit_delete.png
  75. BIN
      src/view/plugins/move-music-score/image/edit_next.png
  76. BIN
      src/view/plugins/move-music-score/image/edit_pre.png
  77. BIN
      src/view/plugins/move-music-score/image/edit_reduce.png
  78. BIN
      src/view/plugins/move-music-score/image/edit_reset.png
  79. BIN
      src/view/plugins/move-music-score/image/edit_save.png
  80. 85 0
      src/view/plugins/move-music-score/index.module.less
  81. 115 29
      src/view/plugins/move-music-score/index.tsx
  82. 2 3
      src/view/plugins/toggleMusicSheet/choosePartName/index.module.less
  83. 36 42
      src/view/plugins/toggleMusicSheet/choosePartName/index.tsx
  84. 6 2
      src/view/plugins/toggleMusicSheet/index.tsx
  85. 23 12
      src/view/plugins/useDrag/index.ts
  86. BIN
      src/view/selection/imgs/pitchHigh.png
  87. BIN
      src/view/selection/imgs/pitchLow.png
  88. 49 30
      src/view/selection/index.module.less
  89. 171 137
      src/view/selection/index.tsx
  90. 22 0
      src/view/selection/multipleRestMeasures.tsx
  91. 5 2
      src/view/tick/index.tsx
  92. 0 0
      stats.html
  93. 3 3
      vite.config.ts

+ 10 - 10
dist/colexiu.html

@@ -2,7 +2,7 @@
 <html lang="en">
 
 <head>
-  <script type="module" crossorigin src="./js/polyfills-4f399b55.js"></script>
+  <script type="module" crossorigin src="./js/polyfills-7c3d5f60.js"></script>
 
   <meta charset="UTF-8" />
   <link rel="icon" type="image/svg+xml" href="./vite.svg" />
@@ -40,13 +40,13 @@
       },
     })
   </script>
-  <script type="module" crossorigin src="./js/colexiu-7df1eb79.js"></script>
-  <link rel="modulepreload" crossorigin href="./js/index-067071d7.js">
-  <link rel="modulepreload" crossorigin href="./js/index-38945c54.js">
-  <link rel="modulepreload" crossorigin href="./js/index-8af08a5d.js">
-  <link rel="modulepreload" crossorigin href="./js/index-316df9ad.js">
-  <link rel="stylesheet" href="./css/index-11fe3793.css">
-  <link rel="stylesheet" href="./css/colexiu-62f31c4f.css">
+  <script type="module" crossorigin src="./js/colexiu-cd0a6ab2.js"></script>
+  <link rel="modulepreload" crossorigin href="./js/index-3c91e594.js">
+  <link rel="modulepreload" crossorigin href="./js/index-f1dfcaac.js">
+  <link rel="modulepreload" crossorigin href="./js/index-6bda73c5.js">
+  <link rel="modulepreload" crossorigin href="./js/index-f16e378b.js">
+  <link rel="stylesheet" href="./css/index-b6f8867e.css">
+  <link rel="stylesheet" href="./css/colexiu-e8b419b5.css">
   <script type="module">import.meta.url;import("_").catch(()=>1);async function* g(){};window.__vite_is_modern_browser=true;</script>
   <script type="module">!function(){if(window.__vite_is_modern_browser)return;console.warn("vite: loading legacy chunks, syntax error above and the same error below should be ignored");var e=document.getElementById("vite-legacy-polyfill"),n=document.createElement("script");n.src=e.src,n.onload=function(){System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'))},document.body.appendChild(n)}();</script>
 </head>
@@ -56,8 +56,8 @@
   <img id="loading" class="show" src="./loading.svg" alt="loading" />
   
   <script nomodule>!function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",(function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()}),!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();</script>
-  <script nomodule crossorigin id="vite-legacy-polyfill" src="./js/polyfills-legacy-309efa15.js"></script>
-  <script nomodule crossorigin id="vite-legacy-entry" data-src="./js/colexiu-legacy-6f74bd74.js">System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'))</script>
+  <script nomodule crossorigin id="vite-legacy-polyfill" src="./js/polyfills-legacy-91efcf98.js"></script>
+  <script nomodule crossorigin id="vite-legacy-entry" data-src="./js/colexiu-legacy-10e926f8.js">System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'))</script>
 </body>
 
 </html>

+ 9 - 9
dist/index.html

@@ -2,7 +2,7 @@
 <html lang="ZH-cn">
 
 <head>
-  <script type="module" crossorigin src="./js/polyfills-4f399b55.js"></script>
+  <script type="module" crossorigin src="./js/polyfills-7c3d5f60.js"></script>
 
   <meta charset="UTF-8">
   <link rel="icon" href="./favicon.ico" />
@@ -75,13 +75,13 @@
       }
     })
   </script>
-  <script type="module" crossorigin src="./js/gym-a67c0780.js"></script>
-  <link rel="modulepreload" crossorigin href="./js/index-067071d7.js">
-  <link rel="modulepreload" crossorigin href="./js/index-38945c54.js">
-  <link rel="modulepreload" crossorigin href="./js/index-a2a34d0e.js">
-  <link rel="modulepreload" crossorigin href="./js/index-316df9ad.js">
+  <script type="module" crossorigin src="./js/gym-dcfde457.js"></script>
+  <link rel="modulepreload" crossorigin href="./js/index-3c91e594.js">
+  <link rel="modulepreload" crossorigin href="./js/index-f1dfcaac.js">
+  <link rel="modulepreload" crossorigin href="./js/index-e4fcc630.js">
+  <link rel="modulepreload" crossorigin href="./js/index-f16e378b.js">
   <link rel="modulepreload" crossorigin href="./js/plyr.min-c8c2777b.js">
-  <link rel="stylesheet" href="./css/index-11fe3793.css">
+  <link rel="stylesheet" href="./css/index-b6f8867e.css">
   <link rel="stylesheet" href="./css/index-85f95688.css">
   <link rel="stylesheet" href="./css/plyr-ad8ef5ae.css">
   <link rel="stylesheet" href="./css/index-171cd132.css">
@@ -98,8 +98,8 @@
   <img id="loading" class="show" src="./loading.svg" alt="loading" />
   
   <script nomodule>!function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",(function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()}),!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();</script>
-  <script nomodule crossorigin id="vite-legacy-polyfill" src="./js/polyfills-legacy-309efa15.js"></script>
-  <script nomodule crossorigin id="vite-legacy-entry" data-src="./js/gym-legacy-7bcdda99.js">System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'))</script>
+  <script nomodule crossorigin id="vite-legacy-polyfill" src="./js/polyfills-legacy-91efcf98.js"></script>
+  <script nomodule crossorigin id="vite-legacy-entry" data-src="./js/gym-legacy-d64c7117.js">System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'))</script>
 </body>
 
 </html>

+ 7 - 7
dist/instrument.html

@@ -2,7 +2,7 @@
 <html lang="en">
 
 <head>
-  <script type="module" crossorigin src="./js/polyfills-4f399b55.js"></script>
+  <script type="module" crossorigin src="./js/polyfills-7c3d5f60.js"></script>
 
   <meta charset="UTF-8" />
   <meta name="viewport"
@@ -41,10 +41,10 @@
       })
     }
   </script>
-  <script type="module" crossorigin src="./js/instrument-687fc6b5.js"></script>
-  <link rel="modulepreload" crossorigin href="./js/index-067071d7.js">
-  <link rel="stylesheet" href="./css/index-11fe3793.css">
-  <link rel="stylesheet" href="./css/instrument-9ce1010f.css">
+  <script type="module" crossorigin src="./js/instrument-174600a1.js"></script>
+  <link rel="modulepreload" crossorigin href="./js/index-3c91e594.js">
+  <link rel="stylesheet" href="./css/index-b6f8867e.css">
+  <link rel="stylesheet" href="./css/instrument-118f68d3.css">
   <script type="module">import.meta.url;import("_").catch(()=>1);async function* g(){};window.__vite_is_modern_browser=true;</script>
   <script type="module">!function(){if(window.__vite_is_modern_browser)return;console.warn("vite: loading legacy chunks, syntax error above and the same error below should be ignored");var e=document.getElementById("vite-legacy-polyfill"),n=document.createElement("script");n.src=e.src,n.onload=function(){System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'))},document.body.appendChild(n)}();</script>
 </head>
@@ -67,8 +67,8 @@
     var vConsole = new window.VConsole();
   </script>   -->
   <script nomodule>!function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",(function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()}),!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();</script>
-  <script nomodule crossorigin id="vite-legacy-polyfill" src="./js/polyfills-legacy-309efa15.js"></script>
-  <script nomodule crossorigin id="vite-legacy-entry" data-src="./js/instrument-legacy-a1cbab87.js">System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'))</script>
+  <script nomodule crossorigin id="vite-legacy-polyfill" src="./js/polyfills-legacy-91efcf98.js"></script>
+  <script nomodule crossorigin id="vite-legacy-entry" data-src="./js/instrument-legacy-edbd6da2.js">System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'))</script>
 </body>
 
 </html>

+ 11 - 11
dist/orchestra.html

@@ -2,7 +2,7 @@
 <html lang="en">
 
 <head>
-  <script type="module" crossorigin src="./js/polyfills-4f399b55.js"></script>
+  <script type="module" crossorigin src="./js/polyfills-7c3d5f60.js"></script>
 
   <meta charset="UTF-8" />
   <!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
@@ -41,15 +41,15 @@
       transition: opacity .3s;
     }
   </style>
-  <script type="module" crossorigin src="./js/orchestra-66a6ea06.js"></script>
-  <link rel="modulepreload" crossorigin href="./js/index-067071d7.js">
-  <link rel="modulepreload" crossorigin href="./js/index-38945c54.js">
-  <link rel="modulepreload" crossorigin href="./js/index-8af08a5d.js">
-  <link rel="modulepreload" crossorigin href="./js/index-a2a34d0e.js">
-  <link rel="modulepreload" crossorigin href="./js/index-316df9ad.js">
-  <link rel="stylesheet" href="./css/index-11fe3793.css">
+  <script type="module" crossorigin src="./js/orchestra-593b6ec1.js"></script>
+  <link rel="modulepreload" crossorigin href="./js/index-3c91e594.js">
+  <link rel="modulepreload" crossorigin href="./js/index-f1dfcaac.js">
+  <link rel="modulepreload" crossorigin href="./js/index-6bda73c5.js">
+  <link rel="modulepreload" crossorigin href="./js/index-e4fcc630.js">
+  <link rel="modulepreload" crossorigin href="./js/index-f16e378b.js">
+  <link rel="stylesheet" href="./css/index-b6f8867e.css">
   <link rel="stylesheet" href="./css/index-85f95688.css">
-  <link rel="stylesheet" href="./css/orchestra-8bc1a9c0.css">
+  <link rel="stylesheet" href="./css/orchestra-8e05e751.css">
   <script type="module">import.meta.url;import("_").catch(()=>1);async function* g(){};window.__vite_is_modern_browser=true;</script>
   <script type="module">!function(){if(window.__vite_is_modern_browser)return;console.warn("vite: loading legacy chunks, syntax error above and the same error below should be ignored");var e=document.getElementById("vite-legacy-polyfill"),n=document.createElement("script");n.src=e.src,n.onload=function(){System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'))},document.body.appendChild(n)}();</script>
 </head>
@@ -70,8 +70,8 @@
   </script>
   
   <script nomodule>!function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",(function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()}),!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();</script>
-  <script nomodule crossorigin id="vite-legacy-polyfill" src="./js/polyfills-legacy-309efa15.js"></script>
-  <script nomodule crossorigin id="vite-legacy-entry" data-src="./js/orchestra-legacy-a355e148.js">System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'))</script>
+  <script nomodule crossorigin id="vite-legacy-polyfill" src="./js/polyfills-legacy-91efcf98.js"></script>
+  <script nomodule crossorigin id="vite-legacy-entry" data-src="./js/orchestra-legacy-4d53b426.js">System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'))</script>
 </body>
 
 </html>

+ 7 - 7
dist/report-share.html

@@ -2,7 +2,7 @@
 <html lang="en">
 
 <head>
-  <script type="module" crossorigin src="./js/polyfills-4f399b55.js"></script>
+  <script type="module" crossorigin src="./js/polyfills-7c3d5f60.js"></script>
 
   <meta charset="UTF-8" />
   <!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
@@ -25,11 +25,11 @@
       transition: opacity .3s;
     }
   </style>
-  <script type="module" crossorigin src="./js/report-share-4fbb07df.js"></script>
-  <link rel="modulepreload" crossorigin href="./js/index-067071d7.js">
+  <script type="module" crossorigin src="./js/report-share-f1dcb99b.js"></script>
+  <link rel="modulepreload" crossorigin href="./js/index-3c91e594.js">
   <link rel="modulepreload" crossorigin href="./js/plyr.min-c8c2777b.js">
-  <link rel="modulepreload" crossorigin href="./js/index-316df9ad.js">
-  <link rel="stylesheet" href="./css/index-11fe3793.css">
+  <link rel="modulepreload" crossorigin href="./js/index-f16e378b.js">
+  <link rel="stylesheet" href="./css/index-b6f8867e.css">
   <link rel="stylesheet" href="./css/plyr-ad8ef5ae.css">
   <link rel="stylesheet" href="./css/report-share-0f4c3151.css">
   <script type="module">import.meta.url;import("_").catch(()=>1);async function* g(){};window.__vite_is_modern_browser=true;</script>
@@ -52,8 +52,8 @@
   </script>
   
   <script nomodule>!function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",(function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()}),!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();</script>
-  <script nomodule crossorigin id="vite-legacy-polyfill" src="./js/polyfills-legacy-309efa15.js"></script>
-  <script nomodule crossorigin id="vite-legacy-entry" data-src="./js/report-share-legacy-ff5fcfcf.js">System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'))</script>
+  <script nomodule crossorigin id="vite-legacy-polyfill" src="./js/polyfills-legacy-91efcf98.js"></script>
+  <script nomodule crossorigin id="vite-legacy-entry" data-src="./js/report-share-legacy-5fded00d.js">System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'))</script>
 </body>
 
 </html>

+ 1 - 1
osmd-extended

@@ -1 +1 @@
-Subproject commit bab1ca04065946e20f10fa1aacdb0c25b533a6f2
+Subproject commit b83a5c3a2b6716788a108aa9d3ab562af45e7a90

+ 4 - 3
src/helpers/calcSpeed.ts

@@ -1,4 +1,5 @@
 import { OpenSheetMusicDisplay, SourceMeasure } from "/osmd-extended/src";
+import { onlyVisible } from "/src/helpers/formateMusic";
 
 export const noteDuration = {
 	"1/2": 2,
@@ -113,8 +114,8 @@ export type GradualItem = {
  * @param xml 始终按照第一分谱进行减慢速度的计算
  */
 export const getGradualLengthByXml = (xml: string) => {
-	// const firstPartXml = onlyVisible(xml, 0)
-	const xmlParse = new DOMParser().parseFromString(xml, "text/xml");
+	const firstPartXml = onlyVisible(xml, 0)
+	const xmlParse = new DOMParser().parseFromString(firstPartXml, "text/xml");
 	const measures = Array.from(xmlParse.querySelectorAll("measure"));
 	const notes = Array.from(xmlParse.querySelectorAll("note"));
 	const words = Array.from(xmlParse.querySelectorAll("words"));
@@ -164,7 +165,7 @@ export const getGradualLengthByXml = (xml: string) => {
 		textContent: "",
 		type: "metronome",
 		allDuration: 1,
-		leftDuration: 1,
+		leftDuration: 0,
 		measureIndex: measures.length,
 	});
 

+ 302 - 15
src/helpers/customMusicScore.ts

@@ -268,6 +268,18 @@ export const limitSingleSvgPageHeight = () => {
 
 }
 
+const isElementInViewport = (el: any) => {
+    const rect = el.getBoundingClientRect();
+    return (
+        rect.top >= 0 &&
+        rect.left >= 0 &&
+        rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
+        rect.right <= (window.innerWidth || document.documentElement.clientWidth)
+    );
+}
+const isNumeric = (str: any) => {
+	return /^\d+$/.test(str);
+  }
 // 谱面优化
 export const resetFormate = () => {
 	container.value = document.getElementById('scrollContainer')
@@ -296,14 +308,23 @@ export const resetFormate = () => {
 		const paths: SVGAElement[] = Array.from(staffline.querySelectorAll(".vf-measure > .vf-stave path"));
 		const dotModifiers: SVGAElement[] = Array.from(staffline.querySelectorAll(".vf-measure .vf-stopDot"));
 		const staves: SVGAElement[] = Array.from(staffline.querySelectorAll(".vf-measure > .vf-stave"));
+		const numberVfTexts = Array.from((container.value as HTMLElement).querySelectorAll(".vf-text > text")); 
 
 		// 获取第一个线谱的y轴坐标
 		const firstLinePathY = paths[0]?.getBBox().y || 0
+		// D.C循环标记没有显示完全修复
 		// 反复标记 和 小节碰撞
-		const repetWord = ["To Coda", "D.S. al Coda", "Coda"];
+		const repetWord = ["To Coda", "D.S. al Coda", "Coda", "D.C."];
 		texts
 			.filter((n) => repetWord.includes(n.textContent || ""))
 			.forEach((t) => {
+				// console.log('文本123',t.textContent,'是否在可视区域内',isElementInViewport(t))
+				// D.C循环标记不在可视区域内,需要修复移动其位置信息
+				// if (t.textContent?.includes('D.C')) {
+				// 	if (!isElementInViewport(t)) {
+				// 		t.style.transform = `translateX(-40px)`;
+				// 	}
+				// }
 				vfbeams.forEach((curve) => {
 					const result = collisionDetection(t, curve);
 					const prePath: SVGAElement = t?.previousSibling as unknown as SVGAElement;
@@ -430,13 +451,14 @@ export const resetFormate = () => {
 				label.textContent = labelText.replace('#','♯')
 			}
 		});
-		dotModifiers.forEach((group: any) => {
-			if (state.musicRenderType === 'fixedTone') {
-				group.setAttribute('transform', 'translate(3,-12)')
-			} else {
-				group.setAttribute('transform', 'translate(3,-7)')
-			}
-		});
+		// numberVfTexts.forEach((label: any) => {
+		// 	const labelText = label.textContent as string
+		// 	if (isNumeric(labelText)) {
+		// 		const _y = Number(label.getAttribute("y"))
+		// 		const endY = firstLinePathY ? firstLinePathY - musicalDistance : _y
+		// 		label.setAttribute("y", endY)
+		// 	}
+		// })
 		const vftextBottom = Array.from(staffline.querySelectorAll(".vf-text > text")).filter((n: any) => n.getBBox().y > stafflineCenter);
 		const vflineBottom = Array.from(staffline.querySelectorAll(".vf-line")).filter((n: any) => n.getBBox().y > stafflineCenter);
 		// 去重
@@ -573,8 +595,8 @@ export const resetFormate = () => {
 		if (!state.isCreateImg && !state.isPreView) {
 			staves.forEach((stave: any,i: number) => {
 				const list = [
-					Array.from(stave?.getElementsByTagName("text") || []),
 					Array.from(stave?.querySelectorAll(".vf-StaveSection") || []),
+					Array.from(stave?.getElementsByTagName("text") || []),
 					Array.from(stave?.querySelectorAll(".vf-Volta") || []),
 					Array.from(stave?.querySelectorAll(".vf-clef") || []),
 					Array.from(stave?.querySelectorAll(".vf-keysignature") || []),
@@ -591,8 +613,8 @@ export const resetFormate = () => {
 					}
 				} catch (error) {}
 				const bbox = stave?.getBBox() || {};
-				const bgColor = state.isEvaluatReport ? '#132D4C' : '#609FCF';
-				const botColor = state.isEvaluatReport ? '#040D1E' : '#2B70A5';
+				const bgColor = state.isEvaluatReport ? '#132D4C' : state.isCbsView ? 'transparent' : '#609FCF';
+				const botColor = state.isEvaluatReport ? '#040D1E' : state.isCbsView ? 'transparent' : '#2B70A5';
 				const rect = `<rect class="vf-custom-bg" x="${bbox.x}" y="${bbox.y}" width="${bbox.width}" height="${bbox.height}" fill=${bgColor} />`
 				const rectBottom = `<rect class="vf-custom-bot" x="${bbox.x}" y="${bbox.y+bbox.height}" width="${bbox.width}" height="7.5" fill=${botColor} />`
 				// const filterDom = `<defs>
@@ -603,10 +625,13 @@ export const resetFormate = () => {
 				const customG = `<g>${rect}${rectBottom}</g>`
 				try {
 					if (list.length) {
-						list.forEach((_el: any) => {
+						for(const _el of list) {
+							if (_el?.parentElement?.classList?.contains('vf-StaveSection')) {
+								continue;
+							}
 							stave?.appendChild(_el)
 							_el?.style?.removeProperty("display");
-						});
+						}
 					}
 				} catch (error) {}
 				stave.innerHTML = customG + stave.innerHTML;
@@ -614,10 +639,47 @@ export const resetFormate = () => {
 			state.vfmeasures = state.vfmeasures.concat(vfmeasures);
 		}
 
+		dotModifiers.forEach((group: any) => {
+			let parent = group?.parentElement; // 获取父元素
+			// 如果需要找更外层的祖先元素,可以一直迭代
+			while (parent && !parent.classList?.contains('vf-measure') && parent.tagName !== 'body' && parent) { // 假设你想找到最外层的 DIV
+				parent = parent.parentElement;
+			}
+			const parentY = parent?.querySelector('.vf-custom-bg')?.getBoundingClientRect()?.y || 0;
+			const dotY = group?.getBoundingClientRect()?.y || 0;
+			const distanceY = parentY - dotY;
+			const translateY = 15 - distanceY;
+			// console.log('距离111',translateY)
+			group.setAttribute('transform', `translate(3,${-translateY})`)
+			// if (state.musicRenderType === 'fixedTone') {
+			// 	// group.setAttribute('transform', 'translate(3,-12)')
+			// } else {
+			// 	// group.setAttribute('transform', 'translate(3,-7)')
+			// }
+		});
+
+		// 修复D.C、D.S等渲染位置不对的问题
+		const repairWord = ["D.S.", "D.C.", "Fine"];
+		[...vfmeasures].forEach((measure: any) => {
+			const needRepairTexts = measure.querySelectorAll('text').length ? Array.from(measure.querySelectorAll('text'))?.filter((item: any) => repairWord.includes(item?.textContent)) : [];
+			if (needRepairTexts.length) {
+				// 该小节结束位置的x坐标
+				const measureCoordinate = measure?.querySelector('.vf-custom-bg')?.getBBox() || null
+				const measureEndX = measureCoordinate ? measureCoordinate?.x + measureCoordinate?.width - 30 : 0;
+				needRepairTexts.forEach((text: any) => {
+					text?.setAttribute('x', measureEndX)
+				})
+			}
+		});
+
 	}
 	if (!state.isCombineRender && state.isSingleLine) {
 		transSinglePage();
 	}
+	// 多行谱,拍号可能被遮挡,需要移动谱面的位置
+	if (!state.isSingleLine) {
+		transMultiPosition();
+	}
 	// setTimeout(() => this.resetGlobalText());
 };
 
@@ -630,7 +692,10 @@ const transSinglePage = () => {
 			// 需要上移的距离
 			// console.log('need',svgPage.height,staffLine.height)
 			const rate = svgPage.height > 400 ? 1.2 : 2;
-			const needTransTop = (svgPage.height - staffLine.height) / rate;
+			let needTransTop = (svgPage.height - staffLine.height) / rate;
+			// 给个安全距离
+			const maxTop = staffLine.top - svgPage.top - 40
+			needTransTop = Math.min(maxTop, needTransTop)
 			// @ts-ignore
 			document.getElementById('osmdSvgPage1').style.transform = `translateY(-${needTransTop}px)`;
 			// document.querySelector('.staffline').style.transform = `translateY(-${needTransTop}px)`;
@@ -641,6 +706,25 @@ const transSinglePage = () => {
 			// document.getElementById('osmdSvgPage1').style.transform = `translateY(${needTransDistance}px)`
 		}
 	}
+	if (state.isSimplePage) {
+		const svgPage = document?.getElementById('osmdSvgPage1')?.getBoundingClientRect();
+		const staffLine = document?.querySelector('.staffline')?.getBoundingClientRect();
+		if (svgPage && staffLine) {
+			const needY = svgPage.height - (staffLine.y+staffLine.height) - 10;
+			// @ts-ignore
+			document.getElementById('osmdSvgPage1').style.transform = `translateY(${needY}px)`;
+		}
+	}
+}
+
+const transMultiPosition = () => {
+	const svgPage = document?.getElementById('osmdSvgPage1')?.getBoundingClientRect();
+	const staffLine = document?.querySelector('.staffline')?.getBoundingClientRect();
+	if (svgPage && staffLine && staffLine.y < svgPage.y) {
+		const needY = svgPage.y - staffLine.y + 5;
+		// @ts-ignore
+		document.querySelector('.staffline').style.transform = `translateY(${needY}px)`;
+	}
 }
 
 // 技巧文本
@@ -1009,4 +1093,207 @@ export const setCustomNoteRealValue = () => {
     if (["12667", "12673"].includes(detailId)){
         customData.customNoteCurrentTime = true
     }
-};
+};
+
+/** 转换简谱的全休止符和二分休止符 */
+export const transferJianNote = (measure: any, divisions: number, preBeats: number, preBeatType: number) => {
+	const multipleXs = preBeatType / 4;
+	const notes = measure.getElementsByTagName("note")
+	for (const note of notes) {
+		// 是否需要考虑带上附点
+		let needAddDot = true;
+		const noteType = note.getElementsByTagName("type")?.[0]?.textContent || '';
+		if ((noteType === 'whole' || noteType === 'half') && note.getElementsByTagName("rest").length) {
+			// 4/4拍,3/4拍
+			if (preBeatType === 4) {
+				let maxNumber = noteType === 'half' ? 2 : preBeats / multipleXs;
+				if (noteType === 'whole') {
+					// 有可能是全休止符,但是该小节又不是整小节都休止,此时这个全休止符不能按照整小节休止来计算
+					const noteDivisions = parseInt(note.getElementsByTagName("duration")[0]?.textContent);
+					if (noteDivisions/divisions !== preBeats) {
+						maxNumber = 4;
+					} else {
+						// 满足了时值,则不需要考虑加上附点
+						needAddDot = false;
+					}
+				}
+				// 如果音符带附点,需要判断处理下
+				if (note.getElementsByTagName("dot").length && needAddDot) {
+					maxNumber = noteType === 'whole' ? maxNumber + 2 : maxNumber + 1;
+				}
+				if (!Number.isInteger(maxNumber)) {
+					return;
+				}
+				// console.log('几个1/4音符',maxNumber)
+				let quarterNoteNumber = 1;
+				while (quarterNoteNumber <= maxNumber) {
+					const newnote = document.createElement('note');
+					newnote.innerHTML = `
+					<rest></rest>
+					<duration>${divisions}</duration>
+					<voice>1</voice>
+					<type>quarter</type>`
+					measure.insertBefore(newnote, note);
+					quarterNoteNumber += 1;
+				};
+				measure.removeChild(note);
+			} else if (preBeats === 3 && preBeatType === 8) {
+				const maxNumber = noteType === 'half' ? 2 : 3;
+				let quarterNoteNumber = 1;
+				while (quarterNoteNumber <= maxNumber) {
+					const newnote = document.createElement('note');
+					newnote.innerHTML = `
+					<rest></rest>
+					<duration>${divisions/2}</duration>
+					<voice>1</voice>
+					<type>eighth</type>`
+					measure.insertBefore(newnote, note);
+					quarterNoteNumber += 1;
+				};
+				measure.removeChild(note);
+			} else if (preBeats === 5 && preBeatType === 8) {
+				if (noteType === 'whole') {
+					const newnote = document.createElement('note');
+					newnote.innerHTML = `
+					<rest></rest>
+					<duration>${divisions+divisions/2}</duration>
+					<voice>1</voice>
+					<type>quarter</type>
+					<dot></dot>`
+					measure.insertBefore(newnote, note);
+					const newnote2 = document.createElement('note');
+					newnote2.innerHTML = `
+					<rest></rest>
+					<duration>${divisions}</duration>
+					<voice>1</voice>
+					<type>quarter</type>`
+					measure.insertBefore(newnote2, note);
+					measure.removeChild(note);
+				} else if (noteType === 'half') {
+					dealDotHalfNote(measure, divisions, note)
+				}
+			} else if (preBeats === 6 && preBeatType === 8) {
+				if (noteType === 'whole') {
+					const maxNumber = 2;
+					let quarterNoteNumber = 1;
+					while (quarterNoteNumber <= maxNumber) {
+						const newnote = document.createElement('note');
+						newnote.innerHTML = `
+						<rest></rest>
+						<duration>${divisions+divisions/2}</duration>
+						<voice>1</voice>
+						<type>quarter</type>
+						<dot></dot>`
+						measure.insertBefore(newnote, note);
+						quarterNoteNumber += 1;
+					};
+					measure.removeChild(note);
+				} else if (noteType === 'half') {
+					dealDotHalfNote(measure, divisions, note)		
+				}
+			} else if (preBeats === 7 && preBeatType === 8) {
+				if (noteType === 'whole') {
+					const newnote2 = document.createElement('note');
+					newnote2.innerHTML = `
+					<rest></rest>
+					<duration>${divisions+divisions/2}</duration>
+					<voice>1</voice>
+					<type>quarter</type>
+					<dot></dot>`
+					measure.insertBefore(newnote2, note);         					
+					const maxNumber = 2;
+					let quarterNoteNumber = 1;
+					while (quarterNoteNumber <= maxNumber) {
+						const newnote = document.createElement('note');
+						newnote.innerHTML = `
+						<rest></rest>
+						<duration>${divisions}</duration>
+						<voice>1</voice>
+						<type>quarter</type>`
+						measure.insertBefore(newnote, note);
+						quarterNoteNumber += 1;
+					};
+					measure.removeChild(note);
+				} else if (noteType === 'half') {
+					dealDotHalfNote(measure, divisions, note)
+				}
+			} else if (preBeats === 9 && preBeatType === 8) {
+				if (noteType === 'whole') {
+					const maxNumber = 3;
+					let quarterNoteNumber = 1;
+					while (quarterNoteNumber <= maxNumber) {
+						const newnote = document.createElement('note');
+						newnote.innerHTML = `
+						<rest></rest>
+						<duration>${divisions+divisions/2}</duration>
+						<voice>1</voice>
+						<type>quarter</type>
+						<dot></dot>`
+						measure.insertBefore(newnote, note);
+						quarterNoteNumber += 1;
+					};
+					measure.removeChild(note);
+				} else if (noteType === 'half') {
+					dealDotHalfNote(measure, divisions, note)
+				}
+			} else if (preBeats === 12 && preBeatType === 8) {
+				if (noteType === 'whole') {
+					const maxNumber = 4;
+					let quarterNoteNumber = 1;
+					while (quarterNoteNumber <= maxNumber) {
+						const newnote = document.createElement('note');
+						newnote.innerHTML = `
+						<rest></rest>
+						<duration>${divisions+divisions/2}</duration>
+						<voice>1</voice>
+						<type>quarter</type>
+						<dot></dot>`
+						measure.insertBefore(newnote, note);
+						quarterNoteNumber += 1;
+					};
+					measure.removeChild(note);
+				} else if (noteType === 'half') {
+					dealDotHalfNote(measure, divisions, note)
+				}
+			}
+		} 
+	}
+}
+
+/** 八几排的小节,二分休止符带附点 */
+const dealDotHalfNote = (measure: any, divisions: number, note: any) => {
+	// 如果音符带附点,需要判断处理下
+	if (note.getElementsByTagName("dot").length) {
+		const maxNumber = 2;
+		let quarterNoteNumber = 1;
+		while (quarterNoteNumber <= maxNumber) {
+			const newnote = document.createElement('note');
+			newnote.innerHTML = `
+			<rest></rest>
+			<duration>${divisions+divisions/2}</duration>
+			<voice>1</voice>
+			<type>quarter</type>
+			<dot></dot>`
+			measure.insertBefore(newnote, note);
+			quarterNoteNumber += 1;
+		};
+		measure.removeChild(note);
+	} else {
+		const newnote = document.createElement('note');
+		newnote.innerHTML = `
+		<rest></rest>
+		<duration>${divisions+divisions/2}</duration>
+		<voice>1</voice>
+		<type>quarter</type>
+		<dot></dot>`
+		measure.insertBefore(newnote, note);
+		const newnote2 = document.createElement('note');
+		newnote2.innerHTML = `
+		<rest></rest>
+		<duration>${divisions/2}</duration>
+		<voice>1</voice>
+		<type>eighth</type>`
+		measure.insertBefore(newnote2, note);
+		measure.removeChild(note);
+	}
+}

+ 199 - 35
src/helpers/formateMusic.ts

@@ -2,6 +2,7 @@ import dayjs from "dayjs";
 import duration from "dayjs/plugin/duration";
 import state, { customData } from "/src/state";
 import { browser } from "../utils/index";
+import { transferJianNote } from "/src/helpers/customMusicScore"
 import {
 	isSpecialMark,
 	isSpeedKeyword,
@@ -616,21 +617,66 @@ export const formatZoom = (num = 1) => {
 	return num * state.zoom;
 };
 
+/** 妙极客多分轨的曲子,可能没有part-name标签,需要手动加上该标签 */
+export const xmlAddPartName = (xml: string) => {
+	if (!xml) return "";
+	const xmlParse = new DOMParser().parseFromString(xml, "text/xml");
+	const scoreParts = Array.from(xmlParse.getElementsByTagName("score-part"));
+	for (const scorePart of scoreParts) {
+		if (scorePart.getElementsByTagName("part-name").length === 0) {
+			state.evxmlAddPartName = true;
+			const name = scorePart.getAttribute("id") || "";
+			const newPartName = `<part-name>${name}</part-name>`
+			// scorePart.prepend(newPartName);
+			scorePart.innerHTML = newPartName + scorePart.innerHTML;
+		} 
+		if (scorePart.getElementsByTagName("part-name").length && !scorePart.getElementsByTagName("part-name")?.[0]?.textContent?.trim() ) {
+			scorePart.getElementsByTagName("part-name")[0].textContent = scorePart.getAttribute("id") || "";
+		}
+	}
+	return new XMLSerializer().serializeToString(xmlParse);
+}
+
 /** 格式化曲谱
  * 1.全休止符的小节,没有音符默认加个全休止符
  */
 export const formatXML = (xml: string, xmlUrl?: string): string => {
 	if (!xml) return "";
-	
 	const xmlParse = new DOMParser().parseFromString(xml, "text/xml");
 
+	// 声调
+	const fifths = xmlParse.getElementsByTagName("fifths");
+	if (fifths && fifths.length) {
+		// 是否是C调
+		state.isCTone = fifths[0].textContent === '0'
+	}
+
+	const endings = Array.from(xmlParse.getElementsByTagName("ending"));
+	for (const ending of endings) {
+		// if (ending.getAttribute('type') === 'stop') {
+		// 	// @ts-ignore
+		// 	ending.parentNode?.removeChild(ending.parentNode?.getElementsByTagName('bar-style')[0])
+		// }
+		// @ts-ignore
+		// ending.parentNode.parentNode?.removeChild(ending.parentNode)
+	}
+
 	const measures = Array.from(xmlParse.getElementsByTagName("measure"));
 	const minutes: any = xmlParse.getElementsByTagName("per-minute");
 	let speeds: any = []
 	for (const minute of minutes) {
-		if (minute.textContent && !!Number(minute.textContent)) {
-			speeds.push(Number(minute.textContent))
+		let measureSpeed = minute.textContent ? Number(minute.textContent) : 0;
+		// 速度带附点,需要转换成不带附点的速度值
+		const hasSpeedDot = Array.from(minute?.parentElement?.children || []).some((item: any) => item?.tagName === 'beat-unit-dot')
+		measureSpeed = hasSpeedDot ? measureSpeed + measureSpeed/2 : measureSpeed;
+		if (minute.textContent && measureSpeed) {
+			speeds.push(Number(measureSpeed))
 		}
+		// if (hasSpeedDot && measureSpeed) {
+		// 	minute.textContent = measureSpeed
+		// 	const dotDom = minute?.parentElement.querySelector('beat-unit-dot')
+		// 	minute?.parentElement?.removeChild(dotDom)
+		// }
 	}
 	speeds = [...new Set(speeds)]
 	const hasVaryingSpeed = speeds.length > 1 ? true : false
@@ -669,6 +715,9 @@ export const formatXML = (xml: string, xmlUrl?: string): string => {
 	let speed = -1
 	let beats = -1;
 	let beatType = -1;
+	// 前面小节的拍子
+	let preBeats: number = 4;
+	let preBeatType: number = 4;
 	// 小节中如果没有节点默认为休止符
 	for (const measure of measures) {
 		if (beats === -1 && measure.getElementsByTagName("beats").length) {
@@ -680,6 +729,11 @@ export const formatXML = (xml: string, xmlUrl?: string): string => {
 		if (speed === -1 && measure.getElementsByTagName('per-minute').length) {
 		  speed = Number(measure.getElementsByTagName('per-minute')[0]?.textContent)
 		}
+		// 当前小节的拍数
+		const currentBeats = measure.getElementsByTagName("beats").length ? measure.getElementsByTagName("beats")[0]?.textContent : preBeats;
+		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");
 		// 如果note节点里面有space节点,并且没有duration节点,代表这是一个空白节点,需要删除
 		if (measure.getElementsByTagName("note").length && state.isEvxml) {
@@ -726,11 +780,14 @@ export const formatXML = (xml: string, xmlUrl?: string): string => {
           <voice>1</voice>
           <type>whole</type>
         </note>`;
+		} else if (state.musicRenderType !== 'staff') {
+			transferJianNote(measure, divisions, preBeats, preBeatType)
 		}
 	}
 	return new XMLSerializer().serializeToString(xmlParse);
 };
 
+
 /** 获取所有音符的时值,以及格式化音符 */
 export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 	const customNoteRealValue = customData.customNoteRealValue;
@@ -789,11 +846,12 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 	let multipleRestMeasures = 0;
 	let staveNoteIndex = 0;
 	let staveIndex = 0;
-	let xmlNoteTime = 0  // xml上面的音符时间
 	let xmlMp3BeatFixTime = 0 // xml上节拍器的时间
 
 	let preNoteEndTime = 0; // 上一个音符的结束时间
 
+	let preNoteMeasureNumber = 0; // 上一个小节的number值
+
 	const _notes = [] as any[];
 	if (state.gradualTimes) {
 		console.log("后台设置的渐慢小节时间", state.gradual, state.gradualTimes);
@@ -805,9 +863,16 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 	let differFrom = 0;
 	// let testIdx = 0;
 	let repeatIdx = 0; // 循环的次数
+	const firstTrackName = state.canSelectTracks[0] || "";
 	while (!iterator.EndReached) {
 		// console.log({ ...iterator });
 		/** 多声轨合并显示,当前音符的时值取所有声轨中的最小值 */
+		if (state.isCombineRender) {
+			iterator.currentVoiceEntries = iterator.currentVoiceEntries.filter((item: any) => {
+				const trackName = state.isEvxml && state.evxmlAddPartName ? item.parentVoice.parent.IdString || '' : item.parentVoice.parent.Name || '';
+				return trackName === firstTrackName
+			});
+		}
 		let minIndex = 0, elRealValue = 0
 		for (let index = 0; index < iterator.currentVoiceEntries.length; index++) {
 			const element = iterator.currentVoiceEntries[index];
@@ -822,8 +887,10 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 			}
 			elRealValue = element.notes[0].length.realValue
 		}
+		if (minIndex !== 0 && state.isCombineRender && iterator.currentVoiceEntries[minIndex]) {
+			iterator.currentVoiceEntries[minIndex].Notes[0].NoteToGraphicalNoteObjectId = iterator.currentVoiceEntries?.[0].Notes[0].NoteToGraphicalNoteObjectId;
+		}
 		const voiceEntries = iterator.currentVoiceEntries?.[minIndex] ? [iterator.currentVoiceEntries?.[minIndex]] : [];
-
 		let currentVoiceEntries: any[] = [];
 		// 多分轨,当前小节最大音符数量
 		let maxNoteNum = 0;
@@ -895,7 +962,8 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 			if (state.multitrack > 0 && currentTime > note.length.realValue) {
 				currentTime = note.length.realValue;
 			}
-			note.maxNoteNum = maxNoteNum
+			note.maxNoteNum = maxNoteNum;
+			note.trackIndex = minIndex;
 			_notes.push({
 				note,
 				iterator: { ...iterator },
@@ -976,6 +1044,10 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 			}
 
 			activeVerticalMeasureList = [note.sourceMeasure?.verticalMeasureList?.[0]] || [];
+			// 某些情况下,合并显示的妙极客曲子,note.sourceMeasure?.verticalMeasureList可能为空数组
+			if (state.isCombineRender && state.isEvxml && note.sourceMeasure?.verticalMeasureList.length === 0) {
+				activeVerticalMeasureList = osmd.GraphicSheet.MeasureList.find((item: any) => item[0]?.MeasureNumber === note.sourceMeasure.MeasureNumberXML) || [];
+			}
 			let currenrtVfVoices = activeVerticalMeasureList[0]?.vfVoices['1'] ? activeVerticalMeasureList[0]?.vfVoices['1'] : activeVerticalMeasureList[0]?.vfVoices['2'] ? activeVerticalMeasureList[0]?.vfVoices['2'] : null;
 			/**
 			 * TODO:多分轨合并的小节,音符可能没有id,此时就去其它分轨找
@@ -1113,7 +1185,7 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 			// 当前音符的持续时长,当前音符的RealValue值*拍数*(60/后台设置的基准速度)
 			let noteLength = gradualLength ? gradualLength : Math.min(vRealValue, NoteRealValue) * formatBeatUnit(beatUnit) * (60 / beatSpeed);
 			// 小节时长
-			const measureLength = vRealValue * vDenominator * (60 / beatSpeed);
+			const measureLength = vRealValue * 4 * (60 / beatSpeed);
 			// console.table({value: iterator.currentTimeStamp.realValue, vRealValue,NoteRealValue, noteLength,measureLength, MeasureNumberXML: note.sourceMeasure.MeasureNumberXML})
 			// console.log(i, Math.min(vRealValue, NoteRealValue),noteLength,gradualLength, formatBeatUnit(beatUnit),beatSpeed, NoteRealValue * formatBeatUnit(beatUnit) * (60 / beatSpeed) )
 			/**
@@ -1194,10 +1266,18 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 				multipleRestMeasures = 0;
 			}
 			if (multipleRestMeasures < totalMultipleRestMeasures) {
-				multipleRestMeasures++;
+				if (note?.sourceMeasure?.MeasureNumberXML !== preNoteMeasureNumber) {
+					multipleRestMeasures++;
+				} else {
+					multipleRestMeasures = allNotes.length ? allNotes.last().multipleRestMeasures : 0;
+				}
 			} else {
-				multipleRestMeasures = 0;
-				totalMultipleRestMeasures = 0;
+				if (note?.sourceMeasure?.MeasureNumberXML !== preNoteMeasureNumber) {
+					multipleRestMeasures = 0;
+					totalMultipleRestMeasures = 0;
+				} else {
+					multipleRestMeasures = allNotes.length ? allNotes.last().multipleRestMeasures : 0;
+				}
 			}
 
 			// console.log(note.tie)
@@ -1205,10 +1285,47 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 			// console.log('频率',note?.pitch?.frequency,i)
 			/**
 			 * evxml的曲子,如果曲谱xml中带有times信息,则音符时值优先取times中的值
+			 * 曲子:1795013295024062466(春暖花开),如果音符有times信息,休止符没有times信息,此种规则是认为休止符不参与时值计算的,需要过滤掉该休止符
+			 * TODO:需要考虑唱名怎么处理,唱名是xml有多少个音符,就需要唱多少个,不能剔除
 			 */
+			if (state.isEvxml && note.isRestFlag && note?.noteTimeInfo?.length === 0 && state.xmlHasTimes ) {
+				const idx = _notes.findIndex(item=>item.note === note);
+				let nextNoteTimes = _notes[idx+1]?.note?.noteTimeInfo?.[0]?.begin*1000 
+				let preNoteTImes = _notes[idx-1]?.note?.noteTimeInfo?.[0]?.end*1000
+				// 当下一个音符也没有时间的时候,再往下一个找
+				if(!nextNoteTimes && nextNoteTimes!==0){
+					let nextIndex = idx + 2
+					while(!nextNoteTimes && nextIndex<_notes.length){
+						nextNoteTimes = _notes[nextIndex]?.note?.noteTimeInfo?.[0]?.begin*1000
+						nextIndex ++
+					}
+					// 当最后音符就是没有打时间的休止小节,可能nextNoteTimes时间找不到,目前没有处理
+				}
+				if(!preNoteTImes && preNoteTImes!==0){
+					let preIndex = idx - 2
+					while(!preNoteTImes && preIndex>-1){
+						preNoteTImes = _notes[preIndex]?.note?.noteTimeInfo?.[0]?.end*1000
+						preIndex --
+					}
+					// 当没有找到preNoteTImes的时候 赋值为0 (当第一个音符就是没有打时间的休止小节会出现这种情况)
+					preNoteTImes || (preNoteTImes = 0)
+				}
+				const allowRange = Math.abs(nextNoteTimes - preNoteTImes)< 10;
+				if (allowRange) {
+					note.maxNoteNum = note.maxNoteNum - 1;
+					// 唱名时间补齐,当删除这个音符的时候,上个音符的持续时间要加上这个音符的时间
+					allNotes[allNotes.length - 1].noteLengthTime += noteLength
+					continue;
+				}
+			}
 			let evNoteStartTime = 0, evNoteEndTime = 0;
-			if (state.isEvxml && note?.noteTimeInfo?.length) {
-				const idx = noteIds.filter((item: any) => item === svgElement?.attrs.id)?.length || 0
+			if (state.isEvxml && note?.noteTimeInfo?.length ) {
+				let idx = noteIds.filter((item: any) => item === svgElement?.attrs.id)?.length || 0;
+				// 如果是合并的小节的休止符
+				if (note.isRestFlag && !svgElement && note?.NoteToGraphicalNoteObjectId) {
+					const customRestId = `rest-${note?.sourceMeasure?.MeasureNumberXML}-${note?.NoteToGraphicalNoteObjectId}`;
+					idx = noteIds.filter((item: any) => item === customRestId)?.length || 0;
+				}
 				evNoteStartTime = note?.noteTimeInfo[idx]?.begin
 				evNoteEndTime = note?.noteTimeInfo[idx]?.end
 				if (evNoteStartTime) {
@@ -1218,6 +1335,10 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 				// usetime = evNoteStartTime - fixtime
 			}
 			svgElement?.attrs.id && noteIds.push(svgElement?.attrs.id)
+			// 如果是合并的休止小节,是没有渲染音符的,所以没有svgElement对象,也就没有id,此时需要添加自定义的一个id进度,便于多遍循环时,找到对应的noteTimeInfo里面的时间信息
+			if (note.isRestFlag && !svgElement && note?.NoteToGraphicalNoteObjectId) {
+				noteIds.push(`rest-${note?.sourceMeasure?.MeasureNumberXML}-${note?.NoteToGraphicalNoteObjectId}`)
+			}
 
 			// 如果该音符包含倚音,添加标记
 			let hasGraceNote = false;
@@ -1226,6 +1347,7 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 			}
 			const filterRepeatIdx = allNotes.filter((item: any) => item.noteId === note.NoteToGraphicalNoteObjectId).length
 			const nodeDetail = {
+				trackIndex: note.trackIndex, // 当前的音符属于第几条分轨
 				isStaccato: note.voiceEntry.isStaccato(),
 				isRestFlag: note.isRestFlag,
 				noteId: note.NoteToGraphicalNoteObjectId,
@@ -1274,14 +1396,15 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 				maxNoteNum: note.maxNoteNum, // 当前小节音符最多的分轨的音符数量
 				// repeatIdx: iterator.repeatIdx || 0, // 标记是第几遍循环,从0开始
 				repeatIdx: filterRepeatIdx,
-				xmlNoteTime: retain(xmlNoteTime), // xml上音符开始时间 唱名用
-				xmlNoteEndTime: retain(xmlNoteTime + noteLength), //xml上音符结束时间 唱名用
+				noteLengthTime: noteLength, //当前音符时长
+				xmlNoteTime: 0, // xml上音符开始时间 唱名用
+				xmlNoteEndTime: 0, //xml上音符结束时间 唱名用
 				xmlMp3BeatFixTime,  //xml上节拍器的时间
 				notBeatFixtime: state.isOpenMetronome ? fixtime - xmlMp3BeatFixTime : fixtime, // 不含节拍器的fixtime值 唱名用
 				notBeatTime: state.isEvxml && evNoteStartTime ? retain(evNoteStartTime) : retain(relativeTime + (state.isOpenMetronome ? fixtime - xmlMp3BeatFixTime : fixtime)), // 不含节拍器的 音符开始时间
 				notBeatEndTime: state.isEvxml && evNoteEndTime ? retain(evNoteEndTime) : retain(relaEndtime + (state.isOpenMetronome ? fixtime - xmlMp3BeatFixTime : fixtime)) // 不含节拍器的 音符结束时间
 			};
-			xmlNoteTime += noteLength
+			// console.log(i,'当前的小节',nodeDetail.MeasureNumberXML,totalMultipleRestMeasures,multipleRestMeasures)
 			// 如果是妙极客的曲子,并且第二遍循环播放需要等待时间,并且是第二遍循环的第一个小节的第一个音符
 			// if (state.isEvxml && state.secondEvXmlBeginTime && nodeDetail.i > 0 && nodeDetail.MeasureNumberXML === 1 && nodeDetail.noteId === 0) {
 			// 	nodeDetail.time = nodeDetail.time + state.secondEvXmlBeginTime;
@@ -1289,7 +1412,9 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 			// 	usetime = usetime + state.secondEvXmlBeginTime;
 			// 	relativeTime = relativeTime + state.secondEvXmlBeginTime;
 			// }
-			if (state.isEvxml && nodeDetail.repeatIdx && nodeDetail.i > 0 && nodeDetail.MeasureNumberXML === 1 && nodeDetail.noteId === 0) {
+			// if (state.isEvxml && nodeDetail.repeatIdx && nodeDetail.i > 0 && nodeDetail.MeasureNumberXML === 1 && nodeDetail.noteId === 0) {
+			const firstRepeatNodeId = allNotes.find((item: any) => item.MeasureNumberXML === state.timegapRepeatMeasureIndex)?.noteId || 0;
+			if (state.isEvxml && nodeDetail.repeatIdx && nodeDetail.i > 0 && nodeDetail.MeasureNumberXML === state.timegapRepeatMeasureIndex && nodeDetail.noteId === firstRepeatNodeId) {
 				const currentWaitTime = state.evXmlBeginArr[nodeDetail.repeatIdx] || 0;
 				nodeDetail.time = nodeDetail.time + currentWaitTime;
 				nodeDetail.endtime = nodeDetail.endtime + currentWaitTime;
@@ -1306,11 +1431,22 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 			// console.log('👀看看endtime', nodeDetail.duration, relaEndtime, fixtime, i)
 			// console.log('音符时间',nodeDetail.i,nodeDetail.time,nodeDetail.endtime)
 			tickables = tickables.filter((tickable: any) => tickable.attrs?.type !== "GhostNote")
-			const maxNum = (state.isCombineRender && note.maxNoteNum) ? note.maxNoteNum : tickables.length;
+			let maxNum = (state.isCombineRender && note.maxNoteNum) ? note.maxNoteNum : tickables.length;
+			// 妙极客的曲子,一个休止小节内可能有多个休止符,此时maxNum是0,需要针对这种情况作处理
+			if (note.isRestFlag && maxNum === 0) {
+				maxNum = note.maxNoteNum;
+			}
 			nodeDetail.noteLength = maxNum || 1;
 			allNotes.push(nodeDetail);
 			allNoteId.push(nodeDetail.id);
-			measures.push(nodeDetail);
+
+			if ( measures.some((item: any) => item.MeasureNumberXML !== nodeDetail.MeasureNumberXML) ) {
+				measures = [];
+				measures.push(nodeDetail);
+				nodeDetail.measures = measures;
+			} else {
+				measures.push(nodeDetail);
+			}
 			/**
 			 * bug: #9877
 			 * 多分轨合并展示的曲子,不同分轨,同一小节音符的数量可能不能,不能只通过tickables的长度判断该小节的音符数量
@@ -1324,12 +1460,21 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 			}
 			preNoteEndTime = nodeDetail.endtime;
 		}
+		preNoteMeasureNumber = note?.sourceMeasure?.MeasureNumberXML;
 		i++;
 	}
 	// 按照时间轴排序
 	const sortArray = allNotes.sort((a, b) => a.relativeTime - b.relativeTime).map((item, index) => Object.assign(item,{i:index}));
 	// const sortArray = allNotes.sort((a, b) => a.time - b.time).map((item, index) => ({ ...item, i: index }));
 	// const sortArray = allNotes.map((item, index) => ({ ...item, i: index }));
+	// 给 xmlNoteTime 和 xmlNoteEndTime 赋值
+	let xmlNoteTime = 0
+	sortArray.map(item => {
+		const noteLengthTime = item.noteLengthTime
+		item.xmlNoteTime = retain(xmlNoteTime)
+		item.xmlNoteEndTime = retain(xmlNoteTime + noteLengthTime)
+		xmlNoteTime += noteLengthTime
+	})
 	console.timeEnd("音符跑完时间");
 	try {
 		osmd.cursor.reset();
@@ -1435,16 +1580,19 @@ const customizationXml = (xmlParse: any) => {
 	const measures: any[] = Array.from(xmlParse.getElementsByTagName("measure"));
 	const notes: any[] = Array.from(xmlParse.getElementsByTagName("note"));
 
-	// 获取音符最多的歌词数,用于自定义循环播放次数
-	let maxLyricNum = 0;
+	// 获取音符最多的歌词数,time最多的次数,取两者的最大值,用于自定义循环播放次数
+	let maxLyricNum = 0, maxTimeNum = 0;
 	if (notes && notes.length) {
 		for (const note of notes) {
 			if (maxLyricNum < note.getElementsByTagName("lyric").length) {
 				maxLyricNum = note.getElementsByTagName("lyric").length
 			}
+			if (maxTimeNum < note.getElementsByTagName("time").length) {
+				maxTimeNum = note.getElementsByTagName("time").length
+			}
 		}
 	}
-	state.maxLyricNum = maxLyricNum;
+	state.maxLyricNum = Math.max(maxLyricNum, maxTimeNum);
 	// state.osmd.EngravingRules.DYCustomRepeatCount = maxLyricNum;
 	;(window as any).DYCustomRepeatCount = state.maxLyricNum;
 	console.log('歌词次数',maxLyricNum)
@@ -1489,20 +1637,26 @@ const customizationXml = (xmlParse: any) => {
 	 * 妙极客xml,多遍歌词循环的曲目,如果没有repeat标签,需要加上repeat标签
 	 * */
 	if (maxLyricNum > 1) {
-		const hasRepeat = xmlParse.querySelectorAll('repeat').length > 0
+		const hasRepeat = xmlParse.querySelectorAll('repeat').length > 0;
 		if (!hasRepeat) {
-			const lastMeasure = measures.last();
-			if (lastMeasure.getElementsByTagName('barline').length) {
-				const barlineDom = lastMeasure.getElementsByTagName('barline')[0]
-				barlineDom.innerHTML = barlineDom.innerHTML + `<repeat direction="backward" />`;
-			} else {
-				lastMeasure.innerHTML = lastMeasure.innerHTML + `
-				<barline location="right">
-					<bar-style>light-heavy</bar-style>
-					<repeat direction="backward" />
-				</barline>`
+			const parts = xmlParse.querySelectorAll('score-partwise>part')
+			if (parts.length) {
+				for (const part of parts) {
+					const currentMeasures = part.querySelectorAll('measure').length ? Array.from(part.querySelectorAll('measure')) : [];
+					const lastMeasure: any = currentMeasures.last();
+					if (lastMeasure?.getElementsByTagName('barline').length) {
+						const barlineDom = lastMeasure?.getElementsByTagName('barline')[0]
+						barlineDom.innerHTML = barlineDom.innerHTML + `<repeat direction="backward" />`;
+					} else {
+						lastMeasure.innerHTML = lastMeasure.innerHTML + `
+						<barline location="right">
+							<bar-style>light-heavy</bar-style>
+							<repeat direction="backward" />
+						</barline>`
+					}
+					// console.log(lastMeasure)
+				}
 			}
-			// console.log(lastMeasure)
 		}
 	}
 }
@@ -1515,16 +1669,26 @@ const analyzeEvxml = (xmlParse: any, xmlUrl?: string) => {
 	const xmlNum2 = xmlParse.getElementsByTagName("timegap")[0]?.getElementsByTagName("values")[0]?.getElementsByTagName("item")[1]?.getAttribute('num');
 	const denNum2 = xmlParse.getElementsByTagName("timegap")[0]?.getElementsByTagName("values")[0]?.getElementsByTagName("item")[1]?.getAttribute('den');
 	const timeGaps: any = xmlParse.getElementsByTagName("timegap")?.length ? Array.from(xmlParse.getElementsByTagName("timegap")?.[0]?.getElementsByTagName("values")?.[0]?.getElementsByTagName("item")) : [];
+	state.xmlHasTimes = !!xmlParse.getElementsByTagName("times")?.length
 	// 第一个音符的起始时间
 	const firstMeasure = xmlParse.getElementsByTagName("measure")[0];
 	if (firstMeasure) {
 		const firstNoteBeginTime = firstMeasure.getElementsByTagName("times")[0]?.getElementsByTagName("time")[0]?.getAttribute('begin');
 		state.evXmlBeginTime = firstNoteBeginTime ? firstNoteBeginTime / 1000 : xmlNum ? 60 / state.originSpeed * xmlNum * 4/denNum : 0;
 		state.secondEvXmlBeginTime = firstNoteBeginTime ? 0 : xmlNum2 ? 60 / state.originSpeed * xmlNum2 * 4/denNum2 : 0;
-		const hasTimeGap = xmlParse.getElementsByTagName("timegap").length > 0;
+		const hasTimeGap = state.xmlHasTimeGap = xmlParse.getElementsByTagName("timegap").length > 0;
 		const hasTimes = xmlParse.getElementsByTagName("times").length > 0;
-
 		if (timeGaps && timeGaps.length && !firstNoteBeginTime) {
+			// 有timegap的曲子,需要找到是从哪一小节开始循环的,默认是从第一节开始循环
+			const startRepeat = Array.from(xmlParse.getElementsByTagName("repeat") || []).filter((item: any) => item?.getAttribute('direction') === 'forward')
+			const firstRepeat: any = startRepeat?.length ? startRepeat[0] : null;
+			if (firstRepeat) {
+				let parentElement = firstRepeat?.parentNode
+				while (parentElement && parentElement.tagName !== 'measure') {
+					parentElement = parentElement.parentNode
+				}
+				state.timegapRepeatMeasureIndex = parentElement?.getAttribute('number') ? Number(parentElement?.getAttribute('number')) : 1;
+			}
 			for (const timeGap of timeGaps) {
 				const num: any = timeGap?.getAttribute('num'), den: any = timeGap?.getAttribute('den');
 				const startTime = num ? 60 / state.originSpeed * num * 4/den : 0;

+ 13 - 7
src/helpers/metronome.ts

@@ -186,16 +186,16 @@ class Metronome {
 			if(state.playType === "play" && state.playSource === "music" && audioDataState.songCollection.beatSongEle){
 				return
 			}			
-			if(state.playType === "play" && state.playSource === "background" && audioDataState.songCollection.betaBackgroundEle){
+			if(state.playType === "play" && state.playSource === "background" && audioDataState.songCollection.beatBackgroundEle){
 				return
 			}			
-			if(state.playType === "sing" && state.playSource === "music" && audioDataState.songCollection.betaFanSongEle){
+			if(state.playType === "sing" && state.playSource === "music" && audioDataState.songCollection.beatFanSongEle){
 				return
 			}			
-			if(state.playType === "sing" && state.playSource === "background" && audioDataState.songCollection.betaBanSongEle){
+			if(state.playType === "sing" && state.playSource === "background" && audioDataState.songCollection.beatBanSongEle){
 				return
 			}			
-			if(state.playType === "sing" && state.playSource === "mingSong" && audioDataState.songCollection.betaMingSongEle){
+			if(state.playType === "sing" && state.playSource === "mingSong" && audioDataState.songCollection.beatMingSongEle){
 				return
 			}			
 		}
@@ -286,10 +286,9 @@ class Metronome {
 			const measureListIndex = note?.noteElement?.sourceMeasure?.measureListIndex;
 			if (measureNumberXML > -1) {
 				if (measureNumberXML != xmlNumber) {
-					// 弱起的时候 根据音符结尾时间减去音符开头时间,得到的不是正常小节的时间,然后平均分配节拍之后,当前节拍间隔会非常短 这里弱起拿正常
-					// 妙极客的弱起不受这个影响
+					// 弱起的时候 根据音符结尾时间减去音符开头时间,得到的不是正常小节的时间,然后平均分配节拍之后,当前节拍间隔会非常短 这里弱起取整个小节的时间
 					let startTime = note.measures[0].time
-					if(!state.isEvxml && i === 0){
+					if(i === 0 && note.measures[0].difftime>0){
 						startTime = note.measures[note.measures.length - 1].endtime - note.measures[0].measureLength
 					}
 					const m = {
@@ -356,6 +355,13 @@ class Metronome {
 
 		let metroList: number[] = [];
 		const metroMeasure: any[] = [];
+		console.log("节拍器 每一小节时间:", measures)
+		console.log("节拍器 间隔:", measures.map(item => {
+			return {
+				time: item.time,
+				measureNumberXML: item.measureNumberXML
+			}
+		}))
 		// 4.按照拍数将时长平均分配
 		try {
 			for (let i = 0; i < measures.length; i++) {

+ 26 - 13
src/page-instrument/App.tsx

@@ -55,7 +55,7 @@ export default defineComponent({
         setToken(query.Authorization);
       }
       if (window.location.href.includes("simple-detail")) {
-        // 
+        //
       } else {
         if (!getToken()) {
           const res = await api_getToken();
@@ -98,19 +98,32 @@ export default defineComponent({
       const _loading = document.getElementById("loading");
       _loading && document.body.removeChild(_loading);
       // console.log(query);
-      if (query.platform == "pc") document.body.addEventListener("keyup", (e: KeyboardEvent) => onKeyBoard(e));
-
-      // 禁用右键菜单
-      document.addEventListener("contextmenu", function (event) {
-        // event.preventDefault();
-      });
-      // 禁用浏览器快捷键
-      document.addEventListener("keydown", function (event) {
-        // 屏蔽 F12 和 Ctrl+Shift+I
-        if (event.key === "F12" || (event.ctrlKey && event.shiftKey && event.key === "I") || (event.metaKey && event.altKey && event.key === "I")) {
+      if (query.platform == "pc" || query.isPreView) {
+        document.body.addEventListener("keyup", (e: KeyboardEvent) => onKeyBoard(e));
+        // 禁用右键菜单
+        document.addEventListener("contextmenu", function (event) {
           event.preventDefault();
-        }
-      });
+        });
+        // 禁用浏览器快捷键
+        document.addEventListener("keydown", function (event) {
+          // 屏蔽 F12 和 Ctrl+Shift+I
+          if (event.key === "F12" || (event.ctrlKey && event.shiftKey && event.key === "I") || (event.metaKey && event.altKey && event.key === "I")) {
+            event.preventDefault();
+          }
+        });
+      }
+
+      // // 禁用右键菜单
+      // document.addEventListener("contextmenu", function (event) {
+      //   // event.preventDefault();
+      // });
+      // // 禁用浏览器快捷键
+      // document.addEventListener("keydown", function (event) {
+      //   // 屏蔽 F12 和 Ctrl+Shift+I
+      //   if (event.key === "F12" || (event.ctrlKey && event.shiftKey && event.key === "I") || (event.metaKey && event.altKey && event.key === "I")) {
+      //     event.preventDefault();
+      //   }
+      // });
     });
 
     onUnmounted(() => {

+ 9 - 0
src/page-instrument/api.ts

@@ -64,6 +64,15 @@ export const api_musicSheetPage = (data: any) => {
   });
 };
 
+/** 获取教程和年级  */
+export const api_musicTagTree = () => {
+  return request.get("/musicTag/tree");
+};
+/** 获取标签  */
+export const api_musicSheetTag = () => {
+  return request.get("/musicSheetTag/queryList");
+};
+
 /**
  * 获取声部列表
  */

+ 9 - 1
src/page-instrument/component/authorName/index.module.less

@@ -1,5 +1,5 @@
 .authorName{
-    height: 1.8rem;
+    height: 1.8rem; // 与smoothAnimationBox高度对应
 }
 .title{
     width: 280px;
@@ -38,4 +38,12 @@
             }
         }
     }
+}
+
+.blackTitle {
+    :global{
+        .van-notice-bar{
+            color: #000 !important;
+        }
+    }
 }

+ 2 - 2
src/page-instrument/component/authorName/index.tsx

@@ -17,11 +17,11 @@ export default defineComponent({
             {
                !smoothAnimationState.isShow.value && !state.isCombineRender && 
                <div class={["authorName", styles.authorName]}>
-                  <div class={styles.title}>
+                  <div class={[styles.title, state.isCbsView && styles.blackTitle]}>
                      <NoticeBar text={state.examSongName} background="none" />
                   </div>
                   <div class={styles.authorCon}>
-                     <div class={styles.author}>
+                     <div class={[styles.author, state.isCbsView && styles.blackTitle]}>
                         {
                            state.isSingleLine ? 
                            <>

+ 282 - 0
src/page-instrument/component/the-music-list/filterList.tsx

@@ -0,0 +1,282 @@
+import { defineComponent, PropType, computed, ref, nextTick, reactive } from "vue";
+import { api_musicTagTree, api_musicSheetTag, api_subjectList } from "../../api";
+import { Popover } from "vant"
+import styles from "./index.module.less";
+import headImg from "./imgs/headImg.png"
+import quedingImg from "./imgs/queding.png"
+import quxiaoImg from "./imgs/quxiao.png"
+import zhankaiImg from "./imgs/zhankai.png"
+import shouqiImg from "./imgs/shouqi.png"
+import sjImg from "./imgs/sj.png"
+import closeImg from "../../header-top/image/closeImg.png"
+import state, { IPlatform } from "/src/state";
+import { getQuery } from "/src/utils/queryString";
+
+export default defineComponent({
+	name: "filterList",
+    emits: ["close", "handleConfirm"],
+	setup(props, { emit }) {
+        const query: any = getQuery();
+        const queryObj = reactive({
+            audioPlayTypes:"",
+            sheetTag:"",
+            course:"",
+            grade:"",
+            subject: {
+                name: "",
+                id: ""
+            }
+        })
+        function handleRefresh(){
+            queryObj.audioPlayTypes = ""
+            queryObj.sheetTag = ""
+            queryObj.course = ""
+            queryObj.grade = ""
+            queryObj.subject = {
+                name: "",
+                id: ""
+            }
+            handleSubjectOne()
+        }
+        function handleConfirm() {
+            emit("handleConfirm",{
+                audioPlayTypes:  queryObj.audioPlayTypes ? queryObj.audioPlayTypes.split(",") : [],
+                musicTutorialIds: queryObj.grade ? queryObj.grade : queryObj.course,
+                musicTagIds: queryObj.sheetTag,
+                musicalInstrumentId: queryObj.subject.id
+            })
+        }
+        // 场景
+        const audioPlayTypesOption = [
+            { text: '全部', value: "" },
+            { text: '演奏', value: "PLAY" },
+            { text: '演唱', value: "SING" },
+            { text: '演奏+演唱', value: "PLAY,SING" },
+        ]
+        function handleSelAudioPlayTypes(item:any){
+            queryObj.audioPlayTypes = item.value
+        }
+        // 获取标签
+        getMusicSheetTag()
+        const sheetTagObj = ref<any[]>([])
+        function getMusicSheetTag(){
+            api_musicSheetTag().then(res=>{
+                if(res.code === 200){
+                    sheetTagObj.value = [{ name:"全部",id:""},...res.data]
+                }
+            })
+        }
+        function handleSelSheetTag(item:any){
+            queryObj.sheetTag = item.id
+        }
+        // 获取教程和年级
+        getMusicTagTree()
+        const courseObj = ref<any[]>([])
+        const gradeObj = ref<any[]>([])
+        function getMusicTagTree(){
+            api_musicTagTree().then(res=>{
+                if(res.code === 200){
+                    courseObj.value = [{ name:"全部",id:""},...res.data]
+                }
+            })
+        }
+        const isExpand = ref(false)
+        const computedCourseObj = computed(()=>{
+            return isExpand.value ? courseObj.value : courseObj.value.slice(0,5)
+        })
+        const courseDom = ref<HTMLDivElement>()
+        const borderBoxConDom = ref<HTMLDivElement>()
+        function handleExpand(){
+            isExpand.value = true
+            nextTick(()=>{
+                const childRect = courseDom.value!.getBoundingClientRect();
+                const parentRect = borderBoxConDom.value!.getBoundingClientRect();
+                const offsetTop = borderBoxConDom.value!.scrollTop + childRect.top - parentRect.top
+                borderBoxConDom.value!.scrollTo({
+                    top: offsetTop,
+                    behavior: 'smooth'
+                });
+            })
+        }
+        function handleSelCourse(item:any){
+            queryObj.course = item.id
+            queryObj.grade = ""
+            gradeObj.value = [{ name:"全部", id:""}, ...(item.children || [])]
+        }
+        function handleSelGrade(item:any){
+            queryObj.grade = item.id
+        }
+        // 获取乐器 
+        state.platform === IPlatform.PC && getSubjectList() // 老师端才加载乐器
+        const subjectList = ref<any[]>([])
+        function getSubjectList(){
+            api_subjectList({}).then(res => {
+                if(res.code === 200){
+                    subjectList.value = [...res.data.map((item:any)=>{
+                        return item.instruments.length > 1 ? Object.assign(item,{ isExpand: ref(false) }) : item
+                    })]
+                    // 赋默认值
+                    handleSubjectOne()
+                }
+            })
+        }
+        function handleSubjectOne(){
+            if(subjectList.value.length > 0){
+                const instruments = subjectList.value.reduce((arr, item) => {
+                    arr.push(...item.instruments)
+                    return arr
+                }, [])
+                const instrumentId = query.instrumentId
+                // 有id 就用id,没有就默认第一个
+                const instrumentObj = instrumentId ? instruments.find((i: any) => {
+                    return i.id === instrumentId
+                }) : instruments[0]
+                if(instrumentObj){
+                    queryObj.subject.id = instrumentObj.id
+                    queryObj.subject.name = instrumentObj.name
+                }
+            }
+        }
+        function isActiveSubjectPop(item:any) {
+            return item.instruments.some((i:any) => {
+                return i.id === queryObj.subject.id
+            })
+        }
+        function handleSelectPop(item: any) {
+            queryObj.subject.id = item.id
+            queryObj.subject.name = item.name
+        }
+		return () => (
+			<div class={[styles.filterList, styles[state.modeType], state.platform === IPlatform.PC && styles.isPc]}>
+                <div class={[styles.head, "top_draging"]}>
+                    <img class={styles.headTit} src={headImg} />
+				</div>
+                <img class={styles.closeImg} src={closeImg} onClick={()=>{ emit("close") }} />
+				<div class={styles.borderCon}>
+                    <div class={styles.borderBox}>
+                        <div ref={borderBoxConDom} class={styles.borderBoxCon}>
+                            {
+                                sheetTagObj.value.length > 1 &&
+                                    <>
+                                        <div class={styles.titCon}>
+                                            标签
+                                        </div>
+                                        <div class={styles.filterCon}>
+                                            {
+                                                sheetTagObj.value.map(item => {
+                                                    return <div class={[styles.tabBox, queryObj.sheetTag === item.id && styles.tabActive]} onClick={() => { handleSelSheetTag(item) }}>{item.name}</div>
+                                                })
+                                            }
+                                        </div>  
+                                    </>
+                            }                      
+                            <div class={styles.titCon}>
+                                场景
+                            </div>
+                            <div class={styles.filterCon}>
+                                {
+                                    audioPlayTypesOption.map(item => {
+                                        return <div class={[styles.tabBox, queryObj.audioPlayTypes === item.value && styles.tabActive]} onClick={() => { handleSelAudioPlayTypes(item) }}>{item.text}</div>
+                                    })
+                                }
+                            </div>
+                            {
+                                courseObj.value.length>1 &&
+                                    <>
+                                        <div ref={courseDom} class={styles.titCon}>
+                                            教程
+                                            {
+                                                isExpand.value &&                          
+                                                    <div class={styles.shouqiImg} onClick={() => {isExpand.value = false}}>
+                                                        收起
+                                                        <img src={shouqiImg} />
+                                                    </div>
+                                            }
+                                        </div>
+                                        <div class={[styles.filterCon, styles.courseType]}>
+                                            {
+                                                computedCourseObj.value.map(item => {
+                                                    return <div class={[styles.tabBox, queryObj.course === item.id && styles.tabActive]} onClick={() => { handleSelCourse(item) }}>{item.name}</div>
+                                                })
+                                            }
+                                            {
+                                                !isExpand.value && 
+                                                    <div class={[styles.tabBox, styles.zhankaiImg]} onClick={handleExpand}>
+                                                        查看更多
+                                                        <img src={zhankaiImg} />
+                                                    </div>
+                                            }
+                                        </div> 
+                                    </>
+                            }
+                            {
+                                gradeObj.value.length > 1 &&
+                                    <>
+                                        <div class={styles.titCon}>
+                                            年级
+                                        </div>
+                                        <div class={styles.filterCon}>
+                                            {
+                                                gradeObj.value.map(item => {
+                                                    return <div class={[styles.tabBox, queryObj.grade === item.id && styles.tabActive]} onClick={() => { handleSelGrade(item) }}>{item.name}</div>
+                                                })
+                                            }
+                                        </div>  
+                                    </>
+                            }   
+                            {
+                                subjectList.value.length>1 && queryObj.audioPlayTypes!=='SING' &&
+                                    <>
+                                        <div class={styles.titCon}>
+                                            乐器
+                                        </div>
+                                        <div class={styles.filterCon}>
+                                            {
+                                                subjectList.value.map(item => {
+                                                    return item.instruments.length > 1 ? 
+                                                        <Popover
+                                                            v-model:show={item.isExpand}
+                                                            trigger="click"
+                                                            class={styles.subjectPopover}
+                                                        >
+                                                            {{
+                                                                default: () => (
+                                                                    <div class={styles.tabPopoverBox}>
+                                                                        {
+                                                                            item.instruments.map((row: any) => {
+                                                                                return <div class={[styles.tabPopover, queryObj.subject.id === row.id && styles.active]} onClick={() => { 
+                                                                                    item.isExpand = false
+                                                                                    handleSelectPop(row) 
+                                                                                }}>{row.name}</div>
+                                                                            })
+                                                                        }
+                                                                    </div>
+                                                                ),                                                                
+                                                                reference: () => (
+                                                                    <div class={[styles.tabBox, styles.tabBoxPopCon, isActiveSubjectPop(item) && styles.tabActive]}>
+                                                                        <div class={[styles.tabBoxPop, item.isExpand && styles.actTabBoxPop]}>
+                                                                            <div>{isActiveSubjectPop(item)?queryObj.subject.name:item.name}</div>
+                                                                            <img class={styles.sjImg} src={sjImg} />
+                                                                        </div>
+                                                                    </div>
+                                                                )
+                                                            }}
+                                                        </Popover> 
+                                                        : 
+                                                        <div class={[styles.tabBox, queryObj.subject.id === item.instruments[0].id && styles.tabActive]} onClick={() => { handleSelectPop(item.instruments[0]) }}>{item.name}</div>
+                                                })
+                                            }
+                                        </div>   
+                                    </>
+                            }
+                        </div>
+                    </div>
+				</div>
+                <div class={styles.btnCon}>
+                    <img src={quxiaoImg} onClick={handleRefresh} />
+                    <img src={quedingImg} onClick={handleConfirm} />
+                </div>
+			</div>
+		);
+	},
+});

BIN
src/page-instrument/component/the-music-list/imgs/headImg.png


BIN
src/page-instrument/component/the-music-list/imgs/queding.png


BIN
src/page-instrument/component/the-music-list/imgs/quxiao.png


BIN
src/page-instrument/component/the-music-list/imgs/shouqi.png


BIN
src/page-instrument/component/the-music-list/imgs/sj.png


BIN
src/page-instrument/component/the-music-list/imgs/xiang.png


BIN
src/page-instrument/component/the-music-list/imgs/zhankai.png


+ 300 - 63
src/page-instrument/component/the-music-list/index.module.less

@@ -78,6 +78,17 @@
             }
         }
     }
+    &.isPc{
+        padding: 20px 0;
+        :global{
+            .van-tabs__wrap{
+                display: none !important;
+            }
+            .van-tabs__content{
+                height: 100% !important;
+            }
+        }
+    }
 }
 
 .wrap {
@@ -139,70 +150,23 @@
         .dropdownMenu{
             border-right: 1px solid #DADCE5;
             margin-right: 8px;
-            :global{
-                .van-dropdown-menu__bar{
-                    height: 20px;
-                    background: transparent;
-                    box-shadow: initial;
-                    .van-dropdown-menu__item{
-                        padding: 0 8px 0 0;
-                    }
-                    .van-dropdown-menu__title{
-                        --van-gray-4: #0CA2EA;
-                        font-weight: 400;
-                        font-size: 14px;
-                        color: #0CA2EA;
-                        padding: 0 12px 0 0;
-                        &::after{
-                            right: 0;
-                            opacity: initial;
-                        }
-                    }
-                }
-                .van-dropdown-item.van-dropdown-item--down{
-                    left: 36px;
-                    width: 148px;
-                    margin-top: 7px;
-                    .van-dropdown-item__content{
-                        margin-left: 10px;
-                        padding: 0 10px;
-                        width: 128px;
-                        box-shadow: 0px 2px 14px 0px rgba(0,0,0,0.12);
-                        border-radius: 8px;
-                        .van-cell{
-                            margin-top: 6px;
-                            padding: 0;
-                            font-weight: 400;
-                            font-size: 14px;
-                            color: #323233;
-                            line-height: 32px;
-                            text-align: center;
-                            &::after{
-                                border: none;
-                            }
-                            &:last-child{
-                                margin-bottom: 6px;
-                            }
-                            &.van-dropdown-item__option--active{
-                                background: #EEF8FF;
-                                border-radius: 4px;
-                                color: #1CACF1;
-                                font-weight: 500;
-                            }
-                            .van-cell__value{
-                                display: none;
-                            }
-                        }
-                    }
-                }
+            flex-shrink: 0;
+            display: flex;
+            align-items: center;
+            cursor: pointer;
+            &:active{
+                opacity: 0.8;
             }
-            &.currItem{
-                :global{
-                    .van-dropdown-menu__bar  .van-dropdown-menu__title{
-                        color: #1CACF1;
-                        --van-gray-4:#1CACF1;
-                    }
-                }
+            &>div{
+                font-weight: 400;
+                font-size: 14px;
+                color: #0CA2EA;
+                line-height: 20px;
+            }
+            &>img{
+                margin: 0 6px 0 4px;
+                width: 9px;
+                height: 5px;
             }
         }
     }
@@ -211,6 +175,10 @@
             margin-top: 10px;
             height: calc(100% - 44px);
             overflow-y: auto;
+            &::-webkit-scrollbar {
+                width: 0;
+                display: none;
+            }
             .van-loading__circular{
                 color: rgba(0,0,0,0.3);
             }
@@ -326,4 +294,273 @@
         color: rgba(0,0,0,0.46);
         margin-top: 10px;
     }
+}
+
+
+.filterList{
+    width: 408px;
+    height: 316px;
+    background: #B0D8FF;
+    box-shadow: inset 0px -2px 3px 0px #6BA5DD;
+    border-radius: 20px;
+    padding: 10px;
+    position: relative;
+    &.follow{
+        background: #BCE6F1;
+        box-shadow: inset 0px -2px 3px 0px #6397A4;
+        .borderCon{
+            background: #DAEFF5;
+            box-shadow: 0px 0px 3px 0px #98C4D0;
+            .borderBox{
+                border-color: #7CC2E0;
+            }
+        }
+        .btnCon{
+            background: linear-gradient( 180deg, rgba(213,232,255,0) 0%, rgba(218,239,245,0.73) 48%, #DAEFF5 100%);
+        }
+    }
+    &.evaluating{
+        background: #C4DAFF;
+        box-shadow: inset 0px -2px 3px 0px #6F86AD;
+        .borderCon{
+            background: #D5E2FF;
+            box-shadow: 0px 0px 3px 0px #889CBE;
+            .borderBox{
+                border-color: #91AAF9;
+            }
+        }
+        .btnCon{
+            background: linear-gradient( 180deg, rgba(213,232,255,0) 0%, rgba(213,226,255,0.73) 48%, #D5E2FF 100%);
+        }
+    }
+    &.isPc{
+        height:initial !important;
+        .borderBoxCon{
+            height: initial !important;
+            max-height: 70vh;
+        }
+    }
+    .head{
+        position: absolute;
+        top: -11px;
+        left: 50%;
+        transform: translateX(-50%);
+        width: 100%;
+        height: 45px;
+        display: flex;
+        justify-content: center;
+        .headTit{
+            width: 164px;
+            height: 100%;
+        }
+    }      
+    .closeImg{
+        position: absolute;
+        top: -9px;
+        right: -40px;
+        width: 32px;
+        height: 32px;
+        cursor: pointer;
+    }
+    .borderCon{
+        width: 100%;
+        height: 100%;
+        background: #D5E8FF;
+        box-shadow: 0px 0px 3px 0px #639ACF;
+        border-radius: 14px;
+        padding: 10px;
+        .borderBox{
+            padding-top: 10px;
+            padding-bottom: 1px;
+            width: 100%;
+            height: 100%;
+            border-radius: 10px 14px 14px 10px;
+            border: 1px dashed rgb(155,201,246);
+            .borderBoxCon{
+                padding: 0 10px 50px 10px;
+                width: 100%;
+                height: 100%;
+                overflow-y: auto;
+                &::-webkit-scrollbar {
+                    width: 0;
+                    display: none;
+                }
+            }
+            .titCon{
+                position: relative;
+                font-weight: 500;
+                font-size: 16px;
+                color: #333333;
+                line-height: 22px;
+                padding-left: 12px;
+                margin-top: 10px;
+                &::before{
+                    content: "";
+                    position: absolute;
+                    top: 50%;
+                    transform: translateY(-50%);
+                    left: 2px;
+                    width: 4px;
+                    height: 11px;
+                    background: #1CACF1;
+                    border-radius: 3px;
+                }
+                .shouqiImg{
+                    position: absolute;
+                    display: flex;
+                    justify-content: center;
+                    align-items: center;
+                    font-weight: 400;
+                    font-size: 12px;
+                    color: #333333;
+                    right: 0;
+                    top: 50%;
+                    transform: translateY(-50%);
+                    cursor: pointer;
+                    &:active{
+                        opacity: 0.8;
+                    }
+                    >img{
+                        margin-left: 2px;
+                        width: 11px;
+                        height: 10px;
+                    }
+                }
+            }
+            .filterCon{
+                display: flex;
+                flex-wrap: wrap;
+                margin-top: 8px;
+                margin-left: -10px;
+                width: calc(100% + 10px);
+                &.courseType{
+                    .tabBox{
+                        width: calc(50% - 10px);
+                        padding: 0 10px;
+                    }
+                }
+                .tabBox{
+                    cursor: pointer;
+                    width: calc(25% - 10px);
+                    height: 32px;
+                    margin-bottom: 8px;
+                    flex-shrink: 0;
+                    margin-left: 10px;
+                    background: #F6F6F6;
+                    border-radius: 6px;
+                    font-weight: 400;
+                    font-size: 12px;
+                    text-align: center;
+                    line-height: 32px;
+                    color: #333333;
+                    padding: 0 5px;
+                    white-space: nowrap;
+                    overflow: hidden;
+                    text-overflow: ellipsis;
+                    &:active{
+                        opacity: 0.8;
+                    }
+                    &.tabActive{
+                        background: #EBF8FF;
+                        border: 1px solid rgba(28,172,241,0.5);
+                        font-weight: 600;
+                        color: #1CACF1;
+                    }
+                    &.zhankaiImg{
+                        display: flex;
+                        justify-content: center;
+                        align-items: center;
+                        >img{
+                            margin-left: 2px;
+                            width: 11px;
+                            height: 10px;
+                        }
+                    }
+                    &.tabBoxPopCon{
+                        width: 100%;
+                        margin-left: 0;
+                        margin-bottom: 0;
+                        .tabBoxPop{
+                            width: 100%;
+                            height: 100%;
+                            display: flex;
+                            justify-content: center;
+                            align-items: center;
+                            &.actTabBoxPop{
+                                .sjImg{
+                                    transform: rotate(180deg);
+                                }
+                            }
+                            &>div{
+                                white-space: nowrap;
+                                overflow: hidden;
+                                text-overflow: ellipsis;  
+                            }
+                            .sjImg{
+                                width: 9px;
+                                height: 5px;
+                                margin-left: 4px;
+                            }
+                        }
+                    }
+                }
+                :global{
+                    .van-popover__wrapper{
+                        width: calc(25% - 10px);
+                        height: 32px;
+                        margin-bottom: 8px;
+                        flex-shrink: 0;
+                        margin-left: 10px;
+                    }
+                }
+            }
+        }
+    }
+    .btnCon{
+        position: absolute;
+        left: 24px;
+        bottom: 21px;
+        padding: 10px 0;
+        width: calc(100% - 48px);
+        display: flex;
+        justify-content: center;
+        background: linear-gradient( 180deg, rgba(213,232,255,0) 0%, rgba(213,232,255,0.73) 48%, #D5E8FF 100%);
+        border-radius: 0px 0px 10px 10px;
+        &>img{
+            width: 139px;
+            height: 39px;
+            cursor: pointer;
+            &:active{
+                opacity: 0.8;
+            }
+            &:first-child{
+                margin-right: 10px;
+            }
+        }
+    }
+}
+.tabPopoverBox{
+    width: 120px;
+    background: #FFFFFF;
+    box-shadow: 0px 0px 18px 0px rgba(0,0,0,0.15);
+    border-radius: 8px;
+    padding: 8px;
+    .tabPopover{
+        cursor: pointer;
+        height: 32px;
+        border-radius: 4px;
+        font-weight: 400;
+        font-size: 14px;
+        color: #323233;
+        line-height: 32px;
+        text-align: center;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        &.active{
+            background: #EEF8FF;
+            color: #1CACF1;
+            font-weight: 500;
+        }
+    }
 }

+ 2 - 2
src/page-instrument/component/the-music-list/index.tsx

@@ -9,7 +9,7 @@ import { getQuery } from "/src/utils/queryString";
 
 const query: any = getQuery();
 export const isMusicList = computed(()=>{
-	return !(query.workRecord || query.modelType || state.platform === IPlatform.PC || query.isCbs)
+	return !(query.workRecord || query.isCbs)
 })
 export const musicListShow = ref(false)
 export default defineComponent({
@@ -18,7 +18,7 @@ export default defineComponent({
 		return () => (
 			<>
 				<Popup class={styles.popup} position="left" v-model:show={musicListShow.value} round overlay-style={{background:'rgba(0, 0, 0, 0.3)'}}>
-					<div class={[styles.tabs, styles[state.modeType]]}>
+					<div class={[styles.tabs, styles[state.modeType], state.platform === IPlatform.PC && styles.isPc]}>
 						<Tabs>
 							<Tab title="其他曲谱">
 								<List />

+ 44 - 27
src/page-instrument/component/the-music-list/list.tsx

@@ -1,13 +1,19 @@
-import { defineComponent, onMounted, reactive, watch } from "vue";
+import { defineComponent, onMounted, reactive, watch, ref} from "vue";
 import styles from "./index.module.less";
 import { api_musicSheetPage } from "../../api";
-import state, { togglePlay } from "/src/state";
-import { List, Image, Field, DropdownMenu, DropdownItem } from "vant";
+import state, { togglePlay, IPlatform } from "/src/state";
+import { List, Image, Field, DropdownMenu, DropdownItem, Popup } from "vant";
 import { postMessage } from "/src/utils/native-message";
 import qs from "query-string";
 import searImg from "./imgs/searImg.png"
 import huoimg from "./imgs/huo.png"
 import emptyImg from "./imgs/empty.png"
+import xiangImg from "./imgs/xiang.png"
+import FilterList from "./filterList"
+import { getQuery } from "/src/utils/queryString";
+import Dragbom from "/src/view/plugins/useDrag/dragbom";
+import { storeData } from "/src/store";
+import useDrag from "/src/view/plugins/useDrag/index";
 
 export default defineComponent({
   name: "TheMusicList-list",
@@ -18,6 +24,7 @@ export default defineComponent({
     },
   },
   setup(props) {
+    const query: any = getQuery();
     const forms = reactive({
       name: "",
       page: 1,
@@ -25,7 +32,10 @@ export default defineComponent({
       musicSheetCategoriesId: state.bizMusicCategoryId,
       recentFlag: props.recentFlag ? true : null,
       excludeMusicId: props.recentFlag ? null : state.examSongId,
-      audioPlayTypes:""
+      audioPlayTypes: [],
+      musicTutorialIds: "",
+      musicTagIds: "",
+      musicalInstrumentId: query.instrumentId || ""
     });
     const data = reactive({
       isFocus: false,
@@ -34,22 +44,12 @@ export default defineComponent({
       loading: false,
       hasNext: true,
     });
-    const audioPlayTypesOption = [
-      { text: '全部场景', value: "" },
-      { text: '演奏', value: "PLAY" },
-      { text: '演唱', value: "SING" },
-      { text: '演奏+演唱', value: "PLAY,SING" },
-    ]
+    const filterShow = ref(false)
     const getList = async () => {
       if (!data.hasNext) return;
       data.loading = true;
       try {
-        const res = await api_musicSheetPage({
-          ...forms,
-          ...{
-            audioPlayTypes: forms.audioPlayTypes ? forms.audioPlayTypes.split(",") : []
-          }
-        });
+        const res = await api_musicSheetPage(forms);
         if (res?.code === 200 && Array.isArray(res.data?.rows)) {
           data.list = [...data.list, ...res.data.rows];
         }
@@ -91,26 +91,32 @@ export default defineComponent({
           type: "fullscreen",
         },
       });
-      location.href =
-        location.origin +
-        location.pathname +
-        "?" +
-        qs.stringify({
-          id: item.id,
-          _t: Date.now(),
-        });
+      let href = location.href.replace(/id=\d+/, `id=${item.id}`); // 替换id
+      href = href.replace(/instrumentId=\d+/, `instrumentId=${forms.musicalInstrumentId}`); // 替换乐器
+      location.href = href
+      location.reload()
     };
     function formatNumber(num:number) {
       return num >= 10000 
           ? (num / 10000).toFixed(1).replace(/\.0$/, '') + "万" 
           : num.toString();
     }
+    // 拖动
+    const parentClassName = "musicListClass_drag";
+		const userId = storeData.user?.id ? String(storeData.user?.id) : "";
+		const positionInfo =
+        state.platform !== IPlatform.PC
+        ? {
+            styleDrag: { value: null },
+          }
+        : useDrag([`${parentClassName} .top_draging`, `${parentClassName} .bom_drag`], parentClassName, filterShow, userId);
     return () => (
       <div class={styles.wrap}>
         <div class={[styles.searchBox,data.isFocus && styles.isFocus]}>
-          <DropdownMenu class={[styles.dropdownMenu]} overlay={false}>
-							<DropdownItem onChange={handleQuery} v-model={forms.audioPlayTypes} options={audioPlayTypesOption}/>
-					</DropdownMenu>
+          <div class={styles.dropdownMenu} onClick={() => { filterShow.value = true }}>
+            <div>筛选</div>
+            <img src={xiangImg} />
+          </div>
           <img src={searImg} />
           <Field placeholder="请输入曲目名称" v-model={forms.name} autocomplete="off" onFocus={()=>{ data.isFocus = true }} onBlur={()=>{ data.isFocus = false }} />
           <div class={styles.searchBtn} onClick={handleQuery}>搜索</div>
@@ -152,6 +158,17 @@ export default defineComponent({
                   <span>暂无内容</span>
                 </div>}
         </List>
+        <Popup  style={positionInfo.styleDrag.value} v-model:show={filterShow.value} class="popup-custom van-scale center-closeBtn musicListClass_drag" transition="van-scale" teleport="body" overlay-style={{ background: "rgba(0, 0, 0, 0.3)" }}>
+          <FilterList onClose={() => { filterShow.value = false }} onHandleConfirm={(queryObj) => {
+            filterShow.value = false
+            forms.audioPlayTypes = queryObj.audioPlayTypes
+            forms.musicTutorialIds = queryObj.musicTutorialIds
+            forms.musicTagIds = queryObj.musicTagIds
+            forms.musicalInstrumentId = queryObj.musicalInstrumentId
+            handleQuery()
+          }}></FilterList>
+          {state.platform === IPlatform.PC && <Dragbom />}
+        </Popup>
       </div>
     );
   },

BIN
src/page-instrument/custom-plugins/guide-driver/images/practise/d11.png


BIN
src/page-instrument/custom-plugins/guide-driver/images/practise/d7.png


BIN
src/page-instrument/custom-plugins/guide-driver/images/report/r2.png


BIN
src/page-instrument/custom-plugins/guide-driver/images/report/r3.png


+ 75 - 12
src/page-instrument/custom-plugins/guide-driver/index.less

@@ -4,22 +4,27 @@
 }
 
 .popoverClass .driver-popover-next-btn {
-  width: 100px;
+  width: 102px;
   height: 34px;
   text-shadow: none;
   border: none;
   font-weight: 600;
   font-size: 13px;
-  color: #006ed1;
+  color: #006ed1 !important;
   text-align: center;
   position: absolute;
   background: url("./images/btn-next.png") no-repeat center transparent;
   background-size: contain;
   background-color: transparent !important;
+  padding: 0;
+  font-family: inherit;
 }
+
 .popoverClass .driver-popover-prev-btn {
   font-weight: 600;
   font-size: 13px;
+  padding: 0;
+  font-family: inherit;
 }
 
 .popoverClass .driver-popover-next-btn:hover,
@@ -95,6 +100,18 @@
   }
 }
 
+.popoverClass11 {
+  width: 264px;
+  height: 245px;
+  background: url("./images/practise/d11.png") no-repeat center;
+  background-size: contain;
+
+  .driver-popover-next-btn {
+    right: 24px;
+    bottom: 23px;
+  }
+}
+
 .popoverClass4 {
   width: 265px;
   height: 245px;
@@ -155,14 +172,37 @@
 }
 
 .popoverClass7 {
-  width: 267px;
-  height: 221px;
+  width: 306px;
+  height: 167px;
   background: url("./images/practise/d7.png") no-repeat center;
   background-size: contain;
 
   .driver-popover-next-btn {
     right: 14px;
-    bottom: 18px;
+    bottom: -18px;
+  }
+
+  &.popoverClose {
+
+    .driver-popover-navigation-btns {
+      position: absolute;
+      bottom: -18px;
+      left: 0;
+      right: 15px;
+      justify-content: flex-start;
+    }
+
+    .driver-popover-next-btn {
+      // right: 14px;
+      // bottom: 18px;
+      position: relative;
+      top: 0;
+      right: 0;
+    }
+
+    .driver-popover-prev-btn {
+      margin-left: 14px;
+    }
   }
 }
 
@@ -176,6 +216,29 @@
     right: 14px;
     bottom: 18px;
   }
+
+  &.popoverClose {
+
+    .driver-popover-navigation-btns {
+      position: absolute;
+      bottom: 23px;
+      left: 0;
+      right: 15px;
+      justify-content: flex-start;
+    }
+
+    .driver-popover-next-btn {
+      // right: 14px;
+      // bottom: 18px;
+      position: relative;
+      top: 0;
+      right: 0;
+    }
+
+    .driver-popover-prev-btn {
+      margin-left: 14px;
+    }
+  }
 }
 
 .popoverClass8 {
@@ -205,8 +268,8 @@
 }
 
 .popoverClass10 {
-  width: 264px;
-  height: 245px;
+  width: 257px;
+  height: 213px;
   background: url("./images/practise/d10.png") no-repeat center;
   background-size: contain;
 
@@ -241,7 +304,7 @@
   .driver-popover-prev-btn {
     border: 1px solid #fff;
     border-radius: 100px;
-    color: #fff;
+    color: #fff !important;
     background-color: transparent;
     font-weight: 400;
     text-shadow: none;
@@ -448,7 +511,7 @@
 
 .popoverClassReport1 {
   width: 270px;
-  height: 143px;
+  height: 142px;
   background: url("./images/report/r1.png") no-repeat center;
   background-size: contain;
 
@@ -460,7 +523,7 @@
 
 .popoverClassReport2 {
   width: 270px;
-  height: 143px;
+  height: 142px;
   background: url("./images/report/r2.png") no-repeat center;
   background-size: contain;
 
@@ -491,7 +554,7 @@
 
 .popoverClassReport3 {
   width: 270px;
-  height: 143px;
+  height: 142px;
   background: url("./images/report/r3.png") no-repeat center;
   background-size: contain;
 
@@ -522,7 +585,7 @@
 
 .popoverClassReport4 {
   width: 270px;
-  height: 143px;
+  height: 142px;
   background: url("./images/report/r5.png") no-repeat center;
   background-size: contain;
 

+ 223 - 118
src/page-instrument/custom-plugins/guide-driver/index.tsx

@@ -6,7 +6,8 @@ import { getGuidance, setGuidance } from "../guide-page/api";
 
 const endGuide = (guideInfo: any) => {
   try {
-    setGuidance({ guideTag: "guideInfo", guideValue: JSON.stringify(guideInfo) });
+    // setGuidance({ guideTag: "guideInfo", guideValue: JSON.stringify(guideInfo) });
+    localStorage.setItem("guideInfo", JSON.stringify(guideInfo));
   } catch (e) {
     console.log(e);
   }
@@ -16,6 +17,8 @@ const endGuide = (guideInfo: any) => {
  * 按钮状态
  */
 type ButtonStatus = {
+  /** 是否显示播放按钮 */
+  playBtnStatus?: Boolean;
   /** 声部状态 */
   subjectStatus?: Boolean;
   /** 练习模式 */
@@ -28,6 +31,8 @@ type ButtonStatus = {
   titleType?: String;
   /** 原声 true 范唱 false */
   originPlayType?: Boolean;
+  /** 是否显示原音 */
+  originBtnStatus?: Boolean;
 };
 
 /** 练习模式 */
@@ -54,6 +59,15 @@ export const PractiseDriver = defineComponent({
 
     const driverOptions = (): Config => {
       let length = 10;
+
+      if (!props.statusAll.playBtnStatus) {
+        length -= 1;
+      }
+
+      if (!props.statusAll.originBtnStatus) {
+        length -= 1;
+      }
+
       // 显示指法
       if (!state.setting.displayFingering) {
         length -= 1;
@@ -80,7 +94,7 @@ export const PractiseDriver = defineComponent({
           length -= 1;
         }
       }
-      console.log(props.statusAll, "statusAll", length);
+      console.log(props.statusAll, "statusAll", length, state.setting.displayFingering);
 
       let options: Config = {
         showProgress: false,
@@ -96,25 +110,27 @@ export const PractiseDriver = defineComponent({
         onHighlighted: () => {
           driverNextStatus.value = false;
         },
-        steps: [
-          {
-            element: ".driver-1",
-            popover: {
-              title: "",
-              description: "",
-              popoverClass: "popoverClass popoverClass1",
-              align: "end",
-              side: "top",
-              nextBtnText: `下一步 (1/${length})`,
-              showButtons: ["next"],
-              onPopoverRender: (popover: PopoverDOM, options: { config: Config; state: State }) => {
-                options.config.stageRadius = 1000;
-                options.config.stagePadding = 0;
-              },
+        steps: [] as DriveStep[],
+      };
+
+      if (props.statusAll.playBtnStatus) {
+        options.steps?.push({
+          element: ".driver-1",
+          popover: {
+            title: "",
+            description: "",
+            popoverClass: "popoverClass popoverClass1",
+            align: "end",
+            side: "top",
+            nextBtnText: `下一步 (1/${length})`,
+            showButtons: ["next"],
+            onPopoverRender: (popover: PopoverDOM, options: { config: Config; state: State }) => {
+              options.config.stageRadius = 1000;
+              options.config.stagePadding = 0;
             },
           },
-        ] as DriveStep[],
-      };
+        });
+      }
 
       if (props.statusAll.playType) {
         options.steps?.push({
@@ -134,8 +150,8 @@ export const PractiseDriver = defineComponent({
         });
       }
 
-      options.steps?.push(
-        {
+      if (props.statusAll.originBtnStatus) {
+        options.steps?.push({
           element: ".driver-3",
           popover: {
             title: "",
@@ -149,7 +165,9 @@ export const PractiseDriver = defineComponent({
               driverInitialPosition(popover, options);
             },
           },
-        },
+        });
+      }
+      options.steps?.push(
         {
           element: ".driver-4",
           popover: {
@@ -158,7 +176,7 @@ export const PractiseDriver = defineComponent({
             popoverClass: "popoverClass popoverClass4",
             align: "start",
             side: "top",
-            nextBtnText: `下一步 (${options.steps.length + 2}/${length})`,
+            nextBtnText: `下一步 (${options.steps.length + 1}/${length})`,
             showButtons: ["next"],
             onPopoverRender: (popover: PopoverDOM, options: { config: Config; state: State }) => {
               driverInitialPosition(popover, options);
@@ -173,7 +191,7 @@ export const PractiseDriver = defineComponent({
             popoverClass: "popoverClass popoverClass5",
             align: "start",
             side: "top",
-            nextBtnText: `下一步 (${options.steps.length + 3}/${length})`,
+            nextBtnText: `下一步 (${options.steps.length + 2}/${length})`,
             showButtons: ["next"],
             onPopoverRender: (popover: PopoverDOM, options: { config: Config; state: State }) => {
               driverInitialPosition(popover, options);
@@ -266,23 +284,46 @@ export const PractiseDriver = defineComponent({
           });
         }
       } else {
-        options.steps?.push({
-          element: ".driver-6",
-          popover: {
-            title: "",
-            description: "",
-            popoverClass: "popoverClass popoverClass6",
-            align: "start",
-            side: "top",
-            nextBtnText: `下一步 (${options.steps.length + 1}/${length})`, //"下一步6/" + length,
-            showButtons: ["next"],
-            onPopoverRender: (popover: PopoverDOM, options: { config: Config; state: State }) => {
-              driverInitialPosition(popover, options);
+        // 判断设置之后是否还有引导
+        if (!state.setting.displayFingering && !props.statusAll.backTitle && !props.statusAll.modelTypeStatus) {
+          options.steps?.push({
+            element: ".driver-6",
+            popover: {
+              title: "",
+              description: "",
+              popoverClass: "popoverClass popoverClass6 popoverClose",
+              align: "start",
+              side: "top",
+              prevBtnText: "再看一遍",
+              doneBtnText: "完成",
+              showButtons: ["next", "previous"],
+              onPopoverRender: (popover: PopoverDOM, options: { config: Config; state: State }) => {
+                driverInitialPosition(popover, options);
+              },
+              onPrevClick: () => {
+                driverObj.drive(0);
+              },
+              onNextClick: () => {
+                onDriverClose();
+              },
             },
-          },
-        });
-
-        if (state.setting.displayFingering) {
+          });
+        } else if (state.setting.displayFingering && !props.statusAll.backTitle && !props.statusAll.modelTypeStatus) {
+          options.steps?.push({
+            element: ".driver-6",
+            popover: {
+              title: "",
+              description: "",
+              popoverClass: "popoverClass popoverClass6",
+              align: "start",
+              side: "top",
+              nextBtnText: `下一步 (${options.steps.length + 1}/${length})`, //"下一步6/" + length,
+              showButtons: ["next"],
+              onPopoverRender: (popover: PopoverDOM, options: { config: Config; state: State }) => {
+                driverInitialPosition(popover, options);
+              },
+            },
+          });
           // 是否有指法图
           // 乐器方向不一样引导位置不一样
           options.steps?.push({
@@ -290,22 +331,61 @@ export const PractiseDriver = defineComponent({
             popover: {
               title: "",
               description: "",
-              popoverClass: `popoverClass ${state.fingeringInfo.direction === "transverse" ? "popoverClass7" : "popoverClass7-1"}`,
+              popoverClass: `popoverClass ${state.fingeringInfo.direction === "transverse" ? "popoverClass7" : "popoverClass7-1"} popoverClose`,
               align: state.fingeringInfo.direction === "transverse" ? "start" : "center",
               side: state.fingeringInfo.direction === "transverse" ? "top" : "left",
-              nextBtnText: `下一步 (${options.steps?.length + 1}/${length})`,
-              showButtons: ["next"],
+              prevBtnText: "再看一遍",
+              doneBtnText: "完成",
+              showButtons: ["next", "previous"],
               onPopoverRender: (popover: PopoverDOM, options: { config: Config; state: State }) => {
                 if (state.fingeringInfo.direction === "transverse") driverInitialPosition(popover, options);
               },
-              onCloseClick: () => {
+              onPrevClick: () => {
+                driverObj.drive(0);
+              },
+              onNextClick: () => {
                 onDriverClose();
               },
             },
           });
-        }
-
-        if (!props.statusAll.modelTypeStatus) {
+        } else if (props.statusAll.backTitle && !props.statusAll.modelTypeStatus) {
+          options.steps?.push({
+            element: ".driver-6",
+            popover: {
+              title: "",
+              description: "",
+              popoverClass: "popoverClass popoverClass6",
+              align: "start",
+              side: "top",
+              nextBtnText: `下一步 (${options.steps.length + 1}/${length})`, //"下一步6/" + length,
+              showButtons: ["next"],
+              onPopoverRender: (popover: PopoverDOM, options: { config: Config; state: State }) => {
+                driverInitialPosition(popover, options);
+              },
+            },
+          });
+          if (state.setting.displayFingering) {
+            // 是否有指法图
+            // 乐器方向不一样引导位置不一样
+            options.steps?.push({
+              element: ".driver-7",
+              popover: {
+                title: "",
+                description: "",
+                popoverClass: `popoverClass ${state.fingeringInfo.direction === "transverse" ? "popoverClass7" : "popoverClass7-1"}`,
+                align: state.fingeringInfo.direction === "transverse" ? "start" : "center",
+                side: state.fingeringInfo.direction === "transverse" ? "top" : "left",
+                nextBtnText: `下一步 (${options.steps?.length + 1}/${length})`,
+                showButtons: ["next"],
+                onPopoverRender: (popover: PopoverDOM, options: { config: Config; state: State }) => {
+                  if (state.fingeringInfo.direction === "transverse") driverInitialPosition(popover, options);
+                },
+                onCloseClick: () => {
+                  onDriverClose();
+                },
+              },
+            });
+          }
           options.steps?.push({
             //props.statusAll.titleType === "TEXT" ? ".driver-8 .van-notice-bar__content" :
             element: ".driver-8",
@@ -340,8 +420,47 @@ export const PractiseDriver = defineComponent({
             },
           });
         } else {
-          options.steps?.push(
-            {
+          options.steps?.push({
+            element: ".driver-6",
+            popover: {
+              title: "",
+              description: "",
+              popoverClass: "popoverClass popoverClass6",
+              align: "start",
+              side: "top",
+              nextBtnText: `下一步 (${options.steps.length + 1}/${length})`, //"下一步6/" + length,
+              showButtons: ["next"],
+              onPopoverRender: (popover: PopoverDOM, options: { config: Config; state: State }) => {
+                driverInitialPosition(popover, options);
+              },
+            },
+          });
+
+          if (state.setting.displayFingering) {
+            // 是否有指法图
+            // 乐器方向不一样引导位置不一样
+            options.steps?.push({
+              element: ".driver-7",
+              popover: {
+                title: "",
+                description: "",
+                popoverClass: `popoverClass ${state.fingeringInfo.direction === "transverse" ? "popoverClass7" : "popoverClass7-1"}`,
+                align: state.fingeringInfo.direction === "transverse" ? "start" : "center",
+                side: state.fingeringInfo.direction === "transverse" ? "top" : "left",
+                nextBtnText: `下一步 (${options.steps?.length + 1}/${length})`,
+                showButtons: ["next"],
+                onPopoverRender: (popover: PopoverDOM, options: { config: Config; state: State }) => {
+                  if (state.fingeringInfo.direction === "transverse") driverInitialPosition(popover, options);
+                },
+                onCloseClick: () => {
+                  onDriverClose();
+                },
+              },
+            });
+          }
+
+          if (props.statusAll.backTitle) {
+            options.steps?.push({
               //  .van-notice-bar__content
               // element: ".driver-8 .van-notice-bar__content",
               // props.statusAll.titleType === "TEXT" ? ".driver-8 .van-notice-bar__content" :
@@ -368,35 +487,36 @@ export const PractiseDriver = defineComponent({
                   } catch {}
                 },
               },
-            },
-            {
-              element: ".driver-9",
-              popover: {
-                title: "",
-                description: "",
-                popoverClass: "popoverClass popoverClass9 popoverClose",
-                align: "end",
-                side: "bottom",
-                prevBtnText: "再看一遍",
-                doneBtnText: "完成",
-                showButtons: ["next", "previous"],
-                onPopoverRender: (popover: PopoverDOM, options: { config: Config; state: State }) => {
-                  options.config.stageRadius = 1000;
-                  options.config.stagePadding = 0;
-                  try {
-                    const rect = options.state.activeElement?.getBoundingClientRect();
-                    popover.wrapper.style.marginLeft = -((rect?.width || 0) / 2 - 8) + "px";
-                  } catch {}
-                },
-                onPrevClick: () => {
-                  driverObj.drive(0);
-                },
-                onNextClick: () => {
-                  onDriverClose();
-                },
+            });
+          }
+
+          options.steps?.push({
+            element: ".driver-9",
+            popover: {
+              title: "",
+              description: "",
+              popoverClass: "popoverClass popoverClass9 popoverClose",
+              align: "end",
+              side: "bottom",
+              prevBtnText: "再看一遍",
+              doneBtnText: "完成",
+              showButtons: ["next", "previous"],
+              onPopoverRender: (popover: PopoverDOM, options: { config: Config; state: State }) => {
+                options.config.stageRadius = 1000;
+                options.config.stagePadding = 0;
+                try {
+                  const rect = options.state.activeElement?.getBoundingClientRect();
+                  popover.wrapper.style.marginLeft = -((rect?.width || 0) / 2 - 8) + "px";
+                } catch {}
+              },
+              onPrevClick: () => {
+                driverObj.drive(0);
+              },
+              onNextClick: () => {
+                onDriverClose();
               },
-            }
-          );
+            },
+          });
         }
       }
 
@@ -421,15 +541,12 @@ export const PractiseDriver = defineComponent({
     const showCloseBtn = ref(false);
     const getAllGuidance = async () => {
       try {
-        if (state.guideInfo) {
-          guideInfo.value = state.guideInfo;
+        // const res = await getGuidance({ guideTag: "guideInfo" });
+        const res = localStorage.getItem("guideInfo");
+        if (res) {
+          guideInfo.value = JSON.parse(res) || null;
         } else {
-          const res = await getGuidance({ guideTag: "guideInfo" });
-          if (res.data) {
-            guideInfo.value = JSON.parse(res.data?.guideValue) || null;
-          } else {
-            guideInfo.value = {};
-          }
+          guideInfo.value = {};
         }
 
         if (!(guideInfo.value && guideInfo.value.practiseDriver)) {
@@ -614,16 +731,13 @@ export const FollowDriver = defineComponent({
     const showCloseBtn = ref(false);
     const getAllGuidance = async () => {
       try {
-        if (state.guideInfo) {
-          guideInfo.value = state.guideInfo;
+        const res = localStorage.getItem("guideInfo");
+        if (res) {
+          guideInfo.value = JSON.parse(res) || null;
         } else {
-          const res = await getGuidance({ guideTag: "guideInfo" });
-          if (res.data) {
-            guideInfo.value = JSON.parse(res.data?.guideValue) || null;
-          } else {
-            guideInfo.value = {};
-          }
+          guideInfo.value = {};
         }
+
         if (!(guideInfo.value && guideInfo.value.followDriver)) {
           document.addEventListener("click", handleClickOutside, true);
           nextTick(() => {
@@ -822,16 +936,13 @@ export const EvaluatingDriver = defineComponent({
     const showCloseBtn = ref(false);
     const getAllGuidance = async () => {
       try {
-        if (state.guideInfo) {
-          guideInfo.value = state.guideInfo;
+        const res = localStorage.getItem("guideInfo");
+        if (res) {
+          guideInfo.value = JSON.parse(res) || null;
         } else {
-          const res = await getGuidance({ guideTag: "guideInfo" });
-          if (res.data) {
-            guideInfo.value = JSON.parse(res.data?.guideValue) || null;
-          } else {
-            guideInfo.value = {};
-          }
+          guideInfo.value = {};
         }
+
         console.log(guideInfo.value, "guideInfo.value", showCloseBtn.value);
         if (!(guideInfo.value && guideInfo.value.evaluatingDriver)) {
           document.addEventListener("click", handleClickOutside, true);
@@ -1025,16 +1136,13 @@ export const EvaluatingResultDriver = defineComponent({
     const showCloseBtn = ref(false);
     const getAllGuidance = async () => {
       try {
-        if (state.guideInfo) {
-          guideInfo.value = state.guideInfo;
+        const res = localStorage.getItem("guideInfo");
+        if (res) {
+          guideInfo.value = JSON.parse(res) || null;
         } else {
-          const res = await getGuidance({ guideTag: "guideInfo" });
-          if (res.data) {
-            guideInfo.value = JSON.parse(res.data?.guideValue) || null;
-          } else {
-            guideInfo.value = {};
-          }
+          guideInfo.value = {};
         }
+
         if (!(guideInfo.value && guideInfo.value.evaluatingResultDriver)) {
           setTimeout(() => {
             document.addEventListener("click", handleClickOutside, true);
@@ -1344,16 +1452,13 @@ export const EvaluatingReportDriver = defineComponent({
     const showCloseBtn = ref(false);
     const getAllGuidance = async () => {
       try {
-        if (state.guideInfo) {
-          guideInfo.value = state.guideInfo;
+        const res = localStorage.getItem("guideInfo");
+        if (res) {
+          guideInfo.value = JSON.parse(res) || null;
         } else {
-          const res = await getGuidance({ guideTag: "guideInfo" });
-          if (res.data) {
-            guideInfo.value = JSON.parse(res.data?.guideValue) || null;
-          } else {
-            guideInfo.value = {};
-          }
+          guideInfo.value = {};
         }
+
         if (!(guideInfo.value && guideInfo.value.evaluatingReportDriver)) {
           // 监听点击事件以实现点击空白区域跳转到下一步
           document.addEventListener("click", handleClickOutside, true);
@@ -1402,4 +1507,4 @@ export const EvaluatingReportDriver = defineComponent({
       </Teleport>
     );
   },
-});
+});

+ 4 - 2
src/page-instrument/custom-plugins/helper-model/recommendation/index.module.less

@@ -114,9 +114,11 @@
                         }
                     }
                     .recommendationDropdownItem{
-                        top: 106px !important;
-                        left: 14px;
+                        position: absolute !important; 
+                        top: 30px !important;
+                        left: -10px;
                         width: 172px;
+                        height: 182px;
                         .van-dropdown-item__content{
                             margin: 6px 0 0 10px;
                             width: 152px;

+ 1 - 1
src/page-instrument/custom-plugins/helper-model/recommendation/index.tsx

@@ -113,7 +113,7 @@ export default defineComponent({
 		}
 		return () => (
 			<div class={[styles.recommendation,styles[state.modeType]]}>
-				<div class={styles.head}>
+				<div class={[styles.head, "top_draging"]}>
 					<img class={styles.headTit} src={headImg("recommendationName.png")} />
 					<img class={styles.closeImg} src={headImg("closeImg.png")} onClick={()=>{ emit("close") }} />
 				</div>

+ 2 - 1
src/page-instrument/custom-plugins/work-index/index.tsx

@@ -24,11 +24,12 @@ export const HANDLE_WORK_ADD = () => {
 
 // 刷新谱面后,设置作业选段
 export const resetSection = () => {
+	console.log('重新设置选段1111')
 	if (data.trainingType === "PRACTICE"){
 		workHomeRef.value?.getWorkData();
 	}
 	if (data.trainingType === "EVALUATION") {
-		workHomeRef.value?.getWorkData();
+		workEvaluatRef.value?.getWorkData();
 	}
 	state.workSectionNeedReset = false;
 };

+ 21 - 14
src/page-instrument/evaluat-model/evaluat-result/index.tsx

@@ -49,7 +49,8 @@ export default defineComponent({
       /** 生成评测记录的时候,记录当前评测的谱面类型,用于评测报告默认展示的谱面类型 */
       evaluatingData.resultData.scoreData.musicType = state.musicRenderType;
       // 评测的速度,如果是选段,则选选段开头小节的速度
-      const evaluatSpeed = state.sectionStatus && state.section.length === 2 && state.section[0].measureSpeed ? state.section[0].measureSpeed * state.basePlayRate : state.speed;      
+      const evaluatSpeed = state.sectionStatus && state.section.length === 2 && state.section[0].measureSpeed ? state.section[0].measureSpeed * state.basePlayRate : state.speed;  
+      const rate = state.basePlayRate * state.originAudioPlayRate; // 播放倍率    
       const body = {
         deviceType: browser().android ? "ANDROID" : "IOS", // 设备类型
         intonation: evaluatingData.resultData.intonation, // 音准
@@ -68,7 +69,9 @@ export default defineComponent({
         playTime: evaluatingData.resultData.playTime / 1000, // 播放时长
         heardLevel: state.setting.evaluationDifficulty, // 听力等级
         recordFilePath: evaluatingData.resultData.url, // 录音文件路径
-        delFlag: evaluatingData.oneselfCancleEvaluating
+        delFlag: evaluatingData.oneselfCancleEvaluating,
+        instrumentId: state.instrumentId,
+        playRate: rate
       };
       data.saveLoading = true;
       const res = await api_musicPracticeRecordSave(body);
@@ -172,27 +175,31 @@ export default defineComponent({
                 <img src={zlycImg} class={[styles.ctrlsBtn, "evaluting-result-2"]} onClick={() => emit("close", "tryagain")} />
                 {evaluatingData.resultData.recordId ? (
                   <div class={styles.saveBtn}>
-                    <img src={noSaveTips.value ? bczpJzImg : bczpImg} class={[styles.ctrlsBtn, "evaluting-result-3"]} style={{ opacity: state.isHideEvaluatReportSaveBtn ? 0.4 : 1 }} onClick={() => {
-                      if (!noSaveTips.value && !state.isHideEvaluatReportSaveBtn) {
-                        saveResult()
-                      }
-                    }} />
-                    {
-                      noSaveTips.value && state.noSavePopShow ? 
+                    <img
+                      src={noSaveTips.value ? bczpJzImg : bczpImg}
+                      class={[styles.ctrlsBtn, "evaluting-result-3"]}
+                      style={{ opacity: state.isHideEvaluatReportSaveBtn ? 0.4 : 1 }}
+                      onClick={() => {
+                        if (!noSaveTips.value && !state.isHideEvaluatReportSaveBtn) {
+                          saveResult();
+                        }
+                      }}
+                    />
+                    {noSaveTips.value && state.noSavePopShow ? (
                       <div class={[styles.noSaveTip]}>
                         <span class={styles.arrowIcon}></span>
                         <span>{noSaveTips.value}</span>
-                        <i onClick={() => state.noSavePopShow = false}></i>
-                      </div> : null                   
-                    }
+                        <i onClick={() => (state.noSavePopShow = false)}></i>
+                      </div>
+                    ) : null}
                   </div>
-                ) : null }
+                ) : null}
                 <img src={ckzpImg} class={[styles.ctrlsBtn, "evaluting-result-4", data.saveLoading ? styles.disablued : ""]} onClick={() => emit("close", "look")} />
               </div>
             </div>
 
             {/* 评测模式-结果弹窗 功能引导 加载音频完成 不是会员 */}
-            {evaluatingData.resulstMode && !evaluatingData.hideResultModal && !evaluatingData.earphoneMode && !query.isCbs && state.audioDone && !state.isVip && <EvaluatingResultDriver />}
+            {evaluatingData.resulstMode && !evaluatingData.hideResultModal && !evaluatingData.earphoneMode && !query.isCbs && state.audioDone && !state.isVip && !data.saveLoading && <EvaluatingResultDriver saveBtn={evaluatingData.resultData.recordId ? true : false} />}
           </div>
         )}
       </>

+ 11 - 8
src/page-instrument/evaluat-model/index.tsx

@@ -156,7 +156,7 @@ export default defineComponent({
           checkErjiTimer = null;
           setTimeout(() => {
             evaluatingData.earphoneMode = false;
-          }, 3000);
+          }, 1500);
         } else {
           // 如果没有佩戴有限耳机,需要持续检测耳机状态
           checkErjiTimer = setTimeout(() => {
@@ -415,15 +415,18 @@ export default defineComponent({
       // 如果是异常状态,先等待500ms再执行后续流程
       if (evaluatingData.isErrorState && !state.setting.soundEffect) {
         // console.log('异常流程1')
-        showLoadingToast({
-          message: "处理中",
-          duration: 1000,
-          overlay: true,
-          overlayClass: styles.scoreMode,
-        });
+        // showLoadingToast({
+        //   message: "处理中",
+        //   duration: 1000,
+        //   overlay: true,
+        //   overlayClass: styles.scoreMode,
+        // });
+        state.loadingText = "处理中…";
+				state.isLoading = true;
         await new Promise<void>((resolve) => {
           setTimeout(() => {
-            closeToast();
+            // closeToast();
+            state.isLoading = false;
             evaluatingData.isErrorState = false;
             // console.log('异常流程2')
             resolve();

+ 4 - 3
src/page-instrument/follow-model/index.tsx

@@ -42,17 +42,18 @@ export default defineComponent({
           followData.isBeginMask && <div class={styles.beginMask}></div>
         }        
         <div class={[styles.operatingBtn, state.platform === IPlatform.PC && state.musicScoreBtnDirection === "left" ? styles.operatingLeft : ""]}>
-          {!followData.start && (
+          {(!followData.start && !followData.practiceStart) && (
             <img
               class={[styles.iconBtn, "follow-1"]}
               src={headImg("icon_play.png")}
               onClick={() => {
-                followData.start = true;
+                // followData.start = true;
+                followData.practiceStart = true;
                 handleFollowStart();
               }}
             />
           )}
-          {followData.start && (
+          {(followData.start || followData.practiceStart)&& (
             <>
               <img class={styles.iconBtn} src={headImg("icon_reset.png")} onClick={() => handleFollowEnd()} />
               <img class={styles.iconBtn} src={headImg("submit.png")} onClick={() => handleFollowEnd()} />

+ 156 - 84
src/page-instrument/header-top/index.module.less

@@ -6,7 +6,8 @@
     flex-shrink: 0;
     margin-left: calc(-1 * var(--detailDataPaddingLeft));
     padding: 0 30px;
-    background: linear-gradient( 180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0.6) 100%);
+    background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.6) 100%);
+
     &.headerTopRight {
         justify-content: flex-end;
     }
@@ -25,59 +26,68 @@
     left: 30px;
     bottom: 20px;
     border-radius: 16px;
-    background-color: rgba(12,51,107,0.61);
+    background-color: rgba(12, 51, 107, 0.61);
     padding: 6px 11px;
     align-items: center;
     display: flex;
     opacity: 0;
     transition: all .3s ease-in;
-    & > div{
+
+    &>div {
         margin-left: 4px;
         font-weight: 500;
         font-size: 14px;
         line-height: 20px;
-        color: rgba(255,255,255,0.7);
-    }    
-    & > img{
+        color: rgba(255, 255, 255, 0.7);
+    }
+
+    &>img {
         width: 18px;
         height: 18px;
     }
+
     &.modeWarnRight {
         left: inherit;
         right: 30px;
     }
 }
-.headTopLeftBox{
+
+.headTopLeftBox {
     position: fixed;
     top: 20px;
     left: 30px;
     display: flex;
     align-items: center;
-    .img{
+
+    .img {
         width: 32px;
         height: 32px;
     }
-    .listImg{
+
+    .listImg {
         margin-left: 16px;
     }
-    .title{
-        width: 216px;
+
+    .title {
+        width: 210px;
         margin-left: 10px;
         position: relative;
-        .symbolNote{
+
+        .symbolNote {
             max-width: calc(216px + 16px);
             position: absolute;
             top: 0;
             left: 0;
             content: "";
-            width: calc(var(--noticeBarWidth,100%) + 16px);
+            width: calc(var(--noticeBarWidth, 100%) + 16px);
             height: 100%;
             background: url("./image/sj.png") no-repeat;
             background-size: 9px 6px;
-            background-position: center right;                   
+            background-position: center right;
         }
-        :global{
-            .van-notice-bar{
+
+        :global {
+            .van-notice-bar {
                 height: 30px;
                 line-height: 30px;
                 padding: 0;
@@ -87,12 +97,22 @@
             }
         }
     }
+    
+    .blackTitle {
+        :global{
+            .van-notice-bar{
+                color: #000 !important;
+            }
+        }
+    }
+
     .hidenBack {
         opacity: 0;
         pointer-events: none;
     }
 }
 .modeChangeBox{
+    cursor: pointer;
     position: fixed;
     top: 20px;
     right: 30px;
@@ -102,17 +122,20 @@
     display: flex;
     align-items: center;
     padding: 0 10px;
-    .img{
+
+    .img {
         width: 18px;
         height: 18px;
     }
-    .title{
+
+    .title {
         margin-left: 6px;
         font-weight: 500;
         font-size: 14px;
         color: #FFFFFF;
     }
 }
+
 .headRight {
     display: flex;
     align-items: center;
@@ -126,13 +149,16 @@
         align-items: center;
         cursor: pointer;
         margin-right: 30px;
-        &:last-child{
+
+        &:last-child {
             margin-right: 0;
         }
+
         .iconBtn {
             width: 24px;
             height: 24px;
         }
+
         span {
             margin-top: 3px;
             font-weight: 500;
@@ -140,97 +166,130 @@
             color: #FFFFFF;
             line-height: 17px;
         }
-        &:active{
-            >span{
+
+        &:active {
+            >span {
                 color: #34D6FF
-            };
+            }
+
+            ;
         }
-        &.playType:active{
-            >img:nth-child(1){
+
+        &.playType:active {
+            >img:nth-child(1) {
                 content: url("./image/performAct.png");
-            }            
-            >img:nth-child(2){
+            }
+
+            >img:nth-child(2) {
                 content: url("./image/singAct.png");
             }
         }
-        &.playSource:active{
-            >img:nth-child(1){
+
+        &.playSource:active {
+            >img:nth-child(1) {
                 content: url("./image/musicAct.png");
-            }            
-            >img:nth-child(2){
+            }
+
+            >img:nth-child(2) {
                 content: url("./image/backgroundAct.png");
-            }            
-        }      
-        &.songSource:active{
-            >img:nth-child(1){
+            }
+        }
+
+        &.songSource:active {
+            >img:nth-child(1) {
                 content: url("./image/music1Act.png");
-            }            
-            >img:nth-child(2){
+            }
+
+            >img:nth-child(2) {
                 content: url("./image/background1Act.png");
-            }            
-            >img:nth-child(3){
+            }
+
+            >img:nth-child(3) {
                 content: url("./image/mingsongAct.png");
             }
         }
-        &.section:active{
-            >img{
+
+        &.section:active {
+            >img {
                 content: url("./image/section2.png");
             }
         }
-        &.isSection{
-            >span{
+
+        &.isSection {
+            >span {
                 color: #34D6FF
-            };
+            }
+
+            ;
         }
-        &.speed:active{
-            >img:nth-child(1){
+
+        &.speed:active {
+            >img:nth-child(1) {
                 content: url("./image/tickonAct.png");
-            }            
-            >img:nth-child(2){
+            }
+
+            >img:nth-child(2) {
                 content: url("./image/tickoffAct.png");
-            } 
+            }
         }
-        &.isSpeed{
-            >img:nth-child(1){
+
+        &.isSpeed {
+            >img:nth-child(1) {
                 content: url("./image/tickonAct.png");
-            }            
-            >img:nth-child(2){
+            }
+
+            >img:nth-child(2) {
                 content: url("./image/tickoffAct.png");
-            } 
-            >span{
+            }
+
+            >span {
                 color: #34D6FF
-            }; 
-        }      
-        &.settingMode:active{
-            >img{
+            }
+
+            ;
+        }
+
+        &.settingMode:active {
+            >img {
                 content: url("./image/icon_menuAct.png");
-            }            
-        }  
-        &.isSettingMode{
-            >img{
+            }
+        }
+
+        &.isSettingMode {
+            >img {
                 content: url("./image/icon_menuAct.png");
-            }            
-            >span{
+            }
+
+            >span {
                 color: #34D6FF
-            }; 
-        }  
-        &.musicSheet:active{
-            >img{
+            }
+
+            ;
+        }
+
+        &.musicSheet:active {
+            >img {
                 content: url("./image/shengguiAct.png");
-            }            
-        }       
-        &.isMusicSheet{
-            >img{
+            }
+        }
+
+        &.isMusicSheet {
+            >img {
                 content: url("./image/shengguiAct.png");
-            }            
-            >span{
+            }
+
+            >span {
                 color: #34D6FF
-            }; 
+            }
+
+            ;
         }
     }
-    .metronomeBtn{
+
+    .metronomeBtn {
         position: relative;
-        .speedCon{
+
+        .speedCon {
             transform: scale(0.83);
             transform-origin: left bottom;
             padding: 2px;
@@ -243,11 +302,13 @@
             background: #FFC121;
             border-radius: 120px 120px 120px 1px;
             border: 1px solid #FFFFFF;
-            >img{
+
+            >img {
                 width: 15px;
                 height: 11px;
             }
-            >div{
+
+            >div {
                 margin-left: 1px;
                 font-weight: 600;
                 font-size: 12px;
@@ -264,10 +325,12 @@
 }
 
 .playBtn {
+    cursor: pointer;
     position: fixed;
     right: 30px;
     bottom: 12px;
     transition: bottom .2s ease;
+
     .btnWrap {
         width: 50px;
         height: 50px;
@@ -278,6 +341,7 @@
             height: 100%;
         }
     }
+
     &.playLeftButton {
         left: 30px !important;
         right: auto !important;
@@ -289,7 +353,7 @@
         left: auto !important;
         bottom: 12px !important;
     }
-    
+
     .progress {
         position: absolute;
         left: 50%;
@@ -301,6 +365,7 @@
 }
 
 .resetBtn {
+    cursor: pointer;
     position: fixed;
     right: 100px;
     bottom: 12px;
@@ -351,11 +416,13 @@
     background: url(./image/bg.png) no-repeat;
     background-size: cover;
     transition: all .3s;
-    &.hidden{
+
+    &.hidden {
         opacity: 0;
         transform: translateY(100%);
         pointer-events: none;
     }
+
     .back {
         position: absolute;
         width: 38px;
@@ -364,6 +431,7 @@
         top: 17px;
         cursor: pointer;
     }
+
     .name {
         position: absolute;
         left: 50%;
@@ -372,6 +440,7 @@
         width: 87px;
         height: 21px;
     }
+
     .modeBox {
         width: 100%;
         display: flex;
@@ -380,13 +449,16 @@
         position: relative;
         top: 50%;
         transform: translateY(-50%);
-        &.twoModeBox{
+
+        &.twoModeBox {
             justify-content: center;
-            > .modeImg + .modeImg{
+
+            >.modeImg+.modeImg {
                 margin-left: 150px;
             }
         }
         > .modeImg {
+            cursor: pointer;
             width: calc((100% - 2*40px)/3);
             max-width: 220px;
         }
@@ -400,6 +472,6 @@
     opacity: 0;
 }
 
-.socketErrorStatus{
+.socketErrorStatus {
     top: 20vh;
 }

+ 157 - 89
src/page-instrument/header-top/index.tsx

@@ -9,7 +9,7 @@ import { Badge, Circle, Popover, Popup, showConfirmDialog, showToast, NoticeBar
 import Speed from "./speed";
 import { evaluatingData, handleStartEvaluat } from "/src/view/evaluating";
 import Settting from "./settting";
-import state, { IPlatform, handleChangeSection, handleResetPlay, handleRessetState, togglePlay, IPlayState, refreshMusicSvg } from "/src/state";
+import state, { IPlatform, handleChangeSection, handleResetPlay, handleRessetState, togglePlay, IPlayState, refreshMusicSvg, EnumMusicRenderType } from "/src/state";
 import { getAudioCurrentTime } from "/src/view/audio-list";
 import { followData, toggleFollow } from "/src/view/follow-practice";
 import { api_back } from "/src/helpers/communication";
@@ -77,10 +77,10 @@ export const headTopData = reactive({
         return;
       }
       // 评测模式,只有一行谱模式
-      if (!state.isSingleLine) {
-        state.isSingleLine = true;
-        refreshMusicSvg();
-      }
+      // if (!state.isSingleLine) {
+      //   state.isSingleLine = true;
+      //   refreshMusicSvg();
+      // }
       smoothAnimationState.isShow.value = false; // 隐藏旋律线
       state.playIngSpeed = state.originSpeed;
       handleStartEvaluat();
@@ -102,7 +102,7 @@ export const headTopData = reactive({
   // 改变模式之前的状态
   oldPlayType: "play",
   // 记录切换模式前的状态
-  oldModeType: "practise" as "practise" | "follow" | "evaluating"
+  oldModeType: "practise" as "practise" | "follow" | "evaluating",
 });
 
 export const headData = reactive({
@@ -115,7 +115,7 @@ let resetBtn: ComputedRef<{
   disabled: boolean;
 }>;
 // 点击切换的时候才触发提醒
-let isClickMode = false
+let isClickMode = false;
 /**
  * 处理模式切换
  * @param oldPlayType   没改变之前的播放模式
@@ -138,13 +138,13 @@ export function handlerModeChange(oldPlayType: "play" | "sing", oldPlaySource: I
     resetBtn && (resetBtn.value.display = false);
   }
   // 当模式改变的时候  放在这里是因为需要等谱面加载完成之后再提示(点击按钮模式切换才提示)
-  if(isClickMode){
+  if (isClickMode) {
     showToast({
       message: state.playType === "play" ? "已切换为演奏场景" : "已切换为演唱场景",
       position: "top",
       className: "selectionToast",
     });
-    isClickMode = false
+    isClickMode = false;
   }
 }
 // 模式切换之后重新给times赋值
@@ -156,7 +156,7 @@ function modeChangeHandleTimes(oldPlayType: "play" | "sing", oldPlaySource: IPla
   // 演奏向演唱切
   if (oldPlayType === "play" && playType === "sing") {
     if (playSource === "mingSong") {
-      // 唱名文件也要加上弱起时间  他们制作曲子加了弱起时间
+      // 唱名文件也要加上弱起时间  他们制作曲子加了弱起时间  注意这修改了之后给总控平台的时值也需要改
       state.fixtime = difftime;
       state.times.map((item) => {
         item.time = item.xmlNoteTime + difftime;
@@ -228,7 +228,7 @@ function modeChangeHandleTimes(oldPlayType: "play" | "sing", oldPlaySource: IPla
     // 演唱之间切换
     // 切到唱名时候
     if (playSource === "mingSong") {
-      // 唱名文件也要加上弱起时间  他们制作曲子加了弱起时间
+      // 唱名文件也要加上弱起时间  他们制作曲子加了弱起时间  注意这修改了之后给总控平台的时值也需要改
       state.fixtime = difftime;
       state.times.map((item) => {
         item.time = item.xmlNoteTime + difftime;
@@ -268,7 +268,8 @@ export default defineComponent({
     // 是否显示引导
     const showGuide = ref(false);
     const showStudentGuide = ref(false);
-    let  displayFingeringCache = false // 指法缓存
+    const showWebGuide = ref(true);
+    let displayFingeringCache = false; // 指法缓存
     /** 设置按钮 */
     const settingBtn = computed(() => {
       // 音频播放中 禁用
@@ -366,7 +367,7 @@ export default defineComponent({
     /** 原声按钮 */
     const originBtn = computed(() => {
       // 没有音源不显示
-      if(state.noMusicSource) return { display: false, disabled: false };
+      if (state.noMusicSource) return { display: false, disabled: false };
       // 选择模式,跟练模式 不显示
       //if (headTopData.modeType !== "show" || state.modeType === "follow") return { display: false, disabled: false };
       if (state.modeType === "follow") return { display: false, disabled: false };
@@ -412,8 +413,8 @@ export default defineComponent({
         // 演唱和演奏 都有数据的时间不禁用
         if (songIndex > 0 && index > 0) {
           // 音频播放中 禁用
-          if(state.playState === "play"){
-            return { display: true, disabled: true }
+          if (state.playState === "play") {
+            return { display: true, disabled: true };
           }
           return { display: true, disabled: false };
         }
@@ -429,9 +430,9 @@ export default defineComponent({
       if (state.isPercussion && state.platform === IPlatform.PC) return { display: false, disabled: false };
       if(state.isCombineRender) return { display: false, disabled: false };
       // 没有音源不显示
-      if(state.noMusicSource) return { display: false, disabled: false };
+      if (state.noMusicSource) return { display: false, disabled: false };
       // 不是演奏模式 影藏
-      if(state.playType !== "play") return { display: false, disabled: false }
+      if (state.playType !== "play") return { display: false, disabled: false };
       // 选择模式, url设置模式 不显示
       if (headTopData.modeType !== "show" || !headTopData.showBack) return { display: false, disabled: false };
       // 跟练开始, 评测开始 播放开始 隐藏
@@ -446,7 +447,7 @@ export default defineComponent({
     /** 播放按钮 */
     const playBtn = computed(() => {
       // 没有音源不显示
-      if(state.noMusicSource) return { display: false, disabled: false };
+      if (state.noMusicSource) return { display: false, disabled: false };
       // 选择模式 不显示
       if (headTopData.modeType !== "show") return { display: false, disabled: false };
       // 评测模式 不显示,跟练模式 不显示
@@ -462,7 +463,7 @@ export default defineComponent({
     /** 重播按钮 */
     resetBtn = computed(() => {
       // 没有音源不显示
-      if(state.noMusicSource) return { display: false, disabled: false };
+      if (state.noMusicSource) return { display: false, disabled: false };
       // 选择模式 不显示
       if (headTopData.modeType !== "show") return { display: false, disabled: false };
       // 评测模式 不显示,跟练模式 不显示
@@ -558,6 +559,16 @@ export default defineComponent({
       if (res?.data?.api === "setPlayState") {
         togglePlay("paused", true);
       }
+      if(res?.data?.api === 'togglePlayState') {
+        // if(state.playState === "play") {
+        //   togglePlay("paused");
+        // }
+        // if(state.playState === 'paused') {
+        //   togglePlay("play");
+        // }
+        console.log('togglePlayState', state.playState)
+        togglePlay(state.playState === "play" ? "paused" : "play");
+      }
       // 上课页面,按钮方向
       if (res?.data?.api === "imagePos") {
         if (res?.data.data) {
@@ -579,8 +590,14 @@ export default defineComponent({
         ? {
             styleDrag: { value: null },
           }
-        : useDrag([`${parentClassName} .top_drag`, `${parentClassName} .bom_drag`], parentClassName, toRef(headTopData, "settingMode"), userId);
-
+        : useDrag([`${parentClassName} .top_draging`, `${parentClassName} .bom_drag`], parentClassName, toRef(headTopData, "settingMode"), userId);
+    const speedClassName = "speedBoxClass_drag";
+    const speedInfo =
+      state.platform !== IPlatform.PC
+        ? {
+            styleDrag: { value: null },
+          }
+        : useDrag([`${speedClassName} .top_draging`, `${speedClassName} .bom_drag`], speedClassName, toRef(headData, "speedShow"), userId);
     onMounted(() => {
       getQueryModelSetModelType();
       window.addEventListener("message", changePlay);
@@ -589,21 +606,38 @@ export default defineComponent({
       } else {
         showStudentGuide.value = true;
       }
+
+      if (query.showWebGuide === "false") {
+        showWebGuide.value = false;
+      }
+
+      document.addEventListener("keydown", (e: KeyboardEvent) => {
+        if (e.code === "Tab") {
+          e.stopPropagation();
+          e.preventDefault();
+          // onStartPlayState();
+          togglePlay(state.playState === "play" ? "paused" : "play");
+        }
+      });
     });
 
     onUnmounted(() => {
       window.removeEventListener("message", changePlay);
     });
-    const noticeBarWidth = ref<number>()
-    watch(()=>smoothAnimationState.isShow.value, ()=>{
-      // NoticeBar能不能滚动
-      if((smoothAnimationState.isShow.value || state.isCombineRender) && isMusicList.value){
-        nextTick(()=>{
-          const widthCon = (document.querySelector("#noticeBarRollDom .van-notice-bar__content") as any)?.offsetWidth || undefined
-          noticeBarWidth.value = widthCon
-        })
-      }
-    },{ immediate: true })
+    const noticeBarWidth = ref<number>();
+    watch(
+      () => smoothAnimationState.isShow.value,
+      () => {
+        // NoticeBar能不能滚动
+        if ((smoothAnimationState.isShow.value || state.isCombineRender) && isMusicList.value) {
+          nextTick(() => {
+            const widthCon = (document.querySelector("#noticeBarRollDom .van-notice-bar__content") as any)?.offsetWidth || undefined;
+            noticeBarWidth.value = widthCon;
+          });
+        }
+      },
+      { immediate: true }
+    );
     // 设置改变触发
     watch(state.setting, () => {
       console.log(state.setting, "state.setting");
@@ -654,61 +688,66 @@ export default defineComponent({
           }}
         >
           {/* 返回和标题 */}
-          {
-            !(state.playState == "play" || followData.start || evaluatingData.startBegin) &&
-              <div id="noticeBarRollDom" class={styles.headTopLeftBox}>
-                <img src={iconBack} class={['headTopBackBtn', styles.img, !headTopData.showBack && styles.hidenBack]} onClick={handleBack} />
-                {
-                  smoothAnimationState.isShow.value || state.isCombineRender ?
-                    <div 
-                      style={
-                        noticeBarWidth.value ? {
-                          "--noticeBarWidth":noticeBarWidth.value + "px"
-                        } : {}
-                      }
-                      class={[styles.title, "driver-8"]} 
-                      onClick={()=>{
-                        isMusicList.value && (musicListShow.value = true)
-                      }}>
-                        {
-                          isMusicList.value && <div class={styles.symbolNote}></div>
+          {!(state.playState == "play" || followData.start || evaluatingData.startBegin) && (
+            <div id="noticeBarRollDom" class={styles.headTopLeftBox}>
+              {
+                !query.isMove && <img src={iconBack} class={["headTopBackBtn", styles.img, !headTopData.showBack && styles.hidenBack]} onClick={handleBack} />
+              }
+              {smoothAnimationState.isShow.value || state.isCombineRender ? (
+                <div
+                  style={
+                    noticeBarWidth.value
+                      ? {
+                          "--noticeBarWidth": noticeBarWidth.value + "px",
                         }
-                        <NoticeBar
-                          text={state.examSongName}
-                          background="none"
-                        />
-                    </div> :
-                    isMusicList.value &&
-                    <img src={listImg} class={[styles.img, styles.listImg, "driver-8"]} onClick={()=>{
-                      musicListShow.value = true
-                    }} />
-                }
-              </div>
-          }
+                      : {}
+                  }
+                  class={[styles.title, state.isCbsView && styles.blackTitle, "headeTopTitleBtn"]}
+                  onClick={() => {
+                    isMusicList.value && (musicListShow.value = true);
+                  }}
+                >
+                  {isMusicList.value && <div class={[styles.symbolNote, "driver-8"]}></div>}
+                  <NoticeBar text={state.examSongName} background="none" />
+                </div>
+              ) : (
+                isMusicList.value && (
+                  <img
+                    src={listImg}
+                    class={[styles.img, styles.listImg, "driver-8"]}
+                    onClick={() => {
+                      musicListShow.value = true;
+                    }}
+                  />
+                )
+              )}
+            </div>
+          )}
+
           {/* 模式切换 */}
           {
-            <div 
+            <div
               id={state.platform === IPlatform.PC ? "teacherTop-0" : "studnetT-0"}
               style={{ display: toggleBtn.value.display ? "" : "none" }}
-              class={["driver-9", styles.modeChangeBox, toggleBtn.value.disabled && styles.disabled]} 
+              class={["driver-9", styles.modeChangeBox, toggleBtn.value.disabled && styles.disabled]}
               onClick={() => {
-                  headTopData.oldModeType = state.modeType
-                  handleRessetState();
-                  headTopData.modeType = "init";
+                headTopData.oldModeType = state.modeType;
+                handleRessetState();
+                headTopData.modeType = "init";
               }}
             >
               <img class={styles.img} src={iconMode} />
-              <div class={styles.title}>{state.modeType==="practise" ? '练习模式' : state.modeType==="follow" ? "跟练模式" : state.modeType==="evaluating" ? "评测模式" : ""}</div>
+              <div class={styles.title}>{state.modeType === "practise" ? "练习模式" : state.modeType === "follow" ? "跟练模式" : state.modeType === "evaluating" ? "评测模式" : ""}</div>
             </div>
           }
+
           {/* 模式提醒 */}
-          {
-            state.modeType === "practise" &&
+          {state.modeType === "practise" && (
             <div class={[styles.modeWarn, "practiseModeWarn", state.platform === IPlatform.PC && state.musicScoreBtnDirection === "left" ? styles.modeWarnRight : ""]}>
-                <img src={state.playType === "play" ? headImg("perform1.png") : headImg("sing1.png")} />
-                <div>{state.playType === "play" ? "演奏场景" : "演唱场景"}</div>
-              </div>
-          }
+              <img src={state.playType === "play" ? headImg("perform1.png") : headImg("sing1.png")} />
+              <div>{state.playType === "play" ? "演奏场景" : "演唱场景"}</div>
+            </div>
+          )}
           {/* 功能按钮 */}
           <div
             class={[styles.headRight]}
@@ -745,10 +784,10 @@ export default defineComponent({
             ) : null} */}
             <div
               style={{ display: playTypeBtn.value.display ? "" : "none" }}
-              class={["driver-2", styles.btn, playTypeBtn.value.disabled && styles.disabled,styles.playType]}
+              class={["driver-2", styles.btn, playTypeBtn.value.disabled && styles.disabled, styles.playType]}
               onClick={() => {
                 const oldPlayType = state.playType;
-                headTopData.oldPlayType = oldPlayType
+                headTopData.oldPlayType = oldPlayType;
                 const oldPlaySource = state.playSource;
                 if (state.playType === "play") {
                   state.playType = "sing";
@@ -757,7 +796,7 @@ export default defineComponent({
                   state.playType = "play";
                   state.playSource = state.music ? "music" : "background";
                 }
-                isClickMode = true
+                isClickMode = true;
                 // 有指法并且显示指法的时候 切换到演唱模式 需要影藏指法
                 let isRefresh = false;
                 if (state.isShowFingering && state.fingeringInfo.name && (state.setting.displayFingering || displayFingeringCache)) {
@@ -788,7 +827,7 @@ export default defineComponent({
             <div
               id={state.platform === IPlatform.PC ? "teacherTop-1" : "studnetT-1"}
               style={{ display: originBtn.value.display ? "" : "none" }}
-              class={["driver-3", styles.btn, originBtn.value.disabled && styles.disabled,state.playType === "play"?styles.playSource:styles.songSource]}
+              class={["driver-3", styles.btn, originBtn.value.disabled && styles.disabled, state.playType === "play" ? styles.playSource : styles.songSource]}
               onClick={() => {
                 const oldPlayType = state.playType;
                 const oldPlaySource = state.playSource;
@@ -816,17 +855,22 @@ export default defineComponent({
               <img style={{ display: state.playSource === "mingSong" ? "" : "none" }} class={styles.iconBtn} src={headImg(`mingsong.png`)} />
               <span>{state.playSource === "music" ? (state.playType === "play" ? "原声" : "范唱") : state.playSource === "background" ? (state.playType === "play" ? "伴奏" : "伴唱") : "唱名"}</span>
             </div>
-            <div id={state.platform === IPlatform.PC ? "teacherTop-2" : "studnetT-2"} style={{ display: selectBtn.value.display ? "" : "none" }} class={["driver-4", styles.btn, selectBtn.value.disabled && styles.disabled, styles.section, state.sectionStatus && styles.isSection]} onClick={() => handleChangeSection()}>
+            <div
+              id={state.platform === IPlatform.PC ? "teacherTop-2" : "studnetT-2"}
+              style={{ display: selectBtn.value.display ? "" : "none" }}
+              class={["driver-4", styles.btn, selectBtn.value.disabled && styles.disabled, styles.section, state.sectionStatus && styles.isSection]}
+              onClick={() => handleChangeSection()}
+            >
               <img style={{ display: state.section.length === 0 ? "" : "none" }} class={styles.iconBtn} src={headImg(`section0.png`)} />
               <img style={{ display: state.section.length === 1 ? "" : "none" }} class={styles.iconBtn} src={headImg(`section1.png`)} />
               <img style={{ display: state.section.length === 2 ? "" : "none" }} class={styles.iconBtn} src={headImg(`section2.png`)} />
               <span>选段</span>
             </div>
-            {(
+            {
               <>
                 <div
                   style={{ display: metronomeBtn.value.display ? "" : "none" }}
-                  class={["driver-5", styles.btn, styles.metronomeBtn, metronomeBtn.value.disabled && styles.disabled,headData.speedShow && styles.isSpeed,styles.speed]}
+                  class={["driver-5", styles.btn, styles.metronomeBtn, metronomeBtn.value.disabled && styles.disabled, headData.speedShow && styles.isSpeed, styles.speed]}
                   onClick={async () => {
                     headData.speedShow = !headData.speedShow;
                   }}
@@ -840,13 +884,13 @@ export default defineComponent({
                   </div>
                 </div>
                 {
-                  <Popup v-model:show={headData.speedShow} class="popup-custom van-scale center-closeBtn settingBoxClass_drag" transition="van-scale" teleport="body" style={positionInfo.styleDrag.value} overlay-style={{ background: "rgba(0, 0, 0, 0.3)" }}>
+                  <Popup v-model:show={headData.speedShow} class="popup-custom van-scale center-closeBtn speedBoxClass_drag" transition="van-scale" teleport="body" style={speedInfo.styleDrag.value} overlay-style={{ background: "rgba(0, 0, 0, 0.3)" }}>
                     <Speed />
                     {state.platform === IPlatform.PC && <Dragbom showGuide={!state.guideInfo?.teacherDrag} onGuideDone={handleGuide} />}
                   </Popup>
                 }
               </>
-            )}
+            }
             {/* {state.enableNotation ? (
               <Popover trigger="manual" v-model:show={headData.musicTypeShow} class={state.platform === IPlatform.PC && styles.pcTransPop} placement={state.platform === IPlatform.PC ? "top-end" : "bottom-end"} overlay={false} offset={state.platform === IPlatform.PC ? [0, 40] : [0, 8]}>
                 {{
@@ -870,7 +914,7 @@ export default defineComponent({
             ) : null} */}
             {state.musicRendered && !query.lessonTrainingId && !query.questionId && state.isConcert && (
               <div
-                class={[styles.btn, state.playState === "play" && fingeringBtn.value.disabled && styles.disabled,toggleMusicSheet.show&&styles.isMusicSheet,styles.musicSheet]}
+                class={[styles.btn, state.playState === "play" && fingeringBtn.value.disabled && styles.disabled, toggleMusicSheet.show && styles.isMusicSheet, styles.musicSheet, "driver-10"]}
                 onClick={() => {
                   toggleMusicSheet.toggle(true);
                 }}
@@ -879,7 +923,12 @@ export default defineComponent({
                 <span>声部</span>
               </div>
             )}
-            <div id={state.platform === IPlatform.PC ? "teacherTop-6" : "studnetT-6"} style={{ display: settingBtn.value.display ? "" : "none" }} class={["driver-6", styles.btn, settingBtn.value.disabled && styles.disabled,headTopData.settingMode&&styles.isSettingMode,styles.settingMode]} onClick={() => (headTopData.settingMode = true)}>
+            <div
+              id={state.platform === IPlatform.PC ? "teacherTop-6" : "studnetT-6"}
+              style={{ display: settingBtn.value.display ? "" : "none" }}
+              class={["driver-6", styles.btn, settingBtn.value.disabled && styles.disabled, headTopData.settingMode && styles.isSettingMode, styles.settingMode]}
+              onClick={() => (headTopData.settingMode = true)}
+            >
               <img class={styles.iconBtn} src={headImg("icon_menu.png")} />
               <span>设置</span>
             </div>
@@ -905,7 +954,21 @@ export default defineComponent({
             playBtn.value.disabled && styles.disabled,
             state.platform === IPlatform.PC && state.musicScoreBtnDirection === "left" ? styles.playLeftButton : state.platform === IPlatform.PC && state.musicScoreBtnDirection === "right" ? styles.playRightButton : "",
           ]}
-          onClick={() => togglePlay(state.playState === "play" ? "paused" : "play")}
+          onClick={() => {
+            // C调能播放唱名,非C调时,只有谱面类型是首调时,才能播放唱名
+            if (!state.isCTone && state.playSource === 'mingSong') {
+              const notPlayDesc = state.musicRenderType === EnumMusicRenderType.staff ? '该曲目的五线谱目前还不支持播放唱名' : state.musicRenderType === EnumMusicRenderType.fixedTone ? '该曲目的固定调目前还不支持播放唱名' : '';
+              if (notPlayDesc) {
+                showToast({
+                  message: notPlayDesc,
+                  position: "top",
+                  className: "selectionToast",
+                });
+                return
+              }
+            }
+            togglePlay(state.playState === "play" ? "paused" : "play")
+          }}
         >
           <div class={styles.btnWrap}>
             <img style={{ display: state.playState === "play" ? "none" : "" }} class={styles.iconBtn} src={headImg("icon_play.png")} />
@@ -938,17 +1001,22 @@ export default defineComponent({
         {isAllBtnsStudent.value && !query.isCbs && showGuideIndex.value && <StudentTop></StudentTop>} */}
 
         {/* 练习模式功能引导 加载音频完成 不是会员 */}
-        {state.modeType === "practise" && headTopData.modeType !== "init" && !query.isCbs && state.audioDone && !state.isLoading && !state.isVip && (
+        {state.modeType === "practise" && headTopData.modeType !== "init" && !query.isCbs && state.audioDone && !state.isLoading && !state.isVip && showWebGuide.value && (
           <PractiseDriver
             statusAll={{
+              playBtnStatus: playBtn.value.display,
               subjectStatus: state.musicRendered && !query.lessonTrainingId && !query.questionId && state.isConcert,
               modelTypeStatus: toggleBtn.value.display,
               playType: playTypeBtn.value.display,
+              originPlayType: state.playType === "play" ? true : false,
+              originBtnStatus: originBtn.value.display,
+              backTitle: !(state.playState == "play" || followData.start || evaluatingData.startBegin) && isMusicList.value,
+              titleType: smoothAnimationState.isShow.value ? "TEXT" : isMusicList.value ? "IMG" : "NONE",
             }}
           />
         )}
         {/* 跟练模式功能引导 加载音频完成 不是会员 */}
-        {state.modeType === "follow" && headTopData.modeType !== "init" && !query.isCbs && state.audioDone && !state.isLoading && !state.isVip && (
+        {state.modeType === "follow" && headTopData.modeType !== "init" && !query.isCbs && state.audioDone && !state.isLoading && !state.isVip && showWebGuide.value && (
           <FollowDriver
             statusAll={{
               subjectStatus: state.musicRendered && !query.lessonTrainingId && !query.questionId && state.isConcert,
@@ -956,7 +1024,7 @@ export default defineComponent({
           />
         )}
         {/* 评测模式功能引导 加载音频完成 不是会员 */}
-        {state.modeType === "evaluating" && headTopData.modeType !== "init" && !evaluatingData.earphoneMode && !query.isCbs && state.audioDone && !state.isLoading && !state.isVip && evaluatingData.websocketState && !evaluatingData.startBegin && evaluatingData.checkEnd && (
+        {state.modeType === "evaluating" && headTopData.modeType !== "init" && !evaluatingData.earphoneMode && !query.isCbs && state.audioDone && !state.isLoading && !state.isVip && evaluatingData.websocketState && !evaluatingData.startBegin && evaluatingData.checkEnd && showWebGuide.value && (
           <EvaluatingDriver
             statusAll={{
               subjectStatus: state.musicRendered && !query.lessonTrainingId && !query.questionId && state.isConcert,
@@ -966,4 +1034,4 @@ export default defineComponent({
       </>
     );
   },
-});
+});

+ 6 - 2
src/page-instrument/header-top/modeView.tsx

@@ -107,7 +107,9 @@ export default defineComponent({
           src={backImg}
           class={styles.back}
           onClick={() => {
-            smoothAnimationState.isShow.value = state.melodyLine;
+            if(state.isSingleLine){
+              smoothAnimationState.isShow.value = state.melodyLine;
+            }
             // 返回的时候 跳转到之前记录的模式
             if(headTopData.oldModeType !== "practise"){
               // 点击评测模式进入评测模块的需要检测耳机状态,通过返回按钮进入评测模块的,不检测耳机状态
@@ -120,7 +122,9 @@ export default defineComponent({
         <img src={nameImg} class={styles.name} />
         <div class={[styles.modeBox, ((!state.isPercussion && !state.enableEvaluation) || (state.isPercussion && state.enableEvaluation) || (state.isPercussion && !state.enableEvaluation)) && styles.twoModeBox]}>
           <Vue3Lottie ref={modeImgDom1} class={styles.modeImg} animationData={lxMode} autoPlay={false} loop={true} onClick={() => {
-            smoothAnimationState.isShow.value = state.melodyLine;
+            if(state.isSingleLine){
+              smoothAnimationState.isShow.value = state.melodyLine;
+            }
             headTopData.handleChangeModeType("practise")
             } }></Vue3Lottie>
           {!state.isPercussion && <Vue3Lottie ref={modeImgDom2} class={styles.modeImg} animationData={glMode} autoPlay={false} loop={true} onClick={() => headTopData.handleChangeModeType("follow")}></Vue3Lottie>}

+ 3 - 2
src/page-instrument/header-top/settting/index.module.less

@@ -256,6 +256,7 @@
                                 font-weight: 600;
                                 font-size: 15px;
                                 color: #1CACF1;
+                                caret-color: #1cacf1;
                             }
                         }
                     }
@@ -276,8 +277,8 @@
                     width: 118px;
                     height: 39px;
                     cursor: pointer;
-                    &:first-child{
-                        margin-right: 20px;
+                    & + img{
+                        margin-left: 20px;
                     }
                 }
             }

+ 27 - 23
src/page-instrument/header-top/settting/index.tsx

@@ -3,7 +3,7 @@ import styles from "./index.module.less"
 import { headImg } from "../image";
 import { headTopData } from "../index"
 import { Switch, showToast, Field, Popup, Slider } from "vant";
-import state, { refreshMusicSvg, IPlatform } from "/src/state"
+import state, { refreshMusicSvg, IPlatform, checkMoveNoSave } from "/src/state"
 import { api_closeCamera, api_openCamera, api_savePicture } from "/src/helpers/communication";
 import { smoothAnimationState} from "/src/page-instrument/view-detail/smoothAnimation"
 import Recommendation from "../../custom-plugins/helper-model/recommendation";
@@ -32,7 +32,7 @@ export default defineComponent({
 			? {
 				styleDrag: { value: null },
 			  }
-			: useDrag([`${parentClassName} .top_drag`, `${parentClassName} .bom_drag`], parentClassName, toRef(helperData, "recommendationShow"), userId);
+			: useDrag([`${parentClassName} .top_draging`, `${parentClassName} .bom_drag`], parentClassName, toRef(helperData, "recommendationShow"), userId);
 
 		// 完成拖动弹窗引导页
 		const handleGuide = async () => {
@@ -66,7 +66,7 @@ export default defineComponent({
 
 		return () => (
 			<div class={[styles.settting, styles[state.modeType]]}>
-                <div class={styles.head}>
+                <div class={[styles.head, "top_draging"]}>
 					<img class={styles.headTit} src={headImg("settingName.png")} />
 					<img class={styles.closeImg} src={headImg("closeImg.png")} onClick={()=>{ headTopData.settingMode = false }} />
 				</div>
@@ -87,12 +87,24 @@ export default defineComponent({
                                 </div>
                         }                        
                         {
+                            state.isSingleLine && state.modeType === "practise" && !state.isCombineRender && !state.isPercussion && 
+                                <div class={styles.cellBox}>
+                                <div class={styles.tit}>旋律线</div>
+                                    <Switch 
+                                        v-model={smoothAnimationState.isShow.value}
+                                        onChange={(value) => {
+                                            state.melodyLine = value
+                                        }}                                        
+                                    ></Switch>
+                                </div>   
+                        }
+                        {
                             state.modeType === 'practise' && state.mingSong && state.mingSongGirl &&
                             <div class={styles.cellBox}>
                                 <div class={styles.tit}>唱名类型</div>
                                 <div class={styles.radioBox}>
                                     {
-                                        [{name:'男生',value:1}, {name:'女生',value:0}].map(item=>{
+                                        [{name:'男声',value:1}, {name:'女声',value:0}].map(item=>{
                                             return <div class={ audioData.mingSongType===item.value && styles.active } onClick={ ()=>{ 
                                                 if(audioData.mingSongType === item.value){
                                                     return
@@ -104,19 +116,7 @@ export default defineComponent({
                                     }
                                 </div>
                             </div>                     
-                        }
-                        {
-                            state.isSingleLine && state.modeType === "practise" && !state.isCombineRender && !state.isPercussion && 
-                                <div class={styles.cellBox}>
-                                <div class={styles.tit}>旋律线</div>
-                                    <Switch 
-                                        v-model={smoothAnimationState.isShow.value}
-                                        onChange={(value) => {
-                                            state.melodyLine = value
-                                        }}                                        
-                                    ></Switch>
-                                </div>   
-                        }                           
+                        }                        
                         {
                             state.modeType === "evaluating" && 
                             <>                       
@@ -202,20 +202,21 @@ export default defineComponent({
                         }
                         {/** 练习模式才有单行/多行谱切换功能,跟练、评测只有单行谱模式 */}
                         {
-                            state.modeType === 'practise' ? 
+                            ["practise", "evaluating"].includes(state.modeType) ? 
                             <div class={styles.cellBox}>
                                 <div class={styles.tit}>切换谱面</div>
                                 <div class={styles.radioBox}>
                                     {
                                         [{name:'单行谱',value:true},{name:'多行谱',value:false}].map(item=>{
-                                            return <div class={ state.isSingleLine===item.value && styles.active } onClick={ ()=>{ 
+                                            return <div class={ state.isSingleLine===item.value && styles.active } onClick={ async ()=>{ 
                                                 if(state.isSingleLine === item.value){
                                                     return
                                                 }
+                                                await checkMoveNoSave();
                                                 headTopData.settingMode = false
-                                                state.isSingleLine = item.value 
                                                 // resetRenderMusicScore(state.musicRenderType)
                                                 const _time = setTimeout(() => {
+                                                    state.isSingleLine = item.value 
                                                     clearTimeout(_time)
                                                     refreshMusicSvg();
                                                 }, 100);
@@ -233,14 +234,15 @@ export default defineComponent({
                                 <div class={styles.radioBox}>
                                     {
                                         notationList.value.map(item=>{
-                                            return <div class={ state.musicRenderType===item.value && styles.active } onClick={ ()=>{ 
+                                            return <div class={ state.musicRenderType===item.value && styles.active } onClick={ async ()=>{ 
                                                 if(state.musicRenderType === item.value){
                                                     return
                                                 }
+                                                await checkMoveNoSave();
                                                 headTopData.settingMode = false
-                                                state.musicRenderType = item.value as any
                                                 // resetRenderMusicScore(state.musicRenderType)
                                                 const _time = setTimeout(() => {
+                                                    state.musicRenderType = item.value as any
                                                     clearTimeout(_time)
                                                     refreshMusicSvg();
                                                 }, 100);
@@ -253,7 +255,9 @@ export default defineComponent({
 
                         <div class={styles.cellBtnBox}>
                             <img  src={headImg("tpbz.png")} onClick={() => (helperData.screenModelShow = true)} />
-                            <img  src={headImg("yjfk.png")} onClick={() => (helperData.recommendationShow = true)} />
+                            {
+                                !query.isCbs && <img  src={headImg("yjfk.png")} onClick={() => (helperData.recommendationShow = true)} />
+                            }
                         </div>
                     </div>  
                 </div>

+ 2 - 2
src/page-instrument/header-top/speed/index.module.less

@@ -168,7 +168,6 @@
             .speedSel{
                 margin-top: 20px;
                 padding-bottom: 18px;
-                border-bottom: 1px solid #D5E0ED;
                 display: flex;
                 justify-content: space-between;
                 & > div{
@@ -191,7 +190,8 @@
                 pointer-events: none;
             }
             .metronome{
-                margin-top: 18px;
+                padding-top: 18px;
+                border-top: 1px solid #D5E0ED;
                 display: flex;
                 justify-content: space-between;
                 align-items: center;

+ 13 - 10
src/page-instrument/header-top/speed/index.tsx

@@ -106,7 +106,7 @@ export default defineComponent({
 		};
 		return () => (
 			<div class={[styles.speedContainer, styles[state.modeType]]}>
-				<div class={styles.head}>
+				<div class={[styles.head, "top_draging"]}>
 					<img class={styles.headTit} src={headImg("headTit.png")} />
 					<img class={styles.closeImg} src={headImg("closeImg.png")} onClick={()=>{ headData.speedShow = false }} />
 				</div>
@@ -136,15 +136,18 @@ export default defineComponent({
 							<div onClick={()=>{ speed.value = 100 }}>100</div>
 							<div onClick={()=>{ speed.value = 110 }}>110</div>
 						</div>
-						<div class={styles.metronome}>
-							<div class={styles.tit}>节拍器</div>
-							<Switch 
-								class={switchLoading.value ? styles.switchLoading : ''}
-								v-model:modelValue={metronomeDisable.value} 
-								loading={switchLoading.value}
-								onChange={toggleSwitch}			
-							></Switch>
-						</div>
+						{
+							state.isMixBeat && 
+							<div class={styles.metronome}>
+								<div class={styles.tit}>节拍器</div>
+								<Switch 
+									class={switchLoading.value ? styles.switchLoading : ''}
+									v-model:modelValue={metronomeDisable.value} 
+									loading={switchLoading.value}
+									onChange={toggleSwitch}			
+								></Switch>
+							</div>
+						}
 					</div>
 				</div>
 			</div>

+ 1 - 0
src/page-instrument/simple-detail/index.tsx

@@ -43,6 +43,7 @@ export default defineComponent({
 				if (currentTime === 0) {
 					// 坐标和小节都改为初始值
 					setTimeout(() => {
+						detailData.currentTime = 0
 						state.activeNoteIndex = 0
 						state.activeMeasureIndex = state.times[0].MeasureNumberXML;
 						handlePlaying(true);

BIN
src/page-instrument/view-detail/images/bg2_left_zs.png


+ 159 - 11
src/page-instrument/view-detail/index.module.less

@@ -10,6 +10,20 @@
     --van-skeleton-paragraph-height: .8rem;
 }
 
+:global {
+
+    // .headHeight>.driver-active-element,
+    // #noticeBarRollDom>.driver-active-element,
+    // :not(body):has(>.headHeight),
+    // :not(body):has(>#noticeBarRollDom),
+    body .headeTopTitleBtn,
+    body #noticeBarRollDom {
+        overflow: initial !important;
+    }
+
+
+}
+
 .detail {
     position: relative;
     width: 100vw;
@@ -17,6 +31,7 @@
     overflow: hidden;
     --header-height: 80px;
     --pc-header-height: 72px;
+
     // &.practise{
     //     background: url("./images/bg1.png") no-repeat;
     //     background-size: 100% 100%;
@@ -36,7 +51,17 @@
         width: 100%;
         height: 100%;
         object-fit: cover; /* 保持宽高比 */
+        &.practise{
+            background-color: #213793;
+        }        
+        &.follow{
+            background-color: #114067;
+        }        
+        &.evaluating{
+            background-color: #142979;
+        }
     }
+
     .headHeight {
         position: absolute;
         bottom: 0;
@@ -47,15 +72,18 @@
 
         &.headHide {
             margin-bottom: calc(0Px - var(--header-height));
-            :global{
-                .practiseModeWarn{
+
+            :global {
+                .practiseModeWarn {
                     opacity: 1;
+
                     img {
                         opacity: 0.7;
                     }
                 }
             }
         }
+
     }
 
     .container {
@@ -66,17 +94,20 @@
         transition: height .2s;
         transition: padding-bottom .2s;
         overflow: hidden;
-        :global{
+
+        :global {
             #musicAndSelection {
-                // 其他位置 这个高度留白是36,这里加了一点,让旋律线靠下一点
-                padding-top: 40px;
+                --musicAndSelectionTop: 40px;
+                padding-top: var(--musicAndSelectionTop);
             }
         }
     }
+
     .pcContainer {
         // height: calc(100vh - var(--header-height) - var(--pc-header-height));
     }
-    .fingeringCon{
+
+    .fingeringCon {
         transition: scale 0.2s;
     }
 }
@@ -177,33 +208,116 @@
 
 .preViewDetail {
     background: #fff !important;
+
     >.pageBg {
         display: none;
     }
+
     .headHeight {
         background: #fff !important;
     }
+
     .container {
         height: 100%;
         padding-bottom: 0 !important;
         padding-right: 0 !important;
+        padding-left: 0 !important;
     }
+
     :global {
+        .authorName {
+            display: none !important;
+        }
+        #musicAndSelection{
+            padding-top: 0 !important;
+        }
         #osmdCanvasPage1 {
             padding-bottom: 0 !important;
         }
+
         #cursorImg-0 {
             opacity: 0 !important;
         }
+
         .noteActive {
             path {
                 fill: #000000;
                 stroke: #000000;
             }
+
+            rect {
+                stroke: #000000;
+            }
+        }
+        .lyricActive {
+            text {
+                fill: #000000;
+                stroke: #000000;
+            }
+        }
+        .voiceActive {
             rect {
+                fill: #000000;
                 stroke: #000000;
             }
         }
+        .rectActive {
+            fill: #000000;
+            stroke: #000000;
+        }
+    }
+}
+
+.cbsViewDetail {
+    background: #fff !important;
+
+    >.pageBg {
+        display: none;
+    }
+
+    .headHeight {
+        // background: #fff !important;
+    }
+
+    :global {
+        #cursorImg-0 {
+            opacity: 0 !important;
+        }
+        .noteActive {
+            path {
+                fill: #0097FF;
+                stroke: #0097FF;
+            }
+
+            rect {
+                stroke: #0097FF;
+            }
+        }
+        .lyricActive {
+            text {
+                fill: #0097FF;
+                stroke: #0097FF;
+            }
+        }
+        .voiceActive {
+            rect {
+                fill: #0097FF;
+                stroke: #0097FF;
+            }
+        }
+        .rectActive {
+            fill: #0097FF;
+            stroke: #0097FF;
+        }
+        #selectionBgBox {
+            display: none;
+        }
+        .vf-numbered_note_lines {
+            rect {
+                fill: #000;
+                stroke: #000;
+            }
+        }
     }
 }
 
@@ -221,29 +335,62 @@
     }
 }
 
+@keyframes rotate {
+    from {
+        transform: rotate(0deg);
+    }
+    to {
+        transform: rotate(360deg);
+    }
+}
 .loadingPop {
     position: fixed;
     left: 0;
     top: 0;
     right: 0;
     bottom: 0;
-    width: 100vw;
-    height: 100vh;
+    width: 100%;
+    min-width: 100vw;
+    height: 100%;
+    min-height: 100vh;
     display: flex;
     flex-direction: column;
     justify-content: center;
     align-items: center;
     z-index: 10000;
     background: rgba(0, 0, 0, .6);
-    &.isPreView{
-        background:transparent;
-        .loadingTip{
+
+    &.isPreView {
+        background: transparent;
+
+        .loadingTip {
             color: #999;
         }
     }
+    .loadingCssBox{
+        width: 27px;
+        height: 27px;
+        display: flex;
+        justify-content: space-between;
+        flex-wrap: wrap;
+        align-content: space-between;
+        margin-bottom: 24px;
+        animation: rotate 1.5s linear infinite;
+        .loadingCssItem{
+            width: 11px;
+            height: 11px;
+            border-radius: 50%;
+            background: #20BDFF;
+            opacity: 0.5;
+            &:nth-child(2){
+                opacity:1;
+            }
+        }
+    }
     .lottie{
         width: 120px;
     }
+
     .loadingTip {
         font-size: 14px;
         color: #fff;
@@ -256,6 +403,7 @@
     left: 0px;
     top: 0;
 }
+
 .bg2Right {
     width: 52px;
     position: absolute;

+ 109 - 78
src/page-instrument/view-detail/index.tsx

@@ -9,7 +9,7 @@ import MusicScore from "../../view/music-score";
 import TestCheck from "/src/view/music-score/testCheck";
 import { sysMusicScoreAccompanimentQueryPage } from "../api";
 import EvaluatModel from "../evaluat-model";
-import HeaderTop, {handlerModeChange} from "../header-top";
+import HeaderTop, { handlerModeChange } from "../header-top";
 import styles from "./index.module.less";
 import { api_cloudAccompanyMessage, api_cloudLoading, api_keepScreenLongLight, api_openCamera, api_openWebView, api_setEventTracking, api_setRequestedOrientation, api_setStatusBarVisibility, isSpecialShapedScreen } from "/src/helpers/communication";
 import { getQuery } from "/src/utils/queryString";
@@ -28,16 +28,17 @@ import { storeData } from "/src/store";
 import ViewFigner from "../view-figner";
 import { recalculateNoteData } from "/src/view/selection";
 import ToggleMusicSheet from "/src/view/plugins/toggleMusicSheet";
-import { setCustomGradual, setCustomNoteRealValue } from "/src/helpers/customMusicScore"
+import { setCustomGradual, setCustomNoteRealValue } from "/src/helpers/customMusicScore";
 import { usePageVisibility } from "@vant/use";
-import { initMidi } from "/src/helpers/midiPlay"
-import TheAudio from "/src/components/the-audio"
+import { initMidi } from "/src/helpers/midiPlay";
+import TheAudio from "/src/components/the-audio";
 import tickWav from "/src/assets/tick.mp3";
-import AuthorName from "../component/authorName"
-import { initSmoothAnimation } from "./smoothAnimation"
-import EmptyMusic, { isEmptyMusicShow } from "./emptyMusic"
+import AuthorName from "../component/authorName";
+import { initSmoothAnimation } from "./smoothAnimation";
+import EmptyMusic, { isEmptyMusicShow } from "./emptyMusic";
 import { position } from "html2canvas/dist/types/css/property-descriptors/position";
 import Loading from "./loading"
+import LoadingCss from "./loadingCss"
 // import bgJson from "./images/index.json";
 import bg2Left from "./images/bg2_left_zs.png";
 import bg2Right from "./images/bg2_right_zs.png";
@@ -117,21 +118,21 @@ export default defineComponent({
       }
     };
     onBeforeMount(async () => {
-      console.time("渲染加载耗时");
+      // console.time("渲染加载耗时");
       api_keepScreenLongLight();
       getAPPData();
       api_setStatusBarVisibility();
       const settting = store.get("musicscoresetting");
       if (settting) {
         //state.setting = settting;
-        Object.assign(state.setting, settting)
+        Object.assign(state.setting, settting);
         //state.setting.beatVolume = state.setting.beatVolume || 50
-        state.setting.beatVolume = 50
+        state.setting.beatVolume = 50;
         if (state.setting.camera) {
           const res = await api_openCamera();
           // 没有授权
           if (res?.content?.reson) {
-            state.setting.camera = false
+            state.setting.camera = false;
             store.set("musicscoresetting", state.setting);
           }
         }
@@ -161,16 +162,17 @@ export default defineComponent({
     onMounted(async () => {
       (window as any).appName = "colexiu";
       const id = query.id || "43554";
+      state.isCbsView = query.isCbs;
       // 如果是纯预览模式,0.65倍缩放谱面
-      state.isPreView = query.isPreView
+      state.isPreView = query.isPreView;
       if (state.isPreView) {
-        state.zoom = 0.65
+        state.zoom = query.zoom  || 0.65
       }
-      if (id == '1814218144844087298' && state.isSingleLine) {
-        state.zoom = 0.7
+      if (id == "1814218144844087298" && state.isSingleLine) {
+        state.zoom = 0.7;
       }
       // 只有总控平台和预览 默认是多行谱
-      (state.isPreView || query.isCbs) && (state.isSingleLine = false)
+      //(state.isPreView || query.isCbs) && (state.isSingleLine = false)
       // Promise.all([sysMusicScoreAccompanimentQueryPage(id)]).then((values) => {
       //   getMusicInfo(values[0]);
       // });
@@ -178,16 +180,27 @@ export default defineComponent({
       try { 
         await getMusicDetail(id);
       } catch (err) {
-        console.error(err)
+        console.error(err);
         state.isLoading = false;
         isEmptyMusicShow.value = true
+        // 需要向外面(iframe)派发计时器数据的时候触发
+        if(query.isbeatTimes){
+          console.log("webApi_beatTimes",err)
+          window.parent.postMessage(
+            {
+              api: "webApi_beatTimes",
+              data: "节拍器时值错误!!"
+            },
+            "*"
+          );
+        }
         return
       }
       detailData.isLoading = false;
       // 如果后台设置了不显示指法,关闭指法开关   如果默认进来是演奏模式 不显示指法开关
       if (!state.isShowFingering || state.playType === "sing") {
-        state.setting.displayFingering = false
-      }      
+        state.setting.displayFingering = false;
+      }
       // api_setEventTracking();
     });
 
@@ -207,15 +220,15 @@ export default defineComponent({
       //   handleSetSpeed(saveSpeed);
       // }
       setCustomGradual();
-			setCustomNoteRealValue();
+      setCustomNoteRealValue();
       state.times = formateTimes(osmd);
       // state.times = resetFrequency(state.times);
       state.times = setNoteHalfTone(state.times);
-      state.xmlHasLyric = state.times.some((item: any) => item?.formatLyricsEntries?.length)
+      state.xmlHasLyric = state.times.some((item: any) => item?.formatLyricsEntries?.length);
       console.log("🚀 ~ state.times:", state.times, state.subjectId, state);
       nextTick(() => {
         state.activeMeasureIndex = state.times[0].MeasureNumberXML;
-      })
+      });
       // 一行谱
       if (state.isSingleLine) {
         // 音符添加位置信息bbox
@@ -224,69 +237,70 @@ export default defineComponent({
         initSmoothAnimation();
       }
       // 初始化midi音频信息
-      const songEndTime = state.times[state.times.length - 1 || 0]?.endtime || 0
+      const songEndTime = state.times[state.times.length - 1 || 0]?.endtime || 0;
       if (state.isAppPlay) {
-        const durationNum = songEndTime
-        initMidi(durationNum, state.midiUrl)
+        const durationNum = songEndTime;
+        initMidi(durationNum, state.midiUrl);
       }
-      state.measureTime = state.times[0]?.measureLength || 0
+      state.measureTime = state.times[0]?.measureLength || 0;
       try {
         metronomeData.metro = new Metronome();
         metronomeData.metro.init(state.times);
       } catch (error) {}
 
       // 需要向外面(iframe)派发计时器数据的时候触发
-      if(query.isbeatTimes){
-        const { isOpenMetronome, isSingOpenMetronome } = state
-        const {xmlMp3BeatFixTime} = state.times[0] 
-        const singBeatTime: number[][] = []
-        const beatTime = metronomeData.metroMeasure.map(metroMeasure => {
-          const singBeat:number[] = []
+      if (query.isbeatTimes) {
+        const { isOpenMetronome, isSingOpenMetronome } = state;
+        const { xmlMp3BeatFixTime, difftime } = state.times[0];
+        const singBeatTime: number[][] = [];
+        const beatTime = metronomeData.metroMeasure.map((metroMeasure) => {
+          const singBeat: number[] = [];
           const beatTimeItem = metroMeasure.map((item: any) => {
-            let singTime = item.time
-            if(isSingOpenMetronome && !isOpenMetronome){
-              singTime += xmlMp3BeatFixTime
-            } else if(!isSingOpenMetronome && isOpenMetronome){
-              singTime -= xmlMp3BeatFixTime
+            let singTime = item.time;
+            if (isSingOpenMetronome && !isOpenMetronome) {
+              singTime += xmlMp3BeatFixTime;
+            } else if (!isSingOpenMetronome && isOpenMetronome) {
+              singTime -= xmlMp3BeatFixTime;
             }
-            singBeat.push(singTime)
-            return item.time
-          })
-          singBeatTime.push(singBeat)
-          return beatTimeItem
-        })
+            singBeat.push(singTime);
+            return item.time;
+          });
+          singBeatTime.push(singBeat);
+          return beatTimeItem;
+        });
         //改为唱名
-        state.fixtime = 0
-        state.times.map(item => {
-          item.time = item.xmlNoteTime
-          item.endtime = item.xmlNoteEndTime
-          item.fixtime = 0
-        })
-        metronomeData.metro.calculation(state.times)
-        const mingBeatTime:number[][] = metronomeData.metroMeasure.map(metroMeasure => {
+        state.fixtime = difftime;
+        state.times.map((item) => {
+          item.time = item.xmlNoteTime + difftime;
+          item.endtime = item.xmlNoteEndTime + difftime;
+          item.fixtime = difftime;
+        });
+        metronomeData.metro.calculation(state.times);
+        const mingBeatTime: number[][] = metronomeData.metroMeasure.map((metroMeasure) => {
           const beatTimeItem = metroMeasure.map((item: any) => {
-            return item.time
-          })
-          return beatTimeItem
-        })
-        console.log("webApi_beatTimes",{beatTime,singBeatTime,mingBeatTime})
+            return item.time;
+          });
+          return beatTimeItem;
+        });
+        const webApi_beatTimes = { beatTime, singBeatTime, mingBeatTime, isBeatTime:!state.isEvxml, isSingBeatTime:!state.isEvxml, isMingBeatTime:!state.isEvxml }
+        console.log("webApi_beatTimes", webApi_beatTimes);
         window.parent.postMessage(
           {
             api: "webApi_beatTimes",
-            data: JSON.stringify({beatTime,singBeatTime,mingBeatTime})
+            data: JSON.stringify(webApi_beatTimes),
           },
           "*"
         );
-        throw new Error("webApi_beatTimes 完成");
+        return
       }
       // 刷新时值
-      handlerModeChange("play", "music")
+      handlerModeChange("play", "music");
       /**
        * 2024.1.25
        * 设置节拍器,跟练需要播放系统节拍器,所以不需要判断needTick状态
        */
       // if (state.needTick) {
-        handleInitTick(osmd?.Sheet?.SheetPlaybackSetting?.Rhythm?.Numerator || 4);
+      handleInitTick(osmd?.Sheet?.SheetPlaybackSetting?.Rhythm?.Numerator || 4, osmd?.Sheet?.SheetPlaybackSetting?.Rhythm?.denominator);
       // }
       // api_cloudLoading();
       // state.playBtnDirection = query.imagePos === 'left' ? 'left' : 'right';
@@ -308,6 +322,23 @@ export default defineComponent({
       // pushAppMusic();
       // console.timeEnd("渲染加载耗时");
     };
+    function handleOnRendered(osmd: any) {
+      try{
+        handleRendered(osmd)
+      }catch(err:any){
+        // 需要向外面(iframe)派发计时器数据的时候触发
+        if(query.isbeatTimes){
+          console.log("webApi_beatTimes",err)
+          window.parent.postMessage(
+            {
+              api: "webApi_beatTimes",
+              data: "节拍器时值错误!!"
+            },
+            "*"
+          );
+        }
+      }
+    }
     /** 指法配置 */
     const fingerConfig = computed<any>(() => {
       if (state.setting.displayFingering && state.fingeringInfo?.name) {
@@ -329,7 +360,7 @@ export default defineComponent({
             }
           };
         } else {
-          console.log('指法',state.playBtnDirection,state.platform)
+          console.log("指法", state.playBtnDirection, state.platform);
           // 老师端,竖向指法,需要根据功能按钮方向进行设置
           if (state.platform === IPlatform.PC) {
             return {
@@ -373,13 +404,12 @@ export default defineComponent({
       () => state.setting.displayFingering,
       () => {
         if (state.fingeringInfo.direction === "vertical") {
-          
           // if (state.setting.displayFingering) {
           //   state.musicScoreBtnDirection = state.playBtnDirection === 'left' ? 'right' : 'left'
           // } else {
           //   state.musicScoreBtnDirection = state.playBtnDirection
           // }
-          state.musicScoreBtnDirection = state.playBtnDirection
+          state.musicScoreBtnDirection = state.playBtnDirection;
         }
       }
     );
@@ -412,9 +442,9 @@ export default defineComponent({
     );
     // 监听跟练的开启状态
     watch(
-      () => followData.start,
+      () => followData.practiceStart,
       () => {
-        headerColumnHide.value = followData.start;
+        headerColumnHide.value = followData.practiceStart;
       }
     );
     // 监听开始评测状态
@@ -485,11 +515,11 @@ export default defineComponent({
     };
     return () => (
       <div
-        class={[styles.detail, styles[state.modeType], state.setting.eyeProtection && "eyeProtection", (state.platform === IPlatform.PC && state.zoom > 0.8) && styles.PC, state.isPreView && styles.preViewDetail]}
+        class={[styles.detail, styles[state.modeType], state.setting.eyeProtection && "eyeProtection", state.platform === IPlatform.PC && state.zoom > 0.8 && styles.PC, state.isPreView && styles.preViewDetail, state.isCbsView && styles.cbsViewDetail]}
         style={{
-          '--detailDataPaddingLeft': detailData.paddingLeft,
+          "--detailDataPaddingLeft": detailData.paddingLeft,
           paddingLeft: detailData.paddingLeft,
-          background: state.setting.camera ? `rgba(${state.setting.eyeProtection ? "253,244,229" : "255,255,255"} ,${state.setting.cameraOpacity / 100}) !important` : "",
+          background: state.setting.camera && state.modeType === 'evaluating' ? `rgba(${state.setting.eyeProtection ? "253,244,229" : "255,255,255"} ,${state.setting.cameraOpacity / 100}) !important` : "",
         }}
       >
         {bgJsonData.value ? 
@@ -497,14 +527,17 @@ export default defineComponent({
             style={{opacity: state.setting.camera && state.modeType === 'evaluating' ? state.setting.cameraOpacity / 100 : 1}} 
             class={styles.pageBg} 
             src={state.modeType === 'practise' ? bgJsonData.value[1] : state.modeType === 'evaluating' ? bgJsonData.value[2] : state.modeType === 'follow' ? bgJsonData.value[3] : ''} 
-          /> : null    
+          /> : <div 
+                style={{opacity: state.setting.camera && state.modeType === 'evaluating' ? state.setting.cameraOpacity / 100 : 1}}  
+                class={[styles.pageBg, styles[state.modeType]]} >
+              </div>    
         }
         {
-          state.modeType === 'evaluating' ? <>
+          state.modeType === 'evaluating' ? (<>
             <img src={bg2Left} class={styles.bg2Left} />
             <img src={bg2Right} class={styles.bg2Right} />
-          </> : null
-        }
+          </>
+        ) : null}
         {/* 骨架屏 */}
         {/* <Transition name="van-fade">
           {detailData.skeletonLoading && (
@@ -516,10 +549,7 @@ export default defineComponent({
         {/* 曲目加载错误的缺省 */}
         <EmptyMusic></EmptyMusic>
         {/** 功能按钮 */}
-        {
-          !state.isPreView && 
-          <div class={[styles.headHeight, headerColumnHide.value && styles.headHide]}>{state.musicRendered && <HeaderTop />}</div>
-        }
+        {!state.isPreView && <div class={["headHeight", styles.headHeight, headerColumnHide.value && styles.headHide]}>{state.musicRendered && <HeaderTop />}</div>}
         <div
           id="scrollContainer"
           style={{ ...fingerConfig.value.container }}
@@ -539,9 +569,9 @@ export default defineComponent({
           {!detailData.isLoading && 
             <MusicScore 
               ref={musicScoreRef}
-              musicColor={state.isPreView ? '#000000' : '#FFFFFF'}
+              musicColor={state.isPreView || state.isCbsView ? '#000000' : '#FFFFFF'}
               showPartNames={state.isCombineRender}
-              onRendered={handleRendered} 
+              onRendered={handleOnRendered} 
             > 
               {/* 旋律线关闭时候的 标题和作者 */}
               <AuthorName></AuthorName>
@@ -610,6 +640,7 @@ export default defineComponent({
           </>
         )}
         <Loading tipText={state.loadingText} />
+        <LoadingCss />
         <Popup
           zIndex={5050}
           teleport="body"

+ 1 - 1
src/page-instrument/view-detail/loading.tsx

@@ -18,7 +18,7 @@ export default defineComponent({
    setup(props) {
       return () =>
          (
-            <div class={[styles.loadingPop, state.isPreView && styles.isPreView]} style={{display:state.isLoading? "flex" : "none"}}>
+            <div class={[styles.loadingPop, (state.isPreView || state.isCbsView) && styles.isPreView]} style={{display:state.isLoading? "flex" : "none"}}>
                <img class={styles.lottie} src={animGif} />
                {/* <Vue3Lottie class={styles.lottie} animationData={animBg}></Vue3Lottie> */}
                <div class={styles.loadingTip}>{props.tipText}</div>

+ 21 - 0
src/page-instrument/view-detail/loadingCss.tsx

@@ -0,0 +1,21 @@
+import { defineComponent, ref} from "vue"
+import styles from "./index.module.less"
+import state from "/src/state"
+
+export const isLoadingCss = ref(false)
+export default defineComponent({
+   name: "loadingCss",
+   setup() {
+      return () => (
+         <div class={[styles.loadingPop, state.isPreView && styles.isPreView]} style={{ display: isLoadingCss.value ? "flex" : "none" }}>
+            <div class={styles.loadingCssBox}>
+                <div class={styles.loadingCssItem}></div>
+                <div class={styles.loadingCssItem}></div>
+                <div class={styles.loadingCssItem}></div>
+                <div class={styles.loadingCssItem}></div>
+            </div>
+            <div class={styles.loadingTip}>正在加载中,请稍等…</div>
+         </div>
+      )
+   }
+})

+ 7 - 2
src/page-instrument/view-detail/smoothAnimation/index.less

@@ -8,7 +8,8 @@
 #musicAndSelection.singleLineMusicBox{
     .smoothAnimationBox{
         display: flex;
-        align-items: center;
+        align-items: flex-end;
+        height: 1.8rem; // 与authorName高度对应
         &.smoothAnimationBoxHide{
             opacity: 0;
             visibility: hidden;
@@ -37,7 +38,11 @@
         position: fixed;
         // position: absolute;
         left: 0;
-        top: 36px;
+        top: var(--musicAndSelectionTop);
         width: 100vw;
     }
+    /* 一行谱翻页动画 */
+    #osmdCanvasPage1,#selectionBgBox,#selectionBox{
+        transition: transform 0.8s;
+    }
 }

+ 79 - 10
src/page-instrument/view-detail/smoothAnimation/index.ts

@@ -22,13 +22,14 @@ type smoothAnimationType = {
    osdmScrollDomWith: number
    osdmScrollDomOffsetLeft: number
    selectionBoxDom: null | HTMLElement
+   selectionBgBoxDom: null | HTMLElement
    batePos: pointsPosType
    pointsPos: pointsPosType
    translateXNum: number
    aveSpeed: number
 }
 
-const _numberOfSegments = 60 // 中间切割线的个数
+let _numberOfSegments = 56 // 中间切割线的个数
 const _canvasDomHeight = 60 // canvans 高度
 
 export const smoothAnimationState = {
@@ -45,6 +46,7 @@ export const smoothAnimationState = {
    osdmScrollDomWith: 0,
    osdmScrollDomOffsetLeft: 0,
    selectionBoxDom: null,
+   selectionBgBoxDom: null,
    batePos: [], // times 直接转换的数组
    pointsPos: [], // 筛选之后的点坐标数组
    translateXNum: 0, // 当前谱面的translateX的距离   谱面的位置信息 由translateX和scrollLeft的偏移一起决定
@@ -70,6 +72,10 @@ export function initSmoothAnimation() {
    const batePos = getPointsPosByBatePos()
    smoothAnimationState.batePos = batePos
    const batePos1 = dataFilter([...batePos])
+   console.log(batePos1, "排序之后的数据")
+   // 这里性能优化,对于超级长的曲子,_numberOfSegments值 动态变化
+   const numberOfSegments = parseInt(16000 / batePos1.length + "")
+   _numberOfSegments = Math.max(18, Math.min(_numberOfSegments, numberOfSegments))
    const batePos2 = createSmoothCurvePoints(batePos1, _numberOfSegments)
    smoothAnimationState.pointsPos = batePos2
    // 初始化旋律线
@@ -141,6 +147,7 @@ export function destroySmoothAnimation() {
       osdmScrollDomWith: 0,
       osdmScrollDomOffsetLeft: 0,
       selectionBoxDom: null,
+      selectionBgBoxDom: null,
       batePos: [],
       pointsPos: [],
       translateXNum: 0,
@@ -207,7 +214,39 @@ export function moveSmoothAnimation(progress: number, activeIndex: number, isMov
    // ) {
    //    return
    // }
-   isMoveOsmd && move_osmd(nowPointsPos)
+   //isMoveOsmd && move_osmd(nowPointsPos)
+   isMoveOsmd && pageTurn_osmd(nowPointsPos)
+}
+
+/**
+ * 谱面翻页逻辑
+ */
+let pageTurnLock = false
+function pageTurn_osmd(nowPointsPos: pointsPosType[0]) {
+   if (pageTurnLock) return
+   // 视口宽度
+   const clientWidth = smoothAnimationState.osdmScrollDomWith
+   let { left, right } = smoothAnimationState.smoothBotDom!.getBoundingClientRect()
+   left -= smoothAnimationState.osdmScrollDomOffsetLeft
+   right -= smoothAnimationState.osdmScrollDomOffsetLeft
+   if (right > clientWidth || left < 0) {
+      // 移动超过屏幕时候;移动小于屏幕时候
+      smoothAnimationState.translateXNum = 0
+      smoothAnimationState.osdmScrollDom!.scrollLeft = nowPointsPos.x - clientWidth * 0.1
+      moveTranslateXNum(smoothAnimationState.translateXNum)
+   } else if (right > clientWidth * 0.85) {
+      const osdmScrollDomScrollLeft = smoothAnimationState.osdmScrollDom?.scrollLeft || 0
+      const maxTranslateXNum = smoothAnimationState.canvasDomWith - smoothAnimationState.osdmScrollDomWith - osdmScrollDomScrollLeft
+      // 当还能翻页时候触发
+      if (maxTranslateXNum > smoothAnimationState.translateXNum) {
+         smoothAnimationState.translateXNum += clientWidth * 0.8 - state.times[0].bbox?.x
+         if (smoothAnimationState.translateXNum > maxTranslateXNum) {
+            smoothAnimationState.translateXNum = maxTranslateXNum
+         }
+         pageTurnLock = true
+         moveTranslateXNum(smoothAnimationState.translateXNum)
+      }
+   }
 }
 
 /**
@@ -279,8 +318,24 @@ function move_osmd(nowPointsPos: pointsPosType[0]) {
  */
 
 export function moveTranslateXNum(translateXNum: number) {
-   smoothAnimationState.osmdCanvasPageDom && (smoothAnimationState.osmdCanvasPageDom.style.transform = `translateX(-${translateXNum}px)`)
-   smoothAnimationState.selectionBoxDom && (smoothAnimationState.selectionBoxDom.style.transform = `translateX(-${translateXNum}px)`)
+   // translateXNum为0的时候不触发过度动画
+   if (translateXNum === 0) {
+      smoothAnimationState.osmdCanvasPageDom && (smoothAnimationState.osmdCanvasPageDom.style.transition = `none`)
+      smoothAnimationState.selectionBoxDom && (smoothAnimationState.selectionBoxDom.style.transition = `none`)
+      smoothAnimationState.selectionBgBoxDom && (smoothAnimationState.selectionBgBoxDom.style.transition = `none`)
+      smoothAnimationState.osmdCanvasPageDom && (smoothAnimationState.osmdCanvasPageDom.style.transform = `translateX(-${translateXNum}px)`)
+      smoothAnimationState.selectionBoxDom && (smoothAnimationState.selectionBoxDom.style.transform = `translateX(-${translateXNum}px)`)
+      smoothAnimationState.selectionBgBoxDom && (smoothAnimationState.selectionBgBoxDom.style.transform = `translateX(-${translateXNum}px)`)
+      smoothAnimationState.smoothBotDom?.offsetHeight
+      smoothAnimationState.osmdCanvasPageDom && (smoothAnimationState.osmdCanvasPageDom.style.transition = "")
+      smoothAnimationState.selectionBoxDom && (smoothAnimationState.selectionBoxDom.style.transition = "")
+      smoothAnimationState.selectionBgBoxDom && (smoothAnimationState.selectionBgBoxDom.style.transition = "")
+      pageTurnLock = false
+   } else {
+      smoothAnimationState.osmdCanvasPageDom && (smoothAnimationState.osmdCanvasPageDom.style.transform = `translateX(-${translateXNum}px)`)
+      smoothAnimationState.selectionBoxDom && (smoothAnimationState.selectionBoxDom.style.transform = `translateX(-${translateXNum}px)`)
+      smoothAnimationState.selectionBgBoxDom && (smoothAnimationState.selectionBgBoxDom.style.transform = `translateX(-${translateXNum}px)`)
+   }
 }
 
 /**
@@ -301,10 +356,15 @@ function createSmoothAnimation() {
    // osmdCanvasPage
    const osmdCanvasPageDom = document.querySelector("#osmdCanvasPage1") as HTMLElement
    smoothAnimationState.osmdCanvasPageDom = osmdCanvasPageDom
+   smoothAnimationState.osmdCanvasPageDom.addEventListener("transitionend", () => {
+      pageTurnLock = false
+   })
    // selectionBox
    setTimeout(() => {
       const selectionBoxDom = document.querySelector("#selectionBox") as HTMLElement
+      const selectionBgBoxDom = document.querySelector("#selectionBgBox") as HTMLElement
       smoothAnimationState.selectionBoxDom = selectionBoxDom
+      smoothAnimationState.selectionBgBoxDom = selectionBgBoxDom
    }, 0)
    // box
    const smoothAnimationBoxDom = document.createElement("div")
@@ -358,7 +418,7 @@ function getPointsPosByBatePos(): pointsPosType {
    const frequencyLineData = quantileScale(frequencyData, 8, _canvasDomHeight - 8) // 最小值和最大值
    const pointsPos = state.times.reduce((posArr: any[], item, index) => {
       // 当休止小节,可能当前音符在谱面上没有实际的音符(没有bbox)
-      if (item.bbox?.x != null && item.noteId != null) {
+      if (item.bbox?.x != null && ![-Infinity, Infinity].includes(item.bbox?.x) && item.noteId != null) {
          posArr.push({
             noteId: item.noteId,
             MeasureNumberXML: item.MeasureNumberXML,
@@ -371,7 +431,7 @@ function getPointsPosByBatePos(): pointsPosType {
             // 这里当第一个音符noteId为null,找不到前一个noteId,所以兼容一下
             noteId: item.noteId != null ? item.noteId : (posArr[posArr.length - 1]?.noteId != null ? posArr[posArr.length - 1]?.noteId : -1) + 0.01, // 这里+0.01 是制造一个假id
             MeasureNumberXML: item.MeasureNumberXML,
-            x: item.bbox?.x != null ? item.bbox.x : posArr[posArr.length - 1]?.x || 10,
+            x: item.bbox?.x != null && ![-Infinity, Infinity].includes(item.bbox?.x) ? item.bbox.x : posArr[posArr.length - 1]?.x || 10,
             y: _canvasDomHeight - frequencyLineData[index]
          })
       }
@@ -402,7 +462,7 @@ function quantileScale(data: number[], minRange = 0, maxRange = _canvasDomHeight
 /**
  * 使用传入的曲线的顶点坐标创建平滑曲线的顶点。
  * @param  {Array}   points  曲线顶点坐标数组,
- * @param  {Int}     numberOfSegments 平滑曲线 2 个顶点间的线段数,默认为 20
+ * @param  {Int}     numberOfSegments 平滑曲线 2 个顶点间的线段数
  * @return {Array}   平滑曲线的顶点坐标数组
  */
 function createSmoothCurvePoints(points: pointsPosType, numSegments: number) {
@@ -437,7 +497,8 @@ function initCanvasSmooth() {
    smoothDomCtx.lineCap = "round"
    smoothDomCtx.lineJoin = "round"
    // 根据坐标花线
-   drawLines(smoothDomCtx, smoothAnimationState.pointsPos, "rgba(255,255,255,0.6)")
+   const defaultColor = state.isCbsView ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.6)";
+   drawLines(smoothDomCtx, smoothAnimationState.pointsPos, defaultColor)
    smoothAnimationState.canvasSmoothDom = smoothDom
 }
 
@@ -458,9 +519,17 @@ function drawLines(context: CanvasRenderingContext2D, pointsPos: pointsPosType,
    context.lineWidth = 2
    context.strokeStyle = color
    context.beginPath()
-   context.moveTo(pointsPos[0].x, pointsPos[0].y)
+   // 记录上一个实际绘制的点
+   let lastDrawnPoint = pointsPos[0]
+   context.moveTo(lastDrawnPoint.x, lastDrawnPoint.y)
    for (let i = 1; i < pointsPos.length; i++) {
-      context.lineTo(pointsPos[i].x, pointsPos[i].y)
+      const currPoint = pointsPos[i]
+      const distance = Math.hypot(currPoint.x - lastDrawnPoint.x, currPoint.y - lastDrawnPoint.y)
+      // 如果两个点之间的距离大于阈值,才进行绘制
+      if (distance > 2) {
+         context.lineTo(currPoint.x, currPoint.y)
+         lastDrawnPoint = currPoint // 更新上一个实际绘制的点
+      }
    }
    context.stroke()
 }

+ 5 - 3
src/page-instrument/view-evaluat-report/component/share-top/index.module.less

@@ -332,8 +332,9 @@
         }
     }
     &.padPlayerBox{
-        width: 418px;
-        height: 248px;
+        width: 428px;
+        height: 252px;
+        padding: 12px;
         .audioBox{
             .audioBga1{
                 width: 112px;
@@ -489,7 +490,8 @@
     flex-wrap: wrap;
     margin-top: 16px;
     position: relative;
-    
+    padding-bottom: 22px;
+
     .item {
         width: 50%;
         display: flex;

+ 18 - 10
src/page-instrument/view-evaluat-report/component/share-top/index.tsx

@@ -119,7 +119,9 @@ export default defineComponent({
 			type propsType = { canvWidth: number; canvHeight: number; canvFillColor: string; lineColor: string; lineGap: number }
 			// canvas
 			const canvasCtx = canvasDom.getContext("2d")!
-			const { width, height } = canvasDom.getBoundingClientRect()
+      let { width, height } = canvasDom.getBoundingClientRect()
+      width = Math.ceil(width)
+      height = Math.ceil(height)
 			canvasDom.width = width
 			canvasDom.height = height
 			// audio
@@ -191,8 +193,8 @@ export default defineComponent({
             return
           }
           //analyser?.getByteFrequencyData(dataArray)
-          draw(generateMixedData(38), canvasCtx, {
-            lineGap: 3,
+          draw(generateMixedData(48), canvasCtx, {
+            lineGap: 2,
             canvWidth: width,
             canvHeight: height,
             canvFillColor: "transparent",
@@ -228,19 +230,25 @@ export default defineComponent({
 				pauseVisualDraw
 			}
 		}
-    function generateMixedData(size:number) {
+    function generateMixedData(size: number) {
       const dataArray = new Uint8Array(size);
       const baseNoiseAmplitude = 30;
       const minFrequency = 0.01;
       const maxFrequency = 0.2;
       const minAmplitude = 50;
       const maxAmplitude = 150;
+      let lastAmplitude = maxAmplitude;  // 初始振幅设置为最大值
+      let lastFrequency = minFrequency + Math.random() * (maxFrequency - minFrequency);
+    
       for (let i = 0; i < size; i++) {
-          const frequency = minFrequency + Math.random() * (maxFrequency - minFrequency);
-          const amplitude = minAmplitude + Math.random() * (maxAmplitude - minAmplitude);
+          const decayFactor = 1 - (i / size);  // 使振幅随时间递减
+          const amplitude = lastAmplitude * decayFactor + (Math.random() - 0.5) * 10;
+          const frequency = lastFrequency + (Math.random() - 0.5) * 0.01;
           const wave = amplitude * (0.5 + 0.5 * Math.sin(frequency * i));
           const noise = Math.floor(Math.random() * baseNoiseAmplitude) - baseNoiseAmplitude / 2;
           dataArray[i] = Math.min(255, Math.max(0, Math.floor(wave + noise)));
+          lastAmplitude += (amplitude - lastAmplitude) * 0.05;
+          lastFrequency += (frequency - lastFrequency) * 0.05;
       }
       return dataArray;
     }
@@ -493,7 +501,7 @@ export default defineComponent({
                     <span>玫红色音符:演奏偏高</span>
                   </div>
                   <div class={styles.item}>
-                    <Note fill="#4BED98" />
+                    <Note fill="#24E37E" />
                     <span>绿色音符:演奏正确</span>
                   </div>
                   <div class={styles.item}>
@@ -510,7 +518,7 @@ export default defineComponent({
                   </div>
                   <div class={styles.item}>
                     <Note fill="#7AB2FF" />
-                    <span>蓝色音符:时值不足</span>
+                    <span>蓝色音符:时值不足</span>
                   </div>
                   <div class={styles.item}>
                     <Note fill="#FF7B00" />
@@ -531,7 +539,7 @@ export default defineComponent({
                     <span>玫红色音符:演奏偏高</span>
                   </div>
                   <div class={styles.itemTone}>
-                    <i style={{ background: bgColors.right }}></i>
+                    <i style={{ background: "#24E37E" }}></i>
                     <span>绿色音符:演奏正确</span>
                   </div>
                   <div class={styles.itemTone}>
@@ -548,7 +556,7 @@ export default defineComponent({
                   </div>
                   <div class={styles.itemTone}>
                     <i style={{ background: bgColors.lack }}></i>
-                    <span>蓝色音符:时值不足</span>
+                    <span>蓝色音符:时值不足</span>
                   </div>
                   <div class={styles.itemTone}>
                     <i style={{ background: bgColors.slow }}></i>

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

@@ -442,7 +442,7 @@ export default defineComponent({
             </div>
           )}
         </Transition>
-        <div class={[styles.headHeight, detailData.headerHide && styles.headHide]} onClick={(e: Event) => e.stopPropagation()}>
+        <div class={["headHeight", styles.headHeight, detailData.headerHide && styles.headHide]} onClick={(e: Event) => e.stopPropagation()}>
           <Transition name="van-slide-down">{state.musicRendered && <ShareTop scoreData={scoreData} />}</Transition>
         </div>
         <div id="scrollContainer" class={[styles.container, !state.setting.displayCursor && "hideCursor"]}>

+ 24 - 5
src/page-instrument/view-figner/change-subject/index.tsx

@@ -10,6 +10,10 @@ export default defineComponent({
       type: Array,
       default: () => [],
     },
+    changeSubjectShow: {
+      type: Boolean,
+      default: false,
+    },
     subject: {
       type: String,
       default: "",
@@ -71,9 +75,27 @@ export default defineComponent({
       }
     };
 
+    const onConfirm = () => {
+      if (state.selectList.length > 0 && !state.instrumentCode) {
+        showToast("请选择乐器");
+        return;
+      }
+      emit("confirm", state.instrumentCode || state.subjectValue);
+    };
+
     onMounted(() => {
       console.log(props.subjectList, "subjectList", props.subject, query);
       selectItem();
+      document.addEventListener("keydown", (e: KeyboardEvent) => {
+        if (e.code === "Tab") {
+          e.stopPropagation();
+          e.preventDefault();
+          // onStartPlayState();
+          if (props.changeSubjectShow) {
+            onConfirm();
+          }
+        }
+      });
     });
     return () => (
       <div class={[styles.changeSubject, query.platform === "pc" && styles.changeSubjectPc]}>
@@ -151,11 +173,8 @@ export default defineComponent({
           <div
             class={[styles.btn, styles.confirmBtn]}
             onClick={() => {
-              if (state.selectList.length > 0 && !state.instrumentCode) {
-                showToast("请选择乐器");
-                return;
-              }
-              emit("confirm", state.instrumentCode || state.subjectValue);
+              console.log(state.selectList, state.instrumentCode);
+              onConfirm();
             }}
           ></div>
           {/* <Button

+ 4 - 0
src/page-instrument/view-figner/index.module.less

@@ -51,6 +51,10 @@
 
             &.tipHidden {
                 margin-right: -43%;
+
+                &>div {
+                    display: none;
+                }
             }
 
             .tipContentbox {

+ 32 - 26
src/page-instrument/view-figner/index.tsx

@@ -44,7 +44,8 @@ export default defineComponent({
   setup(props, { emit }) {
     const query = getQuery();
     const browsInfo = browser();
-    const code = mappingVoicePart(query.code, "INSTRUMENT");
+    const tempCode = query.code ? query.code.split(",")[0] : "";
+    const code = mappingVoicePart(tempCode, "INSTRUMENT");
     const subject = props.isComponent ? props.subject || "pan-flute" : code || "pan-flute";
     const data = reactive({
       linkSource: query.linkSource, // 来源,目前只有课件里使用
@@ -341,39 +342,17 @@ export default defineComponent({
           };
           if (Array.isArray(row.instruments)) {
             row.instruments.forEach((i: any) => {
+              const code = i.code ? i.code.split(",") : [];
               tempList.children.push({
                 text: i.name,
                 id: i.id,
-                value: mappingVoicePart(i.code, "INSTRUMENT"),
+                value: mappingVoicePart(code[0] || "", "INSTRUMENT"),
               });
             });
           }
           tempSubjects.push(tempList);
-          // if (row.instruments && row.instruments.length > 0) {
-          //   if (row.instruments.length > 1) {
-          //     row.instruments.forEach((i: any) => {
-          //       tempList.children.push({
-          //         text: i.name,
-          //         id: i.id,
-          //         value: mappingVoicePart(i.code, "INSTRUMENT"),
-          //       });
-          //     });
-          //   } else {
-          //     const singleRow = row.instruments[0];
-          //     if (singleRow.code) {
-          //       tempList.value = mappingVoicePart(singleRow.code, "INSTRUMENT");
-          //       tempList.id = singleRow.id;
-          //     }
-          //   }
-          // }
-          // data.subjects.push(tempList);
         });
         console.log(data.subject, "data.subject");
-        // tempSubjects.forEach((item: any) => {
-        //   if (item.value === data.subject && item.children?.length > 1) {
-        //     data.subject = item.children[0].value;
-        //   }
-        // });
         data.subjects = tempSubjects;
       } catch (e) {
         //
@@ -738,6 +717,17 @@ export default defineComponent({
             __init();
           }, 100);
         }
+      } else if (res.data.api === "startPlayState") {
+        onStartPlayState();
+      }
+    };
+
+    const onStartPlayState = () => {
+      if (!localStorage.getItem("fingerGuideKey")) return;
+      if (!(props.show && !data.loading && !data.loadingSoundFonts)) return;
+      if (data.changeSubjectShow) return;
+      if (data.fingeringMode === "fingeringMode" || data.fingeringMode === "listenMode") {
+        onActionPlay();
       }
     };
 
@@ -938,7 +928,7 @@ export default defineComponent({
       playAction.listenLock = true; // 锁
       playAction.listenTipsStatus = true;
       // 设置并保存示例数据
-      let randomIndex = data.notes.findIndex((item: any) => item.realKey === 67); // Math.floor(Math.random() * data.notes.length);
+      let randomIndex = data.notes.findIndex((item: any) => item.realKey === 69); // Math.floor(Math.random() * data.notes.length);
 
       playAction.exampleAnser = data.notes[randomIndex];
       data.realKey = playAction.exampleAnser.realKey;
@@ -1053,6 +1043,21 @@ export default defineComponent({
       window.addEventListener("resize", onResize);
       const fingeringContainer = document.getElementById("fingeringContainer");
       fingeringContainer?.addEventListener("wheel", handleWheel);
+      document.addEventListener("keydown", (e: KeyboardEvent) => {
+        if (e.code === "Tab") {
+          e.stopPropagation();
+          e.preventDefault();
+          // onStartPlayState();
+          // 判断是否在应用中
+          window.parent.postMessage(
+            {
+              api: "documentBodyKeyup",
+              code: "Tab",
+            },
+            "*"
+          );
+        }
+      });
     });
 
     onUnmounted(() => {
@@ -2099,6 +2104,7 @@ export default defineComponent({
             }}
           >
             <ChangeSubject
+              changeSubjectShow={data.changeSubjectShow}
               subjectList={data.subjects}
               subject={data.subject}
               onClose={() => (data.changeSubjectShow = false)}

+ 375 - 128
src/state.ts

@@ -1,4 +1,4 @@
-import { closeToast, showToast } from "vant";
+import { closeToast, showToast, showConfirmDialog } from "vant";
 import { nextTick, reactive, watch } from "vue";
 import { OpenSheetMusicDisplay } from "../osmd-extended/src";
 import { metronomeData } from "./helpers/metronome";
@@ -10,16 +10,18 @@ import { audioListStart, getAudioCurrentTime, getAudioDuration, setAudioCurrentT
 import { toggleFollow } from "./view/follow-practice";
 import { browser, setStorageSpeed, setGlobalData } from "./utils";
 import { api_cloudGetMediaStatus, api_createMusicPlayer, api_cloudChangeSpeed, api_cloudSuspend, api_cloudSetCurrentTime, api_cloudDestroy } from "./helpers/communication";
-import { verifyCanRepeat, getDuration } from "./helpers/formateMusic";
+import { verifyCanRepeat, getDuration, xmlAddPartName } from "./helpers/formateMusic";
 import { getMusicSheetDetail } from "./utils/baseApi"
 import { getQuery } from "/src/utils/queryString";
 import { followData, skipNotePractice } from "/src/view/follow-practice/index"
-import { changeSongSourceByBate } from "/src/view/audio-list"
+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 { 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"
 
 const query: any = getQuery();
 
@@ -317,6 +319,8 @@ const state = reactive({
   extConfigJson: {} as any,
   /** 扩展样式字段 */
   extStyleConfigJson: {} as any,
+  /** 简谱扩展样式字段 */
+  extJianStyleConfigJson: {} as any,  
   /** 是否开启节拍器(mp3节拍器) */
   isOpenMetronome: false,
   /** 演唱模式是否开启节拍器(mp3节拍器) */
@@ -368,6 +372,8 @@ const state = reactive({
   needTick: false,
   /** 演唱模式是否需要节拍器 */
   needSingTick: false,
+  /** 是否能使用节拍器  */
+  isMixBeat: true,
   /** 曲谱实例 */
   osmd: null as unknown as OpenSheetMusicDisplay,
   /**是否是特殊乐谱类型, 主要针对管乐迷  */
@@ -439,6 +445,12 @@ const state = reactive({
   secondEvXmlBeginTime: 0,
   /** evxml等待播放的时间集合,多遍反复播放,会有多个timegap(前奏)时间 */
   evXmlBeginArr: [] as any,
+  /** evxml的曲子是否有times */
+  xmlHasTimes: false,  
+  /** evxml的曲子是否有timeGap */
+  xmlHasTimeGap: false,
+  /** 有timeGap的曲子,是从哪个小节开始循环的,默认从第一小节开始循环 */
+  timegapRepeatMeasureIndex: 1,
   /** 指法信息 */
   fingeringInfo: {} as IFingering,
   /** 滚动容器的ID */
@@ -497,6 +509,8 @@ const state = reactive({
   beatStartTime: 0,
   /** 是否为详情预览模式 */
   isPreView: false,
+  /** 是否为内容平台预览模式 */
+  isCbsView: false,  
   /** 是否为评测报告模式 */
   isEvaluatReport: false,
   /** midi播放器是否初始化中 */
@@ -512,7 +526,7 @@ const state = reactive({
   /** 音频文件是否加载完成 */
   audioDone: false,
   /** 是否为单行谱渲染模式 */
-  isSingleLine: true,
+  isSingleLine: false,
   /** 是否是evxml */
   isEvxml: false,
   noTimes: [] as any,
@@ -560,6 +574,11 @@ const state = reactive({
   workSectionNeedReset: false,
   /** 旋律线开关 */
   melodyLine: true,
+  /** 是否是C调,切换到唱名时,只有C调所有的谱面类型都可以播放唱名文件;其它调的只有首调可以播放唱名,因为唱名是按照C调制作的,没有其它调的唱名文件 */
+  isCTone: false,
+  evxmlAddPartName: false, // 妙极客的部分曲子没有part-name,需要自行添加的part-name
+  /** 乐器id */
+  instrumentId: null,
 });
 const browserInfo = browser();
 let offset_duration = 0;
@@ -607,6 +626,7 @@ const autoResetPlay = () => {
   skipNotePlay(0, true);
   // 没有开启自动重播, 不是练习模式
   if (!state.setting.repeatAutoPlay) return;
+  offsetTop = 0;
   scrollViewNote();
   setTimeout(() => {
     togglePlay("play");
@@ -638,7 +658,7 @@ export const onEnded = () => {
 // 根据当前小节动态设置,右上角展示的速度
 const dynamicShowPlaySpeed = (index: number) => {
   if (!headerColumnHide.value) {
-    console.log('动态计算速度')
+    // console.log('动态计算速度')
     const item: any = state.times[index];
     if (item && item.measureSpeed ) {
       // console.log('速度1',item.measureSpeed)
@@ -751,6 +771,7 @@ const handlePlaying = () => {
 };
 /** 跳转到指定音符开始播放 */
 export const skipNotePlay = async (itemIndex: number, isStart = false) => {
+  if (state.isPreView) return;
   console.log('点击音符')
   const item = state.times[itemIndex];
   let itemTime = item.time;
@@ -795,7 +816,11 @@ export const togglePlay = async (playState: "play" | "paused", isForceCLoseToast
   // 播放之前  当为评测模式和不为MIDI时候按  是否禁用节拍器  切换音源
   if (playState === 'play' && state.modeType === "practise" && state.playMode !== "MIDI") {
     console.log("设置音源")
-    changeSongSourceByBate(metronomeData.disable)
+    changeSongSourceByBeat(metronomeData.disable)
+  }
+  if (playState === 'play') {
+    offsetTop = 0;
+    scrollViewNote();
   }
   // midi播放
   if (state.isAppPlay) {
@@ -1000,13 +1025,16 @@ export const gotoNext = (note: any, skipNote?: boolean) => {
   const num = note.i;
 
   if (state.activeNoteIndex === note.i) {
-    try {
-      setCursorPosition(note, state.osmd.cursor, 'init');
-    } catch (error) {
-      console.log(error);
-    }
+    /* 没有光标了  这里就算加上光标也要性能优化 */
+    // try {
+    //   setCursorPosition(note, state.osmd.cursor, 'init');
+    // } catch (error) {
+    //   console.log(error);
+    // }
     // 重置 或者切换演奏演唱的时候 可能出现 state.activeNoteIndex === note.i的情况 执行
-    fillWordColor();
+    if(state.playState === "paused"){
+      fillWordColor();
+    }
     if (state.isSingleLine && state.playState === "paused") {
       moveSvgDom(skipNote);
     }
@@ -1035,14 +1063,15 @@ export const gotoNext = (note: any, skipNote?: boolean) => {
   } else {
     gotoCustomNote(num);
   }
-  try {
-    setCursorPosition(note, state.osmd.cursor, 'refresh');
-  } catch (error) {
-    console.log(error);
-  }
+  /* 取消光标了 */
+  // try {
+  //   setCursorPosition(note, state.osmd.cursor, 'refresh');
+  // } catch (error) {
+  //   console.log(error);
+  // }
   fillWordColor();
   // 一行谱,需要滚动小节
-  if (state.isSingleLine) {
+  if (state.isSingleLine && state.playState === "paused") {
     moveSvgDom(skipNote);
   }
   scrollViewNote();
@@ -1053,7 +1082,7 @@ export const getNote = (currentTime: number) => {
   const len = state.times.length;
   /** 播放超过了最后一个音符的时间,直接结束, 2秒误差 */
   if (currentTime > times[len - 1].endtime + 2 && !state.isAppPlay && !state.isSimplePage) {
-    onEnded();
+    // onEnded();
     return;
   }
   let _item = null as any;
@@ -1093,9 +1122,12 @@ export const handleResetPlay = () => {
     audioData.progress = 0
   }
   // 如果是作业模式,不还原速度
-  if (!query.workRecord) {
-    resetBaseRate();
-  }
+  /**
+   * #TODO:2024.09.14,业务需求变更,重播不还原用户设置的速度
+   */
+  // if (!query.workRecord) {
+  //   resetBaseRate();
+  // }
   resetPlaybackToStart();
   // 如果是暂停, 直接播放
   togglePlay("play");
@@ -1263,14 +1295,26 @@ let offsetTop = 0;
  * @param isScroll 可选: 强制滚动到顶部, 默认: false
  * @returns void
  */
-export const scrollViewNote = () => {
-  const cursorElement = document.getElementById("cursorImg-0")!;
+export const scrollViewNote = (resetTop?: boolean) => {
+  // const cursorElement = document.getElementById("cursorImg-0")!;
+  const noteId = state.times[state.activeNoteIndex].id;
+  if (state.isSingleLine) {
+    return;
+  }
+  if (state.activeNoteIndex <= 1 || resetTop) {
+    offsetTop = 0;
+  }
+  const domId = "vf" + noteId;
+  const cursorElement: any = noteId ? document.querySelector(`[data-vf=${domId}]`)?.parentElement : document.getElementById('restDot')?.parentElement;
   const musicAndSelection = document.getElementById(state.scrollContainer)!;
-  if (!cursorElement || !musicAndSelection || offsetTop === cursorElement.offsetTop) return;
-  offsetTop = cursorElement.offsetTop;
-  if (offsetTop > 50) {
+  // offsetTop = musicAndSelection.scrollTop || offsetTop;
+  const noteCenterOffsetTop = cursorElement ? cursorElement?.offsetTop + (cursorElement?.offsetHeight/2) : 0;
+  // console.log('滑动',offsetTop, noteCenterOffsetTop)
+  if (!cursorElement || !noteCenterOffsetTop || !musicAndSelection || offsetTop === noteCenterOffsetTop || Math.abs(offsetTop - noteCenterOffsetTop) < 30) return;
+  offsetTop = noteCenterOffsetTop;
+  if (offsetTop > 100) {
     musicAndSelection.scrollTo({
-      top: (offsetTop - 50) * state.musicZoom,
+      top: (offsetTop - 100) * state.musicZoom,
       behavior: "smooth",
     });
   } else {
@@ -1333,13 +1377,30 @@ const getMusicInfo = async (res: any) => {
   state.isScoreRender = res.data?.isScoreRender
   // 是否默认显示总谱
   state.defaultScoreRender = res.data?.defaultScoreRender
-  const partIndex = query["part-index"] ? parseInt(query["part-index"]) : -1 // -1为partIndex没有值的时候
+  // 是否显示节拍器
+  state.isMixBeat = res.data?.isMixBeat
+  let partIndex = query["part-index"] ? parseInt(query["part-index"]) : -1 // -1为partIndex没有值的时候
+  // 如果是评测报告,会有默认的分轨index
+  if (state.isEvaluatReport) {
+    partIndex = state.partIndex;
+  }
+  // 布置作业 取作业的乐器id
+  const workRecord = query.workRecord
+  let workRecordInstrumentId:undefined | string
+  if(workRecord){
+    const res = await api_lessonTrainingTrainingStudentDetail(workRecord);
+    if (res?.code === 200) {
+      workRecordInstrumentId = res.data?.instrumentId
+    }
+  }
   /* 获取声轨列表 */
-  const xmlString = await fetch(res.data.xmlFileUrl).then((response) => response.text());
+  let xmlString = await fetch(res.data.xmlFileUrl).then((response) => response.text());
+  xmlString = xmlAddPartName(xmlString);
   downloadXmlStr.value = xmlString //给musice-score 赋值xmlString 以免加载2次
   const tracks = xmlToTracks(xmlString) //获取声轨列表
   // 设置音源  track 为当前的声轨 index为当前的
-  const { track, index, musicalInstrumentId } = state.isSimplePage ? { track:tracks[0], index:0, musicalInstrumentId: '' } : initMusicSource(res.data, tracks, partIndex)
+  const { track, index, musicalInstrumentId } = state.isSimplePage ? { track:tracks[0], index:0, musicalInstrumentId: '' } : initMusicSource(res.data, tracks, partIndex, workRecordInstrumentId)
+  // 这里返回的track可能和实际的对不上,所以重新筛选一下
   const realTrack = musicalInstrumentId && res.data?.musicalInstruments?.length ? res.data?.musicalInstruments.find((item: any) => item?.id == musicalInstrumentId)?.code?.split(',')?.[0] : '';
   const musicInfo = {
     ...res.data,
@@ -1361,32 +1422,35 @@ function xmlToTracks(xmlString: string) {
   }, []);
 }
 // 设置音源
-function initMusicSource(data: any, tracks: string[], partIndex: number) {
+function initMusicSource(data: any, tracks: string[], partIndex: number, workRecordInstrumentId?: string) {
   let track:string,index:number, musicalInstrumentId: string
-  const instrumentId = query.instrumentId || storeData.user?.instrumentId
+  const instrumentId = workRecordInstrumentId || query.instrumentId || storeData.user?.instrumentId
+  state.instrumentId = instrumentId;
   let { musicSheetType, isAllSubject, musicSheetSoundList, musicSheetAccompanimentList } = data
   musicSheetSoundList || (musicSheetSoundList = [])
   musicSheetAccompanimentList || (musicSheetAccompanimentList = [])
   let musicObj, accompanyObj, fanSongObj, banSongObj
    /* 独奏 */
   if (musicSheetType === "SINGLE") {
-    // 适用声部(isAllSubject)为true 时候没有乐器只有一个原音;当前用户有乐器就匹配  不然取第一个原音
+    accompanyObj = musicSheetAccompanimentList.find((item: any) => {
+      return item.audioPlayType === "PLAY"
+    })
+    // 是否全声部(isAllSubject)为true 时候没有乐器只有一个原音(比如节奏练习,这个曲子全部乐器都支持);当前用户有乐器就匹配  不然取第一个原音
     musicObj = musicSheetSoundList.find((item: any) => {
-      return (isAllSubject || !instrumentId) ? item.audioPlayType === "PLAY" : (item.audioPlayType === "PLAY" && item.musicalInstrumentId == instrumentId)
+      return isAllSubject ? item.audioPlayType === "PLAY" : (item.audioPlayType === "PLAY" && item.musicalInstrumentId == instrumentId)
     })
-    // 因为可能根据学生的乐器id也找不到曲目所以尝试取第一个
-    musicObj || (musicObj = musicSheetSoundList.find((item: any) => {
-      return item.audioPlayType === "PLAY"
-    }))
+    // 当没有找到原音的时候,并且instrumentId没有值的时候,取默认第一个乐器
+    if(!musicObj && !instrumentId){
+      musicObj = musicSheetSoundList.find((item: any) => {
+        return item.audioPlayType === "PLAY"
+      })
+    }
     fanSongObj = musicSheetSoundList.find((item: any) => {
       return item.audioPlayType === "SING"
     })
     banSongObj = musicSheetAccompanimentList.find((item: any) => {
       return item.audioPlayType === "SING"
     })
-    accompanyObj = musicSheetAccompanimentList.find((item: any) => {
-      return item.audioPlayType === "PLAY"
-    })
     track = musicObj?.track   //没有原音的时候track为空 不显示指法
     index = tracks.findIndex(item => {
       return item === track
@@ -1399,24 +1463,21 @@ function initMusicSource(data: any, tracks: string[], partIndex: number) {
         // 总谱渲染
         state.isCombineRender = true
         state.partListNames = tracks
-        // 总谱演唱模式是 范唱
-        fanSongObj = musicSheetAccompanimentList.find((item: any) => {
+        banSongObj = musicSheetAccompanimentList.find((item: any) => {
           return item.audioPlayType === "SING"
         })
-        // 先取scoreAudioFileUrl的值
-        if(fanSongObj?.scoreAudioFileUrl){
-          fanSongObj.audioFileUrl = fanSongObj.scoreAudioFileUrl
-          fanSongObj.audioBeatMixUrl = fanSongObj.scoreAudioBeatMixUrl
+        // 总谱演唱模式是 范唱,取banSongObj 里面的scoreAudioFileUrl字段
+        // 先取scoreAudioFileUrl的值 如果 没有就是空
+        if(banSongObj){
+          fanSongObj = {
+            audioFileUrl: banSongObj.scoreAudioFileUrl,
+            audioBeatMixUrl: banSongObj.scoreAudioBeatMixUrl
+          }
         }
         // 总谱演奏模式是 伴奏
         accompanyObj = musicSheetAccompanimentList.find((item: any) => {
           return item.audioPlayType === "PLAY"
         })
-        // 先取scoreAudioFileUrl的值
-        if(accompanyObj?.scoreAudioFileUrl){
-          accompanyObj.audioFileUrl = accompanyObj.scoreAudioFileUrl
-          accompanyObj.audioBeatMixUrl = accompanyObj.scoreAudioBeatMixUrl
-        }
         track = "总谱"
         index = 999
         musicalInstrumentId = ''
@@ -1469,18 +1530,21 @@ function initMusicSource(data: any, tracks: string[], partIndex: number) {
     state.mingSong = fanSongObj?.solmizationFileUrl
     state.mingSongGirl = fanSongObj?.femaleSolmizationFileUrl
   }
-  Object.assign(state.beatSong, {
-    music: musicObj?.audioBeatMixUrl,
-    accompany: accompanyObj?.audioBeatMixUrl,
-    fanSong: fanSongObj?.audioBeatMixUrl,
-    banSong: banSongObj?.audioBeatMixUrl
-  })
-  // 如果没有男唱名
-  if(!fanSongObj?.solmizationBeatUrl){
-    state.beatSong.mingSong = fanSongObj?.femaleSolmizationBeatUrl
-  }else{
-    state.beatSong.mingSong = fanSongObj?.solmizationBeatUrl
-    state.beatSong.mingSongGirl = fanSongObj?.femaleSolmizationBeatUrl
+  // 当使用节拍器的时候才加载节拍器音频
+  if(state.isMixBeat) {
+    Object.assign(state.beatSong, {
+      music: musicObj?.audioBeatMixUrl,
+      accompany: accompanyObj?.audioBeatMixUrl,
+      fanSong: fanSongObj?.audioBeatMixUrl,
+      banSong: banSongObj?.audioBeatMixUrl
+    })
+    // 如果没有男唱名
+    if(!fanSongObj?.solmizationBeatUrl){
+      state.beatSong.mingSong = fanSongObj?.femaleSolmizationBeatUrl
+    }else{
+      state.beatSong.mingSong = fanSongObj?.solmizationBeatUrl
+      state.beatSong.mingSongGirl = fanSongObj?.femaleSolmizationBeatUrl
+    }
   }
   return {
     index,
@@ -1489,20 +1553,57 @@ function initMusicSource(data: any, tracks: string[], partIndex: number) {
   }
 }
 const setState = (data: any, index: number) => {
+  // 获取当前模式 声部切换用
+  const localStoragePlayType = localStorage.getItem("musicScorePlayType")
+  if(localStoragePlayType) {
+    localStorage.removeItem("musicScorePlayType")
+    const fields = localStoragePlayType.split(',')
+    state.playType = fields[0] as any
+    state.playSource = fields[1] as any
+  }
   // 根据当前文件有没有 设置当前的播放模式
-  if(!state.music){
-    if(state.accompany){
-      state.playSource = "background"
+  const playObj = {
+    "play_music":"music",
+    "play_background":"accompany",
+    "sing_music":"fanSong",
+    "sing_background":"banSong",
+    "sing_mingSong":"mingSong",
+  } as any
+  // 当前缓存 有值的时候 用这个,没有的时候 走筛选
+  // @ts-ignore
+  if(!state[playObj[`${state.playType}_${state.playSource}`]]){
+    if(state.playType === "play"){
+      if(state.music){
+        state.playSource = "music"
+      }else if(state.accompany){
+        state.playSource = "background"
+      }else{
+        if(state.fanSong){
+          state.playType = "sing"
+          state.playSource = "music"
+        }else if(state.banSong){
+          state.playType = "sing"
+          state.playSource = "background"
+        }else if(state.mingSong){
+          state.playType = "sing"
+          state.playSource = "mingSong"
+        }
+      }
     }else{
       if(state.fanSong){
-        state.playType = "sing"
         state.playSource = "music"
       }else if(state.banSong){
-        state.playType = "sing"
         state.playSource = "background"
       }else if(state.mingSong){
-        state.playType = "sing"
         state.playSource = "mingSong"
+      }else{
+        if(state.music){
+          state.playType = "play"
+          state.playSource = "music"
+        }else if(state.accompany){
+          state.playType = "play"
+          state.playSource = "background"
+        }
       }
     }
   }
@@ -1520,12 +1621,12 @@ const setState = (data: any, index: number) => {
   /**
    * 单曲,指法根据用户当前的乐器来显示,如果没有则取musicSheetSoundList第一个track
    */
-  const currentInstrumentId = query.instrumentId || storeData.user?.instrumentId;
-  let musicalCode = !currentInstrumentId ? data.musicSheetSoundList?.find((item:any)=>{ return item.audioPlayType === "PLAY" })?.track || '' : data.musicSheetSoundList?.find((item: any) => item?.musicalInstrumentId == currentInstrumentId && item.audioPlayType === "PLAY")?.track || '';
-  const pitchSubject = musicalInstrumentCodeInfo.find((n) => n.code.toLocaleLowerCase() === subjectCode.toLocaleLowerCase())
-  const pitchMusical = musicalInstrumentCodeInfo.find((n) => n.code.toLocaleLowerCase() === musicalCode.toLocaleLowerCase())
-  state.subjectCodeId = pitchSubject ? pitchSubject.id : 0
-  state.musicalCodeId = pitchMusical ? pitchMusical.id : 0
+  // const currentInstrumentId = query.instrumentId || storeData.user?.instrumentId;
+  // let musicalCode = !currentInstrumentId ? data.musicSheetSoundList?.find((item:any)=>{ return item.audioPlayType === "PLAY" })?.track || '' : data.musicSheetSoundList?.find((item: any) => item?.musicalInstrumentId == currentInstrumentId && item.audioPlayType === "PLAY")?.track || '';
+  // const pitchSubject = musicalInstrumentCodeInfo.find((n) => n.code.toLocaleLowerCase() === subjectCode.toLocaleLowerCase())
+  // const pitchMusical = musicalInstrumentCodeInfo.find((n) => n.code.toLocaleLowerCase() === musicalCode.toLocaleLowerCase())
+  // state.subjectCodeId = pitchSubject ? pitchSubject.id : 0
+  // state.musicalCodeId = pitchMusical ? pitchMusical.id : 0
   state.categoriesId = data.musicCategoryId;
   state.categoriesName = data.musicTagNames;
   // state.enableEvaluation = data.isEvaluated ? true : false;
@@ -1579,9 +1680,11 @@ const setState = (data: any, index: number) => {
   state.isConcert = data.musicSheetType === "CONCERT" ? true : false;
   // multiTracksSelection 返回为空,默认代表全部分轨
   state.canSelectTracks = data.multiTracksSelection === "null" || data.multiTracksSelection === "" || data.multiTracksSelection === null ? [] : data.multiTracksSelection?.split(',');
+  state.canSelectTracks = state.canSelectTracks.map((item: any)=>item.trim())
   // 开启预备小节
   state.isOpenPrepare = true;
   state.extStyleConfigJson = data.extStyleConfigJson || {}
+  state.extJianStyleConfigJson = data.extJianStyleConfigJson || {}
   // console.log("🚀 ~ state.subjectId:", state.subjectId, state.track as any , state.subjectId)
   // 是否打击乐
   /**
@@ -1620,7 +1723,9 @@ const setState = (data: any, index: number) => {
   // 如果是PC端,放大曲谱
   state.platform = query.platform?.toLocaleUpperCase() || "";
   if (state.platform === IPlatform.PC) {
-    state.zoom = query.zoom || 1.5;
+    if (query.zoom <= 1) {
+      state.zoom = query.zoom || state.zoom;
+    }
     state.enableEvaluation = false;
   }
   /**
@@ -1629,13 +1734,13 @@ const setState = (data: any, index: number) => {
    * 能否转谱:先取isConvertibleScore字段,如果isConvertibleScore为true,则取musicalInstruments字段匹配的当前分轨的transferFlag,都为true则可以转谱
    * 
    */
-  let pitchTrack = null
-  if (state.isConcert) {
-    musicalCode = musicalInstrumentCodeInfo.find((item: any) => item.id === state.musicalCodeId)?.code
-    pitchTrack = data.musicalInstruments?.find((item: any) => item.code?.split(',')[0] === musicalCode)
-  } else {
-    pitchTrack = data.musicalInstruments?.find((item: any) => item.code?.split(',')[0] === musicalCode)
-  }
+  // let pitchTrack = null
+  // if (state.isConcert) {
+  //   musicalCode = musicalInstrumentCodeInfo.find((item: any) => item.id === state.musicalCodeId)?.code
+  //   pitchTrack = data.musicalInstruments?.find((item: any) => item.code?.split(',')[0] === musicalCode)
+  // } else {
+  //   pitchTrack = data.musicalInstruments?.find((item: any) => item.code?.split(',')[0] === musicalCode)
+  // }
   let musicalRenderType = ''
   // if (pitchTrack?.defaultScore) {
   //   musicalRenderType = pitchTrack?.defaultScore === 'STAVE' ? 'staff' : pitchTrack?.defaultScore === 'JIAN' ? 'fixedTone' : pitchTrack?.defaultScore === 'FIRST' ? 'firstTone' : ''
@@ -1778,22 +1883,49 @@ export const addNoteBBox = (list: any[]) => {
 }
 
 // 给歌词和音符添加动态颜色
+let prevActiveNoteIndex = -1 // 上一个激活的
 export const fillWordColor = () => {
-  // console.log('当前音符',state.activeNoteIndex)
-  state.times.forEach((item: any, idx: number) => {
-    const svgEl = document.getElementById(`vf-${state.times[idx]?.svgElement?.attrs?.id}`)
-    const stemEl = document.getElementById(`vf-${state.times[idx]?.svgElement?.attrs?.id}-stem`)
-    const stemLine = document.getElementById(`vf-${state.times[idx]?.svgElement?.attrs?.id}-lines`)
-    if ((item.i === state.activeNoteIndex || item.id === state.times[state.activeNoteIndex].id) && item.svgElement) {
-      svgEl?.classList.add('noteActive')
-      stemEl?.classList.add('noteActive')
-      stemLine?.classList.add('noteActive')
-    } else {
-      svgEl?.classList.remove('noteActive')
-      stemEl?.classList.remove('noteActive')
-      stemLine?.classList.remove('noteActive')
+  // console.log('当前音符',state.activeNoteIndex,prevActiveNoteIndex)
+  if(prevActiveNoteIndex !== -1) {
+    const prevActiveNoteId = state.times[prevActiveNoteIndex]?.svgElement?.attrs?.id
+    const svgEl = document.getElementById(`vf-${prevActiveNoteId}`)
+    const stemEl = document.getElementById(`vf-${prevActiveNoteId}-stem`)
+    const stemLine = document.getElementById(`vf-${prevActiveNoteId}-lines`)
+    svgEl?.classList.remove('noteActive')
+    stemEl?.classList.remove('noteActive')
+    // stemLine?.classList.remove('noteActive')
+    svgEl?.parentElement?.classList.remove('voiceActive')
+    const si = state.times[prevActiveNoteIndex].si || 0;
+    svgEl?.parentElement?.querySelectorAll('rect')?.forEach((item: any) => {
+      item?.classList.remove('rectActive');
+    })
+    // svgEl?.parentElement?.querySelectorAll('rect')?.[si]?.classList.remove('rectActive');
+  }
+  const activeNoteId = state.times[state.activeNoteIndex]?.svgElement?.attrs?.id
+  const svgEl = document.getElementById(`vf-${activeNoteId}`)
+  const stemEl = document.getElementById(`vf-${activeNoteId}-stem`)
+  const stemLine = document.getElementById(`vf-${activeNoteId}-lines`)
+  svgEl?.classList.add('noteActive')
+  stemEl?.classList.add('noteActive')
+  // stemLine?.classList.add('noteActive')
+  // 如果是简谱、固定调,需要把音符后面跟着的"-"也添加颜色
+  if (state.musicRenderType === EnumMusicRenderType.firstTone || state.musicRenderType === EnumMusicRenderType.fixedTone) {
+    // const parentVoice = svgEl?.parentElement;
+    // 简谱模式下,二分音符和全音符才显示音符右侧的"-"
+    if (state.times[state.activeNoteIndex].noteElement?.length?.realValue >= 0.5) {
+      // 如果是二分音符,只亮该音符后面那个"-",本小节其它的"-"不亮
+      if (state.times[state.activeNoteIndex].noteElement?.length?.realValue === 0.5) {
+        const si = state.times[state.activeNoteIndex].si || 0;
+        const halfNotes = state.times[state.activeNoteIndex].measures.filter((item: any) => item?.noteElement?.length?.realValue === 0.5) || [];
+        const sIdx = halfNotes?.findIndex((item: any) => item?.noteElement === state.times[state.activeNoteIndex]?.noteElement);
+        const filterRects = svgEl?.parentElement?.querySelectorAll('rect')?.length ? Array.from(svgEl?.parentElement?.querySelectorAll('rect')).filter(item => item.parentElement === svgEl?.parentElement) : [];
+        filterRects?.[sIdx]?.classList.add('rectActive');
+      } else {
+        svgEl?.parentElement?.classList.add('voiceActive');
+      }
     }
-  })
+  }
+  prevActiveNoteIndex = state.activeNoteIndex
 
   // 给当前匹配到的歌词添加颜色
   const currentNote = state.times[state.activeNoteIndex];
@@ -1808,6 +1940,10 @@ export const fillWordColor = () => {
     const onlyOneLyric = currentNote.measures?.every((item: any) => item?.formatLyricsEntries?.length <= 1);
     if ((index === currentNote.repeatIdx && currentNote.repeatIdx + 1 == lyricIndex) || (currentNote.repeatIdx != index && !onlyOneLyric && currentNote.repeatIdx + 1 == lyricIndex) || (currentNote.repeatIdx > 0 && currentNote.formatLyricsEntries?.length === 1 && onlyOneLyric)) {
       lyric?.classList.add('lyricActive')
+    } 
+    // bug: #11189,兼容处理需要唱4遍,但是只打了2遍歌词的情况,1、3唱一样的歌词,2、4唱一样的歌词
+    if ( currentNote.formatLyricsEntries.length == 2 && currentNote.repeatIdx >= 2 && index === (currentNote.repeatIdx - 2) ) {
+      lyric?.classList.add('lyricActive')
     }
     // if ((index === currentNote.repeatIdx && currentNote.repeatIdx + 1 == lyricIndex)) {
     //   lyric?.classList.add('lyricActive')
@@ -1828,9 +1964,7 @@ export const moveSvgDom = (skipNote?: boolean) => {
     // 移动小鸟的位置
     moveSmoothAnimation(0, state.activeNoteIndex, false)
     // 移动谱面当当前音符的位置
-    const noteWidth = state.times[state.activeNoteIndex].bbox?.originWidth || state.times[state.activeNoteIndex].bbox?.width;
-    const firstNoteWidth = state.times[0].bbox?.originWidth || state.times[0].bbox?.width;
-    const distance = state.times[state.activeNoteIndex].bbox?.x - state.times[0].bbox?.x + noteWidth / 2 - firstNoteWidth / 2;
+    const distance = state.times[state.activeNoteIndex].bbox?.x - state.times[0].bbox?.x
     smoothAnimationState.osdmScrollDom!.scrollTo({
       left: distance,
       behavior: "smooth",
@@ -1882,15 +2016,33 @@ watch(
     // const matchMeasureNum = state.activeMeasureIndex - needReduceMultipleRestNum - 1
     // console.log('选中的小节',matchMeasureNum,'需要减去的小节',needReduceMultipleRestNum,'当前的小节',state.activeMeasureIndex)
     state.vfmeasures.forEach((item: any, idx: number) => {
-      const measureNum = item.getAttribute('data-num') ? Number(item.getAttribute('data-num')) : -1;
-      const nextMeasureNum = state.vfmeasures[idx+1]?.getAttribute('data-num') ? Number(state.vfmeasures[idx+1]?.getAttribute('data-num')) : -1;
-      if (measureNum >= 0 && (measureNum === state.activeMeasureIndex || (measureNum < state.activeMeasureIndex && nextMeasureNum > state.activeMeasureIndex)) || (measureNum < state.activeMeasureIndex && nextMeasureNum == -1) ) {
-        item.querySelector('.vf-custom-bg')?.setAttribute("fill", "#132D4C")
-        item.querySelector('.vf-custom-bot')?.setAttribute("fill", "#040D1E")
+      const dataNum = item.getAttribute('data-num')  // 值可能为字符串类型的undefined
+      let measureNum = (dataNum && dataNum !== "undefined") ? Number(dataNum) : -1;
+      let nextDataNum = state.vfmeasures[idx+1]?.getAttribute('data-num')
+      // 当有换行小节,下个小节的nextDataNum是undefined,所以这里需要往后找一个
+      if(!(nextDataNum && nextDataNum !== "undefined")){
+        nextDataNum = state.vfmeasures[idx + 2]?.getAttribute('data-num')
+      }
+      const nextMeasureNum = Number(nextDataNum)
+      // 当measureNum 为undefined 则是下一个小节的换行小节,所以这里等于下一个小节
+      if(measureNum === -1) {
+        measureNum = nextMeasureNum
+      }
+      if (measureNum >= 0 && (measureNum === state.activeMeasureIndex || (measureNum < state.activeMeasureIndex && nextMeasureNum > state.activeMeasureIndex))) {
+        if (state.isCbsView) {
+          item.querySelector('.vf-custom-bg')?.setAttribute("fill", "#FFF6E1")
+        } else {
+          item.querySelector('.vf-custom-bg')?.setAttribute("fill", "#132D4C")
+          item.querySelector('.vf-custom-bot')?.setAttribute("fill", "#040D1E")
+        }
         // 预备小节
         if(state.sectionFirst && measureNum === state.sectionFirst.MeasureNumberXML && state.section.length === 2){
-          item?.querySelector('.vf-custom-bg')?.setAttribute("fill", "#71B8BD")
-          item?.querySelector('.vf-custom-bot')?.setAttribute("fill", "#448F9C")
+          if (state.isCbsView) {
+            item?.querySelector('.vf-custom-bg')?.setAttribute("fill", "#E3F1FF")
+          } else {
+            item?.querySelector('.vf-custom-bg')?.setAttribute("fill", "#71B8BD")
+            item?.querySelector('.vf-custom-bot')?.setAttribute("fill", "#448F9C")
+          }
         }
       } else {
         // 有选段只清除选段处的
@@ -1903,17 +2055,34 @@ watch(
             rightMeasureNumberXML = state.section[0].MeasureNumberXML
           }
           if(measureNum >= leftMeasureNumberXML && measureNum <= rightMeasureNumberXML){
-            item.querySelector('.vf-custom-bg')?.setAttribute("fill", "#609FCF")
-            item.querySelector('.vf-custom-bot')?.setAttribute("fill", "#2B70A5")
+            if (!state.isCbsView) {
+              item.querySelector('.vf-custom-bg')?.setAttribute("fill", "#609FCF")
+              item.querySelector('.vf-custom-bot')?.setAttribute("fill", "#2B70A5")
+            } else {
+              item.querySelector('.vf-custom-bg')?.setAttribute("fill", 'transparent')
+            }
+          }
+          if (measureNum >= leftMeasureNumberXML && measureNum <= rightMeasureNumberXML) {
+            if (state.isCbsView) {
+              item.querySelector('.vf-custom-bg')?.setAttribute("fill", "rgba(255,246,225,0.5)")
+            }
           }
           // 预备小节
           if(state.sectionFirst && measureNum === state.sectionFirst.MeasureNumberXML){
-            item?.querySelector('.vf-custom-bg')?.setAttribute("fill", "#71B8BD")
-            item?.querySelector('.vf-custom-bot')?.setAttribute("fill", "#448F9C")
+            if (state.isCbsView) {
+              item?.querySelector('.vf-custom-bg')?.setAttribute("fill", "#E3F1FF")
+            } else {
+              item?.querySelector('.vf-custom-bg')?.setAttribute("fill", "#71B8BD")
+              item?.querySelector('.vf-custom-bot')?.setAttribute("fill", "#448F9C")
+            }
           }
         } else {
-          item.querySelector('.vf-custom-bg')?.setAttribute("fill", "#609FCF")
-          item.querySelector('.vf-custom-bot')?.setAttribute("fill", "#2B70A5")
+          if (!state.isCbsView) {
+            item.querySelector('.vf-custom-bg')?.setAttribute("fill", "#609FCF")
+            item.querySelector('.vf-custom-bot')?.setAttribute("fill", "#2B70A5")
+          } else {
+            item.querySelector('.vf-custom-bg')?.setAttribute("fill", 'transparent')
+          }
         }
       }
     })
@@ -1934,42 +2103,120 @@ watch(
         rightMeasureNumberXML = state.section[0].MeasureNumberXML
       }
       state.vfmeasures.forEach((item: any, idx: number) => {
-        const measureNum = item.getAttribute('data-num') ? Number(item.getAttribute('data-num')) : 1;
+        const dataNum = item.getAttribute('data-num')  // 值可能为字符串类型的undefined
+        let measureNum = (dataNum && dataNum !== "undefined") ? Number(dataNum) : -1;
+        let nextDataNum = state.vfmeasures[idx+1]?.getAttribute('data-num')
+        // 当有换行小节,下个小节的nextDataNum是undefined,所以这里需要往后找一个
+        if(!(nextDataNum && nextDataNum !== "undefined")){
+          nextDataNum = state.vfmeasures[idx + 2]?.getAttribute('data-num')
+        }
+        const nextMeasureNum = Number(nextDataNum)
+        // 当measureNum 为undefined 则是下一个小节的换行小节,所以这里等于下一个小节
+        if(measureNum === -1) {
+          measureNum = nextMeasureNum
+        }
         // 小于选中置灰
         if (measureNum < leftMeasureNumberXML) {
-          item.querySelector('.vf-custom-bg')?.setAttribute("fill", "rgba(96,159,207,0.5)")
-          item.querySelector('.vf-custom-bot')?.setAttribute("fill", "rgba(43,112,165,0.5)")
+          if (!state.isCbsView) {
+            item.querySelector('.vf-custom-bg')?.setAttribute("fill", "rgba(96,159,207,0.5)")
+            item.querySelector('.vf-custom-bot')?.setAttribute("fill", "rgba(43,112,165,0.5)")
+          } else {
+            item.querySelector('.vf-custom-bg')?.setAttribute("fill", 'transparent')
+          }
         }
         // 大于选中置灰
         if(measureNum > rightMeasureNumberXML){
-          item.querySelector('.vf-custom-bg')?.setAttribute("fill", "rgba(96,159,207,0.5)")
-          item.querySelector('.vf-custom-bot')?.setAttribute("fill", "rgba(43,112,165,0.5)")
+          if (!state.isCbsView) {
+            item.querySelector('.vf-custom-bg')?.setAttribute("fill", "rgba(96,159,207,0.5)")
+            item.querySelector('.vf-custom-bot')?.setAttribute("fill", "rgba(43,112,165,0.5)")
+          } else {
+            item.querySelector('.vf-custom-bg')?.setAttribute("fill", 'transparent')
+          }
+        }
+        if (measureNum >= leftMeasureNumberXML && measureNum <= rightMeasureNumberXML) {
+          if (state.isCbsView) {
+            item.querySelector('.vf-custom-bg')?.setAttribute("fill", "rgba(255,246,225,0.5)")
+          }
         }
         // 预备小节
         if(state.sectionFirst && measureNum === state.sectionFirst.MeasureNumberXML){
-          item?.querySelector('.vf-custom-bg')?.setAttribute("fill", "#71B8BD")
-          item?.querySelector('.vf-custom-bot')?.setAttribute("fill", "#448F9C")
+          if (state.isCbsView) {
+            item?.querySelector('.vf-custom-bg')?.setAttribute("fill", "#E3F1FF")
+          } else {
+            item?.querySelector('.vf-custom-bg')?.setAttribute("fill", "#71B8BD")
+            item?.querySelector('.vf-custom-bot')?.setAttribute("fill", "#448F9C")
+          }
         }        
       })
     }else{
       // 恢复选段前
       state.vfmeasures.forEach((item: any, idx: number) => {
-        const measureNum = item.getAttribute('data-num') ? Number(item.getAttribute('data-num')) : -1;
-        const nextMeasureNum = state.vfmeasures[idx+1]?.getAttribute('data-num') ? Number(state.vfmeasures[idx+1]?.getAttribute('data-num')) : -1;
+        const dataNum = item.getAttribute('data-num')  // 值可能为字符串类型的undefined
+        let measureNum = (dataNum && dataNum !== "undefined") ? Number(dataNum) : -1;
+        let nextDataNum = state.vfmeasures[idx+1]?.getAttribute('data-num')
+        // 当有换行小节,下个小节的nextDataNum是undefined,所以这里需要往后找一个
+        if(!(nextDataNum && nextDataNum !== "undefined")){
+          nextDataNum = state.vfmeasures[idx + 2]?.getAttribute('data-num')
+        }
+        const nextMeasureNum = Number(nextDataNum)
+        // 当measureNum 为undefined 则是下一个小节的换行小节,所以这里等于下一个小节
+        if(measureNum === -1) {
+          measureNum = nextMeasureNum
+        }
         if (measureNum >= 0 && (measureNum === state.activeMeasureIndex || (measureNum < state.activeMeasureIndex && nextMeasureNum > state.activeMeasureIndex)) ) {
-          item.querySelector('.vf-custom-bg')?.setAttribute("fill", "#132D4C")
-          item.querySelector('.vf-custom-bot')?.setAttribute("fill", "#040D1E")
+          if (state.isCbsView) {
+            item.querySelector('.vf-custom-bg')?.setAttribute("fill", "transparent")
+          } else {
+            item.querySelector('.vf-custom-bg')?.setAttribute("fill", "#132D4C")
+            item.querySelector('.vf-custom-bot')?.setAttribute("fill", "#040D1E")
+          }
         } else {
-          item.querySelector('.vf-custom-bg')?.setAttribute("fill", "#609FCF")
-          item.querySelector('.vf-custom-bot')?.setAttribute("fill", "#2B70A5")
+          if (state.isCbsView) {
+            item.querySelector('.vf-custom-bg')?.setAttribute("fill", "transparent")
+          } else {
+            item.querySelector('.vf-custom-bg')?.setAttribute("fill", "#609FCF")
+            item.querySelector('.vf-custom-bot')?.setAttribute("fill", "#2B70A5")
+          }
+
         }
       })
     }
   }
 )
 
+// 后台编辑谱面模式,切换谱面时,如果有操作没有保存,需要给出提示
+export const checkMoveNoSave = async () => {
+  return new Promise((resolve, reject) => {
+    if (query.isMove) {
+      if (moveData.open && undoData.undoList.length) {
+        showConfirmDialog({
+          className: "noSaveModal",
+          title: "温馨提示",
+          message: "您有新的修改还未保存,切换谱面后本次编辑的内容将不会保存",
+        }).then(() => {
+          moveData.open = false
+          resolve(true)
+        }).catch(() => {
+          return;
+        });
+      } else {
+        moveData.open = false
+        undoData.undoList = []
+        resolve(true)
+      }
+    } else {
+      resolve(true)
+    }
+  });
+
+
+}
+
+
 /** 刷新谱面 */
 export const refreshMusicSvg = () => {
+  moveData.noteCoords = []
+  moveData.modelList = []
   clearSelection();
   resetBaseRate();
   state.activeMeasureIndex = -1;

+ 4 - 0
src/style.css

@@ -123,6 +123,10 @@ body {
   transition: all 0.3s;
 }
 
+.noSaveModal {
+  transform: scale(0.8) translateY(-50%);
+}
+
 /* 引导动画 */
 @keyframes guideKeyframes {
   0% {

+ 28 - 1
src/view/abnormal-pop/index.module.less

@@ -9,12 +9,39 @@
     height: 100vh;
     overflow: hidden;
     .closeIcon {
-        position: absolute;
+        position: fixed;
         width: 20px;
         height: 20px;
+        max-width: 20px;
+        max-height: 20px;
         right: 30px;
         top: 30px;
     }
+    .closeDom {
+        position: fixed;
+        width: 20px;
+        height: 20px;
+        right: 30px;
+        top: 30px;
+        &::before,
+        &::after {
+            content: '';
+            position: absolute;
+            top: 50%;
+            left: 50%;
+            width: 100%;
+            height: 2px;
+            background-color: #ffffff;
+            transform-origin: center;
+            border-radius: 5px;
+        }
+        &::before {
+            transform: translate(-50%, -50%) rotate(45deg);
+        }
+        &::after {
+            transform: translate(-50%, -50%) rotate(-45deg);
+        }
+    }
     .bg {
         width: 264px;
     }

+ 2 - 1
src/view/abnormal-pop/index.tsx

@@ -15,7 +15,8 @@ export default defineComponent({
       <>
         {evaluatingData.socketErrorStatus === 0 && (
           <div class={styles.fraction}>
-            {evaluatingData.socketErrorStatus === 0 && <img class={styles.closeIcon} src={popImgs.icon_close} onClick={() => emit("close")} />}
+            {/* {evaluatingData.socketErrorStatus === 0 && <img class={styles.closeIcon} src={popImgs.icon_close} onClick={() => emit("close")} />} */}
+            <span class={styles.closeDom} onClick={() => emit("close")}></span>
             <div class={styles.content}>
               <div class={styles.title}>网络连接失败</div>
               <div class={styles.desc}>请确保网络正常后重新连接</div>

+ 45 - 34
src/view/audio-list/index.tsx

@@ -26,10 +26,10 @@ export const audioData = reactive({
 		banSongEle: null as HTMLAudioElement | null,
 		mingSongEle: null as HTMLAudioElement | null,
 		beatSongEle: null as HTMLAudioElement | null,
-		betaBackgroundEle: null as HTMLAudioElement | null,
-		betaFanSongEle: null as HTMLAudioElement | null,
-		betaBanSongEle: null as HTMLAudioElement | null,
-		betaMingSongEle: null as HTMLAudioElement | null
+		beatBackgroundEle: null as HTMLAudioElement | null,
+		beatFanSongEle: null as HTMLAudioElement | null,
+		beatBanSongEle: null as HTMLAudioElement | null,
+		beatMingSongEle: null as HTMLAudioElement | null
 	},
 	midiRender: false,
 	progress: 0, // midi播放进度(单位:秒)
@@ -91,12 +91,17 @@ export const getAudioCurrentTime = () => {
 		// const c = getMidiCurrentTime();
 		return audioData.progress;
 	}
-	// console.log('返回的时间',state.playSource, audioData.songEle?.currentTime,audioData.progress)
-	if (state.playSource === "music") return audioData.songEle?.currentTime || audioData.progress;
-	if (state.playSource === "background") return audioData.backgroundEle?.currentTime || audioData.progress;
-	if (state.playSource === "mingSong") return audioData.mingSongEle?.currentTime || audioData.progress;
-	
-	return audioData.songEle?.currentTime || audioData.progress;
+	if (state.modeType === 'evaluating') {
+		return audioData.progress;
+	} else {
+		// console.log('返回的时间',state.playSource, audioData.songEle?.currentTime,audioData.progress)
+		if (state.playSource === "music") return audioData.songEle?.currentTime || audioData.progress;
+		if (state.playSource === "background") return audioData.backgroundEle?.currentTime || audioData.progress;
+		if (state.playSource === "mingSong") return audioData.mingSongEle?.currentTime || audioData.progress;
+		
+		return audioData.songEle?.currentTime || audioData.progress;
+	}
+
 };
 /** 获取曲谱的总时间 */
 export const getAudioDuration = () => {
@@ -119,9 +124,11 @@ export const setAudioCurrentTime = (time: number, index = 0) => {
 		setMidiCurrentTime(index);
 		return;
 	}
+	if(state.playSource === "mingSong") {
+		audioData.mingSongEle && (audioData.mingSongEle.currentTime = time);
+	}
 	audioData.songEle && (audioData.songEle.currentTime = time);
 	audioData.backgroundEle && (audioData.backgroundEle.currentTime = time);
-	audioData.mingSongEle && (audioData.mingSongEle.currentTime = time);
 	audioData.progress = time;
 };
 
@@ -143,16 +150,10 @@ export const toggleMutePlayAudio = (source: IPlayState, muted: boolean) => {
 };
 
 /** 切换节拍器音源 */
-export const changeSongSourceByBate = (isDisBate:boolean) => {
-	// isDisBate 为true 切换到不带节拍的,为false 切换到带节拍的
-	let songEleCurrentTime = audioData.songEle?.currentTime || 0
-	let backgroundEleCurrentTime = audioData.backgroundEle?.currentTime || 0
-	let mingSongEleCurrentTime = audioData.mingSongEle?.currentTime || 0
-	// 有一种场景,默认模式没有文件内容的时候,songEle,backgroundEle,mingSongEle都为空,在设置音源之前点击跳转位置,这时候以audioData.progress的时间为准
-	if(!audioData.songEle&&!audioData.backgroundEle&&!audioData.mingSongEle){
-		songEleCurrentTime = backgroundEleCurrentTime = mingSongEleCurrentTime = audioData.progress || 0
-	}
-	if (isDisBate) {
+export const changeSongSourceByBeat = (isDisBeat:boolean) => {
+	const currentTime = getAudioCurrentTime()
+	// isDisBeat 为true 切换到不带节拍的,为false 切换到带节拍的
+	if (isDisBeat) {
 		if(state.playType === "play"){
 			audioData.songEle = audioData.songCollection.songEle
 			audioData.backgroundEle = audioData.songCollection.backgroundEle
@@ -165,16 +166,15 @@ export const changeSongSourceByBate = (isDisBate:boolean) => {
 		// 没有节拍器资源的时候 用 不带节拍器的资源,防止播放不了
 		if(state.playType === "play"){
 			audioData.songEle = audioData.songCollection.beatSongEle || audioData.songCollection.songEle
-			audioData.backgroundEle = audioData.songCollection.betaBackgroundEle || audioData.songCollection.backgroundEle
+			audioData.backgroundEle = audioData.songCollection.beatBackgroundEle || audioData.songCollection.backgroundEle
 		} else {
-			audioData.songEle = audioData.songCollection.betaFanSongEle || audioData.songCollection.fanSongEle
-			audioData.backgroundEle = audioData.songCollection.betaBanSongEle || audioData.songCollection.banSongEle
-			audioData.mingSongEle = audioData.songCollection.betaMingSongEle || audioData.songCollection.mingSongEle
+			audioData.songEle = audioData.songCollection.beatFanSongEle || audioData.songCollection.fanSongEle
+			audioData.backgroundEle = audioData.songCollection.beatBanSongEle || audioData.songCollection.banSongEle
+			audioData.mingSongEle = audioData.songCollection.beatMingSongEle || audioData.songCollection.mingSongEle
 		}
 	}
-	audioData.songEle && (audioData.songEle.currentTime = songEleCurrentTime)
-	audioData.backgroundEle && (audioData.backgroundEle.currentTime = backgroundEleCurrentTime)
-	audioData.mingSongEle && (audioData.mingSongEle.currentTime = mingSongEleCurrentTime)
+	// 设置进度
+	setAudioCurrentTime(currentTime)
 	// 设置静音与取消静音
 	if (state.playSource === "music") {
 		audioData.songEle && (audioData.songEle.muted = false);
@@ -190,14 +190,14 @@ export const changeSongSourceByBate = (isDisBate:boolean) => {
 		audioData.mingSongEle && (audioData.mingSongEle.muted = false);
 	}
 }
-/** 切换男生女生唱名  */
+/** 切换男声女声唱名  */
 export const changeMingSongType = () =>{
 	// 当有男声女声都有值时候才能切换 
 	const { mingSongEle, mingSongGirlEle, beatMingSongEle, beatMingSongGirlEle } = audioData.mingSongTypeCollection
 	if(mingSongEle&&mingSongGirlEle){
 		const mingSongType = audioData.mingSongType
 		audioData.songCollection.mingSongEle = mingSongType === 1 ? mingSongEle : mingSongGirlEle
-		audioData.songCollection.betaMingSongEle = mingSongType === 1 ? beatMingSongEle : beatMingSongGirlEle
+		audioData.songCollection.beatMingSongEle = mingSongType === 1 ? beatMingSongEle : beatMingSongGirlEle
 	}
 }
 export default defineComponent({
@@ -240,13 +240,24 @@ export default defineComponent({
 			}
 			return new Promise((resolve) => {
 				const a = new Audio(src + '?v=' + Date.now());
-				a.load();
 				a.onloadedmetadata = () => {
 					resolve(a);
 				};
 				a.onerror = () => {
 					resolve(null);
 				};
+				// 当未加载 资源之前 切换到其他浏览器标签,浏览器可能会禁止资源加载所以无法触发onloadedmetadata事件,导致一直在加载中,这里做个兼容
+				if (document.visibilityState === 'visible') {
+					a.load();
+				} else {
+					const onVisibilityChange = () => {
+						if (document.visibilityState === 'visible') {
+							document.removeEventListener('visibilitychange', onVisibilityChange);
+							a.load();
+						}
+					};
+					document.addEventListener('visibilitychange', onVisibilityChange);
+				}
 			});
 		};
 
@@ -376,9 +387,9 @@ export default defineComponent({
 				const [beatMusic, beatAccompany, beatFanSong, beatBanSong, beatMingSong, beatMingSongGirl] = await loadBeatAudio()
 				Object.assign(audioData.songCollection, {
 					beatSongEle:beatMusic,
-					betaBackgroundEle:beatAccompany,
-					betaFanSongEle:beatFanSong,
-					betaBanSongEle:beatBanSong,
+					beatBackgroundEle:beatAccompany,
+					beatFanSongEle:beatFanSong,
+					beatBanSongEle:beatBanSong,
 					beatMingSongEle:beatMingSong
 				})
 				Object.assign(audioData.mingSongTypeCollection, {

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

@@ -35,7 +35,7 @@ import {
   api_startDelayCheck,
   api_closeDelayCheck,
 } from "/src/helpers/communication";
-import state, { IPlayState, clearSelection, handleStopPlay, onPlay, resetPlaybackToStart, togglePlay, initSetPlayRate, resetBaseRate } from "/src/state";
+import state, { IPlayState, clearSelection, handleStopPlay, onPlay, resetPlaybackToStart, togglePlay, initSetPlayRate, resetBaseRate, scrollViewNote } from "/src/state";
 import { IPostMessage } from "/src/utils/native-message";
 import { usePageVisibility } from "@vant/use";
 import { browser } from "/src/utils";
@@ -358,6 +358,8 @@ const handleScoreResult = (res?: IPostMessage) => {
 
 /** 开始评测 */
 export const handleStartBegin = async (preTimes?: number) => {
+  // 滚动到当前小节所在区域
+  scrollViewNote(true);
   evaluatingData.needPlayTick = false;
 	if (state.isAppPlay) {
 		await api_cloudSetCurrentTime({
@@ -632,7 +634,6 @@ export const handleViewReport = (key: "recordId" | "recordIdStr", type: "gym" |
     statusBarTextColor: false,
     isOpenLight: true,
     c_orientation: 0,
-    showLoadingAnim: true
   });
 };
 

+ 8 - 1
src/view/fingering/fingering-config.ts

@@ -122,7 +122,7 @@ export const mappingVoicePart = (id: number | string, soruce: "GYM" | "COLEXIU"
       tenorsaxophone: 6,
       saxophone: 6,
       upbasshorn: 15,
-      melodica: 137,
+      // melodica: 137,
       hulusiFlute: 136,
       panflute: 135,
       recorder: 120,
@@ -144,11 +144,15 @@ export const mappingVoicePart = (id: number | string, soruce: "GYM" | "COLEXIU"
       29: 15,
       30: 17,
       tenorrecorder: "piccolo",
+      germanrecorder: "piccolo",
       woodwind: "hulusi-flute",
+      hulusi: "hulusi-flute",
       panpipes: "pan-flute",
       ocarina: "ocarina", // 陶笛
+      altoocarina: "ocarina", // 陶笛
       whistling: "whistling", // 高音陶笛
       nai: "melodica",
+      melodica: "melodica",  // 口风琴
       15: "baroque-recorder",
       16: "baroque-recorder",
     };
@@ -169,6 +173,7 @@ export const mappingVoicePart = (id: number | string, soruce: "GYM" | "COLEXIU"
       "Alto Saxophone": 5,
       "Tenor Saxophone": 5,
       "Baritone Saxophone": 5,
+      "Baritone": 15,
       "Trumpet in Bb 1": 12,
       "Trumpet in Bb 2": 12,
       "Horn in F": 13,
@@ -276,6 +281,7 @@ export const matchVoicePart = (id: number | string, type: "SINGLE" | "CONCERT"):
       "Alto Saxophone2": 5,
       "Tenor Saxophone": 5,
       "Baritone Saxophone": 5,
+      "Baritone": 15,
       "Trumpet in Bb 1": 12,
       "Trumpet in Bb 2": 12,
       "Horn in F": 13,
@@ -321,6 +327,7 @@ export const matchVoicePart = (id: number | string, type: "SINGLE" | "CONCERT"):
       5: "melodica",
       26: 12,
       tenorrecorder: "piccolo",
+      germanrecorder: "piccolo",
       woodwind: "hulusi-flute",
       panpipes: "pan-flute",
       ocarina: "ocarina",

+ 1 - 1
src/view/fingering/index.tsx

@@ -37,7 +37,7 @@ export default defineComponent({
 
     const doubeClick = () => {
       // 如果在评测和跟练中,双击指法不跳转
-      if ((state.modeType === 'evaluating' && evaluatingData.startBegin) || (state.modeType === 'follow' && followData.start)) {
+      if ((state.modeType === 'evaluating' && evaluatingData.startBegin) || (state.modeType === 'follow' && followData.start) || state.playState === "play") {
         return;
       }
       const nowTime = Date.now();

+ 20 - 10
src/view/follow-practice/index.tsx

@@ -23,6 +23,7 @@ export const followData = reactive({
 	earphone: false,
 	isBeginMask: false, // 倒计时和系统节拍器时候的遮罩,防止用户点击
 	dontAccredit: true, // 没有开启麦克风权限,不需要调用结束收音的api
+	practiceStart: false,
 });
 
 // 记录跟练时长
@@ -50,6 +51,7 @@ export const toggleFollow = (notCancel = true) => {
 	// 取消跟练
 	if (!notCancel) {
 		followData.start = false;
+		followData.practiceStart = false;
 		// 开启了麦克风授权,才需要调用结束收音
 		if (storeData.isApp && !followData.dontAccredit) {
 			openToggleRecord(false);
@@ -80,6 +82,7 @@ const openToggleRecord = async (open: boolean = true) => {
 		if (!openState && followData.start) {
 			followData.earphone = true;
 			followData.start = false;
+			followData.practiceStart = false;
 		}
 	}
 };
@@ -109,24 +112,30 @@ export const handleFollowStart = async () => {
 	if (res?.content?.reson) {
 		followData.isBeginMask = false
 		followData.start = false;
+		followData.practiceStart = false;
 	} else {
 		followData.dontAccredit = false;
-		// 跟练模式开始前,增加播放系统节拍器
-		const tickend = await handleStartTick();
-		// console.log("🚀 ~ tickend:", tickend)
-		// 节拍器返回false, 取消播放
-		if (!tickend) {
-			followData.isBeginMask = false
-			followData.start = false;
-			return false;
+		// 从头开始跟练,跟练模式开始前,增加播放系统节拍器
+		if (state.activeNoteIndex === 0) {
+			const tickend = await handleStartTick();
+			// console.log("🚀 ~ tickend:", tickend)
+			// 节拍器返回false, 取消播放
+			if (!tickend) {
+				followData.isBeginMask = false
+				followData.start = false;
+				followData.practiceStart = false;
+				return false;
+			}
 		}
 		onClear();
 		followData.isBeginMask = false
 		followData.start = true;
-		followData.index = 0;
+		followData.practiceStart = true;
+		// followData.index = 0;
+		followData.index = state.activeNoteIndex;
 		followData.list = [];
 		initSetPlayRate();
-		resetPlaybackToStart();
+		// resetPlaybackToStart();
 		openToggleRecord(true);
 		getNoteIndex();
 		const duration: any = getDuration(state.osmd as unknown as OpenSheetMusicDisplay);
@@ -140,6 +149,7 @@ export const handleFollowStart = async () => {
 export const handleFollowEnd = () => {
 	onClear();
 	followData.start = false;
+	followData.practiceStart = false;
 	openToggleRecord(false);
 	followData.index = 0;
 	console.log("结束");

+ 71 - 0
src/view/music-score/HorizontalDragScroll.ts

@@ -0,0 +1,71 @@
+import state from "/src/state"
+interface HorizontalDragScrollOptions {
+   speed?: number // 滚动速度
+   cursorGrab?: string // 抓取前的鼠标样式
+}
+
+class HorizontalDragScroll {
+    private container: HTMLElement
+    private options: HorizontalDragScrollOptions
+    private isDown: boolean
+    private startX: number
+    private scrollLeft: number
+
+    constructor(container: HTMLElement, options: HorizontalDragScrollOptions = {}) {
+        this.container = container
+        this.options = {
+            speed: options.speed || 2,
+            cursorGrab: options.cursorGrab || "grab",
+        }
+
+        this.isDown = false
+        this.startX = 0
+        this.scrollLeft = 0
+
+        this.init()
+    }
+
+    private init() {
+        this.container.style.cursor = this.options.cursorGrab || "grab"
+
+        this.container.addEventListener("mousedown", this.onMouseDown.bind(this))
+        this.container.addEventListener("mouseleave", this.onMouseLeave.bind(this))
+        this.container.addEventListener("mouseup", this.onMouseUp.bind(this))
+        this.container.addEventListener("mousemove", this.onMouseMove.bind(this))
+    }
+
+    private onMouseDown(e: MouseEvent) {
+        // 当播放中或者不是一行谱模式  不起作用
+        if(state.playState === "play" || !state.isSingleLine) {
+            return
+        }
+        this.isDown = true
+        this.startX = e.pageX - this.container.offsetLeft
+        this.scrollLeft = this.container.scrollLeft
+    }
+
+    private onMouseLeave() {
+        this.isDown = false
+    }
+
+    private onMouseUp() {
+        this.isDown = false
+    }
+
+    private onMouseMove(e: MouseEvent) {
+        if (!this.isDown) return
+        e.preventDefault()
+        const x = e.pageX - this.container.offsetLeft
+        const walk = (x - this.startX) * (this.options.speed || 2)
+        this.container.scrollLeft = this.scrollLeft - walk
+    }
+
+    public destroy() {
+        this.container.removeEventListener("mousedown", this.onMouseDown.bind(this))
+        this.container.removeEventListener("mouseleave", this.onMouseLeave.bind(this))
+        this.container.removeEventListener("mouseup", this.onMouseUp.bind(this))
+        this.container.removeEventListener("mousemove", this.onMouseMove.bind(this))
+    }
+    }
+
+export default HorizontalDragScroll

+ 19 - 0
src/view/music-score/index.module.less

@@ -46,6 +46,22 @@
             stroke: #FFC121;
         }
     }
+    .voiceActive {
+        rect {
+            fill: #FFC121;
+            stroke: #FFC121;
+        }
+    }
+    .rectActive {
+        fill: #FFC121;
+        stroke: #FFC121;
+    }
+    .vf-numbered_note_lines {
+        rect {
+            fill: #FFFFFF;
+            stroke: #FFFFFF;
+        }
+    }
     .blueMusicXml {
         .vf-stave {
             >path {
@@ -83,6 +99,9 @@
 .notTouch{
     touch-action: none;
 }
+.pcCursorGrab{
+    cursor: initial !important;
+}
 .inGradualRange{
    :global{
         #cursorImg-0{

+ 46 - 28
src/view/music-score/index.tsx

@@ -1,4 +1,4 @@
-import { computed, defineComponent, onMounted, reactive, ref } from "vue";
+import { computed, defineComponent, onMounted, reactive, ref, onUnmounted } from "vue";
 import { formatXML, onlyVisible } from "../../helpers/formateMusic";
 // // @ts-ignore
 import { OpenSheetMusicDisplay } from "/osmd-extended/src";
@@ -9,8 +9,10 @@ import queryString from "query-string";
 import { getGradualLengthByXml } from "/src/helpers/calcSpeed";
 import { resetFormate, resetGivenFormate, setGlobalMusicSheet, limitSingleSvgPageHeight } from "/src/helpers/customMusicScore"
 import { setGlobalData } from "/src/utils";
-import Loading from "/src/view/audio-list/loading"
 import { storeData } from "/src/store";
+import { isLoadingCss } from "/src/page-instrument/view-detail/loadingCss"
+import HorizontalDragScroll from './HorizontalDragScroll';
+import { getQuery } from "/src/utils/queryString";
 
 export const musicRenderTypeKey = "musicRenderType";
 let osmd: any = null;
@@ -61,6 +63,7 @@ export default defineComponent({
 		},
 	},
 	setup(props, { emit, slots, expose }) {
+		const query: any = getQuery();
 		/** 设置 曲谱模式,五线谱还是简谱 */
 		const setRenderType = () => {
 			const musicRenderType: any = sessionStorage.getItem(props.renderTypeKey || musicRenderTypeKey);
@@ -70,8 +73,10 @@ export default defineComponent({
 		};
 		const getXML = async () => {
 			// 当有下载的xml的时候直接使用,否则需要下载
-			const xmlStr = downloadXmlStr.value || await fetch(state.xmlUrl).then((response) => response.text());
-			downloadXmlStr.value = "" // 清空内存
+			if(!downloadXmlStr.value){
+				downloadXmlStr.value = await fetch(state.xmlUrl).then((response) => response.text())
+			}
+			const xmlStr = downloadXmlStr.value;
 			const xml = formatXML(xmlStr);
 			musicData.score = state.isCombineRender ? xml : onlyVisible(xml, state.partIndex);
 			if (state.gradualTimes) {
@@ -80,6 +85,7 @@ export default defineComponent({
 		};
 
 		const init = async () => {
+			console.time("渲染加载耗时");
 			const container = document.getElementById("musicAndSelection");
 			if (!container || !musicData.score) return;
 			setGlobalMusicSheet();
@@ -97,7 +103,7 @@ export default defineComponent({
 				renderSingleHorizontalStaffline: state.isSingleLine ? true : false,
 				// autoGenerateMultipleRestMeasuresFromRestMeasures: state.isSingleLine ? false : true, // 连续休止小节是否合并显示
 				autoGenerateMultipleRestMeasuresFromRestMeasures: true,
-				drawLyrics: ( ((!state.accompany && !state.music ) || state.playType === 'sing') && !state.isSimplePage) ? true : false, // 演唱模式才渲染歌词,simple页面不显示歌词
+				drawLyrics: ( ((!state.accompany && !state.music ) || state.playType === 'sing' || !state.isEvxml) && !state.isSimplePage) ? true : false, // 演唱模式才渲染歌词,simple页面不显示歌词
 				// darkMode: true, // 暗黑模式
 				// pageFormat: 'A4_P',
 				// autoBeam: true,
@@ -125,7 +131,8 @@ export default defineComponent({
 				osmd.EngravingRules.PageBottomMargin = 0;
 			} else {
 				// osmd.EngravingRules.PageTopMargin = state.isEvaluatReport ? 7 : 3; // 顶部间距
-				osmd.EngravingRules.PageTopMargin = state.isPreView ? 1 : 3;
+				// osmd.EngravingRules.PageTopMargin = state.isPreView ? 1 : 3;
+				osmd.EngravingRules.PageTopMargin = (state.isPreView && state.musicRenderType === EnumMusicRenderType.staff) ? 1 : state.isPreView ? 2 : 3;
 				osmd.EngravingRules.PageTopMarginNarrow = 3;
 				osmd.EngravingRules.PageLeftMargin = 3.6;
 				osmd.EngravingRules.PageRightMargin = 3;
@@ -146,6 +153,7 @@ export default defineComponent({
 			}
 			osmd.EngravingRules.DYMusicScoreId = state.examSongId || ''
 			osmd.EngravingRules.DYCustomRepeatCount = state.maxLyricNum || 0;
+			osmd.EngravingRules.DYIsSingleLine = state.isSingleLine;
 			await osmd.load(musicData.score);
 			// 对外暴露 一行谱时候 缩小谱面
 			if(state.isSimplePage){
@@ -154,7 +162,7 @@ export default defineComponent({
 			// 需要渲染总谱的云教练页面
 			if (!state.isSimplePage && state.isCombineRender) {
 				for (let i = 0; i < osmd.Sheet.Instruments.length; i++) {
-					const trackName = osmd.Sheet.Instruments[i].Name || '';
+					const trackName = state.isEvxml && state.evxmlAddPartName ? osmd.Sheet.Instruments[i].idString || '' : osmd.Sheet.Instruments[i].Name || '';
 					osmd.Sheet.Instruments[i].Visible = state.canSelectTracks.includes(trackName)
 				  }
 			}
@@ -171,14 +179,22 @@ export default defineComponent({
 			musicData.containerWidth = document.getElementById("musicAndSelection")?.offsetWidth || 625;
 			// console.log(musicData.containerWidth)
 		};
+		let horizontalDragScroll:HorizontalDragScroll | null
 		onMounted(async () => {
 			getContainerWidth();
 			//setRenderType();
 			await getXML();
 			await init();
 			musicData.isRenderLoading = false;
+			// pc 端支持 拖动滚动
+			if(state.platform === "PC" || query.isCbs){
+				const container = document.querySelector('#musicAndSelection') as HTMLElement;
+				horizontalDragScroll = new HorizontalDragScroll(container);
+			}
 		});
-
+		onUnmounted(() => {
+			horizontalDragScroll?.destroy()
+		})
 		const isInTheGradualRange = computed(() => {
 			let result: boolean = false;
 			const activeMeasureIndex = state.times[state.activeNoteIndex]?.measureListIndex || -1;
@@ -195,31 +211,31 @@ export default defineComponent({
 		});
 
 		/** 刷新曲谱 */
-		const refreshMusicScore = async () => {
-			console.log('刷新谱面123')
-			const container = document.getElementById('musicAndSelection'), svgDom = document.getElementById('osmdCanvasPage1'), selectionBox = document.getElementById('selectionBox');
-			if (container && svgDom) {
-				container?.removeChild(svgDom)
-				container?.removeChild(selectionBox)
+		const refreshMusicScore = () => {
+			const container = document.getElementById('musicAndSelection'), svgDom = document.getElementById('osmdCanvasPage1'), selectionBox = document.getElementById('selectionBox'), selectionBgBox = document.getElementById('selectionBgBox');
+			if (container) {
+				svgDom && container?.removeChild(svgDom)
+				selectionBox && container?.removeChild(selectionBox)
+				selectionBgBox && container?.removeChild(selectionBgBox)
 			}
-			state.vfmeasures = [];
-			musicData.showSelection = false;
-			state.osmd.clear();
-			musicData.isRenderLoading = true;
-			musicData.isRefreshLoading = true;
-			state.loadingText = '正在加载中,请稍等…'
-			state.isLoading = true;
-			// 在下一帧再执行,确保出现loading
-			requestAnimationFrame(async ()=>{
+			// 有可能会有 其他地方的js执行 阻塞 这里确保加载条出来
+			isLoadingCss.value = true
+			setTimeout(async () => {
+				state.evXmlBeginArr = [];
+				state.vfmeasures = [];
+				musicData.showSelection = false;
+				state.osmd.clear();
+				musicData.isRenderLoading = true;
+				musicData.isRefreshLoading = true;
 				getContainerWidth();
 				//setRenderType();
 				await getXML();
 				await init();
 				musicData.isRenderLoading = false;
 				musicData.isRefreshLoading = false;
-				state.isLoading = false;
 				musicData.showSelection = true;
-			})
+				isLoadingCss.value = false
+			}, 120);
 		}
 		expose({
 			refreshMusicScore,
@@ -233,12 +249,14 @@ export default defineComponent({
 					isInTheGradualRange.value && styles.inGradualRange,
 					state.musicRenderType == EnumMusicRenderType.staff ? "staff" : "jianpuTone",
 					state.isSingleLine && "singleLineMusicBox",
-					(!state.isCreateImg && !state.isPreView && state.musicRenderType === EnumMusicRenderType.staff) ? "blueMusicXml" : "",
-					state.isSingleLine && state.playState ==="play" && styles.notTouch
+					(!state.isCreateImg && !state.isPreView && !state.isCbsView && state.musicRenderType === EnumMusicRenderType.staff) ? "blueMusicXml" : "",
+					state.isSingleLine && state.playState ==="play" && styles.notTouch,
+					!state.isSingleLine && (state.platform === "PC" || query.isCbs) &&  styles.pcCursorGrab
+
 				]}
 			>
 				{slots.default?.()}
-				{props.showSelection && musicData.showSelection && !state.isPreView && !state.isEvaluatReport &&!state.isSimplePage && <Selection />}
+				{props.showSelection && musicData.showSelection && !state.isEvaluatReport &&!state.isSimplePage && state.musicRendered && <Selection />}
 			</div>
 		);
 	},

BIN
src/view/plugins/move-music-score/image/edit.png


BIN
src/view/plugins/move-music-score/image/edit_add.png


BIN
src/view/plugins/move-music-score/image/edit_close.png


BIN
src/view/plugins/move-music-score/image/edit_delete.png


BIN
src/view/plugins/move-music-score/image/edit_next.png


BIN
src/view/plugins/move-music-score/image/edit_pre.png


BIN
src/view/plugins/move-music-score/image/edit_reduce.png


BIN
src/view/plugins/move-music-score/image/edit_reset.png


BIN
src/view/plugins/move-music-score/image/edit_save.png


+ 85 - 0
src/view/plugins/move-music-score/index.module.less

@@ -54,4 +54,89 @@
     cursor: pointer;
     transition: all 0.5s;
     transform: rotate(180deg);
+}
+
+.editToolBox {
+    position: fixed;
+    left: 0;
+    top: 0;
+    width: 100%;
+    background: rgba(0, 0, 0, 0.5);
+    z-index: 999999;
+    display: flex;
+    align-items: center;
+    justify-content: flex-end;
+    padding: 14PX 30PX;
+    pointer-events: none;
+    min-height: 58PX;
+    .editItem {
+        display: flex;
+        align-items: center;
+        padding: 5PX 12PX;
+        background: rgba(255,255,255,0.2);
+        border-radius: 20PX;
+        margin-left: 18PX;
+        cursor: pointer;
+        pointer-events: all;
+        &:active {
+            opacity: .5;
+        }
+        img {
+            width: 18PX;
+            height: 18PX;
+            margin-right: 6PX;
+        }
+        span {
+            font-size: 14PX;
+            color: #fff;
+        }
+    }
+    .extraItem {
+        margin-left: 18PX;
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        padding: 5PX 12PX;
+        background: rgba(255,255,255,0.2);
+        border-radius: 20PX;
+        position: relative;
+        width: 76PX;
+        box-sizing: border-box;
+        cursor: pointer;
+        pointer-events: all;
+        img {
+            width: 18PX;
+            height: 18PX;
+            cursor: pointer;
+            &:active {
+                opacity: .5;
+            }
+        }
+        &::before {
+            content: "";
+            position: absolute;
+            left: 50%;
+            top: 50%;
+            transform: translate(-50%,-50%);
+            width: 1PX;
+            height: 20PX;
+            background: rgba(255,255,255,0.3);
+            z-index: 1;
+        }
+    }
+    .disabled {
+        opacity: .5;
+        pointer-events: none;
+    }
+}
+
+.itemDisabled {
+    .editItem {
+        opacity: .5;
+        pointer-events: none;
+    }
+    .canEdit {
+        opacity: 1;
+        pointer-events: visible;
+    }
 }

+ 115 - 29
src/view/plugins/move-music-score/index.tsx

@@ -1,6 +1,6 @@
-import { Row, showToast } from "vant";
-import { defineComponent, onMounted, reactive, nextTick, ref } from "vue";
-import state from "/src/state";
+import { Row, showToast, showConfirmDialog } from "vant";
+import { defineComponent, onMounted, onUnmounted, reactive, nextTick, ref } from "vue";
+import state, { IPlatform } from "/src/state";
 import request from "/src/utils/request";
 import { getQuery } from "/src/utils/queryString";
 import styles from "./index.module.less";
@@ -10,6 +10,14 @@ import "@varlet/ui/es/button-group/style";
 import "@varlet/ui/es/switch/style";
 import { storeData } from "/src/store";
 import rightHideIcon from './image/right_hide_icon.png';
+import editIcon from './image/edit.png';
+import editCloseIcon from './image/edit_close.png';
+import editSaveIcon from './image/edit_save.png';
+import editPreIcon from './image/edit_pre.png';
+import editDeleteIcon from './image/edit_delete.png';
+import editResetIcon from './image/edit_reset.png';
+import editReduceIcon from './image/edit_reduce.png';
+import editAddIcon from './image/edit_add.png';
 
 let extStyleConfigJson: any = {};
 const clientWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
@@ -70,9 +78,10 @@ function initSvgId() {
 	if (!svg) return;
 	const vfstavetempo: HTMLElement[] = Array.from(svg.querySelectorAll(".vf-stavetempo"));
 	const vftext: HTMLElement[] = Array.from(svg.querySelectorAll(".vf-text"));
-	const vfstaveSection: HTMLElement[] = []; //Array.from(svg.querySelectorAll(".vf-StaveSection"));
+	const vfstaveSection: HTMLElement[] = Array.from(svg.querySelectorAll(".vf-StaveSection"));
 	const vfRepetition: HTMLElement[] = Array.from(svg.querySelectorAll(".vf-Repetition"));
 	const vflineGroup: HTMLElement[] = Array.from(svg.querySelectorAll(".vf-lineGroup"));
+	// console.log('速度标记',vfstavetempo)
 	let tempIndex = 1;
 	[...vfstavetempo].forEach((ele) => {
 		setEleId(ele, "temp" + tempIndex);
@@ -80,6 +89,7 @@ function initSvgId() {
 	});
 	let textIndex = 1;
 	[...vftext].forEach((ele) => {
+		// console.log(ele.textContent,textIndex)
 		setEleId(ele, "text" + textIndex);
 		textIndex++;
 	});
@@ -112,10 +122,10 @@ function setEleId(ele: HTMLElement, eleId: string) {
 	if (!id) {
 		ele.setAttribute("id", eleId);
 	}
-	createModelBox(ele as any);
+	createModelBox(ele as any, eleId as any);
 }
 
-function createModelBox(ele: SVGAElement) {
+function createModelBox(ele: SVGAElement, eleId?: any) {
 	const musicContainer = document.getElementById("musicAndSelection")?.getBoundingClientRect() || { x: 0, y: 0 };
 	const parentLeft = musicContainer.x || 0;
 	const parentTop = musicContainer.y || 0;
@@ -128,7 +138,7 @@ function createModelBox(ele: SVGAElement) {
 	};
 	const type = ele.getAttribute("class");
 	moveData.modelList.push({
-		id: ele.getAttribute("id"),
+		id: eleId || ele.getAttribute("id"),
 		bbox,
 		type,
 		isMove: false,
@@ -159,6 +169,22 @@ function getBox(ele: SVGAElement) {
 	};
 }
 
+// 切换开关
+const switchMoveState = () => {
+	// 如果编辑过,没有保存,点击取消状态,需要提醒用户是否取消
+	if (moveData.open && undoData.undoList.length) {
+		showConfirmDialog({
+			className: "noSaveModal",
+			title: "温馨提示",
+			message: "您有新的修改还未保存,取消后本次编辑的内容将不会保存",
+		}).then(() => {
+			moveData.open = false
+		});
+	} else {
+		moveData.open = !moveData.open
+	}
+}
+
 // 过滤数据
 export const filterMoveData = async () => {
 	const examSongId = state.examSongId;
@@ -207,15 +233,27 @@ export const filterMoveData = async () => {
 		// }
 		extStyleConfigJson[moveData.partIndex] = list;
 		console.log("🚀 ~ extStyleConfigJson", extStyleConfigJson)
+		const dataParams = state.musicRenderType === 'staff' ? {
+			id: examSongId,
+			extStyleConfigJson: JSON.stringify(extStyleConfigJson),
+		} : {
+			id: examSongId,
+			extJianStyleConfigJson: JSON.stringify(extStyleConfigJson),
+		}
 		const res = await request.post("/musicSheet/img", {
 			requestType: "json",
-			data: {
-				id: examSongId,
-				extStyleConfigJson: JSON.stringify(extStyleConfigJson),
-			}
+			data: dataParams
 		});
 		if (res && res.code == 200) {
 			showToast("保存成功");
+			undoData.undoList = [];
+			undoData.activeItem = null;
+			if (state.musicRenderType === 'staff') {
+				state.extStyleConfigJson = JSON.stringify(extStyleConfigJson)
+			} else {
+				state.extJianStyleConfigJson = JSON.stringify(extStyleConfigJson)
+			}
+			
 		}
 		clearActiveModel();
 	}
@@ -303,10 +341,15 @@ const renderSvgItem = (item: any) => {
 /** 设置元素位置 */
 async function setModelPostion(item: any, x: number, y: number, repeatEdit?: boolean) {
 	// console.log(item)
-	//console.log('位置',x,y)
+	// console.log('位置',x,y)
 	if (item) {
 		const g = document.querySelector("#" + item.id)!; // svg元素
 		const el: HTMLElement = document.querySelector(`[data-id=${item.id}]`)!; // svg元素的背景div
+		let scaleZoom: number = item.zoom ? item.zoom : moveData.zoom;
+		// 预览页时0.65倍的谱面,需要特殊处理下
+		if (state.isPreView && state.zoom == 0.65) {
+			scaleZoom = 0.65
+		}
 		if (x === 0 && y === 0) {
 			g && g.removeAttribute("transform");
 			el && (el.style.transform = "");
@@ -321,15 +364,19 @@ async function setModelPostion(item: any, x: number, y: number, repeatEdit?: boo
 				if (!moveData.noteCoords.length) {
 					await initNoteCoord()
 				}
-				const targetX = moveData.noteCoords[item.noteIdx].x + item.noteRelativeX, targetY = moveData.noteCoords[item.noteIdx].y + item.noteRelativeY;
+				const targetX = moveData.noteCoords[item.noteIdx].x + item.noteRelativeX*(state.zoom/0.8), targetY = moveData.noteCoords[item.noteIdx].y + item.noteRelativeY*(state.zoom/0.8);
 				const original = document.getElementById(item.id)?.getBoundingClientRect() || { x: 0, y: 0 };
 				tsX = targetX - original.x;
 				tsY = targetY - original.y;
-				// console.log('距离',tsX,tsY,x,y)
-				g && g.setAttribute("transform", `translate(${tsX / moveData.zoom}, ${tsY / moveData.zoom})`);
+				// console.log('距离',tsX,tsY,x,y,moveData.zoom)
+				if (state.platform === IPlatform.PC) {
+					// tsX = tsX / 1.5
+					// tsY = tsY / 1.8
+				}
+				g && g.setAttribute("transform", `translate(${tsX / scaleZoom}, ${tsY / scaleZoom})`);
 				el && (el.style.transform = `translate(${tsX}px, ${tsY}px)`);
 			} else {
-				g && g.setAttribute("transform", `translate(${tsX / moveData.zoom}, ${tsY / moveData.zoom})`);
+				g && g.setAttribute("transform", `translate(${tsX / scaleZoom}, ${tsY / scaleZoom})`);
 				el && (el.style.transform = `translate(${tsX}px, ${tsY}px)`);
 			}
 		}
@@ -462,9 +509,11 @@ const handleUndo = () => {
 
 /** 根据移动数据渲染 */
 export const renderForMoveData = () => {
-	if (state.extStyleConfigJson) {
+	// 一行谱模式暂时不支持谱面编辑和回显
+	if (state.isSingleLine) return; 
+	if (state.extStyleConfigJson || state.extJianStyleConfigJson) {
 		try {
-			extStyleConfigJson = JSON.parse(state.extStyleConfigJson);
+			extStyleConfigJson = state.musicRenderType === 'staff' ? JSON.parse(state.extStyleConfigJson) : JSON.parse(state.extJianStyleConfigJson);
 		} catch (error) {
 			extStyleConfigJson = {};
 		}
@@ -511,10 +560,8 @@ export const renderForMoveData = () => {
 							}
 						}
 					}
-					// index = targetIndex + 1
-					// item.id = `text${index}`
 					index = targetIndex
-					item.id = `text${targetIndex+1}`
+					item.id = moveData.modelList[targetIndex]?.id
 				}
 				// console.log(66666666,index)
 				if (index > -1) {
@@ -544,14 +591,25 @@ export default defineComponent({
 			// 	initSvgId();
 			// }
 			// renderForMoveData();
+			moveData.modelList = []
 			nextTick(() => initNoteCoord())
+			// const hasToolDom = Array.from(document.body.children)?.some((item: any) => item?.id === 'toolBox')
+			// if (!hasToolDom) {
+			// 	const toolBox = document.getElementById("toolBox");
+			// 	toolBox && document.body.appendChild(toolBox);
+			// }
 			const toolBox = document.getElementById("toolBox");
 			toolBox && document.body.appendChild(toolBox);
 		});
+		onUnmounted(() => {
+			moveData.modelList = []
+			const toolBox = document.getElementById("toolBox");
+			toolBox && document.body.removeChild(toolBox);
+		})
 		return () => (
 			<div class={[moveData.open ? "" : styles.moveDisabled]}>
 				<div id="toolBox">
-					<div class={[styles.toolBox, !showToolBox.value && styles.hideTool]} >
+					{/* <div class={[styles.toolBox, !showToolBox.value && styles.hideTool]} >
 						<Switch v-model={moveData.open} />
 						{moveData.open && (
 							<>
@@ -561,12 +619,6 @@ export default defineComponent({
 										<Button onClick={() => handleAddAndSub('sub')}>减</Button>
 									</ButtonGroup>
 								)}
-								{/* <ButtonGroup size="small">
-									
-									<Button>
-										<Icon name="arrow-down" style={{ transform: "rotate(-90deg)" }} />
-									</Button>
-								</ButtonGroup> */}
 								<Button size="small" onClick={handleUndo} disabled={undoData.undoList.length ? false : true}>
 									<Icon name="arrow-down" style={{ transform: "rotate(90deg)" }} />
 								</Button>
@@ -592,7 +644,41 @@ export default defineComponent({
 							class={[styles.rightHideIcon, !showToolBox.value ? styles.rightIconShow : '']} 
 							src={rightHideIcon}
 							onClick={() => showToolBox.value = true } />
-					}  
+					}   */}	
+					<div class={[styles.editToolBox, !moveData.open && styles.itemDisabled]}>		
+						{
+							!state.isSingleLine && 
+							<>
+								<div class={[styles.editItem, styles.canEdit]} onClick={switchMoveState}>
+									<img src={moveData.open ? editCloseIcon : editIcon} />
+									<span>{moveData.open ? '取消' : '编辑'}</span>
+								</div>
+								<div class={styles.editItem} onClick={filterMoveData}>
+									<img src={editSaveIcon} />
+									<span>保存</span>
+								</div>
+								<div class={[styles.editItem, !undoData.undoList.length && styles.disabled]} onClick={handleUndo}>
+									<img src={editPreIcon} />
+									<span>撤回</span>
+								</div>
+								<div class={[styles.editItem, moveData.activeIndex <= -1 && styles.disabled]} onClick={handleDeleteMoveNote}>
+									<img src={editDeleteIcon} />
+									<span>{moveData.modelList[moveData.activeIndex]?.isDelete ? '回显' : '删除'}</span>
+								</div>
+								<div class={styles.editItem} onClick={resetMoveNote}>
+									<img src={editResetIcon} />
+									<span>重置</span>
+								</div>
+								{
+									moveData.tool.isAddAndSub && 
+									<div class={styles.extraItem}>
+										<img src={editReduceIcon} onClick={() => handleAddAndSub('sub')} />
+										<img src={editAddIcon} onClick={() => handleAddAndSub('add')} />
+									</div>								
+								}		
+							</>						
+						}										
+					</div>		
 				</div>
 				{moveData.modelList.map((item: any, index: number) => {
 					return (

+ 2 - 3
src/view/plugins/toggleMusicSheet/choosePartName/index.module.less

@@ -120,12 +120,11 @@
     }
   }
   .button {
+    display: block;
+    margin: 10px auto 0;
     cursor: pointer;
     width: 118px;
     height: 40px;
-    margin: 10px auto 0;
     z-index: 9;
-    background: url("./imgs/okBtn.png") no-repeat;
-    background-size: 100% 100%;
   }
 }

+ 36 - 42
src/view/plugins/toggleMusicSheet/choosePartName/index.tsx

@@ -1,16 +1,17 @@
 import { PropType, computed, defineComponent, ref, toRefs, onMounted, watch, nextTick } from 'vue'
 import { Picker, Button, Icon } from 'vant'
 import styles from './index.module.less'
-import state, { IPlatform } from "/src/state";
+import state, { IPlatform, checkMoveNoSave } from "/src/state";
 import changeName from "./imgs/changeName.png"
 import { headImg } from "/src/page-instrument/header-top/image";
 import { toggleMusicSheet } from "../index"
+import okBtn from "./imgs/okBtn.png"
 
 export default defineComponent({
   name: 'choosePartName',
   props: {
     partListNames: {
-      type: Array as PropType<string[]>,
+      type: Array as PropType<any[]>,
       default: () => [],
     },
     partIndex: {
@@ -20,73 +21,66 @@ export default defineComponent({
   },
   emits: ['close'],
   setup(props, { emit }) {
-    // #9463 bug,未更换声轨点击确定不应该重新加载,现在会导致切换错误
-    const partIndexChanged = ref(false);
-    const { partListNames, partIndex } = toRefs(props)
-    let idx = partListNames.value.findIndex((item: any) => item.value === partIndex.value);
-    idx = idx > -1 ? idx : 0;
-    const selectIndex = ref(idx);
-    const columns = computed(() => {
-      return partListNames.value
-    })
-    // console.log(1111,partListNames.value, partIndex.value, selectIndex.value, columns.value, 999999)
-    /**
-     * 默认选中的
-     * picker组件,3.x的版本可以使用defaultIndex,4.x的版本只能使用v-model传递
-     * */ 
-    const selValues = ref([partIndex.value]);
+    const selValues = ref([props.partIndex]);
     const myPicker = ref();
-    onMounted(() => {
-			// console.log(myPicker.value,99999,selValues.value,props.partIndex)
-		});
-
     watch(
       () => toggleMusicSheet.show,
       () => {
         if (toggleMusicSheet.show) {
-          selectIndex.value = partIndex.value
-          selValues.value = [partIndex.value]
+          selValues.value = [props.partIndex]
         }
-        //console.log('声轨',selValues.value,partIndex.value,selectIndex.value)
       }
     );
-
+    watch(() => toggleMusicSheet.show, ()=>{
+        // 支持滚轮事件
+        if (toggleMusicSheet.show) {
+          nextTick(() => {
+            myPicker.value.$el.addEventListener('wheel', handleWheel)
+          })
+        } else {
+            myPicker.value.$el.removeEventListener('wheel', handleWheel)
+        }
+      },{immediate:true}
+    )
+    function handleWheel(e: WheelEvent){
+      e.preventDefault()
+      // 先停止 惯性滚动
+      myPicker.value.confirm()
+      const direction = e.deltaY > 0 ? 1 : -1
+      const targetObject = myPicker.value.getSelectedOptions(0)[0]
+      const index = props.partListNames.findIndex(
+          obj => obj == targetObject
+      )
+      const newIndex = index + direction
+      if (newIndex >= 0 && newIndex < props.partListNames.length) {
+        selValues.value = [props.partListNames[newIndex].value]
+      }
+    }
     return () => (
       <div class={[styles.container, state.platform === IPlatform.PC && styles.pcContainer, styles[state.modeType]]}>
-        <div class={styles.head}>
+        <div class={[styles.head, "top_draging"]}>
           <img class={styles.headTit} src={changeName} />
           <img class={styles.closeImg} src={headImg("closeImg.png")} onClick={() => emit("close")} />
         </div>
-        { state.platform === IPlatform.PC && <div class={[!state.guideInfo?.teacherDrag && styles.pcPartTopZIndex ,styles.pcPartTop,'top_drag']}></div> }
         <div class={styles.pickerCon}>
           <div class={styles.pickerBox}>
             <Picker
               ref={myPicker}
               class={[styles.picker, state.platform === IPlatform.PC && styles.pcPicker]}
-              defaultIndex={props.partIndex}
               v-model={selValues.value}
               showToolbar={false}
-              columns={columns.value}
+              columns={props.partListNames}
               visible-option-num={5}
               option-height={"1.06666rem"}
-              onChange={(row) => {
-                console.log(1111,'选择的索引', row)
-                if (!partIndexChanged.value) partIndexChanged.value = true
-                selectIndex.value = row.selectedValues[0]
-              }}
             />
-            <div class={styles.button} onClick={() => {
+            <img src={ okBtn } class={styles.button} onClick={async () => {
+                await checkMoveNoSave();
                 myPicker.value.confirm()
                 nextTick(()=>{
-                  // console.log(1111,selectIndex.value)
-                  if (partIndexChanged.value) {
-                    emit('close', selectIndex.value)
-                  } else {
-                    emit('close', partIndex.value)
-                  }
+                  emit('close', selValues.value[0])
                 })
               }
-            }></div>
+            }></img>
           </div>
         </div>
       </div>

+ 6 - 2
src/view/plugins/toggleMusicSheet/index.tsx

@@ -37,7 +37,9 @@ export default defineComponent({
           sortId,
           canselect
         }
-      }).filter((item: any) => item.canselect).sort((a: any, b: any) => a.sortId - b.sortId)
+      }).filter((item: any) => item.canselect)
+      // 不需要自定义排序,改为按照xml声轨顺序显示
+      // .sort((a: any, b: any) => a.sortId - b.sortId)
       // 支持总谱渲染的时候 加上总谱字段
       state.isScoreRender && arr.unshift({canselect:true, sortId:999, text: "总谱", value: 999})
       return arr
@@ -69,6 +71,8 @@ export default defineComponent({
           type: 'fullscreen',
         },
       })
+      // 存储当前 模式
+      localStorage.setItem("musicScorePlayType", `${state.playType},${state.playSource}`)
       const _url =
         location.origin +
         location.pathname +
@@ -89,7 +93,7 @@ export default defineComponent({
       styleDrag: { value: null }
     } : useDrag(
       [
-        `${parentClassName} .top_drag`,
+        `${parentClassName} .top_draging`,
         `${parentClassName} .bom_drag`
       ],
       parentClassName,

+ 23 - 12
src/view/plugins/useDrag/index.ts

@@ -105,19 +105,22 @@ export default function useDrag(
 
 // 拖动
 function drag(el: HTMLElement, parentElement: HTMLElement, pos: Ref<posType>) {
-  function mousedown(e: MouseEvent) {
+  function onDown(e: MouseEvent | TouchEvent) {
+    const isTouchEv = isTouchEvent(e);
+    const event = isTouchEv ? e.touches[0] : e;
     const parentElementRect = parentElement.getBoundingClientRect();
-    const downX = e.clientX;
-    const downY = e.clientY;
+    const downX = event.clientX;
+    const downY = event.clientY;
     const clientWidth = document.documentElement.clientWidth;
     const clientHeight = document.documentElement.clientHeight;
     const maxLeft = clientWidth - parentElementRect.width;
     const maxTop = clientHeight - parentElementRect.height;
     const minLeft = 0;
     const minTop = 0;
-    function onMousemove(e: MouseEvent) {
-      let moveX = parentElementRect.left + (e.clientX - downX);
-      let moveY = parentElementRect.top + (e.clientY - downY);
+    function onMove(e: MouseEvent | TouchEvent) {
+      const event = isTouchEvent(e) ? e.touches[0] : e;
+      let moveX = parentElementRect.left + (event.clientX - downX);
+      let moveY = parentElementRect.top + (event.clientY - downY);
       moveX = moveX < minLeft ? minLeft : moveX > maxLeft ? maxLeft : moveX;
       moveY = moveY < minTop ? minTop : moveY > maxTop ? maxTop : moveY;
       pos.value = {
@@ -125,14 +128,22 @@ function drag(el: HTMLElement, parentElement: HTMLElement, pos: Ref<posType>) {
         left: moveX
       };
     }
-    function onMouseup() {
-      document.removeEventListener('mousemove', onMousemove);
-      document.removeEventListener('mouseup', onMouseup);
+    function onUp() {
+      document.removeEventListener(
+        isTouchEv ? 'touchmove' : 'mousemove',
+        onMove
+      );
+      document.removeEventListener(isTouchEv ? 'touchend' : 'mouseup', onUp);
     }
-    document.addEventListener('mousemove', onMousemove);
-    document.addEventListener('mouseup', onMouseup);
+    document.addEventListener(isTouchEv ? 'touchmove' : 'mousemove', onMove);
+    document.addEventListener(isTouchEv ? 'touchend' : 'mouseup', onUp);
   }
-  el.addEventListener('mousedown', mousedown);
+  el.addEventListener('mousedown', onDown);
+  el.addEventListener('touchstart', onDown);
+}
+
+function isTouchEvent(e: MouseEvent | TouchEvent): e is TouchEvent {
+  return window.TouchEvent && e instanceof window.TouchEvent;
 }
 
 // 缓存

BIN
src/view/selection/imgs/pitchHigh.png


BIN
src/view/selection/imgs/pitchLow.png


+ 49 - 30
src/view/selection/index.module.less

@@ -158,46 +158,65 @@
     }
 }
 
+// .followTipUp, .followTipDown {
+//     display: flex;
+//     align-items: center;
+//     background: rgba(0,0,0,0.75);
+//     position: relative;
+//     padding: 6px 10px;
+//     border-radius: 16px;
+//     width: fit-content;
+//     left: 50%;
+//     top: -42px;
+//     transform: translate(-50%);
+//     img {
+//         width: 18px;
+//         height: 18px;
+//         margin-right: 6px;
+//     }
+//     span {
+//         font-size: 14px;
+//         font-weight: 500;
+//         color: #fff;
+//         word-break: keep-all;
+//         display: flex;
+//         width: fit-content;
+//     }
+//     ::before {
+//         content: "";
+//         position: absolute;
+//         left: 50%;
+//         bottom: -8PX;
+//         transform: translateX(-50%);
+//         width: 13Px;
+//         height: 9Px;
+//         background-image: url('./imgs/arrow_icon.png');
+//         background-size: 100% 100%;
+//         background-position: center center;
+//         background-repeat: no-repeat;
+//         z-index: 2;    
+//         opacity: 0.7;
+//     }
+// }
+
 .followTipUp, .followTipDown {
     display: flex;
     align-items: center;
-    background: rgba(0,0,0,0.75);
     position: relative;
-    padding: 6px 10px;
-    border-radius: 16px;
     width: fit-content;
     left: 50%;
-    top: -42px;
+    top: -40px;
     transform: translate(-50%);
-    img {
-        width: 18px;
-        height: 18px;
-        margin-right: 6px;
+    > img {
+        width: auto;
+        height: 38px;
     }
-    span {
-        font-size: 14px;
-        font-weight: 500;
-        color: #fff;
-        word-break: keep-all;
-        display: flex;
-        width: fit-content;
-    }
-    ::before {
-        content: "";
-        position: absolute;
-        left: 50%;
-        bottom: -9PX;
-        transform: translateX(-50%);
-        width: 13Px;
-        height: 9Px;
-        background-image: url('./imgs/arrow_icon.png');
-        background-size: 100% 100%;
-        background-position: center center;
-        background-repeat: no-repeat;
-        z-index: 2;    
+}
+.isPad{
+    .followTipUp, .followTipDown {
+        transform: translate(-50%) scale(0.7);
     }
 }
-
 .followTipUp {
     i {
         font-style: normal;

+ 171 - 137
src/view/selection/index.tsx

@@ -8,8 +8,10 @@ import { Icon, showToast } from "vant";
 import MoveMusicScore, { moveData, renderForMoveData } from "../plugins/move-music-score";
 import { useRoute } from "vue-router";
 import { getQuery } from "/src/utils/queryString";
-import IntonationDown from "./imgs/down_icon.png"
-import IntonationUp from "./imgs/up_icon.png"
+import IntonationDown from "./imgs/pitchLow.png"
+import IntonationUp from "./imgs/pitchHigh.png"
+import MultipleRestMeasures from "./multipleRestMeasures"
+import { browser } from "../../utils";
 
 const selectData = reactive({
 	notes: [] as any[],
@@ -206,6 +208,8 @@ export const recalculateNoteData = () => {
 export default defineComponent({
 	name: "selection",
 	setup() {
+		const browsInfo = browser();
+		const isPad =  navigator?.userAgent?.includes("UAWEIVRD-W09") || browsInfo?.iPad || browsInfo.isTablet;
 		const route = useRoute();
 		const query: any = {
 			...getQuery(),
@@ -251,149 +255,179 @@ export default defineComponent({
 			// 初始化谱面可移动的元素位置
 			try {
 			moveData.partIndex = state.partIndex + ""
-			nextTick(() => renderForMoveData())
+			// 速度标记元素和谱面并非同时渲染,初始化可移动元素的时候,需要加个延迟
+			setTimeout(() => {
+				renderForMoveData()
+			}, 0);
 			} catch (error) {}
 		});
 		return () => (
-			<div
-				id="selectionBox"
-				class={[
-					styles.selectionContainer,
-				]}
-				onClick={(e: Event) => e.stopPropagation()}
-			>
-				{selectData.staves.map((item: any) => {
-					// 评测得分
-					const scoreItem = item.id && evaluatingData.evaluatings[item.measureListIndex];
-					// for(let idx in evaluatingData.evaluatings) {
-					// 	const { show, measureIndex } = evaluatingData.evaluatings[idx]
-					// 	if (show && measureIndex !== item.measureListIndex) {
-					// 		evaluatingData.evaluatings[idx].show = false
-					// 	}
-					// }
-					// 高级模式下,显示节拍线
-					// 不是报告模式
-					// 不是多小节休止符
-					// 节拍线开关
-					// 当前小节
-					// 当前小节
-					const lineShow =
-						!state.isReport &&
-						metronomeData.cursorMode === 2 &&
-						item.MeasureNumberXML === metronomeData.activeMetro?.measureNumberXML &&
-						state.times[state.activeNoteIndex].MeasureNumberXML === item.MeasureNumberXML;
-					return (
-						<>
-							{item.staveBox && (
-								<div
-									class={[
-										styles.position,
-										// scoreItem ? `scoreItemLeve${scoreItem.leve}` : "", // 去掉评测小节得分的背景色
-										item.multipleRestMeasures <= 1 ? styles.staveBg : "",
-										(state.platform === IPlatform.PC && state.zoom > 0.8) ? styles.linePC : '',
-									]}
-									style={item.staveBox}
-									onClick={() => handleSelection(item)}
-								>
-									{lineShow && (
-										<div style={{height: selectData.measureHeight + 'px', position: 'relative'}}>
+			<>
+				{
+					!state.isPreView &&
+					<div id="selectionBgBox" class={styles.staveBgContainer}>
+					{
+						selectData.staves.map((item: any) => {
+							return (
+								<>
+									{
+										item.staveBox && item.multipleRestMeasures <= 1 && 
 											<div 
-											class={[
-												styles.line,
-												state.setting.eyeProtection ? styles.eyeLine : '',
-												state.musicRenderType == EnumMusicRenderType.staff ? styles.lineStaff : styles.lineJianPu,
-											]} 
-											style={{ left: metronomeData.activeMetro.left }}></div>
-										</div>
-									)}
-									{!state.isReport &&
-										!!item.multipleRestMeasures &&
-										state.activeMeasureIndex == item.MeasureNumberXML && (
-											<div class={styles.dotWrap}>{item.multipleRestMeasures}</div>
-										)}
-									<Transition
-										name="centerTop"
-										onAfterEnter={() => {
-											scoreItem.show = false;
-										}}
+												style={{
+													left:item.staveBox.left,
+													top:`calc(${item.staveBox.top} + ${item.staveBox.height})`,
+													width:item.staveBox.width
+												}}
+												class={[styles.staveBg]}
+											></div>
+									}
+								</>
+							)
+						})
+					}
+					</div>					
+				}
+				<div
+					id="selectionBox"
+					class={[
+						styles.selectionContainer,
+						isPad && styles.isPad
+					]}
+					onClick={(e: Event) => e.stopPropagation()}
+				>
+					{selectData.staves.map((item: any, index) => {
+						// 评测得分
+						const scoreItem = item.id && evaluatingData.evaluatings[item.measureListIndex];
+						// for(let idx in evaluatingData.evaluatings) {
+						// 	const { show, measureIndex } = evaluatingData.evaluatings[idx]
+						// 	if (show && measureIndex !== item.measureListIndex) {
+						// 		evaluatingData.evaluatings[idx].show = false
+						// 	}
+						// }
+						// 高级模式下,显示节拍线
+						// 不是报告模式
+						// 不是多小节休止符
+						// 节拍线开关
+						// 当前小节
+						// 当前小节
+						/* 节拍指针,现在没有节拍器指针了,但是以后要加上的话,这里需要性能优化。现在这样每次节拍指针更新都会刷新这里的虚拟dom */
+						// const lineShow =
+						// 	!state.isReport &&
+						// 	metronomeData.cursorMode === 2 &&
+						// 	item.MeasureNumberXML === metronomeData.activeMetro?.measureNumberXML &&
+						// 	state.times[state.activeNoteIndex].MeasureNumberXML === item.MeasureNumberXML;
+						return (
+							<>
+								{item.staveBox && (
+									<div
+										key={item.id}
+										class={[
+											styles.position,
+											// scoreItem ? `scoreItemLeve${scoreItem.leve}` : "", // 去掉评测小节得分的背景色
+											(state.platform === IPlatform.PC && state.zoom > 0.8) ? styles.linePC : '',
+										]}
+										style={item.staveBox}
+										onClick={() => handleSelection(item)}
 									>
-										{scoreItem?.show && (
-											<div
-												class={styles.scoreItem}
-												style={{ color: leveByScoreMeasureIcons[scoreItem.leve]?.color || "" }}
-											>
-												<img src={leveByScoreMeasureIcons[scoreItem.leve]?.icon} />
-												<span>{scoreItem.score}</span>
+										{/* {lineShow && (
+											<div style={{height: selectData.measureHeight + 'px', position: 'relative'}}>
+												<div 
+												class={[
+													styles.line,
+													state.setting.eyeProtection ? styles.eyeLine : '',
+													state.musicRenderType == EnumMusicRenderType.staff ? styles.lineStaff : styles.lineJianPu,
+												]} 
+												style={{ left: metronomeData.activeMetro.left }}></div>
 											</div>
-										)}
-									</Transition>
-								</div>
-							)}
-						</>
-					);
-				})}
-				{selectData.notes.map((item: any) => {
-					return (
-						<div
-							class={[styles.position, disableClickNote.value && styles.disable, styles.note, `noteIndex_${item.index}`]}
-							style={item.bbox}
-							onClick={() => skipNotePlay(item.index)}
-						>
-							{/* <div class={styles.noteFollow} data-vf={"vf" + item.id}>
-								<Icon name="success" />
-								<Icon name="cross" />
-							</div> */}
-							<div class={styles.noteFollow} data-vf={"vf" + item.id}>
-								{/* <Icon name="success" />
-								<Icon name="cross" /> */}
-								<div class={[styles.followTipUp, 'tip-up']}>
-									<img src={IntonationUp} />
-									<span>音准<i>高了</i></span>
-								</div>
-								<div class={[styles.followTipDown, 'tip-down']}>
-									<img src={IntonationDown} />
-									<span>音准<i>低了</i></span>
-								</div>
-							</div>							
-							<div class={[styles.noteDot, 'node-dot']}></div>
-						</div>
-					);
-				})}
-				{/* 选段 */}
-				{
-					sectionPosData.value.map((item,index) =>{
+										)} */}
+										{!state.isReport &&
+											!!item.multipleRestMeasures &&
+											<MultipleRestMeasures item = {item}></MultipleRestMeasures>
+										}
+										<Transition
+											name="centerTop"
+											onAfterEnter={() => {
+												scoreItem.show = false;
+											}}
+										>
+											{scoreItem?.show && (
+												<div
+													class={styles.scoreItem}
+													style={{ color: leveByScoreMeasureIcons[scoreItem.leve]?.color || "" }}
+												>
+													<img src={leveByScoreMeasureIcons[scoreItem.leve]?.icon} />
+													<span>{scoreItem.score}</span>
+												</div>
+											)}
+										</Transition>
+									</div>
+								)}
+							</>
+						);
+					})}
+					{selectData.notes.map((item: any) => {
 						return (
-							item && <div class={styles.selectBox} style={item}>
-								<div class={[styles.selectHandle,index>0&&styles.selectHandleRight,(state.playState==="play" || query.workRecord)&&styles.playIng]} onClick={()=>{
-									// 如果选择了2个 删除左边的时候
-									if(state.section.length===2&&index === 0){
-										state.section = []
-										// 重置速度和播放倍率
-										resetBaseRate(state.activeNoteIndex);
-										showToast({
-											message: "请选择开始小节",
-											duration: 0,
-											position: "top",
-											className: "selectionToast",
-										});
-									}else{
-										state.section.splice(index,1)
-										state.section = [...state.section]  // 触发 watch
-										showToast({
-											message: state.section.length?"请选择结束小节":"请选择开始小节",
-											duration: 0,
-											position: "top",
-											className: "selectionToast",
-										});
-									}
-								}}></div>
+							<div
+								class={[styles.position, disableClickNote.value && styles.disable, styles.note, `noteIndex_${item.index}`]}
+								style={item.bbox}
+								onClick={() => skipNotePlay(item.index)}
+							>
+								{/* <div class={styles.noteFollow} data-vf={"vf" + item.id}>
+									<Icon name="success" />
+									<Icon name="cross" />
+								</div> */}
+								<div class={styles.noteFollow} data-vf={"vf" + item.id}>
+									{/* <Icon name="success" />
+									<Icon name="cross" /> */}
+									<div class={[styles.followTipUp, 'tip-up']}>
+										<img src={IntonationUp} />
+										{/* <span>音准<i>高了</i></span> */}
+									</div>
+									<div class={[styles.followTipDown, 'tip-down']}>
+										<img src={IntonationDown} />
+										{/* <span>音准<i>低了</i></span> */}
+									</div>
+								</div>							
+								<div class={[styles.noteDot, 'node-dot']}></div>
 							</div>
-						)
-					})
-				}
-				{/* 移动模块 */}
-				{query.isMove == "1" && <MoveMusicScore />}
-			</div>
+						);
+					})}
+					{/* 选段 */}
+					{
+						sectionPosData.value.map((item,index) =>{
+							return (
+								item && <div class={styles.selectBox} style={item}>
+									<div class={[styles.selectHandle,index>0&&styles.selectHandleRight,(state.playState==="play" || query.workRecord)&&styles.playIng]} onClick={()=>{
+										// 如果选择了2个 删除左边的时候
+										if(state.section.length===2&&index === 0){
+											state.section = []
+											// 重置速度和播放倍率
+											resetBaseRate(state.activeNoteIndex);
+											showToast({
+												message: "请选择开始小节",
+												duration: 0,
+												position: "top",
+												className: "selectionToast",
+											});
+										}else{
+											state.section.splice(index,1)
+											state.section = [...state.section]  // 触发 watch
+											showToast({
+												message: state.section.length?"请选择结束小节":"请选择开始小节",
+												duration: 0,
+												position: "top",
+												className: "selectionToast",
+											});
+										}
+									}}></div>
+								</div>
+							)
+						})
+					}
+					{/* 移动模块 */}
+					{query.isMove == "1" && <MoveMusicScore />}
+				</div>
+			</>
 		);
 	},
 });

+ 22 - 0
src/view/selection/multipleRestMeasures.tsx

@@ -0,0 +1,22 @@
+import { defineComponent } from "vue"
+import state from "/src/state"
+import styles from "./index.module.less"
+
+export default defineComponent({
+   name: "multipleRestMeasures",
+   props: {
+      item: {
+         type: Object,
+         required: true
+      }
+   },
+   setup(props) {
+      return () => 
+         <>
+            {
+               state.activeMeasureIndex == props.item.MeasureNumberXML 
+                  && <div class={styles.dotWrap} id="restDot">{props.item.multipleRestMeasures}</div>
+            }
+         </>
+   }
+})

+ 5 - 2
src/view/tick/index.tsx

@@ -10,6 +10,7 @@ import tockWav from "/src/assets/tock.mp3";
 
 const tickData = reactive({
 	len: 0,
+	denominator: undefined as undefined | number,
 	reduceLen: 0,
 	tickEnd: false,
 	/** 节拍器时间 */
@@ -95,9 +96,11 @@ const createAudio = (src: string): Promise<HTMLAudioElement | null> => {
 
 /** 设置节拍器
  * @param beat 节拍数
+ * @param denominator 节拍器分母
  */
-export const handleInitTick = (beat: number) => {
+export const handleInitTick = (beat: number, denominator?: number) => {
 	tickData.len = beat;
+	tickData.denominator = denominator;
 	// 节拍器的个数除以2 直到小于等于4为止 
 	while (beat > 4 && beat % 2 === 0) {
         beat = beat / 2;
@@ -111,7 +114,7 @@ export const handleStartTick = async () => {
 	tickData.show = true;
 	tickData.tickEnd = false;
 	tickData.index = 0;
-	tickData.beatLengthInMilliseconds = (60 / state.speed) * 1000;
+	tickData.beatLengthInMilliseconds = tickData.denominator ? 4 / tickData.denominator * (60 / state.speed) * 1000 : (60 / state.speed) * 1000;
 	for(let i = 0; i <= useLen.value; i++){
 		// 提前结束, 直接放回false
 		if (tickData.tickEnd) return false;

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
stats.html


+ 3 - 3
vite.config.ts

@@ -76,9 +76,9 @@ export default defineConfig({
         // target: "https://kt.colexiu.com",
         // target: "https://test.lexiaoya.cn",
         // target: "https://kt.colexiu.com",
-        // target: "https://test.resource.colexiu.com", // 内容平台开发环境,内容平台开发,需在url链接上加上isCbs=true
-        target: "https://test.kt.colexiu.com",
-        //target: "https://mec.colexiu.com",
+        //target: "https://test.resource.colexiu.com", // 内容平台开发环境,内容平台开发,需在url链接上加上isCbs=true
+        target: "https://dev.kt.colexiu.com",
+        // target: "https://mec.colexiu.com",
         changeOrigin: true,
         rewrite: (path) => path.replace(/^\/instrument/, ""),
       },

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov