7 次代碼提交 7c996ee367 ... 36c60e9a6a

作者 SHA1 備註 提交日期
  黄琪勇 36c60e9a6a 节奏练习功能 1 周之前
  黄琪勇 e934d0e256 Merge branch 'online' of http://git.dayaedu.com/huangqiyong/pptList into hqyNew 1 周之前
  黄琪勇 aa207a277a build 2 周之前
  黄琪勇 30a1368eee Merge branch 'hqyDev' of http://git.dayaedu.com/huangqiyong/pptList into online 2 周之前
  黄琪勇 c5748d8efa 事件传递修改 2 周之前
  黄琪勇 79ab4787e8 样式修改 2 周之前
  黄琪勇 50b9fd9ab3 input 组件增加clearable 功能 2 周之前
共有 51 個文件被更改,包括 747 次插入38 次删除
  1. 3 0
      .env.devProd
  2. 3 0
      .env.development
  3. 3 0
      .env.production
  4. 3 0
      .env.staging
  5. 1 0
      dist/assets/index-B3pVmhZT.js
  6. 1 1
      dist/assets/index-Be_iFCUi.js
  7. 1 1
      dist/assets/index-BtxWkYHn.js
  8. 0 0
      dist/assets/index-C6I6Hmz_.js
  9. 1 1
      dist/assets/index-COWqouEC.css
  10. 0 0
      dist/assets/index-Clt2Y3kb.css
  11. 1 1
      dist/assets/index-D1ZYVrMR.js
  12. 0 0
      dist/assets/index-DJZMmtP9.css
  13. 0 1
      dist/assets/index-DeZfnPEh.js
  14. 1 1
      dist/assets/index-FztzMJ0_.js
  15. 1 1
      dist/assets/index-Nn-e80Sz.js
  16. 0 0
      dist/assets/index-UoNhaH34.js
  17. 0 0
      dist/assets/index-iPVC8daq.js
  18. 1 1
      dist/assets/shapes-E3FiLaW7.js
  19. 1 1
      dist/index.html
  20. 29 3
      src/components/Input.vue
  21. 二進制
      src/components/imgs/clear.png
  22. 2 0
      src/config/index.ts
  23. 19 1
      src/hooks/useCreateElement.ts
  24. 9 0
      src/messageHooks/closePage.ts
  25. 39 0
      src/messageHooks/mobileScreen.ts
  26. 35 0
      src/messageHooks/rhythmPractice.ts
  27. 23 2
      src/types/slides.ts
  28. 3 1
      src/views/Editor/Canvas/EditableElement.vue
  29. 6 2
      src/views/Editor/Canvas/hooks/useDragElement.ts
  30. 28 1
      src/views/Editor/CanvasTool/index.vue
  31. 2 6
      src/views/Editor/EditorHeader/index.vue
  32. 7 0
      src/views/Editor/Toolbar/ElementStylePanel/RhythmPracticeStylePanel.vue
  33. 3 1
      src/views/Editor/Toolbar/ElementStylePanel/index.vue
  34. 4 1
      src/views/Editor/Toolbar/index.vue
  35. 3 1
      src/views/Mobile/MobileEditor/MobileEditableElement.vue
  36. 3 1
      src/views/Screen/ScreenElement.vue
  37. 3 1
      src/views/components/ThumbnailSlide/ThumbnailElement.vue
  38. 8 2
      src/views/components/element/cloudCoachElement/cloudCoachList/cloudCoachList.vue
  39. 8 2
      src/views/components/element/enjoyElement/resourcesList/resourcesList.vue
  40. 2 2
      src/views/components/element/listeningPracticeElement/BaseListeningPracticeElement.vue
  41. 2 2
      src/views/components/element/listeningPracticeElement/ScreenListeningPracticeElement.vue
  42. 43 0
      src/views/components/element/rhythmPracticeElement/BaseRhythmPracticeElement.vue
  43. 57 0
      src/views/components/element/rhythmPracticeElement/ScreenRhythmPracticeElement.vue
  44. 二進制
      src/views/components/element/rhythmPracticeElement/imgs/musicBg.png
  45. 2 0
      src/views/components/element/rhythmPracticeElement/index.ts
  46. 116 0
      src/views/components/element/rhythmPracticeElement/rhythmPracticeElement.vue
  47. 2 0
      src/views/components/element/rhythmPracticeElement/rhythmPracticeList/index.ts
  48. 138 0
      src/views/components/element/rhythmPracticeElement/rhythmPracticeList/rhythmPracticeList.vue
  49. 2 0
      src/views/components/element/rhythmPracticeElement/rhythmPracticePlayer/index.ts
  50. 126 0
      src/views/components/element/rhythmPracticeElement/rhythmPracticePlayer/rhythmPracticePlayer.vue
  51. 2 0
      src/views/mobileScreen/mobileScreen.vue

+ 3 - 0
.env.devProd

@@ -3,3 +3,6 @@ VITE_APP_URL = "https://dev.kt.colexiu.com"
 
 ## 云教练地址
 VITE_YJL_URL = "https://test.kt.colexiu.com/instrument"
+
+## 移动端app地址
+VITE_CLASSAPP_URL = "https://test.kt.colexiu.com/classroom-app"

+ 3 - 0
.env.development

@@ -3,3 +3,6 @@ VITE_APP_URL = "http://localhost:9527/pptApi"
 
 ## 云教练地址
 VITE_YJL_URL = "https://test.kt.colexiu.com/instrument"
+
+## 移动端app地址
+VITE_CLASSAPP_URL = "https://test.kt.colexiu.com/classroom-app"

+ 3 - 0
.env.production

@@ -3,3 +3,6 @@ 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"

+ 3 - 0
.env.staging

@@ -3,3 +3,6 @@ VITE_APP_URL = "https://test.kt.colexiu.com"
 
 ## 云教练地址
 VITE_YJL_URL = "https://test.kt.colexiu.com/instrument"
+
+## 移动端app地址
+VITE_CLASSAPP_URL = "https://test.kt.colexiu.com/classroom-app"

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

@@ -0,0 +1 @@
+import{u as i,ab as l}from"./index-BtxWkYHn.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 S,bG as _}from"./index-FztzMJ0_.js";import"./shapes-E3FiLaW7.js";const b=()=>{function a(o){const{api:e,playState:n}=o.data||{};e==="headerTogge"?window.parent.postMessage({api:"headerTogge",playState:n},"*"):e==="changeTogge"&&window.parent.postMessage({api:"changeTogge"},"*")}p(()=>{window.addEventListener("message",a)}),d(()=>{window.removeEventListener("message",a)})},v=g({__name:"mobileScreen",setup(a){b();const o=m(),e=t(!0),n=t(!1),c=i();return o.setScreenMode("mobileScreen"),c.initPPTData().then(()=>{e.value=!1}),(w,s)=>(r(),u("div",{class:"mobileScreen",onClick:s[0]||(s[0]=k=>n.value=!1)},[e.value?S("",!0):(r(),f(l,{key:0}))]))}}),x=_(v,[["__scopeId","data-v-90b2692a"]]);export{x as default};

+ 1 - 1
dist/assets/index-DVwjfV52.js → dist/assets/index-Be_iFCUi.js

@@ -1 +1 @@
-import{u as s,ab as r}from"./index-Lehg-uQL.js";import{a5 as c,cx as n,R as p,ae as t,af as _,al as u,ag as i,bG as l}from"./index-BSgMIx3Z.js";const m={class:"pptScreen"},d=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",m,[e.value?i("",!0):(t(),u(r,{key:0}))]))}}),P=l(d,[["__scopeId","data-v-678905e9"]]);export{P as default};
+import{u as s,ab as r}from"./index-BtxWkYHn.js";import{a5 as c,cx as n,R as p,ae as t,af as _,al as u,ag as i,bG as l}from"./index-FztzMJ0_.js";const m={class:"pptScreen"},d=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",m,[e.value?i("",!0):(t(),u(r,{key:0}))]))}}),P=l(d,[["__scopeId","data-v-678905e9"]]);export{P as default};

+ 1 - 1
dist/assets/index-Lehg-uQL.js → dist/assets/index-BtxWkYHn.js

@@ -1,4 +1,4 @@
-import{cC as $x,cD as Ux,aG as zx,cE as Hx,cF as Vx,cG as Gx,cH as jx,ah as Yt,cI as Wx,cJ as Xx,cK as Kx,cL as Yx,aM as Zx,aF as Qx,cM as Jx,aB as k0,be as qx,cN as ew,cO as tw,cP as rw,cQ as nw,cR as iw,cS as aw,cT as ow,aE as sw,cU as lw,s as Me,bj as uw,al as Je,ag as je,af as Se,ak as Y,cV as cw,cW as fw,cX as dw,cY as hw,bi as vw,cZ as B0,bo as ov,aq as Ge,c_ as pw,c$ as gw,a5 as Xe,d0 as mw,d1 as yw,d2 as Sw,d3 as bw,d4 as Cw,d5 as xw,d6 as ww,d7 as Tw,d8 as Iw,d9 as Aw,da as _w,v as Ew,db as Pw,dc as Mw,dd as Rw,aR as Dw,de as Lw,df as Ow,dg as kw,dh as Bw,di as Nw,ay as Wn,dj as Fw,dk as $w,dl as Uw,dm as zw,A as Hw,dn as Vw,dp as Gw,bp as jw,dq as Ww,dr as Xw,ds as Kw,ap as Yw,ac as N0,ai as ht,dt as Zw,au as Ie,du as Qw,X as F0,y as Jw,dv as qw,aP as eT,dw as tT,w as xr,dx as rT,dy as nT,H as iT,dz as aT,bz as un,aD as oT,ae as oe,bI as Oa,$ as mc,dA as sT,bH as ka,dB as lT,aC as uT,aO as cT,R as ge,dC as fT,bq as Ud,a_ as Vr,aj as Ns,aY as ct,aZ as il,an as sv,dD as dT,dE as hT,dF as vT,dG as pT,dH as gT,dI as mT,dJ as yT,Q as ST,dK as bT,dL as CT,dM as xT,at as $t,dN as wT,dO as TT,aT as IT,ad as al,aU as AT,dP as _T,dQ as ET,dR as PT,P as he,a6 as MT,dS as RT,dT as DT,dU as LT,dV as OT,a7 as kT,dW as BT,dX as NT,dY as FT,dZ as $T,d_ as UT,b0 as zT,aA as lv,d$ as HT,e0 as VT,E as hr,x as GT,e1 as jT,e2 as WT,e3 as XT,am as xn,e4 as KT,az as rn,a$ as zd,e5 as YT,as as Tn,e6 as ZT,bx as Hd,cn as Vd,co as QT,bG as tt,bt as Gd,e7 as Yi,bB as JT,bv as zt,e8 as qT,cA as $0,bA as U0,e9 as ai,cq as eI,ea as tI,eb as rI,ec as nI,ed as iI,ee as Mp,cp as vo,ef as aI,bE as Rp,cx as yc,cv as oI,eg as Dp,bD as Lp,cB as z0,bu as H0,bw as vr,eh as Cl}from"./index-BSgMIx3Z.js";/**
+import{cC as $x,cD as Ux,aG as zx,cE as Hx,cF as Vx,cG as Gx,cH as jx,ah as Yt,cI as Wx,cJ as Xx,cK as Kx,cL as Yx,aM as Zx,aF as Qx,cM as Jx,aB as k0,be as qx,cN as ew,cO as tw,cP as rw,cQ as nw,cR as iw,cS as aw,cT as ow,aE as sw,cU as lw,s as Me,bj as uw,al as Je,ag as je,af as Se,ak as Y,cV as cw,cW as fw,cX as dw,cY as hw,bi as vw,cZ as B0,bo as ov,aq as Ge,c_ as pw,c$ as gw,a5 as Xe,d0 as mw,d1 as yw,d2 as Sw,d3 as bw,d4 as Cw,d5 as xw,d6 as ww,d7 as Tw,d8 as Iw,d9 as Aw,da as _w,v as Ew,db as Pw,dc as Mw,dd as Rw,aR as Dw,de as Lw,df as Ow,dg as kw,dh as Bw,di as Nw,ay as Wn,dj as Fw,dk as $w,dl as Uw,dm as zw,A as Hw,dn as Vw,dp as Gw,bp as jw,dq as Ww,dr as Xw,ds as Kw,ap as Yw,ac as N0,ai as ht,dt as Zw,au as Ie,du as Qw,X as F0,y as Jw,dv as qw,aP as eT,dw as tT,w as xr,dx as rT,dy as nT,H as iT,dz as aT,bz as un,aD as oT,ae as oe,bI as Oa,$ as mc,dA as sT,bH as ka,dB as lT,aC as uT,aO as cT,R as ge,dC as fT,bq as Ud,a_ as Vr,aj as Ns,aY as ct,aZ as il,an as sv,dD as dT,dE as hT,dF as vT,dG as pT,dH as gT,dI as mT,dJ as yT,Q as ST,dK as bT,dL as CT,dM as xT,at as $t,dN as wT,dO as TT,aT as IT,ad as al,aU as AT,dP as _T,dQ as ET,dR as PT,P as he,a6 as MT,dS as RT,dT as DT,dU as LT,dV as OT,a7 as kT,dW as BT,dX as NT,dY as FT,dZ as $T,d_ as UT,b0 as zT,aA as lv,d$ as HT,e0 as VT,E as hr,x as GT,e1 as jT,e2 as WT,e3 as XT,am as xn,e4 as KT,az as rn,a$ as zd,e5 as YT,as as Tn,e6 as ZT,bx as Hd,cn as Vd,co as QT,bG as tt,bt as Gd,e7 as Yi,bB as JT,bv as zt,e8 as qT,cA as $0,bA as U0,e9 as ai,cq as eI,ea as tI,eb as rI,ec as nI,ed as iI,ee as Mp,cp as vo,ef as aI,bE as Rp,cx as yc,cv as oI,eg as Dp,bD as Lp,cB as z0,bu as H0,bw as vr,eh as Cl}from"./index-FztzMJ0_.js";/**
 * vue v3.4.34
 * (c) 2018-present Yuxi (Evan) You and Vue contributors
 * @license MIT

文件差異過大導致無法顯示
+ 0 - 0
dist/assets/index-C6I6Hmz_.js


+ 1 - 1
dist/assets/index-Bd3Y-_vx.css → dist/assets/index-COWqouEC.css

@@ -1 +1 @@
-.mobile-thumbnails[data-v-530fb6fe]{padding:10px;white-space:nowrap;overflow-x:auto;overflow-y:hidden}.thumbnail-item[data-v-530fb6fe]{position:relative;display:inline-block;outline:2px solid #aaa}.thumbnail-item+.thumbnail-item[data-v-530fb6fe]{margin-left:10px}.thumbnail-item.active[data-v-530fb6fe]{outline-color:#198cfe}.thumbnail-item.active .label[data-v-530fb6fe]{background-color:#198cfe}.thumbnail-item .label[data-v-530fb6fe]{min-width:20px;height:14px;line-height:14px;position:absolute;right:-1px;top:-1px;color:#fff;background-color:#aaa;z-index:1;font-size:12px;text-align:center;padding:0 5px}.sortable-chosen[data-v-530fb6fe]{top:-5px}[data-v-530fb6fe]::-webkit-scrollbar{width:0;height:0}.mobileScreen[data-v-2471f971]{width:100%;height:100%}.mobileScreen .thumbnails[data-v-2471f971]{width:100%;position:absolute;bottom:0;left:0;z-index:99;background-color:#1d1d1db3;overflow:auto!important;animation:slideInUp-2471f971 .15s}@keyframes slideInUp-2471f971{0%{transform:translateY(100%)}to{transform:translateY(0)}}.mobileScreen .tool-icon[data-v-2471f971]{position:fixed;bottom:8px;right:8px;font-size:25px;color:#666;z-index:10}
+.mobile-thumbnails[data-v-530fb6fe]{padding:10px;white-space:nowrap;overflow-x:auto;overflow-y:hidden}.thumbnail-item[data-v-530fb6fe]{position:relative;display:inline-block;outline:2px solid #aaa}.thumbnail-item+.thumbnail-item[data-v-530fb6fe]{margin-left:10px}.thumbnail-item.active[data-v-530fb6fe]{outline-color:#198cfe}.thumbnail-item.active .label[data-v-530fb6fe]{background-color:#198cfe}.thumbnail-item .label[data-v-530fb6fe]{min-width:20px;height:14px;line-height:14px;position:absolute;right:-1px;top:-1px;color:#fff;background-color:#aaa;z-index:1;font-size:12px;text-align:center;padding:0 5px}.sortable-chosen[data-v-530fb6fe]{top:-5px}[data-v-530fb6fe]::-webkit-scrollbar{width:0;height:0}.mobileScreen[data-v-90b2692a]{width:100%;height:100%}.mobileScreen .thumbnails[data-v-90b2692a]{width:100%;position:absolute;bottom:0;left:0;z-index:99;background-color:#1d1d1db3;overflow:auto!important;animation:slideInUp-90b2692a .15s}@keyframes slideInUp-90b2692a{0%{transform:translateY(100%)}to{transform:translateY(0)}}.mobileScreen .tool-icon[data-v-90b2692a]{position:fixed;bottom:8px;right:8px;font-size:25px;color:#666;z-index:10}

文件差異過大導致無法顯示
+ 0 - 0
dist/assets/index-Clt2Y3kb.css


+ 1 - 1
dist/assets/index-DcsdPdf7.js → dist/assets/index-D1ZYVrMR.js

@@ -1 +1 @@
-import{_ as a}from"./404-R0y5dkvZ.js";import{a5 as o,el as t,ae as r,af as c,bH as _,bI as d,ak as s,bG as p}from"./index-BSgMIx3Z.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,el as t,ae as r,af as c,bH as _,bI as d,ak as s,bG as p}from"./index-FztzMJ0_.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};

文件差異過大導致無法顯示
+ 0 - 0
dist/assets/index-DJZMmtP9.css


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

@@ -1 +0,0 @@
-import{u as c,ab as l}from"./index-Lehg-uQL.js";import{a5 as i,cx as m,R as t,ae as a,af as p,al as u,ag as f,bG as _}from"./index-BSgMIx3Z.js";import"./shapes-DIB7l2CO.js";const S=i({__name:"mobileScreen",setup(d){const s=m(),e=t(!0),n=t(!1),r=c();return s.setScreenMode("mobileScreen"),r.initPPTData().then(()=>{e.value=!1}),(k,o)=>(a(),p("div",{class:"mobileScreen",onClick:o[0]||(o[0]=b=>n.value=!1)},[e.value?f("",!0):(a(),u(l,{key:0}))]))}}),B=_(S,[["__scopeId","data-v-2471f971"]]);export{B as default};

文件差異過大導致無法顯示
+ 1 - 1
dist/assets/index-FztzMJ0_.js


+ 1 - 1
dist/assets/index-CNRUTgNt.js → dist/assets/index-Nn-e80Sz.js

@@ -1 +1 @@
-import{_ as t}from"./404-R0y5dkvZ.js";import{E as c}from"./index-CEUjMIB9.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-BSgMIx3Z.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-UoNhaH34.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-FztzMJ0_.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};

文件差異過大導致無法顯示
+ 0 - 0
dist/assets/index-UoNhaH34.js


文件差異過大導致無法顯示
+ 0 - 0
dist/assets/index-iPVC8daq.js


+ 1 - 1
dist/assets/shapes-DIB7l2CO.js → dist/assets/shapes-E3FiLaW7.js

@@ -1,4 +1,4 @@
-import{cA as O,bA as j1,cB as h2}from"./index-BSgMIx3Z.js";import{ac as W}from"./index-Lehg-uQL.js";var Y1={exports:{}};/*!
+import{cA as O,bA as j1,cB as h2}from"./index-FztzMJ0_.js";import{ac as W}from"./index-BtxWkYHn.js";var Y1={exports:{}};/*!
  * clipboard.js v2.0.11
  * https://clipboardjs.com/
  *

+ 1 - 1
dist/index.html

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

+ 29 - 3
src/components/Input.vue

@@ -19,9 +19,10 @@
       @input="$event => handleInput($event)"
       @focus="$event => handleFocus($event)"
       @blur="$event => handleBlur($event)"
-      @change="$event => emit('change', $event)"
+      @change="$event => emit('change', ($event.target as HTMLInputElement).value)"
       @keydown.enter="$event => emit('enter', $event)"
     />
+    <div v-if="clearable && value" v-show="focused" @click="handleClear" class="clear"></div>
     <span class="suffix">
       <slot name="suffix"></slot>
     </span>
@@ -37,21 +38,24 @@ withDefaults(
     disabled?: boolean
     placeholder?: string
     simple?: boolean
+    clearable?: boolean
   }>(),
   {
     disabled: false,
     placeholder: "",
-    simple: false
+    simple: false,
+    clearable: false
   }
 )
 
 const emit = defineEmits<{
   (event: "update:value", payload: string): void
   (event: "input", payload: Event): void
-  (event: "change", payload: Event): void
+  (event: "change", payload: string): void
   (event: "blur", payload: Event): void
   (event: "focus", payload: Event): void
   (event: "enter", payload: Event): void
+  (event: "clear"): void
 }>()
 
 const focused = ref(false)
@@ -68,6 +72,12 @@ const handleFocus = (e: Event) => {
   emit("focus", e)
 }
 
+function handleClear() {
+  emit("update:value", "")
+  emit("change", "")
+  emit("clear")
+}
+
 const inputRef = ref<HTMLInputElement>()
 const focus = () => {
   if (inputRef.value) inputRef.value.focus()
@@ -87,6 +97,22 @@ defineExpose({
   transition: border-color 0.25s;
   font-size: 13px;
   display: flex;
+  &:not(.disabled):hover {
+    .clear {
+      display: initial !important;
+    }
+  }
+  .clear {
+    width: 16px;
+    height: 16px;
+    background: url("./imgs/clear.png") no-repeat;
+    background-size: 100% 100%;
+    margin-right: 5px;
+    cursor: pointer;
+    &:hover {
+      opacity: 0.8;
+    }
+  }
 
   input {
     min-width: 0;

二進制
src/components/imgs/clear.png


+ 2 - 0
src/config/index.ts

@@ -1,3 +1,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

+ 19 - 1
src/hooks/useCreateElement.ts

@@ -379,6 +379,23 @@ export default () => {
       top: 0
     })
   }
+
+  /**
+   * 节奏练习
+   */
+  const createRhythmPracticeElement = (dataJson: string) => {
+    createElement({
+      type: "elf",
+      subtype: "elf-rhythm-practice",
+      dataJson,
+      id: nanoid(10),
+      width: viewportSize.value,
+      height: viewportSize.value * viewportRatio.value,
+      rotate: 0,
+      left: 0,
+      top: 0
+    })
+  }
   return {
     createImageElement,
     createChartElement,
@@ -391,6 +408,7 @@ export default () => {
     createAudioElement,
     createCloudCoachElement,
     createEnjoyElement,
-    createListeningPracticeElement
+    createListeningPracticeElement,
+    createRhythmPracticeElement
   }
 }

+ 9 - 0
src/messageHooks/closePage.ts

@@ -0,0 +1,9 @@
+// 关闭 像父级派发 页面
+export const iframeExitMes = () => {
+  window.parent.postMessage(
+    {
+      api: "iframe_exit"
+    },
+    "*"
+  )
+}

+ 39 - 0
src/messageHooks/mobileScreen.ts

@@ -0,0 +1,39 @@
+/* 移动端预览的时候 监听按键事件 传给学生端控制操作按钮 */
+import { onMounted, onUnmounted } from "vue"
+
+export const changeToggeMes = () => {
+  function handleMessage(event: any) {
+    const { api, playState } = event.data || {}
+    if (api === "headerTogge") {
+      window.parent.postMessage(
+        {
+          api: "headerTogge",
+          playState: playState
+        },
+        "*"
+      )
+    } else if (api === "changeTogge") {
+      // 云教练切换
+      window.parent.postMessage(
+        {
+          api: "changeTogge"
+        },
+        "*"
+      )
+    } else if (["clickTempo", "clickViewFigner"].includes(api)) {
+      // 节奏练习 听音练习 切换
+      window.parent.postMessage(
+        {
+          api
+        },
+        "*"
+      )
+    }
+  }
+  onMounted(() => {
+    window.addEventListener("message", handleMessage)
+  })
+  onUnmounted(() => {
+    window.removeEventListener("message", handleMessage)
+  })
+}

+ 35 - 0
src/messageHooks/rhythmPractice.ts

@@ -0,0 +1,35 @@
+import { onMounted, onUnmounted } from "vue"
+
+export const rhythmPracticeMes = () => {
+  let resFun: (value: string) => void
+
+  // 获取节奏练习设置
+  function getRhythmPracticeSetting(rhythmPracticeRef?: HTMLIFrameElement): Promise<string> {
+    return new Promise(res => {
+      resFun = res
+      if (rhythmPracticeRef) {
+        rhythmPracticeRef.contentWindow?.postMessage({ api: "getTempoSetting" }, "*")
+      } else {
+        resFun("")
+      }
+    })
+  }
+  function handleMessage(event: any) {
+    const { api, data } = event.data || {}
+    if (api === "getTempoSetting") {
+      const dataRes = data ? JSON.parse(data) : {}
+      resFun(JSON.stringify(dataRes.setting) || "")
+    }
+  }
+
+  onMounted(() => {
+    window.addEventListener("message", handleMessage)
+  })
+  onUnmounted(() => {
+    window.removeEventListener("message", handleMessage)
+  })
+
+  return {
+    getRhythmPracticeSetting
+  }
+}

+ 23 - 2
src/types/slides.ts

@@ -36,7 +36,8 @@ export const enum ElementSubtypeTypes {
   AUDIO = "elf-audio",
   SING_PLAY = "elf-sing-play",
   ENJOY = "elf-enjoy",
-  LISTENING_PRACTICE = "elf-listening-practice"
+  LISTENING_PRACTICE = "elf-listening-practice",
+  RHYTHM_PRACTICE = "elf-rhythm-practice"
 }
 
 /**
@@ -670,7 +671,10 @@ export interface PPTEnjoyElement extends PPTBaseElement {
  *
  * code: 乐器code
  *
- * 乐器img
+ * instrumentImg:乐器图片
+ *
+ * name: 乐器名称
+ *
  *
  */
 export interface PPTListeningPracticeElement extends PPTBaseElement {
@@ -681,6 +685,22 @@ export interface PPTListeningPracticeElement extends PPTBaseElement {
   instrumentImg: string
 }
 
+/**
+ * 节奏练习
+ *
+ * type: elf
+ *
+ * subtype: elf-rhythm-practice
+ *
+ * dataJson:默认配置设置
+ *
+ */
+export interface PPTRhythmPracticeElement extends PPTBaseElement {
+  type: "elf"
+  subtype: "elf-rhythm-practice"
+  dataJson: string
+}
+
 export type PPTElement =
   | PPTTextElement
   | PPTImageElement
@@ -694,6 +714,7 @@ export type PPTElement =
   | PPTCloudCoachElement
   | PPTEnjoyElement
   | PPTListeningPracticeElement
+  | PPTRhythmPracticeElement
 
 export type AnimationType = "in" | "out" | "attention"
 export type AnimationTrigger = "click" | "meantime" | "auto"

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

@@ -38,6 +38,7 @@ import AudioElement from "@/views/components/element/AudioElement/index.vue"
 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"
 
 const props = defineProps<{
   elementInfo: PPTElement
@@ -63,7 +64,8 @@ const currentElementComponent = computed<unknown>(() => {
     [ElementSubtypeTypes.VIDEO]: VideoElement,
     [ElementSubtypeTypes.SING_PLAY]: cloudCoachElement,
     [ElementSubtypeTypes.ENJOY]: enjoyElement,
-    [ElementSubtypeTypes.LISTENING_PRACTICE]: listeningPracticeElement
+    [ElementSubtypeTypes.LISTENING_PRACTICE]: listeningPracticeElement,
+    [ElementSubtypeTypes.RHYTHM_PRACTICE]: rhythmPracticeElement
   }
   return elementTypeMap[props.elementInfo.type] || elementSubtypeMap[props.elementInfo.subtype] || null
 })

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

@@ -22,7 +22,11 @@ export default (elementList: Ref<PPTElement[]>, alignmentLines: Ref<AlignmentLin
     let isMouseDown = true
     /* 选中移动的时候 云教练模块标记移动中 */
     elementList.value.map(item => {
-      if (activeElementIdList.value.includes(item.id) && item.type === "elf" && ["elf-sing-play", "elf-listening-practice"].includes(item.subtype)) {
+      if (
+        activeElementIdList.value.includes(item.id) &&
+        item.type === "elf" &&
+        ["elf-sing-play", "elf-listening-practice", "elf-rhythm-practice"].includes(item.subtype)
+      ) {
         item.isMove = true
       }
     })
@@ -294,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"].includes(item.subtype)
+          ["elf-sing-play", "elf-listening-practice", "elf-rhythm-practice"].includes(item.subtype)
         ) {
           item.isMove = false
         }

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

@@ -313,6 +313,26 @@
         "
       />
     </Modal>
+    <Modal
+      :contentStyle="{
+        width: '1100px',
+        height: '750px',
+        boxShadow: '0px 2px 10px 0px rgba(0,0,0,0.08)',
+        borderRadius: '16px',
+        border: '1px solid #DEDEDE',
+        padding: '0'
+      }"
+      v-model:visible="rhythmPracticeVisible"
+    >
+      <rhythmPracticeList
+        @update="handleAddRhythmPractice"
+        @close="
+          () => {
+            rhythmPracticeVisible = false
+          }
+        "
+      />
+    </Modal>
   </div>
 </template>
 
@@ -341,6 +361,7 @@ import { ElUpload, ElMessage, type UploadRequestOptions } from "element-plus"
 import cloudCoachList from "@/views/components/element/cloudCoachElement/cloudCoachList"
 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 fileUpload from "@/utils/oss-file-upload"
 import usePptWork from "@/store/pptWork"
 
@@ -374,7 +395,8 @@ const {
   createAudioElement,
   createCloudCoachElement,
   createEnjoyElement,
-  createListeningPracticeElement
+  createListeningPracticeElement,
+  createRhythmPracticeElement
 } = useCreateElement()
 
 const insertImageElement = (files: FileList) => {
@@ -427,6 +449,11 @@ function handleAddListeningPractice(item: Record<string, any>) {
   createListeningPracticeElement(item.code, item.img, item.name)
   listeningPracticeVisible.value = false
 }
+// 处理节奏练习
+function handleAddRhythmPractice(value: string) {
+  createRhythmPracticeElement(value)
+  rhythmPracticeVisible.value = false
+}
 // 处理资源创建
 function handleResources(item: Record<string, any>) {
   if (item.type === "SONG") {

+ 2 - 6
src/views/Editor/EditorHeader/index.vue

@@ -74,6 +74,7 @@ import PopoverMenuItem from "@/components/PopoverMenuItem.vue"
 import { ref, computed } from "vue"
 import { ElMessageBox } from "element-plus"
 import usePptWork from "@/store/pptWork"
+import { iframeExitMes } from "@/messageHooks/closePage"
 
 const slidesStore = useSlidesStore()
 const { title } = storeToRefs(slidesStore)
@@ -124,12 +125,7 @@ function handleSave() {
 /* 关闭页面 */
 function handleClose() {
   window.close()
-  window.parent.postMessage(
-    {
-      api: "iframe_exit",
-    },
-    "*"
-  );
+  iframeExitMes()
 }
 </script>
 

+ 7 - 0
src/views/Editor/Toolbar/ElementStylePanel/RhythmPracticeStylePanel.vue

@@ -0,0 +1,7 @@
+<template>
+  <div class="RhythmPracticeStylePanel">节奏练习配置</div>
+</template>
+
+<script setup lang="ts"></script>
+
+<style lang="scss" scoped></style>

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

@@ -23,6 +23,7 @@ import MultiStylePanel from "./MultiStylePanel.vue"
 import CloudCoachStylePanel from "./CloudCoachStylePanel.vue"
 import EnjoyStylePanel from "./EnjoyStylePanel.vue"
 import ListeningPracticeStylePanel from "./ListeningPracticeStylePanel.vue"
+import RhythmPracticeStylePanel from "./RhythmPracticeStylePanel.vue"
 
 const panelMap = {
   [ElementTypes.TEXT]: TextStylePanel,
@@ -39,7 +40,8 @@ const elementSubtypeMap = {
   [ElementSubtypeTypes.VIDEO]: VideoStylePanel,
   [ElementSubtypeTypes.SING_PLAY]: CloudCoachStylePanel,
   [ElementSubtypeTypes.ENJOY]: EnjoyStylePanel,
-  [ElementSubtypeTypes.LISTENING_PRACTICE]: ListeningPracticeStylePanel
+  [ElementSubtypeTypes.LISTENING_PRACTICE]: ListeningPracticeStylePanel,
+  [ElementSubtypeTypes.RHYTHM_PRACTICE]: RhythmPracticeStylePanel
 }
 const { activeElementIdList, activeElementList, handleElement, activeGroupElementId } = storeToRefs(useMainStore())
 

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

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

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

@@ -25,6 +25,7 @@ import AudioElement from "@/views/components/element/AudioElement/index.vue"
 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"
 
 const props = defineProps<{
   elementInfo: PPTElement
@@ -48,7 +49,8 @@ const currentElementComponent = computed<unknown>(() => {
     [ElementSubtypeTypes.VIDEO]: VideoElement,
     [ElementSubtypeTypes.SING_PLAY]: cloudCoachElement,
     [ElementSubtypeTypes.ENJOY]: enjoyElement,
-    [ElementSubtypeTypes.LISTENING_PRACTICE]: listeningPracticeElement
+    [ElementSubtypeTypes.LISTENING_PRACTICE]: listeningPracticeElement,
+    [ElementSubtypeTypes.RHYTHM_PRACTICE]: rhythmPracticeElement
   }
   return elementTypeMap[props.elementInfo.type] || elementSubtypeMap[props.elementInfo.subtype] || null
 })

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

@@ -35,6 +35,7 @@ import ScreenAudioElement from "@/views/components/element/AudioElement/ScreenAu
 import ScreenCloudCoachElement from "@/views/components/element/cloudCoachElement/ScreenCloudCoachElement.vue"
 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"
 
 const props = defineProps<{
   elementInfo: PPTElement
@@ -60,7 +61,8 @@ const currentElementComponent = computed<unknown>(() => {
     [ElementSubtypeTypes.VIDEO]: ScreenVideoElement,
     [ElementSubtypeTypes.SING_PLAY]: ScreenCloudCoachElement,
     [ElementSubtypeTypes.ENJOY]: ScreenEnjoyElement,
-    [ElementSubtypeTypes.LISTENING_PRACTICE]: ScreenListeningPracticeElement
+    [ElementSubtypeTypes.LISTENING_PRACTICE]: ScreenListeningPracticeElement,
+    [ElementSubtypeTypes.RHYTHM_PRACTICE]: ScreenRhythmPracticeElement
   }
   return elementTypeMap[props.elementInfo.type] || elementSubtypeMap[props.elementInfo.subtype] || null
 })

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

@@ -26,6 +26,7 @@ import BaseAudioElement from "@/views/components/element/AudioElement/BaseAudioE
 import BaseCloudCoachElement from "@/views/components/element/cloudCoachElement/BaseCloudCoachElement.vue"
 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"
 
 const props = defineProps<{
   elementInfo: PPTElement
@@ -48,7 +49,8 @@ const currentElementComponent = computed<unknown>(() => {
     [ElementSubtypeTypes.VIDEO]: BaseVideoElement,
     [ElementSubtypeTypes.SING_PLAY]: BaseCloudCoachElement,
     [ElementSubtypeTypes.ENJOY]: BaseEnjoyElement,
-    [ElementSubtypeTypes.LISTENING_PRACTICE]: BaseListeningPracticeElement
+    [ElementSubtypeTypes.LISTENING_PRACTICE]: BaseListeningPracticeElement,
+    [ElementSubtypeTypes.RHYTHM_PRACTICE]: BaseRhythmPracticeElement
   }
   return elementTypeMap[props.elementInfo.type] || elementSubtypeMap[props.elementInfo.subtype] || null
 })

+ 8 - 2
src/views/components/element/cloudCoachElement/cloudCoachList/cloudCoachList.vue

@@ -23,7 +23,7 @@
           </div>
         </div>
         <div class="query">
-          <Input :placeholder="'请输入搜索关键词'" v-model:value="queryData.name" @enter="handleQuery">
+          <Input :placeholder="'请输入搜索关键词'" v-model:value="queryData.name" clearable @enter="handleQuery" @clear="handleQuery">
             <template #prefix>
               <img class="img" src="./imgs/query.png" alt="" />
             </template>
@@ -140,7 +140,13 @@
             <Empty v-if="!musicList.length && !loading" />
           </div>
           <div class="pagination" v-show="musicList.length">
-            <el-pagination layout="prev, pager, next" :default-page-size="21" @current-change="handleCurrentChange" :total="queryData.total" />
+            <el-pagination
+              layout="prev, pager, next"
+              :default-page-size="queryData.rows"
+              :current-page="queryData.page"
+              @current-change="handleCurrentChange"
+              :total="queryData.total"
+            />
           </div>
         </div>
       </div>

+ 8 - 2
src/views/components/element/enjoyElement/resourcesList/resourcesList.vue

@@ -36,7 +36,7 @@
           </div>
         </div>
         <div class="query">
-          <Input :placeholder="'请输入搜索关键词'" v-model:value="queryData.name" @enter="handleQuery">
+          <Input :placeholder="'请输入搜索关键词'" clearable v-model:value="queryData.name" @enter="handleQuery" @clear="handleQuery">
             <template #prefix>
               <img class="img" src="../../cloudCoachElement/cloudCoachList/imgs/query.png" alt="" />
             </template>
@@ -132,7 +132,13 @@
             <Empty v-if="!musicList.length && !loading" />
           </div>
           <div class="pagination" v-show="musicList.length">
-            <el-pagination layout="prev, pager, next" :default-page-size="20" @current-change="handleCurrentChange" :total="queryData.total" />
+            <el-pagination
+              layout="prev, pager, next"
+              :default-page-size="queryData.rows"
+              :current-page="queryData.page"
+              @current-change="handleCurrentChange"
+              :total="queryData.total"
+            />
           </div>
         </div>
       </div>

+ 2 - 2
src/views/components/element/listeningPracticeElement/BaseListeningPracticeElement.vue

@@ -1,6 +1,6 @@
 <template>
   <div
-    class="base-element-cloudCoach"
+    class="base-element-listeningPractice"
     :style="{
       top: elementInfo.top + 'px',
       left: elementInfo.left + 'px',
@@ -25,7 +25,7 @@ defineProps<{
 </script>
 
 <style lang="scss" scoped>
-.base-element-cloudCoach {
+.base-element-listeningPractice {
   position: absolute;
 }
 .rotate-wrapper {

+ 2 - 2
src/views/components/element/listeningPracticeElement/ScreenListeningPracticeElement.vue

@@ -1,6 +1,6 @@
 <template>
   <div
-    class="base-element-cloudCoach screen-element-cloudCoach"
+    class="base-element-cloudCoach screen-element-listeningPractice"
     :style="{
       top: elementInfo.top + 'px',
       left: elementInfo.left + 'px',
@@ -43,7 +43,7 @@ const inCurrentSlide = computed(() => currentSlide.value.id === slideId.value)
 </script>
 
 <style lang="scss" scoped>
-.screen-element-cloudCoach {
+.screen-element-listeningPractice {
   position: absolute;
 }
 .rotate-wrapper {

+ 43 - 0
src/views/components/element/rhythmPracticeElement/BaseRhythmPracticeElement.vue

@@ -0,0 +1,43 @@
+<template>
+  <div
+    class="base-element-rhythmPractice"
+    :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>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import type { PPTListeningPracticeElement } from "@/types/slides"
+
+defineProps<{
+  elementInfo: PPTListeningPracticeElement
+}>()
+</script>
+
+<style lang="scss" scoped>
+.base-element-rhythmPractice {
+  position: absolute;
+}
+.rotate-wrapper {
+  width: 100%;
+  height: 100%;
+}
+.element-content {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  background: url("./imgs/musicBg.png") no-repeat;
+  background-size: cover;
+  position: relative;
+}
+</style>

+ 57 - 0
src/views/components/element/rhythmPracticeElement/ScreenRhythmPracticeElement.vue

@@ -0,0 +1,57 @@
+<template>
+  <div
+    class="base-element-cloudCoach screen-element-rhythmPractice"
+    :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">
+        <rhythmPracticePlayer
+          v-if="inCurrentSlide"
+          :width="elementInfo.width"
+          :height="elementInfo.height"
+          :scale="scale"
+          :data-json="elementInfo.dataJson"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { computed, inject, ref } from "vue"
+import { storeToRefs } from "pinia"
+import { useSlidesStore } from "@/store"
+import type { PPTRhythmPracticeElement } from "@/types/slides"
+import { injectKeySlideId, injectKeySlideScale } from "@/types/injectKey"
+import rhythmPracticePlayer from "./rhythmPracticePlayer"
+
+defineProps<{
+  elementInfo: PPTRhythmPracticeElement
+}>()
+
+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-rhythmPractice {
+  position: absolute;
+}
+.rotate-wrapper {
+  width: 100%;
+  height: 100%;
+}
+.element-content {
+  width: 100%;
+  height: 100%;
+}
+</style>

二進制
src/views/components/element/rhythmPracticeElement/imgs/musicBg.png


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

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

+ 116 - 0
src/views/components/element/rhythmPracticeElement/rhythmPracticeElement.vue

@@ -0,0 +1,116 @@
+<template>
+  <div
+    class="rhythmPracticeElement"
+    :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>
+        <rhythmPracticePlayer :width="elementInfo.width" :height="elementInfo.height" :scale="canvasScale" :dataJson="elementInfo.dataJson" />
+        <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 { PPTRhythmPracticeElement } from "@/types/slides"
+import type { ContextmenuItem } from "@/components/Contextmenu/types"
+import rhythmPracticePlayer from "./rhythmPracticePlayer"
+import { computed } from "vue"
+
+const props = defineProps<{
+  elementInfo: PPTRhythmPracticeElement
+  selectElement: (e: MouseEvent | TouchEvent, element: PPTRhythmPracticeElement, 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>
+.rhythmPracticeElement {
+  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>

+ 2 - 0
src/views/components/element/rhythmPracticeElement/rhythmPracticeList/index.ts

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

+ 138 - 0
src/views/components/element/rhythmPracticeElement/rhythmPracticeList/rhythmPracticeList.vue

@@ -0,0 +1,138 @@
+<template>
+  <div class="rhythmPracticeList">
+    <div class="headCon">
+      <div class="headLeft">
+        <img class="tipImg" src="@/views/Editor/CanvasTool/imgs/jzlx.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">
+      <rhythmPracticePlayer @handleIframeLoad="handleIframeLoad" :settingMode="true" ref="rhythmPracticePlayerDom" :width="1098" :height="618" />
+    </div>
+    <div class="btnCon">
+      <div class="cancelBtn" @click="emits('close')">取消</div>
+      <div class="addBtn" :class="{ disabled: loading }" @click="handleAddrhythmPractice">添加</div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import rhythmPracticePlayer from "../rhythmPracticePlayer"
+import { ref } from "vue"
+import { rhythmPracticeMes } from "@/messageHooks/rhythmPractice"
+
+const emits = defineEmits<{
+  (event: "update", value: string): void
+  (event: "close"): void
+}>()
+
+const loading = ref(true)
+const rhythmPracticePlayerDom = ref<InstanceType<typeof rhythmPracticePlayer>>()
+const { getRhythmPracticeSetting } = rhythmPracticeMes()
+
+async function handleAddrhythmPractice() {
+  if (loading.value) return
+  try {
+    const rhythmPracticeSetting = await getRhythmPracticeSetting(rhythmPracticePlayerDom.value?.rhythmPracticeIframeDom)
+    emits("update", rhythmPracticeSetting)
+  } catch (e) {
+    console.log(e)
+  }
+}
+function handleIframeLoad() {
+  loading.value = false
+}
+</script>
+
+<style lang="scss" scoped>
+.rhythmPracticeList {
+  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);
+  }
+  .btnCon {
+    width: 100%;
+    height: 66px;
+    padding: 0 30px;
+    display: flex;
+    justify-content: flex-end;
+    align-items: center;
+    .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>

+ 2 - 0
src/views/components/element/rhythmPracticeElement/rhythmPracticePlayer/index.ts

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

+ 126 - 0
src/views/components/element/rhythmPracticeElement/rhythmPracticePlayer/rhythmPracticePlayer.vue

@@ -0,0 +1,126 @@
+<template>
+  <div
+    class="rhythmPracticePlayer"
+    :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 ref="rhythmPracticeIframeDom" class="musicIframe" frameborder="0" :src="url" @load="handleIframeLoad"></iframe>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed } from "vue"
+import { getToken } from "@/libs/auth"
+import { CLASSAPP_URL_API } from "@/config/index"
+import queryParams from "@/queryParams"
+
+const props = withDefaults(
+  defineProps<{
+    width: number
+    height: number
+    scale?: number
+    settingMode?: boolean
+    dataJson?: string
+  }>(),
+  {
+    scale: 1,
+    settingMode: false
+  }
+)
+const emits = defineEmits<{
+  (event: "handleIframeLoad"): void
+}>()
+
+const url = computed(() => {
+  // 当是设置模式的时候
+  if (props.settingMode) {
+    return `${CLASSAPP_URL_API}/#/tempo-practice?v=${Date.now()}&Authorization=${getToken()}&platform=modal`
+  }
+  if (queryParams.fromType == "CLASS") {
+    return `${CLASSAPP_URL_API}/#/tempo-practice?v=${Date.now()}&Authorization=${getToken()}&modeType=courseware&dataJson=${props.dataJson}`
+  } else {
+    return `${CLASSAPP_URL_API}/#/tempo-practice?v=${Date.now()}&Authorization=${getToken()}&platform=modal&dataJson=${props.dataJson}&win=pc`
+  }
+})
+
+const rhythmPracticeIframeDom = ref<HTMLIFrameElement>()
+// 先关闭这个功能
+const loading = ref(true)
+function handleIframeLoad() {
+  loading.value = false
+  emits("handleIframeLoad")
+}
+
+defineExpose({
+  rhythmPracticeIframeDom
+})
+</script>
+
+<style lang="scss" scoped>
+.rhythmPracticePlayer {
+  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>

+ 2 - 0
src/views/mobileScreen/mobileScreen.vue

@@ -14,7 +14,9 @@ import Screen from "../Screen/index.vue"
 import usePptWork from "@/store/pptWork"
 import { useScreenStore } from "@/store"
 import MobileThumbnails from "../Mobile/MobileThumbnails.vue"
+import { changeToggeMes } from "@/messageHooks/mobileScreen"
 
+changeToggeMes()
 const screenStore = useScreenStore()
 
 const initLoading = ref(true)

部分文件因文件數量過多而無法顯示