yonge 1 неделя назад
Родитель
Сommit
a0e614060c

+ 1 - 1
dist/assets/index-BTaoV6AE.js → dist/assets/index-BWpLu7Ty.js

@@ -1 +1 @@
-import{_ as t}from"./404-R0y5dkvZ.js";import{E as c}from"./index-BTt4Ga5a.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-QfGvh56f.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-D8MJNqy7.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-Doi9nW2k.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-DbaM6D9B.js → dist/assets/index-CEqi0-5P.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-QfGvh56f.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-Doi9nW2k.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-BQbc2p8e.js → dist/assets/index-CPy5wlue.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-QfGvh56f.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-Doi9nW2k.js";/**
 * vue v3.4.34
 * (c) 2018-present Yuxi (Evan) You and Vue contributors
 * @license MIT

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


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


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


+ 1 - 1
dist/assets/index-bBMzNlm8.js → dist/assets/index-Dv5BViNo.js

@@ -1 +1 @@
-import{u as c,af as l}from"./index-BQbc2p8e.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-QfGvh56f.js";import"./shapes-CMKeeEjK.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-CPy5wlue.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-Doi9nW2k.js";import"./shapes-DZJHFobw.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-Dr4QGPNx.js → dist/assets/index-EUiDQWTW.js

@@ -1 +1 @@
-import{u as s,af as r}from"./index-BQbc2p8e.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-QfGvh56f.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-CPy5wlue.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-Doi9nW2k.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-CMKeeEjK.js → dist/assets/shapes-DZJHFobw.js

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

+ 4 - 452
src/utils/aiCoursewareToSlides.ts

@@ -1,459 +1,11 @@
-import { nanoid } from "nanoid"
-import type { Slide, PPTElement, SlideTheme, PPTTextElement, PPTImageElement, PPTShapeElement, SlideBackground, PPTElementShadow } from "@/types/slides"
-import type { AiCoursewareOutline, AiCoursewarePage } from "@/api/aiCourseware"
+import type { Slide, SlideTheme } from "@/types/slides"
+import type { AiCoursewareOutline } from "@/api/aiCourseware"
+import { buildAiPptSlides } from "@/utils/aiPptTemplateEngine"
 
 interface AiCoursewareVisualOptions {
   backgroundImage?: string
 }
 
-interface Palette {
-  primary: string
-  secondary: string
-  accent: string
-  warm: string
-  background: string
-  panelAlt: string
-  text: string
-  muted: string
-}
-
-const SLIDE_WIDTH = 1600
-const SLIDE_HEIGHT = 900
-const FONT_NAME = "Microsoft Yahei"
-
 export function aiCoursewareToSlides(outline: AiCoursewareOutline, options: AiCoursewareVisualOptions = {}): { title: string; theme: Partial<SlideTheme>; slides: Slide[] } {
-  const palette = buildPalette(outline.style)
-  const theme: Partial<SlideTheme> = {
-    themeColor: palette.primary,
-    backgroundColor: palette.background,
-    fontColor: palette.text,
-    fontName: FONT_NAME,
-    fontSize: "36px"
-  }
-
-  return {
-    title: outline.title || outline.chapterName || "音乐课件",
-    theme,
-    slides: outline.pages.map(page => buildSlide(page, outline, palette, options))
-  }
-}
-
-function buildSlide(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette, options: AiCoursewareVisualOptions): Slide {
-  const type = pageKind(page)
-  const elements = baseElements(palette, options)
-
-  if (type.includes("cover") || page.pageNo === 1) {
-    elements.push(...coverLayout(page, outline, palette))
-  } else if (type.includes("goal")) {
-    elements.push(...goalLayout(page, outline, palette))
-  } else if (type.includes("listen")) {
-    elements.push(...listenLayout(page, outline, palette))
-  } else if (type.includes("interaction") || type.includes("activity")) {
-    elements.push(...interactionLayout(page, outline, palette))
-  } else if (type.includes("summary")) {
-    elements.push(...summaryLayout(page, outline, palette))
-  } else if (type.includes("homework")) {
-    elements.push(...homeworkLayout(page, outline, palette))
-  } else if (type.includes("knowledge") || type.includes("skill")) {
-    elements.push(...knowledgeLayout(page, outline, palette))
-  } else {
-    elements.push(...contentLayout(page, outline, palette))
-  }
-
-  return {
-    id: nanoid(10),
-    elements,
-    background: backgroundConfig(palette, options),
-    remark: slideRemark(page)
-  }
-}
-
-function coverLayout(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
-  const elements: PPTElement[] = []
-  const subtitle = [outline.gradeName, outline.textbookName, outline.unitName].filter(Boolean).join(" · ")
-  elements.push(shapeElement({ left: 84, top: 96, width: 94, height: 10, fill: palette.primary, radius: 6 }))
-  elements.push(textElement({ left: 84, top: 138, width: 820, height: 106, content: page.title || outline.title, fontSize: 58, color: palette.primary, bold: true }))
-  if (subtitle) {
-    elements.push(textElement({ left: 88, top: 264, width: 820, height: 38, content: escapeHtml(subtitle), fontSize: 23, color: palette.text }))
-  }
-  elements.push(...mainVisual(page, palette, { left: 940, top: 112, width: 520, height: 470, mode: "hero" }))
-  elements.push(...bulletCards(studentPoints(page, 3), palette, { left: 88, top: 390, width: 720, itemHeight: 72, fontSize: 28, mark: "♪" }))
-  const intro = activityTask(page) || "从声音、画面和问题进入音乐情境"
-  elements.push(textElement({ left: 92, top: 746, width: 900, height: 42, content: escapeHtml(intro), fontSize: 22, color: palette.muted }))
-  return elements
-}
-
-function contentLayout(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
-  const elements: PPTElement[] = []
-  addTitle(elements, page, outline, palette)
-  const headline = studentHeadline(page)
-  if (headline && headline !== page.title) {
-    elements.push(shapeElement({ left: 96, top: 178, width: 760, height: 78, fill: palette.panelAlt, radius: 18 }))
-    elements.push(textElement({ left: 130, top: 202, width: 692, height: 34, content: escapeHtml(headline), fontSize: 29, color: palette.text, bold: true }))
-  }
-  elements.push(...mainVisual(page, palette, { left: 960, top: 178, width: 470, height: 470, mode: "photo" }))
-  elements.push(...bulletCards(studentPoints(page, 4), palette, { left: 98, top: headline && headline !== page.title ? 300 : 190, width: 760, itemHeight: 78, fontSize: 26, mark: "•" }))
-  addPrompt(elements, page, palette, 98, 706, 1220)
-  return elements
-}
-
-function goalLayout(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
-  const elements: PPTElement[] = []
-  addTitle(elements, page, outline, palette)
-  const goals = studentPoints(page, 3)
-  const colors = [palette.primary, palette.accent, palette.secondary]
-  goals.forEach((goal, index) => {
-    const left = 100 + index * 470
-    elements.push(shapeElement({ left, top: 238, width: 392, height: 300, fill: "#FFFFFF", radius: 24, shadow: softShadow() }))
-    elements.push(shapeElement({ left: left + 30, top: 270, width: 58, height: 58, fill: colors[index % colors.length], radius: 20 }))
-    elements.push(textElement({ left: left + 45, top: 284, width: 34, height: 34, content: `${index + 1}`, fontSize: 28, color: "#FFFFFF", bold: true }))
-    elements.push(textElement({ left: left + 32, top: 362, width: 328, height: 132, content: escapeHtml(goal), fontSize: 30, color: palette.text, bold: true, lineHeight: 1.35 }))
-  })
-  addPrompt(elements, page, palette, 112, 650, 1180)
-  return elements
-}
-
-function listenLayout(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
-  const elements: PPTElement[] = []
-  addTitle(elements, page, outline, palette)
-  const task = activityTask(page) || studentHeadline(page) || "听一听:音乐中出现了哪些有特点的声音?"
-  elements.push(shapeElement({ left: 92, top: 184, width: 660, height: 150, fill: palette.primary, radius: 24, shadow: softShadow() }))
-  elements.push(textElement({ left: 132, top: 224, width: 570, height: 64, content: escapeHtml(task), fontSize: 34, color: "#FFFFFF", bold: true, lineHeight: 1.25 }))
-  elements.push(...soundWave(128, 382, palette, 530, 118))
-  elements.push(...bulletCards(studentPoints(page, 3), palette, { left: 96, top: 548, width: 620, itemHeight: 58, fontSize: 24, mark: "听" }))
-  elements.push(...mainVisual(page, palette, { left: 835, top: 178, width: 560, height: 500, mode: "photo" }))
-  addPrompt(elements, page, palette, 112, 716, 1200, page.interaction?.question || page.classroomAction?.studentTask || "听完后,请说一说你听到了什么。")
-  return elements
-}
-
-function knowledgeLayout(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
-  const elements: PPTElement[] = []
-  addTitle(elements, page, outline, palette)
-  const items = studentPoints(page, 4)
-  items.forEach((item, index) => {
-    const left = 100 + (index % 2) * 520
-    const top = 210 + Math.floor(index / 2) * 190
-    elements.push(shapeElement({ left, top, width: 460, height: 142, fill: index % 2 === 0 ? "#FFFFFF" : palette.panelAlt, radius: 22, shadow: softShadow() }))
-    elements.push(textElement({ left: left + 34, top: top + 36, width: 390, height: 68, content: `${index + 1}. ${escapeHtml(item)}`, fontSize: 27, color: palette.text, bold: true, lineHeight: 1.32 }))
-  })
-  elements.push(...mainVisual(page, palette, { left: 1132, top: 230, width: 300, height: 300, mode: "compact" }))
-  addPrompt(elements, page, palette, 100, 672, 1150)
-  return elements
-}
-
-function interactionLayout(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
-  const elements: PPTElement[] = []
-  addTitle(elements, page, outline, palette)
-  const question = page.interaction?.question || activityTask(page) || studentHeadline(page) || "请同学们参与课堂互动"
-  elements.push(shapeElement({ left: 96, top: 178, width: 930, height: 210, fill: palette.primary, radius: 28, shadow: softShadow() }))
-  elements.push(textElement({ left: 140, top: 222, width: 820, height: 104, content: escapeHtml(question), fontSize: 38, color: "#FFFFFF", bold: true, lineHeight: 1.28 }))
-  elements.push(...mainVisual(page, palette, { left: 1090, top: 170, width: 330, height: 330, mode: "compact" }))
-  elements.push(...stepCards(activitySteps(page, 3), palette, 112, 486))
-  addPrompt(elements, page, palette, 112, 732, 1200, page.interaction?.answer ? `参考:${page.interaction.answer}` : "")
-  return elements
-}
-
-function summaryLayout(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
-  const elements: PPTElement[] = []
-  addTitle(elements, page, outline, palette)
-  const items = studentPoints(page, 4)
-  elements.push(shapeElement({ left: 110, top: 190, width: 960, height: 470, fill: "#FFFFFF", radius: 26, shadow: softShadow() }))
-  items.forEach((item, index) => {
-    const top = 236 + index * 96
-    elements.push(shapeElement({ left: 154, top, width: 48, height: 48, fill: palette.accent, radius: 16 }))
-    elements.push(textElement({ left: 168, top: top + 8, width: 22, height: 24, content: "✓", fontSize: 24, color: "#FFFFFF", bold: true }))
-    elements.push(textElement({ left: 228, top: top + 4, width: 760, height: 48, content: escapeHtml(item), fontSize: 28, color: palette.text, bold: true }))
-  })
-  elements.push(...mainVisual(page, palette, { left: 1150, top: 250, width: 300, height: 300, mode: "compact" }))
-  return elements
-}
-
-function homeworkLayout(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
-  const elements: PPTElement[] = []
-  addTitle(elements, page, outline, palette)
-  elements.push(shapeElement({ left: 110, top: 188, width: 590, height: 500, fill: "#FFFFFF", radius: 28, shadow: softShadow() }))
-  elements.push(textElement({ left: 150, top: 236, width: 460, height: 44, content: "课后练习", fontSize: 36, color: palette.primary, bold: true }))
-  elements.push(...bulletCards(activitySteps(page, 4), palette, { left: 150, top: 324, width: 490, itemHeight: 66, fontSize: 25, mark: "做" }))
-  elements.push(...mainVisual(page, palette, { left: 825, top: 188, width: 520, height: 500, mode: "photo" }))
-  return elements
-}
-
-function baseElements(palette: Palette, options: AiCoursewareVisualOptions): PPTElement[] {
-  const elements: PPTElement[] = []
-  if (options.backgroundImage) {
-    elements.push(shapeElement({ left: 0, top: 0, width: SLIDE_WIDTH, height: SLIDE_HEIGHT, fill: "rgba(255, 255, 255, 0.78)", opacity: 0.94 }))
-  }
-  elements.push(shapeElement({ left: 0, top: 0, width: 12, height: SLIDE_HEIGHT, fill: palette.primary, radius: 0 }))
-  elements.push(shapeElement({ left: 1260, top: 72, width: 210, height: 210, fill: palette.panelAlt, opacity: 0.38, radius: 100 }))
-  elements.push(shapeElement({ left: 1380, top: 690, width: 90, height: 90, fill: palette.warm, opacity: 0.38, radius: 44 }))
-  return elements
-}
-
-function addTitle(elements: PPTElement[], page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette) {
-  elements.push(shapeElement({ left: 90, top: 68, width: 76, height: 8, fill: palette.primary, radius: 5 }))
-  elements.push(textElement({ left: 92, top: 96, width: 1120, height: 62, content: page.title || outline.title || outline.chapterName, fontSize: 42, color: palette.primary, bold: true }))
-}
-
-function mainVisual(page: AiCoursewarePage, palette: Palette, rect: { left: number; top: number; width: number; height: number; mode: "hero" | "photo" | "compact" }): PPTElement[] {
-  if (page.image?.url) {
-    return [
-      shapeElement({ left: rect.left - 16, top: rect.top + 18, width: rect.width, height: rect.height, fill: palette.warm, opacity: 0.48, radius: 30 }),
-      imageElement({ left: rect.left, top: rect.top, width: rect.width, height: rect.height, src: page.image.url, radius: rect.mode === "compact" ? 28 : 34 })
-    ]
-  }
-  return teachingVisualElements(page, palette, rect)
-}
-
-function teachingVisualElements(page: AiCoursewarePage, palette: Palette, rect: { left: number; top: number; width: number; height: number; mode: "hero" | "photo" | "compact" }): PPTElement[] {
-  const elements: PPTElement[] = []
-  const visual = visualConfig(page, palette)
-  const title = visualMain(page) || visual.label
-  const chips = visualElements(page).slice(0, rect.mode === "compact" ? 2 : 4)
-  elements.push(shapeElement({ left: rect.left, top: rect.top, width: rect.width, height: rect.height, fill: visual.light, radius: 30, shadow: softShadow() }))
-  elements.push(shapeElement({ left: rect.left + rect.width * 0.11, top: rect.top + rect.height * 0.12, width: rect.width * 0.78, height: rect.height * 0.42, fill: "#FFFFFF", opacity: 0.92, radius: 34 }))
-  elements.push(textElement({ left: rect.left + rect.width * 0.43, top: rect.top + rect.height * 0.18, width: rect.width * 0.16, height: 76, content: visual.icon, fontSize: rect.mode === "compact" ? 54 : 74, color: visual.color, bold: true }))
-  elements.push(textElement({ left: rect.left + rect.width * 0.12, top: rect.top + rect.height * 0.42, width: rect.width * 0.76, height: 44, content: escapeHtml(title), fontSize: rect.mode === "compact" ? 18 : 24, color: visual.color, bold: true }))
-  elements.push(...soundWave(rect.left + rect.width * 0.2, rect.top + rect.height * 0.58, palette, rect.width * 0.58, rect.mode === "compact" ? 56 : 78))
-  chips.forEach((chip, index) => {
-    const chipWidth = rect.mode === "compact" ? rect.width * 0.66 : rect.width * 0.38
-    const chipLeft = rect.left + rect.width * 0.12 + (rect.mode === "compact" ? 0 : (index % 2) * rect.width * 0.43)
-    const chipTop = rect.top + rect.height * 0.76 + Math.floor(index / (rect.mode === "compact" ? 1 : 2)) * 44
-    elements.push(shapeElement({ left: chipLeft, top: chipTop, width: chipWidth, height: 32, fill: "#FFFFFF", opacity: 0.9, radius: 16 }))
-    elements.push(textElement({ left: chipLeft + 14, top: chipTop + 7, width: chipWidth - 28, height: 20, content: escapeHtml(trimText(chip, 12)), fontSize: rect.mode === "compact" ? 15 : 18, color: palette.text }))
-  })
-  return elements
-}
-
-function bulletCards(items: string[], palette: Palette, config: { left: number; top: number; width: number; itemHeight: number; fontSize: number; mark: string }): PPTElement[] {
-  const elements: PPTElement[] = []
-  items.forEach((item, index) => {
-    const top = config.top + index * (config.itemHeight + 16)
-    elements.push(shapeElement({ left: config.left, top, width: config.width, height: config.itemHeight, fill: "#FFFFFF", radius: 20, shadow: softShadow() }))
-    elements.push(shapeElement({ left: config.left + 24, top: top + config.itemHeight / 2 - 18, width: 36, height: 36, fill: palette.primary, radius: 14 }))
-    elements.push(textElement({ left: config.left + 32, top: top + config.itemHeight / 2 - 13, width: 24, height: 22, content: escapeHtml(config.mark), fontSize: 18, color: "#FFFFFF", bold: true }))
-    elements.push(textElement({ left: config.left + 78, top: top + 18, width: config.width - 104, height: config.itemHeight - 28, content: escapeHtml(item), fontSize: config.fontSize, color: palette.text, lineHeight: 1.25 }))
-  })
-  return elements
-}
-
-function stepCards(items: string[], palette: Palette, left: number, top: number): PPTElement[] {
-  const elements: PPTElement[] = []
-  items.forEach((item, index) => {
-    const itemLeft = left + index * 405
-    elements.push(shapeElement({ left: itemLeft, top, width: 350, height: 160, fill: "#FFFFFF", radius: 24, shadow: softShadow() }))
-    elements.push(textElement({ left: itemLeft + 30, top: top + 26, width: 80, height: 32, content: `步骤${index + 1}`, fontSize: 22, color: palette.primary, bold: true }))
-    elements.push(textElement({ left: itemLeft + 30, top: top + 72, width: 290, height: 68, content: escapeHtml(item), fontSize: 25, color: palette.text, lineHeight: 1.3 }))
-  })
-  return elements
-}
-
-function addPrompt(elements: PPTElement[], page: AiCoursewarePage, palette: Palette, left: number, top: number, width: number, fallback = "") {
-  const text = fallback || page.interaction?.question || ""
-  if (!text) return
-  elements.push(shapeElement({ left, top, width, height: 76, fill: palette.panelAlt, radius: 20 }))
-  elements.push(textElement({ left: left + 28, top: top + 20, width: width - 56, height: 34, content: escapeHtml(text), fontSize: 23, color: palette.text, lineHeight: 1.3 }))
-}
-
-function soundWave(left: number, top: number, palette: Palette, width = 330, maxHeight = 140): PPTElement[] {
-  const elements: PPTElement[] = []
-  const heights = [46, 76, 112, 70, 132, 92, 54].map(height => (height / 140) * maxHeight)
-  heights.forEach((height, index) => {
-    const barWidth = Math.max(18, width / 18)
-    const gap = Math.max(20, width / 10)
-    elements.push(shapeElement({ left: left + index * gap, top: top + (maxHeight - height) / 2, width: barWidth, height, fill: index % 2 === 0 ? palette.primary : palette.accent, radius: 10 }))
-  })
-  return elements
-}
-
-function studentHeadline(page: AiCoursewarePage) {
-  return String(page.studentContent?.headline || page.subtitle || "").trim()
-}
-
-function studentPoints(page: AiCoursewarePage, limit: number): string[] {
-  const structuredPoints = (page.studentContent?.points || []).filter(Boolean).map(item => item.trim()).filter(Boolean)
-  const legacyPoints = (page.bullets || []).filter(Boolean).map(item => item.trim()).filter(Boolean)
-  const source = structuredPoints.length ? structuredPoints : legacyPoints
-  const result = source.length ? source.slice(0, limit) : [page.subtitle || page.title || "课堂内容"]
-  return result.map(item => trimText(item, 42))
-}
-
-function activityTask(page: AiCoursewarePage) {
-  return String(page.classroomAction?.studentTask || page.activity?.task || "").trim()
-}
-
-function activitySteps(page: AiCoursewarePage, limit: number): string[] {
-  const steps = [
-    page.classroomAction?.studentTask,
-    ...(page.activity?.steps || [])
-  ].filter(Boolean).map(item => String(item).trim()).filter(Boolean)
-  const result = steps.length ? steps.slice(0, limit) : studentPoints(page, limit)
-  return result.map(item => trimText(item, 46))
-}
-
-function pageKind(page: AiCoursewarePage) {
-  return `${page.type || ""} ${page.visualPlan?.layout || ""} ${page.visual?.layout || ""}`.toLowerCase()
-}
-
-function slideRemark(page: AiCoursewarePage) {
-  return [page.speakerNotes, page.teacherTips, page.classroomAction?.teacherInstruction, page.teachingPurpose]
-    .filter(Boolean)
-    .map(item => String(item).trim())
-    .filter(Boolean)
-    .filter((item, index, array) => array.indexOf(item) === index)
-    .join("\n")
-}
-
-function backgroundConfig(palette: Palette, options: AiCoursewareVisualOptions): SlideBackground {
-  if (options.backgroundImage) {
-    return {
-      type: "image",
-      image: options.backgroundImage,
-      imageSize: "cover"
-    }
-  }
-  return {
-    type: "solid",
-    color: palette.background
-  }
-}
-
-function buildPalette(style: string): Palette {
-  if (style === "clean") {
-    return {
-      primary: "#2563EB",
-      secondary: "#0F766E",
-      accent: "#7C3AED",
-      warm: "#FEF3C7",
-      background: "#F7FAFC",
-      panelAlt: "#EAF2FF",
-      text: "#1F2933",
-      muted: "#64748B"
-    }
-  }
-  return {
-    primary: "#FF7A3D",
-    secondary: "#2BAE9F",
-    accent: "#5B8DEF",
-    warm: "#FFE8B8",
-    background: "#FFF7EC",
-    panelAlt: "#EAF6F3",
-    text: "#263238",
-    muted: "#6B7C8F"
-  }
-}
-
-function visualConfig(page: AiCoursewarePage, palette: Palette) {
-  const type = pageKind(page)
-  const visualLabel = trimText(visualMain(page) || page.visual?.style || "", 10)
-  if (type.includes("interaction") || type.includes("activity")) return { icon: "?", label: visualLabel || "课堂互动", color: palette.accent, light: "#EEF2FF" }
-  if (type.includes("summary")) return { icon: "✓", label: visualLabel || "课堂小结", color: "#2E7D32", light: "#EAF7EC" }
-  if (type.includes("goal")) return { icon: "◎", label: visualLabel || "学习目标", color: "#7B1FA2", light: "#F3E8FF" }
-  if (type.includes("listen")) return { icon: "♪", label: visualLabel || "听一听", color: palette.secondary, light: "#E5F6F3" }
-  if (type.includes("homework")) return { icon: "✎", label: visualLabel || "课后练习", color: "#6D4C41", light: "#F3ECE8" }
-  return { icon: "♫", label: visualLabel || "音乐课堂", color: palette.primary, light: palette.warm }
-}
-
-function visualMain(page: AiCoursewarePage) {
-  return String(page.visualPlan?.mainVisual || page.visual?.mainVisual || "").trim()
-}
-
-function visualElements(page: AiCoursewarePage) {
-  const elements = (page.visualPlan?.elements || []).filter(Boolean).map(item => item.trim()).filter(Boolean)
-  const keywords = (page.visualPlan?.imageKeywords || page.visual?.imageKeywords || page.imageKeywords || []).filter(Boolean).map(item => item.trim()).filter(Boolean)
-  return elements.length ? elements : keywords
-}
-
-function textElement(params: {
-  left: number
-  top: number
-  width: number
-  height: number
-  content: string
-  fontSize: number
-  color: string
-  bold?: boolean
-  fill?: string
-  lineHeight?: number
-}): PPTTextElement {
-  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;" : ""}">${params.content}</div>`,
-    defaultFontName: FONT_NAME,
-    defaultColor: params.color,
-    fill: params.fill,
-    lineHeight: params.lineHeight || 1.25,
-    wordSpace: 0,
-    opacity: 1
-  }
-}
-
-function imageElement(params: { left: number; top: number; width: number; height: number; src: string; radius?: number }): PPTImageElement {
-  return {
-    id: nanoid(10),
-    type: "image",
-    left: params.left,
-    top: params.top,
-    width: params.width,
-    height: params.height,
-    rotate: 0,
-    src: params.src,
-    fixedRatio: false,
-    radius: params.radius || 18,
-    shadow: softShadow()
-  }
-}
-
-function shapeElement(params: {
-  left: number
-  top: number
-  width: number
-  height: number
-  fill: string
-  opacity?: number
-  radius?: number
-  shadow?: PPTElementShadow
-}): PPTShapeElement {
-  return {
-    id: nanoid(10),
-    type: "shape",
-    left: params.left,
-    top: params.top,
-    width: params.width,
-    height: params.height,
-    rotate: 0,
-    viewBox: [200, 200],
-    path: roundedRectPath(params.radius ?? 18),
-    fixedRatio: false,
-    fill: params.fill,
-    opacity: params.opacity ?? 1,
-    shadow: params.shadow
-  }
-}
-
-function softShadow(): PPTElementShadow {
-  return {
-    h: 0,
-    v: 8,
-    blur: 22,
-    color: "rgba(38, 50, 56, 0.14)"
-  }
-}
-
-function roundedRectPath(radius: number) {
-  const r = Math.max(0, Math.min(radius, 100))
-  return `M ${r} 0 L ${200 - r} 0 Q 200 0 200 ${r} L 200 ${200 - r} Q 200 200 ${200 - r} 200 L ${r} 200 Q 0 200 0 ${200 - r} L 0 ${r} Q 0 0 ${r} 0 Z`
-}
-
-function trimText(value: string, maxLength: number) {
-  return value.length > maxLength ? `${value.slice(0, maxLength - 1)}...` : value
-}
-
-function escapeHtml(value: string) {
-  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
+  return buildAiPptSlides(outline, options)
 }

+ 511 - 0
src/utils/aiPptTemplateEngine.ts

@@ -0,0 +1,511 @@
+import { nanoid } from "nanoid"
+import type { Slide, PPTElement, SlideTheme, PPTTextElement, PPTImageElement, PPTShapeElement, SlideBackground, PPTElementShadow } from "@/types/slides"
+import type { AiCoursewareOutline, AiCoursewarePage } from "@/api/aiCourseware"
+
+interface AiPptOptions {
+  backgroundImage?: string
+}
+
+interface Palette {
+  primary: string
+  secondary: string
+  accent: string
+  warm: string
+  background: string
+  panel: string
+  panelAlt: string
+  text: string
+  muted: string
+}
+
+interface VisualRect {
+  left: number
+  top: number
+  width: number
+  height: number
+}
+
+const SLIDE_WIDTH = 1600
+const SLIDE_HEIGHT = 900
+const FONT_NAME = "Microsoft Yahei"
+
+export function buildAiPptSlides(outline: AiCoursewareOutline, options: AiPptOptions = {}): { title: string; theme: Partial<SlideTheme>; slides: Slide[] } {
+  const palette = buildPalette(outline)
+  return {
+    title: outline.title || outline.chapterName || "音乐课件",
+    theme: {
+      themeColor: palette.primary,
+      backgroundColor: palette.background,
+      fontColor: palette.text,
+      fontName: FONT_NAME,
+      fontSize: "36px"
+    },
+    slides: outline.pages.map(page => buildSlide(page, outline, palette, options))
+  }
+}
+
+function buildSlide(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette, options: AiPptOptions): Slide {
+  const elements = baseElements(palette, options)
+  const kind = pageKind(page)
+
+  if (page.pageNo === 1 || kind.includes("cover")) {
+    elements.push(...coverTemplate(page, outline, palette))
+  } else if (kind.includes("goal")) {
+    elements.push(...cardsTemplate(page, outline, palette, "学习目标"))
+  } else if (kind.includes("listen")) {
+    elements.push(...listenTemplate(page, outline, palette))
+  } else if (kind.includes("lyric") || kind.includes("content")) {
+    elements.push(...contentTemplate(page, outline, palette))
+  } else if (kind.includes("rhythm") || kind.includes("skill")) {
+    elements.push(...rhythmTemplate(page, outline, palette))
+  } else if (kind.includes("knowledge") || kind.includes("extension")) {
+    elements.push(...cardsTemplate(page, outline, palette, "音乐知识"))
+  } else if (kind.includes("interaction") || kind.includes("activity")) {
+    elements.push(...activityTemplate(page, outline, palette))
+  } else if (kind.includes("summary")) {
+    elements.push(...summaryTemplate(page, outline, palette))
+  } else if (kind.includes("homework")) {
+    elements.push(...homeworkTemplate(page, outline, palette))
+  } else {
+    elements.push(...splitTemplate(page, outline, palette))
+  }
+
+  return {
+    id: nanoid(10),
+    elements: keepInCanvas(elements),
+    background: backgroundConfig(palette, options),
+    remark: slideRemark(page)
+  }
+}
+
+function coverTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
+  const elements: PPTElement[] = []
+  const subtitle = [outline.gradeName, outline.textbookName, outline.unitName].filter(Boolean).join(" · ")
+  elements.push(titleMark(92, 92, palette.primary))
+  elements.push(textElement({ left: 92, top: 138, width: 780, height: 100, content: pageTitle(page, outline), fontSize: 54, color: palette.primary, bold: true }))
+  if (subtitle) elements.push(textElement({ left: 96, top: 266, width: 780, height: 34, content: subtitle, fontSize: 22, color: palette.text }))
+  elements.push(...heroVisual(page, palette, { left: 940, top: 122, width: 470, height: 420 }))
+  elements.push(...compactBullets(studentPoints(page, 2), palette, 96, 390, 660, "♪"))
+  addTeacherAction(elements, page, palette, 96, 715, 900)
+  return elements
+}
+
+function splitTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
+  const elements: PPTElement[] = []
+  addSlideTitle(elements, page, outline, palette)
+  elements.push(...visualBlock(page, palette, { left: 925, top: 178, width: 500, height: 430 }))
+  elements.push(...largeStudentPanel(page, palette, 96, 188, 700, 440))
+  addTeacherAction(elements, page, palette, 96, 692, 1190)
+  return elements
+}
+
+function contentTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
+  const elements: PPTElement[] = []
+  addSlideTitle(elements, page, outline, palette)
+  elements.push(shapeElement({ left: 96, top: 184, width: 720, height: 460, fill: palette.panel, radius: 24, shadow: softShadow() }))
+  elements.push(textElement({ left: 136, top: 226, width: 640, height: 54, content: studentHeadline(page) || page.title, fontSize: 34, color: palette.primary, bold: true }))
+  studentPoints(page, 4).forEach((point, index) => {
+    const top = 318 + index * 72
+    elements.push(shapeElement({ left: 138, top, width: 24, height: 24, fill: index % 2 ? palette.secondary : palette.primary, radius: 6 }))
+    elements.push(textElement({ left: 186, top: top - 4, width: 560, height: 42, content: point, fontSize: 28, color: palette.text }))
+  })
+  elements.push(...visualBlock(page, palette, { left: 900, top: 204, width: 510, height: 410 }))
+  addTeacherAction(elements, page, palette, 110, 700, 1180)
+  return elements
+}
+
+function listenTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
+  const elements: PPTElement[] = []
+  addSlideTitle(elements, page, outline, palette)
+  const task = activityTask(page) || studentHeadline(page) || "听一听:音乐中有哪些声音?"
+  elements.push(shapeElement({ left: 96, top: 184, width: 760, height: 150, fill: palette.primary, radius: 26, shadow: softShadow() }))
+  elements.push(textElement({ left: 142, top: 226, width: 660, height: 60, content: task, fontSize: 34, color: "#FFFFFF", bold: true, lineHeight: 1.25 }))
+  elements.push(...soundWave(136, 390, palette, 610, 115))
+  elements.push(...compactBullets(studentPoints(page, 3), palette, 104, 552, 690, "听"))
+  elements.push(...visualBlock(page, palette, { left: 925, top: 190, width: 490, height: 430 }))
+  addTeacherAction(elements, page, palette, 110, 724, 1180)
+  return elements
+}
+
+function rhythmTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
+  const elements: PPTElement[] = []
+  addSlideTitle(elements, page, outline, palette)
+  const task = activityTask(page) || studentHeadline(page) || "拍一拍,感受节奏"
+  elements.push(shapeElement({ left: 105, top: 185, width: 570, height: 96, fill: palette.primary, radius: 22, shadow: softShadow() }))
+  elements.push(textElement({ left: 144, top: 216, width: 500, height: 36, content: task, fontSize: 30, color: "#FFFFFF", bold: true }))
+  const beats = activitySteps(page, 4)
+  beats.forEach((beat, index) => {
+    const left = 108 + index * 205
+    elements.push(shapeElement({ left, top: 365, width: 160, height: 160, fill: index % 2 ? palette.panelAlt : palette.panel, radius: 24, shadow: softShadow() }))
+    elements.push(textElement({ left: left + 44, top: 400, width: 74, height: 48, content: `${index + 1}`, fontSize: 38, color: palette.primary, bold: true }))
+    elements.push(textElement({ left: left + 22, top: 468, width: 116, height: 40, content: beat, fontSize: 20, color: palette.text, lineHeight: 1.25 }))
+  })
+  elements.push(...visualBlock(page, palette, { left: 980, top: 230, width: 360, height: 360 }))
+  addTeacherAction(elements, page, palette, 108, 704, 1190)
+  return elements
+}
+
+function cardsTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette, fallbackHeadline: string): PPTElement[] {
+  const elements: PPTElement[] = []
+  addSlideTitle(elements, page, outline, palette)
+  const points = studentPoints(page, 4)
+  points.forEach((point, index) => {
+    const left = 108 + (index % 2) * 520
+    const top = 210 + Math.floor(index / 2) * 188
+    elements.push(shapeElement({ left, top, width: 460, height: 136, fill: index % 2 ? palette.panelAlt : palette.panel, radius: 22, shadow: softShadow() }))
+    elements.push(shapeElement({ left: left + 28, top: top + 34, width: 42, height: 42, fill: index % 2 ? palette.secondary : palette.primary, radius: 12 }))
+    elements.push(textElement({ left: left + 42, top: top + 42, width: 18, height: 22, content: `${index + 1}`, fontSize: 20, color: "#FFFFFF", bold: true }))
+    elements.push(textElement({ left: left + 92, top: top + 34, width: 320, height: 58, content: point, fontSize: 25, color: palette.text, bold: true, lineHeight: 1.3 }))
+  })
+  elements.push(...visualBlock(page, palette, { left: 1145, top: 248, width: 265, height: 265 }))
+  addTeacherAction(elements, page, palette, 108, 706, 1180, activityTask(page) || fallbackHeadline)
+  return elements
+}
+
+function activityTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
+  const elements: PPTElement[] = []
+  addSlideTitle(elements, page, outline, palette)
+  const task = activityTask(page) || page.interaction?.question || studentHeadline(page) || "课堂互动"
+  elements.push(shapeElement({ left: 108, top: 185, width: 900, height: 165, fill: palette.primary, radius: 28, shadow: softShadow() }))
+  elements.push(textElement({ left: 154, top: 235, width: 800, height: 58, content: task, fontSize: 36, color: "#FFFFFF", bold: true, lineHeight: 1.25 }))
+  activitySteps(page, 3).forEach((step, index) => {
+    const left = 118 + index * 392
+    elements.push(shapeElement({ left, top: 455, width: 335, height: 142, fill: palette.panel, radius: 22, shadow: softShadow() }))
+    elements.push(textElement({ left: left + 26, top: 484, width: 88, height: 26, content: `步骤${index + 1}`, fontSize: 21, color: palette.primary, bold: true }))
+    elements.push(textElement({ left: left + 26, top: 532, width: 280, height: 42, content: step, fontSize: 24, color: palette.text, lineHeight: 1.28 }))
+  })
+  elements.push(...visualBlock(page, palette, { left: 1095, top: 195, width: 300, height: 300 }))
+  addTeacherAction(elements, page, palette, 116, 710, 1170, page.interaction?.answer || "")
+  return elements
+}
+
+function summaryTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
+  const elements: PPTElement[] = []
+  addSlideTitle(elements, page, outline, palette)
+  elements.push(shapeElement({ left: 112, top: 188, width: 900, height: 470, fill: palette.panel, radius: 26, shadow: softShadow() }))
+  studentPoints(page, 4).forEach((point, index) => {
+    const top = 240 + index * 90
+    elements.push(shapeElement({ left: 160, top, width: 44, height: 44, fill: palette.secondary, radius: 14 }))
+    elements.push(textElement({ left: 172, top: top + 8, width: 20, height: 20, content: "✓", fontSize: 23, color: "#FFFFFF", bold: true }))
+    elements.push(textElement({ left: 236, top: top + 4, width: 690, height: 38, content: point, fontSize: 28, color: palette.text, bold: true }))
+  })
+  elements.push(...visualBlock(page, palette, { left: 1120, top: 250, width: 280, height: 280 }))
+  return elements
+}
+
+function homeworkTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
+  const elements: PPTElement[] = []
+  addSlideTitle(elements, page, outline, palette)
+  elements.push(shapeElement({ left: 112, top: 196, width: 610, height: 460, fill: palette.panel, radius: 28, shadow: softShadow() }))
+  elements.push(textElement({ left: 154, top: 244, width: 460, height: 44, content: studentHeadline(page) || "课后练习", fontSize: 34, color: palette.primary, bold: true }))
+  elements.push(...compactBullets(activitySteps(page, 4), palette, 154, 340, 480, "做"))
+  elements.push(...visualBlock(page, palette, { left: 850, top: 200, width: 500, height: 420 }))
+  return elements
+}
+
+function largeStudentPanel(page: AiCoursewarePage, palette: Palette, left: number, top: number, width: number, height: number): PPTElement[] {
+  const elements: PPTElement[] = []
+  const headline = studentHeadline(page)
+  elements.push(shapeElement({ left, top, width, height, fill: palette.panel, radius: 26, shadow: softShadow() }))
+  if (headline) {
+    elements.push(textElement({ left: left + 42, top: top + 40, width: width - 84, height: 44, content: headline, fontSize: 34, color: palette.primary, bold: true }))
+  }
+  studentPoints(page, 4).forEach((point, index) => {
+    const itemTop = top + (headline ? 130 : 68) + index * 70
+    elements.push(shapeElement({ left: left + 42, top: itemTop + 8, width: 26, height: 26, fill: index % 2 ? palette.secondary : palette.primary, radius: 7 }))
+    elements.push(textElement({ left: left + 92, top: itemTop, width: width - 140, height: 38, content: point, fontSize: 27, color: palette.text, lineHeight: 1.25 }))
+  })
+  return elements
+}
+
+function compactBullets(items: string[], palette: Palette, left: number, top: number, width: number, mark: string): PPTElement[] {
+  const elements: PPTElement[] = []
+  items.slice(0, 4).forEach((item, index) => {
+    const itemTop = top + index * 64
+    elements.push(shapeElement({ left, top: itemTop, width, height: 48, fill: "#FFFFFF", opacity: 0.94, radius: 14, shadow: lightShadow() }))
+    elements.push(shapeElement({ left: left + 20, top: itemTop + 13, width: 22, height: 22, fill: index % 2 ? palette.secondary : palette.primary, radius: 5 }))
+    elements.push(textElement({ left: left + 62, top: itemTop + 8, width: width - 88, height: 30, content: item, fontSize: 24, color: palette.text }))
+  })
+  return elements
+}
+
+function visualBlock(page: AiCoursewarePage, palette: Palette, rect: VisualRect): PPTElement[] {
+  if (page.image?.url) {
+    return [
+      shapeElement({ left: rect.left - 14, top: rect.top + 16, width: rect.width, height: rect.height, fill: palette.warm, opacity: 0.45, radius: 26 }),
+      imageElement({ left: rect.left, top: rect.top, width: rect.width, height: rect.height, src: page.image.url, radius: 28 })
+    ]
+  }
+  return fallbackVisual(page, palette, rect)
+}
+
+function heroVisual(page: AiCoursewarePage, palette: Palette, rect: VisualRect): PPTElement[] {
+  return visualBlock(page, palette, rect)
+}
+
+function fallbackVisual(page: AiCoursewarePage, palette: Palette, rect: VisualRect): PPTElement[] {
+  const elements: PPTElement[] = []
+  const visual = visualConfig(page, palette)
+  const label = visualMain(page) || visual.label
+  const tags = visualElements(page).slice(0, 3)
+  elements.push(shapeElement({ left: rect.left, top: rect.top, width: rect.width, height: rect.height, fill: visual.light, radius: 28, shadow: softShadow() }))
+  elements.push(shapeElement({ left: rect.left + rect.width * 0.18, top: rect.top + rect.height * 0.13, width: rect.width * 0.64, height: rect.height * 0.36, fill: "#FFFFFF", opacity: 0.92, radius: 30 }))
+  elements.push(textElement({ left: rect.left + rect.width * 0.42, top: rect.top + rect.height * 0.2, width: rect.width * 0.18, height: 58, content: visual.icon, fontSize: 54, color: visual.color, bold: true }))
+  elements.push(...soundWave(rect.left + rect.width * 0.24, rect.top + rect.height * 0.58, palette, rect.width * 0.5, 70))
+  elements.push(textElement({ left: rect.left + rect.width * 0.14, top: rect.top + rect.height * 0.43, width: rect.width * 0.72, height: 34, content: trimText(label, 14), fontSize: 21, color: visual.color, bold: true }))
+  tags.forEach((tag, index) => {
+    const tagTop = rect.top + rect.height * 0.76 + index * 34
+    elements.push(shapeElement({ left: rect.left + rect.width * 0.22, top: tagTop, width: rect.width * 0.56, height: 26, fill: "#FFFFFF", opacity: 0.86, radius: 13 }))
+    elements.push(textElement({ left: rect.left + rect.width * 0.26, top: tagTop + 5, width: rect.width * 0.48, height: 16, content: trimText(tag, 12), fontSize: 14, color: palette.muted }))
+  })
+  return elements
+}
+
+function addSlideTitle(elements: PPTElement[], page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette) {
+  elements.push(titleMark(92, 74, palette.primary))
+  elements.push(textElement({ left: 92, top: 102, width: 1120, height: 58, content: page.title || outline.title || outline.chapterName, fontSize: 40, color: palette.primary, bold: true }))
+}
+
+function addTeacherAction(elements: PPTElement[], page: AiCoursewarePage, palette: Palette, left: number, top: number, width: number, fallback = "") {
+  const text = fallback || page.interaction?.question || page.classroomAction?.studentTask || activityTask(page)
+  if (!text) return
+  elements.push(shapeElement({ left, top, width, height: 54, fill: palette.panelAlt, opacity: 0.92, radius: 14 }))
+  elements.push(textElement({ left: left + 24, top: top + 15, width: width - 48, height: 24, content: trimText(text, 48), fontSize: 20, color: palette.text }))
+}
+
+function titleMark(left: number, top: number, color: string): PPTShapeElement {
+  return shapeElement({ left, top, width: 78, height: 8, fill: color, radius: 5 })
+}
+
+function soundWave(left: number, top: number, palette: Palette, width = 330, maxHeight = 120): PPTElement[] {
+  const elements: PPTElement[] = []
+  const heights = [42, 76, 108, 70, 120, 88, 52].map(height => (height / 120) * maxHeight)
+  heights.forEach((height, index) => {
+    const barWidth = Math.max(14, width / 22)
+    const gap = Math.max(18, width / 9)
+    elements.push(shapeElement({ left: left + index * gap, top: top + (maxHeight - height) / 2, width: barWidth, height, fill: index % 2 === 0 ? palette.primary : palette.accent, radius: 8 }))
+  })
+  return elements
+}
+
+function baseElements(palette: Palette, options: AiPptOptions): PPTElement[] {
+  const elements: PPTElement[] = []
+  if (options.backgroundImage) {
+    elements.push(shapeElement({ left: 0, top: 0, width: SLIDE_WIDTH, height: SLIDE_HEIGHT, fill: "rgba(255,255,255,0.82)", opacity: 0.96 }))
+  }
+  elements.push(shapeElement({ left: 0, top: 0, width: 10, height: SLIDE_HEIGHT, fill: palette.primary, radius: 0 }))
+  elements.push(shapeElement({ left: 1260, top: 70, width: 210, height: 210, fill: palette.panelAlt, opacity: 0.28, radius: 100 }))
+  elements.push(shapeElement({ left: 1380, top: 690, width: 88, height: 88, fill: palette.warm, opacity: 0.3, radius: 42 }))
+  return elements
+}
+
+function studentHeadline(page: AiCoursewarePage) {
+  return cleanText(page.studentContent?.headline || page.subtitle || "")
+}
+
+function studentPoints(page: AiCoursewarePage, limit: number): string[] {
+  const structured = (page.studentContent?.points || []).map(cleanText).filter(Boolean)
+  const legacy = (page.bullets || []).map(cleanText).filter(Boolean)
+  const source = structured.length ? structured : legacy
+  return (source.length ? source : [studentHeadline(page) || page.title || "课堂内容"]).slice(0, limit).map(item => trimText(item, 34))
+}
+
+function activityTask(page: AiCoursewarePage) {
+  return cleanText(page.classroomAction?.studentTask || page.activity?.task || "")
+}
+
+function activitySteps(page: AiCoursewarePage, limit: number): string[] {
+  const steps = [page.classroomAction?.studentTask, ...(page.activity?.steps || [])].map(cleanText).filter(Boolean)
+  return (steps.length ? steps : studentPoints(page, limit)).slice(0, limit).map(item => trimText(item, 32))
+}
+
+function pageTitle(page: AiCoursewarePage, outline: AiCoursewareOutline) {
+  return cleanText(page.title || outline.title || outline.chapterName || "音乐课件")
+}
+
+function pageKind(page: AiCoursewarePage) {
+  return `${page.type || ""} ${page.visualPlan?.layout || ""} ${page.visual?.layout || ""}`.toLowerCase()
+}
+
+function slideRemark(page: AiCoursewarePage) {
+  return [page.speakerNotes, page.teacherTips, page.classroomAction?.teacherInstruction, page.teachingPurpose]
+    .map(cleanText)
+    .filter(Boolean)
+    .filter((item, index, array) => array.indexOf(item) === index)
+    .join("\n")
+}
+
+function visualMain(page: AiCoursewarePage) {
+  return cleanText(page.visualPlan?.mainVisual || page.visual?.mainVisual || "")
+}
+
+function visualElements(page: AiCoursewarePage) {
+  const elements = (page.visualPlan?.elements || []).map(cleanText).filter(Boolean)
+  const keywords = (page.visualPlan?.imageKeywords || page.visual?.imageKeywords || page.imageKeywords || []).map(cleanText).filter(Boolean)
+  return elements.length ? elements : keywords
+}
+
+function visualConfig(page: AiCoursewarePage, palette: Palette) {
+  const kind = pageKind(page)
+  const label = trimText(visualMain(page) || page.visual?.style || "", 12)
+  if (kind.includes("interaction") || kind.includes("activity")) return { icon: "?", label: label || "课堂互动", color: palette.accent, light: "#EEF2FF" }
+  if (kind.includes("summary")) return { icon: "✓", label: label || "课堂小结", color: "#2E7D32", light: "#EAF7EC" }
+  if (kind.includes("goal")) return { icon: "◎", label: label || "学习目标", color: "#7B1FA2", light: "#F3E8FF" }
+  if (kind.includes("listen")) return { icon: "♪", label: label || "听赏任务", color: palette.secondary, light: "#E5F6F3" }
+  if (kind.includes("rhythm") || kind.includes("skill")) return { icon: "♩", label: label || "节奏练习", color: palette.primary, light: "#FFF1E7" }
+  if (kind.includes("homework")) return { icon: "✎", label: label || "课后练习", color: "#6D4C41", light: "#F3ECE8" }
+  return { icon: "♫", label: label || "音乐课堂", color: palette.primary, light: palette.warm }
+}
+
+function backgroundConfig(palette: Palette, options: AiPptOptions): SlideBackground {
+  if (options.backgroundImage) return { type: "image", image: options.backgroundImage, imageSize: "cover" }
+  return { type: "solid", color: palette.background }
+}
+
+function buildPalette(outline: AiCoursewareOutline): Palette {
+  if (outline.style === "clean") {
+    return {
+      primary: "#2563EB",
+      secondary: "#0F766E",
+      accent: "#7C3AED",
+      warm: "#FEF3C7",
+      background: "#F7FAFC",
+      panel: "#FFFFFF",
+      panelAlt: "#EAF2FF",
+      text: "#1F2933",
+      muted: "#64748B"
+    }
+  }
+  const variants: Palette[] = [
+    {
+      primary: "#FF7A3D",
+      secondary: "#2BAE9F",
+      accent: "#5B8DEF",
+      warm: "#FFE8B8",
+      background: "#FFF7EC",
+      panel: "#FFFFFF",
+      panelAlt: "#EAF6F3",
+      text: "#263238",
+      muted: "#6B7C8F"
+    },
+    {
+      primary: "#EF5DA8",
+      secondary: "#36B37E",
+      accent: "#4C6FFF",
+      warm: "#FFF0C7",
+      background: "#FFF8F8",
+      panel: "#FFFFFF",
+      panelAlt: "#ECFDF5",
+      text: "#263238",
+      muted: "#6B7C8F"
+    }
+  ]
+  return variants[Math.abs((outline.chapterName || outline.title || "").length) % variants.length]
+}
+
+function textElement(params: {
+  left: number
+  top: number
+  width: number
+  height: number
+  content: string
+  fontSize: number
+  color: string
+  bold?: boolean
+  lineHeight?: number
+}): PPTTextElement {
+  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
+  }
+}
+
+function imageElement(params: { left: number; top: number; width: number; height: number; src: string; radius?: number }): PPTImageElement {
+  return {
+    id: nanoid(10),
+    type: "image",
+    left: params.left,
+    top: params.top,
+    width: params.width,
+    height: params.height,
+    rotate: 0,
+    src: params.src,
+    fixedRatio: false,
+    radius: params.radius || 18,
+    shadow: softShadow()
+  }
+}
+
+function shapeElement(params: {
+  left: number
+  top: number
+  width: number
+  height: number
+  fill: string
+  opacity?: number
+  radius?: number
+  shadow?: PPTElementShadow
+}): PPTShapeElement {
+  return {
+    id: nanoid(10),
+    type: "shape",
+    left: params.left,
+    top: params.top,
+    width: params.width,
+    height: params.height,
+    rotate: 0,
+    viewBox: [200, 200],
+    path: roundedRectPath(params.radius ?? 18),
+    fixedRatio: false,
+    fill: params.fill,
+    opacity: params.opacity ?? 1,
+    shadow: params.shadow
+  }
+}
+
+function keepInCanvas(elements: PPTElement[]) {
+  return elements.map(element => ({
+    ...element,
+    left: Math.max(0, Math.min(element.left, SLIDE_WIDTH - 1)),
+    top: Math.max(0, Math.min(element.top, SLIDE_HEIGHT - 1)),
+    width: Math.max(1, Math.min(element.width, SLIDE_WIDTH - element.left)),
+    height: Math.max(1, Math.min(element.height, SLIDE_HEIGHT - element.top))
+  })) as PPTElement[]
+}
+
+function softShadow(): PPTElementShadow {
+  return { h: 0, v: 8, blur: 22, color: "rgba(38, 50, 56, 0.14)" }
+}
+
+function lightShadow(): PPTElementShadow {
+  return { h: 0, v: 6, blur: 16, color: "rgba(38, 50, 56, 0.1)" }
+}
+
+function roundedRectPath(radius: number) {
+  const r = Math.max(0, Math.min(radius, 100))
+  return `M ${r} 0 L ${200 - r} 0 Q 200 0 200 ${r} L 200 ${200 - r} Q 200 200 ${200 - r} 200 L ${r} 200 Q 0 200 0 ${200 - r} L 0 ${r} Q 0 0 ${r} 0 Z`
+}
+
+function cleanText(value?: string) {
+  return String(value || "").replace(/\s+/g, " ").trim()
+}
+
+function trimText(value: string, maxLength: number) {
+  const text = cleanText(value)
+  return text.length > maxLength ? `${text.slice(0, maxLength - 1)}...` : text
+}
+
+function escapeHtml(value: string) {
+  return cleanText(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
+}

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