Browse Source

Merge branch 'dev' of http://git.dayaedu.com/huangqiyong/classroom into staging

黄琪勇 8 months ago
parent
commit
c77e6d1cf7
96 changed files with 3393 additions and 96 deletions
  1. 1 0
      .env
  2. 5 1
      .env.devProd
  3. 5 1
      .env.development
  4. 5 1
      .env.production
  5. 5 1
      .env.staging
  6. 1 0
      dist/css/201.4c3c76b1.css
  7. 0 0
      dist/css/658.8937cb61.css
  8. 0 0
      dist/css/679.4f636f8d.css
  9. 0 0
      dist/css/69.c76c3af5.css
  10. 0 0
      dist/css/727.976624a6.css
  11. 0 0
      dist/css/849.a7abbf54.css
  12. 0 0
      dist/css/app.36097b9e.css
  13. 0 0
      dist/css/app.e4023486.css
  14. BIN
      dist/img/btn-submit.74928013.png
  15. BIN
      dist/img/icon_default.20dca3c0.png
  16. BIN
      dist/img/mac3_2.a8dd00d7.png
  17. BIN
      dist/img/mac5_2.5aa757d5.png
  18. BIN
      dist/img/win8_2.6e5ea084.png
  19. 4 0
      dist/index.html
  20. 0 0
      dist/js/201.61c8652a.js
  21. 0 0
      dist/js/278.8baaea51.js
  22. 0 0
      dist/js/333.bebff96c.js
  23. 0 0
      dist/js/530.a9aeccbb.js
  24. 0 0
      dist/js/596.2eb4fd29.js
  25. 0 0
      dist/js/627.863acaac.js
  26. 0 0
      dist/js/654.fddf6f08.js
  27. 0 0
      dist/js/658.5b56e923.js
  28. 0 0
      dist/js/679.aea79778.js
  29. 0 0
      dist/js/727.b01a84a1.js
  30. 0 0
      dist/js/849.6b8307ea.js
  31. 0 0
      dist/js/app.aea40cbb.js
  32. 0 0
      dist/js/chunk-vendors.4c98f5fd.js
  33. 167 0
      public/osmd/index.html
  34. 1 0
      public/osmd/opensheetmusicdisplay.min.js
  35. 7 1
      src/api/ApiInstance.ts
  36. 54 0
      src/api/cloudPractice.api.ts
  37. 53 1
      src/api/cloudTextbooks.api.ts
  38. 27 1
      src/api/user.api.ts
  39. 12 0
      src/assets/normalize.css
  40. 3 0
      src/config/index.ts
  41. 64 0
      src/config/menus.ts
  42. BIN
      src/hooks/useSecureAnth/img/mac3_2.png
  43. BIN
      src/hooks/useSecureAnth/img/mac5_2.png
  44. BIN
      src/hooks/useSecureAnth/img/win8_2.png
  45. 32 6
      src/hooks/useSecureAnth/secureAnth.vue
  46. BIN
      src/img/cloudPractice/btn-submit.png
  47. BIN
      src/img/cloudPractice/header-ring.png
  48. BIN
      src/img/cloudPractice/icon-arrow-down.png
  49. BIN
      src/img/cloudPractice/icon-btn-pause.png
  50. BIN
      src/img/cloudPractice/icon-btn-play.png
  51. BIN
      src/img/cloudPractice/icon-change.png
  52. BIN
      src/img/cloudPractice/icon-left-active.png
  53. BIN
      src/img/cloudPractice/icon-left-default.png
  54. BIN
      src/img/cloudPractice/icon-search.png
  55. BIN
      src/img/cloudPractice/icon-tag.png
  56. BIN
      src/img/cloudPractice/icon-transfer.png
  57. BIN
      src/img/cloudPractice/icon_default.png
  58. BIN
      src/img/cloudPractice/icon_next.png
  59. BIN
      src/img/cloudPractice/icon_pause.png
  60. BIN
      src/img/cloudPractice/icon_play.png
  61. BIN
      src/img/cloudPractice/icon_pre.png
  62. BIN
      src/img/cloudPractice/song-arrow.png
  63. BIN
      src/img/layout/fullscreen.png
  64. BIN
      src/img/layout/fullscreenClose.png
  65. 1 1
      src/libs/auth.ts
  66. 36 0
      src/libs/fullscreen.ts
  67. 228 0
      src/libs/instruments.ts
  68. 28 0
      src/libs/utils.ts
  69. 7 0
      src/router/index.ts
  70. 2 1
      src/router/modules.ts
  71. 10 0
      src/shims-vue.d.ts
  72. 43 19
      src/store/modules/user.ts
  73. 3 0
      src/type.d.ts
  74. 749 0
      src/views/cloudPractice/cloudPractice.tsx
  75. 42 0
      src/views/cloudPractice/cloudPractice.vue
  76. 217 0
      src/views/cloudPractice/component/play-item/index.module.scss
  77. 226 0
      src/views/cloudPractice/component/play-item/index.tsx
  78. 48 0
      src/views/cloudPractice/component/play-loading/index.module.scss
  79. 16 0
      src/views/cloudPractice/component/play-loading/index.tsx
  80. 495 0
      src/views/cloudPractice/index.module.scss
  81. 2 0
      src/views/cloudPractice/index.ts
  82. 349 0
      src/views/cloudPractice/instrument.ts
  83. 89 0
      src/views/cloudPractice/speed-tag.ts
  84. 40 0
      src/views/cloudPractice/useData.ts
  85. 10 4
      src/views/cloudTextbooks/chooseDialog.vue
  86. 37 5
      src/views/cloudTextbooks/cloudTextbooks.vue
  87. 110 7
      src/views/cloudTextbooks/useData.ts
  88. 6 2
      src/views/coursewarePlay/components/courseCollapse/courseCollapse.vue
  89. 39 19
      src/views/coursewarePlay/coursewarePlay.vue
  90. 16 5
      src/views/curriculum/curriculumDetail.vue
  91. 13 4
      src/views/curriculum/hooks/useStartClass.ts
  92. 48 9
      src/views/homePage/homePage.vue
  93. 21 3
      src/viewsframe/layout/layout.vue
  94. 9 3
      src/viewsframe/login/login.vue
  95. 6 0
      vue.config.js
  96. 1 1
      yarn.lock

+ 1 - 0
.env

@@ -4,4 +4,5 @@ VUE_APP_TITLE="乐教通"
 #安全证书
 VUE_APP_MAC_GYM_SECUREANTH="https://oss.dayaedu.com/https-ssl/gym/安全证书.p12"  #密码  dayaedu.com
 VUE_APP_MAC_GYT_SECUREANTH="https://oss.dayaedu.com/https-ssl/gyt/安全证书.p12"  #密码  lexiaoya.cn
+VUE_APP_MAC_KLX_SECUREANTH="https://oss.dayaedu.com/https-ssl/klx/安全证书.p12"  #密码  colexiu.com
 VUE_APP_WIN_SECUREANTH="https://oss.dayaedu.com/https-ssl/安全证书.pfx"

+ 5 - 1
.env.devProd

@@ -4,6 +4,7 @@ NODE_ENV = production
 VUE_APP_URL = "https://dev.resource.colexiu.com/cbs-app"
 VUE_APP_URL_GYM = "https://dev.gym.lexiaoya.cn"
 VUE_APP_URL_GYT = "https://dev.lexiaoya.cn"
+VUE_APP_URL_KLX = "https://dev.colexiu.com"
 
 # 标注画板地址
 VUE_APP_WHITEBOARD = "https://test.lexiaoya.cn/whiteboard-noCollab"
@@ -12,4 +13,7 @@ VUE_APP_WHITEBOARD = "https://test.lexiaoya.cn/whiteboard-noCollab"
 VUE_APP_TEACH_GYT = "https://test.lexiaoya.cn/orchestra-music-score/"
 
 #管乐迷 云教练
-VUE_APP_TEACH_GYM ="https://test.gym.lexiaoya.cn/accompany-web/"
+VUE_APP_TEACH_GYM ="https://test.gym.lexiaoya.cn/accompany-teacher/"
+
+#酷乐秀 云教练
+VUE_APP_TEACH_KLX ="https://test.colexiu.com/accompany/"

+ 5 - 1
.env.development

@@ -3,6 +3,7 @@
 VUE_APP_URL = "https://dev.resource.colexiu.com/cbs-app"
 VUE_APP_URL_GYM = "/gym"
 VUE_APP_URL_GYT = "/gyt"
+VUE_APP_URL_KLX = "/klx"
 
 # 标注画板地址
 VUE_APP_WHITEBOARD = "https://test.lexiaoya.cn/whiteboard-noCollab"
@@ -11,4 +12,7 @@ VUE_APP_WHITEBOARD = "https://test.lexiaoya.cn/whiteboard-noCollab"
 VUE_APP_TEACH_GYT = "https://test.lexiaoya.cn/orchestra-music-score/"
 
 #管乐迷 云教练
-VUE_APP_TEACH_GYM ="https://test.gym.lexiaoya.cn/accompany-web/"
+VUE_APP_TEACH_GYM ="https://test.gym.lexiaoya.cn/accompany-teacher/"
+
+#酷乐秀 云教练
+VUE_APP_TEACH_KLX ="https://test.colexiu.com/accompany/"

+ 5 - 1
.env.production

@@ -3,6 +3,7 @@
 VUE_APP_URL = "https://mec.colexiu.com/cbs-app"
 VUE_APP_URL_GYM = "https://gym.lexiaoya.cn"
 VUE_APP_URL_GYT = "https://online.lexiaoya.cn"
+VUE_APP_URL_KLX = "https://online.colexiu.com"
 
 # 标注画板地址
 VUE_APP_WHITEBOARD = "https://online.lexiaoya.cn/whiteboard-noCollab"
@@ -11,4 +12,7 @@ VUE_APP_WHITEBOARD = "https://online.lexiaoya.cn/whiteboard-noCollab"
 VUE_APP_TEACH_GYT = "https://online.lexiaoya.cn/orchestra-music-score/"
 
 #管乐迷 云教练
-VUE_APP_TEACH_GYM ="https://gym.lexiaoya.cn/accompany-web/"
+VUE_APP_TEACH_GYM ="https://gym.lexiaoya.cn/accompany-teacher/"
+
+#酷乐秀 云教练
+VUE_APP_TEACH_KLX ="https://online.colexiu.com/accompany/"

+ 5 - 1
.env.staging

@@ -4,6 +4,7 @@ NODE_ENV = production
 VUE_APP_URL = "https://test.resource.colexiu.com/cbs-app"
 VUE_APP_URL_GYM = "https://test.gym.lexiaoya.cn"
 VUE_APP_URL_GYT = "https://test.lexiaoya.cn"
+VUE_APP_URL_KLX = "https://test.colexiu.com"
 
 # 标注画板地址
 VUE_APP_WHITEBOARD = "https://test.lexiaoya.cn/whiteboard-noCollab"
@@ -12,4 +13,7 @@ VUE_APP_WHITEBOARD = "https://test.lexiaoya.cn/whiteboard-noCollab"
 VUE_APP_TEACH_GYT = "https://test.lexiaoya.cn/orchestra-music-score/"
 
 #管乐迷 云教练
-VUE_APP_TEACH_GYM ="https://test.gym.lexiaoya.cn/accompany-web/"
+VUE_APP_TEACH_GYM ="https://test.gym.lexiaoya.cn/accompany-teacher/"
+
+#酷乐秀 云教练
+VUE_APP_TEACH_KLX ="https://test.colexiu.com/accompany/"

File diff suppressed because it is too large
+ 1 - 0
dist/css/201.4c3c76b1.css


File diff suppressed because it is too large
+ 0 - 0
dist/css/658.8937cb61.css


File diff suppressed because it is too large
+ 0 - 0
dist/css/679.4f636f8d.css


File diff suppressed because it is too large
+ 0 - 0
dist/css/69.c76c3af5.css


File diff suppressed because it is too large
+ 0 - 0
dist/css/727.976624a6.css


File diff suppressed because it is too large
+ 0 - 0
dist/css/849.a7abbf54.css


File diff suppressed because it is too large
+ 0 - 0
dist/css/app.36097b9e.css


File diff suppressed because it is too large
+ 0 - 0
dist/css/app.e4023486.css


BIN
dist/img/btn-submit.74928013.png


BIN
dist/img/icon_default.20dca3c0.png


BIN
dist/img/mac3_2.a8dd00d7.png


BIN
dist/img/mac5_2.5aa757d5.png


BIN
dist/img/win8_2.6e5ea084.png


+ 4 - 0
dist/index.html

@@ -1,6 +1,10 @@
 <!doctype html><html lang=""><head><meta charset="utf-8"/><meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="renderer" content="webkit"/><meta name="force-rendering" content="webkit"/><link rel="icon" href="/favicon.ico"/><title>乐教通</title><script>if (!!window.ActiveXObject || "ActiveXObject" in window) {
             window.location.href = "./ieIncompatible/index.html"
+<<<<<<< HEAD
          }</script><script defer="defer" src="/js/chunk-vendors.d6ffa1a3.js"></script><script defer="defer" src="/js/app.841a8afd.js"></script><link href="/css/app.36097b9e.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but classroom doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"><style>.firstLoading {
+=======
+         }</script><script defer="defer" src="/js/chunk-vendors.4c98f5fd.js"></script><script defer="defer" src="/js/app.aea40cbb.js"></script><link href="/css/app.e4023486.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but classroom doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"><style>.firstLoading {
+>>>>>>> 5ac4856f9f4c974f7567ed64b0d52d30734a29c9
                position: fixed;
                left: 50%;
                top: 50%;

File diff suppressed because it is too large
+ 0 - 0
dist/js/201.61c8652a.js


File diff suppressed because it is too large
+ 0 - 0
dist/js/278.8baaea51.js


File diff suppressed because it is too large
+ 0 - 0
dist/js/333.bebff96c.js


File diff suppressed because it is too large
+ 0 - 0
dist/js/530.a9aeccbb.js


File diff suppressed because it is too large
+ 0 - 0
dist/js/596.2eb4fd29.js


+ 0 - 0
dist/js/627.5a6c1436.js → dist/js/627.863acaac.js


File diff suppressed because it is too large
+ 0 - 0
dist/js/654.fddf6f08.js


File diff suppressed because it is too large
+ 0 - 0
dist/js/658.5b56e923.js


File diff suppressed because it is too large
+ 0 - 0
dist/js/679.aea79778.js


File diff suppressed because it is too large
+ 0 - 0
dist/js/727.b01a84a1.js


File diff suppressed because it is too large
+ 0 - 0
dist/js/849.6b8307ea.js


File diff suppressed because it is too large
+ 0 - 0
dist/js/app.aea40cbb.js


File diff suppressed because it is too large
+ 0 - 0
dist/js/chunk-vendors.4c98f5fd.js


+ 167 - 0
public/osmd/index.html

@@ -0,0 +1,167 @@
+<!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,
+      drawTitle: false,
+      drawPartNames: false,
+      drawLyricist: false,
+      followCursor: false,
+      autoResize: true,
+      drawSubtitle: false,
+      drawTitle: false,
+      autoBeam: true,
+      drawMetronomeMarks: false,
+      drawMeasureNumbers: false,
+      drawComposer: false,
+      autoGenerateMutipleRestMeasuresFromRestMeasures: true,
+      autoGenerateMultipleRestMeasuresFromRestMeasures: true,
+    });
+    osmd.EngravingRules.RenderMultipleRestMeasures = true;
+    osmd.EngravingRules.CompactMode = true;
+    osmd.EngravingRules.PageTopMarginNarrow = 6.0; // for compact mode
+    osmd.EngravingRules.PageBottomMargin = 15.0;
+    osmd.EngravingRules.PageLeftMargin = 1.0
+    osmd.EngravingRules.PageRightMargin = 1.0
+
+    // console.log('osmd.EngravingRules', 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, isComberRender) {
+      osmd
+        .load(xmlUrl)
+        .then(
+          function () {
+            // 是否合并显示
+            if (!isComberRender) {
+              for (let i = 0; i < osmd.Sheet.Instruments.length; i++) {
+                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>

File diff suppressed because it is too large
+ 1 - 0
public/osmd/opensheetmusicdisplay.min.js


+ 7 - 1
src/api/ApiInstance.ts

@@ -1,6 +1,6 @@
 import Http from "@/libs/axios"
 import { getToken } from "@/libs/auth"
-import { URL_API, URL_API_GYM, URL_API_GYT } from "@/config"
+import { URL_API, URL_API_GYM, URL_API_GYT, URL_API_KLX } from "@/config"
 
 /** axios实例 */
 
@@ -23,3 +23,9 @@ export const httpAxios_gyt = new Http(URL_API_GYT, {
    tokenName: "Authorization",
    getTokenFun: getToken
 })
+
+/* 酷乐秀 */
+export const httpAxios_klx = new Http(URL_API_KLX, {
+   tokenName: "Authorization",
+   getTokenFun: getToken
+})

+ 54 - 0
src/api/cloudPractice.api.ts

@@ -0,0 +1,54 @@
+import {
+   httpAxios_gym
+   // httpAxios_gyt
+} from "@/api/ApiInstance"
+
+/** 管乐迷 */
+export const queryTree_gym = () => {
+   return httpAxios_gym.axioseRquest({
+      method: "get",
+      url: "/api-teacher/sysMusicScoreCategories/queryTree"
+   })
+}
+
+export const querySubjectIds_gym = (data?: any) => {
+   return httpAxios_gym.axioseRquest({
+      method: "get",
+      url: "/api-teacher/sysMusicScoreAccompaniment/querySubjectIds",
+      params: data
+   })
+}
+
+export const queryPage2_gym = (data: any) => {
+   return httpAxios_gym.axioseRquest({
+      method: "get",
+      url: "/api-teacher/sysMusicScore/queryPage2",
+      params: data
+   })
+}
+
+/**
+ * 管乐团
+ */
+export const queryTree_gyt = () => {
+   return httpAxios_gym.axioseRquest({
+      method: "post",
+      url: "/api-teacher/musicSheetCategories/page"
+   })
+}
+
+export const querySubjectIds_gyt = (data?: any) => {
+   return httpAxios_gym.axioseRquest({
+      method: "get",
+      url: "/api-teacher/subject/musicList",
+      params: data
+   })
+}
+
+export const queryPage2_gyt = (data: any) => {
+   return httpAxios_gym.axioseRquest({
+      method: "get",
+      url: "/api-teacher/musicSheet/page",
+      params: data
+   })
+}

+ 53 - 1
src/api/cloudTextbooks.api.ts

@@ -1,4 +1,4 @@
-import { httpAxios_gym, httpAxios_gyt } from "@/api/ApiInstance"
+import { httpAxios_gym, httpAxios_gyt, httpAxios_klx } from "@/api/ApiInstance"
 
 /** 管乐迷 */
 
@@ -69,3 +69,55 @@ export const getLessonCoursewareDetail_gyt = (id: string) => {
       url: "/api-teacher/lessonCoursewareDetail/detail/" + id
    })
 }
+
+/**
+ * 酷乐秀
+ */
+
+// 专辑列表
+export const getuyAlbumInfo_klx = () => {
+   return httpAxios_klx.axioseRquest({
+      method: "get",
+      url: "/api-teacher/tenantGroupAlbum/buyAlbumInfo"
+   })
+}
+
+// 乐器列表
+export const queryPageSubject_klx = () => {
+   return httpAxios_klx.axioseRquest({
+      method: "get",
+      url: "/api-tenant/open/subject/queryPage?page=1&rows=9999&queryType=list"
+   })
+}
+
+//获取课程列表
+export const queryLessonCourseware_klx = (type: string, albumId: string, abortController: AbortController) => {
+   return httpAxios_klx.axioseRquest({
+      signal: abortController.signal,
+      method: "post",
+      url: "/api-teacher/tenantAlbumMusic/page",
+      data: {
+         subjectType: "COURSEWARE",
+         albumId: albumId,
+         page: 1,
+         rows: 9999,
+         subjectId: type
+      }
+   })
+}
+
+// 课程详情列表
+export const getLessonCoursewareCourseList_klx = (id: string) => {
+   return httpAxios_klx.axioseRquest({
+      method: "get",
+      url: "/api-teacher/tenantAlbumMusic/getLessonCoursewareCourseList/" + id
+   })
+}
+
+// 课程详情 播放用
+export const getLessonCourseDetail_klx = (id: string) => {
+   return httpAxios_klx.axioseRquest({
+      method: "get",
+      url: "/api-teacher/tenantAlbumMusic/getLessonCourseDetail/" + id
+   })
+}

+ 27 - 1
src/api/user.api.ts

@@ -1,4 +1,4 @@
-import { httpAxios, httpAxiosNoNprogress, httpAxios_gym, httpAxios_gyt } from "@/api/ApiInstance"
+import { httpAxios, httpAxiosNoNprogress, httpAxios_gym, httpAxios_gyt, httpAxios_klx } from "@/api/ApiInstance"
 
 /** 获取二维码 */
 export const createQrcodeApi = (uuid?: string) => {
@@ -78,3 +78,29 @@ export const mutualTLSQuery_gyt = () => {
       url: "/api-teacher/open/mutualTLS/query?isLogin=true" // 后面跟参数来区分是不是登录511
    })
 }
+
+/** 酷乐秀 */
+
+// 获取用户信息
+export const getUserInfo_klx = () => {
+   return httpAxios_klx.axioseRquest({
+      method: "get",
+      url: "/api-teacher/teacher/queryUserInfo"
+   })
+}
+
+// 退出登录
+export const logout_klx = () => {
+   return httpAxios_klx.axioseRquest({
+      method: "get",
+      url: "/api-auth/exit"
+   })
+}
+
+// 安全证书
+export const mutualTLSQuery_klx = () => {
+   return httpAxios_klx.axioseRquest({
+      method: "get",
+      url: "/api-auth/open/mutualTLS/query?isLogin=true" // 后面跟参数来区分是不是登录511
+   })
+}

+ 12 - 0
src/assets/normalize.css

@@ -39,6 +39,18 @@ p {
    margin: 0;
 }
 
+img {
+   user-select: none;
+   -webkit-user-select: none;
+   -moz-user-select: none;
+   -ms-user-select: none;
+   -webkit-touch-callout: none;
+   -webkit-user-drag: none;
+   -moz-user-drag: none;
+   -ms-user-drag: none;
+   user-drag: none;
+}
+
 /**
  * 1. Correct the line height in all browsers.
  * 2. Prevent adjustments of font size after orientation changes in

+ 3 - 0
src/config/index.ts

@@ -1,6 +1,7 @@
 export const URL_API = process.env.VUE_APP_URL as string
 export const URL_API_GYT = process.env.VUE_APP_URL_GYT as string
 export const URL_API_GYM = process.env.VUE_APP_URL_GYM as string
+export const URL_API_KLX = process.env.VUE_APP_URL_KLX as string
 
 // 画板地址
 export const URL_WHITEBOARD = process.env.VUE_APP_WHITEBOARD as string
@@ -8,8 +9,10 @@ export const URL_WHITEBOARD = process.env.VUE_APP_WHITEBOARD as string
 // 安全证书地址
 export const URL_MAC_GYM_SECUREANTH = process.env.VUE_APP_MAC_GYM_SECUREANTH as string
 export const URL_MAC_GYT_SECUREANTH = process.env.VUE_APP_MAC_GYT_SECUREANTH as string
+export const URL_MAC_KLX_SECUREANTH = process.env.VUE_APP_MAC_KLX_SECUREANTH as string
 export const URL_WIN_SECUREANTH = process.env.VUE_APP_WIN_SECUREANTH as string
 
 // 云练习地址
 export const URL_TEACH_GYT = process.env.VUE_APP_TEACH_GYT as string
 export const URL_TEACH_GYM = process.env.VUE_APP_TEACH_GYM as string
+export const URL_TEACH_KLX = process.env.VUE_APP_TEACH_KLX as string

+ 64 - 0
src/config/menus.ts

@@ -41,6 +41,16 @@ export const menus_gyt: menuType[] = [
          routeType: "layout"
       },
       children: []
+   },
+   {
+      path: "/cloudPractice",
+      component: "cloudPractice",
+      title: "云练习",
+      icon: "",
+      meta: {
+         routeType: "layout"
+      },
+      children: []
    }
 ]
 
@@ -85,5 +95,59 @@ export const menus_gym: menuType[] = [
          routeType: "layout"
       },
       children: []
+   },
+   {
+      path: "/cloudPractice",
+      component: "cloudPractice",
+      title: "云练习",
+      icon: "",
+      meta: {
+         routeType: "layout"
+      },
+      children: []
+   }
+]
+
+// 酷乐秀
+export const menus_klx: menuType[] = [
+   {
+      path: "/homePage",
+      component: "homePage",
+      title: "首页",
+      icon: "",
+      meta: {
+         routeType: "layout"
+      },
+      children: []
+   },
+   {
+      path: "/cloudTextbooks",
+      component: "cloudTextbooks",
+      title: "云课堂",
+      icon: "",
+      meta: {
+         routeType: "layout"
+      },
+      children: []
+   },
+   {
+      path: "/coursewarePlay/:id",
+      component: "coursewarePlay",
+      title: "播放器",
+      icon: "",
+      meta: {
+         routeType: "singlepage"
+      },
+      children: []
+   },
+   {
+      path: "/cloudPractice",
+      component: "cloudPractice",
+      title: "云练习",
+      icon: "",
+      meta: {
+         routeType: "layout"
+      },
+      children: []
    }
 ]

BIN
src/hooks/useSecureAnth/img/mac3_2.png


BIN
src/hooks/useSecureAnth/img/mac5_2.png


BIN
src/hooks/useSecureAnth/img/win8_2.png


+ 32 - 6
src/hooks/useSecureAnth/secureAnth.vue

@@ -36,13 +36,13 @@
                   <div class="stepNum">03</div>
                   <div class="titleCon">
                      输入证书密码:
-                     <span class="colorTwo">{{ userType === "GYM" ? "dayaedu.com" : "lexiaoya.cn" }}</span>
+                     <span class="colorTwo">{{ passwordObj[userType!] }}</span>
                      ,点击
                      <span class="colorOne">【好】</span>
                   </div>
                </div>
                <div class="contentBox">
-                  <img class="mac3Img" :src="userType === 'GYM' ? require('./img/mac3_1.png') : require('./img/mac3.png')" />
+                  <img class="mac3Img" :src="mac3ImgObj[userType!] " />
                </div>
             </div>
             <div class="stepCon" v-if="stepNum === 3">
@@ -69,7 +69,7 @@
                   </div>
                </div>
                <div class="contentBox">
-                  <img class="mac5Img" :src="userType === 'GYM' ? require('./img/mac5_1.png') : require('./img/mac5.png')" />
+                  <img class="mac5Img" :src="mac5ImgObj[userType!]" />
                </div>
             </div>
             <div class="stepCon" v-if="stepNum === 5">
@@ -186,7 +186,7 @@
                   <div class="titleCon">在【选择证书】弹窗中点击【确定】按钮,证书安装完成,开始使用乐教通吧!</div>
                </div>
                <div class="contentBox">
-                  <img class="win8Img" :src="userType === 'GYM' ? require('./img/win8_1.png') : require('./img/win8.png')" />
+                  <img class="win8Img" :src="win8ImgObj[userType!]" />
                </div>
             </div>
          </template>
@@ -201,7 +201,7 @@
 <script setup lang="ts">
 import { ref } from "vue"
 import { ElScrollbar } from "element-plus"
-import { URL_MAC_GYM_SECUREANTH, URL_MAC_GYT_SECUREANTH, URL_WIN_SECUREANTH } from "@/config"
+import { URL_MAC_GYM_SECUREANTH, URL_MAC_GYT_SECUREANTH, URL_WIN_SECUREANTH, URL_MAC_KLX_SECUREANTH } from "@/config"
 import { getUserType } from "@/libs/auth"
 
 // 因为没有登录页面没有获取用户信息也会弹窗,所以这里取cookie里面的 userType
@@ -221,7 +221,12 @@ const elScrollbarDom = ref<InstanceType<typeof ElScrollbar>>()
 // 下载证书
 function handleDownload() {
    if (isMac) {
-      window.open(userType === "GYM" ? URL_MAC_GYM_SECUREANTH : URL_MAC_GYT_SECUREANTH)
+      const SECUREANTHObj = {
+         GYT: URL_MAC_GYT_SECUREANTH,
+         GYM: URL_MAC_GYM_SECUREANTH,
+         KLX: URL_MAC_KLX_SECUREANTH
+      }
+      window.open(SECUREANTHObj[userType!])
    } else {
       window.open(URL_WIN_SECUREANTH)
    }
@@ -239,6 +244,27 @@ function handleStep(num: -1 | 1) {
    }
    elScrollbarDom.value?.setScrollTop(0)
 }
+
+const passwordObj = {
+   GYT: "lexiaoya.cn",
+   GYM: "dayaedu.com",
+   KLX: "colexiu.com"
+}
+const mac3ImgObj = {
+   GYT: require("./img/mac3.png"),
+   GYM: require("./img/mac3_1.png"),
+   KLX: require("./img/mac3_2.png")
+}
+const mac5ImgObj = {
+   GYT: require("./img/mac5.png"),
+   GYM: require("./img/mac5_1.png"),
+   KLX: require("./img/mac5_2.png")
+}
+const win8ImgObj = {
+   GYT: require("./img/win8.png"),
+   GYM: require("./img/win8_1.png"),
+   KLX: require("./img/win8_2.png")
+}
 </script>
 
 <style lang="scss" scoped>

BIN
src/img/cloudPractice/btn-submit.png


BIN
src/img/cloudPractice/header-ring.png


BIN
src/img/cloudPractice/icon-arrow-down.png


BIN
src/img/cloudPractice/icon-btn-pause.png


BIN
src/img/cloudPractice/icon-btn-play.png


BIN
src/img/cloudPractice/icon-change.png


BIN
src/img/cloudPractice/icon-left-active.png


BIN
src/img/cloudPractice/icon-left-default.png


BIN
src/img/cloudPractice/icon-search.png


BIN
src/img/cloudPractice/icon-tag.png


BIN
src/img/cloudPractice/icon-transfer.png


BIN
src/img/cloudPractice/icon_default.png


BIN
src/img/cloudPractice/icon_next.png


BIN
src/img/cloudPractice/icon_pause.png


BIN
src/img/cloudPractice/icon_play.png


BIN
src/img/cloudPractice/icon_pre.png


BIN
src/img/cloudPractice/song-arrow.png


BIN
src/img/layout/fullscreen.png


BIN
src/img/layout/fullscreenClose.png


+ 1 - 1
src/libs/auth.ts

@@ -34,7 +34,7 @@ export function removeToken() {
 const userType = "userType"
 
 export function getUserType() {
-   return Cookies.get(userType)
+   return Cookies.get(userType) as rolesType | undefined
 }
 
 export function setUserType(type: string) {

+ 36 - 0
src/libs/fullscreen.ts

@@ -0,0 +1,36 @@
+import screenfull from "screenfull"
+import { ref } from "vue"
+import { ElMessage } from "element-plus"
+
+export const isFullscreen = ref(false)
+/**
+ * 全屏
+ * @isScreenfull 为true的时候,是全屏的时候 不关闭
+ * @isErr 为false 不提示
+ */
+export const handleFullscreen = (isScreenfull = false, isErr = true) => {
+   if (!screenfull.isEnabled) {
+      isErr &&
+         ElMessage({
+            message: "当前浏览器不支持全屏!",
+            type: "warning"
+         })
+      return
+   }
+   if (isFullscreen.value && isScreenfull) {
+      return
+   }
+   screenfull.toggle()
+}
+
+screenfull.on("change", () => {
+   isFullscreen.value = screenfull.isFullscreen
+})
+
+// 禁用f11
+document.addEventListener("keydown", function (event) {
+   if (event.key === "F11") {
+      event.preventDefault()
+      handleFullscreen()
+   }
+})

+ 228 - 0
src/libs/instruments.ts

@@ -0,0 +1,228 @@
+const instruments: any = {
+   "Acoustic Grand Piano": "大钢琴",
+   "Bright Acoustic Piano": "明亮的钢琴",
+   "Electric Grand Piano": "电钢琴",
+   "Rhodes Piano": "柔和的电钢琴",
+   "Chorused Piano": "加合唱效果的电钢琴",
+   Harpsichord: "羽管键琴",
+   Clavichord: "科拉维科特琴",
+   Celesta: "钢片琴",
+   Glockenspiel: "钢片琴",
+   "Music box": "八音盒",
+   Vibraphone: "颤音琴",
+   Marimba: "马林巴",
+   Xylophone: "木琴",
+   "Tubular Bells": "管钟",
+   Dulcimer: "大扬琴",
+   "Hammond Organ": "击杆风琴",
+   "Percussive Organ": "打击式风琴",
+   "Rock Organ": "摇滚风琴",
+   "Church Organ": "教堂风琴",
+   "Reed Organ": "簧管风琴",
+   Accordian: "手风琴",
+   Harmonica: "口琴",
+   "Tango Accordian": "探戈手风琴",
+   "Acoustic Guitar": "钢弦吉他",
+   "Electric Guitar": "闷音电吉他",
+   "Overdriven Guitar": "加驱动效果的电吉他",
+   "Distortion Guitar": "加失真效果的电吉他",
+   "Guitar Harmonics": "吉他和音",
+   "Acoustic Bass": "大贝司",
+   "Electric Bass": "电贝司",
+   "Fretless Bass": "无品贝司",
+   "Slap Bass": "掌击",
+   "Synth Bass": "电子合成",
+   Violin: "小提琴",
+   Viola: "中提琴",
+   Cello: "大提琴",
+   Contrabass: "低音大提琴",
+   "Tremolo Strings": "弦乐群颤音音色",
+   "Pizzicato Strings": "弦乐群拨弦音色",
+   "Orchestral Harp": "竖琴",
+   Timpani: "定音鼓",
+   "String Ensemble": "弦乐合奏音色",
+   "Synth Strings": "合成弦乐合奏音色",
+   "Choir Aahs": "人声合唱",
+   "Voice Oohs": "人声",
+   "Synth Voice": "合成人声",
+   "Orchestra Hit": "管弦乐敲击齐奏",
+   Trumpet: "小号",
+   Trombone: "长号",
+   Tuba: "大号",
+   "Muted Trumpet": "加弱音器小号",
+   "French Horn": "法国号",
+   "Brass Section": "铜管组",
+   "Synth Brass": "合成铜管音色",
+   "Soprano Sax": "高音萨克斯管",
+   "Alto Sax": "中音萨克斯管",
+   "Tenor Sax": "次中音萨克斯管",
+   "Baritone Sax": "低音萨克斯管",
+   Oboe: "双簧管",
+   "English Horn": "英国管",
+   Bassoon: "巴松",
+   "Soprano Saxophone": "高音萨克斯管",
+   "Alto Saxophone": "中音萨克斯管",
+   "Tenor Saxophone": "次中音萨克斯管",
+   "Baritone Saxophone": "低音萨克斯管",
+   Piccolo: "短笛",
+   Flute: "长笛",
+   Recorder: "竖笛",
+   "Soprano Recorder": "高音竖笛",
+   "Pan Flute": "排箫",
+   "Bottle Blow": "瓶木管",
+   Whistle: "口哨声",
+   Ocarina: "陶笛",
+   Lead: "合成主音",
+   "Lead lead": "合成主音",
+   "Pad age": "合成音色",
+   Pad: "合成音色",
+   FX: "合成效果  科幻",
+   Sitar: "西塔尔",
+   Banjo: "班卓琴",
+   Shamisen: "三昧线",
+   Koto: "十三弦筝",
+   Kalimba: "卡林巴",
+   Bagpipe: "风笛",
+   Fiddle: "民族提琴",
+   Shanai: "山奈",
+   "Tinkle Bell": "叮当铃",
+   Agogos: "阿戈戈铃",
+   "Steel Drums": "钢鼓",
+   "Taiko Drum": "太鼓",
+   "Melodic Toms": "嗵嗵鼓",
+   "Synth Drums": "合成鼓",
+   "Reverse Cymbals": "反向镲",
+   "Agogo Bells": "阿戈戈铃",
+   "Taiko Drums": "太鼓",
+   Bongos: "邦戈鼓",
+   "Bongo Bell": "邦戈铃",
+   Congas: "康加鼓",
+   Guiro: "刮壶",
+   "Guitar Fret Noise": "吉他换把杂音",
+   "Breath Noise": "呼吸声",
+   Seashore: "海浪声",
+   "Bird Tweet": "鸟鸣",
+   "Telephone Ring": "电话铃",
+   Helicopter: "直升机",
+   Applause: "鼓掌声",
+   Gunshot: "枪声",
+   "Acoustic Bass Drum": "大鼓",
+   "Bass Drum": "大鼓",
+   "Side Drum": "小鼓鼓边",
+   "Acoustic Snare": "小鼓",
+   "Hand Claps": "拍手",
+   "Electric Snare": "小鼓",
+   "Low Floor Tom": "低音嗵鼓",
+   "Closed Hi-Hat": "闭合踩镲",
+   "High Floor Tom": "高音落地嗵鼓",
+   "Pedal Hi-Hat": "脚踏踩镲",
+   "Low Tom": "低音嗵鼓",
+   "Open Hi-Hat": "开音踩镲",
+   "Low-Mid Tom": "中低音嗵鼓",
+   "Hi Mid Tom": "高音鼓",
+   "Crash Cymbals": "对镲",
+   "High Tom": "高音嗵鼓",
+   "Ride Cymbals": "叮叮镲",
+   "Chinese Cymbals": "中国镲",
+   "Ride Bell": "圆铃",
+   Tambourine: "铃鼓",
+   "Splash Cymbal": "溅音镲",
+   Cowbell: "牛铃",
+   "Crash Cymbal": "强音钹",
+   "Vibra-Slap": "颤音器",
+   "Ride Cymbal": "打点钹",
+   "Hi Bongo": "高音邦戈鼓",
+   "Low Bongo": "低音邦戈鼓",
+   "Mute Hi Conga": "弱音高音康加鼓",
+   "Open Hi Conga": "强音高音康加鼓",
+   "Low Conga": "低音康加鼓",
+   "High Timbale": "高音天巴鼓",
+   "Low Timbale": "低音天巴鼓",
+   "High Agogo": "高音阿戈戈铃",
+   "Low Agogo": "低音阿戈戈铃",
+   Cabasa: "卡巴萨",
+   Maracas: "沙锤",
+   "Short Whistle": "短口哨",
+   "Long Whistle": "长口哨",
+   "Short Guiro": "短刮壶",
+   "Long Guiro": "长刮壶",
+   Claves: "响棒",
+   "Hi Wood Block": "高音木鱼",
+   "Low Wood Block": "低音木鱼",
+   "Mute Triangle": "弱音三角铁",
+   "Open Triangle": "强音三角铁",
+   "Drum Set": "架子鼓",
+   "Hulusi flute": "葫芦丝",
+   Melodica: "口风琴",
+   "Snare Drum": "小军鼓",
+   "Horn in F": "圆号",
+   Triangle: "三角铁",
+   Vibrato: "颤音琴",
+   "Suspend Cymbals": "吊镲",
+   "Suspended Cymbals": "吊镲",
+   "Tom-Toms": "嗵嗵鼓",
+   Bell: "铃铛",
+   Bells: "铃铛",
+   "Alto Clarinet": "中音单簧管",
+   "Bass Clarinet": "低音单簧管",
+   Clarinet: "单簧管",
+   Cornet: "短号",
+   Euphonium: "上低音号",
+   "crash cymbals": "对镲",
+   Castanets: "响板",
+   Shaker: "沙锤",
+   "Mark tree": "音树",
+   Chimes: "管钟",
+   "Mark Tree": "音树",
+   "Tom-toms": "嗵嗵鼓",
+   "Hi-Hat": "踩镲",
+   "Sleigh Bells": "雪橇铃",
+   Flexatone: "弹音器",
+   "Brake drum": "闸鼓",
+   Gong: "锣",
+   "concert tom": "音乐会嗵嗵鼓",
+   "brake drum": "车轮鼓",
+   "finger cymbal": "指钹",
+   "ride cymbal": "叮叮镲",
+   "Concert Toms": "音乐会嗵嗵鼓",
+   Vibraslap: "弹音器",
+   "Wood Blocks": "木鱼",
+   "Temple Blocks": "木鱼",
+   "Wood Block": "木鱼",
+   "Field Drum": "军鼓",
+   "Quad-Toms": "筒鼓",
+   Quads: "筒鼓",
+   "Drums set": "架子鼓",
+   "High Bongo": "邦戈",
+   Timbales: "天巴鼓",
+   "rain stick": "雨棒",
+   "String Bass": "弦乐低音",
+   "Floor Tom": "侧嗵鼓",
+   "Brake Drum": "闸鼓",
+   "Tam-tam": "大锣",
+   Cymbal: "镲",
+   Cymbals: "镲"
+}
+
+/**
+ * 获取乐器名称
+ * @param instrumentName 乐器code
+ * @returns
+ */
+export const getInstrumentName = (instrumentName: string) => {
+   const _instrumentName = instrumentName.replace(/ /g, " ").toLocaleLowerCase()
+   const _instrument: any = Object.keys(instruments)
+   for (let i = 0; i < _instrument.length; i++) {
+      const _name = _instrument[i].replace(/ /g, " ").toLocaleLowerCase()
+      if (_name === _instrumentName) {
+         return instruments[_instrument[i]] || ""
+      }
+   }
+   for (let i = 0; i < _instrument.length; i++) {
+      const _name = _instrument[i].replace(/ /g, " ").toLocaleLowerCase()
+      if (_instrumentName.includes(_name)) {
+         return instruments[_instrument[i]] || ""
+      }
+   }
+   return ""
+}

+ 28 - 0
src/libs/utils.ts

@@ -80,3 +80,31 @@ export const generateAsyncMenus = (menus: menuType[]) => {
       })
    }
 }
+
+// 秒转分
+export const getSecondRPM = (second: number, type?: string) => {
+   if (isNaN(second)) return "00:00"
+   const mm = Math.floor(second / 60)
+      .toString()
+      .padStart(2, "0")
+   const dd = Math.floor(second % 60)
+      .toString()
+      .padStart(2, "0")
+   if (type === "cn") {
+      return mm + "分" + dd + "秒"
+   } else {
+      return mm + ":" + dd
+   }
+}
+
+// 秒转分
+export const getSecond = (second: number) => {
+   if (isNaN(second)) return "0000"
+   const mm = Math.floor(second / 60)
+      .toString()
+      .padStart(2, "0")
+   const dd = Math.floor(second % 60)
+      .toString()
+      .padStart(2, "0")
+   return `${mm}${dd}`
+}

+ 7 - 0
src/router/index.ts

@@ -56,6 +56,13 @@ router.beforeEach((to, from, next) => {
             })
       }
    } else {
+      // 如果有token,并且是登录页 直接返回首页
+      if (hasToken && to.path === "/login") {
+         next({
+            path: "/"
+         })
+         return
+      }
       if (authWhiteList.includes(to.path) || isRegWhite(regWhiteList, to.path)) {
          next()
       } else {

+ 2 - 1
src/router/modules.ts

@@ -2,5 +2,6 @@ export default {
    homePage: () => import("@/views/homePage/homePage.vue"), // 首页
    cloudTextbooks: () => import("@/views/cloudTextbooks"), // 云课堂
    coursewarePlay: () => import("@/views/coursewarePlay"), // 播放器
-   curriculum: () => import("@/views/curriculum") // 课表
+   curriculum: () => import("@/views/curriculum"), // 课表
+   cloudPractice: () => import("@/views/cloudPractice") // 云练习
 }

+ 10 - 0
src/shims-vue.d.ts

@@ -5,6 +5,16 @@ declare module "*.vue" {
    export default component
 }
 
+declare module '*.module.scss' {
+   const classes: { readonly [key: string]: string }
+   export default classes
+}
+
+declare module '*.png' {
+   const classes: { readonly [key: string]: string }
+   export default classes
+}
+
 /** json 文件导出 */
 declare module "*.json" {
    const value: any

+ 43 - 19
src/store/modules/user.ts

@@ -1,11 +1,11 @@
 import { defineStore } from "pinia"
 import { store } from "../index"
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
-import { logout_gym, logout_gyt, getUserInfo_gyt, queryUserInfo_gym } from "@/api/user.api"
+import { logout_gym, logout_gyt, logout_klx, getUserInfo_gyt, queryUserInfo_gym, getUserInfo_klx } from "@/api/user.api"
 import { removeToken, setToken, setUserType, getUserType, removeUserType, setCODE401 } from "@/libs/auth"
 import router, { resetRouter } from "@/router"
 import { httpAjax, httpAjaxLoading } from "@/plugin/httpAjax"
-import { menus_gym, menus_gyt } from "@/config/menus"
+import { menus_gym, menus_gyt, menus_klx } from "@/config/menus"
 
 interface userDataType {
    appKey: string
@@ -17,8 +17,9 @@ interface userType {
       username?: string
       realName?: string
       phone?: string
+      subjectId?: string
    }
-   roles?: "GYM" | "GYT"
+   roles?: rolesType
 }
 
 const useStore = defineStore("user", {
@@ -31,8 +32,13 @@ const useStore = defineStore("user", {
    actions: {
       /** 登录 */
       async login({ data, appKey }: userDataType) {
-         // GYM 和 GYT 区分   获取token的区分
-         setToken(appKey === "GYM" ? data : data.access_token)
+         // GYM,GYT,KLX 区分 获取token的区分
+         const tokenObj = {
+            GYT: data.access_token,
+            GYM: data,
+            KLX: data
+         }
+         setToken(tokenObj[appKey as rolesType])
          setUserType(appKey)
          return Promise.resolve()
       },
@@ -46,23 +52,38 @@ const useStore = defineStore("user", {
                message: "获取用户信息出错!"
             })
          }
-         // GYM 和 GYT 区分 code验证错误码的区分
-         userType === "GYM" || setCODE401(5000)
-         // GYM 和 GYT 区分 获取用户信息的接口区分
-         const userInfoRes = await httpAjaxLoading(userType === "GYM" ? queryUserInfo_gym : getUserInfo_gyt)
+         // GYM,GYT,KLX 区分 code验证错误码的区分 管乐团是5000,其他的是403
+         userType === "GYT" && setCODE401(5000)
+         // GYM,GYT,KLX 区分 获取用户信息的接口区分
+         const userInfoApi = {
+            GYT: getUserInfo_gyt,
+            GYM: queryUserInfo_gym,
+            KLX: getUserInfo_klx
+         }
+         const userInfoRes = await httpAjaxLoading(userInfoApi[userType])
          if (userInfoRes.code !== 200) {
             return Promise.reject(userInfoRes)
          }
-         // GYM 和 GYT 区分 用户信息数据不一样区分
+         // GYM,GYT,KLX 区分 用户信息数据不一样区分
          this.userInfo = handleUserInfo(userType, userInfoRes.data)
-         this.roles = userType as any
-         // GYM 和 GYT 区分  区分菜单然后分配路由
-         return Promise.resolve(userType === "GYM" ? menus_gym : menus_gyt)
+         this.roles = userType
+         // GYM,GYT,KLX 区分  区分菜单然后分配路由
+         const menusObj = {
+            GYT: menus_gyt,
+            GYM: menus_gym,
+            KLX: menus_klx
+         }
+         return Promise.resolve(menusObj[userType])
       },
       /** 退出登录 */
       async loginOut() {
-         // GYM 和 GYT 区分  区分退出登录
-         const logoutRes = await httpAjax(this.roles === "GYM" ? logout_gym : logout_gyt)
+         // GYM,GYT,KLX 区分  区分退出登录
+         const logoutApi = {
+            GYT: logout_gyt,
+            GYM: logout_gym,
+            KLX: logout_klx
+         }
+         const logoutRes = await httpAjax(logoutApi[this.roles!])
          if (logoutRes.code !== 200) {
             return Promise.reject(logoutRes)
          }
@@ -86,23 +107,26 @@ export default () => {
    return useStore(store)
 }
 
-function handleUserInfo(userType: string, userInfo: Record<string, any>) {
-   let avatar, username, realName, phone
-   if (userType === "GYM") {
+function handleUserInfo(userType: rolesType, userInfo: Record<string, any>) {
+   let avatar, username, realName, phone, subjectId
+   if (["GYM", "KLX"].includes(userType)) {
       avatar = userInfo.avatar
       username = userInfo.username
       realName = userInfo.realName
       phone = userInfo.phone
+      subjectId = userInfo.subjectId
    } else {
       avatar = userInfo.avatar
       username = userInfo.nickname
       realName = userInfo.realName
       phone = userInfo.phone
+      subjectId = userInfo.subjectId
    }
    return {
       avatar,
       username,
       realName,
-      phone
+      phone,
+      subjectId
    }
 }

+ 3 - 0
src/type.d.ts

@@ -25,6 +25,9 @@ declare interface menuType {
    }
 }
 
+/** 角色类型 */
+declare type rolesType = "GYM" | "GYT" | "KLX"
+
 /**
  *type tool
  */

+ 749 - 0
src/views/cloudPractice/cloudPractice.tsx

@@ -0,0 +1,749 @@
+import { computed, defineComponent, onMounted, reactive, ref, shallowRef } from "vue"
+import styles from "./index.module.scss"
+import NavContainer from "@/businessComponents/navContainer"
+import { ElEmpty, ElScrollbar } from "element-plus"
+import Dictionary from "@/components/dictionary"
+import MyInput from "@/components/myInput"
+import { NImage, NPopselect, NSpin } from "naive-ui"
+// import PlayLoading from "./component/play-loading"
+import PlayItem from "./component/play-item"
+import icon_default from "../../img/cloudPractice/icon_default.png"
+import iconBtnPause from "../../img/cloudPractice/icon-btn-pause.png"
+import iconBtnPlay from "../../img/cloudPractice/icon-btn-play.png"
+import btnSubmit from "../../img/cloudPractice/btn-submit.png"
+import iconTransfer from "../../img/cloudPractice/icon-transfer.png"
+import { httpAjaxErrMsg } from "@/plugin/httpAjax"
+import { queryPage2_gym, queryPage2_gyt, querySubjectIds_gym, querySubjectIds_gyt, queryTree_gym, queryTree_gyt } from "@/api/cloudPractice.api"
+// import { getToken } from "@/libs/auth"
+// import { URL_TEACH_GYM } from "@/config"
+import axios from "axios"
+import { getInstrumentName } from "@/libs/instruments"
+import { formatXML, getCustomInfo, onlyVisible } from "./instrument"
+import { useFunction } from "./useData"
+import userStore from "@/store/modules/user"
+
+export default defineComponent({
+   name: "cloudPractice",
+   setup() {
+      const userStoreHook = userStore()
+      const { goToCloud } = useFunction()
+      const navs = [
+         {
+            name: "主页",
+            url: "/"
+         },
+         {
+            name: "云练习"
+         }
+      ]
+      const spinRef = ref()
+      const state = reactive({
+         finshed: false,
+         reshing: false,
+         page: 1,
+         rows: 20,
+         iframeSrc: "",
+         listActive: 0, // 当前选中的对象
+         firstTreeId: null as any, // 左侧
+         categoryId: null as any, // 类型
+         categoryName: "" as any, // 类型名称
+         categoryList: [] as any[],
+         levelList: [] as any[], // 级别
+         levelId: null as any,
+         typeList: [] as any[], // 类型
+         typeId: -1 as any,
+         subjectList: [] as any[], // 声部列表
+         subjectId: -1 as any,
+         list: [] as any[],
+         searchStatus: false,
+         queryStr: "", // 搜索条件
+         partList: [] as any[],
+         partNames: [] as any[],
+         selectedPartName: "" as any,
+         selectedPartIndex: 0,
+         partXmlIndex: 0,
+         playState: "pause" as "play" | "pause", // 播放状态
+         showPlayer: false // 是否显示播放器
+      })
+      const partColumns = ref<any>([])
+
+      /** 选中的item */
+      const activeItem = computed(() => {
+         const list = state.list[state.listActive] || {}
+         const mp3 = {
+            GYT: "",
+            GYM: list?.musicSheetType === "CONCERT" ? list?.metronomeUrl : list?.metronomeMp3Url || list?.mp3Url,
+            KLX: ""
+         }
+
+         return {
+            id: list?.id,
+            name: list?.name,
+            background: list?.background,
+            xmlUrl: list?.xmlUrl,
+            musicSheetType: list?.musicSheetType,
+            audioFileUrl: mp3[userStoreHook.roles!]
+         }
+      })
+
+      const songPrevNextStatus = computed(() => {
+         let prev = true,
+            next = true
+         if (state.listActive === 0) {
+            prev = false
+         }
+
+         if (state.listActive >= state.list.length - 1) {
+            next = false
+         }
+
+         return {
+            prev,
+            next
+         }
+      })
+
+      const loading = ref(false)
+      const staffLoading = ref(false)
+      const storeData = shallowRef<any[]>([])
+      const handleSearchList_gym = async () => {
+         loading.value = true
+         await httpAjaxErrMsg(queryTree_gym).then(res => {
+            loading.value = false
+            if (res.code === 200) {
+               storeData.value = res.data || []
+
+               setDefaultData()
+            }
+         })
+      }
+
+      const handleGetSubject_gym = async () => {
+         loading.value = true
+         // , { categoriesId: state.categoryId || state.firstTreeId }
+         await httpAjaxErrMsg(querySubjectIds_gym).then(res => {
+            loading.value = false
+            if (res.code === 200) {
+               const result = res.data || []
+
+               state.subjectList = result.map((item: any) => {
+                  return {
+                     label: item.name,
+                     value: item.id
+                  }
+               })
+
+               state.subjectList.unshift({
+                  label: "全部声部",
+                  value: -1
+               })
+
+               const userSubjectId = userStoreHook.userInfo.subjectId
+               if (userSubjectId) {
+                  const tempSubjectId = userSubjectId.split(",")[0]
+                  state.subjectList.forEach((item: any) => {
+                     // 判断是否存在声部编号
+                     if (item.value === Number(tempSubjectId)) {
+                        state.subjectId = Number(tempSubjectId)
+                     }
+                  })
+               }
+            }
+         })
+      }
+
+      const handleGetList_gym = async () => {
+         loading.value = true
+         const params = {
+            page: state.page,
+            rows: state.rows,
+            subjectId: state.subjectId === -1 ? null : state.subjectId,
+            categoriesId: state.typeId === -1 ? state.levelId : state.typeId,
+            search: state.queryStr
+         }
+         console.log(state.typeId, state.levelId, "level")
+         await httpAjaxErrMsg(queryPage2_gym, params).then(res => {
+            loading.value = false
+            if (res.code === 200) {
+               const result = res.data || []
+
+               if (state.reshing) {
+                  state.list = []
+                  state.reshing = false
+               }
+
+               if (Array.isArray(result.rows)) {
+                  state.list = [...state.list, ...result.rows]
+                  state.finshed = state.page >= result.totalPage
+               } else {
+                  state.finshed = true
+               }
+            }
+         })
+      }
+
+      /** 管乐团数据查询 */
+      const handleSearchList_gyt = async () => {
+         loading.value = true
+         await httpAjaxErrMsg(queryTree_gyt).then(res => {
+            loading.value = false
+            if (res.code === 200) {
+               storeData.value = res.data || []
+
+               setDefaultData()
+            }
+         })
+      }
+
+      const handleGetSubject_gyt = async () => {
+         loading.value = true
+         await httpAjaxErrMsg(querySubjectIds_gyt, {
+            enableFlag: true,
+            page: 1,
+            rows: 100
+         }).then(res => {
+            loading.value = false
+            if (res.code === 200) {
+               const result = res.data || []
+
+               state.subjectList = result.map((item: any) => {
+                  return {
+                     label: item.name,
+                     value: item.id
+                  }
+               })
+
+               state.subjectList.unshift({
+                  label: "全部声部",
+                  value: -1
+               })
+            }
+         })
+      }
+
+      const handleGetList_gyt = async () => {
+         loading.value = true
+         const params = {
+            page: state.page,
+            rows: state.rows,
+            musicSubject: state.subjectId === -1 ? null : state.subjectId,
+            musicSheetCategoriesId: state.typeId === -1 ? state.levelId : state.typeId,
+            keyword: state.queryStr,
+            status: 1
+         }
+         console.log(state.typeId, state.levelId, "level")
+         await httpAjaxErrMsg(queryPage2_gyt, params).then(res => {
+            loading.value = false
+            if (res.code === 200) {
+               const result = res.data || []
+
+               if (state.reshing) {
+                  state.list = []
+                  state.reshing = false
+               }
+
+               if (Array.isArray(result.rows)) {
+                  state.list = [...state.list, ...result.rows]
+                  state.finshed = state.page >= result.totalPage
+               } else {
+                  state.finshed = true
+               }
+            }
+         })
+      }
+
+      /** 条件查询 */
+      const handleAllSearchList = async () => {
+         //  GYM,GYT,KLX 区分   查询搜索条件数据
+         if (userStoreHook.roles === "GYM") {
+            await handleSearchList_gym()
+         } else if (userStoreHook.roles === "GYT") {
+            await handleSearchList_gyt()
+         } else if (userStoreHook.roles === "KLX") {
+            //
+         }
+      }
+
+      const handleAllGetSubject = async () => {
+         //  GYM,GYT,KLX 区分   查询声部数据
+         if (userStoreHook.roles === "GYM") {
+            await handleGetSubject_gym()
+         } else if (userStoreHook.roles === "GYT") {
+            await handleGetSubject_gyt()
+         } else if (userStoreHook.roles === "KLX") {
+            //
+         }
+      }
+
+      const handleAllGetList = async () => {
+         //  GYM,GYT,KLX 区分   查询声部数据·
+         if (userStoreHook.roles === "GYM") {
+            await handleGetList_gym()
+         } else if (userStoreHook.roles === "GYT") {
+            await handleGetList_gyt()
+         } else if (userStoreHook.roles === "KLX") {
+            //
+         }
+      }
+
+      /** 初始化数据 */
+      const setDefaultData = (type?: "first" | "category" | "level" | "type") => {
+         if (storeData.value.length > 0 && !["category", "level", "type"].includes(type as any)) {
+            let result: any = []
+            if (type === "first" && state.firstTreeId) {
+               result = storeData.value.find((item: any) => item.id === state.firstTreeId)?.sysMusicScoreCategoriesList || []
+            } else {
+               state.firstTreeId = storeData.value[0]?.id
+               result = storeData.value[0]?.sysMusicScoreCategoriesList || []
+            }
+            state.categoryList = result.map((item: any) => {
+               return {
+                  label: item.name,
+                  value: item.id,
+                  sysMusicScoreCategoriesList: item.sysMusicScoreCategoriesList || []
+               }
+            })
+         }
+         if (state.categoryList.length > 0 && !["level", "type"].includes(type as any)) {
+            let result: any = []
+            if (type === "category" && state.categoryId) {
+               result = state.categoryList.find((item: any) => item.value === state.categoryId)?.sysMusicScoreCategoriesList || []
+            } else {
+               state.categoryId = state.categoryList[0]?.value
+               state.categoryName = state.categoryList[0]?.label
+               result = state.categoryList[0]?.sysMusicScoreCategoriesList || []
+            }
+            console.log(result, "result", type)
+            state.levelList = result.map((item: any) => {
+               return {
+                  label: item.name,
+                  value: item.id,
+                  sysMusicScoreCategoriesList: item.sysMusicScoreCategoriesList || []
+               }
+            })
+         }
+
+         if (state.levelList.length > 0) {
+            let result: any = []
+            if (type === "level" && state.levelId) {
+               result = state.levelList.find((item: any) => item.value === state.levelId)?.sysMusicScoreCategoriesList
+            } else {
+               state.levelId = state.levelList[0]?.value
+               result = state.levelList[0]?.sysMusicScoreCategoriesList || []
+            }
+
+            state.typeList = result.map((item: any) => {
+               return {
+                  label: item.name,
+                  value: item.id
+               }
+            })
+            state.typeList.unshift({
+               label: "全部",
+               value: -1
+            })
+         }
+      }
+
+      const __init = async () => {
+         await handleAllSearchList()
+         await handleAllGetSubject()
+         await handleAllGetList()
+         await toDetail()
+         renderStaff()
+      }
+
+      __init()
+
+      const handleResh = () => {
+         if (loading.value || state.finshed) return
+         state.page = state.page + 1
+         handleAllGetList()
+      }
+
+      const handleGetList = async () => {
+         state.listActive = 0
+         state.showPlayer = false
+         state.playState = "pause"
+         document.querySelector(".musicList-container")?.scroll(0, 0)
+         state.page = 1
+         state.finshed = false
+         state.reshing = true
+         await handleAllGetList()
+      }
+
+      const toDetail = async () => {
+         const row: any = activeItem.value
+         if (row.musicSheetType === "SINGLE") {
+            loading.value = false
+            return
+         }
+         console.log(row, "row")
+         state.partNames = await getPartNames(row.xmlUrl)
+         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 = state.partNames.findIndex((name: any) => name === item.track)
+            return {
+               text: item.track + (instrumentName ? `(${instrumentName})` : ""),
+               instrumentName: instrumentName,
+               xmlIndex,
+               value: index
+            }
+         })
+         // 初始化数据
+         const defaultShowStaff = partColumns.value[state.selectedPartIndex]
+         console.log(defaultShowStaff, partList)
+         state.selectedPartName = defaultShowStaff?.instrumentName
+         state.partXmlIndex = defaultShowStaff?.xmlIndex
+
+         console.log(partColumns.value, "partColumns partColumns")
+      }
+
+      const getPartNames = async (xmlUrl: string) => {
+         const partNames: string[] = []
+         try {
+            const res: any = await axios.get(xmlUrl)
+            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 musicIframeLoad = async () => {
+         const iframeRef: any = document.getElementById("staffIframeRef")
+         if (iframeRef && iframeRef.contentWindow?.renderXml) {
+            staffLoading.value = true
+            const res = await axios.get(activeItem.value.xmlUrl)
+            const parseXmlInfo = getCustomInfo(res.data)
+            const xml = formatXML(parseXmlInfo.parsedXML)
+            const currentXml = onlyVisible(xml, state.selectedPartIndex)
+            iframeRef.contentWindow.renderXml(currentXml, state.selectedPartIndex)
+         }
+      }
+      const resetRender = async () => {
+         const iframeRef: any = document.getElementById("staffIframeRef")
+         if (iframeRef && iframeRef.contentWindow?.renderXml) {
+            staffLoading.value = true
+            const res = await axios.get(activeItem.value.xmlUrl)
+            const parseXmlInfo = getCustomInfo(res.data)
+            const xml = formatXML(parseXmlInfo.parsedXML)
+            const currentXml = onlyVisible(xml, state.selectedPartIndex)
+            iframeRef.contentWindow.renderXml(currentXml, state.selectedPartIndex)
+         }
+      }
+
+      const renderStaff = async () => {
+         try {
+            // ${location.origin}${location.pathname}
+            state.iframeSrc = `/osmd/index.html`
+         } catch (error) {
+            //
+         }
+      }
+
+      /** 音频控制 */
+      const handleChangeAudio = (type: "play" | "pause" | "pre" | "next") => {
+         if (type === "play") {
+            state.playState = "play"
+         } else if (type === "pause") {
+            state.playState = "pause"
+         } else if (type === "pre") {
+            if (state.list[state.listActive - 1]) {
+               handlePlay(state.list[state.listActive - 1])
+            }
+         } else if (type === "next") {
+            if (state.list[state.listActive + 1]) {
+               handlePlay(state.list[state.listActive + 1])
+            }
+         }
+      }
+
+      /** 播放曲目 */
+      const handlePlay = (item: any) => {
+         const index = state.list.findIndex((_item: any) => _item.id === item.id)
+         if (index > -1) {
+            if (state.listActive === index) {
+               state.playState = state.playState === "play" ? "pause" : "play"
+            } else {
+               state.playState = "play"
+            }
+            state.showPlayer = true
+            state.listActive = index
+         }
+      }
+
+      const showLoading = async (e: any) => {
+         if (e.data?.api === "musicStaffRender") {
+            staffLoading.value = e.data.loading
+         }
+      }
+
+      onMounted(() => {
+         const obv = new IntersectionObserver(entries => {
+            if (entries[0].intersectionRatio > 0) {
+               handleResh()
+            }
+         })
+         obv.observe(spinRef.value)
+
+         window.addEventListener("message", showLoading)
+      })
+      return () => (
+         <NavContainer navs={navs}>
+            <ElScrollbar class="elScrollbar">
+               <div class={styles.cloudPractice}>
+                  <div class={styles.leftContainer}>
+                     <div class={styles.details}>
+                        {storeData.value.length > 0 && (
+                           <ElScrollbar class={styles.leftSection}>
+                              {/* 基 础 云 练 */}
+                              {storeData.value.map((item: any) => (
+                                 <div
+                                    class={[styles.leftSection_item, item.id === state.firstTreeId && styles.leftSection_item__active]}
+                                    onClick={() => {
+                                       state.firstTreeId = item.id
+                                       setDefaultData("first")
+                                       handleGetList()
+                                    }}
+                                 >
+                                    {item.name}
+                                 </div>
+                              ))}
+                           </ElScrollbar>
+                        )}
+
+                        <div class={[styles.musicList, "musicList-container"]}>
+                           <div class={styles.searchHeader}>
+                              {state.categoryList.length > 1 && (
+                                 <div class={[styles.categorySection]}>
+                                    <NPopselect
+                                       options={state.categoryList}
+                                       v-model:value={state.categoryId}
+                                       onUpdate:value={(val: any) => {
+                                          const item = state.categoryList.find((item: any) => item.value === val)
+                                          console.log(item, "item")
+                                          if (item) {
+                                             state.categoryName = item.label
+                                             state.categoryId = item.value
+                                             setDefaultData("category")
+                                             handleGetList()
+                                          }
+                                       }}
+                                       trigger="click"
+                                       class={"PopSelect"}
+                                    >
+                                       <span class={styles.iconTagName}>{state.categoryName}</span>
+                                    </NPopselect>
+                                 </div>
+                              )}
+
+                              <div class={styles.searchMore}>
+                                 <div class={styles.searchSection}>
+                                    <Dictionary
+                                       popperClass="classTypePopper"
+                                       v-model={state.subjectId}
+                                       height={42}
+                                       options={state.subjectList}
+                                       placeholder="全部声部"
+                                       onChange={handleGetList}
+                                    />
+                                    <Dictionary
+                                       popperClass="classTypePopper"
+                                       v-model={state.levelId}
+                                       height={42}
+                                       options={state.levelList}
+                                       placeholder="级别"
+                                       onChange={() => {
+                                          setDefaultData("level")
+                                          handleGetList()
+                                       }}
+                                    />
+                                    <Dictionary
+                                       popperClass="classTypePopper"
+                                       v-model={state.typeId}
+                                       height={42}
+                                       options={state.typeList}
+                                       propsOpt={{
+                                          labelField: "name",
+                                          valueField: "id"
+                                       }}
+                                       placeholder="分类"
+                                       onChange={handleGetList}
+                                    />
+                                 </div>
+                                 <div
+                                    class={[styles.btnSearch, state.searchStatus && styles.btnSearchActive]}
+                                    onClick={() => (state.searchStatus = !state.searchStatus)}
+                                 ></div>
+                              </div>
+                              {state.searchStatus && (
+                                 <MyInput
+                                    class="queryCp"
+                                    v-model={state.queryStr}
+                                    height={42}
+                                    placeholder="请输入曲目关键词"
+                                    onKeyup={(e: any) => {
+                                       if (e.code === "Enter" || e.key === "Enter") {
+                                          handleGetList()
+                                       }
+                                    }}
+                                    onHandleQuery={handleGetList}
+                                    clearable
+                                 />
+                              )}
+                           </div>
+
+                           <div class={[styles.wrapList, !state.list.length && !loading.value && styles.wrapListEmpty]}>
+                              {state.list.map((item: any, index: number) => (
+                                 <div
+                                    class={[styles.item, index === state.listActive && styles.active]}
+                                    onClick={() => {
+                                       state.listActive = index
+                                       resetRender()
+                                    }}
+                                 >
+                                    <div class={styles.itemInfo}>
+                                       <div class={styles.img}>
+                                          <NImage
+                                             lazy
+                                             objectFit="cover"
+                                             previewDisabled={true}
+                                             src={icon_default}
+                                             onLoad={(e: any) => {
+                                                ;(e.target as any).dataset.loaded = "true"
+                                             }}
+                                          />
+                                          {/* <PlayLoading
+                                       class={[
+                                         data.listActive === index &&
+                                         data.playState === 'play'
+                                           ? ''
+                                           : styles.showPlayLoading
+                                       ]}
+                                       /> */}
+                                       </div>
+                                       <div class={styles.title}>
+                                          <div class={styles.titleName}>
+                                             <ellipsisScroll title={item.name} />
+                                          </div>
+                                       </div>
+                                    </div>
+                                    <div class={styles.btnSection}>
+                                       <div
+                                          class={styles.btn}
+                                          onClick={(e: any) => {
+                                             e.stopPropagation()
+                                             handlePlay(item)
+                                             if (state.listActive === index && state.playState === "play") {
+                                                musicIframeLoad()
+                                             }
+                                          }}
+                                       >
+                                          {state.listActive === index && (
+                                             <>
+                                                {state.playState === "pause" ? "播放" : "暂停"}
+                                                <img src={state.playState === "pause" ? iconBtnPlay : (iconBtnPause as any)} />
+                                             </>
+                                          )}
+                                          {state.listActive !== index && (
+                                             <>
+                                                播放
+                                                <img src={iconBtnPlay as any} />
+                                             </>
+                                          )}
+                                       </div>
+                                    </div>
+                                 </div>
+                              ))}
+
+                              {!state.list.length && !loading.value && (
+                                 <ElEmpty class={styles.empty} image={require("@/img/layout/empty.png")} description="暂无结果" />
+                              )}
+
+                              <div ref={spinRef} class={[styles.loadingWrap, state.finshed && styles.showLoading]}>
+                                 <NSpin show={true} stroke="#FF531C"></NSpin>
+                              </div>
+                           </div>
+                        </div>
+                     </div>
+                  </div>
+                  <div class={styles.rightContainer}>
+                     <i class={styles.leftArrow}></i>
+
+                     <NSpin show={staffLoading.value} stroke="#FF531C">
+                        <div class={styles.musicName}>{activeItem.value.name}</div>
+                        <div class={[styles.staffImgs, !state.list.length && !loading.value && styles.staffImgsEmpty]}>
+                           {state.iframeSrc && activeItem.value?.id && (
+                              <iframe
+                                 id="staffIframeRef"
+                                 style={{
+                                    // opacity: loading.value ? 0 : 1,
+                                    width: "100%",
+                                    height: "100%"
+                                 }}
+                                 src={state.iframeSrc}
+                                 onLoad={musicIframeLoad}
+                              ></iframe>
+                           )}
+
+                           {!loading.value && !activeItem.value?.id && (
+                              <ElEmpty class={styles.empty} image={require("@/img/layout/empty.png")} description="暂无结果" />
+                           )}
+                        </div>
+                     </NSpin>
+
+                     <img
+                        style={{
+                           display: activeItem.value?.id ? "" : "none"
+                        }}
+                        class={[styles.goBtn]}
+                        src={btnSubmit as any}
+                        onClick={() => goToCloud(activeItem.value.id)}
+                     />
+
+                     <div
+                        class={styles.rightBtns}
+                        style={{ display: activeItem.value.id && activeItem.value.musicSheetType === "CONCERT" ? "" : "none" }}
+                     >
+                        <NPopselect
+                           //  options={data.trackList}
+                           trigger="hover"
+                           //  v-model:value={data.musicInstrumentIndex}
+                           onUpdate:value={async () => {
+                              // await analyzeXml();
+                              // //
+                              // musicIfrcmeLoad();
+                           }}
+                           class={[styles.popSelect]}
+                        >
+                           <img class={styles.transBtn} src={iconTransfer as any} />
+                        </NPopselect>
+                     </div>
+                  </div>
+               </div>
+            </ElScrollbar>
+
+            {state.list.length !== 0 && activeItem.value.audioFileUrl && (
+               <PlayItem
+                  show={state.showPlayer}
+                  playState={state.playState}
+                  songPrevNextStatus={songPrevNextStatus.value}
+                  item={activeItem.value}
+                  onChange={value => handleChangeAudio(value)}
+                  onShow={(status: boolean) => {
+                     state.showPlayer = status
+                  }}
+               />
+            )}
+         </NavContainer>
+      )
+   }
+})

+ 42 - 0
src/views/cloudPractice/cloudPractice.vue

@@ -0,0 +1,42 @@
+<!--
+* @FileDescription: 云练习
+* @Author: 王新雷
+* @Date:2024年9月2日17:44:14
+-->
+<template>
+   <navContainer :navs="navs">
+      <div class="cloudPractice">
+         <div class="leftContainer">
+            <div class="details"></div>
+         </div>
+         <div class="rightContainer"></div>
+      </div>
+   </navContainer>
+</template>
+
+<script setup lang="ts">
+import navContainer from "@/businessComponents/navContainer"
+
+const navs = [
+   {
+      name: "主页",
+      url: "/"
+   },
+   {
+      name: "云练习"
+   }
+]
+</script>
+
+<style lang="scss" scoped>
+.leftContainer {
+   flex-shrink: 0;
+   margin-left: 50px;
+   margin-top: -35px;
+   width: 572px;
+   height: 764px;
+   background: url("@/img/homePage/bg1.png") no-repeat;
+   background-size: 100% 100%;
+   position: relative;
+}
+</style>

+ 217 - 0
src/views/cloudPractice/component/play-item/index.module.scss

@@ -0,0 +1,217 @@
+.container {
+   position: fixed;
+   left: 0;
+   bottom: 0;
+   right: 0;
+   display: flex;
+   align-items: center;
+   height: 108px;
+   padding: 0 160px 0 60px;
+   background-color: #fff;
+   box-shadow: 0px 2px 12px 0px rgba(0, 0, 0, 0.1);
+   z-index: 10;
+   transition: all 0.3s;
+
+   &.previewcontainer {
+      left: 0;
+      padding-right: 380px;
+   }
+
+   &.containerModal {
+      position: absolute;
+      left: 0;
+   }
+}
+
+.hidden {
+   transform: translateY(100%);
+   &.item {
+      opacity: 0;
+      display: none;
+   }
+}
+
+.item {
+   position: relative;
+   display: flex;
+   align-items: center;
+   width: 100%;
+
+   .img {
+      position: relative;
+      width: 64px;
+      height: 64px;
+      border-radius: 50%;
+      margin-right: 12px;
+      background-color: #000;
+      box-shadow: 0 0 10px 4px rgba(27, 35, 55, 0.1);
+      padding: 7px;
+      overflow: hidden;
+      flex-shrink: 0;
+
+      :global {
+         .n-image {
+            border-radius: 50%;
+            width: 100%;
+            height: 100%;
+         }
+      }
+
+      img {
+         transition: opacity 0.3s;
+         opacity: 0;
+         animation: rotateImg 6s linear infinite;
+      }
+
+      &.imgRotate {
+         img {
+            animation-play-state: paused;
+         }
+      }
+
+      img[data-loaded="true"] {
+         opacity: 1;
+      }
+   }
+
+   .svgcontainer {
+      position: fixed;
+      z-index: -1000;
+      pointer-events: none;
+   }
+
+   .progress {
+      position: absolute;
+      left: 4px;
+      top: 4px;
+      width: 56px;
+      pointer-events: none;
+      transform: rotate(180deg);
+
+      :global {
+         .n-progress-graph .n-progress-graph-circle .n-progress-graph-circle-fill {
+            stroke: url(#GradientProgress);
+         }
+      }
+   }
+
+   .title {
+      margin-right: 15px;
+      width: 200px;
+
+      .titleName {
+         font-size: max(16px, 13px);
+         font-weight: 600;
+         color: #131415;
+         line-height: 28px;
+         white-space: nowrap;
+      }
+
+      .titleDes {
+         font-size: max(14px, 12px);
+         font-weight: 400;
+         color: #777777;
+         line-height: 20px;
+         white-space: nowrap;
+      }
+   }
+}
+
+@keyframes rotateImg {
+   100% {
+      transform: rotate(360deg);
+   }
+}
+
+.playBtns {
+   margin-left: 140px;
+   display: flex;
+   align-items: center;
+
+   :global {
+      .n-button {
+         width: 40px;
+         height: 40px;
+
+         img {
+            width: 100%;
+            height: 100%;
+         }
+      }
+   }
+
+   .disabled {
+      opacity: 0.7;
+      cursor: not-allowed;
+   }
+
+   .playBtn {
+      margin: 0 48px;
+      width: 50px;
+      height: 50px;
+
+      img {
+         display: block;
+         width: 100%;
+         height: 100%;
+      }
+   }
+}
+
+.timeWrap {
+   flex: 1;
+   display: flex;
+   align-items: center;
+   margin-left: 88px;
+
+   .timeProgress {
+      margin-right: 24px;
+      border-radius: 6px;
+      --n-rail-height: 5px !important;
+      --n-fill-color: #ff531c !important;
+      --n-fill-color-hover: #ff531c !important;
+   }
+
+   .time {
+      width: 90px;
+      white-space: nowrap;
+      flex-shrink: 0;
+      color: #777777;
+   }
+}
+
+.iconArrow {
+   position: absolute;
+   top: -24px;
+   right: 30px;
+   display: flex;
+   align-items: center;
+   justify-content: center;
+   background-color: #fff;
+   cursor: pointer;
+   background: #ffffff;
+   border-radius: 100px 100px 0px 0px;
+   width: 44px;
+   height: 24px;
+   img {
+      margin-top: 3px;
+      width: 14px;
+      height: 14px;
+   }
+   //  &::before {
+   //     content: "";
+   //     display: inline-block;
+   //     background: url("../../../../img/cloudPractice/song-arrow.png") no-repeat center #fff;
+   //     background-size: contain;
+   //     width: 14px;
+   //     height: 14px;
+   //     margin-top: -4px;
+   //  }
+
+   &.down {
+      img {
+        margin-top: 0px;
+         transform: rotate(180deg);
+      }
+   }
+}

+ 226 - 0
src/views/cloudPractice/component/play-item/index.tsx

@@ -0,0 +1,226 @@
+import { PropType, computed, defineComponent, reactive, ref, watch } from "vue"
+import styles from "./index.module.scss"
+import { NButton, NImage, NProgress, NSlider } from "naive-ui"
+// import { IMusicItem } from '../../type';
+import icon_pre from "../../../../img/cloudPractice/icon_pre.png"
+import icon_next from "../../../../img/cloudPractice/icon_next.png"
+import icon_play from "../../../../img/cloudPractice/icon_play.png"
+import icon_pause from "../../../../img/cloudPractice/icon_pause.png"
+import song_arrow from "../../../../img/cloudPractice/song-arrow.png"
+import { getSecondRPM } from "@/libs/utils"
+// import TheNoticeBar from '/src/components/TheNoticeBar';
+
+export default defineComponent({
+   name: "playItem",
+   props: {
+      item: {
+         type: Object as PropType<any>,
+         default: () => ({})
+      },
+      show: {
+         type: Boolean,
+         default: false
+      },
+      playState: {
+         type: String as PropType<"play" | "pause">,
+         default: "pause"
+      },
+      type: {
+         type: String,
+         default: ""
+      },
+      songPrevNextStatus: {
+         type: Object,
+         default: () => ({})
+      }
+   },
+   emits: ["change", "show"],
+   setup(props, { emit }) {
+      let timer = null as any
+      const audioData = reactive({
+         isFirst: true,
+         duration: 0,
+         currentTime: 0
+      })
+      const audioRef = ref()
+      /** 加载成功 */
+      const onLoadedmetadata = () => {
+         audioData.duration = audioRef.value?.duration
+         if (audioData.isFirst) {
+            audioData.isFirst = false
+            return
+         }
+         if (props.playState === "play") {
+            audioRef.value.play()
+         }
+
+         // 判断是否有链接
+         if (!props.item.audioFileUrl && !props.item.metronomeUrl) {
+            emit("change", "pause")
+         }
+      }
+      /** 改变时间 */
+      const handleChangeTime = (val: number) => {
+         audioRef.value.pause()
+         audioData.currentTime = val
+         clearTimeout(timer)
+         timer = setTimeout(() => {
+            audioRef.value.currentTime = val
+            if (props.playState === "play") {
+               audioRef.value.play()
+            }
+            timer = null
+         }, 300)
+      }
+      const time = computed(() => {
+         return `${getSecondRPM(audioData.currentTime)} / ${getSecondRPM(audioData.duration)}`
+      })
+
+      watch(
+         () => props.playState,
+         val => {
+            console.log(props.playState, "props.playState")
+            if (val === "play") {
+               audioRef.value.play().catch(() => {
+                  audioRef.value.play()
+               })
+            } else {
+               audioRef.value.pause()
+            }
+         }
+      )
+
+      watch(
+         () => props.item,
+         () => {
+            // 判断是否有链接
+            console.log(props.item, "props.item")
+            if (!props.item.audioFileUrl && !props.item.metronomeUrl) {
+               emit("change", "pause")
+            }
+         }
+      )
+
+      return () => (
+         <div
+            class={[
+               styles.container,
+               props.type === "preview" && styles.previewcontainer,
+               props.type === "modal" && styles.containerModal,
+               props.show ? styles.show : styles.hidden
+            ]}
+         >
+            <div class={[styles.item]}>
+               <div class={[styles.img, props.playState !== "play" && styles.imgRotate]}>
+                  <NImage
+                     lazy
+                     objectFit="cover"
+                     previewDisabled={true}
+                     src={props.item.titleImg || "https://oss.dayaedu.com/klx/16983720423251690789356356.png"}
+                     onLoad={(e: any) => {
+                        ;(e.target as any).dataset.loaded = "true"
+                     }}
+                  />
+
+                  <svg class={styles.svgcontainer}>
+                     <defs>
+                        <linearGradient id="GradientProgress">
+                           <stop stop-color="#FF5B20" offset="0%" />
+                           <stop stop-color="#FF9946" offset="100%" />
+                        </linearGradient>
+                     </defs>
+                  </svg>
+
+                  <NProgress
+                     type="circle"
+                     class={styles.progress}
+                     showIndicator={false}
+                     percentage={(audioData.currentTime / audioData.duration) * 100}
+                  />
+               </div>
+               <div class={styles.title}>
+                  <div class={styles.titleName}>
+                     {/* <TheNoticeBar text={props.item.musicSheetName} /> */}
+                     {props.item.name}
+                  </div>
+                  {/* <div class={styles.titleDes}>{props.item.composer}</div> */}
+               </div>
+
+               <div class={styles.playBtns}>
+                  <NButton
+                     color="rgba(246,246,246,1)"
+                     class={!props.songPrevNextStatus.prev && styles.disabled}
+                     circle
+                     bordered={false}
+                     onClick={() => emit("change", "pre")}
+                  >
+                     <img src={icon_pre as any} />
+                  </NButton>
+                  <NButton
+                     color="rgba(246,246,246,1)"
+                     class={styles.playBtn}
+                     circle
+                     bordered={false}
+                     onClick={() => emit("change", props.playState === "pause" ? "play" : "pause")}
+                  >
+                     <img
+                        style={{
+                           display: props.playState === "pause" ? "" : "none"
+                        }}
+                        src={icon_play as any}
+                     />
+                     <img
+                        style={{
+                           display: props.playState === "play" ? "" : "none"
+                        }}
+                        src={icon_pause as any}
+                     />
+                  </NButton>
+                  <NButton
+                     color="rgba(246,246,246,1)"
+                     class={!props.songPrevNextStatus.next && styles.disabled}
+                     circle
+                     bordered={false}
+                     onClick={() => emit("change", "next")}
+                  >
+                     <img src={icon_next as any} />
+                  </NButton>
+               </div>
+
+               <div class={styles.timeWrap}>
+                  <NSlider
+                     tooltip={false}
+                     step={0.01}
+                     class={styles.timeProgress}
+                     value={audioData.currentTime}
+                     max={audioData.duration}
+                     onUpdate:value={(val: any) => handleChangeTime(val)}
+                  />
+                  <div class={styles.time}>{time.value}</div>
+                  <audio
+                     ref={audioRef}
+                     src={props.item.audioFileUrl || props.item.metronomeUrl}
+                     onLoadedmetadata={onLoadedmetadata}
+                     onEnded={() => {
+                        emit("change", "pause")
+                     }}
+                     onTimeupdate={() => {
+                        if (timer) return
+                        audioData.currentTime = audioRef.value?.currentTime
+                     }}
+                  ></audio>
+               </div>
+            </div>
+
+            <div
+               class={[styles.iconArrow, props.show ? "" : styles.down]}
+               onClick={() => {
+                  emit("show", !props.show)
+               }}
+            >
+               <img src={song_arrow as any} />
+            </div>
+         </div>
+      )
+   }
+})

+ 48 - 0
src/views/cloudPractice/component/play-loading/index.module.scss

@@ -0,0 +1,48 @@
+.audioAnimate {
+    position: absolute;
+    left: 0;
+    right: 0;
+    top: 0;
+    bottom: 0;
+    background-color: rgba(0, 0, 0, .6);
+    display: flex;
+    justify-content: center;
+    align-items: flex-end;
+    padding-bottom: 30%;
+
+    div {
+        width: 5px;
+        height: 20px;
+        background: linear-gradient(135deg, #34FFC5 0%, #1BD2FF 100%);
+        transform-origin: bottom;
+        border-radius: 5px 5px 0 0;
+        margin: 0 2px;
+    }
+
+    & div:nth-child(1) {
+        animation: musicWave 0.5s infinite linear both alternate;
+    }
+
+    & div:nth-child(2) {
+        animation: musicWave 0.2s infinite linear both alternate;
+    }
+
+    & div:nth-child(3) {
+        animation: musicWave 0.6s infinite linear both alternate;
+    }
+
+    & div:nth-child(4) {
+        animation: musicWave 0.3s infinite linear both alternate;
+    }
+}
+
+
+@keyframes musicWave {
+    0% {
+        height: 5px;
+    }
+
+    100% {
+        height: 20px;
+    }
+}

+ 16 - 0
src/views/cloudPractice/component/play-loading/index.tsx

@@ -0,0 +1,16 @@
+import { defineComponent } from "vue"
+import styles from "./index.module.scss"
+
+export default defineComponent({
+   name: "playLoading",
+   setup() {
+      return () => (
+         <div class={styles.audioAnimate}>
+            <div></div>
+            <div></div>
+            <div></div>
+            <div></div>
+         </div>
+      )
+   }
+})

+ 495 - 0
src/views/cloudPractice/index.module.scss

@@ -0,0 +1,495 @@
+.cloudPractice {
+   display: flex;
+   width: 100%;
+   height: 100%;
+
+   & > :deep(.elScrollbar) {
+      .el-scrollbar__view {
+         width: 100%;
+         display: flex;
+         padding: 50px 50px 0;
+      }
+      .el-scrollbar__wrap {
+         overflow-x: hidden;
+      }
+   }
+}
+
+:global {
+   .PopSelect {
+      width: 184px;
+      background: #ffffff;
+      box-shadow: 0px 2px 17px 0px rgba(0, 0, 0, 0.08);
+      border-radius: 12px;
+      --n-space-arrow: 12px !important;
+      --n-option-height: 40px !important;
+
+      .n-base-select-menu-option-wrapper {
+         padding-top: 4px;
+         padding-bottom: 4px;
+      }
+      .n-base-select-option:hover::before {
+         background: #fff3d7 !important;
+      }
+      .n-base-select-option {
+         font-size: 16px !important;
+      }
+      .n-base-select-option.n-base-select-option--selected {
+         font-weight: 600;
+
+         color: #994d1c;
+      }
+      .n-base-select-option::before {
+         left: 6px;
+         right: 6px;
+      }
+      .n-base-select-option.n-base-select-option--selected.n-base-select-option--pending::before {
+         background-color: inherit;
+      }
+      .n-base-select-option .n-base-select-option__check {
+         display: none;
+      }
+   }
+}
+.leftContainer {
+   flex-shrink: 0;
+   margin-left: 60px;
+   margin-top: 47px;
+   width: 600px;
+   height: 729px;
+   position: relative;
+   background: linear-gradient(270deg, #fede94 0%, #ffe3a3 48%, #fede94 100%);
+   border-radius: 36px;
+   padding-top: 47px;
+
+   &::before {
+      content: "";
+      width: 335px;
+      height: 59px;
+      background: url("@/img/cloudPractice/header-ring.png");
+      background-size: contain;
+      display: block;
+      position: absolute;
+      top: -30px;
+      left: 50%;
+      margin-left: -167.5px;
+   }
+
+   .details {
+      position: relative;
+      margin: 0 32px 32px;
+      width: 536px;
+      height: 650px;
+      background: #ffffff;
+      border-radius: 30px;
+   }
+   .leftSection {
+      position: absolute;
+      left: -49px;
+      top: 21px;
+      height: 608px;
+      overflow-x: hidden;
+      overflow-y: auto;
+
+      &::-webkit-scrollbar {
+         width: 4px;
+         display: none;
+      }
+      .leftSection_item {
+         padding-left: 10px;
+         letter-spacing: 4px;
+         width: 49px;
+         height: 131px;
+         background: url("@/img/cloudPractice/icon-left-default.png");
+         background-size: contain;
+         margin-bottom: 12px;
+         display: flex;
+         align-items: center;
+         justify-content: center;
+         writing-mode: vertical-lr;
+         font-weight: 600;
+         font-size: 20px;
+         color: #ffffff;
+         line-height: 24px;
+         text-shadow: 0px 1px 1px #ffac2d;
+         user-select: none;
+         cursor: pointer;
+
+         &:last-child {
+            margin-bottom: 0;
+         }
+      }
+      .leftSection_item__active {
+         background: url("@/img/cloudPractice/icon-left-active.png");
+         background-size: contain;
+         color: #fe7846;
+      }
+   }
+
+   .musicList {
+      height: 100%;
+      overflow-x: hidden;
+      overflow-y: auto;
+
+      &::-webkit-scrollbar {
+         width: 0;
+         display: none;
+      }
+   }
+
+   .searchHeader {
+      position: sticky;
+      top: 0;
+      left: 0;
+      z-index: 9;
+      padding: 28px 0 15px;
+      background-color: #fff;
+      border-radius: 36px 36px 0 0;
+
+      .categorySection {
+         border-bottom: 1px solid #f2f2f2;
+         padding: 0 20px 16px;
+         margin-bottom: 18px;
+      }
+      .iconTagName {
+         cursor: pointer;
+         &::before {
+            content: "";
+            display: inline-block;
+            margin-right: 8px;
+            width: 22px;
+            height: 22px;
+            background: url("../../img/cloudPractice/icon-tag.png") no-repeat center;
+            background-size: contain;
+            vertical-align: text-bottom;
+         }
+         font-weight: 600;
+         font-size: 18px;
+         color: #333333;
+         line-height: 22px;
+         height: 22px;
+
+         &::after {
+            content: "";
+            display: inline-block;
+            margin-left: 8px;
+            width: 12px;
+            height: 12px;
+            background: url("../../img/cloudPractice/icon-arrow-down.png") no-repeat center;
+            background-size: contain;
+         }
+      }
+   }
+
+   .searchMore {
+      display: flex;
+      justify-content: space-between;
+      padding: 0 20px;
+   }
+
+   .searchSection {
+      gap: 0 16px;
+      display: flex;
+      margin-right: 16px;
+   }
+
+   :global {
+      .h_dictionary {
+         flex: 1;
+      }
+      .h_dictionary .el-input__wrapper {
+         background: #fff3d7;
+         padding-left: 14px;
+         padding-right: 7px;
+         font-size: 16px;
+         .el-input__icon {
+            margin-left: 0;
+         }
+      }
+      .el-cascader:not(.is-disabled):hover .el-input__wrapper {
+         box-shadow: none;
+      }
+
+      .queryCp {
+         margin: 12px 30px 0 !important;
+         width: calc(100% - 60px) !important;
+         // margin-top: 12px;
+         background: #fff3d7 !important;
+         .el-input__wrapper {
+            background: #fff3d7 !important;
+         }
+      }
+   }
+
+   .btnSearch {
+      flex-shrink: 0;
+      width: 42px;
+      height: 42px;
+      background: url("@/img/cloudPractice/icon-search.png") no-repeat center;
+      background-size: contain;
+      cursor: pointer;
+
+      &.btnSearchActive {
+         opacity: 0.6;
+      }
+   }
+
+   .wrapList {
+      width: 100%;
+      // min-width: 294px;
+      min-height: 100%;
+      padding: 0 20px;
+      // background: #fff;
+      // border-radius: 16px;
+      &.wrapListEmpty {
+         display: flex;
+         align-items: center;
+         justify-content: center;
+
+         .empty {
+            margin-top: -180px;
+            --el-empty-image-width: 277px;
+            :global {
+               .el-empty__description p {
+                  font-weight: 400;
+                  font-size: 22px;
+                  color: #aaaaaa;
+               }
+            }
+         }
+      }
+      .item {
+         position: relative;
+         display: flex;
+         align-items: center;
+         justify-content: space-between;
+         padding: 15px 10px 15px 26px;
+         border-radius: 12px;
+
+         cursor: pointer;
+
+         &:hover {
+            background: #fff3d7;
+         }
+
+         &.active {
+            background: #fff3d7;
+
+            .arrow {
+               opacity: 1;
+            }
+         }
+
+         .img {
+            position: relative;
+            width: 60px;
+            height: 60px;
+            border-radius: 16px;
+            margin-right: 12px;
+            overflow: hidden;
+            flex-shrink: 0;
+            :global {
+               .n-image {
+                  width: 60px;
+                  height: 60px;
+               }
+            }
+
+            img {
+               transition: opacity 0.3s;
+               opacity: 0;
+               height: 100%;
+               width: 100%;
+            }
+
+            img[data-loaded="true"] {
+               opacity: 1;
+            }
+         }
+
+         .itemInfo {
+            display: flex;
+            align-items: center;
+         }
+
+         .titleName {
+            font-weight: 600;
+            font-size: 20px;
+            color: #a15228;
+            line-height: 28px;
+            max-width: 270px;
+            overflow: hidden;
+         }
+
+         .btn {
+            cursor: pointer;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            width: 84px;
+            height: 36px;
+            background: linear-gradient(312deg, #ff9e49 0%, #ff531c 100%);
+            border-radius: 18px;
+            font-weight: 600;
+            font-size: 16px;
+            color: #ffffff;
+
+            img {
+               margin-left: 7px;
+               width: 14px;
+               height: 14px;
+            }
+         }
+      }
+   }
+
+   .loadingWrap {
+      display: flex;
+      justify-content: center;
+      min-height: 80px;
+
+      &.showLoading {
+         height: 0;
+         opacity: 0;
+         min-height: 0;
+         display: none;
+      }
+   }
+}
+
+.rightContainer {
+   flex: 1;
+   background: #ffffff;
+   border-radius: 36px;
+   margin-left: 32px;
+   margin-top: 47px;
+   margin-right: 60px;
+   position: relative;
+   display: flex;
+   flex-direction: column;
+
+   .leftArrow {
+      position: absolute;
+      top: 245px;
+      left: -17px;
+      display: inline-block;
+      width: 28px;
+      height: 28px;
+      background: #ffffff;
+      border-radius: 6px;
+      transform: rotate(45deg);
+   }
+
+   .goBtn {
+      position: absolute;
+      left: 50%;
+      bottom: 46px;
+      transform: translateX(-50%);
+      height: 102px;
+      cursor: pointer;
+      transition: all 0.2s ease-in;
+   }
+
+   .musicName {
+      padding-top: 79px;
+      font-size: 26px;
+      font-weight: 500;
+      color: #333;
+      line-height: 24px;
+      text-align: center;
+   }
+
+   .staffImgs {
+      flex: 1;
+      // overflow-y: auto;
+      height: 100%;
+      width: 100%;
+      padding: 0 30px;
+
+      & > img {
+         width: 100%;
+      }
+
+      &.staffImgsEmpty {
+         display: flex;
+         align-items: center;
+         justify-content: center;
+         .empty {
+            margin-top: -80px;
+            --el-empty-image-width: 277px;
+            :global {
+               .el-empty__description p {
+                  font-weight: 400;
+                  font-size: 22px;
+                  color: #aaaaaa;
+               }
+            }
+         }
+      }
+
+      :global {
+         iframe {
+            // visibility: hidden;
+            border: none;
+            width: 100%;
+            // height: 500px;
+            flex: 1 auto;
+            overflow: hidden;
+
+            body {
+               ::-webkit-scrollbar-thumb {
+                  background-color: #efeff0;
+                  border: 1px solid transparent;
+                  background-clip: padding-box;
+                  border-radius: 5px;
+               }
+            }
+         }
+      }
+   }
+
+   .rightBtns {
+      position: absolute;
+      top: 30px;
+      right: 40px;
+      display: flex;
+      align-items: center;
+      .transBtn {
+         width: 48px;
+         height: 48px;
+         cursor: pointer;
+         margin-right: 15px;
+         &:hover {
+            opacity: 0.7;
+         }
+      }
+   }
+
+   .popSelect {
+      font-size: 16px;
+      width: 300px;
+      max-height: 500px;
+      overflow-y: scroll;
+      box-shadow: 0px 2px 16px 0px rgba(0, 0, 0, 0.08);
+      border-radius: 11px;
+      --n-option-height: 34px;
+
+      :global {
+         .n-base-select-option__content {
+            width: 80% !important;
+         }
+      }
+   }
+
+   :global {
+      .n-spin-container {
+         height: 100%;
+      }
+      .n-spin-content {
+         height: 100%;
+         display: flex;
+         align-items: center;
+         flex-direction: column;
+      }
+   }
+}

+ 2 - 0
src/views/cloudPractice/index.ts

@@ -0,0 +1,2 @@
+import cloudPractice from "./cloudPractice"
+export default cloudPractice

+ 349 - 0
src/views/cloudPractice/instrument.ts

@@ -0,0 +1,349 @@
+// import { isSpecialMark, isSpeedKeyword, isGradientWords, GRADIENT_SPEED_RESET_TAG } from "./speed-tag"
+
+export class StringUtil {
+   public static StringContainsSeparatedWord(str: string, wordRegExString: string, ignoreCase = false): boolean {
+      const regExp = new RegExp("( |^)" + wordRegExString + "([ .]|$)", ignoreCase ? "i" : undefined)
+      return regExp.test(str)
+   }
+}
+
+export const formatXML = (xml: string): string => {
+   if (!xml) return ""
+   const xmlParse = new DOMParser().parseFromString(xml, "text/xml")
+   const measures: any = xmlParse.getElementsByTagName("measure")
+   // const repeats: any = Array.from(xmlParse.querySelectorAll('repeat'))
+   // 处理重复小节信息
+   // let speed = -1
+   let beats = -1
+   let beatType = -1
+   // 小节中如果没有节点默认为休止符
+   for (const measure of measures) {
+      if (beats === -1 && measure.getElementsByTagName("beats").length) {
+         beats = parseInt(measure.getElementsByTagName("beats")[0].textContent || "4")
+      }
+      if (beatType === -1 && measure.getElementsByTagName("beat-type").length) {
+         beatType = parseInt(measure.getElementsByTagName("beat-type")[0].textContent || "4")
+      }
+      // if (speed === -1 && measure.getElementsByTagName('per-minute').length) {
+      //   speed = parseInt(measure.getElementsByTagName('per-minute')[0].textContent || this.firstLib?.speed)
+      // }
+      const divisions = parseInt(measure.getElementsByTagName("divisions")[0]?.textContent || "256")
+      if (measure.getElementsByTagName("note").length === 0) {
+         const forwardTimeElement = measure.getElementsByTagName("forward")[0]?.getElementsByTagName("duration")[0]
+         if (forwardTimeElement) {
+            forwardTimeElement.textContent = "0"
+         }
+         measure.innerHTML =
+            measure.innerHTML +
+            `
+        <note>
+          <rest measure="yes"/>
+          <duration>${divisions * beats}</duration>
+          <voice>1</voice>
+          <type>whole</type>
+        </note>`
+      }
+   }
+   return new XMLSerializer().serializeToString(xmlParse)
+}
+
+export const onlyVisible = (xml: string, partIndex: number): string => {
+   if (!xml) return ""
+   const xmlParse = new DOMParser().parseFromString(xml, "text/xml")
+   const partList = xmlParse.getElementsByTagName("part-list")?.[0]?.getElementsByTagName("score-part") || []
+   const partListNames = Array.from(partList).map(item => item.getElementsByTagName("part-name")?.[0].textContent || "")
+   const parts: any = xmlParse.getElementsByTagName("part")
+   // const firstTimeInfo = parts[0]?.getElementsByTagName('metronome')[0]?.parentElement?.parentElement?.cloneNode(true)
+   const part: any = parts[0]
+   const firstMeasures = [...part.getElementsByTagName("measure")]
+   const metronomes = [...part.getElementsByTagName("metronome")]
+   // const words = [...part.getElementsByTagName("words")]
+   // const codas = [...part.getElementsByTagName("coda")]
+   // const rehearsals = [...part.getElementsByTagName("rehearsal")]
+
+   /** 第一分谱如果是约定的配置分谱则跳过 */
+   if (partListNames[0]?.toLocaleUpperCase?.() === "COMMON") {
+      partIndex++
+      partListNames.shift()
+   }
+   const visiblePartInfo = partList[partIndex]
+   // console.log(visiblePartInfo, partIndex)
+   if (visiblePartInfo) {
+      const id = visiblePartInfo.getAttribute("id")
+      Array.from(parts).forEach((part: any) => {
+         if (part && part.getAttribute("id") !== id) {
+            part.parentNode?.removeChild(part)
+            // 不等于第一行才添加避免重复添加
+         } else if (part && part.getAttribute("id") !== "P1") {
+            // 速度标记仅保留最后一个
+            const metronomeData: {
+               [key in string]: Element
+            } = {}
+            for (let i = 0; i < metronomes.length; i++) {
+               const metronome = metronomes[i]
+               const metronomeContainer = metronome.parentElement?.parentElement?.parentElement
+               if (metronomeContainer) {
+                  const index = firstMeasures.indexOf(metronomeContainer)
+                  metronomeData[index] = metronome
+               }
+            }
+            Object.values(metronomeData).forEach(metronome => {
+               const metronomeContainer: any = metronome.parentElement?.parentElement
+               const parentMeasure: any = metronomeContainer?.parentElement
+               const measureMetronomes = [...(parentMeasure?.childNodes || [])]
+               const metronomesIndex = metronomeContainer ? measureMetronomes.indexOf(metronomeContainer) : -1
+               // console.log(parentMeasure)
+               if (parentMeasure && metronomesIndex > -1) {
+                  const index = firstMeasures.indexOf(parentMeasure)
+                  const activeMeasure = part.getElementsByTagName("measure")[index]
+                  setElementNoteBefore(metronomeContainer, parentMeasure, activeMeasure)
+                  //   console.log(measureMetronomes, metronomesIndex, activeMeasure?.childNodes, activeMeasure?.childNodes[metronomesIndex])
+                  //   activeMeasure?.insertBefore(metronomeContainer.cloneNode(true), activeMeasure?.childNodes[metronomesIndex])
+                  //   // part.getElementsByTagName('measure')[index]?.appendChild(metronomeContainer.cloneNode(true))
+                  //   // console.log(index, parentMeasure, firstMeasures.indexOf(parentMeasure))
+               }
+            })
+            /** word比较特殊需要精确到note位置 */
+            // words.forEach((word) => {
+            //   let text = word.textContent || "";
+            //   text = ["cresc."].includes(text) ? "" : text;
+            //   if ((isSpecialMark(text) || isSpeedKeyword(text) || isGradientWords(text) || isRepeatWord(text) || GRADIENT_SPEED_RESET_TAG) && text) {
+            //     const wordContainer = word.parentElement?.parentElement;
+            //     const parentMeasure = wordContainer?.parentElement;
+            //     const measureWords = [...(parentMeasure?.childNodes || [])];
+            //     const wordIndex = wordContainer ? measureWords.indexOf(wordContainer) : -1;
+            //     if (wordContainer && parentMeasure && wordIndex > -1) {
+            //       const index = firstMeasures.indexOf(parentMeasure);
+            //       const activeMeasure = part.getElementsByTagName("measure")[index];
+            //       // 找当前小节是否包含word标签
+            //       const _words: any = Array.from(activeMeasure?.getElementsByTagName("words") || []);
+            //       // 遍历word标签,检查是否和第一小节重复,如果有重复则不平移word
+            //       const total = _words.reduce((total: any, _word) => {
+            //         if (_word.textContent?.includes(text)) {
+            //           total++;
+            //         }
+            //         return total;
+            //       }, 0);
+            //       if (total === 0) {
+            //         setElementNoteBefore(wordContainer, parentMeasure, activeMeasure);
+
+            //       }
+            //     }
+            //   }
+            // });
+            /** word比较特殊需要精确到note位置 */
+            // codas.forEach((coda) => {
+            //   const wordContainer = coda.parentElement?.parentElement;
+            //   const parentMeasure = wordContainer?.parentElement;
+            //   const measureWords = [...(parentMeasure?.childNodes || [])];
+            //   const wordIndex = wordContainer ? measureWords.indexOf(wordContainer) : -1;
+            //   if (wordContainer && parentMeasure && wordIndex > -1) {
+            //     const index = firstMeasures.indexOf(parentMeasure);
+            //     const activeMeasure = part.getElementsByTagName("measure")[index];
+
+            //     setElementNoteBefore(wordContainer, parentMeasure, activeMeasure);
+
+            //   }
+            // });
+            // rehearsals.forEach((rehearsal) => {
+            //   const container = rehearsal.parentElement?.parentElement;
+            //   const parentMeasure = container?.parentElement;
+            //   // console.log(rehearsal)
+            //   if (parentMeasure) {
+            //     const index = firstMeasures.indexOf(parentMeasure);
+            //     part.getElementsByTagName("measure")[index]?.appendChild(container.cloneNode(true));
+            //     // console.log(index, parentMeasure, firstMeasures.indexOf(parentMeasure))
+            //   }
+            // });
+         } else {
+            // words.forEach((word) => {
+            //   const text = word.textContent || "";
+            //   if (isSpeedKeyword(text) && text) {
+            //     const wordContainer = word.parentElement?.parentElement?.parentElement;
+            //     if (wordContainer && wordContainer.firstElementChild && wordContainer.firstElementChild !== word) {
+            //       const wordParent = word.parentElement?.parentElement;
+            //       const fisrt = wordContainer.firstElementChild;
+            //       wordContainer.insertBefore(wordParent, fisrt);
+            //     }
+            //   }
+            // });
+         }
+
+         // 最后一个小节的结束线元素不在最后 调整
+         if (part && part.getAttribute("id") === id) {
+            const barlines = part.getElementsByTagName("barline")
+            const lastParent = barlines[barlines.length - 1]?.parentElement
+            if (lastParent?.lastElementChild?.tagName !== "barline") {
+               const children: any = lastParent?.children || []
+               for (const el of children) {
+                  if (el.tagName === "barline") {
+                     // 将结束线元素放到最后
+                     lastParent?.appendChild(el)
+                     break
+                  }
+               }
+            }
+         }
+      })
+      Array.from(partList).forEach(part => {
+         if (part && part.getAttribute("id") !== id) {
+            part.parentNode?.removeChild(part)
+         }
+      })
+      // 处理装饰音问题
+      // const notes = xmlParse.getElementsByTagName("note")
+      // const getNextvNoteDuration = (i: number) => {
+      //    let nextNote = notes[i + 1]
+      //    // 可能存在多个装饰音问题,取下一个非装饰音时值
+      //    for (let index = i; index < notes.length; index++) {
+      //       const note = notes[index]
+      //       if (!note.getElementsByTagName("grace")?.length) {
+      //          nextNote = note
+      //          break
+      //       }
+      //    }
+      //    const nextNoteDuration = nextNote?.getElementsByTagName("duration")[0]
+      //    return nextNoteDuration
+      // }
+      // Array.from(notes).forEach((note) => {
+      //    const graces = note.getElementsByTagName("grace")
+      //    if (graces && graces.length) {
+      //       // if (i !== 0) {
+      //       // note.appendChild(getNextvNoteDuration(i)?.cloneNode(true))
+      //       // }
+      //    }
+      // })
+   }
+   // console.log(xmlParse)
+   return new XMLSerializer().serializeToString(appoggianceFormate(xmlParse))
+}
+
+// 倚音后连音线
+export const appoggianceFormate = (xmlParse: Document): Document => {
+   if (!xmlParse) return xmlParse
+   const graces: any = xmlParse.querySelectorAll("grace")
+   if (!graces.length) return xmlParse
+   const getNextElement = (el: HTMLElement): HTMLElement => {
+      if (el.querySelector("grace")) {
+         return getNextElement(el?.nextElementSibling as HTMLElement)
+      }
+      return el
+   }
+   for (const grace of graces) {
+      const notations = grace.parentElement?.querySelector("notations")
+      if (notations && notations.querySelectorAll("slur").length > 1) {
+         const nextEle: Element = getNextElement(grace.parentElement?.nextElementSibling as HTMLElement)
+         if (nextEle && nextEle.querySelectorAll("slur").length > 0) {
+            const slurNumber = Array.from(nextEle.querySelector("notations")?.children || []).map((el: Element) => {
+               return el.getAttribute("number")
+            })
+            const slurs = notations.querySelectorAll("slur")
+            for (const nota of slurs) {
+               if (!slurNumber.includes(nota.getAttribute("number"))) {
+                  nextEle.querySelector("notations")?.appendChild(nota)
+               }
+            }
+         }
+      }
+   }
+   return xmlParse
+}
+
+/**
+ * 添加第一分谱信息至当前分谱
+ * @param ele 需要插入的元素
+ * @param fitstParent 合奏谱第一个分谱
+ * @param parent 需要添加的分谱
+ */
+const setElementNoteBefore = (ele: Element, fitstParent: Element, parent?: Element | null) => {
+   let noteIndex = 0
+   if (!fitstParent) {
+      return
+   }
+   for (let index = 0; index < fitstParent.childNodes.length; index++) {
+      const element = fitstParent.childNodes[index]
+      if (element.nodeName === "note") {
+         noteIndex++
+      }
+      if (element === ele) {
+         break
+      }
+   }
+   if (noteIndex === 0 && parent) {
+      parent.insertBefore(ele, parent.childNodes[0])
+      return
+   }
+   if (parent && parent.childNodes.length > 0) {
+      let noteIndex2 = 0
+      const notes = Array.from(parent.childNodes).filter(child => child.nodeName === "note")
+      const lastNote = notes[notes.length - 1]
+      if (noteIndex >= notes.length && lastNote) {
+         parent.insertBefore(ele, parent.childNodes[Array.from(parent.childNodes).indexOf(lastNote)])
+         return
+      }
+      for (let index = 0; index < notes.length; index++) {
+         const element = notes[index]
+         if (element.nodeName === "note") {
+            noteIndex2 = noteIndex2 + 1
+            if (noteIndex2 === noteIndex) {
+               parent.insertBefore(ele, element)
+               break
+            }
+         }
+      }
+   }
+   // console.log(noteIndex, parent)
+}
+
+/**
+ * 检查传入文字是否为重复关键词
+ * @param text 总谱xml
+ * @returns 是否是重复关键词
+ */
+export const isRepeatWord = (text: string): boolean => {
+   if (text) {
+      const innerText = text.toLocaleLowerCase()
+      const dsRegEx = "d\\s?\\.s\\."
+      const dcRegEx = "d\\.\\s?c\\."
+
+      return (
+         innerText === "@" ||
+         StringUtil.StringContainsSeparatedWord(innerText, dsRegEx + " al fine", true) ||
+         StringUtil.StringContainsSeparatedWord(innerText, dsRegEx + " al coda", true) ||
+         StringUtil.StringContainsSeparatedWord(innerText, dcRegEx + " al fine", true) ||
+         StringUtil.StringContainsSeparatedWord(innerText, dcRegEx + " al coda", true) ||
+         StringUtil.StringContainsSeparatedWord(innerText, dcRegEx) ||
+         StringUtil.StringContainsSeparatedWord(innerText, "da\\s?capo", true) ||
+         StringUtil.StringContainsSeparatedWord(innerText, dsRegEx, true) ||
+         StringUtil.StringContainsSeparatedWord(innerText, "dal\\s?segno", true) ||
+         StringUtil.StringContainsSeparatedWord(innerText, "al\\s?coda", true) ||
+         StringUtil.StringContainsSeparatedWord(innerText, "to\\s?coda", true) ||
+         StringUtil.StringContainsSeparatedWord(innerText, "a (la )?coda", true) ||
+         StringUtil.StringContainsSeparatedWord(innerText, "fine", true) ||
+         StringUtil.StringContainsSeparatedWord(innerText, "coda", true) ||
+         StringUtil.StringContainsSeparatedWord(innerText, "segno", true)
+      )
+   }
+   return false
+}
+
+/** 从xml中获取自定义信息,并删除多余的字符串 */
+export const getCustomInfo = (xml: string): any => {
+   const data = {
+      showSpeed: true,
+      parsedXML: xml
+   }
+   const xmlParse = new DOMParser().parseFromString(xml, "text/xml")
+   const words: any = xmlParse.getElementsByTagName("words")
+   for (const word of words) {
+      if (word && word.textContent?.trim() === "隐藏速度") {
+         data.showSpeed = false
+         word.textContent = ""
+      }
+      if (word && word.textContent?.trim() === "@") {
+         word.textContent = "segno"
+      }
+   }
+   data.parsedXML = new XMLSerializer().serializeToString(xmlParse)
+   return data
+}

+ 89 - 0
src/views/cloudPractice/speed-tag.ts

@@ -0,0 +1,89 @@
+/**
+  Grava壮板=40
+  Largo广板=46
+  Lento慢板=52
+  Adagio柔板=56
+  Larghetto小广板=60
+  Andante行板=66
+  Anderato/Andantino小行板=69
+  Moderato中板=88
+  Allegretto小快板=108
+  Allegro Moderato=108 // 考级需要
+  Allegro快板=132
+  Vivace快速有生气=152
+  Vivo快速有生气=160
+  Vivacissimo极其活泼的快板=168
+  Presto急板=184
+  Prestissimo最急板=208
+ */
+
+// import {TextAlignmentEnum} from "../../Common/Enums/TextAlignment";
+
+export const SpeedTag: { [key in string]: number } = {
+   Grava: 40,
+   Largo: 46,
+   Lento: 52,
+   Adagio: 56,
+   Larghetto: 60,
+   Andante: 66,
+   Anderato: 69,
+   Andantino: 69,
+   Moderato: 88,
+   Allegretto: 108,
+   "Allegro Moderato": 108,
+   Allegro: 132,
+   Vivace: 152,
+   Vivo: 160,
+   Vivacissimo: 168,
+   Presto: 184,
+   Prestissimo: 208
+}
+
+export const SpecialMarks: string[] = ["纯律", "纯律结束"]
+
+export const HideWords: string[] = ["跳过下一个", "b", "#", "§", "º", "X"]
+
+export const GradientWords: string[] = ["poco rit.", "rall.", "rit.", "accel.", "molto rit.", "molto rall", "lentando", "poco accel.", "calando"]
+
+export const GRADIENT_SPEED_CLOSE_TAG = "结束范围速度"
+
+export const GRADIENT_SPEED_RESET_TAG = "a tempo"
+
+export const SpecialWords: string[] = [GRADIENT_SPEED_CLOSE_TAG]
+
+export const SpeedKeyword = "速度 "
+export const SpeedHiddenKeyword = "仅文本速度 "
+
+/** 是否为速度关键词 */
+export function isSpeedKeyword(str: string): boolean {
+   return str.indexOf(SpeedKeyword) === 0
+}
+
+/** 是否为速度关键词 */
+export function isSpeedHiddenKeyword(str: string): boolean {
+   return str.indexOf(SpeedHiddenKeyword) === 0
+}
+
+/** 格式化速度关键词移除前缀 */
+export function formatSpeedKeyword(str: string): string {
+   return str.replace(SpeedHiddenKeyword, "").replace(SpeedKeyword, "")
+}
+
+/** 是否是渐变速度关键词 */
+export function isGradientWords(str: string): boolean {
+   return GradientWords.includes(str)
+}
+
+/**
+ * 是否为特殊标记
+ * 包含中文与英文,中文正则存在部分问题
+ */
+export function isSpecialMark(str: string): boolean {
+   return [...Object.keys(SpeedTag), ...SpecialMarks, ...SpecialWords, ...HideWords]
+      .map((s: string) => s.trim().toLocaleUpperCase())
+      .includes(str.toLocaleUpperCase().trim())
+}
+
+// export function isTopFont(textAlignment): boolean {
+//   return [TextAlignmentEnum.CenterTop, TextAlignmentEnum.RightTop].includes(textAlignment);
+// };

+ 40 - 0
src/views/cloudPractice/useData.ts

@@ -0,0 +1,40 @@
+// 处理 区分处理管乐迷 管乐团的数据
+
+// import { queryTree_gym } from "@/api/cloudPractice.api"
+import { URL_TEACH_GYM, URL_TEACH_GYT, URL_TEACH_KLX } from "@/config"
+import { getToken } from "@/libs/auth"
+
+// import { httpAjaxErrMsg, httpAjax } from "@/plugin/httpAjax"
+// import { CODE_ERR_CANCELED } from "@/libs/auth"
+// import { ElMessage } from "element-plus"
+
+import userStore from "@/store/modules/user"
+import { ref } from "vue"
+
+/**
+ * 搜索数据
+ */
+export const useFunction = () => {
+   const userStoreHook = userStore()
+   const loading = ref(false)
+
+   /** 跳转云教练 */
+   const goToCloud = (musicId: string) => {
+      const urlObj = {
+         GYT: `${URL_TEACH_GYT}?id=${musicId}&modelType=practice&modeType=json&Authorization=${getToken()}`,
+         GYM: `${URL_TEACH_GYM}?Authorization=${getToken()}&platform=web&liveConfig=1#/detail/${musicId}?isHideBack=true`,
+         KLX: `${URL_TEACH_KLX}??Authorization=${getToken()}&id=${musicId}&isHideBack=true&limitModel=practice`
+      }
+      window.open(urlObj[userStoreHook.roles!], "_blank")
+   }
+
+   return { loading, goToCloud }
+}
+
+// function chunkArray(array: any[], size: number) {
+//    const result = []
+//    for (let i = 0; i < array.length; i += size) {
+//       result.push(array.slice(i, i + size))
+//    }
+//    return result
+// }

+ 10 - 4
src/views/cloudTextbooks/chooseDialog.vue

@@ -60,11 +60,17 @@ function close() {
 }
 
 function handlePaly(id: string) {
-   const url = router.resolve({
+   // const url = router.resolve({
+   //    name: "coursewarePlay",
+   //    params: { id }
+   // }).href
+   // window.open(url, "_blank")
+   router.push({
       name: "coursewarePlay",
-      params: { id }
-   }).href
-   window.open(url, "_blank")
+      params: {
+         id
+      }
+   })
 }
 </script>
 

+ 37 - 5
src/views/cloudTextbooks/cloudTextbooks.vue

@@ -9,6 +9,18 @@
          <div class="headCon">
             <div class="operate">
                <dictionary
+                  v-if="albumOpt.length > 1"
+                  class="albumOpt"
+                  :popper-class="'classTypePopper'"
+                  v-model="albumId"
+                  :width="200"
+                  :height="40"
+                  :clearable="false"
+                  :options="albumOpt"
+                  placeholder="专辑"
+                  @change="handleQuery"
+               />
+               <dictionary
                   :popper-class="'classTypePopper'"
                   v-model="classType"
                   :width="150"
@@ -61,12 +73,12 @@ import chooseDialog from "./chooseDialog.vue"
 import userStore from "@/store/modules/user"
 import { useDataList } from "./useData"
 import { debounce } from "@/libs/tools"
-import { getLessonCoursewareSubjectList_gym } from "@/api/cloudTextbooks.api"
+import { getLessonCoursewareSubjectList_gym, queryPageSubject_klx } from "@/api/cloudTextbooks.api"
 import { httpAjax } from "@/plugin/httpAjax"
 import myInput from "@/components/myInput"
 
 const userStoreHook = userStore()
-const { handleGetList, listData, loading, handleListQuery } = useDataList()
+const { handleGetList, listData, loading, albumId, albumOpt, handleListQuery } = useDataList()
 
 const navs = [
    {
@@ -99,7 +111,7 @@ enum courseEmnu {
    TUBA_SINGLE = "大号"
 }
 function handleGetClassTypeOpt() {
-   //  区分管乐团和管乐迷课程类型
+   //  GYM,GYT,KLX 区分   课程类型
    if (userStoreHook.roles === "GYM") {
       httpAjax(getLessonCoursewareSubjectList_gym).then(res => {
          if (res.code === 200) {
@@ -108,7 +120,7 @@ function handleGetClassTypeOpt() {
                   value: "",
                   label: "全部"
                },
-               ...res.data.map((item: any) => {
+               ...(res.data || []).map((item: any) => {
                   return {
                      value: item.id,
                      label: item.name
@@ -117,7 +129,7 @@ function handleGetClassTypeOpt() {
             ]
          }
       })
-   } else {
+   } else if (userStoreHook.roles === "GYT") {
       classTypeOpt.value = [
          {
             value: "",
@@ -130,6 +142,23 @@ function handleGetClassTypeOpt() {
             }
          })
       ]
+   } else if (userStoreHook.roles === "KLX") {
+      httpAjax(queryPageSubject_klx).then(res => {
+         if (res.code === 200) {
+            classTypeOpt.value = [
+               {
+                  value: "",
+                  label: "全部"
+               },
+               ...(res.data?.rows || []).map((item: any) => {
+                  return {
+                     value: item.id,
+                     label: item.name
+                  }
+               })
+            ]
+         }
+      })
    }
 }
 
@@ -169,6 +198,9 @@ function handleClick(id: string) {
          .queryIpt {
             margin-left: 12px;
          }
+         > :deep(.albumOpt) {
+            margin-right: 12px;
+         }
       }
    }
    .cloudTextbooksCon {

+ 110 - 7
src/views/cloudTextbooks/useData.ts

@@ -4,7 +4,10 @@ import {
    queryLessonCourseware_gym,
    getLessonCoursewareCourseList_gym,
    getMyCourseware_gyt,
-   getMyCoursewareDetail_gyt
+   getMyCoursewareDetail_gyt,
+   getuyAlbumInfo_klx,
+   queryLessonCourseware_klx,
+   getLessonCoursewareCourseList_klx
 } from "@/api/cloudTextbooks.api"
 
 import { httpAjaxErrMsg, httpAjax } from "@/plugin/httpAjax"
@@ -29,14 +32,47 @@ export const useDataList = () => {
    const userStoreHook = userStore()
    const listData = shallowRef<listType>([])
    let storeData: listType[number] = []
+   // 专辑
+   const albumId = ref("")
+   const albumOpt = shallowRef<{ value: string; label: string }[]>([])
    const loading = ref(false)
    let coursewareController: AbortController
 
    function handleGetList() {
-      userStoreHook.roles === "GYM" ? handleGetList_gym("", "") : handleGetList_gyt()
+      //  GYM,GYT,KLX 区分   获取列表数据
+      if (userStoreHook.roles === "GYM") {
+         handleGetList_gym("", "")
+      } else if (userStoreHook.roles === "GYT") {
+         handleGetList_gyt()
+      } else if (userStoreHook.roles === "KLX") {
+         httpAjax(getuyAlbumInfo_klx).then(res => {
+            if (res.code === 200) {
+               // 专辑赋值
+               albumOpt.value = (res.data || []).reduce((arr: any[], item: any) => {
+                  if (item.coursewareCounts > 0) {
+                     arr.push({
+                        value: item.id,
+                        label: item.name
+                     })
+                  }
+                  return arr
+               }, [])
+               // 默认取第一个专辑
+               albumOpt.value.length && (albumId.value = albumOpt.value[0].value)
+               handleGetList_klx("", albumId.value, "")
+            }
+         })
+      }
    }
    function handleListQuery(type: string, queryStr: string) {
-      userStoreHook.roles === "GYM" ? handleQueryGetList_gym(type, queryStr) : handleQueryGetList_gyt(type, queryStr)
+      //  GYM,GYT,KLX 区分   查询列表数据
+      if (userStoreHook.roles === "GYM") {
+         handleQueryGetList_gym(type, queryStr)
+      } else if (userStoreHook.roles === "GYT") {
+         handleQueryGetList_gyt(type, queryStr)
+      } else if (userStoreHook.roles === "KLX") {
+         handleQueryGetList_klx(type, albumId.value, queryStr)
+      }
    }
 
    // 获取管乐团数据
@@ -107,8 +143,53 @@ export const useDataList = () => {
    function handleQueryGetList_gym(type: string, queryStr: string) {
       handleGetList_gym(type, queryStr)
    }
-
-   return { loading, listData, handleGetList, handleListQuery }
+   // 获取酷乐秀数据
+   function handleGetList_klx(type: string, albumId: string, queryStr: string) {
+      // 当没有专辑id时候 不查询
+      if (!albumId) {
+         return
+      }
+      if (coursewareController) {
+         coursewareController.abort()
+      }
+      coursewareController = new AbortController()
+      loading.value = true
+      httpAjax(queryLessonCourseware_klx, type, albumId, coursewareController).then(res => {
+         // 自己关闭的时候不取消加载
+         if (res.code === CODE_ERR_CANCELED) {
+            return
+         }
+         loading.value = false
+         if (res.code === 200) {
+            const data = (res.data?.rows || []).reduce((arr: any[], item: any) => {
+               if (queryStr ? item.musicSheetName.includes(queryStr) : true) {
+                  arr.push({
+                     name: item.musicSheetName,
+                     type: item.subjectId,
+                     img: item.titleImg,
+                     id: item.id,
+                     courseNum: item.courseNum
+                  })
+               }
+               return arr
+            }, [])
+            listData.value = chunkArray(data, 5)
+         } else {
+            if (res.code !== 511) {
+               ElMessage({
+                  showClose: true,
+                  message: res.message,
+                  type: "error"
+               })
+            }
+         }
+      })
+   }
+   // 管乐迷数据查询
+   function handleQueryGetList_klx(type: string, albumId: string, queryStr: string) {
+      handleGetList_klx(type, albumId, queryStr)
+   }
+   return { loading, listData, albumId, albumOpt, handleGetList, handleListQuery }
 }
 
 /**
@@ -131,7 +212,14 @@ export const useDataDetailList = () => {
       return [data[0] || [], data[1] || []]
    })
    function handleGetDetailList(id: string) {
-      userStoreHook.roles === "GYM" ? handleGetDetaList_gym(id) : handleGetDetailList_gyt(id)
+      //  GYM,GYT,KLX 区分   查询详情列表
+      if (userStoreHook.roles === "GYM") {
+         handleGetDetaList_gym(id)
+      } else if (userStoreHook.roles === "GYT") {
+         handleGetDetailList_gyt(id)
+      } else if (userStoreHook.roles === "KLX") {
+         handleGetDetailList_klx(id)
+      }
    }
    function handlePage(type: "next" | "prev") {
       type === "next" ? pageNum.value++ : pageNum.value--
@@ -170,7 +258,22 @@ export const useDataDetailList = () => {
          }
       })
    }
-
+   // 获取酷乐秀
+   function handleGetDetailList_klx(id: string) {
+      loading.value = true
+      httpAjaxErrMsg(getLessonCoursewareCourseList_klx, id).then(res => {
+         loading.value = false
+         if (res.code === 200) {
+            const data = (res.data || []).map((item: any) => {
+               return {
+                  name: item.coursewareDetailName,
+                  id: item.coursewareDetailId
+               }
+            })
+            listData.value = chunkArray(chunkArray(data, 7), 2)
+         }
+      })
+   }
    return {
       handleGetDetailList,
       loading,

+ 6 - 2
src/views/coursewarePlay/components/courseCollapse/courseCollapse.vue

@@ -21,7 +21,7 @@
             <template v-if="item.materialList">
                <div
                   class="courseList"
-                  :class="{ isActive: activeCollapse?.id === i.id }"
+                  :class="{ isActive: activeCollapse?.id === i.id && activeCollapse?.knowledgePointId === i.knowledgePointId }"
                   v-for="i in item.materialList"
                   :key="i.id"
                   @click="handleClick(i)"
@@ -33,7 +33,10 @@
                      </div>
                   </div>
                   <div class="iconArrow">
-                     <img v-if="activeCollapse?.id === i.id" src="@/img/coursewarePlay/icon-load.gif" />
+                     <img
+                        v-if="activeCollapse?.id === i.id && activeCollapse?.knowledgePointId === i.knowledgePointId"
+                        src="@/img/coursewarePlay/icon-load.gif"
+                     />
                   </div>
                </div>
             </template>
@@ -52,6 +55,7 @@ type materialListType = {
    type: string
    typeCode?: string
    name: string
+   knowledgePointId: string
 }
 type courseListType = {
    id: string

+ 39 - 19
src/views/coursewarePlay/coursewarePlay.vue

@@ -79,7 +79,7 @@
          <div class="leftMenu">
             <img @click="handleGoBack" class="backImg" src="@/img/coursewarePlay/back.png" />
             <playRecordTime
-               v-if="route.query.modeId && coursewareTotalTime && !(userStoreHook.roles === 'GYM')"
+               v-if="route.query.modeId && coursewareTotalTime && userStoreHook.roles === 'GYT'"
                :modeId="route.query.modeId as string"
                :coursewareTotalTime="coursewareTotalTime"
             />
@@ -119,11 +119,11 @@
 
 <script setup lang="ts">
 import videoPlay from "./videoPlay"
-import { getLessonCourseDetail_gym, getLessonCoursewareDetail_gyt } from "@/api/cloudTextbooks.api"
+import { getLessonCourseDetail_gym, getLessonCoursewareDetail_gyt, getLessonCourseDetail_klx } from "@/api/cloudTextbooks.api"
 import { checkWebCourse_gyt } from "@/api/coursewarePlay.api"
 import { httpAjaxErrMsg, httpAjaxLoadingErrMsg } from "@/plugin/httpAjax"
 import userStore from "@/store/modules/user"
-import { useRoute } from "vue-router"
+import { useRoute, useRouter } from "vue-router"
 import { shallowRef, ref, computed, onUnmounted, onMounted, watch, nextTick } from "vue"
 import { ElMessageBox } from "element-plus"
 import courseCollapse from "./components/courseCollapse"
@@ -132,9 +132,11 @@ import playRecordTime from "./components/playRecordTime"
 import useDialogConfirm from "@/hooks/useDialogConfirm"
 import { getRecentCourseSchedule_gym } from "@/api/homePage.api"
 import { getToken } from "@/libs/auth"
-import { URL_TEACH_GYT, URL_TEACH_GYM } from "@/config"
+import { URL_TEACH_GYT, URL_TEACH_GYM, URL_TEACH_KLX } from "@/config"
+import { handleFullscreen } from "@/libs/fullscreen"
 
 const route = useRoute()
+const router = useRouter()
 const userStoreHook = userStore()
 // 批注
 const penShow = ref(false)
@@ -156,9 +158,15 @@ const songPlaySrc = computed<string>(() => {
    if (fileType.value !== "SONG") {
       return ""
    }
-   return userStoreHook.roles === "GYM"
-      ? `${URL_TEACH_GYM}?Authorization=${getToken()}&platform=web&liveConfig=1#/detail/${activeCourseware.value?.content}?isHideBack=true`
-      : `${URL_TEACH_GYT}?id=${activeCourseware.value?.content}&modelType=practice&modeType=json&Authorization=${getToken()}`
+   //  GYM,GYT,KLX 区分   云教练
+   const urlObj = {
+      GYT: `${URL_TEACH_GYT}?id=${activeCourseware.value?.content}&modelType=practice&modeType=json&Authorization=${getToken()}&isYjt=1`,
+      GYM: `${URL_TEACH_GYM}?Authorization=${getToken()}&platform=web&liveConfig=1#/detail/${
+         activeCourseware.value?.content
+      }?isHideBack=trueis&Yjt=1`,
+      KLX: `${URL_TEACH_KLX}??Authorization=${getToken()}&id=${activeCourseware.value?.content}&isHideBack=true&limitModel=practice&isYjt=1`
+   }
+   return urlObj[userStoreHook.roles!]
 })
 const activeCoursewareIndex = ref(0)
 const drawerShow = ref(false)
@@ -178,7 +186,13 @@ watch(activeCourseware, () => {
 })
 getCoursewareList()
 function getCoursewareList() {
-   httpAjaxErrMsg(userStoreHook.roles === "GYM" ? getLessonCourseDetail_gym : getLessonCoursewareDetail_gyt, route.params.id as string).then(res => {
+   //  GYM,GYT,KLX 区分   查询接口
+   const LessonCoursewareDetailApi = {
+      GYT: getLessonCoursewareDetail_gyt,
+      GYM: getLessonCourseDetail_gym,
+      KLX: getLessonCourseDetail_klx
+   }
+   httpAjaxErrMsg(LessonCoursewareDetailApi[userStoreHook.roles!], route.params.id as string).then(res => {
       if (res.code === 200) {
          const { lockFlag, knowledgePointList } = res.data || {}
          if (lockFlag) {
@@ -218,7 +232,7 @@ function handlePointList(pointList: any[]) {
    // 如果url里面有materialId 代表指定资料播放
    if (route.query.materialId) {
       const index = flattenCoursewareListData.findIndex((item: any) => {
-         return route.query.materialId === item.id + ""
+         return route.query.materialId === item.id + "" && route.query.knowledgePointId === item.knowledgePointId + ""
       })
       index > -1 && (activeCoursewareIndex.value = index)
    }
@@ -255,7 +269,7 @@ function handleChangeCourseware(index: -1 | 1) {
 }
 function handleCourseClick(value: any) {
    activeCoursewareIndex.value = flattenCoursewareList.value.findIndex((item: any) => {
-      return value.id === item.id
+      return value.id === item.id && value.knowledgePointId === item.knowledgePointId
    })
 }
 /* 播放器相关 */
@@ -282,6 +296,9 @@ function handlePlayVideo({ src, name }: { src: string; name: string }) {
    })
    showController()
 }
+
+// 全屏显示
+handleFullscreen(true, false)
 /* 按键事件相关 */
 onMounted(() => {
    document.addEventListener("keydown", handleKeydown)
@@ -335,8 +352,9 @@ function hideController() {
 }
 /* 结束课程 */
 function handleGoBack() {
-   window.open("about:blank", "_self")
-   window.close()
+   // window.open("about:blank", "_self")
+   // window.close()
+   router.go(-1)
 }
 function handleCoursewareEnd() {
    if (route.query.modeId) {
@@ -357,7 +375,7 @@ function handleCoursewareEnd() {
                }
             }
          })
-      } else {
+      } else if (userStoreHook.roles === "GYT") {
          httpAjaxLoadingErrMsg(checkWebCourse_gyt, route.query.modeId as string).then(res => {
             if (res.code === 200) {
                if (res.data?.signOut === false) {
@@ -383,14 +401,16 @@ function handleCoursewareEnd() {
 // 去练习
 const activeCoursewareResourceId = computed<string | undefined>(() => {
    const materialRefs = activeCourseware.value?.materialRefs
-   return materialRefs ? (userStoreHook.roles === "GYM" ? materialRefs[0]?.resourceIdStr : materialRefs[0]?.resourceId) : undefined
+   return materialRefs ? (["GYM", "KLX"].includes(userStoreHook.roles!) ? materialRefs[0]?.resourceIdStr : materialRefs[0]?.resourceId) : undefined
 })
 function handleGoPracticeBtn(activeCoursewareResourceId: string) {
-   const url =
-      userStoreHook.roles === "GYM"
-         ? `${URL_TEACH_GYM}?Authorization=${getToken()}&platform=web&liveConfig=1#/detail/${activeCoursewareResourceId}?isHideBack=true`
-         : `${URL_TEACH_GYT}?id=${activeCoursewareResourceId}&modelType=practice&modeType=json&Authorization=${getToken()}`
-   window.open(url, "_blank")
+   //  GYM,GYT,KLX 区分   云教练
+   const urlObj = {
+      GYT: `${URL_TEACH_GYT}?id=${activeCoursewareResourceId}&modelType=practice&modeType=json&Authorization=${getToken()}&isYjt=1`,
+      GYM: `${URL_TEACH_GYM}?Authorization=${getToken()}&platform=web&liveConfig=1#/detail/${activeCoursewareResourceId}?isYjt=1`,
+      KLX: `${URL_TEACH_KLX}??Authorization=${getToken()}&id=${activeCoursewareResourceId}&limitModel=practice&isYjt=1`
+   }
+   window.open(urlObj[userStoreHook.roles!], "_blank")
 }
 </script>
 

+ 16 - 5
src/views/curriculum/curriculumDetail.vue

@@ -192,14 +192,25 @@ function handleSetUpCourseware(id: string) {
    })
 }
 function handleCourseClick(item: any) {
-   const url = router.resolve({
+   // const url = router.resolve({
+   //    name: "coursewarePlay",
+   //    params: { id: curriculumDetailData.value.id },
+   //    query: {
+   //       materialId: item.id,
+   //       knowledgePointId: item.knowledgePointId
+   //    }
+   // }).href
+   // window.open(url, "_blank")
+   router.push({
       name: "coursewarePlay",
-      params: { id: curriculumDetailData.value.id },
+      params: {
+         id: curriculumDetailData.value.id
+      },
       query: {
-         materialId: item.id
+         materialId: item.id,
+         knowledgePointId: item.knowledgePointId
       }
-   }).href
-   window.open(url, "_blank")
+   })
 }
 </script>
 

+ 13 - 4
src/views/curriculum/hooks/useStartClass.ts

@@ -120,12 +120,21 @@ export function handleStartClass_gyt(id: string) {
 }
 
 function handlePaly(coursewareDetailId: string, coursewareId: string) {
-   const url = router.resolve({
+   // const url = router.resolve({
+   //    name: "coursewarePlay",
+   //    params: { id: coursewareDetailId },
+   //    query: {
+   //       modeId: coursewareId
+   //    }
+   // }).href
+   // window.open(url, "_blank")
+   router.push({
       name: "coursewarePlay",
-      params: { id: coursewareDetailId },
+      params: {
+         id: coursewareDetailId
+      },
       query: {
          modeId: coursewareId
       }
-   }).href
-   window.open(url, "_blank")
+   })
 }

+ 48 - 9
src/views/homePage/homePage.vue

@@ -8,12 +8,7 @@
       <div class="homePage">
          <ElScrollbar class="elScrollbar">
             <div class="classTypes">
-               <div
-                  class="classType"
-                  v-for="item in userStoreHook.roles === 'GYM' ? classTypes_gym : classTypes_gyt"
-                  :key="item.name"
-                  @click="handleRouter(item.url)"
-               >
+               <div class="classType" v-for="item in classTypeObj[userStoreHook.roles!]" :key="item.name" @click="handleRouter(item.url)">
                   <img :src="item.img" />
                   <div>{{ item.name }}</div>
                </div>
@@ -25,7 +20,7 @@
                         <div class="line"></div>
                         <div class="title">下次课程</div>
                      </div>
-                     <img class="rightRouter" @click="handleRouterCurriculum" src="@/img/homePage/back.png" />
+                     <img v-if="userStoreHook.roles !== 'KLX'" class="rightRouter" @click="handleRouterCurriculum" src="@/img/homePage/back.png" />
                   </div>
                   <el-skeleton class="elSkeleton" :loading="classDataLoading">
                      <template #template>
@@ -102,7 +97,7 @@ const classTypes_gym = [
    {
       img: require("@/img/homePage/yjl.png"),
       name: "云练习",
-      url: ""
+      url: "/cloudPractice"
    },
    {
       img: require("@/img/homePage/yjc.png"),
@@ -134,14 +129,51 @@ const classTypes_gyt = [
    {
       img: require("@/img/homePage/yjl.png"),
       name: "云练习",
+      url: "/cloudPractice"
+   },
+   {
+      img: require("@/img/homePage/yjc.png"),
+      name: "云课堂",
+      url: "/cloudTextbooks"
+   }
+]
+const classTypes_klx = [
+   {
+      img: require("@/img/homePage/kb.png"),
+      name: "课表",
+      url: ""
+   },
+   {
+      img: require("@/img/homePage/xl.png"),
+      name: "训练",
+      url: ""
+   },
+   {
+      img: require("@/img/homePage/jdcp.png"),
+      name: "进度测评",
+      url: ""
+   },
+   {
+      img: require("@/img/homePage/xltj.png"),
+      name: "训练统计",
       url: ""
    },
    {
+      img: require("@/img/homePage/yjl.png"),
+      name: "云练习",
+      url: "/cloudPractice"
+   },
+   {
       img: require("@/img/homePage/yjc.png"),
       name: "云课堂",
       url: "/cloudTextbooks"
    }
 ]
+const classTypeObj = {
+   GYT: classTypes_gyt,
+   GYM: classTypes_gym,
+   KLX: classTypes_klx
+}
 
 /* 下次课程信息 */
 const classData = ref<Record<string, any>>({})
@@ -149,7 +181,14 @@ const classDataLoading = ref(true)
 const isEmptyClassData = computed(() => {
    return !Object.keys(classData.value).length
 })
-handleClassData()
+
+// 现在酷乐秀没有课表功能
+if (userStoreHook.roles !== "KLX") {
+   handleClassData()
+} else {
+   classDataLoading.value = false
+}
+
 function handleClassData() {
    if (userStoreHook.roles === "GYM") {
       classDataLoading.value = true

+ 21 - 3
src/viewsframe/layout/layout.vue

@@ -32,6 +32,15 @@
                </div>
             </template>
          </el-dropdown>
+         <img
+            @click="
+               () => {
+                  handleFullscreen()
+               }
+            "
+            class="fullscreen"
+            :src="isFullscreen ? require('@/img/layout/fullscreenClose.png') : require('@/img/layout/fullscreen.png')"
+         />
       </div>
       <RouterView />
    </div>
@@ -39,9 +48,9 @@
 
 <script setup lang="ts">
 import userStore from "@/store/modules/user"
-import { computed } from "vue"
-import { ref } from "vue"
+import { computed, ref } from "vue"
 import { ElMessage } from "element-plus"
+import { isFullscreen, handleFullscreen } from "@/libs/fullscreen"
 
 const userStoreHook = userStore()
 
@@ -73,7 +82,7 @@ function handleLogout() {
       padding-top: 26px;
       padding-right: 54px;
       display: flex;
-      flex-direction: row-reverse;
+      justify-content: flex-end;
       .avatar-con {
          cursor: pointer;
          padding-left: 30px;
@@ -120,6 +129,15 @@ function handleLogout() {
             }
          }
       }
+      .fullscreen {
+         margin-left: 17px;
+         width: 45px;
+         height: 45px;
+         cursor: pointer;
+         &:active {
+            opacity: $opacity-hover;
+         }
+      }
    }
 }
 .dropdown {

+ 9 - 3
src/viewsframe/login/login.vue

@@ -40,7 +40,7 @@ import { ref, computed } from "vue"
 import userStore from "@/store/modules/user"
 import { useRouter } from "vue-router"
 import useSecureAnth from "@/hooks/useSecureAnth"
-import { mutualTLSQuery_gym, mutualTLSQuery_gyt } from "@/api/user.api"
+import { mutualTLSQuery_gym, mutualTLSQuery_gyt, mutualTLSQuery_klx } from "@/api/user.api"
 import { httpAjaxLoading } from "@/plugin/httpAjax"
 
 const userStoreHook = userStore()
@@ -69,8 +69,14 @@ function handleQrcodeStatus() {
          if (status === "FINISH") {
             // 登录成功
             userStoreHook.login(res.data).then(() => {
-               // 安全证书
-               httpAjaxLoading(res.data.appKey === "GYM" ? mutualTLSQuery_gym : mutualTLSQuery_gyt).then(res => {
+               const appKey = res.data.appKey as rolesType
+               // GYM,GYT,KLX 区分  安全证书
+               const mutualTLSQueryApi = {
+                  GYT: mutualTLSQuery_gyt,
+                  GYM: mutualTLSQuery_gym,
+                  KLX: mutualTLSQuery_klx
+               }
+               httpAjaxLoading(mutualTLSQueryApi[appKey]).then(res => {
                   if (res.code === 511) {
                      useSecureAnth({
                         onCancel() {

+ 6 - 0
vue.config.js

@@ -43,6 +43,12 @@ module.exports = defineConfig({
             pathRewrite: {
                "^/gyt": ""
             }
+         },
+         "/klx": {
+            target: "https://dev.colexiu.com",
+            pathRewrite: {
+               "^/klx": ""
+            }
          }
       }
    }

+ 1 - 1
yarn.lock

@@ -5960,7 +5960,7 @@ schema-utils@^4.0.0:
 
 screenfull@^6.0.2:
   version "6.0.2"
-  resolved "https://registry.npmjs.org/screenfull/-/screenfull-6.0.2.tgz"
+  resolved "https://registry.npmmirror.com/screenfull/-/screenfull-6.0.2.tgz#3dbe4b8c4f8f49fb8e33caa8f69d0bca730ab238"
   integrity sha512-AQdy8s4WhNvUZ6P8F6PB21tSPIYKniic+Ogx0AacBMjKP1GUHN2E9URxQHtCusiwxudnCKkdy4GrHXPPJSkCCw==
 
 seemly@^0.3.6, seemly@^0.3.8:

Some files were not shown because too many files changed in this diff