yonge 4 days ago
parent
commit
36fc4740ae

+ 1 - 1
dist/assets/index-B1gUsGV_.js → dist/assets/index-BASCbLby.js

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

File diff suppressed because it is too large
+ 0 - 0
dist/assets/index-BR-AEsZy.css


File diff suppressed because it is too large
+ 0 - 0
dist/assets/index-BrKd9nTF.js


+ 1 - 1
dist/assets/index-BDP32FYt.js → dist/assets/index-C3f6UED5.js

@@ -1 +1 @@
-import{u as s,ah as r}from"./index-B1gUsGV_.js";import{a5 as c,cD as n,R as p,ae as t,af as _,al as u,ag as i,bH as l}from"./index-Dm5vyDrL.js";const m={class:"pptScreen"},d=c({__name:"pptScreen",setup(f){const a=n(),e=p(!0),o=s();return a.setScreenMode("pptScreen"),o.initPPTData().then(()=>{e.value=!1}),(S,k)=>(t(),_("div",m,[e.value?i("",!0):(t(),u(r,{key:0}))]))}}),P=l(d,[["__scopeId","data-v-678905e9"]]);export{P as default};
+import{u as s,ah as r}from"./index-BASCbLby.js";import{a5 as c,cD as n,R as p,ae as t,af as _,al as u,ag as i,bH as l}from"./index-DhtvsvPP.js";const m={class:"pptScreen"},d=c({__name:"pptScreen",setup(f){const a=n(),e=p(!0),o=s();return a.setScreenMode("pptScreen"),o.initPPTData().then(()=>{e.value=!1}),(S,k)=>(t(),_("div",m,[e.value?i("",!0):(t(),u(r,{key:0}))]))}}),P=l(d,[["__scopeId","data-v-678905e9"]]);export{P as default};

File diff suppressed because it is too large
+ 0 - 0
dist/assets/index-C8UX5GFB.css


File diff suppressed because it is too large
+ 0 - 0
dist/assets/index-CJd_4N3P.js


+ 1 - 1
dist/assets/index-DCu7Qthk.js → dist/assets/index-Co6LbKFR.js

@@ -1 +1 @@
-import{u as c,ah as l}from"./index-B1gUsGV_.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-Dm5vyDrL.js";import"./shapes-DpCC2j3E.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,ah as l}from"./index-BASCbLby.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-DhtvsvPP.js";import"./shapes-ek89awhm.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};

File diff suppressed because it is too large
+ 1 - 1
dist/assets/index-DhtvsvPP.js


+ 1 - 1
dist/assets/index-TsOUuXNc.js → dist/assets/index-NdlCmiuj.js

@@ -1 +1 @@
-import{_ as t}from"./404-R0y5dkvZ.js";import{E as c}from"./index-B9iNxUWT.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-Dm5vyDrL.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-CJd_4N3P.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-DhtvsvPP.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-BitbgHQf.js → dist/assets/index-w0k1vHRR.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-Dm5vyDrL.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-DhtvsvPP.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/shapes-DpCC2j3E.js → dist/assets/shapes-ek89awhm.js

@@ -1,4 +1,4 @@
-import{cG as k,bB as $1}from"./index-Dm5vyDrL.js";import{ai as Z,aj as H1,ak as U,al as n0,am as c0,an as i0,ao as j1,ap as K1,aq as Y1,ar as R}from"./index-B1gUsGV_.js";var z1={exports:{}};/*!
+import{cG as k,bB as $1}from"./index-DhtvsvPP.js";import{ai as Z,aj as H1,ak as U,al as n0,am as c0,an as i0,ao as j1,ap as K1,aq as Y1,ar as R}from"./index-BASCbLby.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-Dm5vyDrL.js"></script>
+    <script type="module" crossorigin src="./assets/index-DhtvsvPP.js"></script>
     <link rel="stylesheet" crossorigin href="./assets/index-AavUEaYy.css">
   </head>
   <body>

+ 14 - 1
scripts/aiPptSelfTest.mjs

@@ -20,6 +20,16 @@ try {
     visualPlan: { layout, visualDensity: "medium" },
     ...extra
   })
+  const elementText = element => {
+    const content = element.type === "text" ? element.content : element.type === "shape" && element.text ? element.text.content : ""
+    return String(content || "").replace(/<[^>]+>/g, "")
+  }
+  const missingStudentTexts = (outline, slides) =>
+    outline.pages.flatMap((item, index) => {
+      const expected = [item.studentContent?.headline, ...(item.studentContent?.points || item.bullets || [])].filter(Boolean)
+      const rendered = (slides[index]?.elements || []).map(elementText).join("\n")
+      return expected.filter(text => !rendered.includes(text)).map(text => ({ pageNo: item.pageNo, text }))
+    })
   const singingOutline = {
     title: "凤鸣山谷的故事",
     gradeName: "一年级",
@@ -29,7 +39,7 @@ try {
     courseType: "music",
     style: "cartoon",
     pages: [
-      page(1, "cover", "凤鸣山谷的故事", "cover_scene", ["聆听自然的声音", "发现音乐的强弱变化"], { image: { url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" } }),
+      page(1, "cover", "凤鸣山谷的故事", "cover_scene", ["聆听自然的声音", "发现音乐的强弱变化", "用声音和乐器讲故事"], { image: { url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" } }),
       page(2, "scene_intro", "山谷里传来了什么声音?", "story_stage", ["观察画面", "模仿你听到的声音"], { interaction: { type: "question", question: "你觉得是谁在歌唱?", options: ["小鸟", "溪水", "风声"] } }),
       page(3, "objectives", "今天我们要学会", "goal_cards", ["分辨声音的高低", "感受音乐的强弱", "合作完成声音表演"]),
       page(4, "work_context", "走进凤鸣山谷", "split_image", ["清晨的山谷十分安静", "鸟鸣由远处慢慢靠近", "溪水一直轻轻流淌"]),
@@ -136,6 +146,7 @@ try {
         .map((slide, index) => ({ pageNo: index + 1, empty: index > 0 && !String(slide.remark || "").trim() }))
         .filter(item => item.empty)
         .map(item => item.pageNo)
+      const missingVisibleContent = missingStudentTexts(outline, converted.slides)
       results.push({
         scenario: variant.id,
         expectedSlideCount: outline.pages.length,
@@ -153,6 +164,7 @@ try {
         expectedMediaPlaceholders: variant.expectedMediaPlaceholders,
         mediaPlaceholderPages,
         emptyRemarkPages,
+        missingVisibleContent,
         unknownBackgrounds,
         missingBackgroundFiles,
         errors: report.errors,
@@ -171,6 +183,7 @@ try {
     if (result.layoutMismatchCount) messages.push(`视觉布局匹配错误:${result.layoutMismatchCount}`)
     if (result.mediaPlaceholderPages.length !== result.expectedMediaPlaceholders) messages.push(`媒体占位页不符合预期:${result.mediaPlaceholderPages.join("、") || "无"}`)
     if (result.emptyRemarkPages.length) messages.push(`教师备注为空:${result.emptyRemarkPages.join("、")}`)
+    if (result.missingVisibleContent.length) messages.push(`学生内容未渲染:${result.missingVisibleContent.map(item => `P${item.pageNo} ${item.text}`).join("、")}`)
     if (result.unknownBackgrounds.length) messages.push(`未应用母版背景:${result.unknownBackgrounds.join(",")}`)
     if (result.missingBackgroundFiles.length) messages.push(`母版背景文件缺失:${result.missingBackgroundFiles.join(",")}`)
     if (result.errors.length) messages.push(...result.errors.map(item => `P${item.pageNo || "-"} ${item.code}: ${item.message}`))

+ 180 - 51
src/utils/aiPptTemplateEngine.ts

@@ -133,7 +133,8 @@ function fillTemplatePage(master: TemplateMaster, masterPage: TemplateMasterPage
   }
   const elements: PPTElement[] = []
   const title = masterPage.layout === "cover" ? chapterTitle(outline, page) : displayPageTitle(page, outline)
-  const points = studentPoints(page, visiblePointLimit(page))
+  // 学生可见内容必须与编辑区保持一致,不能因模板容量静默丢弃条目。
+  const points = studentPoints(page)
   const headline = activityTask(page) || studentHeadline(page)
 
   addSlotText(elements, slotOf(masterPage, "title"), title, masterPage.layout === "cover" ? master.coverTitleColor : master.titleColor, true)
@@ -185,7 +186,7 @@ function fillTemplatePage(master: TemplateMaster, masterPage: TemplateMasterPage
 
   if (masterPage.layout === "diagram") {
     if (role === "notation") addNotationVisual(elements, slotOf(masterPage, "image"), page, master, palette, false)
-    else if (role === "rhythm") addStepGrid(elements, slotOf(masterPage, "image"), activitySteps(page, 4), master, palette)
+    else if (role === "rhythm") addStepGrid(elements, slotOf(masterPage, "image"), activitySteps(page), master, palette)
     else addVisualOrResource(elements, slotOf(masterPage, "image"), page, master, palette, true)
     if (role !== "rhythm") addSlotList(elements, slotOf(masterPage, "body"), points, master, palette)
     addInlineMedia(elements, slotOf(masterPage, "media"), page, master, palette, intent.showMedia)
@@ -204,7 +205,7 @@ function fillTemplatePage(master: TemplateMaster, masterPage: TemplateMasterPage
 
   if (masterPage.layout === "activity") {
     addSlotText(elements, slotOf(masterPage, "headline"), headline || "一起完成音乐任务", palette.primary, true)
-    addStepGrid(elements, slotOf(masterPage, "steps"), activitySteps(page, 4), master, palette)
+    addStepGrid(elements, slotOf(masterPage, "steps"), activitySteps(page), master, palette)
     addInlineMedia(elements, slotOf(masterPage, "media"), page, master, palette, intent.showMedia)
     return elements
   }
@@ -309,33 +310,53 @@ function addSlotText(elements: PPTElement[], slot: TemplateSlot | undefined, con
   }))
 }
 
-function addSlotList(elements: PPTElement[], slot: TemplateSlot | undefined, sourceItems: string[], master: TemplateMaster, palette: Palette, cards = true, textColor = master.textColor) {
+function addSlotList(
+  elements: PPTElement[],
+  slot: TemplateSlot | undefined,
+  sourceItems: string[],
+  master: TemplateMaster,
+  palette: Palette,
+  cards = true,
+  textColor = master.textColor
+) {
   if (!slot) return
-  const items = fitItemsToSlot(uniqueVisibleTexts(sourceItems.map(cleanText).filter(Boolean)), slot.maxItems || 4)
+  const items = uniqueVisibleTexts(sourceItems.map(cleanText).filter(Boolean))
   if (!items.length) return
-  const gap = Math.min(16, Math.max(8, slot.height / Math.max(1, items.length * 6)))
+  const gap = collectionGap(slot.height, items.length, 16)
   const rowHeight = (slot.height - gap * (items.length - 1)) / items.length
   items.forEach((item, index) => {
     const top = slot.top + index * (rowHeight + gap)
+    const inset = cards ? Math.min(34, Math.max(12, rowHeight * 0.22)) : 0
+    const textTopInset = Math.min(6, Math.max(1, rowHeight * 0.08))
+    const textHeight = Math.max(12, rowHeight - textTopInset * 2)
+    const textWidth = Math.max(40, slot.width - (cards ? inset + 24 : 0))
+    const minimumFontSize = adaptiveMinimumFont(slot, textHeight)
     if (cards) {
       addCardFrame(elements, slot.left, top, slot.width, rowHeight, index, master, palette)
     }
-    elements.push(textElement({
-      left: slot.left + (cards ? 34 : 0),
-      top: top + 6,
-      width: slot.width - (cards ? 58 : 0),
-      height: Math.max(28, rowHeight - 12),
-      content: item,
-      fontSize: fitFont(item, slot.width - 58, Math.max(28, rowHeight - 12), slot.preferredFontSize || 30, slot.minimumFontSize || 22),
-      minimumFontSize: slot.minimumFontSize,
-      color: textColor,
-      bold: index === 0 && items.length === 1
-    }))
+    elements.push(
+      textElement({
+        left: slot.left + inset,
+        top: top + textTopInset,
+        width: textWidth,
+        height: textHeight,
+        content: item,
+        fontSize: fitFont(item, textWidth, textHeight, slot.preferredFontSize || 30, minimumFontSize),
+        minimumFontSize,
+        color: textColor,
+        bold: index === 0 && items.length === 1
+      })
+    )
   })
 }
 
-function fitItemsToSlot(items: string[], maxItems: number) {
-  return items.slice(0, maxItems)
+function collectionGap(height: number, count: number, preferred: number) {
+  if (count <= 1) return 0
+  return Math.max(1, Math.min(preferred, height / Math.max(8, count * 8)))
+}
+
+function adaptiveMinimumFont(slot: TemplateSlot, availableHeight: number) {
+  return Math.min(slot.minimumFontSize || 22, Math.max(12, Math.floor(availableHeight / 1.35)))
 }
 
 function addCardFrame(elements: PPTElement[], left: number, top: number, width: number, height: number, index: number, master: TemplateMaster, palette: Palette) {
@@ -373,30 +394,62 @@ function addCardFrame(elements: PPTElement[], left: number, top: number, width:
 
 function addCardGrid(elements: PPTElement[], slot: TemplateSlot | undefined, sourceItems: string[], master: TemplateMaster, palette: Palette) {
   if (!slot) return
-  const items = fitItemsToSlot(uniqueVisibleTexts(sourceItems.map(cleanText).filter(Boolean)), slot.maxItems || 4)
+  const items = uniqueVisibleTexts(sourceItems.map(cleanText).filter(Boolean))
   if (!items.length) return
   const columns = items.length === 1 ? 1 : Math.min(3, items.length)
   const rows = Math.ceil(items.length / columns)
-  const gap = 22
+  const gap = collectionGap(slot.height, rows, 22)
   const width = (slot.width - gap * (columns - 1)) / columns
   const height = (slot.height - gap * (rows - 1)) / rows
   items.forEach((item, index) => {
     const left = slot.left + (index % columns) * (width + gap)
     const top = slot.top + Math.floor(index / columns) * (height + gap)
+    const inset = Math.min(34, Math.max(10, Math.min(width, height) * 0.1))
+    const textWidth = Math.max(36, width - inset * 2)
+    const textHeight = Math.max(12, height - inset * 2)
+    const minimumFontSize = adaptiveMinimumFont(slot, textHeight)
     addCardFrame(elements, left, top, width, height, index, master, palette)
-    elements.push(textElement({ left: left + 34, top: top + 34, width: width - 68, height: height - 68, content: item, fontSize: slot.preferredFontSize || 32, minimumFontSize: slot.minimumFontSize, color: master.textColor, bold: true }))
+    elements.push(
+      textElement({
+        left: left + inset,
+        top: top + inset,
+        width: textWidth,
+        height: textHeight,
+        content: item,
+        fontSize: fitFont(item, textWidth, textHeight, slot.preferredFontSize || 32, minimumFontSize),
+        minimumFontSize,
+        color: master.textColor,
+        bold: true
+      })
+    )
   })
 }
 
 function addLyricLines(elements: PPTElement[], slot: TemplateSlot | undefined, sourceItems: string[], master: TemplateMaster, palette: Palette) {
   if (!slot) return
-  const items = fitItemsToSlot(uniqueVisibleTexts(sourceItems.map(cleanText).filter(Boolean)), slot.maxItems || 5)
-  const gap = 12
-  const height = Math.max(56, (slot.height - gap * Math.max(0, items.length - 1)) / Math.max(1, items.length))
+  const items = uniqueVisibleTexts(sourceItems.map(cleanText).filter(Boolean))
+  const gap = collectionGap(slot.height, items.length, 12)
+  const height = (slot.height - gap * Math.max(0, items.length - 1)) / Math.max(1, items.length)
   items.forEach((item, index) => {
     const top = slot.top + index * (height + gap)
-    if (index % 2 === 0) elements.push(shapeElement({ left: slot.left, top, width: slot.width, height, fill: palette.soft, opacity: 0.55, radius: 18 }))
-    elements.push(textElement({ left: slot.left + 45, top: top + 8, width: slot.width - 90, height: height - 16, content: `♪  ${item}`, fontSize: slot.preferredFontSize || 38, minimumFontSize: slot.minimumFontSize, color: master.textColor, bold: index === 0 }))
+    const inset = Math.min(8, Math.max(1, height * 0.08))
+    const textHeight = Math.max(12, height - inset * 2)
+    const minimumFontSize = adaptiveMinimumFont(slot, textHeight)
+    if (index % 2 === 0)
+      elements.push(shapeElement({ left: slot.left, top, width: slot.width, height, fill: palette.soft, opacity: 0.55, radius: 18 }))
+    elements.push(
+      textElement({
+        left: slot.left + 45,
+        top: top + inset,
+        width: slot.width - 90,
+        height: textHeight,
+        content: `♪  ${item}`,
+        fontSize: fitFont(item, slot.width - 90, textHeight, slot.preferredFontSize || 38, minimumFontSize),
+        minimumFontSize,
+        color: master.textColor,
+        bold: index === 0
+      })
+    )
   })
 }
 
@@ -423,28 +476,111 @@ function addNotationVisual(elements: PPTElement[], slot: TemplateSlot | undefine
 
 function addStepGrid(elements: PPTElement[], slot: TemplateSlot | undefined, sourceItems: string[], master: TemplateMaster, palette: Palette) {
   if (!slot) return
-  const items = fitItemsToSlot(uniqueVisibleTexts(sourceItems.map(cleanText).filter(Boolean)), slot.maxItems || 4)
+  const items = uniqueVisibleTexts(sourceItems.map(cleanText).filter(Boolean))
   if (!items.length) return
-  const gap = 22
-  const width = (slot.width - gap * (items.length - 1)) / items.length
+  const columns = Math.min(4, items.length)
+  const rows = Math.ceil(items.length / columns)
+  const gap = collectionGap(slot.height, rows, 22)
+  const width = (slot.width - gap * (columns - 1)) / columns
+  const height = (slot.height - gap * (rows - 1)) / rows
   items.forEach((item, index) => {
-    addCardFrame(elements, slot.left + index * (width + gap), slot.top + 42, width, slot.height - 42, index, master, palette)
-    elements.push(shapeElement({ left: slot.left + index * (width + gap) + 28, top: slot.top, width: 72, height: 72, fill: index % 2 ? palette.secondary : palette.primary, radius: 36 }))
-    elements.push(textElement({ left: slot.left + index * (width + gap) + 49, top: slot.top + 13, width: 34, height: 42, content: String(index + 1), fontSize: 28, color: "#FFFFFF", bold: true }))
-    elements.push(textElement({ left: slot.left + index * (width + gap) + 30, top: slot.top + 112, width: width - 60, height: slot.height - 150, content: item, fontSize: 29, minimumFontSize: slot.minimumFontSize, color: master.textColor, bold: true }))
+    const left = slot.left + (index % columns) * (width + gap)
+    const top = slot.top + Math.floor(index / columns) * (height + gap)
+    const badgeSize = Math.min(72, Math.max(24, height * 0.2))
+    const cardTop = top + badgeSize * 0.55
+    const cardHeight = Math.max(12, height - badgeSize * 0.55)
+    const textInset = Math.min(30, Math.max(8, width * 0.08))
+    const textTop = cardTop + Math.min(70, Math.max(18, cardHeight * 0.22))
+    const textHeight = Math.max(12, top + height - textTop - 8)
+    const minimumFontSize = adaptiveMinimumFont(slot, textHeight)
+    addCardFrame(elements, left, cardTop, width, cardHeight, index, master, palette)
+    elements.push(
+      shapeElement({
+        left: left + textInset,
+        top,
+        width: badgeSize,
+        height: badgeSize,
+        fill: index % 2 ? palette.secondary : palette.primary,
+        radius: badgeSize / 2
+      })
+    )
+    elements.push(
+      textElement({
+        left: left + textInset,
+        top: top + badgeSize * 0.15,
+        width: badgeSize,
+        height: badgeSize * 0.62,
+        content: String(index + 1),
+        fontSize: Math.min(28, Math.max(14, badgeSize * 0.4)),
+        minimumFontSize: 12,
+        color: "#FFFFFF",
+        bold: true
+      })
+    )
+    elements.push(
+      textElement({
+        left: left + textInset,
+        top: textTop,
+        width: Math.max(36, width - textInset * 2),
+        height: textHeight,
+        content: item,
+        fontSize: fitFont(item, width - textInset * 2, textHeight, 29, minimumFontSize),
+        minimumFontSize,
+        color: master.textColor,
+        bold: true
+      })
+    )
   })
 }
 
 function addChecklist(elements: PPTElement[], slot: TemplateSlot | undefined, sourceItems: string[], master: TemplateMaster, palette: Palette) {
   if (!slot) return
-  const items = fitItemsToSlot(uniqueVisibleTexts(sourceItems.map(cleanText).filter(Boolean)), slot.maxItems || 5)
-  const gap = 16
+  const items = uniqueVisibleTexts(sourceItems.map(cleanText).filter(Boolean))
+  const gap = collectionGap(slot.height, items.length, 16)
   const height = (slot.height - gap * Math.max(0, items.length - 1)) / Math.max(1, items.length)
   items.forEach((item, index) => {
     const top = slot.top + index * (height + gap)
-    elements.push(shapeElement({ left: slot.left, top: top + 8, width: 42, height: 42, fill: index % 2 ? palette.secondary : palette.primary, radius: 21 }))
-    elements.push(textElement({ left: slot.left + 12, top: top + 10, width: 28, height: 34, content: "✓", fontSize: 24, color: "#FFFFFF", bold: true }))
-    elements.push(textElement({ left: slot.left + 72, top, width: slot.width - 72, height, content: item, fontSize: slot.preferredFontSize || 32, minimumFontSize: slot.minimumFontSize, color: master.textColor, bold: index === 0 }))
+    const iconSize = Math.min(42, Math.max(16, height * 0.58))
+    const iconTop = top + Math.max(0, (height - iconSize) / 2)
+    const textLeft = slot.left + iconSize + Math.min(30, Math.max(8, iconSize * 0.7))
+    const textWidth = Math.max(40, slot.left + slot.width - textLeft)
+    const minimumFontSize = adaptiveMinimumFont(slot, height)
+    elements.push(
+      shapeElement({
+        left: slot.left,
+        top: iconTop,
+        width: iconSize,
+        height: iconSize,
+        fill: index % 2 ? palette.secondary : palette.primary,
+        radius: iconSize / 2
+      })
+    )
+    elements.push(
+      textElement({
+        left: slot.left + iconSize * 0.2,
+        top: iconTop + iconSize * 0.12,
+        width: iconSize * 0.6,
+        height: iconSize * 0.7,
+        content: "✓",
+        fontSize: Math.min(24, Math.max(10, iconSize * 0.55)),
+        minimumFontSize: 10,
+        color: "#FFFFFF",
+        bold: true
+      })
+    )
+    elements.push(
+      textElement({
+        left: textLeft,
+        top,
+        width: textWidth,
+        height,
+        content: item,
+        fontSize: fitFont(item, textWidth, height, slot.preferredFontSize || 32, minimumFontSize),
+        minimumFontSize,
+        color: master.textColor,
+        bold: index === 0
+      })
+    )
   })
 }
 
@@ -612,13 +748,13 @@ function studentHeadline(page: AiCoursewarePage) {
   return sameVisibleText(headline, page.title) ? "" : headline
 }
 
-function studentPoints(page: AiCoursewarePage, limit: number) {
+function studentPoints(page: AiCoursewarePage) {
   const structured = (page.studentContent?.points || []).map(cleanText).filter(Boolean)
   const legacy = (page.bullets || []).map(cleanText).filter(Boolean)
   const points = structured.length ? structured : legacy
   const excluded = [page.title, studentHeadline(page)].map(normalizeVisibleText).filter(Boolean)
   const unique = uniqueVisibleTexts(points).filter(item => !excluded.includes(normalizeVisibleText(item)))
-  return (unique.length ? unique : [studentHeadline(page) || cleanText(page.title) || "课堂内容"]).slice(0, limit)
+  return unique.length ? unique : [studentHeadline(page) || cleanText(page.title) || "课堂内容"]
 }
 
 function activityTask(page: AiCoursewarePage) {
@@ -626,17 +762,10 @@ function activityTask(page: AiCoursewarePage) {
   return cleanText(page.classroomAction?.studentTask || studentHeadline(page) || "")
 }
 
-function visiblePointLimit(page: AiCoursewarePage) {
-  const density = page.visualPlan?.visualDensity
-  if (density === "low") return 3
-  if (density === "high") return 5
-  return 4
-}
-
-function activitySteps(page: AiCoursewarePage, limit: number) {
+function activitySteps(page: AiCoursewarePage) {
   const task = activityTask(page)
-  const points = studentPoints(page, limit).filter(item => !sameVisibleText(item, task))
-  return uniqueVisibleTexts(points).slice(0, limit)
+  const points = studentPoints(page).filter(item => !sameVisibleText(item, task))
+  return uniqueVisibleTexts(points)
 }
 
 function slideRemark(page: AiCoursewarePage) {

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

@@ -248,6 +248,9 @@
                     placeholder="每行一条学生可见要点,避免写老师操作步骤"
                     @input="event => updateStudentPoints(selectedPage!, event)"
                   ></textarea>
+                  <div v-if="studentContentReadabilityHint" class="content-readability-hint">
+                    {{ studentContentReadabilityHint }}
+                  </div>
                 </div>
               </div>
             </section>
@@ -468,6 +471,10 @@ const studentPoints = computed(() => {
   if (!selectedPage.value) return []
   return selectedPage.value.studentContent?.points?.length ? selectedPage.value.studentContent.points : selectedPage.value.bullets || []
 })
+const studentContentReadabilityHint = computed(() => {
+  const contentCount = studentPoints.value.length + (studentHeadline.value ? 1 : 0)
+  return contentCount > 6 ? `当前有${contentCount}条学生内容,预览会完整显示,但投屏字号可能变小,建议适当精简。` : ""
+})
 const samplePageIndex = computed(() => {
   const pages = outline.value?.pages || []
   const selectedIndex = pages.findIndex(page => page.pageNo === selectedPageNo.value)
@@ -1766,6 +1773,12 @@ function shortProgressLabel(message?: string) {
   min-height: 124px;
   height: 124px;
 }
+.content-readability-hint {
+  margin-top: 6px;
+  color: #b76e00;
+  font-size: 11px;
+  line-height: 1.45;
+}
 .representative-samples {
   display: grid;
   grid-template-columns: repeat(3, minmax(0, 1fr));

Some files were not shown because too many files changed in this diff