Browse Source

Merge branch 'hqyNew' of http://git.dayaedu.com/huangqiyong/pptList into test-online

黄琪勇 1 month ago
parent
commit
45eb8b57fc
68 changed files with 2109 additions and 16 deletions
  1. 3 0
      .env.devProd
  2. 3 0
      .env.development
  3. 4 1
      .env.production
  4. 3 0
      .env.staging
  5. 5 0
      dist/assets/index-2ozVxlgV.js
  6. 8 0
      dist/assets/index-6ldn4hSR.js
  7. 4 0
      dist/assets/index-BGEVwK9d.js
  8. 1 0
      dist/assets/index-BIfT6hvl.js
  9. 5 0
      dist/assets/index-BQlPHm6c.js
  10. 5 0
      dist/assets/index-CN48yuxX.js
  11. 9 0
      dist/assets/index-CT4f4PW9.js
  12. 4 0
      dist/assets/index-CbFFYSh5.js
  13. 0 0
      dist/assets/index-CiwqWoI3.js
  14. 1 0
      dist/assets/index-CtXERsYB.js
  15. 1 0
      dist/assets/index-DJCWf0z5.js
  16. 1 0
      dist/assets/index-DKmzOQH8.js
  17. 5 0
      dist/assets/index-DuZmFDUP.js
  18. 4 0
      dist/assets/shapes-ClFFFtPX.js
  19. 9 0
      dist/assets/shapes-GaKprgT0.js
  20. 4 0
      dist/index.html
  21. 48 0
      src/api/musicResources.ts
  22. 1 1
      src/api/pptOperate.ts
  23. 2 0
      src/components/Popover.vue
  24. 2 0
      src/config/index.ts
  25. 23 1
      src/hooks/useCreateElement.ts
  26. 21 0
      src/libs/cipher.ts
  27. 4 0
      src/libs/jsonTool.ts
  28. 4 0
      src/messageHooks/mobileScreen.ts
  29. 26 1
      src/types/slides.ts
  30. 3 1
      src/views/Editor/Canvas/EditableElement.vue
  31. 2 2
      src/views/Editor/Canvas/hooks/useDragElement.ts
  32. BIN
      src/views/Editor/CanvasTool/imgs/mqjs.png
  33. BIN
      src/views/Editor/CanvasTool/imgs/ylzs.png
  34. BIN
      src/views/Editor/CanvasTool/imgs/yqbk.png
  35. BIN
      src/views/Editor/CanvasTool/imgs/yyj.png
  36. 114 1
      src/views/Editor/CanvasTool/index.vue
  37. 2 2
      src/views/Editor/Toolbar/ElementPositionPanel.vue
  38. 10 0
      src/views/Editor/Toolbar/ElementStylePanel/MusicResourcesStylePanel.vue
  39. 3 1
      src/views/Editor/Toolbar/ElementStylePanel/index.vue
  40. 1 1
      src/views/Editor/Toolbar/index.vue
  41. 3 1
      src/views/Mobile/MobileEditor/MobileEditableElement.vue
  42. 9 0
      src/views/Screen/BaseView.vue
  43. 3 1
      src/views/Screen/ScreenElement.vue
  44. 3 1
      src/views/components/ThumbnailSlide/ThumbnailElement.vue
  45. 1 1
      src/views/components/element/enjoyElement/resourcesList/resourcesList.vue
  46. 62 0
      src/views/components/element/musicResourcesElement/BaseMusicResourcesElement.vue
  47. 58 0
      src/views/components/element/musicResourcesElement/ScreenMusicResourcesElement.vue
  48. 29 0
      src/views/components/element/musicResourcesElement/index.ts
  49. 122 0
      src/views/components/element/musicResourcesElement/musicResourcesElement.vue
  50. 94 0
      src/views/components/element/musicResourcesElement/musicResourcesList/components/instrumentList.vue
  51. 90 0
      src/views/components/element/musicResourcesElement/musicResourcesList/components/musicList.vue
  52. 94 0
      src/views/components/element/musicResourcesElement/musicResourcesList/components/musicianList.vue
  53. BIN
      src/views/components/element/musicResourcesElement/musicResourcesList/imgs/btnLeft.png
  54. BIN
      src/views/components/element/musicResourcesElement/musicResourcesList/imgs/btnRight..png
  55. BIN
      src/views/components/element/musicResourcesElement/musicResourcesList/imgs/icon_default.png
  56. BIN
      src/views/components/element/musicResourcesElement/musicResourcesList/imgs/musicBg.png
  57. 2 0
      src/views/components/element/musicResourcesElement/musicResourcesList/index.ts
  58. 82 0
      src/views/components/element/musicResourcesElement/musicResourcesList/musicPreview.vue
  59. 654 0
      src/views/components/element/musicResourcesElement/musicResourcesList/musicResourcesList.vue
  60. 2 0
      src/views/components/element/musicResourcesElement/musicResourcesPlayer/index.ts
  61. 107 0
      src/views/components/element/musicResourcesElement/musicResourcesPlayer/musicResourcesPlayer.vue
  62. 168 0
      src/views/components/element/musicResourcesElement/musicTheoryList/courseCollapse.vue
  63. BIN
      src/views/components/element/musicResourcesElement/musicTheoryList/imgs/actJ.png
  64. BIN
      src/views/components/element/musicResourcesElement/musicTheoryList/imgs/actList.png
  65. BIN
      src/views/components/element/musicResourcesElement/musicTheoryList/imgs/j.png
  66. BIN
      src/views/components/element/musicResourcesElement/musicTheoryList/imgs/list.png
  67. 2 0
      src/views/components/element/musicResourcesElement/musicTheoryList/index.ts
  68. 179 0
      src/views/components/element/musicResourcesElement/musicTheoryList/musicTheoryList.vue

+ 3 - 0
.env.devProd

@@ -6,3 +6,6 @@ VITE_YJL_URL = "https://test.kt.colexiu.com/instrument"
 
 ## 移动端app地址
 VITE_CLASSAPP_URL = "https://test.kt.colexiu.com/classroom-app"
+
+## 老师端地址
+VITE_CLASSROOM_URL = "https://test.kt.colexiu.com/classroom"

+ 3 - 0
.env.development

@@ -6,3 +6,6 @@ VITE_YJL_URL = "https://test.kt.colexiu.com/instrument"
 
 ## 移动端app地址
 VITE_CLASSAPP_URL = "https://test.kt.colexiu.com/classroom-app"
+
+## 老师端地址
+VITE_CLASSROOM_URL = "https://test.kt.colexiu.com/classroom"

+ 4 - 1
.env.production

@@ -5,4 +5,7 @@ VITE_APP_URL = "https://kt.colexiu.com"
 VITE_YJL_URL = "https://mec.colexiu.com/instrument"
 
 ## 移动端app地址
-VITE_CLASSAPP_URL = "https://mec.colexiu.com/classroom-app"
+VITE_CLASSAPP_URL = "https://kt.colexiu.com/classroom-app"
+
+## 老师端地址
+VITE_CLASSROOM_URL = "https://kt.colexiu.com/classroom"

+ 3 - 0
.env.staging

@@ -6,3 +6,6 @@ VITE_YJL_URL = "https://test.kt.colexiu.com/instrument"
 
 ## 移动端app地址
 VITE_CLASSAPP_URL = "https://test.kt.colexiu.com/classroom-app"
+
+## 老师端地址
+VITE_CLASSROOM_URL = "https://test.kt.colexiu.com/classroom"

+ 5 - 0
dist/assets/index-2ozVxlgV.js

@@ -0,0 +1,5 @@
+<<<<<<<< HEAD:dist/assets/index-CbFFYSh5.js
+import{_ as t}from"./404-R0y5dkvZ.js";import{E as c}from"./index-BIfT6hvl.js";import{a5 as n,ae as i,af as r,ak as o,aq as _,am as d,P as l,bo as p,bH as m,bI as f,bG as u}from"./index-BQlPHm6c.js";const s=a=>(m("data-v-340bbdaa"),a=a(),f(),a),b={class:"login"},h={class:"error"},g=s(()=>o("img",{src:t,class:"img",alt:""},null,-1)),k=s(()=>o("div",{class:"tit"},"登录已过期或服务器错误!",-1)),v=n({__name:"login",setup(a){function e(){window.close()}return(x,B)=>(i(),r("div",b,[o("div",h,[g,k,_(l(c),{class:"backBtn",type:"primary",plain:"",onClick:e},{default:d(()=>[p("关闭页面")]),_:1})])]))}}),E=u(v,[["__scopeId","data-v-340bbdaa"]]);export{E as default};
+========
+import{_ as t}from"./404-R0y5dkvZ.js";import{E as c}from"./index-DJCWf0z5.js";import{a5 as n,ae as i,af as r,ak as o,aq as _,am as d,P as l,bo as p,bH as m,bI as f,bG as u}from"./index-CT4f4PW9.js";const s=a=>(m("data-v-340bbdaa"),a=a(),f(),a),b={class:"login"},h={class:"error"},g=s(()=>o("img",{src:t,class:"img",alt:""},null,-1)),k=s(()=>o("div",{class:"tit"},"登录已过期或服务器错误!",-1)),v=n({__name:"login",setup(a){function e(){window.close()}return(x,B)=>(i(),r("div",b,[o("div",h,[g,k,_(l(c),{class:"backBtn",type:"primary",plain:"",onClick:e},{default:d(()=>[p("关闭页面")]),_:1})])]))}}),E=u(v,[["__scopeId","data-v-340bbdaa"]]);export{E as default};
+>>>>>>>> f584aafe10cd562928d7301ac09e3afbcfa866c0:dist/assets/index-2ozVxlgV.js

File diff suppressed because it is too large
+ 8 - 0
dist/assets/index-6ldn4hSR.js


+ 4 - 0
dist/assets/index-BGEVwK9d.js

@@ -1 +1,5 @@
+<<<<<<<< HEAD:dist/assets/index-BGEVwK9d.js
 import{_ as a}from"./404-R0y5dkvZ.js";import{a5 as o,em as t,ae as r,af as c,bH as _,bI as d,ak as s,bG as p}from"./index-BQlPHm6c.js";const i=e=>(_("data-v-dda25179"),e=e(),d(),e),n={class:"errorPage"},m=i(()=>s("div",{class:"error"},[s("img",{src:a,class:"img",alt:""}),s("div",{class:"tit"},"页面找不到了~")],-1)),l=[m],f=o({__name:"errorPage",setup(e){return t(),(u,h)=>(r(),c("div",n,l))}}),I=p(f,[["__scopeId","data-v-dda25179"]]);export{I as default};
+========
+import{_ as a}from"./404-R0y5dkvZ.js";import{a5 as o,em as t,ae as r,af as c,bH as _,bI as d,ak as s,bG as p}from"./index-CT4f4PW9.js";const i=e=>(_("data-v-dda25179"),e=e(),d(),e),n={class:"errorPage"},m=i(()=>s("div",{class:"error"},[s("img",{src:a,class:"img",alt:""}),s("div",{class:"tit"},"页面找不到了~")],-1)),l=[m],f=o({__name:"errorPage",setup(e){return t(),(u,h)=>(r(),c("div",n,l))}}),I=p(f,[["__scopeId","data-v-dda25179"]]);export{I as default};
+>>>>>>>> f584aafe10cd562928d7301ac09e3afbcfa866c0:dist/assets/index-CN48yuxX.js

File diff suppressed because it is too large
+ 1 - 0
dist/assets/index-BIfT6hvl.js


File diff suppressed because it is too large
+ 5 - 0
dist/assets/index-BQlPHm6c.js


+ 5 - 0
dist/assets/index-CN48yuxX.js

@@ -0,0 +1,5 @@
+<<<<<<<< HEAD:dist/assets/index-BGEVwK9d.js
+import{_ as a}from"./404-R0y5dkvZ.js";import{a5 as o,em as t,ae as r,af as c,bH as _,bI as d,ak as s,bG as p}from"./index-BQlPHm6c.js";const i=e=>(_("data-v-dda25179"),e=e(),d(),e),n={class:"errorPage"},m=i(()=>s("div",{class:"error"},[s("img",{src:a,class:"img",alt:""}),s("div",{class:"tit"},"页面找不到了~")],-1)),l=[m],f=o({__name:"errorPage",setup(e){return t(),(u,h)=>(r(),c("div",n,l))}}),I=p(f,[["__scopeId","data-v-dda25179"]]);export{I as default};
+========
+import{_ as a}from"./404-R0y5dkvZ.js";import{a5 as o,em as t,ae as r,af as c,bH as _,bI as d,ak as s,bG as p}from"./index-CT4f4PW9.js";const i=e=>(_("data-v-dda25179"),e=e(),d(),e),n={class:"errorPage"},m=i(()=>s("div",{class:"error"},[s("img",{src:a,class:"img",alt:""}),s("div",{class:"tit"},"页面找不到了~")],-1)),l=[m],f=o({__name:"errorPage",setup(e){return t(),(u,h)=>(r(),c("div",n,l))}}),I=p(f,[["__scopeId","data-v-dda25179"]]);export{I as default};
+>>>>>>>> f584aafe10cd562928d7301ac09e3afbcfa866c0:dist/assets/index-CN48yuxX.js

File diff suppressed because it is too large
+ 9 - 0
dist/assets/index-CT4f4PW9.js


+ 4 - 0
dist/assets/index-CbFFYSh5.js

@@ -1 +1,5 @@
+<<<<<<<< HEAD:dist/assets/index-CbFFYSh5.js
 import{_ as t}from"./404-R0y5dkvZ.js";import{E as c}from"./index-BIfT6hvl.js";import{a5 as n,ae as i,af as r,ak as o,aq as _,am as d,P as l,bo as p,bH as m,bI as f,bG as u}from"./index-BQlPHm6c.js";const s=a=>(m("data-v-340bbdaa"),a=a(),f(),a),b={class:"login"},h={class:"error"},g=s(()=>o("img",{src:t,class:"img",alt:""},null,-1)),k=s(()=>o("div",{class:"tit"},"登录已过期或服务器错误!",-1)),v=n({__name:"login",setup(a){function e(){window.close()}return(x,B)=>(i(),r("div",b,[o("div",h,[g,k,_(l(c),{class:"backBtn",type:"primary",plain:"",onClick:e},{default:d(()=>[p("关闭页面")]),_:1})])]))}}),E=u(v,[["__scopeId","data-v-340bbdaa"]]);export{E as default};
+========
+import{_ as t}from"./404-R0y5dkvZ.js";import{E as c}from"./index-DJCWf0z5.js";import{a5 as n,ae as i,af as r,ak as o,aq as _,am as d,P as l,bo as p,bH as m,bI as f,bG as u}from"./index-CT4f4PW9.js";const s=a=>(m("data-v-340bbdaa"),a=a(),f(),a),b={class:"login"},h={class:"error"},g=s(()=>o("img",{src:t,class:"img",alt:""},null,-1)),k=s(()=>o("div",{class:"tit"},"登录已过期或服务器错误!",-1)),v=n({__name:"login",setup(a){function e(){window.close()}return(x,B)=>(i(),r("div",b,[o("div",h,[g,k,_(l(c),{class:"backBtn",type:"primary",plain:"",onClick:e},{default:d(()=>[p("关闭页面")]),_:1})])]))}}),E=u(v,[["__scopeId","data-v-340bbdaa"]]);export{E as default};
+>>>>>>>> f584aafe10cd562928d7301ac09e3afbcfa866c0:dist/assets/index-2ozVxlgV.js

File diff suppressed because it is too large
+ 0 - 0
dist/assets/index-CiwqWoI3.js


+ 1 - 0
dist/assets/index-CtXERsYB.js

@@ -0,0 +1 @@
+import{u as s,ad as r}from"./index-6ldn4hSR.js";import{a5 as c,cx as n,R as p,ae as t,af as _,al as u,ag as i,bG as d}from"./index-CT4f4PW9.js";const l={class:"pptScreen"},m=c({__name:"pptScreen",setup(f){const a=n(),e=p(!0),o=s();return a.setScreenMode("pptScreen"),o.initPPTData().then(()=>{e.value=!1}),(S,k)=>(t(),_("div",l,[e.value?i("",!0):(t(),u(r,{key:0}))]))}}),P=d(m,[["__scopeId","data-v-678905e9"]]);export{P as default};

File diff suppressed because it is too large
+ 1 - 0
dist/assets/index-DJCWf0z5.js


+ 1 - 0
dist/assets/index-DKmzOQH8.js

@@ -0,0 +1 @@
+import{u as c,ad as l}from"./index-6ldn4hSR.js";import{w as p,bz as d,a5 as g,cx as m,R as t,ae as r,af as u,al as f,ag as w,bG as k}from"./index-CT4f4PW9.js";import"./shapes-GaKprgT0.js";const S=()=>{function a(o){const{api:e,playState:s}=o.data||{};e==="headerTogge"?window.parent.postMessage({api:"headerTogge",playState:s},"*"):e==="changeTogge"?window.parent.postMessage({api:"changeTogge"},"*"):["clickTempo","clickViewFigner"].includes(e)&&window.parent.postMessage({api:e},"*")}p(()=>{window.addEventListener("message",a)}),d(()=>{window.removeEventListener("message",a)})},_=g({__name:"mobileScreen",setup(a){S();const o=m(),e=t(!0),s=t(!1),i=c();return o.setScreenMode("mobileScreen"),i.initPPTData().then(()=>{e.value=!1}),(v,n)=>(r(),u("div",{class:"mobileScreen",onClick:n[0]||(n[0]=b=>s.value=!1)},[e.value?w("",!0):(r(),f(l,{key:0}))]))}}),x=k(_,[["__scopeId","data-v-90b2692a"]]);export{x as default};

File diff suppressed because it is too large
+ 5 - 0
dist/assets/index-DuZmFDUP.js


+ 4 - 0
dist/assets/shapes-ClFFFtPX.js

@@ -1,4 +1,8 @@
+<<<<<<<< HEAD:dist/assets/shapes-ClFFFtPX.js
 import{cA as O,bA as j1,cB as h2}from"./index-BQlPHm6c.js";import{ae as W}from"./index-DuZmFDUP.js";var Y1={exports:{}};/*!
+========
+import{cA as O,bA as j1,cB as h2}from"./index-CT4f4PW9.js";import{ae as W}from"./index-6ldn4hSR.js";var Y1={exports:{}};/*!
+>>>>>>>> f584aafe10cd562928d7301ac09e3afbcfa866c0:dist/assets/shapes-GaKprgT0.js
  * clipboard.js v2.0.11
  * https://clipboardjs.com/
  *

File diff suppressed because it is too large
+ 9 - 0
dist/assets/shapes-GaKprgT0.js


+ 4 - 0
dist/index.html

@@ -10,7 +10,11 @@
     <meta name="description" content="ppt编辑器" />
     <meta name="keywords" content="ppt,powerpoint,office powerpoint,在线ppt,幻灯片,演示文稿,ppt在线制作,Vue3,TypeScript" />
     <title>PPT</title>
+<<<<<<< HEAD
     <script type="module" crossorigin src="./assets/index-BQlPHm6c.js"></script>
+=======
+    <script type="module" crossorigin src="./assets/index-CT4f4PW9.js"></script>
+>>>>>>> f584aafe10cd562928d7301ac09e3afbcfa866c0
     <link rel="stylesheet" crossorigin href="./assets/index-C0x2lxv7.css">
   </head>
   <body>

+ 48 - 0
src/api/musicResources.ts

@@ -0,0 +1,48 @@
+import { httpAxios } from "@/api/ApiInstance"
+import { type pptContentType } from "@/views/components/element/musicResourcesElement"
+
+/**
+ * 获取乐理知识 旁边的列表
+ */
+
+export const getListKnowledge = () => {
+  return httpAxios.axioseRquest({
+    method: "post",
+    url: "/edu-app/lessonCoursewareDetail/listKnowledge",
+    data: {
+      type: "COURSEWARE"
+    }
+  })
+}
+
+/**
+ * 获取查询配置
+ */
+
+export const getKnowledgeWikiCategoryType = (type: Exclude<pptContentType, "THEORY">) => {
+  return httpAxios.axioseRquest({
+    method: "post",
+    url: "/edu-app/knowledgeWikiCategoryType/page",
+    data: {
+      page: 1,
+      rows: 99,
+      type
+    }
+  })
+}
+
+/**
+ * 获取查询配置
+ */
+
+export const getKnowledgeWikiPage = (
+  data: { page: number; rows: number; type: string; wikiCategoryId: string },
+  abortController: AbortController
+) => {
+  return httpAxios.axioseRquest({
+    signal: abortController.signal,
+    method: "post",
+    url: "/edu-app/knowledgeWiki/page",
+    data
+  })
+}

+ 1 - 1
src/api/pptOperate.ts

@@ -1,5 +1,5 @@
 import { httpAxios } from "@/api/ApiInstance"
-import { type queryParamsType } from "@/queryParams/"
+import { type queryParamsType } from "@/queryParams"
 
 //根据id获取课程信息
 export const getTeacherChapterKnowledgeMaterial = (id: string, fromType: queryParamsType["fromType"]) => {

+ 2 - 0
src/components/Popover.vue

@@ -102,6 +102,8 @@ onMounted(() => {
   font-size: 14px;
   color: #333333;
   line-height: 20px;
+  max-height: 400px;
+  overflow-y: auto;
 }
 </style>
 

+ 2 - 0
src/config/index.ts

@@ -3,3 +3,5 @@ export const URL_API = import.meta.env.VITE_APP_URL as string
 export const YJL_URL_API = import.meta.env.VITE_YJL_URL as string
 
 export const CLASSAPP_URL_API = import.meta.env.VITE_CLASSAPP_URL as string
+
+export const CLASSROOM_URL_API = import.meta.env.VITE_CLASSROOM_URL as string

+ 23 - 1
src/hooks/useCreateElement.ts

@@ -7,6 +7,7 @@ import { type ShapePoolItem, SHAPE_PATH_FORMULAS } from "@/configs/shapes"
 import type { LinePoolItem } from "@/configs/lines"
 import { CHART_DEFAULT_DATA } from "@/configs/chart"
 import useHistorySnapshot from "@/hooks/useHistorySnapshot"
+import { type pptContentType } from "@/views/components/element/musicResourcesElement"
 
 interface CommonElementPosition {
   top: number
@@ -396,6 +397,26 @@ export default () => {
       top: 0
     })
   }
+
+  /**
+   * 音乐资源
+   */
+  const createMusicResourcesElement = (sid: string, name: string, sType: pptContentType) => {
+    createElement({
+      type: "elf",
+      subtype: "elf-music-resources",
+      sid,
+      name,
+      sType,
+      id: nanoid(10),
+      width: viewportSize.value,
+      height: viewportSize.value * viewportRatio.value,
+      rotate: 0,
+      left: 0,
+      top: 0
+    })
+  }
+
   return {
     createImageElement,
     createChartElement,
@@ -409,6 +430,7 @@ export default () => {
     createCloudCoachElement,
     createEnjoyElement,
     createListeningPracticeElement,
-    createRhythmPracticeElement
+    createRhythmPracticeElement,
+    createMusicResourcesElement
   }
 }

+ 21 - 0
src/libs/cipher.ts

@@ -0,0 +1,21 @@
+import { encrypt, decrypt } from "crypto-js/aes"
+import { parse } from "crypto-js/enc-utf8"
+import pkcs7 from "crypto-js/pad-pkcs7"
+import ECB from "crypto-js/mode-ecb"
+import UTF8 from "crypto-js/enc-utf8"
+// 注意 key 和 iv 至少都需要 16 位
+const AES_KEY = "1111111111000000"
+const AES_IV = "0000001111111111"
+
+/**
+ *
+ * @description 加密:反序列化字符串参数
+ */
+
+export function stringifyQuery(query: string) {
+  return `?${encrypt(query, parse(AES_KEY), {
+    mode: ECB,
+    padding: pkcs7,
+    iv: parse(AES_IV)
+  }).toString()}`
+}

+ 4 - 0
src/libs/jsonTool.ts

@@ -84,6 +84,10 @@ function formatSlides(slides: any[]): any[] {
         }
       }
     })
+    // 兼容动画 妙极课动画没有effect属性  先把妙极客的动画去掉  之后做兼容
+    if (item.animations) {
+      item.animations = (item.animations || []).filter((item: Record<string, any>) => item.effect)
+    }
     return item
   })
 }

+ 4 - 0
src/messageHooks/mobileScreen.ts

@@ -3,6 +3,10 @@ import { onMounted, onUnmounted } from "vue"
 
 export const changeToggeMes = () => {
   function handleMessage(event: any) {
+    // 当这个页面不是 iframe包裹的时候  会循环触发headerTogge  因为window.parent === window 相等 所以排除掉这个
+    if (window.parent === window) {
+      return
+    }
     const { api, playState } = event.data || {}
     if (api === "headerTogge") {
       window.parent.postMessage(

+ 26 - 1
src/types/slides.ts

@@ -37,7 +37,8 @@ export const enum ElementSubtypeTypes {
   SING_PLAY = "elf-sing-play",
   ENJOY = "elf-enjoy",
   LISTENING_PRACTICE = "elf-listening-practice",
-  RHYTHM_PRACTICE = "elf-rhythm-practice"
+  RHYTHM_PRACTICE = "elf-rhythm-practice",
+  MUSIC_RESOURCES = "elf-music-resources"
 }
 
 /**
@@ -701,6 +702,29 @@ export interface PPTRhythmPracticeElement extends PPTBaseElement {
   dataJson: string
 }
 
+/**
+ * 音乐知识
+ *
+ * type: elf
+ *
+ * subtype: elf-music-resources
+ *
+ * sid: 资源id
+ *
+ * name: 资源名称
+ *
+ * sType: 资源类型
+ *
+ */
+import { type pptContentType } from "@/views/components/element/musicResourcesElement"
+export interface PPTMusicResourcesElement extends PPTBaseElement {
+  type: "elf"
+  subtype: "elf-music-resources"
+  sid: string
+  name: string
+  sType: pptContentType
+}
+
 export type PPTElement =
   | PPTTextElement
   | PPTImageElement
@@ -715,6 +739,7 @@ export type PPTElement =
   | PPTEnjoyElement
   | PPTListeningPracticeElement
   | PPTRhythmPracticeElement
+  | PPTMusicResourcesElement
 
 export type AnimationType = "in" | "out" | "attention"
 export type AnimationTrigger = "click" | "meantime" | "auto"

+ 3 - 1
src/views/Editor/Canvas/EditableElement.vue

@@ -39,6 +39,7 @@ import cloudCoachElement from "@/views/components/element/cloudCoachElement"
 import enjoyElement from "@/views/components/element/enjoyElement"
 import listeningPracticeElement from "@/views/components/element/listeningPracticeElement"
 import rhythmPracticeElement from "@/views/components/element/rhythmPracticeElement"
+import musicResourcesElement from "@/views/components/element/musicResourcesElement"
 
 const props = defineProps<{
   elementInfo: PPTElement
@@ -65,7 +66,8 @@ const currentElementComponent = computed<unknown>(() => {
     [ElementSubtypeTypes.SING_PLAY]: cloudCoachElement,
     [ElementSubtypeTypes.ENJOY]: enjoyElement,
     [ElementSubtypeTypes.LISTENING_PRACTICE]: listeningPracticeElement,
-    [ElementSubtypeTypes.RHYTHM_PRACTICE]: rhythmPracticeElement
+    [ElementSubtypeTypes.RHYTHM_PRACTICE]: rhythmPracticeElement,
+    [ElementSubtypeTypes.MUSIC_RESOURCES]: musicResourcesElement
   }
   return elementTypeMap[props.elementInfo.type] || elementSubtypeMap[props.elementInfo.subtype] || null
 })

+ 2 - 2
src/views/Editor/Canvas/hooks/useDragElement.ts

@@ -25,7 +25,7 @@ export default (elementList: Ref<PPTElement[]>, alignmentLines: Ref<AlignmentLin
       if (
         activeElementIdList.value.includes(item.id) &&
         item.type === "elf" &&
-        ["elf-sing-play", "elf-listening-practice", "elf-rhythm-practice"].includes(item.subtype)
+        ["elf-sing-play", "elf-listening-practice", "elf-rhythm-practice", "elf-music-resources"].includes(item.subtype)
       ) {
         item.isMove = true
       }
@@ -298,7 +298,7 @@ export default (elementList: Ref<PPTElement[]>, alignmentLines: Ref<AlignmentLin
         if (
           activeElementIdList.value.includes(item.id) &&
           item.type === "elf" &&
-          ["elf-sing-play", "elf-listening-practice", "elf-rhythm-practice"].includes(item.subtype)
+          ["elf-sing-play", "elf-listening-practice", "elf-rhythm-practice", "elf-music-resources"].includes(item.subtype)
         ) {
           item.isMove = false
         }

BIN
src/views/Editor/CanvasTool/imgs/mqjs.png


BIN
src/views/Editor/CanvasTool/imgs/ylzs.png


BIN
src/views/Editor/CanvasTool/imgs/yqbk.png


BIN
src/views/Editor/CanvasTool/imgs/yyj.png


+ 114 - 1
src/views/Editor/CanvasTool/index.vue

@@ -91,6 +91,61 @@
                 <div class="tit">节奏练习</div>
               </div>
             </PopoverMenuItem>
+            <PopoverMenuItem
+              @click="
+                () => {
+                  expandedKnowledgeVisible = false
+                  musicResourcesVisible = true
+                  musicResourcesType = 'INSTRUMENT'
+                }
+              "
+            >
+              <div class="menuItem">
+                <img src="./imgs/yqbk.png" alt="" />
+                <div class="tit">乐器百科</div>
+              </div>
+            </PopoverMenuItem>
+            <PopoverMenuItem
+              @click="
+                () => {
+                  expandedKnowledgeVisible = false
+                  musicResourcesVisible = true
+                  musicResourcesType = 'MUSIC'
+                }
+              "
+            >
+              <div class="menuItem">
+                <img src="./imgs/mqjs.png" alt="" />
+                <div class="tit">名曲鉴赏</div>
+              </div>
+            </PopoverMenuItem>
+            <PopoverMenuItem
+              @click="
+                () => {
+                  expandedKnowledgeVisible = false
+                  musicResourcesVisible = true
+                  musicResourcesType = 'MUSICIAN'
+                }
+              "
+            >
+              <div class="menuItem">
+                <img src="./imgs/yyj.png" alt="" />
+                <div class="tit">音乐家</div>
+              </div>
+            </PopoverMenuItem>
+            <PopoverMenuItem
+              @click="
+                () => {
+                  expandedKnowledgeVisible = false
+                  musicTheoryVisible = true
+                }
+              "
+            >
+              <div class="menuItem">
+                <img src="./imgs/ylzs.png" alt="" />
+                <div class="tit">乐理知识</div>
+              </div>
+            </PopoverMenuItem>
           </template>
           <div class="tit">扩展知识</div>
         </Popover>
@@ -333,6 +388,49 @@
         "
       />
     </Modal>
+    <Modal
+      :contentStyle="{
+        width: '70%',
+        minWidth: '1200px',
+        height: '86%',
+        boxShadow: '0px 2px 10px 0px rgba(0,0,0,0.08)',
+        borderRadius: '16px',
+        border: '1px solid #DEDEDE',
+        padding: '0'
+      }"
+      v-model:visible="musicTheoryVisible"
+    >
+      <musicTheoryList
+        @update="handleAddMusicResources"
+        @close="
+          () => {
+            musicTheoryVisible = false
+          }
+        "
+      />
+    </Modal>
+    <Modal
+      :contentStyle="{
+        width: musicResourcesType === 'MUSIC' ? '70%' : '1100px',
+        minWidth: '1100px',
+        height: '86%',
+        boxShadow: '0px 2px 10px 0px rgba(0,0,0,0.08)',
+        borderRadius: '16px',
+        border: '1px solid #DEDEDE',
+        padding: '0'
+      }"
+      v-model:visible="musicResourcesVisible"
+    >
+      <musicResourcesList
+        :type="musicResourcesType"
+        @update="handleAddMusicResources"
+        @close="
+          () => {
+            musicResourcesVisible = false
+          }
+        "
+      />
+    </Modal>
   </div>
 </template>
 
@@ -362,8 +460,11 @@ import cloudCoachList from "@/views/components/element/cloudCoachElement/cloudCo
 import resourcesList from "@/views/components/element/enjoyElement/resourcesList"
 import listeningPracticeList from "@/views/components/element/listeningPracticeElement/listeningPracticeList"
 import rhythmPracticeList from "@/views/components/element/rhythmPracticeElement/rhythmPracticeList"
+import musicTheoryList from "@/views/components/element/musicResourcesElement/musicTheoryList"
+import musicResourcesList from "@/views/components/element/musicResourcesElement/musicResourcesList"
 import fileUpload from "@/utils/oss-file-upload"
 import usePptWork from "@/store/pptWork"
+import { type pptContentType } from "@/views/components/element/musicResourcesElement"
 
 const useSlidesHook = useSlidesStore()
 
@@ -396,7 +497,8 @@ const {
   createCloudCoachElement,
   createEnjoyElement,
   createListeningPracticeElement,
-  createRhythmPracticeElement
+  createRhythmPracticeElement,
+  createMusicResourcesElement
 } = useCreateElement()
 
 const insertImageElement = (files: FileList) => {
@@ -418,6 +520,9 @@ const moreToolsVisible = ref(false)
 const expandedKnowledgeVisible = ref(false)
 const listeningPracticeVisible = ref(false)
 const rhythmPracticeVisible = ref(false)
+const musicTheoryVisible = ref(false)
+const musicResourcesVisible = ref(false)
+const musicResourcesType = ref<Exclude<pptContentType, "THEORY">>("INSTRUMENT")
 
 // 音视频
 function handleUpload(fileData: UploadRequestOptions) {
@@ -507,6 +612,14 @@ function handleResources(item: Record<string, any>) {
     resourcesListVisible.value = false
   }
 }
+
+// 音乐知识
+function handleAddMusicResources(item: Record<string, any>, type: pptContentType) {
+  createMusicResourcesElement(item.id, item.name, type)
+  musicTheoryVisible.value = false
+  musicResourcesVisible.value = false
+}
+
 // 绘制文字范围
 const drawText = (vertical = false) => {
   mainStore.setCreatingElement({

+ 2 - 2
src/views/Editor/Toolbar/ElementPositionPanel.vue

@@ -42,7 +42,7 @@
       <div class="row">
         <NumberInput
           :min="minSize"
-          :max="1500"
+          :max="1920"
           :step="5"
           :disabled="isVerticalText"
           :value="width"
@@ -58,7 +58,7 @@
         <div style="width: 10%" v-else></div>
         <NumberInput
           :min="minSize"
-          :max="800"
+          :max="1080"
           :step="5"
           :disabled="isHorizontalText || handleElement!.type === 'table'"
           :value="height"

+ 10 - 0
src/views/Editor/Toolbar/ElementStylePanel/MusicResourcesStylePanel.vue

@@ -0,0 +1,10 @@
+<!--
+* 音乐资源设置
+-->
+<template>
+  <div class=""></div>
+</template>
+
+<script setup lang="ts"></script>
+
+<style lang="scss" scoped></style>

+ 3 - 1
src/views/Editor/Toolbar/ElementStylePanel/index.vue

@@ -24,6 +24,7 @@ import CloudCoachStylePanel from "./CloudCoachStylePanel.vue"
 import EnjoyStylePanel from "./EnjoyStylePanel.vue"
 import ListeningPracticeStylePanel from "./ListeningPracticeStylePanel.vue"
 import RhythmPracticeStylePanel from "./RhythmPracticeStylePanel.vue"
+import MusicResourcesStylePanel from "./MusicResourcesStylePanel.vue"
 
 const panelMap = {
   [ElementTypes.TEXT]: TextStylePanel,
@@ -41,7 +42,8 @@ const elementSubtypeMap = {
   [ElementSubtypeTypes.SING_PLAY]: CloudCoachStylePanel,
   [ElementSubtypeTypes.ENJOY]: EnjoyStylePanel,
   [ElementSubtypeTypes.LISTENING_PRACTICE]: ListeningPracticeStylePanel,
-  [ElementSubtypeTypes.RHYTHM_PRACTICE]: RhythmPracticeStylePanel
+  [ElementSubtypeTypes.RHYTHM_PRACTICE]: RhythmPracticeStylePanel,
+  [ElementSubtypeTypes.MUSIC_RESOURCES]: MusicResourcesStylePanel
 }
 const { activeElementIdList, activeElementList, handleElement, activeGroupElementId } = storeToRefs(useMainStore())
 

+ 1 - 1
src/views/Editor/Toolbar/index.vue

@@ -41,7 +41,7 @@ const elementTabs = computed<ElementTabs[]>(() => {
   }
   if (
     handleElement.value?.type === "elf" &&
-    ["elf-sing-play", "elf-enjoy", "elf-listening-practice", "elf-rhythm-practice"].includes(handleElement.value?.subtype)
+    ["elf-sing-play", "elf-enjoy", "elf-listening-practice", "elf-rhythm-practice", "elf-music-resources"].includes(handleElement.value?.subtype)
   ) {
     return [
       { label: "位置", key: ToolbarStates.EL_POSITION },

+ 3 - 1
src/views/Mobile/MobileEditor/MobileEditableElement.vue

@@ -26,6 +26,7 @@ import cloudCoachElement from "@/views/components/element/cloudCoachElement"
 import enjoyElement from "@/views/components/element/enjoyElement"
 import listeningPracticeElement from "@/views/components/element/listeningPracticeElement"
 import rhythmPracticeElement from "@/views/components/element/rhythmPracticeElement"
+import musicResourcesElement from "@/views/components/element/musicResourcesElement"
 
 const props = defineProps<{
   elementInfo: PPTElement
@@ -50,7 +51,8 @@ const currentElementComponent = computed<unknown>(() => {
     [ElementSubtypeTypes.SING_PLAY]: cloudCoachElement,
     [ElementSubtypeTypes.ENJOY]: enjoyElement,
     [ElementSubtypeTypes.LISTENING_PRACTICE]: listeningPracticeElement,
-    [ElementSubtypeTypes.RHYTHM_PRACTICE]: rhythmPracticeElement
+    [ElementSubtypeTypes.RHYTHM_PRACTICE]: rhythmPracticeElement,
+    [ElementSubtypeTypes.MUSIC_RESOURCES]: musicResourcesElement
   }
   return elementTypeMap[props.elementInfo.type] || elementSubtypeMap[props.elementInfo.subtype] || null
 })

+ 9 - 0
src/views/Screen/BaseView.vue

@@ -43,6 +43,15 @@
         <IconPower v-if="screenStore.mode === 'pptEditor'" class="tool-btn" v-tooltip="'结束放映'" @click="exitScreening()" />
       </div>
     </div> -->
+    <!-- 编辑模式下 预览加上退出全屏按钮 -->
+    <div v-if="screenStore.mode === 'pptEditor'" class="tools-right visible">
+      <div class="content">
+        <div class="tool-btn page-number" @click="slideThumbnailModelVisible = true">幻灯片 {{ slideIndex + 1 }} / {{ slides.length }}</div>
+        <IconOffScreenOne class="tool-btn" v-tooltip="'退出全屏'" v-if="fullscreenState" @click="manualExitFullscreen()" />
+        <IconFullScreenOne class="tool-btn" v-tooltip="'进入全屏'" v-else @click="enterFullscreen()" />
+        <IconPower v-if="screenStore.mode === 'pptEditor'" class="tool-btn" v-tooltip="'结束放映'" @click="exitScreening()" />
+      </div>
+    </div>
   </div>
 </template>
 

+ 3 - 1
src/views/Screen/ScreenElement.vue

@@ -36,6 +36,7 @@ import ScreenCloudCoachElement from "@/views/components/element/cloudCoachElemen
 import ScreenEnjoyElement from "@/views/components/element/enjoyElement/ScreenEnjoyElement.vue"
 import ScreenListeningPracticeElement from "@/views/components/element/listeningPracticeElement/ScreenListeningPracticeElement.vue"
 import ScreenRhythmPracticeElement from "@/views/components/element/rhythmPracticeElement/ScreenRhythmPracticeElement.vue"
+import ScreenMusicResourcesElement from "@/views/components/element/musicResourcesElement/ScreenMusicResourcesElement.vue"
 
 const props = defineProps<{
   elementInfo: PPTElement
@@ -62,7 +63,8 @@ const currentElementComponent = computed<unknown>(() => {
     [ElementSubtypeTypes.SING_PLAY]: ScreenCloudCoachElement,
     [ElementSubtypeTypes.ENJOY]: ScreenEnjoyElement,
     [ElementSubtypeTypes.LISTENING_PRACTICE]: ScreenListeningPracticeElement,
-    [ElementSubtypeTypes.RHYTHM_PRACTICE]: ScreenRhythmPracticeElement
+    [ElementSubtypeTypes.RHYTHM_PRACTICE]: ScreenRhythmPracticeElement,
+    [ElementSubtypeTypes.MUSIC_RESOURCES]: ScreenMusicResourcesElement
   }
   return elementTypeMap[props.elementInfo.type] || elementSubtypeMap[props.elementInfo.subtype] || null
 })

+ 3 - 1
src/views/components/ThumbnailSlide/ThumbnailElement.vue

@@ -27,6 +27,7 @@ import BaseCloudCoachElement from "@/views/components/element/cloudCoachElement/
 import BaseEnjoyElement from "@/views/components/element/enjoyElement/BaseEnjoyElement.vue"
 import BaseListeningPracticeElement from "@/views/components/element/listeningPracticeElement/BaseListeningPracticeElement.vue"
 import BaseRhythmPracticeElement from "@/views/components/element/rhythmPracticeElement/BaseRhythmPracticeElement.vue"
+import BaseMusicResourcesElement from "@/views/components/element/musicResourcesElement/BaseMusicResourcesElement.vue"
 
 const props = defineProps<{
   elementInfo: PPTElement
@@ -50,7 +51,8 @@ const currentElementComponent = computed<unknown>(() => {
     [ElementSubtypeTypes.SING_PLAY]: BaseCloudCoachElement,
     [ElementSubtypeTypes.ENJOY]: BaseEnjoyElement,
     [ElementSubtypeTypes.LISTENING_PRACTICE]: BaseListeningPracticeElement,
-    [ElementSubtypeTypes.RHYTHM_PRACTICE]: BaseRhythmPracticeElement
+    [ElementSubtypeTypes.RHYTHM_PRACTICE]: BaseRhythmPracticeElement,
+    [ElementSubtypeTypes.MUSIC_RESOURCES]: BaseMusicResourcesElement
   }
   return elementTypeMap[props.elementInfo.type] || elementSubtypeMap[props.elementInfo.subtype] || null
 })

+ 1 - 1
src/views/components/element/enjoyElement/resourcesList/resourcesList.vue

@@ -696,7 +696,7 @@ const highlightedText = (text: string, query: string) => {
               flex-direction: column;
               overflow: hidden;
               cursor: pointer;
-              &:nth-last-child(-n + 3) {
+              &:nth-last-child(-n + 4) {
                 margin-bottom: 0;
               }
               &:hover {

+ 62 - 0
src/views/components/element/musicResourcesElement/BaseMusicResourcesElement.vue

@@ -0,0 +1,62 @@
+<template>
+  <div
+    class="base-element-musicResources"
+    :style="{
+      top: elementInfo.top + 'px',
+      left: elementInfo.left + 'px',
+      width: elementInfo.width + 'px',
+      height: elementInfo.height + 'px'
+    }"
+  >
+    <div class="rotate-wrapper" :style="{ transform: `rotate(${elementInfo.rotate}deg)` }">
+      <div class="element-content">
+        <div class="title" :style="{ fontSize: 13 / scale + 'px' }">{{ `${titNameObj[elementInfo.sType]}-${elementInfo.name}` }}</div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import type { PPTMusicResourcesElement } from "@/types/slides"
+import { ref, inject } from "vue"
+import { injectKeySlideScale } from "@/types/injectKey"
+
+defineProps<{
+  elementInfo: PPTMusicResourcesElement
+}>()
+
+const scale = inject(injectKeySlideScale) || ref(1)
+
+const titNameObj = {
+  INSTRUMENT: "乐器百科",
+  MUSICIAN: "音乐家",
+  MUSIC: "名曲鉴赏",
+  THEORY: "乐理知识"
+}
+</script>
+
+<style lang="scss" scoped>
+.base-element-musicResources {
+  position: absolute;
+}
+.rotate-wrapper {
+  width: 100%;
+  height: 100%;
+}
+.element-content {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  background: url("./musicResourcesList/imgs/musicBg.png") no-repeat;
+  background-size: 100% 100%;
+  .title {
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    padding: 0 40px;
+    color: #333333;
+  }
+}
+</style>

+ 58 - 0
src/views/components/element/musicResourcesElement/ScreenMusicResourcesElement.vue

@@ -0,0 +1,58 @@
+<template>
+  <div
+    class="base-element-cloudCoach screen-element-musicResources"
+    :style="{
+      top: elementInfo.top + 'px',
+      left: elementInfo.left + 'px',
+      width: elementInfo.width + 'px',
+      height: elementInfo.height + 'px'
+    }"
+  >
+    <div class="rotate-wrapper" :style="{ transform: `rotate(${elementInfo.rotate}deg)` }">
+      <div class="element-content">
+        <musicResourcesPlayer
+          v-if="inCurrentSlide"
+          :width="elementInfo.width"
+          :height="elementInfo.height"
+          :scale="scale"
+          :sid="elementInfo.sid"
+          :type="elementInfo.sType"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { computed, inject, ref } from "vue"
+import { storeToRefs } from "pinia"
+import { useSlidesStore } from "@/store"
+import type { PPTMusicResourcesElement } from "@/types/slides"
+import { injectKeySlideId, injectKeySlideScale } from "@/types/injectKey"
+import musicResourcesPlayer from "./musicResourcesPlayer"
+
+defineProps<{
+  elementInfo: PPTMusicResourcesElement
+}>()
+
+const { currentSlide } = storeToRefs(useSlidesStore())
+
+const scale = inject(injectKeySlideScale) || ref(1)
+const slideId = inject(injectKeySlideId) || ref("")
+
+const inCurrentSlide = computed(() => currentSlide.value.id === slideId.value)
+</script>
+
+<style lang="scss" scoped>
+.screen-element-musicResources {
+  position: absolute;
+}
+.rotate-wrapper {
+  width: 100%;
+  height: 100%;
+}
+.element-content {
+  width: 100%;
+  height: 100%;
+}
+</style>

+ 29 - 0
src/views/components/element/musicResourcesElement/index.ts

@@ -0,0 +1,29 @@
+import musicResourcesElement from "./musicResourcesElement.vue"
+export default musicResourcesElement
+
+import { getToken } from "@/libs/auth"
+import { stringifyQuery } from "@/libs/cipher"
+import { CLASSROOM_URL_API, CLASSAPP_URL_API } from "@/config"
+import queryParams from "@/queryParams"
+
+export type pptContentType = "INSTRUMENT" | "MUSICIAN" | "MUSIC" | "THEORY"
+type pptType = "modal" | "preview"
+
+const typeObj = {
+  MUSIC: "MUSIC_WIKI",
+  INSTRUMENT: "INSTRUMENT",
+  MUSICIAN: "MUSICIAN",
+  THEORY: "THEORY"
+}
+/**
+ * 获取资源url
+ */
+export function getMusicResourcesUrl(pptContentType: pptContentType, pptType: pptType, pptId: string) {
+  if (queryParams.fromType == "CLASS") {
+    return `${CLASSAPP_URL_API}/#/pptResources?${`Authorization=${getToken()}&pptContentType=${typeObj[pptContentType]}&pptType=${pptType}&pptId=${pptId}`}`
+    //return `http://192.168.0.147:9002/#/pptResources?${`Authorization=${getToken()}&pptContentType=${typeObj[pptContentType]}&pptType=${pptType}&pptId=${pptId}`}`
+  } else {
+    return `${CLASSROOM_URL_API}/#/pptResources?${stringifyQuery(`Authorization=${getToken()}&source=admin&pptContentType=${pptContentType}&pptType=${pptType}&pptId=${pptId}`)}`
+    //return `http://localhost:5005/#/pptResources?${stringifyQuery(`Authorization=${getToken()}&source=admin&pptContentType=${typeObj[pptContentType]}&pptType=${pptType}&pptId=${pptId}`)}`
+  }
+}

+ 122 - 0
src/views/components/element/musicResourcesElement/musicResourcesElement.vue

@@ -0,0 +1,122 @@
+<template>
+  <div
+    class="musicResourcesElement"
+    :class="{ lock: elementInfo.lock }"
+    :style="{
+      top: elementInfo.top + 'px',
+      left: elementInfo.left + 'px',
+      width: elementInfo.width + 'px',
+      height: elementInfo.height + 'px'
+    }"
+  >
+    <div class="rotate-wrapper" :style="{ transform: `rotate(${elementInfo.rotate}deg)` }">
+      <div
+        class="element-content"
+        v-contextmenu="contextmenus"
+        @mousedown="$event => handleSelectElement($event)"
+        @touchstart="$event => handleSelectElement($event)"
+      >
+        <div
+          v-if="isShowMask"
+          @mousedown.stop="$event => handleSelectElement($event, false)"
+          @touchstart.stop="$event => handleSelectElement($event, false)"
+          class="mask"
+        ></div>
+        <musicResourcesPlayer
+          :width="elementInfo.width"
+          :height="elementInfo.height"
+          :scale="canvasScale"
+          :sid="elementInfo.sid"
+          :type="elementInfo.sType"
+        />
+        <div
+          :class="['handler-border', item]"
+          v-for="item in ['t', 'b', 'l', 'r']"
+          :key="item"
+          @mousedown="$event => handleSelectElement($event)"
+          @touchstart="$event => handleSelectElement($event)"
+        ></div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { storeToRefs } from "pinia"
+import { useMainStore } from "@/store"
+import type { PPTMusicResourcesElement } from "@/types/slides"
+import type { ContextmenuItem } from "@/components/Contextmenu/types"
+import musicResourcesPlayer from "./musicResourcesPlayer"
+import { computed } from "vue"
+
+const props = defineProps<{
+  elementInfo: PPTMusicResourcesElement
+  selectElement: (e: MouseEvent | TouchEvent, element: PPTMusicResourcesElement, canMove?: boolean) => void
+  contextmenus: () => ContextmenuItem[] | null
+}>()
+
+const mainStore = useMainStore()
+const { canvasScale } = storeToRefs(mainStore)
+
+/* 当没有选中 或者 拖动过程中加一个遮罩,以免事件进入 iframe 无法触发*/
+const isShowMask = computed(() => {
+  return mainStore.handleElementId !== props.elementInfo.id || props.elementInfo.isMove
+})
+const handleSelectElement = (e: MouseEvent | TouchEvent, canMove = true) => {
+  if (props.elementInfo.lock) return
+  e.stopPropagation()
+
+  props.selectElement(e, props.elementInfo, canMove)
+}
+</script>
+
+<style lang="scss" scoped>
+.musicResourcesElement {
+  position: absolute;
+  &.lock .handler-border {
+    cursor: default;
+  }
+}
+.rotate-wrapper {
+  width: 100%;
+  height: 100%;
+}
+.element-content {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  .mask {
+    position: absolute;
+    width: 100%;
+    height: 100%;
+  }
+}
+.handler-border {
+  position: absolute;
+  cursor: move;
+  &.t {
+    width: 100%;
+    height: 10px;
+    top: 0;
+    left: 0;
+  }
+  &.b {
+    width: 100%;
+    height: 10px;
+    bottom: 0;
+    left: 0;
+  }
+  &.l {
+    width: 10px;
+    height: 100%;
+    left: 0;
+    top: 0;
+  }
+  &.r {
+    width: 10px;
+    height: 100%;
+    right: 0;
+    top: 0;
+  }
+}
+</style>

+ 94 - 0
src/views/components/element/musicResourcesElement/musicResourcesList/components/instrumentList.vue

@@ -0,0 +1,94 @@
+<!--
+* 乐器百科 列表
+-->
+<template>
+  <div class="musicListBox">
+    <div class="musicCon" v-for="item in musicList" :key="item.id" @click="emits('handlePreview', item)">
+      <img class="avatar" :src="item.avatar || icon_default" alt="" />
+      <div class="addBtn" @click.stop="emits('handleAdd', item)">添加</div>
+      <div class="highName">
+        <EllipsisScroll :title="item.highName || ''" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import EllipsisScroll from "@/components/ellipsisScroll"
+import icon_default from "../imgs/icon_default.png"
+
+const props = defineProps<{
+  musicList: any[]
+}>()
+
+const emits = defineEmits<{
+  (event: "handlePreview", item: Record<string, any>): void
+  (event: "handleAdd", item: Record<string, any>): void
+}>()
+</script>
+
+<style lang="scss" scoped>
+.musicListBox {
+  width: calc(100% + 40px);
+  margin-left: -40px;
+  display: flex;
+  flex-wrap: wrap;
+  padding: 0 30px;
+  .musicCon {
+    width: calc(16.6666% - 40px);
+    margin-left: 40px;
+    margin-bottom: 32px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    border-radius: 12px;
+    cursor: pointer;
+    position: relative;
+    &:nth-last-child(-n + 6) {
+      margin-bottom: 0;
+    }
+    &:hover {
+      .avatar {
+        border-color: #198cfe;
+        transform: scale(1.02);
+        transition: all 0.2s ease;
+      }
+    }
+    .highName {
+      text-align: center;
+      width: 100%;
+      margin-top: 12px;
+      overflow: hidden;
+      font-weight: 600;
+      font-size: 14px;
+      color: #131415;
+      line-height: 20px;
+      &::v-deep(.highlighted) {
+        color: $themeColor;
+      }
+    }
+    .avatar {
+      box-sizing: initial;
+      border: 2px solid transparent;
+      width: 140px;
+      height: 140px;
+      border-radius: 12px;
+    }
+    .addBtn {
+      top: 6px;
+      right: 6px;
+      position: absolute;
+      font-weight: 600;
+      font-size: 12px;
+      color: #ffffff;
+      line-height: 18px;
+      padding: 2px 10px;
+      background: #198cfe;
+      border-radius: 4px;
+      &:hover {
+        opacity: 0.8;
+      }
+    }
+  }
+}
+</style>

+ 90 - 0
src/views/components/element/musicResourcesElement/musicResourcesList/components/musicList.vue

@@ -0,0 +1,90 @@
+<!--
+* 名曲鉴赏 列表
+-->
+<template>
+  <div class="musicListBox">
+    <div class="musicCon" v-for="item in musicList" :key="item.id" @click="emits('handlePreview', item)">
+      <img class="avatar" :src="item.avatar || icon_default" alt="" />
+      <div class="highName">
+        <EllipsisScroll :title="item.highName || ''" />
+      </div>
+      <div class="addBtn" @click.stop="emits('handleAdd', item)">添加</div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import EllipsisScroll from "@/components/ellipsisScroll"
+import icon_default from "../imgs/icon_default.png"
+
+const props = defineProps<{
+  musicList: any[]
+}>()
+
+const emits = defineEmits<{
+  (event: "handlePreview", item: Record<string, any>): void
+  (event: "handleAdd", item: Record<string, any>): void
+}>()
+</script>
+
+<style lang="scss" scoped>
+.musicListBox {
+  width: calc(100% + 24px);
+  margin-left: -24px;
+  display: flex;
+  flex-wrap: wrap;
+  padding: 0 30px;
+  .musicCon {
+    width: calc(33.3333% - 24px);
+    margin-left: 24px;
+    margin-bottom: 24px;
+    padding: 16px;
+    display: flex;
+    align-items: center;
+    background: #f5f6fa;
+    border-radius: 12px;
+    cursor: pointer;
+    border: 2px solid transparent;
+    &:hover {
+      border-color: #198cfe;
+      transform: scale(1.02);
+      transition: all 0.2s ease;
+    }
+    &:nth-last-child(-n + 3) {
+      margin-bottom: 0;
+    }
+    .highName {
+      overflow: hidden;
+      flex-grow: 1;
+      font-weight: 600;
+      font-size: 15px;
+      color: #131415;
+      line-height: 21px;
+      &::v-deep(.highlighted) {
+        color: $themeColor;
+      }
+    }
+    .avatar {
+      flex-shrink: 0;
+      width: 60px;
+      height: 60px;
+      border-radius: 7px;
+      margin-right: 12px;
+    }
+    .addBtn {
+      margin-left: 20px;
+      flex-shrink: 0;
+      font-weight: 600;
+      font-size: 13px;
+      color: #ffffff;
+      line-height: 19px;
+      padding: 4px 12px;
+      background: #198cfe;
+      border-radius: 4px;
+      &:hover {
+        opacity: 0.8;
+      }
+    }
+  }
+}
+</style>

+ 94 - 0
src/views/components/element/musicResourcesElement/musicResourcesList/components/musicianList.vue

@@ -0,0 +1,94 @@
+<!--
+* 音乐家 列表
+-->
+<template>
+  <div class="musicListBox">
+    <div class="musicCon" v-for="item in musicList" :key="item.id" @click="emits('handlePreview', item)">
+      <img class="avatar" :src="item.avatar || icon_default" alt="" />
+      <div class="addBtn" @click.stop="emits('handleAdd', item)">添加</div>
+      <div class="highName">
+        <EllipsisScroll :title="item.highName || ''" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import EllipsisScroll from "@/components/ellipsisScroll"
+import icon_default from "../imgs/icon_default.png"
+
+const props = defineProps<{
+  musicList: any[]
+}>()
+
+const emits = defineEmits<{
+  (event: "handlePreview", item: Record<string, any>): void
+  (event: "handleAdd", item: Record<string, any>): void
+}>()
+</script>
+
+<style lang="scss" scoped>
+.musicListBox {
+  width: calc(100% + 40px);
+  margin-left: -40px;
+  display: flex;
+  flex-wrap: wrap;
+  padding: 0 30px;
+  .musicCon {
+    width: calc(16.6666% - 40px);
+    margin-left: 40px;
+    margin-bottom: 32px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    border-radius: 12px;
+    cursor: pointer;
+    position: relative;
+    &:nth-last-child(-n + 6) {
+      margin-bottom: 0;
+    }
+    &:hover {
+      .avatar {
+        border-color: #198cfe;
+        transform: scale(1.02);
+        transition: all 0.2s ease;
+      }
+    }
+    .highName {
+      text-align: center;
+      width: 100%;
+      margin-top: 12px;
+      overflow: hidden;
+      font-weight: 600;
+      font-size: 14px;
+      color: #131415;
+      line-height: 20px;
+      &::v-deep(.highlighted) {
+        color: $themeColor;
+      }
+    }
+    .avatar {
+      box-sizing: initial;
+      border: 2px solid transparent;
+      width: 140px;
+      height: 160px;
+      border-radius: 12px;
+    }
+    .addBtn {
+      top: 6px;
+      right: 6px;
+      position: absolute;
+      font-weight: 600;
+      font-size: 12px;
+      color: #ffffff;
+      line-height: 18px;
+      padding: 2px 10px;
+      background: #198cfe;
+      border-radius: 4px;
+      &:hover {
+        opacity: 0.8;
+      }
+    }
+  }
+}
+</style>

BIN
src/views/components/element/musicResourcesElement/musicResourcesList/imgs/btnLeft.png


BIN
src/views/components/element/musicResourcesElement/musicResourcesList/imgs/btnRight..png


BIN
src/views/components/element/musicResourcesElement/musicResourcesList/imgs/icon_default.png


BIN
src/views/components/element/musicResourcesElement/musicResourcesList/imgs/musicBg.png


+ 2 - 0
src/views/components/element/musicResourcesElement/musicResourcesList/index.ts

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

+ 82 - 0
src/views/components/element/musicResourcesElement/musicResourcesList/musicPreview.vue

@@ -0,0 +1,82 @@
+<!-- 预览 弹窗列表 -->
+<template>
+  <div class="musicPreview">
+    <div class="headCon">
+      <div class="headLeft">
+        <div class="title">{{ musicObj.name }}</div>
+      </div>
+      <div class="headright">
+        <img @click="emits('close')" class="closeBtn" src="../../cloudCoachElement/cloudCoachList/imgs/close.png" alt="" />
+      </div>
+    </div>
+    <div class="content">
+      <iframe v-if="props.musicObj.id" :key="props.musicObj.id" class="musicIframe" frameborder="0" :src="url"></iframe>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed } from "vue"
+import { getMusicResourcesUrl } from "../index"
+import { type pptContentType } from "@/views/components/element/musicResourcesElement"
+
+const emits = defineEmits<{
+  (event: "close"): void
+}>()
+
+const props = defineProps<{
+  musicObj: Record<string, any>
+  type: Exclude<pptContentType, "THEORY">
+}>()
+
+const url = computed(() => {
+  return getMusicResourcesUrl(props.type, "modal", props.musicObj.id)
+})
+</script>
+
+<style lang="scss" scoped>
+.musicPreview {
+  width: 100%;
+  height: 100%;
+  .headCon {
+    width: 100%;
+    height: 64px;
+    border-bottom: 1px solid #eaeaea;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    .headLeft {
+      margin-left: 30px;
+      display: flex;
+      align-items: center;
+      .title {
+        font-weight: 600;
+        font-size: 18px;
+        color: #131415;
+        line-height: 24px;
+      }
+    }
+    .headright {
+      margin-right: 30px;
+      display: flex;
+      align-items: center;
+      .closeBtn {
+        width: 24px;
+        height: 24px;
+        cursor: pointer;
+        &:hover {
+          opacity: 0.8;
+        }
+      }
+    }
+  }
+  .content {
+    width: 100%;
+    height: calc(100% - 64px);
+    .musicIframe {
+      width: 100%;
+      height: 100%;
+    }
+  }
+}
+</style>

+ 654 - 0
src/views/components/element/musicResourcesElement/musicResourcesList/musicResourcesList.vue

@@ -0,0 +1,654 @@
+<!-- 弹窗列表 -->
+<template>
+  <div class="musicwikiList">
+    <div class="headCon">
+      <div class="headLeft">
+        <img class="tipImg" :src="tipImgObj[type]" alt="" />
+        <div class="title">{{ titObj[type] }}</div>
+      </div>
+      <div class="headright">
+        <img @click="emits('close')" class="closeBtn" src="../../cloudCoachElement/cloudCoachList/imgs/close.png" alt="" />
+      </div>
+    </div>
+    <div class="content">
+      <div class="tabTools">
+        <div class="tabBox">
+          <div class="tabCon" ref="tabConDom" @wheel.prevent="handleMousewheelTabCon">
+            <div
+              class="tab"
+              @click="handleResourcesTypeChange(item)"
+              :class="{ active: item.id === queryData.resourcesType }"
+              v-for="item in resourcesTypeOption"
+              :key="item.id"
+            >
+              {{ item.name }}
+            </div>
+          </div>
+          <div v-if="horizontalScrollbar" class="tabChangeCon">
+            <div class="tabChangeLeft" @click="handleScrollTabCon(-150)"></div>
+            <div class="tabChangeRight" @click="handleScrollTabCon(150)"></div>
+          </div>
+        </div>
+        <div class="query">
+          <Input :placeholder="'请输入搜索关键词'" v-model:value="queryData.keyword" clearable @enter="handleQuery" @clear="handleQuery">
+            <template #prefix>
+              <img class="img" src="../../cloudCoachElement/cloudCoachList/imgs/query.png" alt="" />
+            </template>
+            <template #suffix>
+              <div class="queryBtn" @click="handleQuery">搜索</div>
+            </template>
+          </Input>
+        </div>
+      </div>
+      <div class="musicListCon">
+        <div class="queryFrom">
+          <div class="queryFromList" v-if="classificationOption.length">
+            <div class="tit">分类:</div>
+            <div class="queryFromCon">
+              <div
+                @click="handleClassificationChange(item)"
+                v-for="item in classificationOption"
+                :key="item.id"
+                :class="['queryTip', queryData.classification === item.id && 'active']"
+              >
+                {{ item.name }}
+              </div>
+            </div>
+          </div>
+          <div class="queryFromList" v-if="typeOption.length">
+            <div class="tit">类型:</div>
+            <div class="queryFromCon">
+              <template v-for="item in typeOption">
+                <div
+                  :class="['queryTip', queryData.type.id === item.id && 'active']"
+                  @click="handleTypeChange(item)"
+                  v-if="item.childrenList.length === 0"
+                  :key="item.id"
+                >
+                  {{ item.name }}
+                </div>
+                <Popover v-model:value="item.isExpand" trigger="mouseenter" v-else :offset="-4" :key="item.id + '_'">
+                  <template #content>
+                    <PopoverMenuItem
+                      @click="
+                        () => {
+                          handleTypeChange(row)
+                          item.isExpand = false
+                        }
+                      "
+                      v-for="row in item.childrenList"
+                      :key="row.id"
+                      :active="row.id === queryData.type.id"
+                      >{{ row.name }}</PopoverMenuItem
+                    >
+                  </template>
+                  <div class="queryTip" :class="{ hoverActive: isActiveSubjectPop(item) }">
+                    <div>{{ queryData.type.id !== item.id && isActiveSubjectPop(item) ? queryData.type.name : item.name }}</div>
+                    <img src="../../cloudCoachElement/cloudCoachList/imgs/jt.png" alt="" />
+                  </div>
+                </Popover>
+              </template>
+            </div>
+          </div>
+        </div>
+        <div class="musicListConBox" v-loading="loading">
+          <div class="musicList" :class="{ empty: !musicList.length && !loading }">
+            <template v-if="musicList.length && !loading">
+              <musicListVue :musicList="musicList" @handle-add="handleAdd" @handle-preview="handlePreview" v-if="type === 'MUSIC'" />
+              <musicianListVue :musicList="musicList" @handle-add="handleAdd" @handle-preview="handlePreview" v-else-if="type === 'MUSICIAN'" />
+              <instrumentListVue :musicList="musicList" @handle-add="handleAdd" @handle-preview="handlePreview" v-else />
+            </template>
+            <Empty v-if="!musicList.length && !loading" />
+          </div>
+          <div class="pagination" v-show="musicList.length">
+            <el-pagination
+              layout="prev, pager, next"
+              :default-page-size="queryData.rows"
+              :current-page="queryData.page"
+              @current-change="handleCurrentChange"
+              :total="queryData.total"
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+  <Modal
+    :contentStyle="{
+      width: '70%',
+      minWidth: '1200px',
+      height: '86%',
+      boxShadow: '0px 2px 10px 0px rgba(0,0,0,0.08)',
+      borderRadius: '16px',
+      border: '1px solid #DEDEDE',
+      padding: '0'
+    }"
+    v-model:visible="previewMusicObjVisible"
+  >
+    <musicPreview
+      :musicObj="previewMusicObj"
+      :type="type"
+      @close="
+        () => {
+          previewMusicObjVisible = false
+        }
+      "
+    />
+  </Modal>
+</template>
+
+<script setup lang="ts">
+import { getKnowledgeWikiCategoryType, getKnowledgeWikiPage } from "@/api/musicResources"
+import { ElLoading, ElPagination } from "element-plus"
+import Input from "@/components/Input.vue"
+import Popover from "@/components/Popover.vue"
+import PopoverMenuItem from "@/components/PopoverMenuItem.vue"
+import Empty from "@/components/Empty"
+import Modal from "@/components/Modal.vue"
+import { httpAjax } from "@/plugins/httpAjax"
+import { reactive, ref, nextTick } from "vue"
+import musicPreview from "./musicPreview.vue"
+import mqjsImg from "@/views/Editor/CanvasTool/imgs/mqjs.png"
+import yyjImg from "@/views/Editor/CanvasTool/imgs/yyj.png"
+import yqbkImg from "@/views/Editor/CanvasTool/imgs/yqbk.png"
+import { CODE_ERR_CANCELED } from "@/libs/auth"
+import musicListVue from "./components/musicList.vue"
+import musicianListVue from "./components/musicianList.vue"
+import instrumentListVue from "./components/instrumentList.vue"
+import { type pptContentType } from "@/views/components/element/musicResourcesElement"
+
+const props = defineProps<{
+  type: Exclude<pptContentType, "THEORY">
+}>()
+
+const emits = defineEmits<{
+  (event: "update", item: Record<string, any>, type: Exclude<pptContentType, "THEORY">): void
+  (event: "close"): void
+}>()
+
+const titObj = {
+  MUSIC: "名曲鉴赏",
+  INSTRUMENT: "乐器百科",
+  MUSICIAN: "音乐家"
+}
+const tipImgObj = {
+  MUSIC: mqjsImg,
+  INSTRUMENT: yqbkImg,
+  MUSICIAN: yyjImg
+}
+
+const musicList = ref<any[]>([])
+const loading = ref(true)
+const vLoading = ElLoading.directive
+// 资源类型
+const resourcesTypeOption = ref<any[]>([])
+const classificationOption = ref<any[]>([])
+const typeOption = ref<any[]>([])
+
+// 不同类型 每页显示不同的数量
+const pageRowObj = {
+  MUSIC: 21,
+  INSTRUMENT: 24,
+  MUSICIAN: 24
+}
+
+const queryData = reactive({
+  page: 1,
+  rows: pageRowObj[props.type],
+  total: 0,
+  keyword: "",
+  resourcesType: "",
+  classification: "",
+  type: {
+    id: "",
+    name: ""
+  }
+})
+
+getQueryList()
+function getQueryList() {
+  httpAjax(getKnowledgeWikiCategoryType, props.type).then(res => {
+    if (res.code === 200) {
+      resourcesTypeOption.value = res.data?.rows || []
+      // 初始化第三层数据
+      resourcesTypeOption.value.map(item => {
+        item.childrenList?.map((itemVal: any) => {
+          itemVal.childrenList?.map((itemV: any) => {
+            if (itemV.childrenList?.length > 0) {
+              itemV.childrenList = [
+                {
+                  id: itemV.id,
+                  name: "全部",
+                  childrenList: []
+                },
+                ...itemV.childrenList
+              ]
+              Object.assign(itemV, { isExpand: ref(false) })
+            }
+          })
+        })
+      })
+      handleResourcesTypeChange(resourcesTypeOption.value[0])
+      // 判断 有没有滚动条
+      nextTick(() => {
+        hasHorizontalScrollbar()
+      })
+    }
+  })
+}
+
+function handleResourcesTypeChange(item: Record<string, any>) {
+  queryData.resourcesType = item.id
+  classificationOption.value = []
+  queryData.classification = ""
+  typeOption.value = []
+  queryData.type = {
+    id: "",
+    name: ""
+  }
+  if (item.childrenList?.length) {
+    classificationOption.value = [
+      {
+        id: item.id,
+        name: "全部",
+        childrenList: []
+      },
+      ...item.childrenList
+    ]
+    queryData.classification = item.id
+    handleClassificationChange(classificationOption.value[0])
+    return
+  }
+  handleQuery()
+}
+
+function handleClassificationChange(item: Record<string, any>) {
+  queryData.classification = item.id
+  typeOption.value = []
+  queryData.type = {
+    id: "",
+    name: ""
+  }
+  if (item.childrenList?.length) {
+    typeOption.value = [
+      {
+        id: item.id,
+        name: "全部",
+        childrenList: []
+      },
+      ...item.childrenList
+    ]
+    handleTypeChange(typeOption.value[0])
+    return
+  }
+  handleQuery()
+}
+
+function handleTypeChange(item: Record<string, any>) {
+  queryData.type = {
+    id: item.id,
+    name: item.name
+  }
+  handleQuery()
+}
+function isActiveSubjectPop(item: any) {
+  return item.childrenList.some((i: any) => {
+    return i.id === queryData.type.id
+  })
+}
+
+function handleCurrentChange(e: number) {
+  queryData.page = e
+  handleGetQuery()
+}
+function handleQuery() {
+  queryData.page = 1
+  queryData.rows = pageRowObj[props.type]
+  handleGetQuery()
+}
+
+let controller: AbortController
+function handleGetQuery() {
+  loading.value = true
+  let { page, rows, keyword, resourcesType, classification, type } = queryData
+  const wikiCategoryId = type.id || classification || resourcesType
+  const params = {
+    keyword,
+    page,
+    rows,
+    type: props.type,
+    wikiCategoryId
+  }
+  if (controller) {
+    controller.abort()
+  }
+  controller = new AbortController()
+  httpAjax(getKnowledgeWikiPage, params, controller).then(res => {
+    // 自己关闭的时候不取消加载
+    if (res.code === CODE_ERR_CANCELED) {
+      return
+    }
+    if (res.code === 200) {
+      musicList.value = res.data.rows.map((item: any) => {
+        item.highName = highlightedText(item.name, queryData.keyword)
+        return item
+      })
+      queryData.total = res.data.total
+    }
+    loading.value = false
+  })
+}
+const highlightedText = (text: string, query: string) => {
+  if (!text) {
+    return ""
+  }
+  if (!query) {
+    return text
+  }
+  const regex = new RegExp(`(${query})`, "gi")
+  return text.replace(regex, '<span class="highlighted">$1</span>')
+}
+
+/* 预览 */
+const previewMusicObjVisible = ref(false)
+const previewMusicObj = ref<Record<string, any>>({
+  id: "",
+  name: ""
+})
+function handlePreview(item: Record<string, any>) {
+  previewMusicObj.value.id = item.id
+  previewMusicObj.value.name = item.name
+  previewMusicObjVisible.value = true
+}
+function handleAdd(item: Record<string, any>) {
+  emits("update", item, props.type)
+}
+
+// 横向拖动
+const tabConDom = ref<HTMLElement>()
+const horizontalScrollbar = ref(false)
+function handleMousewheelTabCon(event: WheelEvent) {
+  handleScrollTabCon(event.deltaY)
+}
+function handleScrollTabCon(num: number) {
+  tabConDom.value?.scrollBy(num, 0)
+}
+function hasHorizontalScrollbar() {
+  if (tabConDom.value) {
+    horizontalScrollbar.value = tabConDom.value.scrollWidth > tabConDom.value.clientWidth
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.musicwikiList {
+  width: 100%;
+  height: 100%;
+  .headCon {
+    width: 100%;
+    height: 64px;
+    border-bottom: 1px solid #eaeaea;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    .headLeft {
+      margin-left: 30px;
+      display: flex;
+      align-items: center;
+      .tipImg {
+        width: 24px;
+        height: 24px;
+      }
+      .title {
+        font-weight: 600;
+        font-size: 18px;
+        color: #131415;
+        margin-left: 8px;
+      }
+    }
+    .headright {
+      margin-right: 30px;
+      display: flex;
+      align-items: center;
+      .closeBtn {
+        width: 24px;
+        height: 24px;
+        cursor: pointer;
+        &:hover {
+          opacity: 0.8;
+        }
+      }
+    }
+  }
+  .content {
+    width: 100%;
+    height: calc(100% - 64px);
+    display: flex;
+    flex-direction: column;
+    .tabTools {
+      height: 72px;
+      width: 100%;
+      padding: 18px 30px;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      .tabBox {
+        display: flex;
+        margin-right: 20px;
+        .tabCon {
+          display: flex;
+          overflow-x: auto;
+          &::-webkit-scrollbar {
+            display: none;
+          }
+          .tab {
+            flex-shrink: 0;
+            margin-right: 32px;
+            font-weight: 400;
+            font-size: 16px;
+            color: #8b8d98;
+            line-height: 22px;
+            cursor: pointer;
+            &:hover {
+              opacity: 0.8;
+            }
+            &:last-child {
+              margin-right: 0;
+            }
+            &.active {
+              font-weight: 600;
+              color: #131415;
+              position: relative;
+              &::after {
+                content: "";
+                position: absolute;
+                width: 100%;
+                height: 10px;
+                background: linear-gradient(90deg, #77bbff 0%, rgba(163, 231, 255, 0.22) 100%);
+                bottom: 0;
+                left: 0;
+                z-index: -1;
+              }
+            }
+          }
+        }
+        .tabChangeCon {
+          margin-left: 16px;
+          flex-shrink: 0;
+          display: flex;
+          .tabChangeLeft {
+            width: 24px;
+            height: 24px;
+            background: url("./imgs/btnLeft.png") no-repeat;
+            background-size: 100% 100%;
+            cursor: pointer;
+            &:hover {
+              opacity: 0.8;
+            }
+          }
+          .tabChangeRight {
+            margin-left: 16px;
+            width: 24px;
+            height: 24px;
+            background: url("./imgs/btnRight..png") no-repeat;
+            background-size: 100% 100%;
+            cursor: pointer;
+            &:hover {
+              opacity: 0.8;
+            }
+          }
+        }
+      }
+      .query {
+        width: 400px;
+        height: 36px;
+        flex-shrink: 0;
+        &::v-deep(.input) {
+          align-items: center;
+          padding: 0 3px 0 12px;
+          border-radius: 18px;
+          height: 100%;
+          &:not(.disabled):hover,
+          &.focused {
+            .img {
+              opacity: 1;
+            }
+            .queryBtn {
+              opacity: 1;
+            }
+          }
+          input {
+            font-size: 14px;
+          }
+          .img {
+            width: 16px;
+            height: 16px;
+            opacity: 0.4;
+          }
+          .queryBtn {
+            width: 60px;
+            height: 30px;
+            background: #198cfe;
+            border-radius: 16px;
+            font-weight: 500;
+            font-size: 14px;
+            color: #ffffff;
+            line-height: 30px;
+            text-align: center;
+            opacity: 0.4;
+            cursor: pointer;
+            &:hover {
+              opacity: 0.8 !important;
+            }
+          }
+        }
+      }
+    }
+    .musicListCon {
+      width: 100%;
+      flex-grow: 1;
+      overflow: hidden;
+      display: flex;
+      flex-direction: column;
+      .queryFrom {
+        flex-shrink: 0;
+        padding: 0 30px;
+        .queryFromList {
+          display: flex;
+          margin-bottom: 4px;
+          .tit {
+            flex-shrink: 0;
+            font-weight: 500;
+            font-size: 14px;
+            color: #131415;
+            line-height: 32px;
+            margin-right: 16px;
+          }
+          .queryFromCon {
+            display: flex;
+            flex-wrap: wrap;
+            .queryTip {
+              margin: 0 16px 12px 0;
+              font-weight: 400;
+              font-size: 14px;
+              color: rgba(0, 0, 0, 0.6);
+              line-height: 20px;
+              padding: 6px 16px;
+              background: #f5f6fa;
+              border-radius: 6px;
+              cursor: pointer;
+              display: flex;
+              align-items: center;
+              & > img {
+                width: 7px;
+                height: 4px;
+                margin-left: 6px;
+              }
+              &:hover {
+                background: #e8e9ed;
+                color: #5d5d5e;
+                > img {
+                  transform: rotate(180deg);
+                }
+              }
+              &.active {
+                background: #d2ecff;
+                color: rgba(0, 0, 0, 1);
+              }
+              &.hoverActive {
+                background: #d2ecff;
+                color: rgba(0, 0, 0, 1);
+              }
+            }
+          }
+        }
+      }
+      .isExpand {
+        flex-shrink: 0;
+        margin-bottom: 12px;
+        cursor: pointer;
+        display: flex;
+        justify-content: center;
+        font-weight: 400;
+        font-size: 14px;
+        color: #198cfe;
+        line-height: 20px;
+        align-items: center;
+        &:hover {
+          opacity: 0.8;
+        }
+        &.active > img {
+          transform: rotate(0deg);
+        }
+        & > img {
+          transform: rotate(180deg);
+          margin-left: 4px;
+          width: 10px;
+          height: 10px;
+        }
+      }
+      .musicListConBox {
+        flex-grow: 1;
+        overflow: hidden;
+        .musicList {
+          padding: 4px 0;
+          height: calc(100% - 60px);
+          overflow: auto;
+          &.empty {
+            display: flex;
+            justify-content: center;
+            align-items: center;
+          }
+        }
+        .pagination {
+          padding: 0 30px;
+          display: flex;
+          justify-content: flex-end;
+          align-items: center;
+          height: 60px;
+        }
+      }
+    }
+  }
+}
+</style>

+ 2 - 0
src/views/components/element/musicResourcesElement/musicResourcesPlayer/index.ts

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

+ 107 - 0
src/views/components/element/musicResourcesElement/musicResourcesPlayer/musicResourcesPlayer.vue

@@ -0,0 +1,107 @@
+<template>
+  <div
+    class="musicResourcesPlayer"
+    :style="{
+      width: width * scale + 'px',
+      height: height * scale + 'px',
+      transform: `scale(${1 / scale})`
+    }"
+  >
+    <div v-if="loading" class="loading-overlay">
+      <div class="loadingBox">
+        <div class="loadingItem"></div>
+        <div class="loadingItem"></div>
+        <div class="loadingItem"></div>
+        <div class="loadingItem"></div>
+      </div>
+    </div>
+    <iframe class="musicIframe" frameborder="0" :src="url" @load="handleIframeLoad"></iframe>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed } from "vue"
+import { getMusicResourcesUrl } from "../index"
+import { type pptContentType } from "@/views/components/element/musicResourcesElement"
+
+const props = withDefaults(
+  defineProps<{
+    width: number
+    height: number
+    scale?: number
+    type: pptContentType
+    sid: string
+  }>(),
+  {
+    scale: 1
+  }
+)
+
+const url = computed(() => {
+  return getMusicResourcesUrl(props.type, "preview", props.sid)
+})
+
+// 先关闭这个功能
+const loading = ref(true)
+function handleIframeLoad() {
+  loading.value = false
+}
+</script>
+
+<style lang="scss" scoped>
+.musicResourcesPlayer {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  overflow: hidden;
+  user-select: none;
+  line-height: 1;
+  transform-origin: 0 0;
+  .musicIframe {
+    width: 100%;
+    height: 100%;
+  }
+  .loading-overlay {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    flex-direction: column;
+    z-index: 10;
+    color: #fff;
+    background: url("../cloudCoachList/imgs/musicBg.png");
+    background-size: cover;
+    .loadingBox {
+      width: 30px;
+      height: 30px;
+      display: flex;
+      justify-content: space-between;
+      flex-wrap: wrap;
+      align-content: space-between;
+      animation: rotate 1.5s linear infinite;
+      .loadingItem {
+        width: 12px;
+        height: 12px;
+        border-radius: 50%;
+        background: #20bdff;
+        opacity: 0.5;
+        &:nth-child(2) {
+          opacity: 1;
+        }
+      }
+    }
+    @keyframes rotate {
+      from {
+        transform: rotate(0deg);
+      }
+      to {
+        transform: rotate(360deg);
+      }
+    }
+  }
+}
+</style>

+ 168 - 0
src/views/components/element/musicResourcesElement/musicTheoryList/courseCollapse.vue

@@ -0,0 +1,168 @@
+<template>
+  <ElCollapse class="courseCollapse" accordion v-model="myActiveCollapse">
+    <ElCollapseItem v-for="item in props.courseList" :key="item.id" :name="item.id">
+      <template #title>
+        <div class="courseCollapseHead">
+          <div class="tip"></div>
+          <div class="list"></div>
+          <div class="courseCollapseHeadTit">
+            {{ item.name }}
+          </div>
+        </div>
+      </template>
+      <div class="courseCollapseCon">
+        <div
+          class="itemCon"
+          :class="{ active: myActiveCollapseId === itemV.id }"
+          v-for="itemV in item.lessonCoursewareDetailKnowledgeDetailList"
+          :key="itemV.id"
+          @click="handleCollapseItem(itemV)"
+        >
+          <div class="itemName">{{ itemV.name }}</div>
+        </div>
+      </div>
+    </ElCollapseItem>
+  </ElCollapse>
+</template>
+
+<script setup lang="ts">
+import { ElCollapse, ElCollapseItem } from "element-plus"
+import { ref, watch } from "vue"
+
+const props = defineProps<{
+  activeCollapse: string
+  activeCollapseId: string
+  courseList: any[]
+}>()
+
+const emits = defineEmits<{
+  (event: "handleCollapseItem", item: Record<string, any>): void
+}>()
+
+const myActiveCollapse = ref(props.activeCollapse)
+const myActiveCollapseId = ref(props.activeCollapseId)
+watch(
+  () => props.activeCollapse,
+  () => {
+    myActiveCollapse.value = props.activeCollapse
+  }
+)
+watch(
+  () => props.activeCollapseId,
+  () => {
+    myActiveCollapseId.value = props.activeCollapseId
+  }
+)
+
+function handleCollapseItem(item: Record<string, any>) {
+  emits("handleCollapseItem", item)
+}
+</script>
+
+<style lang="scss" scoped>
+.courseCollapse {
+  border: none;
+  ::v-deep(.el-collapse-item) {
+    padding: 0 8px;
+    &.is-active {
+      background: #f5f6fa;
+      border-radius: 10px;
+      .el-collapse-item__header,
+      .el-collapse-item__wrap {
+        background-color: transparent;
+      }
+      .courseCollapseHead {
+        .tip {
+          background: url("./imgs/actJ.png") no-repeat;
+          background-size: 16px 16px;
+        }
+        .list {
+          background-image: url("./imgs//actList.png");
+        }
+        .courseCollapseHeadTit {
+          font-weight: 600;
+          color: #131415;
+        }
+      }
+    }
+    .el-collapse-item__header {
+      border-bottom: none;
+    }
+    .el-collapse-item__arrow {
+      display: none;
+    }
+    .el-collapse-item__wrap {
+      border-bottom: none;
+      .el-collapse-item__content {
+        padding-bottom: 12px;
+      }
+    }
+  }
+  .courseCollapseHead {
+    display: flex;
+    align-items: center;
+    width: 100%;
+    .tip {
+      margin-left: 8px;
+      width: 16px;
+      height: 16px;
+      background: url("./imgs/j.png") no-repeat;
+      background-size: 8px 12px;
+      background-position: center;
+      flex-shrink: 0;
+    }
+    .list {
+      margin: 0 8px;
+      width: 16px;
+      height: 18px;
+      background: url("./imgs/list.png") no-repeat;
+      background-size: 100% 100%;
+      flex-shrink: 0;
+    }
+    .courseCollapseHeadTit {
+      font-weight: 400;
+      font-size: 16px;
+      color: #8b8d98;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+    }
+  }
+  .courseCollapseCon {
+    .itemCon {
+      width: 100%;
+      height: 40px;
+      border-radius: 6px;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-top: 8px;
+      cursor: pointer;
+      &:first-child {
+        margin-top: 0;
+      }
+      &:hover {
+        opacity: 0.8;
+      }
+      &.active {
+        background: rgba(16, 31, 87, 0.06);
+        .itemName {
+          font-weight: 600;
+          color: #0482ff;
+        }
+      }
+    }
+    .itemName {
+      margin-left: 56px;
+      margin-right: 10px;
+      font-weight: 400;
+      font-size: 14px;
+      color: #131415;
+      line-height: 20px;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+    }
+  }
+}
+</style>

BIN
src/views/components/element/musicResourcesElement/musicTheoryList/imgs/actJ.png


BIN
src/views/components/element/musicResourcesElement/musicTheoryList/imgs/actList.png


BIN
src/views/components/element/musicResourcesElement/musicTheoryList/imgs/j.png


BIN
src/views/components/element/musicResourcesElement/musicTheoryList/imgs/list.png


+ 2 - 0
src/views/components/element/musicResourcesElement/musicTheoryList/index.ts

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

+ 179 - 0
src/views/components/element/musicResourcesElement/musicTheoryList/musicTheoryList.vue

@@ -0,0 +1,179 @@
+<!-- 乐理知识 弹窗列表 -->
+<template>
+  <div class="musicTheoryList">
+    <div class="headCon">
+      <div class="headLeft">
+        <img class="tipImg" src="@/views/Editor/CanvasTool/imgs/ylzs.png" alt="" />
+        <div class="title">乐理知识</div>
+      </div>
+      <div class="headright">
+        <img @click="emits('close')" class="closeBtn" src="../../cloudCoachElement/cloudCoachList/imgs/close.png" alt="" />
+      </div>
+    </div>
+    <div class="content">
+      <div class="leftCon">
+        <courseCollapse
+          :courseList="listKnowledgeData"
+          :activeCollapse="listKnowledgeData[0]?.id || ''"
+          :activeCollapseId="activeCollapseItem?.id || ''"
+          @handleCollapseItem="handleCollapseItem"
+        />
+      </div>
+      <div class="rightCon">
+        <iframe v-if="activeCollapseItem.id" :key="activeCollapseItem.id" class="musicIframe" frameborder="0" :src="url"></iframe>
+      </div>
+    </div>
+    <div class="btnCon">
+      <div class="cancelBtn" @click="emits('close')">取消</div>
+      <div class="addBtn" @click="handleAdd">添加</div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { getListKnowledge } from "@/api/musicResources"
+import { httpAjax } from "@/plugins/httpAjax"
+import { ref, computed } from "vue"
+import courseCollapse from "./courseCollapse.vue"
+import { getMusicResourcesUrl } from "../index"
+
+const emits = defineEmits<{
+  (event: "update", item: Record<string, any>, type: "THEORY"): void
+  (event: "close"): void
+}>()
+
+const listKnowledgeData = ref<any[]>([])
+const activeCollapseItem = ref<Record<string, any>>({})
+initListKnowledge()
+function initListKnowledge() {
+  httpAjax(getListKnowledge).then(res => {
+    if (res.code === 200) {
+      listKnowledgeData.value = res.data || []
+      activeCollapseItem.value = listKnowledgeData.value[0]?.lessonCoursewareDetailKnowledgeDetailList[0] || {}
+    }
+  })
+}
+function handleCollapseItem(item: Record<string, any>) {
+  activeCollapseItem.value = item
+}
+const url = computed(() => {
+  return getMusicResourcesUrl("THEORY", "modal", activeCollapseItem.value?.id)
+})
+
+function handleAdd() {
+  emits("update", activeCollapseItem.value, "THEORY")
+}
+</script>
+
+<style lang="scss" scoped>
+.musicTheoryList {
+  width: 100%;
+  height: 100%;
+  .headCon {
+    width: 100%;
+    height: 64px;
+    border-bottom: 1px solid #eaeaea;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    .headLeft {
+      margin-left: 30px;
+      display: flex;
+      align-items: center;
+      .tipImg {
+        width: 24px;
+        height: 24px;
+      }
+      .title {
+        font-weight: 600;
+        font-size: 18px;
+        color: #131415;
+        margin-left: 8px;
+      }
+    }
+    .headright {
+      margin-right: 30px;
+      display: flex;
+      align-items: center;
+      .closeBtn {
+        width: 24px;
+        height: 24px;
+        cursor: pointer;
+        &:hover {
+          opacity: 0.8;
+        }
+      }
+    }
+  }
+  .content {
+    width: 100%;
+    height: calc(100% - 130px);
+    background: #f5f6fa;
+    display: flex;
+    padding: 24px 12px 0 30px;
+    .leftCon {
+      overflow-y: auto;
+      width: 360px;
+      height: 100%;
+      flex-shrink: 0;
+      background: #ffffff;
+      border-top-left-radius: 16px;
+      border-top-right-radius: 16px;
+      padding: 12px;
+    }
+    .rightCon {
+      margin-left: 20px;
+      height: 100%;
+      flex-grow: 1;
+      .musicIframe {
+        width: 100%;
+        height: 100%;
+      }
+    }
+  }
+  .btnCon {
+    width: 100%;
+    height: 66px;
+    padding: 0 30px;
+    display: flex;
+    justify-content: flex-end;
+    align-items: center;
+    border-top: 1px solid #eaeaea;
+    .addBtn {
+      width: 76px;
+      height: 34px;
+      background: linear-gradient(312deg, #1b7af8 0%, #3cbbff 100%);
+      border-radius: 6px;
+      line-height: 34px;
+      text-align: center;
+      font-weight: 600;
+      font-size: 14px;
+      color: #ffffff;
+      cursor: pointer;
+      &:hover {
+        opacity: 0.8;
+      }
+      &.disabled {
+        opacity: 0.7;
+        cursor: default;
+      }
+    }
+    .cancelBtn {
+      margin-right: 12px;
+      width: 76px;
+      height: 34px;
+      background: #f1f2f6;
+      border-radius: 6px;
+      line-height: 34px;
+      text-align: center;
+      font-weight: 600;
+      font-size: 14px;
+      color: #1e2022;
+      cursor: pointer;
+      &:hover {
+        opacity: 0.8;
+      }
+    }
+  }
+}
+</style>

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