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

BIN
dist/ai-courseware-templates/fairytale-forest.pptx


BIN
dist/ai-courseware-templates/ink-wash.pptx


BIN
dist/ai-courseware-templates/modern-geometry.pptx


BIN
dist/ai-courseware-templates/music-doodle.pptx


BIN
dist/ai-courseware-templates/stage-documentary.pptx


BIN
dist/ai-courseware-templates/warm-handdrawn.pptx


+ 1 - 1
dist/assets/index-cOqpW_Yu.js → dist/assets/index-Bb4im07Y.js

@@ -1 +1 @@
-import{_ as t}from"./404-R0y5dkvZ.js";import{E as c}from"./index-Dx5yLpXe.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,bO as m,bP as f,bH as u}from"./index-RU3f-24x.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})])]))}}),C=u(v,[["__scopeId","data-v-340bbdaa"]]);export{C as default};
+import{_ as t}from"./404-R0y5dkvZ.js";import{E as c}from"./index-Czb4c4TQ.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,bO as m,bP as f,bH as u}from"./index-Cl8r2IqW.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})])]))}}),C=u(v,[["__scopeId","data-v-340bbdaa"]]);export{C as default};

+ 1 - 1
dist/assets/index-3u7FQsBF.js → dist/assets/index-BojStd_o.js

@@ -1 +1 @@
-import{_ as a}from"./404-R0y5dkvZ.js";import{a5 as o,en as t,ae as r,af as c,bO as _,bP as d,ak as s,bH as p}from"./index-RU3f-24x.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,bO as _,bP as d,ak as s,bH as p}from"./index-Cl8r2IqW.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-D9LdPdRD.js → dist/assets/index-CXImoIxQ.js

@@ -1 +1 @@
-import{u as s,af as r}from"./index-jcQEKldN.js";import{a5 as c,cD as n,R as p,ae as t,af as _,al as u,ag as i,bH as f}from"./index-RU3f-24x.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}))]))}}),h=f(m,[["__scopeId","data-v-678905e9"]]);export{h as default};
+import{u as s,af as r}from"./index-DAf27j6U.js";import{a5 as c,cD as n,R as p,ae as t,af as _,al as u,ag as i,bH as f}from"./index-Cl8r2IqW.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}))]))}}),h=f(m,[["__scopeId","data-v-678905e9"]]);export{h as default};

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


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


+ 1 - 1
dist/assets/index-jcQEKldN.js → dist/assets/index-DAf27j6U.js

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

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


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


+ 1 - 1
dist/assets/index-lDM_MoI3.js → dist/assets/index-QgeN-F-_.js

@@ -1 +1 @@
-import{u as c,af as l}from"./index-jcQEKldN.js";import{w as p,bA as d,a5 as g,cD as m,R as t,ae as r,af as u,al as f,ag as w,bH as k}from"./index-RU3f-24x.js";import"./shapes-BHz5E06k.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}))]))}}),P=k(_,[["__scopeId","data-v-90b2692a"]]);export{P as default};
+import{u as c,af as l}from"./index-DAf27j6U.js";import{w as p,bA as d,a5 as g,cD as m,R as t,ae as r,af as u,al as f,ag as w,bH as k}from"./index-Cl8r2IqW.js";import"./shapes-CgitaJ8K.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}))]))}}),P=k(_,[["__scopeId","data-v-90b2692a"]]);export{P as default};

+ 1 - 1
dist/assets/shapes-BHz5E06k.js → dist/assets/shapes-CgitaJ8K.js

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

BIN
public/ai-courseware-templates/fairytale-forest.pptx


BIN
public/ai-courseware-templates/ink-wash.pptx


BIN
public/ai-courseware-templates/modern-geometry.pptx


BIN
public/ai-courseware-templates/music-doodle.pptx


BIN
public/ai-courseware-templates/stage-documentary.pptx


BIN
public/ai-courseware-templates/warm-handdrawn.pptx


+ 300 - 0
scripts/build_music_template_masters.py

@@ -0,0 +1,300 @@
+from __future__ import annotations
+
+from pathlib import Path
+
+from pptx import Presentation
+from pptx.dml.color import RGBColor
+from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE
+from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
+from pptx.util import Inches, Pt
+
+
+OUT_DIR = Path("public/ai-courseware-templates")
+W, H = 13.333, 7.5
+
+
+def rgb(value: str) -> RGBColor:
+    value = value.lstrip("#")
+    return RGBColor(int(value[0:2], 16), int(value[2:4], 16), int(value[4:6], 16))
+
+
+def box(slide, x, y, w, h, color, radius=False, alpha=None, line=None):
+    shape_type = MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE if radius else MSO_AUTO_SHAPE_TYPE.RECTANGLE
+    shape = slide.shapes.add_shape(shape_type, Inches(x), Inches(y), Inches(w), Inches(h))
+    shape.fill.solid()
+    shape.fill.fore_color.rgb = rgb(color)
+    if alpha is not None:
+        shape.fill.transparency = alpha
+    shape.line.fill.background() if line is None else setattr(shape.line.color, "rgb", rgb(line))
+    return shape
+
+
+def circle(slide, x, y, d, color, alpha=None):
+    shape = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.OVAL, Inches(x), Inches(y), Inches(d), Inches(d))
+    shape.fill.solid()
+    shape.fill.fore_color.rgb = rgb(color)
+    if alpha is not None:
+        shape.fill.transparency = alpha
+    shape.line.fill.background()
+    return shape
+
+
+def line(slide, x1, y1, x2, y2, color, width=1.5, alpha=None):
+    shape = slide.shapes.add_connector(1, Inches(x1), Inches(y1), Inches(x2), Inches(y2))
+    shape.line.color.rgb = rgb(color)
+    shape.line.width = Pt(width)
+    if alpha is not None:
+        shape.line.transparency = alpha
+    return shape
+
+
+def text(slide, value, x, y, w, h, size=20, color="#1F2937", bold=False, font="Microsoft YaHei", align=PP_ALIGN.LEFT):
+    shape = slide.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h))
+    frame = shape.text_frame
+    frame.clear()
+    frame.word_wrap = True
+    frame.vertical_anchor = MSO_ANCHOR.MIDDLE
+    paragraph = frame.paragraphs[0]
+    paragraph.alignment = align
+    run = paragraph.add_run()
+    run.text = value
+    run.font.name = font
+    run.font.size = Pt(size)
+    run.font.bold = bold
+    run.font.color.rgb = rgb(color)
+    return shape
+
+
+def placeholder(slide, label, x, y, w, h, border, fill="#FFFFFF"):
+    card = box(slide, x, y, w, h, fill, radius=True)
+    card.line.color.rgb = rgb(border)
+    card.line.width = Pt(1.2)
+    text(slide, label, x + 0.18, y + h / 2 - 0.18, w - 0.36, 0.36, 12, border, True, align=PP_ALIGN.CENTER)
+    return card
+
+
+def header(slide, title, eyebrow, palette, page_no, light=False):
+    primary, secondary, accent, background, text_color = palette
+    color = "#FFFFFF" if light else text_color
+    text(slide, eyebrow.upper(), 0.65, 0.38, 3.8, 0.28, 9, secondary if not light else "#D7E8E5", True)
+    text(slide, title, 0.65, 0.68, 8.7, 0.55, 25, color, True)
+    text(slide, f"{page_no:02d}", 12.05, 0.46, 0.55, 0.35, 11, accent if not light else "#F7CE77", True, align=PP_ALIGN.RIGHT)
+    line(slide, 0.65, 1.35, 12.65, 1.35, secondary if not light else "#FFFFFF", 1, 0.45)
+
+
+def add_notes(slide, primary="#3478C8"):
+    text(slide, "可替换标题", 0.7, 6.9, 1.3, 0.2, 8, primary, True)
+    text(slide, "可替换正文 / 图片 / 互动题", 2.1, 6.9, 3.3, 0.2, 8, "#8A96A3")
+
+
+def fairytale(slide, page, palette):
+    primary, secondary, accent, bg, dark = palette
+    box(slide, 0, 0, W, H, bg)
+    circle(slide, 10.9, -0.55, 3.0, "#F9D86D", 0.18)
+    for x, y, d, c in [(0.0, 5.95, 2.2, "#A8D7A5"), (1.7, 6.2, 2.7, "#77BDA7"), (9.8, 6.05, 3.2, "#A8D7A5"), (11.3, 5.8, 2.4, "#77BDA7")]:
+        circle(slide, x, y, d, c, 0.1)
+    if page == 1:
+        text(slide, "音乐小森林", 0.72, 1.05, 6.2, 0.7, 34, dark, True)
+        text(slide, "用耳朵发现声音,用身体回应节奏", 0.75, 1.83, 5.6, 0.4, 17, primary)
+        box(slide, 0.75, 2.7, 5.2, 0.9, "#FFFFFF", True)
+        text(slide, "课题名称 / 班级 / 执教教师", 1.05, 2.95, 4.6, 0.36, 16, "#61736F", True, align=PP_ALIGN.CENTER)
+        circle(slide, 8.3, 1.35, 2.1, "#F07F6A")
+        circle(slide, 9.55, 2.45, 1.6, "#F7C96D")
+        circle(slide, 7.35, 3.5, 1.25, "#49A795")
+        text(slide, "♪", 8.88, 1.72, 0.7, 0.7, 42, "#FFFFFF", True, align=PP_ALIGN.CENTER)
+        text(slide, "♫", 9.96, 2.77, 0.7, 0.7, 31, "#FFFFFF", True, align=PP_ALIGN.CENTER)
+    elif page == 2:
+        header(slide, "今天我们要做什么?", "学习目标", palette, page)
+        for idx, value in enumerate(["听见声音的变化", "用动作表现节奏", "和同伴合作展示"]):
+            circle(slide, 1.15, 2.05 + idx * 1.2, 0.58, [primary, secondary, accent][idx])
+            text(slide, str(idx + 1), 1.15, 2.14 + idx * 1.2, 0.58, 0.28, 14, "#FFFFFF", True, align=PP_ALIGN.CENTER)
+            placeholder(slide, value, 1.95, 1.95 + idx * 1.2, 8.2, 0.72, "#BFD8D1", "#FFFFFF")
+        placeholder(slide, "主视觉 / 情境插画", 10.55, 1.95, 2.0, 3.95, "#6EBAA4", "#E7F4EE")
+    else:
+        header(slide, ["听一听,找一找", "动一动,拍一拍", "收获小星星", "课后继续玩音乐", "课堂回声"][page - 3], "课堂任务", palette, page)
+        placeholder(slide, "主视觉 / 图片", 0.72, 1.75, 5.25, 4.4, "#BFD8D1", "#FFFFFF")
+        placeholder(slide, "一句学生任务", 6.42, 1.75, 5.95, 0.75, "#BFD8D1", "#FFFFFF")
+        for idx in range(3):
+            placeholder(slide, f"步骤 {idx + 1} / 可替换短句", 6.42, 2.85 + idx * 0.95, 5.95, 0.66, "#BFD8D1", "#FFFFFF")
+        add_notes(slide, primary)
+
+
+def warm(slide, page, palette):
+    primary, secondary, accent, bg, dark = palette
+    box(slide, 0, 0, W, H, bg)
+    box(slide, 0, 0, W, 0.18, secondary)
+    circle(slide, 10.6, 0.8, 1.55, "#F8D5B1", 0.15)
+    circle(slide, 11.75, 1.72, 0.92, "#E9A7A7", 0.18)
+    if page == 1:
+        text(slide, "把音乐带进今天", 0.78, 1.0, 6.6, 0.72, 33, dark, True)
+        text(slide, "温暖、清晰、适合师生共同阅读的音乐课堂", 0.82, 1.83, 6.4, 0.38, 16, "#5D6E76")
+        placeholder(slide, "课题名称", 0.82, 2.73, 4.9, 0.8, "#8FBFB3", "#FFFFFF")
+        box(slide, 7.25, 1.22, 4.8, 4.25, "#FFFFFF", True)
+        text(slide, "♪", 8.25, 1.75, 2.6, 1.4, 66, primary, True, align=PP_ALIGN.CENTER)
+        text(slide, "用一段旋律,开启一节课", 7.8, 3.55, 3.7, 0.42, 15, "#5B6E75", True, align=PP_ALIGN.CENTER)
+    else:
+        titles = ["学习地图", "感受这段音乐", "发现音乐小秘密", "合作练习", "我的课堂收获", "课后聆听"]
+        header(slide, titles[page - 2], "温暖手绘", palette, page)
+        if page in {2, 6}:
+            for idx in range(3):
+                placeholder(slide, f"{idx + 1}. 可替换学习要点", 0.9, 1.95 + idx * 1.05, 7.0, 0.68, "#9ECABD", "#FFFFFF")
+            placeholder(slide, "插画 / 图片", 8.45, 1.95, 3.8, 3.75, "#9ECABD", "#ECF6F1")
+        elif page == 5:
+            for idx in range(4):
+                box(slide, 0.9 + idx * 2.9, 2.15, 2.35, 1.8, "#FFFFFF", True)
+                text(slide, f"第 {idx + 1} 步", 1.12 + idx * 2.9, 2.45, 1.9, 0.28, 12, secondary, True, align=PP_ALIGN.CENTER)
+                text(slide, "可替换任务", 1.12 + idx * 2.9, 2.95, 1.9, 0.35, 15, dark, True, align=PP_ALIGN.CENTER)
+        else:
+            placeholder(slide, "主视觉 / 听赏素材", 0.9, 1.9, 5.2, 3.95, "#9ECABD", "#FFFFFF")
+            placeholder(slide, "课堂问题", 6.65, 1.9, 5.55, 0.75, "#9ECABD", "#FFFFFF")
+            placeholder(slide, "学生观察 / 回答区", 6.65, 2.95, 5.55, 2.9, "#9ECABD", "#FFFFFF")
+        add_notes(slide, primary)
+
+
+def doodle(slide, page, palette):
+    primary, secondary, accent, bg, dark = palette
+    box(slide, 0, 0, W, H, "#17233D")
+    for x, y, symbol, size in [(0.7, 0.6, "♪", 22), (11.75, 0.62, "♫", 25), (11.65, 6.55, "♩", 19), (0.72, 6.5, "♬", 18)]:
+        text(slide, symbol, x, y, 0.6, 0.5, size, "#FFFFFF", True)
+    if page == 1:
+        text(slide, "音符涂鸦课堂", 0.85, 1.05, 6.4, 0.72, 34, "#FFFFFF", True)
+        text(slide, "让节奏、旋律和想法在黑板上跳起来", 0.88, 1.85, 6.5, 0.36, 16, "#D5E0F1")
+        box(slide, 0.88, 2.72, 5.0, 0.85, "#FFFFFF", True)
+        text(slide, "课题名称", 1.15, 2.95, 4.45, 0.36, 16, "#17233D", True, align=PP_ALIGN.CENTER)
+        circle(slide, 8.5, 1.55, 2.15, accent)
+        circle(slide, 9.85, 3.05, 1.35, secondary)
+        text(slide, "♪", 9.12, 2.0, 0.85, 0.55, 36, "#17233D", True, align=PP_ALIGN.CENTER)
+    else:
+        titles = ["节奏挑战", "旋律发现", "听辨 A / B", "小组创作", "快速检测", "一句话收获"]
+        header(slide, titles[page - 2], "music doodle", palette, page, light=True)
+        if page in {2, 4, 6}:
+            placeholder(slide, "可替换任务标题", 0.85, 1.85, 7.1, 0.75, "#93B0D8", "#FFFFFF")
+            for idx in range(3):
+                box(slide, 0.85, 2.95 + idx * 0.86, 0.55, 0.55, [accent, secondary, primary][idx], True)
+                text(slide, str(idx + 1), 0.85, 3.07 + idx * 0.86, 0.55, 0.22, 12, "#17233D", True, align=PP_ALIGN.CENTER)
+                placeholder(slide, "可替换步骤或选项", 1.62, 2.88 + idx * 0.86, 6.33, 0.68, "#93B0D8", "#FFFFFF")
+            placeholder(slide, "节奏谱 / 图片", 8.45, 1.85, 3.7, 3.7, "#93B0D8", "#FFFFFF")
+        else:
+            placeholder(slide, "主视觉", 0.85, 1.85, 5.15, 3.95, "#93B0D8", "#FFFFFF")
+            placeholder(slide, "观察问题", 6.5, 1.85, 5.65, 0.72, "#93B0D8", "#FFFFFF")
+            placeholder(slide, "学生回答 / 对比结果", 6.5, 2.9, 5.65, 2.9, "#93B0D8", "#FFFFFF")
+        add_notes(slide, "#FFFFFF")
+
+
+def ink(slide, page, palette):
+    primary, secondary, accent, bg, dark = palette
+    box(slide, 0, 0, W, H, "#F7F1E7")
+    for x, y, w, h, c, a in [(8.4, 5.8, 4.9, 1.2, "#36594B", 0.25), (9.7, 5.25, 3.7, 1.25, "#36594B", 0.38), (10.7, 4.85, 2.6, 1.1, "#9D2B22", 0.78)]:
+        box(slide, x, y, w, h, c, True, a)
+    circle(slide, 11.75, 0.48, 0.48, "#9D2B22")
+    if page == 1:
+        text(slide, "听见东方的留白", 0.82, 1.1, 6.6, 0.76, 34, "#332A24", True, font="Microsoft YaHei")
+        text(slide, "民乐、戏曲与传统文化主题的音乐课件", 0.85, 1.92, 6.2, 0.36, 16, "#746A5F")
+        line(slide, 0.85, 2.6, 4.9, 2.6, accent, 2)
+        placeholder(slide, "课题名称", 0.85, 3.0, 4.85, 0.78, "#B99058", "#FFFDF8")
+        text(slide, "印", 11.86, 0.58, 0.27, 0.22, 9, "#FFFFFF", True, align=PP_ALIGN.CENTER)
+    else:
+        titles = ["入境:一幅声音画卷", "初听:听见什么", "乐器与音色", "文化线索", "表现与分享", "余韵"]
+        header(slide, titles[page - 2], "东方音乐", palette, page)
+        if page in {2, 5}:
+            placeholder(slide, "意境主视觉 / 山水或乐器", 0.85, 1.85, 5.35, 3.95, "#B99058", "#FFFDF8")
+            placeholder(slide, "一条核心问题", 6.7, 1.85, 5.35, 0.75, "#B99058", "#FFFDF8")
+            for idx in range(3):
+                placeholder(slide, f"可替换文化线索 {idx + 1}", 6.7, 2.95 + idx * 0.9, 5.35, 0.62, "#B99058", "#FFFDF8")
+        else:
+            for idx in range(3):
+                box(slide, 1.05 + idx * 3.85, 2.05, 3.1, 2.65, "#FFFDF8", True)
+                text(slide, f"线索 {idx + 1}", 1.35 + idx * 3.85, 2.35, 2.5, 0.3, 12, primary, True, align=PP_ALIGN.CENTER)
+                text(slide, "可替换内容", 1.35 + idx * 3.85, 3.15, 2.5, 0.4, 16, dark, True, align=PP_ALIGN.CENTER)
+        add_notes(slide, primary)
+
+
+def stage(slide, page, palette):
+    primary, secondary, accent, bg, dark = palette
+    box(slide, 0, 0, W, H, "#1C2940")
+    circle(slide, 8.2, -1.05, 4.0, "#D8B45B", 0.7)
+    circle(slide, 10.6, -0.5, 3.3, "#6B7E9E", 0.72)
+    if page == 1:
+        text(slide, "舞台正在发生", 0.85, 1.08, 7.0, 0.75, 35, "#FFFFFF", True)
+        text(slide, "用现场感打开世界音乐与演出欣赏", 0.88, 1.95, 6.7, 0.36, 16, "#DDE6F4")
+        box(slide, 0.88, 2.85, 5.2, 0.82, "#FFFFFF", True)
+        text(slide, "课题名称", 1.15, 3.07, 4.65, 0.36, 16, dark, True, align=PP_ALIGN.CENTER)
+        line(slide, 0.88, 4.35, 5.05, 4.35, accent, 2)
+    else:
+        titles = ["演出现场:先看再听", "声音从哪里来", "音乐要素拆解", "文化背景", "编排一段小演出", "谢幕与回响"]
+        header(slide, titles[page - 2], "stage documentary", palette, page, light=True)
+        if page in {2, 3, 5}:
+            placeholder(slide, "横版演出照片 / 主视觉", 0.85, 1.85, 7.25, 3.85, "#C7D2E2", "#FFFFFF")
+            placeholder(slide, "作品信息 / 核心问题", 8.55, 1.85, 3.65, 0.78, "#C7D2E2", "#FFFFFF")
+            placeholder(slide, "听赏提示", 8.55, 2.98, 3.65, 1.25, "#C7D2E2", "#FFFFFF")
+            placeholder(slide, "感受或证据", 8.55, 4.55, 3.65, 1.15, "#C7D2E2", "#FFFFFF")
+        else:
+            for idx in range(3):
+                placeholder(slide, f"演出线索 {idx + 1}", 0.9 + idx * 3.9, 2.05, 3.2, 2.7, "#C7D2E2", "#FFFFFF")
+        add_notes(slide, "#FFFFFF")
+
+
+def geometry(slide, page, palette):
+    primary, secondary, accent, bg, dark = palette
+    box(slide, 0, 0, W, H, "#F8FBFA")
+    box(slide, 8.7, 0, 4.63, H, "#E7F3EF")
+    box(slide, 9.55, 1.05, 2.8, 1.2, secondary, True, 0.15)
+    box(slide, 10.85, 5.55, 1.8, 1.1, accent, True, 0.2)
+    if page == 1:
+        text(slide, "看见旋律的结构", 0.82, 1.1, 7.3, 0.74, 34, dark, True)
+        text(slide, "用清晰关系图、时间线与对比框架组织知识", 0.86, 1.93, 6.8, 0.36, 16, "#657871")
+        placeholder(slide, "课题名称", 0.86, 2.9, 4.85, 0.78, "#6FAE99", "#FFFFFF")
+        for idx, x in enumerate([8.95, 10.25, 11.55]):
+            circle(slide, x, 3.0 + idx * 0.58, 0.42, [primary, secondary, accent][idx])
+            line(slide, x + 0.42, 3.21 + idx * 0.58, x + 1.0, 3.21 + idx * 0.58, primary, 1.2, 0.35)
+    else:
+        titles = ["学习路线", "旋律时间线", "结构对比", "规律发现", "项目任务", "结论与迁移"]
+        header(slide, titles[page - 2], "modern geometry", palette, page)
+        if page in {2, 3, 4}:
+            placeholder(slide, "概念 / 主问题", 0.85, 1.78, 11.6, 0.72, "#7BB6A4", "#FFFFFF")
+            for idx in range(4):
+                x = 0.95 + idx * 3.0
+                circle(slide, x, 3.15, 0.58, [primary, secondary, accent, primary][idx])
+                text(slide, str(idx + 1), x, 3.3, 0.58, 0.18, 11, "#FFFFFF", True, align=PP_ALIGN.CENTER)
+                placeholder(slide, f"结构节点 {idx + 1}", x - 0.35, 4.0, 1.28, 0.9, "#7BB6A4", "#FFFFFF")
+                if idx < 3:
+                    line(slide, x + 0.58, 3.43, x + 2.75, 3.43, secondary, 1.5, 0.35)
+        else:
+            placeholder(slide, "左侧:素材 / 图表 / 旋律片段", 0.85, 1.85, 5.35, 3.85, "#7BB6A4", "#FFFFFF")
+            placeholder(slide, "右侧:推理步骤与结论", 6.7, 1.85, 5.55, 3.85, "#7BB6A4", "#FFFFFF")
+        add_notes(slide, primary)
+
+
+BUILDERS = {
+    "fairytale-forest": (fairytale, ("#3478C8", "#32A07C", "#F08A3C", "#F7FBF8", "#24495C")),
+    "warm-handdrawn": (warm, ("#347D73", "#8FC6B6", "#E98B73", "#F7FBFA", "#284C52")),
+    "music-doodle": (doodle, ("#5164A7", "#80B9D7", "#F6C945", "#17233D", "#17233D")),
+    "ink-wash": (ink, ("#9D2B22", "#36594B", "#C58A35", "#F7F1E7", "#332A24")),
+    "stage-documentary": (stage, ("#D8B45B", "#8AA1C2", "#E7C469", "#1C2940", "#1C2940")),
+    "modern-geometry": (geometry, ("#287B72", "#4FAD93", "#E07A35", "#F8FBFA", "#20342F")),
+}
+
+
+def build_template(template_id: str, builder, palette) -> None:
+    presentation = Presentation()
+    presentation.slide_width = Inches(W)
+    presentation.slide_height = Inches(H)
+    presentation.core_properties.title = f"{template_id} 音乐课件母版"
+    presentation.core_properties.author = "系统模板"
+    presentation.core_properties.subject = "可编辑音乐课件母版"
+    blank = presentation.slide_layouts[6]
+    for page in range(1, 8):
+        slide = presentation.slides.add_slide(blank)
+        builder(slide, page, palette)
+    presentation.save(OUT_DIR / f"{template_id}.pptx")
+
+
+def main() -> None:
+    OUT_DIR.mkdir(parents=True, exist_ok=True)
+    for template_id, (builder, palette) in BUILDERS.items():
+        build_template(template_id, builder, palette)
+        print(f"{template_id}.pptx")
+
+
+if __name__ == "__main__":
+    main()

+ 2 - 0
src/api/aiCourseware.ts

@@ -1,4 +1,5 @@
 import { httpAxios } from "@/api/ApiInstance"
+import type { AiCoursewareTemplateId } from "@/config/aiCoursewareTemplates"
 
 export interface AiCoursewareOutlineQuery {
   id: string
@@ -11,6 +12,7 @@ export interface AiCoursewareGenerateQuery {
   id: string
   fromType?: string
   outline: AiCoursewareOutline
+  templateId?: AiCoursewareTemplateId
   visualPrompt?: string
   preferences?: AiCoursewarePreferences
   regeneratePageNo?: number

+ 6 - 6
src/config/aiCoursewareTemplates.ts

@@ -97,7 +97,7 @@ export const AI_COURSEWARE_TEMPLATES: AiCoursewareTemplate[] = [
     scene: "低年级歌曲与律动",
     description: "绘本场景、故事导入和童趣互动",
     templateFile: "/ai-courseware-templates/fairytale-forest.pptx",
-    templateSlides: 4,
+    templateSlides: 7,
     baseStyle: "cartoon",
     layoutFamily: "story",
     typography: "圆润醒目的低龄课堂字体层级,标题短而大,正文不低于30px",
@@ -125,7 +125,7 @@ export const AI_COURSEWARE_TEMPLATES: AiCoursewareTemplate[] = [
     scene: "小学音乐活动",
     description: "轻柔插画、情境聆听和课堂练习",
     templateFile: "/ai-courseware-templates/warm-handdrawn.pptx",
-    templateSlides: 4,
+    templateSlides: 7,
     baseStyle: "clean",
     layoutFamily: "education",
     typography: "清晰现代的教学字体层级,标题明确,正文适合远距离投屏",
@@ -153,7 +153,7 @@ export const AI_COURSEWARE_TEMPLATES: AiCoursewareTemplate[] = [
     scene: "世界音乐与演出欣赏",
     description: "大图舞台、演出现场和音乐文化体验",
     templateFile: "/ai-courseware-templates/stage-documentary.pptx",
-    templateSlides: 4,
+    templateSlides: 7,
     baseStyle: "clean",
     layoutFamily: "academic",
     typography: "具有演出节目单感的标题、正文和注释三级层级",
@@ -180,7 +180,7 @@ export const AI_COURSEWARE_TEMPLATES: AiCoursewareTemplate[] = [
     scene: "传统文化主题",
     description: "民歌、戏曲、民族音乐和传统节日",
     templateFile: "/ai-courseware-templates/ink-wash.pptx",
-    templateSlides: 4,
+    templateSlides: 7,
     baseStyle: "clean",
     layoutFamily: "culture",
     typography: "现代中文标题与舒展正文层级",
@@ -207,7 +207,7 @@ export const AI_COURSEWARE_TEMPLATES: AiCoursewareTemplate[] = [
     scene: "项目学习与乐理分析",
     description: "结构讲解、旋律分析和知识项目展示",
     templateFile: "/ai-courseware-templates/modern-geometry.pptx",
-    templateSlides: 4,
+    templateSlides: 7,
     baseStyle: "clean",
     layoutFamily: "technology",
     typography: "高对比现代标题与简洁正文层级",
@@ -234,7 +234,7 @@ export const AI_COURSEWARE_TEMPLATES: AiCoursewareTemplate[] = [
     scene: "乐理、节奏与欣赏",
     description: "音符插画、节奏任务和轻松知识讲解",
     templateFile: "/ai-courseware-templates/music-doodle.pptx",
-    templateSlides: 4,
+    templateSlides: 7,
     baseStyle: "cartoon",
     layoutFamily: "activity",
     typography: "大字号动作指令与清晰编号层级",

+ 93 - 19
src/utils/aiPptTemplateEngine.ts

@@ -114,40 +114,43 @@ function templateDecorationElements(templateId: AiCoursewareTemplateId | undefin
   switch (templateId) {
     case "fairytale-forest":
       return [
-        shapeElement({ left: 1450, top: 36, width: 74, height: 74, fill: palette.warm, opacity: 0.5, radius: 37 }),
-        shapeElement({ left: 1518, top: 92, width: 42, height: 42, fill: palette.accent, opacity: 0.45, radius: 21 }),
-        shapeElement({ left: 26, top: 792, width: 58, height: 58, fill: palette.secondary, opacity: 0.26, radius: 29 })
+        shapeElement({ left: 1370, top: -90, width: 300, height: 300, fill: palette.warm, opacity: 0.42, radius: 150 }),
+        shapeElement({ left: -80, top: 760, width: 300, height: 220, fill: palette.secondary, opacity: 0.22, radius: 150 }),
+        shapeElement({ left: 1290, top: 770, width: 390, height: 180, fill: palette.secondary, opacity: 0.18, radius: 90 })
       ]
     case "warm-handdrawn":
       return [
-        shapeElement({ left: 0, top: 0, width: 1600, height: 12, fill: palette.secondary, opacity: 0.85, radius: 0 }),
-        shapeElement({ left: 1530, top: 742, width: 70, height: 158, fill: palette.soft, opacity: 0.8, radius: 0 })
+        shapeElement({ left: 0, top: 0, width: 1600, height: 16, fill: palette.secondary, opacity: 0.9, radius: 0 }),
+        shapeElement({ left: 1405, top: 730, width: 230, height: 230, fill: palette.soft, opacity: 0.82, radius: 115 }),
+        shapeElement({ left: 1465, top: 690, width: 90, height: 90, fill: palette.warm, opacity: 0.45, radius: 45 })
       ]
     case "stage-documentary":
       return [
-        shapeElement({ left: 0, top: 0, width: 1600, height: 18, fill: palette.primary, radius: 0 }),
-        shapeElement({ left: 1548, top: 38, width: 18, height: 132, fill: palette.accent, radius: 0 }),
-        shapeElement({ left: 1574, top: 38, width: 8, height: 84, fill: palette.secondary, radius: 0 })
+        shapeElement({ left: 0, top: 0, width: 1600, height: 96, fill: palette.dark, radius: 0 }),
+        shapeElement({ left: 0, top: 76, width: 1600, height: 20, fill: palette.primary, opacity: 0.84, radius: 0 }),
+        shapeElement({ left: 1450, top: 0, width: 150, height: 18, fill: palette.accent, radius: 0 }),
+        textElement({ left: 82, top: 29, width: 260, height: 30, content: "MUSIC ON STAGE", fontSize: 14, color: "#FFFFFF", bold: true })
       ]
     case "ink-wash":
       return [
-        shapeElement({ left: 1460, top: 42, width: 88, height: 88, fill: palette.primary, opacity: 0.14, radius: 44 }),
-        shapeElement({ left: 1499, top: 52, width: 10, height: 68, fill: palette.primary, opacity: 0.62, radius: 2 }),
-        shapeElement({ left: 1434, top: 148, width: 114, height: 4, fill: palette.accent, opacity: 0.55, radius: 2 })
+        shapeElement({ left: 1250, top: 690, width: 420, height: 250, fill: palette.secondary, opacity: 0.12, radius: 125 }),
+        shapeElement({ left: 1375, top: 755, width: 260, height: 160, fill: palette.primary, opacity: 0.13, radius: 80 }),
+        shapeElement({ left: 80, top: 116, width: 5, height: 680, fill: palette.primary, opacity: 0.45, radius: 2 }),
+        textElement({ left: 116, top: 79, width: 220, height: 30, content: "东方音乐", fontSize: 15, color: palette.primary, bold: true })
       ]
     case "modern-geometry":
       return [
-        shapeElement({ left: 80, top: 120, width: 1440, height: 2, fill: palette.secondary, opacity: 0.16, radius: 0 }),
-        shapeElement({ left: 80, top: 780, width: 1440, height: 2, fill: palette.primary, opacity: 0.16, radius: 0 }),
-        shapeElement({ left: 118, top: 80, width: 2, height: 740, fill: palette.secondary, opacity: 0.12, radius: 0 }),
-        shapeElement({ left: 1480, top: 80, width: 2, height: 740, fill: palette.primary, opacity: 0.12, radius: 0 }),
-        shapeElement({ left: 1468, top: 108, width: 26, height: 26, fill: palette.secondary, opacity: 0.75, radius: 4 })
+        shapeElement({ left: 1325, top: 0, width: 275, height: 900, fill: palette.soft, opacity: 0.78, radius: 0 }),
+        shapeElement({ left: 1415, top: 88, width: 145, height: 68, fill: palette.secondary, opacity: 0.24, radius: 8 }),
+        shapeElement({ left: 1470, top: 690, width: 110, height: 110, fill: palette.accent, opacity: 0.26, radius: 10 }),
+        shapeElement({ left: 80, top: 118, width: 1160, height: 2, fill: palette.primary, opacity: 0.18, radius: 0 })
       ]
     case "music-doodle":
       return [
-        shapeElement({ left: 1390, top: 30, width: 58, height: 18, fill: palette.accent, radius: 9 }),
-        shapeElement({ left: 1458, top: 30, width: 92, height: 18, fill: palette.secondary, radius: 9 }),
-        shapeElement({ left: 44, top: 816, width: 116, height: 20, fill: palette.warm, opacity: 0.8, radius: 10 })
+        shapeElement({ left: 0, top: 0, width: 1600, height: 84, fill: palette.dark, radius: 0 }),
+        shapeElement({ left: 0, top: 84, width: 1600, height: 8, fill: palette.accent, radius: 0 }),
+        textElement({ left: 76, top: 26, width: 100, height: 38, content: "♪", fontSize: 28, color: palette.accent, bold: true }),
+        textElement({ left: 1420, top: 24, width: 100, height: 38, content: "♫", fontSize: 28, color: palette.secondary, bold: true })
       ]
   }
 }
@@ -169,6 +172,9 @@ function entranceAnimations(elements: PPTElement[]): PPTAnimation[] {
 }
 
 function templateForPage(page: AiCoursewarePage, role: PageRole, templateId?: AiCoursewareTemplateId) {
+  if (role === "cover" && templateId) {
+    return (currentPage: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette) => templateCover(currentPage, outline, palette, templateId)
+  }
   const requestedLayout = cleanText(page.visualPlan?.layout || page.visual?.layout).toLowerCase()
   const layout = resolveTemplateLayout(requestedLayout, role, templateId)
   switch (layout) {
@@ -1298,6 +1304,74 @@ function addClassroomCue(elements: PPTElement[], page: AiCoursewarePage, palette
   void palette
 }
 
+function templateCover(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette, templateId: AiCoursewareTemplateId): PPTElement[] {
+  const title = chapterTitle(outline, page)
+  const subtitle = [outline.gradeName, conciseTextbook(outline.textbookName), outline.unitName].filter(Boolean).join(" · ")
+  const points = studentPoints(page, 2)
+  const elements: PPTElement[] = []
+  let titleColor = palette.dark
+  let subColor = palette.muted
+  let label = "音乐课堂"
+
+  switch (templateId) {
+    case "fairytale-forest":
+      label = "MUSIC FOREST"
+      elements.push(shapeElement({ left: 0, top: 0, width: 1600, height: 900, fill: "#F7FBF8", radius: 0 }))
+      elements.push(shapeElement({ left: 1150, top: -170, width: 470, height: 470, fill: "#F7D66D", opacity: 0.85, radius: 235 }))
+      elements.push(shapeElement({ left: -100, top: 710, width: 550, height: 290, fill: "#32A07C", opacity: 0.74, radius: 200 }))
+      elements.push(shapeElement({ left: 1140, top: 630, width: 550, height: 320, fill: "#3478C8", opacity: 0.16, radius: 180 }))
+      break
+    case "warm-handdrawn":
+      label = "MUSIC FOR TODAY"
+      elements.push(shapeElement({ left: 0, top: 0, width: 1600, height: 900, fill: "#F7FBFA", radius: 0 }))
+      elements.push(shapeElement({ left: 0, top: 0, width: 1600, height: 22, fill: "#8FC6B6", radius: 0 }))
+      elements.push(shapeElement({ left: 1060, top: 105, width: 430, height: 470, fill: "#FFFFFF", radius: 36, shadow: softShadow() }))
+      elements.push(shapeElement({ left: 1210, top: 38, width: 180, height: 180, fill: "#F2CC8F", opacity: 0.55, radius: 90 }))
+      break
+    case "music-doodle":
+      label = "MUSIC DOODLE"
+      titleColor = "#FFFFFF"
+      subColor = "#D5E0F1"
+      elements.push(shapeElement({ left: 0, top: 0, width: 1600, height: 900, fill: "#17233D", radius: 0 }))
+      elements.push(shapeElement({ left: 0, top: 0, width: 1600, height: 16, fill: "#F6C945", radius: 0 }))
+      elements.push(shapeElement({ left: 1040, top: 135, width: 290, height: 290, fill: "#F6C945", radius: 145 }))
+      elements.push(shapeElement({ left: 1260, top: 390, width: 180, height: 180, fill: "#80B9D7", radius: 90 }))
+      elements.push(textElement({ left: 1150, top: 205, width: 110, height: 90, content: "♪", fontSize: 76, color: "#17233D", bold: true }))
+      break
+    case "ink-wash":
+      label = "东方音乐"
+      elements.push(shapeElement({ left: 0, top: 0, width: 1600, height: 900, fill: "#F7F1E7", radius: 0 }))
+      elements.push(shapeElement({ left: 1110, top: 590, width: 580, height: 270, fill: "#36594B", opacity: 0.16, radius: 130 }))
+      elements.push(shapeElement({ left: 1250, top: 665, width: 380, height: 190, fill: "#9D2B22", opacity: 0.17, radius: 95 }))
+      elements.push(shapeElement({ left: 92, top: 142, width: 6, height: 560, fill: "#9D2B22", opacity: 0.75, radius: 2 }))
+      break
+    case "stage-documentary":
+      label = "MUSIC ON STAGE"
+      titleColor = "#FFFFFF"
+      subColor = "#DDE6F4"
+      elements.push(shapeElement({ left: 0, top: 0, width: 1600, height: 900, fill: "#1C2940", radius: 0 }))
+      elements.push(shapeElement({ left: 1040, top: -170, width: 610, height: 610, fill: "#D8B45B", opacity: 0.74, radius: 305 }))
+      elements.push(shapeElement({ left: 1180, top: 170, width: 430, height: 430, fill: "#8AA1C2", opacity: 0.42, radius: 215 }))
+      elements.push(shapeElement({ left: 0, top: 830, width: 1600, height: 70, fill: "#D8B45B", opacity: 0.72, radius: 0 }))
+      break
+    case "modern-geometry":
+      label = "MUSIC STRUCTURE"
+      elements.push(shapeElement({ left: 0, top: 0, width: 1600, height: 900, fill: "#F8FBFA", radius: 0 }))
+      elements.push(shapeElement({ left: 1120, top: 0, width: 480, height: 900, fill: "#E7F3EF", radius: 0 }))
+      elements.push(shapeElement({ left: 1220, top: 160, width: 270, height: 120, fill: "#4FAD93", opacity: 0.25, radius: 14 }))
+      elements.push(shapeElement({ left: 1345, top: 610, width: 180, height: 180, fill: "#E07A35", opacity: 0.3, radius: 16 }))
+      break
+  }
+
+  if (hasImage(page)) elements.push(imageElement(page, { left: 935, top: 175, width: 490, height: 470, radius: 28 }))
+  elements.push(textElement({ left: 122, top: 108, width: 360, height: 32, content: label, fontSize: 15, color: templateId === "stage-documentary" || templateId === "music-doodle" ? "#FFFFFF" : palette.primary, bold: true }))
+  elements.push(textElement({ left: 120, top: 195, width: 710, height: 170, content: title, fontSize: fitFont(title, 710, 170, 70, 48), color: titleColor, bold: true }))
+  if (subtitle) elements.push(textElement({ left: 124, top: 405, width: 650, height: 46, content: subtitle, fontSize: 24, color: subColor }))
+  elements.push(shapeElement({ left: 124, top: 492, width: 120, height: 10, fill: palette.accent, radius: 5 }))
+  points.forEach((point, index) => elements.push(textElement({ left: 124, top: 552 + index * 62, width: 650, height: 44, content: point, fontSize: fitFont(point, 650, 44, 29, 22), color: titleColor, bold: index === 0 })))
+  return elements
+}
+
 function resolveTemplateLayout(requestedLayout: string, role: PageRole, templateId?: AiCoursewareTemplateId) {
   // 乐谱、歌词、节奏和乐器页面具有内容专属性,不能被模板包的通用版式覆盖。
   const contentSpecificLayouts = new Set(["lyric_focus", "score_focus", "rhythm_grid", "instrument_focus", "homework_list"])

+ 3 - 0
src/views/Editor/EditorHeader/AiCoursewareDialog.vue

@@ -684,6 +684,7 @@ async function handleGenerate() {
     id: pptWork.id,
     fromType: queryParams.pptResourcesType || queryParams.fromType,
     outline: sourceOutline,
+    templateId: selectedTemplateId.value,
     visualPrompt: buildVisualPrompt(),
     preferences: { ...preferences }
   })
@@ -703,6 +704,7 @@ async function regenerateSamplePage() {
     id: pptWork.id,
     fromType: queryParams.pptResourcesType || queryParams.fromType,
     outline: outline.value,
+    templateId: selectedTemplateId.value,
     visualPrompt: `${buildVisualPrompt()}当前只调整第${samplePageNo.value}页样张。${feedback ? `用户反馈:${feedback}。` : "请更换视觉组织方式,保持教学目标不变。"}`,
     preferences: { ...preferences },
     regeneratePageNo: samplePageNo.value
@@ -859,6 +861,7 @@ async function regenerateDraftPage(index: number) {
     id: pptWork.id,
     fromType: queryParams.pptResourcesType || queryParams.fromType,
     outline: draftOutline.value,
+    templateId: selectedTemplateId.value,
     visualPrompt: buildVisualPrompt(),
     preferences: { ...preferences },
     regeneratePageNo: index + 1

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