yonge 6 дней назад
Родитель
Сommit
0a36dcac8f

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
dist/assets/index-C7tzRCFc.css


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
dist/assets/index-C_slUrwq.css


+ 1 - 1
dist/assets/index-BLfK_g0y.js → dist/assets/index-Cl_-BXNF.js

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

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
dist/assets/index-DNizitQs.js


+ 1 - 1
dist/assets/index-DUb7yzkd.js → dist/assets/index-DWKiXTzU.js

@@ -1 +1 @@
-import{_ as t}from"./404-R0y5dkvZ.js";import{E as c}from"./index-BoDmUEjK.js";import{a5 as n,ae as i,af as r,ak as o,aq as _,am as d,P as l,aS as p,bM as m,bN as f,bH as u}from"./index-Xrdt_JH7.js";const s=a=>(m("data-v-340bbdaa"),a=a(),f(),a),h={class:"login"},b={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",h,[o("div",b,[g,k,_(l(c),{class:"backBtn",type:"primary",plain:"",onClick:e},{default:d(()=>[p("关闭页面")]),_:1})])]))}}),S=u(v,[["__scopeId","data-v-340bbdaa"]]);export{S as default};
+import{_ as t}from"./404-R0y5dkvZ.js";import{E as c}from"./index-IMrmjUBC.js";import{a5 as n,ae as i,af as r,ak as o,aq as _,am as d,P as l,aS as p,bM as m,bN as f,bH as u}from"./index-DWQm3SGv.js";const s=a=>(m("data-v-340bbdaa"),a=a(),f(),a),h={class:"login"},b={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",h,[o("div",b,[g,k,_(l(c),{class:"backBtn",type:"primary",plain:"",onClick:e},{default:d(()=>[p("关闭页面")]),_:1})])]))}}),S=u(v,[["__scopeId","data-v-340bbdaa"]]);export{S as default};

Разница между файлами не показана из-за своего большого размера
+ 1 - 1
dist/assets/index-DWQm3SGv.js


+ 1 - 1
dist/assets/index-BnpuTYrS.js → dist/assets/index-DwBUAZFG.js

@@ -1,4 +1,4 @@
-import{cF as dw,cG as hw,aG as vw,cH as pw,cI as gw,cJ as mw,cK as yw,ah as Yt,cL as Sw,cM as bw,cN as xw,cO as Cw,aM as ww,aF as Tw,cP as Iw,aB as c1,bg as _w,cQ as Aw,cR as Ew,cS as Pw,cT as Mw,cU as Rw,cV as Dw,cW as Lw,aE as kw,cX as Ow,s as Ae,bl as Bw,al as je,ag as ze,af as ve,ak as Z,cY as Nw,cZ as Fw,c_ as $w,c$ as zw,bk as Uw,cw as ul,aS as Cv,aq as Ye,d0 as Hw,d1 as Vw,a5 as $e,d2 as Gw,d3 as Ww,d4 as jw,d5 as Kw,d6 as Xw,d7 as Yw,d8 as Zw,d9 as Qw,da as Jw,db as qw,dc as eT,v as tT,dd as rT,de as nT,df as iT,aW as aT,dg as oT,dh as sT,di as lT,dj as uT,dk as cT,ay as sr,dl as fT,dm as dT,dn as hT,dp as vT,A as pT,dq as gT,dr as mT,bq as yT,ds as ST,dt as bT,du as xT,ap as CT,ac as f1,ai as dt,dv as wT,au as Ce,dw as TT,X as d1,y as IT,dx as _T,aP as AT,dy as ET,w as _r,dz as PT,dA as MT,H as RT,dB as DT,bA as hn,aD as LT,ae as ie,bN as ea,$ as wc,dC as kT,bM as ta,dD as OT,aC as BT,aO as NT,R as fe,dE as FT,br as rh,b3 as Lr,aj as Hs,b1 as lt,b2 as Ua,an as wv,dF as $T,dG as zT,dH as UT,dI as HT,dJ as VT,dK as GT,dL as WT,Q as jT,dM as KT,dN as XT,dO as YT,at as Mt,dP as ZT,aR as QT,aY as JT,ad as cl,aZ as qT,dQ as eI,dR as tI,dS as rI,P as le,a6 as nI,dT as iI,dU as aI,dV as oI,dW as sI,a7 as lI,dX as uI,dY as cI,dZ as fI,d_ as dI,d$ as hI,b4 as vI,aA as Tv,e0 as pI,e1 as gI,E as gr,x as mI,e2 as yI,e3 as SI,e4 as bI,am as _n,e5 as xI,az as hr,aU as nh,e6 as CI,as as jr,e7 as wI,e8 as Iv,cE as mr,by as ih,ct as ah,bI as TI,bH as Ve,bu as oh,bv as _v,bx as Wt,bB as Tc,bL as di,e9 as wu,bE as hi,ea as II,bJ as ra,bC as _I,bw as Bt,eb as AI,ec as ui,cu as EI,ed as PI,ee as MI,ef as RI,eg as DI,bK as Zp,eh as LI,bF as Qp,cB as Ic,cA as kI,ei as Hu,ej as Il}from"./index-Xrdt_JH7.js";/**
+import{cF as dw,cG as hw,aG as vw,cH as pw,cI as gw,cJ as mw,cK as yw,ah as Yt,cL as Sw,cM as bw,cN as xw,cO as Cw,aM as ww,aF as Tw,cP as Iw,aB as c1,bg as _w,cQ as Aw,cR as Ew,cS as Pw,cT as Mw,cU as Rw,cV as Dw,cW as Lw,aE as kw,cX as Ow,s as Ae,bl as Bw,al as je,ag as ze,af as ve,ak as Z,cY as Nw,cZ as Fw,c_ as $w,c$ as zw,bk as Uw,cw as ul,aS as Cv,aq as Ye,d0 as Hw,d1 as Vw,a5 as $e,d2 as Gw,d3 as Ww,d4 as jw,d5 as Kw,d6 as Xw,d7 as Yw,d8 as Zw,d9 as Qw,da as Jw,db as qw,dc as eT,v as tT,dd as rT,de as nT,df as iT,aW as aT,dg as oT,dh as sT,di as lT,dj as uT,dk as cT,ay as sr,dl as fT,dm as dT,dn as hT,dp as vT,A as pT,dq as gT,dr as mT,bq as yT,ds as ST,dt as bT,du as xT,ap as CT,ac as f1,ai as dt,dv as wT,au as Ce,dw as TT,X as d1,y as IT,dx as _T,aP as AT,dy as ET,w as _r,dz as PT,dA as MT,H as RT,dB as DT,bA as hn,aD as LT,ae as ie,bN as ea,$ as wc,dC as kT,bM as ta,dD as OT,aC as BT,aO as NT,R as fe,dE as FT,br as rh,b3 as Lr,aj as Hs,b1 as lt,b2 as Ua,an as wv,dF as $T,dG as zT,dH as UT,dI as HT,dJ as VT,dK as GT,dL as WT,Q as jT,dM as KT,dN as XT,dO as YT,at as Mt,dP as ZT,aR as QT,aY as JT,ad as cl,aZ as qT,dQ as eI,dR as tI,dS as rI,P as le,a6 as nI,dT as iI,dU as aI,dV as oI,dW as sI,a7 as lI,dX as uI,dY as cI,dZ as fI,d_ as dI,d$ as hI,b4 as vI,aA as Tv,e0 as pI,e1 as gI,E as gr,x as mI,e2 as yI,e3 as SI,e4 as bI,am as _n,e5 as xI,az as hr,aU as nh,e6 as CI,as as jr,e7 as wI,e8 as Iv,cE as mr,by as ih,ct as ah,bI as TI,bH as Ve,bu as oh,bv as _v,bx as Wt,bB as Tc,bL as di,e9 as wu,bE as hi,ea as II,bJ as ra,bC as _I,bw as Bt,eb as AI,ec as ui,cu as EI,ed as PI,ee as MI,ef as RI,eg as DI,bK as Zp,eh as LI,bF as Qp,cB as Ic,cA as kI,ei as Hu,ej as Il}from"./index-DWQm3SGv.js";/**
 * vue v3.4.34
 * vue v3.4.34
 * (c) 2018-present Yuxi (Evan) You and Vue contributors
 * (c) 2018-present Yuxi (Evan) You and Vue contributors
 * @license MIT
 * @license MIT

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
dist/assets/index-IMrmjUBC.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
dist/assets/index-dibYAj39.js


+ 1 - 1
dist/assets/index-BaaNoof-.js → dist/assets/index-w0l_Dxut.js

@@ -1 +1 @@
-import{_ as a}from"./404-R0y5dkvZ.js";import{a5 as o,en as t,ae as r,af as c,bM as _,bN as d,ak as s,bH as p}from"./index-Xrdt_JH7.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,en as t,ae as r,af as c,bM as _,bN as d,ak as s,bH as p}from"./index-DWQm3SGv.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};

+ 1 - 1
dist/assets/index-1L61q_kv.js → dist/assets/index-yW4sYuDn.js

@@ -1 +1 @@
-import{u as s,af as r}from"./index-BnpuTYrS.js";import{a5 as c,cB as n,R as p,ae as t,af as _,al as u,ag as i,bH as f}from"./index-Xrdt_JH7.js";const l={class:"pptScreen"},m=c({__name:"pptScreen",setup(d){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=f(m,[["__scopeId","data-v-678905e9"]]);export{P as default};
+import{u as s,af as r}from"./index-DwBUAZFG.js";import{a5 as c,cB as n,R as p,ae as t,af as _,al as u,ag as i,bH as f}from"./index-DWQm3SGv.js";const l={class:"pptScreen"},m=c({__name:"pptScreen",setup(d){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=f(m,[["__scopeId","data-v-678905e9"]]);export{P as default};

+ 1 - 1
dist/assets/shapes-CSNaN2fQ.js → dist/assets/shapes-DcUNpqAi.js

@@ -1,4 +1,4 @@
-import{cE as k,bB as $1}from"./index-Xrdt_JH7.js";import{ag as Z,ah as H1,ai as U,aj as n0,ak as c0,al as i0,am as j1,an as K1,ao as Y1,ap as R}from"./index-BnpuTYrS.js";var z1={exports:{}};/*!
+import{cE as k,bB as $1}from"./index-DWQm3SGv.js";import{ag as Z,ah as H1,ai as U,aj as n0,ak as c0,al as i0,am as j1,an as K1,ao as Y1,ap as R}from"./index-DwBUAZFG.js";var z1={exports:{}};/*!
  * clipboard.js v2.0.11
  * clipboard.js v2.0.11
  * https://clipboardjs.com/
  * https://clipboardjs.com/
  *
  *

+ 1 - 1
dist/index.html

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

+ 22 - 0
src/api/aiCourseware.ts

@@ -29,7 +29,10 @@ export interface AiCoursewarePage {
   title: string
   title: string
   subtitle?: string
   subtitle?: string
   bullets: string[]
   bullets: string[]
+  teachingStage?: string
   teachingPurpose?: string
   teachingPurpose?: string
+  learningEvidence?: string
+  durationMinutes?: number
   studentContent?: {
   studentContent?: {
     headline?: string
     headline?: string
     points?: string[]
     points?: string[]
@@ -51,8 +54,12 @@ export interface AiCoursewarePage {
   }
   }
   visualPlan?: {
   visualPlan?: {
     layout?: string
     layout?: string
+    visualRole?: string
+    focalContent?: string
+    visualDensity?: "low" | "medium" | "high" | string
     mainVisual?: string
     mainVisual?: string
     elements?: string[]
     elements?: string[]
+    contentPriority?: string[]
     imageKeywords?: string[]
     imageKeywords?: string[]
     style?: string
     style?: string
   }
   }
@@ -73,6 +80,21 @@ export interface AiCoursewarePage {
     height?: number
     height?: number
     score?: number
     score?: number
   }
   }
+  resourceBindings?: AiCoursewareResourceBinding[]
+}
+
+export interface AiCoursewareResourceBinding {
+  resourceId: string
+  resourceType: "IMG" | "VIDEO" | "SONG" | "MUSIC" | "LISTEN" | "RHYTHM" | "THEORY" | "MUSIC_WIKI" | "INSTRUMENT" | "MUSICIAN" | string
+  name?: string
+  contentUrl?: string
+  coverUrl?: string
+  dataJson?: string
+  elementSubtype?: string
+  purpose?: string
+  dedicatedPage?: boolean
+  required?: boolean
+  verified?: boolean
 }
 }
 
 
 export interface AiCoursewareTaskInfo {
 export interface AiCoursewareTaskInfo {

+ 85 - 34
src/utils/aiPptTemplateEngine.ts

@@ -4,6 +4,12 @@ import type {
   PPTElement,
   PPTElement,
   PPTElementShadow,
   PPTElementShadow,
   PPTImageElement,
   PPTImageElement,
+  PPTVideoElement,
+  PPTEnjoyElement,
+  PPTCloudCoachElement,
+  PPTListeningPracticeElement,
+  PPTRhythmPracticeElement,
+  PPTMusicResourcesElement,
   PPTLineElement,
   PPTLineElement,
   PPTShapeElement,
   PPTShapeElement,
   PPTTextElement,
   PPTTextElement,
@@ -65,7 +71,12 @@ export function buildAiPptSlides(outline: AiCoursewareOutline, options: AiPptOpt
 
 
 function buildSlide(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette, options: AiPptOptions, index: number): Slide {
 function buildSlide(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette, options: AiPptOptions, index: number): Slide {
   const role = pageRole(page, index)
   const role = pageRole(page, index)
-  const elements = templateFor(role)(page, outline, palette, index)
+  const dedicatedResource = dedicatedResourceElement(page)
+  const elements = dedicatedResource
+    ? [dedicatedResource]
+    : videoBinding(page)
+      ? videoResourceTemplate(page, outline, palette)
+      : [...templateFor(role)(page, outline, palette, index), ...inlineResourceElements(page, palette)]
   return {
   return {
     id: nanoid(10),
     id: nanoid(10),
     elements: keepInCanvas(elements),
     elements: keepInCanvas(elements),
@@ -117,7 +128,7 @@ function coverTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, pal
   elements.push(textElement({ left: 92, top: 112, width: 380, height: 70, content: "音乐课堂", fontSize: 30, color: "#FFFFFF", bold: true }))
   elements.push(textElement({ left: 92, top: 112, width: 380, height: 70, content: "音乐课堂", fontSize: 30, color: "#FFFFFF", bold: true }))
   elements.push(textElement({ left: 650, top: 170, width: 780, height: 170, content: title, fontSize: fitFont(title, 780, 170, 70, 50), color: palette.primary, bold: true }))
   elements.push(textElement({ left: 650, top: 170, width: 780, height: 170, content: title, fontSize: fitFont(title, 780, 170, 70, 50), color: palette.primary, bold: true }))
   if (subtitle) elements.push(textElement({ left: 656, top: 370, width: 740, height: 44, content: subtitle, fontSize: 24, color: palette.muted }))
   if (subtitle) elements.push(textElement({ left: 656, top: 370, width: 740, height: 44, content: subtitle, fontSize: 24, color: palette.muted }))
-  elements.push(...melodyStaff(120, 350, 360, palette))
+  elements.push(textElement({ left: 92, top: 360, width: 380, height: 180, content: "听 · 唱 · 动", fontSize: 52, color: "#FFFFFF", bold: true }))
   elements.push(shapeElement({ left: 654, top: 486, width: 120, height: 10, fill: palette.secondary, radius: 4 }))
   elements.push(shapeElement({ left: 654, top: 486, width: 120, height: 10, fill: palette.secondary, radius: 4 }))
   points.forEach((point, index) => elements.push(textElement({ left: 656, top: 548 + index * 66, width: 720, height: 46, content: point, fontSize: fitFont(point, 720, 46, 30, 23), color: palette.text, bold: index === 0 })))
   points.forEach((point, index) => elements.push(textElement({ left: 656, top: 548 + index * 66, width: 720, height: 46, content: point, fontSize: fitFont(point, 720, 46, 30, 23), color: palette.text, bold: index === 0 })))
   return elements
   return elements
@@ -166,7 +177,7 @@ function listenTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, pa
   } else {
   } else {
     elements.push(shapeElement({ left: 92, top: 210, width: 1416, height: 220, fill: palette.dark, radius: 8 }))
     elements.push(shapeElement({ left: 92, top: 210, width: 1416, height: 220, fill: palette.dark, radius: 8 }))
     elements.push(textElement({ left: 150, top: 270, width: 1300, height: 92, content: task, fontSize: fitFont(task, 1300, 92, 44, 30), color: "#FFFFFF", bold: true }))
     elements.push(textElement({ left: 150, top: 270, width: 1300, height: 92, content: task, fontSize: fitFont(task, 1300, 92, 44, 30), color: "#FFFFFF", bold: true }))
-    elements.push(...soundWave(250, 520, palette, 1080, 150))
+    elements.push(...studentPanel(page, palette, 170, 500, 1260))
   }
   }
   addClassroomCue(elements, page, palette)
   addClassroomCue(elements, page, palette)
   return elements
   return elements
@@ -189,15 +200,7 @@ function notationTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline,
   const elements = slideHeader(page, outline, palette, "旋律与谱例")
   const elements = slideHeader(page, outline, palette, "旋律与谱例")
   const headline = studentHeadline(page) || "看一看,唱一唱"
   const headline = studentHeadline(page) || "看一看,唱一唱"
   elements.push(textElement({ left: 92, top: 190, width: 1416, height: 72, content: headline, fontSize: fitFont(headline, 1416, 72, 40, 30), color: palette.primary, bold: true }))
   elements.push(textElement({ left: 92, top: 190, width: 1416, height: 72, content: headline, fontSize: fitFont(headline, 1416, 72, 40, 30), color: palette.primary, bold: true }))
-  elements.push(shapeElement({ left: 92, top: 300, width: 1416, height: 300, fill: palette.surface, radius: 8, shadow: softShadow() }))
-  for (let i = 0; i < 5; i++) elements.push(lineElement(160, 370 + i * 42, 1440, 370 + i * 42, palette.muted, 2))
-  studentPoints(page, 4).forEach((point, index) => {
-    const left = 180 + index * 315
-    const top = 392 + (index % 2) * 44
-    elements.push(shapeElement({ left, top, width: 42, height: 34, fill: index % 2 ? palette.secondary : palette.primary, radius: 8 }))
-    elements.push(lineElement(left + 38, top - 52, left + 38, top + 12, index % 2 ? palette.secondary : palette.primary, 5))
-    elements.push(textElement({ left: left - 26, top: 610, width: 250, height: 50, content: point, fontSize: fitFont(point, 250, 50, 27, 21), color: palette.text, bold: true }))
-  })
+  elements.push(...knowledgeList(studentPoints(page, 3), palette, 92, 310, 1416))
   addClassroomCue(elements, page, palette)
   addClassroomCue(elements, page, palette)
   return elements
   return elements
 }
 }
@@ -216,7 +219,7 @@ function instrumentTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline
   } else {
   } else {
     elements.push(shapeElement({ left: 92, top: 210, width: 1416, height: 190, fill: palette.primary, radius: 8 }))
     elements.push(shapeElement({ left: 92, top: 210, width: 1416, height: 190, fill: palette.primary, radius: 8 }))
     elements.push(textElement({ left: 150, top: 260, width: 1300, height: 90, content: studentHeadline(page) || page.title, fontSize: fitFont(studentHeadline(page) || page.title, 1300, 90, 44, 30), color: "#FFFFFF", bold: true }))
     elements.push(textElement({ left: 150, top: 260, width: 1300, height: 90, content: studentHeadline(page) || page.title, fontSize: fitFont(studentHeadline(page) || page.title, 1300, 90, 44, 30), color: "#FFFFFF", bold: true }))
-    elements.push(...soundWave(210, 500, palette, 1180, 150))
+    elements.push(...knowledgeList(points, palette, 170, 470, 1260))
   }
   }
   addClassroomCue(elements, page, palette)
   addClassroomCue(elements, page, palette)
   return elements
   return elements
@@ -400,30 +403,76 @@ function knowledgeList(items: string[], palette: Palette, left: number, top: num
   return elements
   return elements
 }
 }
 
 
-function melodyStaff(left: number, top: number, width: number, palette: Palette): PPTElement[] {
-  const elements: PPTElement[] = []
-  for (let i = 0; i < 5; i++) elements.push(lineElement(left, top + i * 34, left + width, top + i * 34, "rgba(255,255,255,0.55)"))
-  ;[0.12, 0.36, 0.62, 0.84].forEach((position, index) => {
-    elements.push(shapeElement({ left: left + width * position, top: top + 48 + (index % 2) * 30, width: 34, height: 28, fill: index % 2 ? palette.accent : palette.warm, radius: 8 }))
-    elements.push(lineElement(left + width * position + 30, top + 10 + (index % 2) * 30, left + width * position + 30, top + 60 + (index % 2) * 30, index % 2 ? palette.accent : palette.warm, 5))
-  })
-  return elements
+function addClassroomCue(elements: PPTElement[], page: AiCoursewarePage, palette: Palette) {
+  const cue = page.interaction?.question || activityTask(page)
+  if (!cue) return
+  elements.push(shapeElement({ left: 92, top: 824, width: 1416, height: 48, fill: palette.soft, radius: 8 }))
+  elements.push(textElement({ left: 120, top: 834, width: 1360, height: 28, content: cue, fontSize: fitFont(cue, 1360, 28, 20, 17), color: palette.text, bold: true }))
 }
 }
 
 
-function soundWave(left: number, top: number, palette: Palette, width: number, maxHeight: number): PPTElement[] {
-  const heights = [0.28, 0.55, 0.82, 0.45, 1, 0.7, 0.38, 0.64, 0.32]
-  return heights.map((scale, index) => shapeElement({
-    left: left + index * (width / heights.length), top: top + (maxHeight - maxHeight * scale) / 2,
-    width: Math.max(18, width / 24), height: maxHeight * scale,
-    fill: index % 3 === 0 ? palette.accent : index % 2 === 0 ? palette.secondary : palette.primary, radius: 8,
-  }))
+function dedicatedResourceElement(page: AiCoursewarePage): PPTElement | undefined {
+  const binding = (page.resourceBindings || []).find(item => item.verified !== false && item.dedicatedPage)
+  if (!binding) return undefined
+  const base = { id: nanoid(10), left: 0, top: 0, width: SLIDE_WIDTH, height: SLIDE_HEIGHT, rotate: 0 }
+  if (binding.resourceType === "MUSIC") {
+    return { ...base, type: "elf", subtype: "elf-sing-play", sid: binding.resourceId, title: binding.name || page.title } as PPTCloudCoachElement
+  }
+  if (binding.resourceType === "RHYTHM" && binding.dataJson) {
+    return { ...base, type: "elf", subtype: "elf-rhythm-practice", dataJson: binding.dataJson } as PPTRhythmPracticeElement
+  }
+  if (binding.resourceType === "LISTEN") {
+    const metadata = parseResourceJson(binding.dataJson)
+    const code = String(metadata.code || metadata.instrumentCode || binding.contentUrl || "")
+    if (code) {
+      return {
+        ...base, type: "elf", subtype: "elf-listening-practice", code,
+        name: binding.name || page.title, instrumentImg: binding.coverUrl || String(metadata.img || ""),
+      } as PPTListeningPracticeElement
+    }
+  }
+  const typeMap: Record<string, "INSTRUMENT" | "MUSICIAN" | "MUSIC" | "THEORY"> = {
+    INSTRUMENT: "INSTRUMENT", MUSICIAN: "MUSICIAN", MUSIC_WIKI: "MUSIC", THEORY: "THEORY",
+  }
+  const sType = typeMap[binding.resourceType]
+  if (sType) {
+    return { ...base, type: "elf", subtype: "elf-music-resources", sid: binding.resourceId, name: binding.name || page.title, sType } as PPTMusicResourcesElement
+  }
 }
 }
 
 
-function addClassroomCue(elements: PPTElement[], page: AiCoursewarePage, palette: Palette) {
-  const cue = page.interaction?.question || activityTask(page)
-  if (!cue) return
-  elements.push(shapeElement({ left: 92, top: 754, width: 1416, height: 62, fill: palette.soft, radius: 8 }))
-  elements.push(textElement({ left: 120, top: 772, width: 1360, height: 30, content: cue, fontSize: fitFont(cue, 1360, 30, 22, 18), color: palette.text, bold: true }))
+function inlineResourceElements(page: AiCoursewarePage, palette: Palette): PPTElement[] {
+  const binding = (page.resourceBindings || []).find(item => item.verified !== false && item.resourceType === "SONG" && item.contentUrl)
+  if (!binding) return []
+  return [{
+    id: nanoid(10), type: "elf", subtype: "elf-enjoy", left: 110, top: 700, width: 1380, height: 100, rotate: 0,
+    sid: binding.resourceId, title: binding.name || page.title, src: binding.contentUrl || "",
+    enjoyList: [{ id: binding.resourceId, title: binding.name || page.title, src: binding.contentUrl || "" }],
+  } as PPTEnjoyElement]
+}
+
+function videoBinding(page: AiCoursewarePage) {
+  return (page.resourceBindings || []).find(item => item.verified !== false && item.resourceType === "VIDEO" && item.contentUrl)
+}
+
+function videoResourceTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
+  const binding = videoBinding(page)!
+  const elements = slideHeader(page, outline, palette, page.teachingStage || "观看与发现")
+  elements.push({
+    id: nanoid(10), type: "elf", subtype: "elf-video", left: 190, top: 190, width: 1220, height: 570, rotate: 0,
+    src: binding.contentUrl || "", poster: binding.coverUrl, autoplay: false,
+  } as PPTVideoElement)
+  addClassroomCue(elements, page, palette)
+  return elements
+}
+
+function parseResourceJson(value?: string): Record<string, unknown> {
+  if (!value) return {}
+  try {
+    const parsed = JSON.parse(value)
+    return parsed && typeof parsed === "object" ? parsed : {}
+  }
+  catch {
+    return {}
+  }
 }
 }
 
 
 function normalizeOutline(outline: AiCoursewareOutline): AiCoursewareOutline {
 function normalizeOutline(outline: AiCoursewareOutline): AiCoursewareOutline {
@@ -503,7 +552,9 @@ function activitySteps(page: AiCoursewarePage, limit: number) {
 }
 }
 
 
 function slideRemark(page: AiCoursewarePage) {
 function slideRemark(page: AiCoursewarePage) {
-  return [page.speakerNotes, page.teacherTips, page.classroomAction?.teacherInstruction, page.teachingPurpose]
+  const lessonMeta = [page.teachingStage, page.durationMinutes ? `建议用时:${page.durationMinutes}分钟` : ""].filter(Boolean).join(" · ")
+  const evidence = page.learningEvidence ? `学习表现:${page.learningEvidence}` : ""
+  return [lessonMeta, page.teachingPurpose, page.speakerNotes, page.teacherTips, page.classroomAction?.teacherInstruction, evidence]
     .map(cleanText).filter(Boolean).filter((item, index, all) => all.indexOf(item) === index).join("\n")
     .map(cleanText).filter(Boolean).filter((item, index, all) => all.indexOf(item) === index).join("\n")
 }
 }
 
 

+ 68 - 1
src/views/Editor/EditorHeader/AiCoursewareDialog.vue

@@ -111,6 +111,19 @@
               ></textarea>
               ></textarea>
           </div>
           </div>
 
 
+          <div v-if="selectedPage.resourceBindings?.length" class="resource-card">
+            <div class="resource-head">
+              <div class="field-title">课堂资源</div>
+              <div class="resource-hint">生成后可直接播放或展示</div>
+            </div>
+            <div class="resource-list">
+              <div v-for="resource in selectedPage.resourceBindings" :key="`${resource.resourceType}-${resource.resourceId}`" class="resource-item">
+                <span class="resource-type">{{ resourceTypeName(resource.resourceType) }}</span>
+                <span class="resource-name">{{ resource.name || "已匹配资源" }}</span>
+              </div>
+            </div>
+          </div>
+
           <div class="keyword-card">
           <div class="keyword-card">
             <div class="keyword-head">
             <div class="keyword-head">
               <div class="keyword-label">图片关键词</div>
               <div class="keyword-label">图片关键词</div>
@@ -191,6 +204,7 @@ const progressValue = computed(() => taskInfo.value?.progress || 0)
 const progressSteps = [
 const progressSteps = [
   { threshold: 0, label: "准备中" },
   { threshold: 0, label: "准备中" },
   { threshold: 25, label: "生成内容" },
   { threshold: 25, label: "生成内容" },
+  { threshold: 50, label: "优化设计" },
   { threshold: 65, label: "匹配图片" },
   { threshold: 65, label: "匹配图片" },
   { threshold: 100, label: "生成完成" }
   { threshold: 100, label: "生成完成" }
 ]
 ]
@@ -307,7 +321,7 @@ async function applyResult(taskId: string) {
   slidesStore.setSlides(converted.slides)
   slidesStore.setSlides(converted.slides)
   slidesStore.updateSlideIndex(0)
   slidesStore.updateSlideIndex(0)
   addHistorySnapshot()
   addHistorySnapshot()
-  ElMessage.success("AI课件已生成,请检查后手动保存")
+  ElMessage.success("PPT已生成,请检查后手动保存")
   emit("update:visible", false)
   emit("update:visible", false)
 }
 }
 
 
@@ -362,12 +376,21 @@ function shortProgressLabel(message?: string) {
   if (!message) return ""
   if (!message) return ""
   if (message.includes("读取") || message.includes("等待")) return "准备中"
   if (message.includes("读取") || message.includes("等待")) return "准备中"
   if (message.includes("页面") || message.includes("内容")) return "生成内容"
   if (message.includes("页面") || message.includes("内容")) return "生成内容"
+  if (message.includes("优化") || message.includes("教学设计")) return "优化设计"
   if (message.includes("图片") || message.includes("检索")) return "匹配图片"
   if (message.includes("图片") || message.includes("检索")) return "匹配图片"
   if (message.includes("完成")) return "生成完成"
   if (message.includes("完成")) return "生成完成"
   if (message.includes("失败")) return "生成失败"
   if (message.includes("失败")) return "生成失败"
   return message.length > 4 ? message.slice(0, 4) : message
   return message.length > 4 ? message.slice(0, 4) : message
 }
 }
 
 
+function resourceTypeName(type: string) {
+  const names: Record<string, string> = {
+    IMG: "图片", VIDEO: "视频", SONG: "音频", MUSIC: "乐谱", LISTEN: "听音练习",
+    RHYTHM: "节奏练习", THEORY: "乐理知识", MUSIC_WIKI: "名曲鉴赏", INSTRUMENT: "乐器百科", MUSICIAN: "音乐家",
+  }
+  return names[type] || "课堂资源"
+}
+
 </script>
 </script>
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
@@ -592,6 +615,50 @@ function shortProgressLabel(message?: string) {
 .page-design {
 .page-design {
   height: 90px;
   height: 90px;
 }
 }
+.resource-card {
+  margin-top: 12px;
+  padding: 10px 12px;
+  border: 1px solid #dcefe9;
+  border-radius: 6px;
+  background: #f6fbf9;
+}
+.resource-head {
+  display: flex;
+  align-items: baseline;
+  gap: 10px;
+}
+.resource-hint {
+  color: #7b8794;
+  font-size: 12px;
+}
+.resource-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+}
+.resource-item {
+  display: flex;
+  align-items: center;
+  min-width: 0;
+  height: 30px;
+  padding: 0 10px;
+  border: 1px solid #cfe6de;
+  border-radius: 4px;
+  background: #fff;
+  font-size: 12px;
+}
+.resource-type {
+  margin-right: 8px;
+  color: #168f86;
+  font-weight: 600;
+}
+.resource-name {
+  max-width: 240px;
+  overflow: hidden;
+  color: #374151;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
 .keyword-card {
 .keyword-card {
   margin-top: 12px;
   margin-top: 12px;
 }
 }

Некоторые файлы не были показаны из-за большого количества измененных файлов