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

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

@@ -1 +1 @@
-import{u as s,af as r}from"./index-DMJHYSHL.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-vIcuuR4g.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-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};

+ 1 - 1
dist/assets/index-Cwlda7Q4.js → dist/assets/index-BLfK_g0y.js

@@ -1 +1 @@
-import{u as c,af as l}from"./index-DMJHYSHL.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-vIcuuR4g.js";import"./shapes-Cp7YSuKf.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-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};

+ 1 - 1
dist/assets/index-CVwIohN9.js → dist/assets/index-BaaNoof-.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-vIcuuR4g.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-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};

+ 1 - 1
dist/assets/index-DMJHYSHL.js → dist/assets/index-BnpuTYrS.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-vIcuuR4g.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-Xrdt_JH7.js";/**
 * vue v3.4.34
 * (c) 2018-present Yuxi (Evan) You and Vue contributors
 * @license MIT

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


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


+ 1 - 1
dist/assets/index-0KJWMmVV.js → dist/assets/index-DUb7yzkd.js

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

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


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

@@ -1,4 +1,4 @@
-import{cE as k,bB as $1}from"./index-vIcuuR4g.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-DMJHYSHL.js";var z1={exports:{}};/*!
+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:{}};/*!
  * 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-vIcuuR4g.js"></script>
+    <script type="module" crossorigin src="./assets/index-Xrdt_JH7.js"></script>
     <link rel="stylesheet" crossorigin href="./assets/index-AavUEaYy.css">
   </head>
   <body>

+ 133 - 9
src/utils/aiPptTemplateEngine.ts

@@ -17,7 +17,7 @@ interface AiPptOptions {
   backgroundImage?: string
 }
 
-type PageRole = "cover" | "intro" | "goal" | "story" | "listen" | "lyric" | "knowledge" | "rhythm" | "interaction" | "activity" | "summary" | "homework" | "content"
+type PageRole = "cover" | "intro" | "goal" | "story" | "listen" | "lyric" | "notation" | "instrument" | "compare" | "knowledge" | "rhythm" | "interaction" | "performance" | "practice" | "activity" | "summary" | "homework" | "content"
 
 interface Palette {
   primary: string
@@ -80,9 +80,14 @@ function templateFor(role: PageRole) {
     case "goal": return goalTemplate
     case "listen": return listenTemplate
     case "lyric": return lyricTemplate
+    case "notation": return notationTemplate
+    case "instrument": return instrumentTemplate
+    case "compare": return compareTemplate
     case "rhythm": return rhythmTemplate
     case "knowledge": return knowledgeTemplate
     case "interaction": return interactionTemplate
+    case "performance": return performanceTemplate
+    case "practice": return practiceTemplate
     case "activity": return activityTemplate
     case "summary": return summaryTemplate
     case "homework": return homeworkTemplate
@@ -180,6 +185,62 @@ function lyricTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, pal
   return elements
 }
 
+function notationTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
+  const elements = slideHeader(page, outline, palette, "旋律与谱例")
+  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(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 }))
+  })
+  addClassroomCue(elements, page, palette)
+  return elements
+}
+
+function instrumentTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
+  const elements = slideHeader(page, outline, palette, "认识声音")
+  const points = studentPoints(page, 3)
+  if (hasImage(page)) {
+    elements.push(imageElement(page, { left: 84, top: 190, width: 760, height: 540, radius: 8 }))
+    elements.push(textElement({ left: 920, top: 220, width: 580, height: 96, content: studentHeadline(page) || page.title, fontSize: fitFont(studentHeadline(page) || page.title, 580, 96, 42, 30), color: palette.primary, bold: true }))
+    points.forEach((point, index) => {
+      const top = 360 + index * 104
+      elements.push(textElement({ left: 920, top, width: 56, height: 48, content: `0${index + 1}`, fontSize: 30, color: index % 2 ? palette.secondary : palette.accent, bold: true }))
+      elements.push(textElement({ left: 992, top, width: 490, height: 64, content: point, fontSize: fitFont(point, 490, 64, 30, 23), color: palette.text, bold: true }))
+    })
+  } else {
+    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(...soundWave(210, 500, palette, 1180, 150))
+  }
+  addClassroomCue(elements, page, palette)
+  return elements
+}
+
+function compareTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
+  const elements = slideHeader(page, outline, palette, "对比听赏")
+  const points = studentPoints(page, 4)
+  const leftPoints = points.filter((_, index) => index % 2 === 0)
+  const rightPoints = points.filter((_, index) => index % 2 === 1)
+  elements.push(shapeElement({ left: 92, top: 210, width: 690, height: 470, fill: palette.soft, radius: 8 }))
+  elements.push(shapeElement({ left: 818, top: 210, width: 690, height: 470, fill: palette.surface, radius: 8, shadow: lightShadow() }))
+  elements.push(textElement({ left: 140, top: 252, width: 570, height: 64, content: "听一听 A", fontSize: 36, color: palette.primary, bold: true }))
+  elements.push(textElement({ left: 866, top: 252, width: 570, height: 64, content: "听一听 B", fontSize: 36, color: palette.secondary, bold: true }))
+  ;[leftPoints, rightPoints].forEach((items, group) => items.forEach((point, index) => {
+    const left = group === 0 ? 140 : 866
+    const top = 365 + index * 110
+    elements.push(shapeElement({ left, top: top + 8, width: 22, height: 22, fill: group === 0 ? palette.primary : palette.secondary, radius: 8 }))
+    elements.push(textElement({ left: left + 48, top, width: 520, height: 70, content: point, fontSize: fitFont(point, 520, 70, 30, 23), color: palette.text, bold: true }))
+  }))
+  addClassroomCue(elements, page, palette)
+  return elements
+}
+
 function rhythmTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
   const elements = slideHeader(page, outline, palette, "节奏练习")
   const task = activityTask(page) || studentHeadline(page) || "拍一拍,感受节奏"
@@ -234,6 +295,38 @@ function activityTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline,
   return elements
 }
 
+function performanceTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
+  const elements = slideHeader(page, outline, palette, "合作表现")
+  const task = activityTask(page) || studentHeadline(page) || "一起完成音乐表演"
+  elements.push(shapeElement({ left: 92, top: 200, width: 1416, height: 150, fill: palette.primary, radius: 8 }))
+  elements.push(textElement({ left: 150, top: 242, width: 1300, height: 70, content: task, fontSize: fitFont(task, 1300, 70, 42, 30), color: "#FFFFFF", bold: true }))
+  const steps = activitySteps(page, 4)
+  steps.forEach((step, index) => {
+    const left = 126 + index * 355
+    elements.push(shapeElement({ left, top: 438, width: 76, height: 76, fill: index % 2 ? palette.secondary : palette.accent, radius: 38 }))
+    elements.push(textElement({ left: left + 23, top: 454, width: 34, height: 38, content: `${index + 1}`, fontSize: 28, color: "#FFFFFF", bold: true }))
+    elements.push(textElement({ left: left - 16, top: 550, width: 280, height: 100, content: step, fontSize: fitFont(step, 280, 100, 29, 22), color: palette.text, bold: true }))
+    if (index < steps.length - 1) elements.push(lineElement(left + 96, 476, left + 325, 476, palette.muted, 3))
+  })
+  addClassroomCue(elements, page, palette)
+  return elements
+}
+
+function practiceTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
+  const elements = slideHeader(page, outline, palette, "课堂检测")
+  const question = page.interaction?.question || studentHeadline(page) || activityTask(page) || "你能完成今天的音乐挑战吗?"
+  elements.push(textElement({ left: 92, top: 200, width: 1416, height: 82, content: question, fontSize: fitFont(question, 1416, 82, 42, 30), color: palette.text, bold: true }))
+  studentPoints(page, 4).forEach((point, index) => {
+    const top = 330 + index * 94
+    elements.push(shapeElement({ left: 110, top, width: 70, height: 62, fill: index % 2 ? palette.secondary : palette.primary, radius: 8 }))
+    elements.push(textElement({ left: 132, top: top + 14, width: 32, height: 34, content: String.fromCharCode(65 + index), fontSize: 26, color: "#FFFFFF", bold: true }))
+    elements.push(textElement({ left: 220, top: top + 4, width: 1220, height: 58, content: point, fontSize: fitFont(point, 1220, 58, 31, 24), color: palette.text, bold: true }))
+    elements.push(lineElement(220, top + 72, 1450, top + 72, palette.soft, 2))
+  })
+  addClassroomCue(elements, page, palette)
+  return elements
+}
+
 function summaryTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
   const elements = slideHeader(page, outline, palette, "课堂小结")
   studentPoints(page, 4).forEach((point, index) => {
@@ -351,17 +444,22 @@ function normalizeOutline(outline: AiCoursewareOutline): AiCoursewareOutline {
 function pageRole(page: AiCoursewarePage, index: number): PageRole {
   if (index === 0) return "cover"
   const kind = `${page.type || ""} ${page.visualPlan?.layout || ""} ${page.visual?.layout || ""}`.toLowerCase()
-  if (kind.includes("goal")) return "goal"
+  if (kind.includes("objective") || kind.includes("goal")) return "goal"
+  if (kind.includes("practice_check") || kind.includes("assessment")) return "practice"
+  if (kind.includes("compare")) return "compare"
+  if (kind.includes("instrument")) return "instrument"
+  if (kind.includes("melody") || kind.includes("score") || kind.includes("notation")) return "notation"
+  if (kind.includes("performance") || kind.includes("expressive") || kind.includes("creative")) return "performance"
   if (kind.includes("listen")) return "listen"
-  if (kind.includes("lyric") || kind.includes("sing")) return "lyric"
+  if (kind.includes("lyric") || kind.includes("phrase_sing") || kind.includes("sing_focus")) return "lyric"
   if (kind.includes("rhythm") || kind.includes("skill")) return "rhythm"
-  if (kind.includes("knowledge") || kind.includes("extension")) return "knowledge"
+  if (kind.includes("knowledge")) return "knowledge"
   if (kind.includes("interaction")) return "interaction"
   if (kind.includes("activity")) return "activity"
   if (kind.includes("summary")) return "summary"
-  if (kind.includes("homework")) return "homework"
-  if (kind.includes("intro")) return "intro"
-  if (kind.includes("background") || kind.includes("story")) return "story"
+  if (kind.includes("homework") || kind.includes("extension")) return "homework"
+  if (kind.includes("intro") || kind.includes("scene")) return "intro"
+  if (kind.includes("context") || kind.includes("background") || kind.includes("story")) return "story"
   return "content"
 }
 
@@ -440,13 +538,39 @@ function fitFont(content: string, width: number, height: number, preferred: numb
 }
 
 function textElement(params: { left: number; top: number; width: number; height: number; content: string; fontSize: number; color: string; bold?: boolean; lineHeight?: number }): PPTTextElement {
+  const lineHeight = params.lineHeight || 1.25
+  const visibleContent = fitVisibleText(params.content, params.width - 20, params.height, params.fontSize, lineHeight)
   return {
     id: nanoid(10), type: "text", left: params.left, top: params.top, width: params.width, height: params.height, rotate: 0,
-    content: `<div style="font-family:${FONT_NAME};font-size:${params.fontSize}px;${params.bold ? "font-weight:700;" : ""}">${escapeHtml(params.content)}</div>`,
-    defaultFontName: FONT_NAME, defaultColor: params.color, lineHeight: params.lineHeight || 1.25, wordSpace: 0, opacity: 1,
+    content: `<div style="box-sizing:border-box;width:calc(100% - 20px);font-family:${FONT_NAME};font-size:${params.fontSize}px;${params.bold ? "font-weight:700;" : ""}">${escapeHtml(visibleContent)}</div>`,
+    defaultFontName: FONT_NAME, defaultColor: params.color, lineHeight, paragraphSpace: 0, wordSpace: 0, opacity: 1,
   }
 }
 
+function fitVisibleText(content: string, width: number, height: number, fontSize: number, lineHeight: number) {
+  const text = cleanText(content)
+  if (!text) return ""
+  const safeWidth = Math.max(1, width)
+  const safeHeight = Math.max(1, height)
+  const maxLines = Math.max(1, Math.floor(safeHeight / (fontSize * lineHeight)))
+  const maxUnits = Math.max(1, Math.floor((safeWidth / fontSize) * maxLines))
+  if (textUnits(text) <= maxUnits) return text
+
+  let units = 0
+  let result = ""
+  for (const char of text) {
+    const charUnits = /[\x00-\xff]/.test(char) ? 0.55 : 1
+    if (units + charUnits > Math.max(1, maxUnits - 1)) break
+    result += char
+    units += charUnits
+  }
+  return `${result.trim()}…`
+}
+
+function textUnits(content: string) {
+  return Array.from(content).reduce((total, char) => total + (/[\x00-\xff]/.test(char) ? 0.55 : 1), 0)
+}
+
 function imageElement(page: AiCoursewarePage, rect: ImageRect): PPTImageElement {
   const width = page.image?.width || 0
   const height = page.image?.height || 0

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