فهرست منبع

添加曲谱详情页面

lex 1 سال پیش
والد
کامیت
efde321311
29فایلهای تغییر یافته به همراه1384 افزوده شده و 6 حذف شده
  1. 103 0
      package-lock.json
  2. 1 0
      package.json
  3. 159 0
      public/osmd/index.html
  4. 1 0
      public/osmd/opensheetmusicdisplay.min.js
  5. 6 0
      src/components/o-sticky/index.tsx
  6. 8 0
      src/router/routes-common.ts
  7. 44 0
      src/views/accompany/download.module.less
  8. 125 0
      src/views/accompany/download.tsx
  9. 79 0
      src/views/accompany/formatSvgToImg.ts
  10. 97 0
      src/views/accompany/imageFunction.ts
  11. BIN
      src/views/accompany/images/icon-change.png
  12. BIN
      src/views/accompany/images/icon-download.png
  13. BIN
      src/views/accompany/images/icon-music.png
  14. BIN
      src/views/accompany/images/logoWatermark.png
  15. BIN
      src/views/accompany/images/music-detail-bg.png
  16. BIN
      src/views/accompany/images/music-img-default.png
  17. 156 0
      src/views/accompany/music-detail.module.less
  18. 379 0
      src/views/accompany/music-detail.tsx
  19. 13 6
      src/views/accompany/music-list.tsx
  20. BIN
      src/views/accompany/staff-change/images/activeButtonIcon.png
  21. BIN
      src/views/accompany/staff-change/images/first-active.png
  22. BIN
      src/views/accompany/staff-change/images/first-default.png
  23. BIN
      src/views/accompany/staff-change/images/fixed-active.png
  24. BIN
      src/views/accompany/staff-change/images/fixed-default.png
  25. BIN
      src/views/accompany/staff-change/images/inactiveButtonIcon.png
  26. BIN
      src/views/accompany/staff-change/images/staff-active.png
  27. BIN
      src/views/accompany/staff-change/images/staff-default.png
  28. 88 0
      src/views/accompany/staff-change/index.module.less
  29. 125 0
      src/views/accompany/staff-change/index.tsx

+ 103 - 0
package-lock.json

@@ -14,6 +14,7 @@
         "@vueuse/core": "^8.4.1",
         "animate.css": "^4.1.1",
         "browserslist": "^4.20.2",
+        "canvg": "^4.0.2",
         "classnames": "^2.3.1",
         "clean-deep": "^3.4.0",
         "cos-js-sdk-v5": "^1.4.20",
@@ -2108,6 +2109,11 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@types/raf": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmmirror.com/@types/raf/-/raf-3.4.3.tgz",
+      "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw=="
+    },
     "node_modules/@types/through": {
       "version": "0.0.30",
       "resolved": "https://registry.npmmirror.com/@types/through/-/through-0.0.30.tgz",
@@ -3207,6 +3213,21 @@
         }
       ]
     },
+    "node_modules/canvg": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmmirror.com/canvg/-/canvg-4.0.2.tgz",
+      "integrity": "sha512-/7kIZger/mdFci4KXdtMr+NQB4GU1InkJ4RwSyDBRcvy4BUlg1hD+ZUWo550sWPyWaKZ8purqby6kjf09qVriw==",
+      "dependencies": {
+        "@types/raf": "^3.4.0",
+        "raf": "^3.4.1",
+        "rgbcolor": "^1.0.1",
+        "stackblur-canvas": "^2.0.0",
+        "svg-pathdata": "^6.0.3"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
     "node_modules/capital-case": {
       "version": "1.0.4",
       "resolved": "https://registry.npmmirror.com/capital-case/-/capital-case-1.0.4.tgz",
@@ -7763,6 +7784,11 @@
         "node": ">=8"
       }
     },
+    "node_modules/performance-now": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/performance-now/-/performance-now-2.1.0.tgz",
+      "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="
+    },
     "node_modules/picocolors": {
       "version": "1.0.0",
       "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.0.tgz",
@@ -8185,6 +8211,14 @@
       ],
       "license": "MIT"
     },
+    "node_modules/raf": {
+      "version": "3.4.1",
+      "resolved": "https://registry.npmmirror.com/raf/-/raf-3.4.1.tgz",
+      "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
+      "dependencies": {
+        "performance-now": "^2.1.0"
+      }
+    },
     "node_modules/rangetouch": {
       "version": "2.0.1",
       "resolved": "https://registry.npmmirror.com/rangetouch/-/rangetouch-2.0.1.tgz",
@@ -8418,6 +8452,14 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/rgbcolor": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/rgbcolor/-/rgbcolor-1.0.1.tgz",
+      "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
+      "engines": {
+        "node": ">= 0.8.15"
+      }
+    },
     "node_modules/rimraf": {
       "version": "3.0.2",
       "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz",
@@ -8711,6 +8753,14 @@
       "resolved": "https://registry.npmmirror.com/ssr-window/-/ssr-window-4.0.2.tgz",
       "integrity": "sha512-ISv/Ch+ig7SOtw7G2+qkwfVASzazUnvlDTwypdLoPoySv+6MqlOV10VwPSE6EWkGjhW50lUmghPmpYZXMu/+AQ=="
     },
+    "node_modules/stackblur-canvas": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmmirror.com/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
+      "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
+      "engines": {
+        "node": ">=0.1.14"
+      }
+    },
     "node_modules/store": {
       "version": "2.0.12",
       "resolved": "https://registry.npmmirror.com/store/-/store-2.0.12.tgz",
@@ -8910,6 +8960,14 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/svg-pathdata": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmmirror.com/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
+      "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
     "node_modules/svg-tags": {
       "version": "1.0.0",
       "resolved": "https://registry.npmmirror.com/svg-tags/-/svg-tags-1.0.0.tgz",
@@ -11663,6 +11721,11 @@
       "integrity": "sha512-A8F30k2gYJ/6e07spSCPpkuZu79LCnkPTvqmIWQzNGcrzwFKpVOydG41lNt5wZXjSI149qjyzC2L1+F2PD/NUA==",
       "dev": true
     },
+    "@types/raf": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmmirror.com/@types/raf/-/raf-3.4.3.tgz",
+      "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw=="
+    },
     "@types/through": {
       "version": "0.0.30",
       "resolved": "https://registry.npmmirror.com/@types/through/-/through-0.0.30.tgz",
@@ -12394,6 +12457,18 @@
       "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001450.tgz",
       "integrity": "sha512-qMBmvmQmFXaSxexkjjfMvD5rnDL0+m+dUMZKoDYsGG8iZN29RuYh9eRoMvKsT6uMAWlyUUGDEQGJJYjzCIO9ew=="
     },
+    "canvg": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmmirror.com/canvg/-/canvg-4.0.2.tgz",
+      "integrity": "sha512-/7kIZger/mdFci4KXdtMr+NQB4GU1InkJ4RwSyDBRcvy4BUlg1hD+ZUWo550sWPyWaKZ8purqby6kjf09qVriw==",
+      "requires": {
+        "@types/raf": "^3.4.0",
+        "raf": "^3.4.1",
+        "rgbcolor": "^1.0.1",
+        "stackblur-canvas": "^2.0.0",
+        "svg-pathdata": "^6.0.3"
+      }
+    },
     "capital-case": {
       "version": "1.0.4",
       "resolved": "https://registry.npmmirror.com/capital-case/-/capital-case-1.0.4.tgz",
@@ -15463,6 +15538,11 @@
       "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
       "dev": true
     },
+    "performance-now": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/performance-now/-/performance-now-2.1.0.tgz",
+      "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="
+    },
     "picocolors": {
       "version": "1.0.0",
       "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.0.tgz",
@@ -15769,6 +15849,14 @@
       "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
       "dev": true
     },
+    "raf": {
+      "version": "3.4.1",
+      "resolved": "https://registry.npmmirror.com/raf/-/raf-3.4.1.tgz",
+      "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
+      "requires": {
+        "performance-now": "^2.1.0"
+      }
+    },
     "rangetouch": {
       "version": "2.0.1",
       "resolved": "https://registry.npmmirror.com/rangetouch/-/rangetouch-2.0.1.tgz",
@@ -15941,6 +16029,11 @@
       "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==",
       "dev": true
     },
+    "rgbcolor": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/rgbcolor/-/rgbcolor-1.0.1.tgz",
+      "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw=="
+    },
     "rimraf": {
       "version": "3.0.2",
       "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz",
@@ -16149,6 +16242,11 @@
       "resolved": "https://registry.npmmirror.com/ssr-window/-/ssr-window-4.0.2.tgz",
       "integrity": "sha512-ISv/Ch+ig7SOtw7G2+qkwfVASzazUnvlDTwypdLoPoySv+6MqlOV10VwPSE6EWkGjhW50lUmghPmpYZXMu/+AQ=="
     },
+    "stackblur-canvas": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmmirror.com/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
+      "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ=="
+    },
     "store": {
       "version": "2.0.12",
       "resolved": "https://registry.npmmirror.com/store/-/store-2.0.12.tgz",
@@ -16270,6 +16368,11 @@
       "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
       "dev": true
     },
+    "svg-pathdata": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmmirror.com/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
+      "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw=="
+    },
     "svg-tags": {
       "version": "1.0.0",
       "resolved": "https://registry.npmmirror.com/svg-tags/-/svg-tags-1.0.0.tgz",

+ 1 - 0
package.json

@@ -27,6 +27,7 @@
     "@vueuse/core": "^8.4.1",
     "animate.css": "^4.1.1",
     "browserslist": "^4.20.2",
+    "canvg": "^4.0.2",
     "classnames": "^2.3.1",
     "clean-deep": "^3.4.0",
     "cos-js-sdk-v5": "^1.4.20",

+ 159 - 0
public/osmd/index.html

@@ -0,0 +1,159 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+  <meta charset="UTF-8">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Document</title>
+  <script src="./opensheetmusicdisplay.min.js"></script>
+  <style>
+    * {
+      margin: 0;
+      padding: 0;
+    }
+
+    body {
+      padding-bottom: 60px;
+      height: 600px;
+      overflow: hidden;
+    }
+
+    .vf-text {
+      display: none;
+    }
+
+    #cursorImg-0 {
+      display: none;
+    }
+  </style>
+</head>
+
+<body>
+  <div id="osmdContainer" />
+  <script>
+    var osmd = new opensheetmusicdisplay.OpenSheetMusicDisplay("osmdContainer");
+    osmd.setOptions({
+      backend: "svg",
+      drawTitle: false,
+      drawPartNames: false,
+      drawLyricist: false,
+    });
+    // osmd.EngravingRules.RenderMultipleRestMeasures = true;
+    // osmd.EngravingRules.CompactMode = true;
+    // osmd.EngravingRules.PageTopMarginNarrow = 5.0; // for compact mode
+    // osmd.EngravingRules.PageBottomMargin = 15.0;
+
+
+    // osmd.EngravingRules.DYMusicScoreType =
+    //     state.musicRenderType === EnumMusicRenderType.staff ? "staff" : "jianpu";
+    //   // 如果为固定调,需要加入全局
+    //   if (state.musicRenderType === EnumMusicRenderType.fixedTone) {
+    //     window.sett = {
+    //       keySignature: true,
+    //     };
+    //   }
+    // debugger
+    // console.log('osmd.EngravingRules')
+    // osmd.EngravingRules.StaffDistance = 1.0;
+    // osmd.EngravingRules.BetweenStaffDistance = 1.0;
+    // osmd.EngravingRules.MinimumStaffLineDistance = 1.0;
+    //osmd.EngravingRules.MinSkyBottomDistBetweenStaves = 1.0; // default 1.0. this can cause collisions with slurs and dynamics sometimes
+    osmd.EngravingRules.MinSkyBottomDistBetweenSystems = 3.0; // default 5.0
+    // note that osmd.EngravingRules === osmd.rules, since it's passed as a reference
+
+
+    osmd.EngravingRules.MinimumDistanceBetweenSystems = 1;
+    // osmd.setPageFormat('794x1123')
+    osmd.setPageFormat('650x884')
+    function getSvgPngToSize(osmd) {
+      if (osmd) {
+        if (osmd.Drawer.Backends.length > 0) {
+          var imgList = []
+
+          for (var idx = 0, len = osmd.Drawer.Backends.length; idx < len; idx++) {
+            var backend = osmd.Drawer.Backends[idx]
+            var state = backend.ctx.state;
+            var width = backend.ctx.width / state.scale.x;
+            var height = backend.ctx.height / state.scale.y;
+            var cont = new XMLSerializer().serializeToString(
+              backend.ctx.svg
+            )
+
+            imgList.push({
+              img: cont,
+              width: width,
+              height: height,
+            })
+          }
+          return imgList
+        }
+      } else {
+        console.log('没有OSMD')
+      }
+    }
+    function render() {
+      osmd.render();
+      // console.log(getSvgPngToSize(osmd), 'getSvgPngToSize(osmd)')
+      window.parent.postMessage({
+        api: 'musicStaffRender',
+        loading: false,
+        osmdImg: getSvgPngToSize(osmd)
+      }, '*');
+    }
+
+    function renderXml(xmlUrl, partIndex) {
+      osmd
+        .load(xmlUrl)
+        .then(
+          function () {
+            for (let i = 0; i < osmd.Sheet.Instruments.length; i++) {
+              // console.log(osmd.Sheet.Instruments[i].Name);
+              osmd.Sheet.Instruments[i].Visible = i === partIndex;
+            }
+            osmd.zoom = .5
+            render();
+          }
+        );
+    }
+    function resetRender(partIndex) {
+      for (let i = 0; i < osmd.Sheet.Instruments.length; i++) {
+        osmd.Sheet.Instruments[i].Visible = i === partIndex;
+      }
+      render();
+
+    }
+
+    function resetRenderPage(type, xmlUrl) {
+      let str = 'staff'
+      if (type === 'first') {
+        str = 'jianpu'
+        window.sett = {
+          keySignature: false,
+        };
+      } else if (type === 'fixed') {
+        str = 'jianpu'
+        window.sett = {
+          keySignature: true,
+        };
+      }
+
+      osmd.EngravingRules.DYMusicScoreType = str
+      // console.log(type, xmlUrl)
+      osmd
+        .load(xmlUrl)
+        .then(
+          function () {
+            // for (let i = 0; i < osmd.Sheet.Instruments.length; i++) {
+            //   // console.log(osmd.Sheet.Instruments[i].Name);
+            //   osmd.Sheet.Instruments[i].Visible = i === partIndex;
+            // }
+            osmd.zoom = .5
+            render();
+          }
+        );
+    }
+  </script>
+</body>
+
+</html>

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 0
public/osmd/opensheetmusicdisplay.min.js


+ 6 - 0
src/components/o-sticky/index.tsx

@@ -24,6 +24,11 @@ export default defineComponent({
     },
     offsetBottom: {
       default: '0px'
+    },
+    // 变量名
+    varName: {
+      type: String,
+      default: '--header-height'
     }
   },
   data() {
@@ -81,6 +86,7 @@ export default defineComponent({
     __initHeight(height: any) {
       this.sectionStyle.height = `${height}px`
       this.heightV = height
+      document.documentElement.style.setProperty(this.varName, `${height}px`)
       this.$emit('getHeight', height)
     }
   },

+ 8 - 0
src/router/routes-common.ts

@@ -52,6 +52,14 @@ export const router: RouteRecordRaw[] = [
     }
   },
   {
+    path: '/musicDetail',
+    name: 'music-detail',
+    component: () => import('@/views/accompany/music-detail'),
+    meta: {
+      title: '曲谱详情'
+    }
+  },
+  {
     path: '/information-list',
     name: 'information-list',
     component: () => import('@/views/information'),

+ 44 - 0
src/views/accompany/download.module.less

@@ -0,0 +1,44 @@
+.downloadContainer {
+  padding: 20px 18px;
+}
+
+.musicContainer {
+  text-align: center;
+  max-height: 420px;
+  overflow: hidden;
+  overflow-y: auto;
+
+  h2 {
+    font-size: 16px;
+    color: #1a1a1a;
+    line-height: 22px;
+  }
+
+  .musicImg {
+    min-height: 408px;
+  }
+}
+
+.num {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 14px;
+  color: #777777;
+  padding: 0 0 12px;
+
+  .page {
+    font-size: 16px;
+    font-weight: 600;
+    line-height: 22px;
+    color: #131415;
+  }
+}
+
+.downloadBtn {
+  // box-shadow: 0px 2px 7px 0px rgba(45, 199, 170, 0.25);
+  font-size: 16px;
+  font-weight: 600;
+  color: #ffffff;
+  line-height: 20px;
+}

+ 125 - 0
src/views/accompany/download.tsx

@@ -0,0 +1,125 @@
+import { defineComponent, reactive, ref, watch } from 'vue'
+import styles from './download.module.less'
+import { postMessage, promisefiyPostMessage } from '@/helpers/native-message'
+import { addMusicTitle, addWatermark, convasToImg, imgToCanvas } from './imageFunction'
+import { Button, Swipe, SwipeItem, Toast, Image } from 'vant'
+
+export default defineComponent({
+  name: 'download',
+  props: {
+    imgList: {
+      type: Array,
+      default: () => []
+    },
+    musicSheetName: {
+      type: String,
+      default: ''
+    }
+  },
+  setup(props) {
+    const list = ref(props.imgList)
+    const swipeRef = ref()
+    watch(
+      () => props.imgList,
+      (val: any) => {
+        list.value = val
+        acitveIndex.value = 0
+        if (swipeRef.value) swipeRef.value.swipeTo(0)
+      }
+    )
+    const acitveIndex = ref(0)
+    const saveLoading = ref<boolean>(false)
+    const image = ref('')
+    const onSaveImg = async () => {
+      // 判断是否在保存中...
+      if (saveLoading.value) {
+        return
+      }
+      saveLoading.value = true
+      // 判断是否已经生成图片
+      if (image.value) {
+        saveImg()
+      } else {
+        console.log(list.value[acitveIndex.value], 'list.value[acitveIndex.value]')
+        const tempCanvas = await imgToCanvas(list.value[acitveIndex.value] as any)
+        const titleCanvas = addMusicTitle(tempCanvas, {
+          title: props.musicSheetName,
+          size: 12
+        })
+        // const canvas = await addWatermark(titleCanvas, '管乐团')
+        image.value = convasToImg(titleCanvas)
+        console.log(image.value, 'image.value')
+        await saveImg()
+      }
+    }
+
+    const saveImg = async () => {
+      Toast.loading({
+        message: '图片生成中...',
+        forbidClick: true
+      })
+      setTimeout(() => {
+        saveLoading.value = false
+      }, 100)
+      const res = await promisefiyPostMessage({
+        api: 'savePicture',
+        content: {
+          base64: image.value
+        }
+      })
+      if (res?.content?.status === 'success') {
+        Toast.success('已保存到相册')
+      } else {
+        Toast.fail('保存失败')
+      }
+    }
+    return () => {
+      return (
+        <div class={styles.downloadContainer}>
+          <div class={styles.musicContainer}>
+            <h2>{props.musicSheetName}</h2>
+            <div class={styles.musicImg}>
+              <Swipe
+                ref={swipeRef}
+                showIndicators={false}
+                loop={false}
+                onChange={(index: number) => {
+                  acitveIndex.value = index
+                  image.value = ''
+                }}
+              >
+                {list.value.length > 0 &&
+                  list.value.map((img: any) => (
+                    <SwipeItem>
+                      <Image src={img} />
+                    </SwipeItem>
+                  ))}
+              </Swipe>
+            </div>
+          </div>
+          <div class={styles.buttonGroup}>
+            <div class={styles.num}>
+              <span class={styles.page}>
+                {acitveIndex.value + 1}/{list.value.length}
+              </span>
+              <span class={styles.countPage}>(共{list.value.length}页)</span>
+            </div>
+
+            <Button
+              type="primary"
+              color="#FF8057"
+              class={styles.downloadBtn}
+              block
+              round
+              onClick={() => onSaveImg()}
+              loading={saveLoading.value}
+              loadingText={'下载中...'}
+            >
+              下载当前页面
+            </Button>
+          </div>
+        </div>
+      )
+    }
+  }
+})

+ 79 - 0
src/views/accompany/formatSvgToImg.ts

@@ -0,0 +1,79 @@
+import { Canvg, presets } from 'canvg'
+
+// https://gist.githubusercontent.com/n1ru4l/9c7eff52fe084d67ff15ae6b0af5f171/raw/da9fe36d72171d4e36b92aced587b48dc5182792/offscreen-canvas-polyfill.js
+if (!window.OffscreenCanvas) {
+  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+  // @ts-ignore
+  window.OffscreenCanvas = class OffscreenCanvas {
+    canvas: HTMLCanvasElement
+    constructor(width: number, height: number) {
+      this.canvas = document.createElement('canvas')
+      this.canvas.width = width
+      this.canvas.height = height
+
+      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+      // @ts-ignore
+      this.canvas.convertToBlob = () => {
+        return new Promise(resolve => {
+          this.canvas.toBlob(resolve)
+        })
+      }
+
+      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+      // @ts-ignore
+      return this.canvas
+    }
+  }
+}
+
+const preset: any = presets.offscreen()
+const blobToBase64 = (blob: any) => {
+  return new Promise((resolve, _) => {
+    const reader = new FileReader()
+    reader.onloadend = () => resolve(reader.result)
+    reader.readAsDataURL(blob)
+  })
+}
+let canvas = null as any
+export const svgtopng = async (svg: any, width: any, height: any) => {
+
+  if (!canvas) {
+    canvas = new OffscreenCanvas(width, height)
+  }
+
+  const ctx = canvas.getContext('2d')!
+  let v: any = await Canvg.fromString(ctx!, svg, preset)
+
+  /**
+   * Resize SVG to fit in given size.
+   * @param width
+   * @param height
+   * @param preserveAspectRatio
+   */
+  v.resize(width * 2, height * 2, 'xMidYMid meet')
+
+  // Render only first frame, ignoring animations and mouse.
+  await v.start()
+  const blob = await canvas.convertToBlob()
+  const base64 = await blobToBase64(blob)
+  // canvas.drawImage(base64
+  // await v.stop()
+  // releaseCanvas(canvas)
+  ctx.clearRect(0, 0, canvas.width, canvas.height)
+  // canvas = null
+  // console.log(canvas, 'draw')
+  await v.stop()
+  v = null
+  return base64
+}
+
+// function releaseCanvas(canvasElement) {
+//   // 清空 Canvas 上的内容
+//   const ctx = canvasElement.getContext('2d')
+//   ctx.clearRect(0, 0, canvasElement.width, canvasElement.height)
+
+//   // 停止任何正在进行的动画或定时器
+//   cancelAnimationFrame(canvasElement.animationId)
+//   // 删除对 Canvas 元素的引用
+//   canvasElement = null
+// }

+ 97 - 0
src/views/accompany/imageFunction.ts

@@ -0,0 +1,97 @@
+import imgList from './images/logoWatermark.png'
+export const imgToCanvas = async (url: string) => {
+  console.log('imgToCanvas', url)
+  const img = document.createElement('img')
+  img.setAttribute('crossOrigin', 'anonymous')
+  // 为了处理base64 和 连接加载不同的
+  if (url && typeof url == 'string' && url.includes('data:image')) {
+    img.src = url
+  } else {
+    img.src = url + `?t=${+new Date()}`
+  }
+
+  // 防止跨域引起的 Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.
+  await new Promise(resolve => (img.onload = resolve))
+  // 创建canvas DOM元素,并设置其宽高和图片一样
+  const canvas = document.createElement('canvas')
+  canvas.width = img.width
+  canvas.height = img.height
+  // 坐标(0,0) 表示从此处开始绘制,相当于偏移。
+  const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
+
+  ctx.fillStyle = 'rgb(255, 255, 255)'
+  ctx.fillStyle = '#fff'
+  ctx.fillRect(0, 0, img.width, img.height)
+  ctx.drawImage(img, 0, 0)
+  return canvas
+}
+
+/* canvas添加水印
+
+* @param {canvas对象} canvas
+
+* @param {水印文字} text
+
+*/
+
+export const addWatermark = async (canvas, text) => {
+  console.log('addWatermark')
+  try {
+    const ctx = canvas.getContext('2d')
+    // ctx.fillStyle = '#fff'
+    const img = document.createElement('img')
+    img.setAttribute('crossOrigin', 'anonymous')
+    // 为了处理base64 和 连接加载不同的
+    if (
+      imgList &&
+      typeof imgList == 'string' &&
+      imgList.includes('data:image')
+    ) {
+      img.src = imgList
+    } else {
+      img.src = imgList + `?${new Date().getTime()}`
+    }
+    // 防止跨域引起的 Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.
+    await new Promise(resolve => (img.onload = resolve))
+    // 创建canvas DOM元素,并设置其宽高和图片一样
+    const water = document.createElement('canvas')
+    water.width = 600
+    water.height = 500
+
+    // 第一层
+    const waterCtx = water.getContext('2d') as CanvasRenderingContext2D
+    waterCtx.clearRect(0, 0, water.width, water.height)
+    // 小水印中文字偏转角度
+    waterCtx.rotate((-30 * Math.PI) / 180)
+    waterCtx.drawImage(img, 0, 300)
+    const pat = ctx.createPattern(water, 'repeat')
+    ctx.fillStyle = pat
+    ctx.fillRect(0, 0, canvas.width, canvas.height)
+
+    return canvas
+  } catch (e) {
+    console.log(e)
+  }
+}
+
+export const addMusicTitle = (canvas, info) => {
+  canvas.getContext('2d')
+  const water = document.createElement('canvas')
+
+  // 小水印画布大小
+  water.width = canvas.width
+  water.height = canvas.height + 70
+  const waterCtx = water.getContext('2d') as CanvasRenderingContext2D
+  waterCtx.fillStyle = '#fff'
+  waterCtx.fillRect(0, 0, canvas.width, canvas.height + 90)
+  waterCtx.font = `40pt Calibri`
+  waterCtx.fillStyle = '#000'
+  waterCtx.textAlign = 'center'
+  waterCtx.drawImage(canvas, 0, 70)
+  waterCtx.fillText(info.title, canvas.width / 2, 120)
+  return water
+}
+
+export const convasToImg = canvas => {
+  return canvas.toDataURL('image/png')
+}

BIN
src/views/accompany/images/icon-change.png


BIN
src/views/accompany/images/icon-download.png


BIN
src/views/accompany/images/icon-music.png


BIN
src/views/accompany/images/logoWatermark.png


BIN
src/views/accompany/images/music-detail-bg.png


BIN
src/views/accompany/images/music-img-default.png


+ 156 - 0
src/views/accompany/music-detail.module.less

@@ -0,0 +1,156 @@
+.musicDetail {
+  background: url('./images/music-detail-bg.png') top center/ cover no-repeat;
+  background-size: contain;
+  min-height: 100vh;
+  display: flex;
+  flex-direction: column;
+}
+
+
+.musicContainer {
+  flex: 1 auto;
+  display: flex;
+  flex-direction: column;
+  position: relative;
+  background-color: #fff;
+  padding: 0;
+  z-index: 12;
+  border-radius: 16px 16px 0 0;
+  position: relative;
+  margin: 54px 0 0;
+}
+
+.musicInfos {
+  display: flex;
+  align-items: center;
+  flex-direction: column;
+  padding: 10px 20px 8;
+
+  .musicImg {
+    margin-top: -32px;
+    background: #C4C0BD;
+    border-radius: 16px;
+    border: 2px solid #FFFFFF;
+    width: 85px;
+    height: 85px;
+    overflow: hidden;
+
+    :global {
+      .van-image {
+        width: 100%;
+        height: 100%;
+
+      }
+    }
+  }
+
+  .info {
+    padding-left: 12px;
+
+    p {
+      margin: 0;
+    }
+
+    .names {
+      padding-top: 8px;
+      display: flex;
+      align-items: center;
+      line-height: 25px;
+      font-weight: 600;
+      font-size: 18px;
+      color: #131415;
+      padding-right: 6px;
+      max-width: 180px;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+
+    .author {
+      padding-top: 6px;
+      font-size: 12px;
+      text-align: center;
+    }
+  }
+
+}
+
+.audio {
+  --plyr-control-icon-size: 10px;
+  --plyr-color-main: #FF8057 !important;
+  // --plyr-control-spacing: 10px 20px;
+  padding: 8px 10px 2px;
+
+  :global {
+    .plyr__controls .plyr__controls__item:first-child {
+      background-color: #FF8057;
+      color: #fff;
+      border-radius: 50%;
+
+      &.icon--not-pressed {
+        padding-left: 1px;
+      }
+    }
+
+    .plyr__time+.plyr__time {
+      display: inline-block;
+    }
+  }
+}
+
+.showImgContainer {
+  padding: 0 11px;
+  height: calc(100vh - var(--header-height) - var(--footer-height) - 164px);
+  flex: 1 auto;
+  overflow: hidden;
+
+
+  .musicImg {
+    width: 100%;
+  }
+}
+
+.footers {
+  display: flex;
+  align-items: center;
+  padding: 0 20px 20px; // height: 45px;
+
+  :global {
+    .van-button {
+      font-size: 16px;
+      font-weight: 600;
+      color: #FFFFFF;
+      line-height: 25px;
+    }
+  }
+
+  .iconGroup {
+    display: flex;
+    align-items: center;
+
+    .icon {
+      display: flex;
+      align-items: center;
+      flex-direction: column;
+      margin-right: 30px;
+    }
+
+    img {
+      width: 22px;
+      height: 22px;
+    }
+
+    span {
+      padding-top: 4px;
+      font-size: 10px;
+      color: #333333;
+      line-height: 14px;
+    }
+
+  }
+}
+
+.staffChange {
+  --van-popup-close-icon-color: #333333;
+  --van-popup-close-icon-size: 18px;
+}

+ 379 - 0
src/views/accompany/music-detail.tsx

@@ -0,0 +1,379 @@
+import OHeader from '@/components/o-header'
+import OSticky from '@/components/o-sticky'
+import { defineComponent, nextTick, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
+import styles from './music-detail.module.less'
+import { Button, Image, Picker, Popup, Skeleton } from 'vant'
+import iconBg from './images/music-img-default.png'
+import iconDownload from './images/icon-download.png'
+import iconChange from './images/icon-change.png'
+import iconMusic from './images/icon-music.png'
+import request from '@/helpers/request'
+import { state } from '@/state'
+import { useRoute } from 'vue-router'
+import Plyr from 'plyr'
+import 'plyr/dist/plyr.css'
+import deepClone from '@/helpers/deep-clone'
+import StaffChange from './staff-change'
+import Download from './download'
+import { svgtopng } from './formatSvgToImg'
+import requestOrigin from 'umi-request'
+import { getInstrumentName } from '@/constant/instruments'
+
+export default defineComponent({
+  name: 'music-detail',
+  setup() {
+    const route = useRoute()
+    const audioRef = ref()
+    const player = ref<any>(null)
+    const partColumns = ref<any>([])
+    const staffData = reactive({
+      details: {} as any,
+      status: false,
+      open: false,
+      audioReady: false,
+      iframeSrc: '',
+      musicXml: [] as any,
+      instrumentName: '',
+      iframeRef: null as any,
+      imgs: [] as any,
+      radio: 'staff' as any,
+      partList: [] as any[],
+      partNames: [] as any[],
+
+      selectedPartName: '' as any,
+      selectedPartIndex: 0,
+      partXmlIndex: 0
+    })
+    const loading = ref(false)
+    const downloadStatus = ref(false)
+    const showImg = ref([] as any)
+
+    watch(
+      () => staffData.radio,
+      (val: string) => {
+        if (val == 'first') {
+          showImg.value = deepClone(staffData.details.musicFirstSvg?.split(','))
+        } else if (val == 'fixed') {
+          showImg.value = deepClone(staffData.details.musicJianSvg?.split(','))
+        } else {
+          showImg.value = deepClone(staffData.details.musicImg?.split(','))
+        }
+      }
+    )
+
+    const musicIframeLoad = async () => {
+      const iframeRef: any = document.getElementById('staffIframeRef')
+      if (iframeRef && iframeRef.contentWindow.renderXml) {
+        iframeRef.contentWindow.renderXml(staffData.details.xmlFileUrl, staffData.partXmlIndex)
+      }
+    }
+    const resetRender = async () => {
+      const iframeRef: any = document.getElementById('staffIframeRef')
+      if (iframeRef && iframeRef.contentWindow.renderXml) {
+        loading.value = true
+        iframeRef.contentWindow.resetRender(staffData.partXmlIndex)
+      }
+    }
+
+    const renderStaff = async () => {
+      try {
+        staffData.iframeSrc = `${location.origin}/osmd/index.html`
+        // staffData.iframeSrc = `${location.origin}${location.pathname}osmd/index.html`
+      } catch (error) {
+        //
+      }
+    }
+    const getPartNames = async (xmlUrl: string) => {
+      const partNames: string[] = []
+      try {
+        const res = await requestOrigin.get(xmlUrl, { mode: 'cors' })
+        const xml: any = new DOMParser().parseFromString(res, 'text/xml')
+        for (const item of xml.getElementsByTagName('part-name')) {
+          if (item.textContent) {
+            partNames.push(item.textContent)
+          }
+        }
+      } catch (error) {
+        //
+      }
+      return partNames.filter((text: string) => text.toLocaleUpperCase() !== 'COMMON') || []
+    }
+
+    const toDetail = async (row: any) => {
+      staffData.partNames = await getPartNames(row.xmlFileUrl)
+      console.log(staffData.partNames, 'partNames')
+      let partList = row.background || []
+      partList = partList.filter(
+        (item: any) => !item.track?.toLocaleUpperCase()?.includes('COMMON')
+      )
+      partColumns.value = partList.map((item: any, index: number) => {
+        const instrumentName = getInstrumentName(item.track)
+        const xmlIndex = staffData.partNames.findIndex((name: any) => name === item.track)
+        return {
+          text: item.track + (instrumentName ? `(${instrumentName})` : ''),
+          instrumentName: instrumentName,
+          xmlIndex,
+          value: index
+        }
+      })
+      // 初始化数据
+      const defaultShowStaff = partColumns.value[staffData.selectedPartIndex]
+      staffData.selectedPartName = defaultShowStaff.instrumentName
+      staffData.partXmlIndex = defaultShowStaff.xmlIndex
+    }
+
+    const getMusicDetail = async () => {
+      loading.value = true
+      try {
+        if (!route.query.id) return
+        const { data } = await request.get(
+          state.platformApi + '/musicSheet/detail/' + route.query.id
+        )
+
+        staffData.details = data || {}
+        showImg.value = staffData.details.musicImg?.split(',')
+
+        nextTick(async () => {
+          if (data.audioFileUrl) {
+            initAudio()
+          } else {
+            if (data.musicSheetType === 'SINGLE') {
+              loading.value = false
+              return
+            }
+            await toDetail(staffData.details)
+            renderStaff()
+          }
+        })
+      } catch (e) {
+        //
+        console.log(e)
+      }
+    }
+
+    const initAudio = async () => {
+      const controls = [
+        // 'play-large',
+        'play',
+        'progress',
+        'captions',
+        // 'fullscreen',
+        'current-time',
+        'duration'
+      ]
+      player.value = new Plyr(audioRef.value, {
+        controls: controls
+      })
+
+      player.value.on('ready', () => {
+        staffData.audioReady = true
+        nextTick(async () => {
+          if (staffData.details.musicSheetType === 'SINGLE') {
+            loading.value = false
+            return
+          }
+          await toDetail(staffData.details)
+          renderStaff()
+        })
+      })
+    }
+
+    //进入云练习
+    const openView = async (item: any) => {
+      const src = `${location.origin}/orchestra-music-score/?id=${item.id}&part-index=${staffData.selectedPartIndex}`
+      console.log('🚀 ~ src:', src)
+      postMessage({
+        api: 'openAccompanyWebView',
+        content: {
+          url: src,
+          orientation: 0,
+          isHideTitle: true,
+          statusBarTextColor: false,
+          isOpenLight: true
+        }
+      })
+    }
+
+    const onSubmit = () => {
+      openView(staffData.details)
+    }
+
+    const showLoading = async (e: any) => {
+      console.log(e, 'enter')
+      if (e.data?.api === 'musicStaffRender') {
+        try {
+          const osmdImg = e.data.osmdImg
+          const imgs: any = []
+          for (let i = 0; i < osmdImg.length; i++) {
+            const img: any = await svgtopng(osmdImg[i].img, osmdImg[i].width, osmdImg[i].height)
+            imgs.push(img)
+          }
+          showImg.value = imgs
+        } catch (e) {
+          //
+        }
+        loading.value = e.data.loading
+      }
+    }
+
+    onMounted(async () => {
+      await getMusicDetail()
+      window.addEventListener('message', showLoading)
+    })
+    onUnmounted(() => {
+      window.removeEventListener('message', showLoading)
+    })
+    return () => (
+      <div class={styles.musicDetail}>
+        <OSticky mode="sticky" position="top">
+          <OHeader border={false} background={'transparent'} />
+        </OSticky>
+
+        <div class={styles.musicContainer}>
+          <div class={styles.musicInfos}>
+            <div class={styles.musicImg}>
+              <Image src={iconBg} />
+            </div>
+            <div class={styles.info}>
+              <p class={styles.names}>
+                {staffData.details.musicSheetName}
+                {staffData.details.musicSheetType === 'CONCERT' && staffData.selectedPartName
+                  ? `(${staffData.selectedPartName})`
+                  : ''}
+              </p>
+              <p class={styles.author}>{staffData.details.composer}</p>
+            </div>
+          </div>
+
+          <div class={styles.showImgContainer}>
+            {staffData.details?.musicSheetType === 'CONCERT' ? (
+              <>
+                {loading.value && (
+                  <>
+                    <Skeleton title row={7} />
+                  </>
+                )}
+                <iframe
+                  id="staffIframeRef"
+                  style={{
+                    opacity: loading.value ? 0 : 1,
+                    width: '100%',
+                    height: '100%'
+                  }}
+                  src={staffData.iframeSrc}
+                  onLoad={musicIframeLoad}
+                ></iframe>
+              </>
+            ) : (
+              <>
+                {showImg.value.length > 0 && (
+                  <>
+                    <img src={showImg.value[0]} alt="" class={styles.musicImg} />
+                  </>
+                )}
+              </>
+            )}
+          </div>
+        </div>
+
+        <OSticky position="bottom" varName="--footer-height">
+          <div class={styles.bottomStyle} style={{ background: '#fff' }}>
+            <div
+              class={[styles.audio, styles.collectCell]}
+              style={{ opacity: staffData.audioReady ? 1 : 0 }}
+            >
+              <audio id="player" controls ref={audioRef} style={{ height: '40px' }}>
+                <source src={staffData.details?.audioFileUrl} type="audio/mp3" />
+              </audio>
+            </div>
+
+            <div class={styles.footers}>
+              <div class={styles.iconGroup}>
+                <div
+                  class={styles.icon}
+                  onClick={() => {
+                    if (loading.value) return
+                    downloadStatus.value = true
+                  }}
+                >
+                  <img src={iconDownload} />
+                  <span>下载</span>
+                </div>
+                {staffData.details?.musicSheetType === 'CONCERT' ? (
+                  <div
+                    class={styles.icon}
+                    onClick={() => {
+                      if (loading.value) return
+                      staffData.open = true
+                    }}
+                  >
+                    <img src={iconMusic} />
+                    <span>声轨</span>
+                  </div>
+                ) : (
+                  <div
+                    class={styles.icon}
+                    onClick={() => {
+                      if (loading.value) return
+                      staffData.status = true
+                    }}
+                  >
+                    <img src={iconChange} />
+                    <span>转谱</span>
+                  </div>
+                )}
+              </div>
+              <Button
+                round
+                block
+                type="primary"
+                disabled={loading.value}
+                color={'#FF8057'}
+                onClick={onSubmit}
+              >
+                开始练习
+              </Button>
+            </div>
+          </div>
+        </OSticky>
+
+        <Popup
+          v-model:show={staffData.status}
+          teleport="body"
+          closeable
+          style={{ width: '80%' }}
+          class={styles.staffChange}
+          round
+        >
+          <StaffChange v-model:radio={staffData.radio} onClose={() => (staffData.status = false)} />
+        </Popup>
+
+        <Popup v-model:show={downloadStatus.value} position="bottom" round>
+          {downloadStatus.value && (
+            <Download
+              imgList={JSON.parse(JSON.stringify(showImg.value))}
+              musicSheetName={staffData.details.musicSheetName}
+            />
+          )}
+        </Popup>
+
+        <Popup teleport="body" position="bottom" round v-model:show={staffData.open}>
+          <Picker
+            columns={partColumns.value}
+            onConfirm={(value) => {
+              staffData.open = false
+              staffData.selectedPartIndex = value.selectedValues[0]
+              staffData.selectedPartName = value.selectedOptions[0].instrumentName
+              staffData.partXmlIndex = value.selectedOptions[0].xmlIndex
+              // openView({ id: staffData.instrumentName })
+              nextTick(() => {
+                resetRender()
+              })
+            }}
+            onCancel={() => (staffData.open = false)}
+          />
+        </Popup>
+      </div>
+    )
+  }
+})

+ 13 - 6
src/views/accompany/music-list.tsx

@@ -17,7 +17,7 @@ import {
   showLoadingToast
 } from 'vant'
 import { defineComponent, reactive, ref, onMounted, nextTick, computed } from 'vue'
-import { useRoute } from 'vue-router'
+import { useRoute, useRouter } from 'vue-router'
 import { getImage } from './images'
 import styles from './index.module.less'
 import OSticky from '@/components/o-sticky'
@@ -35,6 +35,7 @@ export default defineComponent({
   },
   setup() {
     const route = useRoute()
+    const router = useRouter()
     const imgDefault = getImage('icon-music.svg')
     const userInfo = ref<any>({})
     const subjectKey = state.user?.data?.phone || 'accompany-music-list-subject'
@@ -337,11 +338,17 @@ export default defineComponent({
                     title={item.musicSheetName}
                     isLink
                     onClick={() => {
-                      if (item.musicSheetType == 'CONCERT') {
-                        openMutilPart(item)
-                        return
-                      }
-                      openView(item)
+                      // if (item.musicSheetType == 'CONCERT') {
+                      //   openMutilPart(item)
+                      //   return
+                      // }
+                      // openView(item)
+                      router.push({
+                        path: '/musicDetail',
+                        query: {
+                          id: item.id
+                        }
+                      })
                     }}
                   >
                     {{

BIN
src/views/accompany/staff-change/images/activeButtonIcon.png


BIN
src/views/accompany/staff-change/images/first-active.png


BIN
src/views/accompany/staff-change/images/first-default.png


BIN
src/views/accompany/staff-change/images/fixed-active.png


BIN
src/views/accompany/staff-change/images/fixed-default.png


BIN
src/views/accompany/staff-change/images/inactiveButtonIcon.png


BIN
src/views/accompany/staff-change/images/staff-active.png


BIN
src/views/accompany/staff-change/images/staff-default.png


+ 88 - 0
src/views/accompany/staff-change/index.module.less

@@ -0,0 +1,88 @@
+.staffContainer {
+  background: linear-gradient(180deg, #FFECDD 0%, #FFFFFF 100%) no-repeat;
+  background-size: 100% 49px;
+  // text-align: center;
+  padding: 15px 15px 24px;
+
+  .staffTitle {
+    padding-bottom: 25px;
+    font-size: 16px;
+    color: #1a1a1a;
+    line-height: 22px;
+    text-align: center;
+  }
+
+  .staffImg {
+    width: 32px;
+    height: 20px;
+  }
+
+  .name {
+    padding-left: 17px;
+    font-size: 14px;
+    color: #333333;
+  }
+
+  .boxStyle {
+    background: transparent !important;
+    width: 20px;
+    height: 20px;
+    font-size: 20px;
+    border: transparent !important;
+  }
+
+  .active {
+    background: #FFF0E6;
+    border-radius: 8px;
+
+    .name {
+      font-weight: 600;
+    }
+  }
+
+  :global {
+    .van-cell {
+      padding: 9px 16px 9px;
+      margin-bottom: 6px;
+
+      &:hover,
+      &:active,
+      &.active {
+        background: #FFF0E6;
+        border-radius: 8px;
+
+        .name {
+          color: var(--van-primary);
+        }
+      }
+
+      &:last-child {
+        margin-bottom: 0;
+      }
+    }
+
+    .van-cell__value {
+      display: flex;
+      justify-content: flex-end;
+    }
+
+    .van-checkbox {
+      overflow: inherit;
+      height: 18px;
+      display: flex;
+      align-items: center;
+      justify-content: flex-end;
+    }
+
+    .van-checkbox__icon {
+      height: 15px;
+      line-height: 15px;
+      display: inline-block;
+      vertical-align: middle;
+    }
+
+    .van-checkbox__label {
+      line-height: 15px;
+    }
+  }
+}

+ 125 - 0
src/views/accompany/staff-change/index.tsx

@@ -0,0 +1,125 @@
+import { defineComponent, toRefs } from 'vue'
+import styles from './index.module.less'
+import { Cell, CellGroup, Radio, RadioGroup, Image, Icon } from 'vant'
+import inactiveButtonIcon from './images/inactiveButtonIcon.png'
+import activeButtonIcon from './images/activeButtonIcon.png'
+import staffDetafult from './images/staff-default.png'
+import staffActive from './images/staff-active.png'
+import fixedDefault from './images/fixed-default.png'
+import fixedActive from './images/fixed-active.png'
+import firstDefault from './images/first-default.png'
+import firstActive from './images/first-active.png'
+
+export default defineComponent({
+  name: 'staff-change',
+  props: {
+    radio: {
+      type: String,
+      default: 'staff'
+    }
+  },
+  emits: ['update:radio', 'close'],
+  setup(props, { emit }) {
+    const { radio } = toRefs(props)
+
+    const onChangeStaff = (type: string) => {
+      //
+      radio.value = type
+      emit('update:radio', type)
+      emit('close')
+    }
+    return () => (
+      <div class={styles.staffContainer}>
+        <div class={styles.staffTitle}>转换曲谱</div>
+        <RadioGroup v-model={radio.value}>
+          <CellGroup border={false}>
+            <Cell
+              center
+              border={false}
+              class={radio.value === 'staff' ? styles.active : ''}
+              onClick={() => onChangeStaff('staff')}
+            >
+              {{
+                icon: () => (
+                  <Image
+                    src={radio.value === 'staff' ? staffActive : staffDetafult}
+                    class={styles.staffImg}
+                  />
+                ),
+                title: () => <span class={styles.name}>五线谱</span>,
+                value: () => (
+                  <Radio name="staff">
+                    {{
+                      icon: (props: any) => (
+                        <Icon
+                          class={styles.boxStyle}
+                          name={props.checked ? activeButtonIcon : inactiveButtonIcon}
+                        />
+                      )
+                    }}
+                  </Radio>
+                )
+              }}
+            </Cell>
+            <Cell
+              center
+              border={false}
+              class={radio.value === 'first' ? styles.active : ''}
+              onClick={() => onChangeStaff('first')}
+            >
+              {{
+                icon: () => (
+                  <Image
+                    src={radio.value === 'first' ? firstActive : firstDefault}
+                    class={styles.staffImg}
+                  />
+                ),
+                title: () => <span class={styles.name}>简谱-首调</span>,
+                value: () => (
+                  <Radio name="first">
+                    {{
+                      icon: (props: any) => (
+                        <Icon
+                          class={styles.boxStyle}
+                          name={props.checked ? activeButtonIcon : inactiveButtonIcon}
+                        />
+                      )
+                    }}
+                  </Radio>
+                )
+              }}
+            </Cell>
+            <Cell
+              center
+              border={false}
+              class={radio.value === 'fixed' ? styles.active : ''}
+              onClick={() => onChangeStaff('fixed')}
+            >
+              {{
+                icon: () => (
+                  <Image
+                    src={radio.value === 'fixed' ? fixedActive : fixedDefault}
+                    class={styles.staffImg}
+                  />
+                ),
+                title: () => <span class={styles.name}>简谱-固定调</span>,
+                value: () => (
+                  <Radio name="fixed">
+                    {{
+                      icon: (props: any) => (
+                        <Icon
+                          class={styles.boxStyle}
+                          name={props.checked ? activeButtonIcon : inactiveButtonIcon}
+                        />
+                      )
+                    }}
+                  </Radio>
+                )
+              }}
+            </Cell>
+          </CellGroup>
+        </RadioGroup>
+      </div>
+    )
+  }
+})

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است