yonge 21 ساعت پیش
والد
کامیت
bded9ea44e

+ 1 - 1
dist/assets/index-C2ik0Ucq.js → dist/assets/index-B1e9HvGm.js

@@ -1 +1 @@
-import{_ as a}from"./404-R0y5dkvZ.js";import{a5 as o,eo as t,ae as r,af as c,bP as _,bQ as d,ak as s,bH as p}from"./index-DCWQzVyk.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,eo as t,ae as r,af as c,bP as _,bQ as d,ak as s,bH as p}from"./index-BwKz9wtC.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};

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
dist/assets/index-B6IOms75.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 1
dist/assets/index-BwKz9wtC.js


+ 1 - 1
dist/assets/index-C6td7eq8.js → dist/assets/index-DJr8z2rx.js

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

+ 1 - 1
dist/assets/index-CCiYMC4d.js → dist/assets/index-DW8ZTFID.js

@@ -1 +1 @@
-import{u as s,ag as r}from"./index-C6td7eq8.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-DCWQzVyk.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}))]))}}),g=l(d,[["__scopeId","data-v-678905e9"]]);export{g as default};
+import{u as s,ag as r}from"./index-DJr8z2rx.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-BwKz9wtC.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}))]))}}),g=l(d,[["__scopeId","data-v-678905e9"]]);export{g as default};

+ 1 - 1
dist/assets/index-DZgPMWmg.js → dist/assets/index-DYPecWb2.js

@@ -1 +1 @@
-import{_ as t}from"./404-R0y5dkvZ.js";import{E as c}from"./index-DhcPcWc_.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,bP as m,bQ as f,bH as u}from"./index-DCWQzVyk.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-B6IOms75.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,bP as m,bQ as f,bH as u}from"./index-BwKz9wtC.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};

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
dist/assets/index-DsgSV8Cv.js


+ 1 - 1
dist/assets/index-Bup-yDxT.js → dist/assets/index-YIl3r7-1.js

@@ -1 +1 @@
-import{u as c,ag as l}from"./index-C6td7eq8.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-DCWQzVyk.js";import"./shapes-B0c-921d.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,ag as l}from"./index-DJr8z2rx.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-BwKz9wtC.js";import"./shapes-hf4ERphj.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-B0c-921d.js → dist/assets/shapes-hf4ERphj.js

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

+ 201 - 0
dist/licenses/iconpark.txt

@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright 2019-present Bytedance Inc.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 201 - 0
public/licenses/iconpark.txt

@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright 2019-present Bytedance Inc.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 6 - 4
scripts/aiPptDialogTest.html

@@ -17,6 +17,8 @@ import { draftKey, loadAiDraft, saveAiDraft } from "/src/utils/aiCoursewareDraft
 import useAiCoursewarePageRepair from "/src/hooks/useAiCoursewarePageRepair.ts"
 import { aiCoursewareToSlides } from "/src/utils/aiCoursewareToSlides.ts"
 import { liveRegressionFixture } from './fixtures/aiPptLiveRegression.mjs'
+import { storyVisualFixture } from './fixtures/aiPptStoryVisual.mjs'
+import { nativeVisualFixture } from './fixtures/aiPptNativeVisual.mjs'
 import "/src/assets/styles/global.scss"
 import "/src/assets/styles/elementTheme.scss"
 import "/src/assets/styles/prosemirror.scss"
@@ -142,15 +144,15 @@ const app=createApp({setup(){
   const slidesStore=useSlidesStore()
   const pptWork=usePptWork()
   const repair = useAiCoursewarePageRepair()
-  if (scenario === 'editor-live' || scenario === 'screen-live') {
-    const result = aiCoursewareToSlides(liveRegressionFixture(), { templateId: 'blank' })
+  if (['editor-live', 'screen-live', 'editor-story', 'screen-story', 'editor-native', 'screen-native'].includes(scenario)) {
+    const result = aiCoursewareToSlides(scenario.endsWith('native') ? nativeVisualFixture() : scenario.endsWith('story') ? storyVisualFixture() : liveRegressionFixture(), { templateId: 'blank' })
     slidesStore.setViewportSize(1920)
     slidesStore.setViewportRatio(.5625)
     slidesStore.setTheme({ ...slidesStore.theme, ...result.theme })
     slidesStore.setSlides(result.slides)
-    slidesStore.updateSlideIndex(6)
+    slidesStore.updateSlideIndex(scenario.endsWith('live') ? 6 : 1)
   }
-  if (scenario === 'screen-live') return () => h('div', { style: 'height:100vh;width:100vw' }, [h(Screen)])
+  if (['screen-live', 'screen-story', 'screen-native'].includes(scenario)) return () => h('div', { style: 'height:100vh;width:100vw' }, [h(Screen)])
   if (scenario.startsWith('whole-repair')) {
     const fixture = structuredClone(outline)
     fixture.templateId = 'blank'

+ 20 - 1
scripts/aiPptSelfTest.mjs

@@ -9,6 +9,8 @@ import { inquiryFixture } from './fixtures/aiPptInquiry.mjs'
 import { lessonRepairFixture } from './fixtures/aiPptLessonRepair.mjs'
 import { contentGateFixture } from './fixtures/aiPptContentGate.mjs'
 import { liveRegressionFixture } from './fixtures/aiPptLiveRegression.mjs'
+import { storyVisualFixture } from './fixtures/aiPptStoryVisual.mjs'
+import { nativeVisualFixture } from './fixtures/aiPptNativeVisual.mjs'
 
 const vite = await createServer({ server: { middlewareMode: true }, appType: "custom", logLevel: "error" })
 
@@ -61,7 +63,24 @@ try {
     lessonMinutes: 40, minPages: 9, maxPages: 15, resourceMode: 'teacher_supplement', requireAudio: true, requireVideo: true
   })
   assert.deepEqual(inquiryReport.errors, [], `无媒体教学样例:${JSON.stringify(inquiryReport.errors)}`)
-  assert(inquiryResult.slides.every(slide => slide.background.color === '#FAF8F2'), '默认模板必须采用整课主题色')
+  assert(inquiryResult.slides.some(slide => slide.background.color.toUpperCase() === '#FAF8F2'), '保留整课纸底主题色')
+  assert(new Set(inquiryResult.slides.map(slide => slide.background.color)).size >= 3, '封面、情境和原生图示使用有层次的主题底色')
+  const story = storyVisualFixture(), storySlides = aiCoursewareToSlides(story).slides
+  assert.deepEqual(inspectAiPptQuality(story, storySlides).errors.filter(issue => layoutCodes.includes(issue.code)), [], '角色配图、步骤与完整范例不得重叠或缺字')
+  const roleImage = storySlides[1].elements.find(e => e.type === 'image')
+  assert(roleImage && roleImage.width * roleImage.height / (1920 * 1080) >= .15, '角色活动图必须具有可观察面积')
+  assert(Math.abs(roleImage.width / roleImage.height - 1264 / 704) < .001, '真实横图不能被压缩变形')
+  assert(liveText(storySlides[1]).includes(story.pages[1].studentContent.example), '学生范例完整保留在页面')
+  const native = nativeVisualFixture(), nativeSlides = aiCoursewareToSlides(native).slides
+  assert.deepEqual(inspectAiPptQuality(native, nativeSlides).errors.filter(issue => layoutCodes.includes(issue.code)), [], '无图课件的色块、任务图标和文字应完整且不重叠')
+  assert(nativeSlides.every(slide => slide.elements.every(e => e.type !== 'image')), '原生视觉不应新增图片资源')
+  const taskIcons = slide => slide.elements.filter(e => e.name?.startsWith('教学图标:'))
+  assert.deepEqual(taskIcons(nativeSlides[1]).map(e => e.name), ['教学图标:聆听', '教学图标:观察', '教学图标:交流'])
+  assert.equal(taskIcons(nativeSlides[2]).length, 3)
+  assert.equal(taskIcons(nativeSlides[3]).length, 0, '节奏图示不混入任务装饰图标')
+  assert(taskIcons(nativeSlides[1]).every(e => e.type === 'shape' && e.fixedRatio && /^[MLCZ0-9. ,]+$/.test(e.path)), '图标应是可编辑且可导出的矢量路径')
+  const { teachingActionIcon } = await vite.ssrLoadModule('/src/config/aiPptTeachingIcons.ts')
+  assert.equal(teachingActionIcon('听起来很轻巧'), undefined, '音色描述不能被误判为聆听任务')
   assert(inquiryResult.slides.every(slide => !slide.elements.some(element => element.type === 'elf')), '媒体由教师补充时不生成空播放器或谱例')
   assert(inquiryResult.slides[3].elements.filter(element => element.type === 'shape').length >= 4, '强弱图须绘制为可编辑圆形')
   assert(inquiryResult.slides[5].remark.includes('教学示范:') && inquiryResult.slides[5].remark.includes('判断标准:'), '示范与反馈必须进入备注')

+ 39 - 0
scripts/fixtures/aiPptNativeVisual.mjs

@@ -0,0 +1,39 @@
+import { inquiryFixture } from './aiPptInquiry.mjs'
+
+// 无外部图片的课堂设计回归:色块、任务图标、原生节奏图和比较排版。
+export function nativeVisualFixture() {
+  const outline = inquiryFixture()
+  outline.title = outline.chapterName = '让声音有表情'
+  outline.visualBible.palette = ['#243C5A', '#F8F6F0', '#CB652F']
+  const cover = structuredClone(outline.pages[0])
+  cover.title = outline.title
+  cover.studentContent = { headline: '听见差别,唱出变化', points: [] }
+  cover.classroomAction.studentTask = ''
+  const listen = structuredClone(outline.pages[1])
+  listen.title = '先听见,再说出'
+  listen.studentContent = { headline: '同一句歌,可以有什么不同?',
+    points: ['聆听两次演唱,留意力度', '观察同伴怎样表现声音', '说一说你听到的差别'] }
+  listen.classroomAction.studentTask = ''
+  listen.visualPlan.composition = 'concept_grid'
+  const steps = structuredClone(listen)
+  steps.type = 'performance_activity'
+  steps.title = '把发现变成自己的唱法'
+  steps.studentContent = { headline: '试着改变一种声音表现', points: [],
+    steps: ['聆听老师的示范', '讨论准备改变的地方', '创编自己的表现方式'] }
+  steps.visualPlan.composition = 'steps'
+  steps.visualPlan.textPresentation = 'steps'
+  const summary = structuredClone(outline.pages[8])
+  summary.studentContent = { headline: '我能说出自己的改变', points: ['声音哪里发生了变化?', '这样的变化带来了什么感受?'] }
+  summary.classroomAction.studentTask = ''
+  summary.visualPlan.composition = 'focus_question'
+  outline.pages = [cover, listen, steps, outline.pages[3], outline.pages[4], summary].map((page, i) => {
+    const native = structuredClone(page)
+    delete native.image
+    native.pageNo = i + 1
+    native.imageKeywords = []
+    native.requiredVisual = false
+    native.visualPlan = { ...native.visualPlan, visualMode: native.diagram ? 'native_diagram' : 'text_only', imagePurpose: 'none', imageNecessity: 'none' }
+    return native
+  })
+  return outline
+}

+ 29 - 0
scripts/fixtures/aiPptStoryVisual.mjs

@@ -0,0 +1,29 @@
+import { inquiryFixture } from './aiPptInquiry.mjs'
+
+// 按线上“角色对话”文字结构回归。图片只用于验证真实横图的排版,不代表重新生图结果。
+export function storyVisualFixture() {
+  const outline = inquiryFixture()
+  outline.title = outline.chapterName = '凤鸣山谷的故事'
+  outline.gradeName = '一年级'
+  outline.visualBible.palette = ['#233C48', '#FFF8EC', '#BC652F']
+  const image = { url: 'https://cdn.oss.dayaedu.com/pptList/2040740720920272898/ai-images/ai-ppt-2040740720920272898-8-9087586543834633432.png', width: 1264, height: 704 }
+  const role = structuredClone(outline.pages[5])
+  Object.assign(role, {
+    pageNo: 2, type: 'performance_activity', title: '角色对话', image,
+    studentContent: {
+      headline: '用不同声音,让伙伴们说说话',
+      points: ['小九:用歌声回应', '伶伦:用笛声回应', '麒麒:用脚步回应'],
+      steps: ['选好角色', '轮流回应', '合作表演'],
+      example: '小九先唱一句,麒麒用两下脚步回应,伶伦再接上笛声。'
+    },
+    classroomAction: { ...role.classroomAction, studentTask: '三人一组,按顺序表演' },
+    visualPlan: { composition: 'steps', visualMode: 'generated_image', visualShare: .55, textPresentation: 'steps', imageNecessity: 'supporting', imagePurpose: 'scene', layout: 'activity' }
+  })
+  const cover = structuredClone(outline.pages[0])
+  cover.title = outline.title
+  cover.studentContent = { headline: '听见声音,走进故事', points: [] }
+  cover.classroomAction.studentTask = ''
+  cover.image = { ...image, url: 'https://cdn.oss.dayaedu.com/pptList/2040740720920272898/ai-images/ai-ppt-2040740720920272898-1-8375623099960674268.png' }
+  outline.pages = [cover, role, outline.pages[3], outline.pages[4], outline.pages[8]].map((page, i) => ({ ...page, pageNo: i + 1 }))
+  return outline
+}

+ 20 - 0
src/config/aiPptTeachingIcons.ts

@@ -0,0 +1,20 @@
+// Outline paths adapted from the installed IconPark icons (Apache-2.0).
+// H/V commands are expanded to L for editable PPT shapes. License: /licenses/iconpark.txt.
+const icons = {
+  listen: { label: '聆听', path: 'M10 36L10 24C10 16.268 16.268 10 24 10C31.732 10 38 16.268 38 24L38 36M10 26L6 26C4.89543 26 4 26.8954 4 28L4 36C4 37.1046 4.89543 38 6 38L10 38L10 26ZM38 26L42 26C43.1046 26 44 26.8954 44 28L44 36C44 37.1046 43.1046 38 42 38L38 38L38 26ZM16 32L20 32L22 26L26 38L28 32L32 32' },
+  observe: { label: '观察', path: 'M24 36C35.0457 36 44 24 44 24C44 24 35.0457 12 24 12C12.9543 12 4 24 4 24C4 24 12.9543 36 24 36ZM24 29C26.7614 29 29 26.7614 29 24C29 21.2386 26.7614 19 24 19C21.2386 19 19 21.2386 19 24C19 26.7614 21.2386 29 24 29Z' },
+  discuss: { label: '交流', path: 'M33 38L22 38L22 30L36 30L36 22L44 22L44 38L39 38L36 41L33 38ZM4 6L36 6L36 30L17 30L13 34L9 30L4 30L4 6ZM12 22L18 22M12 14L24 14' },
+  create: { label: '创编', path: 'M7 42L43 42M11 26.7199L11 34L18.3172 34L39 13.3081L31.6951 6L11 26.7199Z' },
+  review: { label: '检查', path: 'M24 44C29.5228 44 34.5228 41.7614 38.1421 38.1421C41.7614 34.5228 44 29.5228 44 24C44 18.4772 41.7614 13.4772 38.1421 9.85786C34.5228 6.23858 29.5228 4 24 4C18.4772 4 13.4772 6.23858 9.85786 9.85786C6.23858 13.4772 4 18.4772 4 24C4 29.5228 6.23858 34.5228 9.85786 38.1421C13.4772 41.7614 18.4772 44 24 44ZM16 24L22 30L34 18' }
+}
+
+/** 只标识明确的学生动作;描述音色、角色或答案的句子不自动贴图标。 */
+export function teachingActionIcon(text: string) {
+  const action = text.replace(/^(?:先|再|请|一起|试着|仔细|认真)/, '')
+  if (/^(听一听|聆听|听辨|听音乐|听声音|听老师)/.test(action)) return icons.listen
+  if (/^(看一看|观察|找一找)/.test(action)) return icons.observe
+  if (/^(说一说|说说|讨论|交流|分享)/.test(action)) return icons.discuss
+  if (/^(编一编|创编|画一画|写一写)/.test(action)) return icons.create
+  if (/^(检查|核对|自评|互评)/.test(action)) return icons.review
+  return undefined
+}

+ 16 - 3
src/utils/aiPptPageDesign.ts

@@ -36,7 +36,16 @@ export function compilePageDesign(page: AiCoursewarePage, index: number, master:
   if (content.task) slots.push({ ...slot('instruction', 80, baseTop - 4, 1440, instructionHeight, 30), kind: 'headline' })
   const top = baseTop + (content.task ? instructionHeight + 14 : 0)
   const bottom = showMedia ? 730 : 830
-  const height = bottom - top
+  // 角色活动的范例横跨底部,避免与步骤一起挤进图片旁的窄栏。
+  const examples = hasVisual && content.steps.length && !content.groups.length
+    ? content.points.filter(text => text.startsWith('例子:')) : []
+  const exampleHeight = examples.length ? aiPptBlockHeight(examples, 1440, 'plain', 32) : 0
+  const separateExample = exampleHeight > 0 && exampleHeight <= 130 && bottom - top - exampleHeight - 24 >= 300
+  if (separateExample) {
+    slots.push({ ...slot('example', 80, bottom - exampleHeight, 1440, exampleHeight, 32), kind: 'body' })
+    content.points = content.points.filter(text => !examples.includes(text))
+  }
+  const height = bottom - top - (separateExample ? exampleHeight + 24 : 0)
   if (showMedia) slots.push(slot('media', 80, 758, 1440, 72, 30))
   const presentation = plan.textPresentation || 'plain'
   const bodyHeight = (width: number) => content.groups.length >= 2
@@ -46,13 +55,17 @@ export function compilePageDesign(page: AiCoursewarePage, index: number, master:
     : aiPptBlockHeight(content.steps, width, presentation)
       + aiPptBlockHeight(content.points, width, content.steps.length ? 'plain' : presentation)
       + (content.steps.length && content.points.length ? 24 : 0)
+  // 卡片/步骤框的装饰高度不应挤掉情境主图;正文可改用可读的紧凑列表。
+  const compactBodyHeight = (width: number) => content.groups.length ? bodyHeight(width)
+    : Math.min(bodyHeight(width), [...content.steps.map((text, i) => `${i + 1}. ${text}`), ...content.points]
+      .reduce((sum, text) => sum + measureAiPptText(text, width, 32), 0))
   let body = slot('body', 80, top, 1440, height)
   if (hasVisual) {
-    if (preferSide || ['image_left', 'image_right', 'concept_grid', 'focus_question'].includes(composition)) {
+    if (preferSide || content.steps.length > 0 || ['image_left', 'image_right', 'concept_grid', 'focus_question'].includes(composition)) {
       let imageWidth = (1440 - 36) * share
       if ([...content.points, ...content.steps].some(text => text.length > 50)) imageWidth = Math.min(imageWidth, (1440 - 36) * .35)
       // 文字较多时逐步让出横向空间,不把正文压成狭长小字。
-      while (imageWidth > (1440 - 36) * .35 && bodyHeight(1440 - imageWidth - 36) > height) imageWidth = Math.max((1440 - 36) * .35, imageWidth - 70)
+      while (imageWidth > (1440 - 36) * .35 && compactBodyHeight(1440 - imageWidth - 36) > height) imageWidth = Math.max((1440 - 36) * .35, imageWidth - 70)
       const bodyWidth = 1440 - imageWidth - 36
       const imageLeft = composition === 'image_right' ? 80 + bodyWidth + 36 : 80
       slots.push(slot('image', imageLeft, top, imageWidth, height))

+ 65 - 3
src/utils/aiPptTemplateEngine.ts

@@ -20,6 +20,7 @@ import type {
 import type { AiCoursewareOutline, AiCoursewarePage } from "@/api/aiCourseware"
 import { audioBindings, scoreBinding, isScorePage, usableResourceUrl, isTeacherFacingText, teacherFacingTexts, modelPageContent, rhythmRows } from "@/utils/aiPptContent"
 import { compilePageDesign, hasModelPageDesign, externalVisual } from "@/utils/aiPptPageDesign"
+import { teachingActionIcon } from '@/config/aiPptTeachingIcons'
 import {
   AI_COURSEWARE_TEMPLATES,
   getAiCoursewarePalette,
@@ -107,7 +108,17 @@ function buildSlide(page: AiCoursewarePage, outline: AiCoursewareOutline, palett
   const role = pageRole(page, index)
   const intent = pageRenderIntent(page, role)
   const originalMaster = getAiCoursewareTemplateMaster(options.templateId!)
-  const master = options.templateId === 'blank' && hasModelPageDesign(page) ? {
+  const designedTheme = hasModelPageDesign(page)
+  // 保持整课同一组颜色,以内容角色区分纸底、情境底和章节重点底。
+  // 使用真实背景填色,不把装饰图当作缺失的教学图片。
+  if (designedTheme) {
+    const emphasis = role === 'cover' || role === 'summary'
+    const tint = emphasis ? 20 : externalVisual(page) ? 10 : page.visualPlan?.visualMode === 'native_diagram' ? 5 : 0
+    const candidate = tinycolor.mix(palette.background, palette.secondary, tint).toHexString()
+    const background = tinycolor.readability(candidate, palette.text) >= 4.5 ? candidate : palette.background
+    palette = { ...palette, background }
+  }
+  const master = designedTheme ? {
     ...originalMaster, titleColor: palette.primary, coverTitleColor: palette.primary,
     textColor: palette.text, mutedColor: palette.muted,
     pages: originalMaster.pages.map(item => ({ ...item, background: palette.background }))
@@ -143,6 +154,8 @@ function buildSlide(page: AiCoursewarePage, outline: AiCoursewareOutline, palett
   assertContentContract(master, masterPage, contentElements)
   const elements = [
     ...renderTemplateMasterElements(masterPage.base),
+    ...(designed ? [lineElement(52, slotOf(masterPage, 'title')!.top + 8, 52,
+      slotOf(masterPage, 'title')!.top + Math.min(slotOf(masterPage, 'title')!.height - 8, 64), palette.secondary, 6)] : []),
     ...contentElements,
     ...renderTemplateMasterElements(masterPage.chrome)
   ]
@@ -151,7 +164,7 @@ function buildSlide(page: AiCoursewarePage, outline: AiCoursewareOutline, palett
     elements: scaleElementsToViewport(keepInCanvas(elements)),
     background: backgroundConfig(masterPage, options),
     remark: slideRemark(page),
-    animations: options.enableAnimations ? entranceAnimations(contentElements) : undefined,
+    animations: options.enableAnimations ? entranceAnimations(designed ? elements : contentElements) : undefined,
     turningMode: options.enableAnimations ? "fade" : "no"
   }
 }
@@ -180,7 +193,10 @@ export function getAiPptImageAspectRatios(templateId: AiCoursewareTemplateId): R
 function fillDesignedPage(master: TemplateMaster, layout: TemplateMasterPage, page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette, index: number, headline: string, intent: PageRenderIntent): PPTElement[] {
   const elements: PPTElement[] = []
   const content = modelPageContent(page)
-  const points = content.points
+  const exampleSlot = layout.slots.find(slot => slot.id === 'example')
+  const examples = exampleSlot ? content.points.filter(text => text.startsWith('例子:')) : []
+  const points = content.points.filter(text => !examples.includes(text))
+  if (exampleSlot) addMeasuredFlow(elements, exampleSlot, examples, master.textColor, false, 32)
   addSlotText(elements, slotOf(layout, 'title'), page.title, master.titleColor, true)
   if (index === 0) addSlotText(elements, slotOf(layout, 'meta'), [outline.gradeName, conciseTextbook(outline.textbookName), outline.unitName].filter(Boolean).join(' · '), master.mutedColor)
   addSlotText(elements, slotOf(layout, 'headline'), headline, master.textColor, true)
@@ -190,6 +206,8 @@ function fillDesignedPage(master: TemplateMaster, layout: TemplateMasterPage, pa
   const presentation = page.visualPlan!.textPresentation || 'plain'
   const paintText = (slot: TemplateSlot | undefined, values: string[], format = presentation) => {
     if (!slot || !values.length) return
+    if (!hasImage(page) && !page.diagram && !content.groups.length
+      && addDesignedPanels(elements, slot, values, format, master, palette)) return
     if (slot?.textFlow === 'columns' && values.length) {
       const width = (slot.width - 28 * (values.length - 1)) / values.length
       for (let index = 0; index < values.length; index++) addSlotText(elements, { ...slot, left: slot.left + index * (width + 28), width,
@@ -296,6 +314,50 @@ function fillDesignedPage(master: TemplateMaster, layout: TemplateMasterPage, pa
   return elements
 }
 
+/** 短任务用有语义的图标和色块形成阅读顺序,密集正文继续采用测量后的文字流。 */
+function addDesignedPanels(elements: PPTElement[], slot: TemplateSlot, values: string[], format: string, master: TemplateMaster, palette: Palette) {
+  if (values.length < 2 || values.length > 4 || slot.width < 1100 || slot.height < 240
+    || values.some(value => value.length > 36 || value.startsWith('例子:'))) return false
+  const icons = values.map(teachingActionIcon)
+  if (['plain', 'steps'].includes(format) && !icons.every(Boolean)) return false
+  const gap = 28, width = (slot.width - gap * (values.length - 1)) / values.length
+  const font = 36, inset = 28, iconSize = 64
+  const textWidth = width - inset * 2
+  const measure = (value: string) => measureAiPptText(value, textWidth, font, 1.2, true)
+  // 过长的任务优先在已有标点处分行,避免“力度”等词只剩一个字落到下一行。
+  const clauses = values.map(value => {
+    const split = value.match(/^(.{3,}[,;:])(.+)$/)
+    return split && measure(value) > measure('字') * 1.2 ? [split[1], split[2]] : [value]
+  })
+  const textHeight = Math.max(...clauses.map(parts => parts.reduce((sum, part) => sum + measure(part), 0)))
+  const hasIcons = icons.some(Boolean)
+  const textTop = hasIcons || format === 'steps' ? 124 : 48
+  const height = Math.max(hasIcons ? 260 : 150, textTop + textHeight + 30)
+  if (height > slot.height) return false
+  const top = slot.top + Math.min(50, (slot.height - height) / 2)
+  values.forEach((value, i) => {
+    const left = slot.left + i * (width + gap)
+    const tint = tinycolor.mix(palette.background, i % 2 ? palette.primary : palette.secondary, 12).toHexString()
+    const fill = tinycolor.readability(tint, master.textColor) >= 4.5 ? tint : palette.background
+    elements.push(shapeElement({ left, top, width, height, fill }))
+    elements.push(lineElement(left, top, left + width, top, i % 2 ? palette.primary : palette.secondary, 5))
+    const icon = icons[i]
+    if (icon) elements.push({ id: nanoid(10), name: `教学图标:${icon.label}`, type: 'shape',
+      left: left + inset, top: top + 28, width: iconSize, height: iconSize, rotate: 0,
+      viewBox: [48, 48], path: icon.path, fixedRatio: true, fill: 'none',
+      outline: { style: 'solid', color: master.textColor, width: 3 } })
+    if (format === 'steps') addSlotText(elements, { ...slot, left: left + width - 88, top: top + 28, width: 60, height: 60, preferredFontSize: 32 }, String(i + 1), master.titleColor, true)
+    addSlotText(elements, { ...slot, left: left + inset, top: top + textTop, width: textWidth,
+      height: textHeight, preferredFontSize: font, minimumFontSize: font }, value, master.textColor, true)
+    if (clauses[i].length === 2) {
+      const text = elements[elements.length - 1] as PPTTextElement
+      // 编辑器会把 br 归一化为空格,使用原生段落才能在编辑与导出时保留断行。
+      text.content = text.content.replace(escapeHtml(value), clauses[i].map(part => `<p style="margin:0">${escapeHtml(part)}</p>`).join(''))
+    }
+  })
+  return true
+}
+
 function flowHeight(values: string[], width: number, font: number, numbered = false) {
   return values.reduce((sum, text, i) => sum + measureAiPptText(numbered ? `${i + 1}. ${text}` : text, width, font), 0)
     + Math.max(0, values.length - 1) * 16

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است