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

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

+ 1 - 1
dist/assets/index-DPDnn2YN.js → dist/assets/index-BxpAusfI.js

@@ -1 +1 @@
-import{u as c,af as l}from"./index-B9Psu6rF.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-BwOBmnxZ.js";import"./shapes-oj-TN6vd.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-Buyn7FlE.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-C9woczL1.js";import"./shapes-73wbLX8W.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/index-C9woczL1.js


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


+ 1 - 1
dist/assets/index-BdcNZ2_e.js → dist/assets/index-Ca0rgJ1b.js

@@ -1 +1 @@
-import{_ as t}from"./404-R0y5dkvZ.js";import{E as c}from"./index-B_Q7eBRc.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-BwOBmnxZ.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-HnNlB0lC.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-C9woczL1.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-Ctm9Yv6G.css


+ 1 - 1
dist/assets/index-DSq_rUPp.js → dist/assets/index-DKlXn9Ut.js

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

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


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


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


+ 1 - 1
dist/assets/index-CqQxGCBv.js → dist/assets/index-XNucre4b.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-BwOBmnxZ.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-C9woczL1.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-oj-TN6vd.js → dist/assets/shapes-73wbLX8W.js

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

+ 1 - 0
src/utils/aiCoursewareToSlides.ts

@@ -5,6 +5,7 @@ import { buildAiPptSlides } from "@/utils/aiPptTemplateEngine"
 interface AiCoursewareVisualOptions {
   backgroundImage?: string
   enableAnimations?: boolean
+  paletteVariant?: number
 }
 
 export function aiCoursewareToSlides(

+ 55 - 2
src/utils/aiPptTemplateEngine.ts

@@ -20,6 +20,7 @@ import type { AiCoursewareOutline, AiCoursewarePage } from "@/api/aiCourseware"
 interface AiPptOptions {
   backgroundImage?: string
   enableAnimations?: boolean
+  paletteVariant?: number
 }
 
 type PageRole =
@@ -76,7 +77,7 @@ export function buildAiPptSlides(
   options: AiPptOptions = {}
 ): { title: string; theme: Partial<SlideTheme>; slides: Slide[] } {
   const normalized = normalizeOutline(outline)
-  const palette = buildPalette(normalized)
+  const palette = buildPalette(normalized, options.paletteVariant || 0)
   const slides = normalized.pages.map((page, index) => buildSlide(page, normalized, palette, options, index))
   runPreflight(slides)
   return {
@@ -1414,8 +1415,34 @@ function slideRemark(page: AiCoursewarePage) {
     .join("\n")
 }
 
-function buildPalette(outline: AiCoursewareOutline): Palette {
+function buildPalette(outline: AiCoursewareOutline, variant: number): Palette {
   const clean = outline.style === "clean" || /[初高]中|七年级|八年级|九年级/.test(outline.gradeName || "")
+  if (clean && variant % 3 === 1)
+    return {
+      primary: "#1E3A5F",
+      secondary: "#2F6F66",
+      accent: "#D97706",
+      warm: "#F4C95D",
+      background: "#F6F7F9",
+      surface: "#FFFFFF",
+      soft: "#E9EEF3",
+      text: "#18212B",
+      muted: "#667085",
+      dark: "#102A43"
+    }
+  if (clean && variant % 3 === 2)
+    return {
+      primary: "#4F46A5",
+      secondary: "#287A62",
+      accent: "#C2415B",
+      warm: "#E5C07B",
+      background: "#F7F7FB",
+      surface: "#FFFFFF",
+      soft: "#ECECF7",
+      text: "#202033",
+      muted: "#6B6B80",
+      dark: "#27244F"
+    }
   if (clean)
     return {
       primary: "#2563EB",
@@ -1429,6 +1456,32 @@ function buildPalette(outline: AiCoursewareOutline): Palette {
       muted: "#64748B",
       dark: "#132A3A"
     }
+  if (variant % 3 === 1)
+    return {
+      primary: "#3478C8",
+      secondary: "#32A07C",
+      accent: "#F08A3C",
+      warm: "#F7D66D",
+      background: "#F7FBF8",
+      surface: "#FFFFFF",
+      soft: "#E6F5EE",
+      text: "#24333B",
+      muted: "#69808A",
+      dark: "#24495C"
+    }
+  if (variant % 3 === 2)
+    return {
+      primary: "#7A5AC8",
+      secondary: "#20A39E",
+      accent: "#EE6C78",
+      warm: "#FFD166",
+      background: "#FBF8FF",
+      surface: "#FFFFFF",
+      soft: "#F0EAFB",
+      text: "#312D3D",
+      muted: "#756E84",
+      dark: "#3C3154"
+    }
   return {
     primary: "#E64A78",
     secondary: "#168F86",

+ 427 - 92
src/views/Editor/EditorHeader/AiCoursewareDialog.vue

@@ -14,6 +14,17 @@
         </div>
       </div>
 
+      <div class="workflow-steps">
+        <div
+          v-for="step in workflowSteps"
+          :key="step.value"
+          :class="['workflow-step', { active: workflowStep === step.value, done: workflowStep > step.value }]"
+        >
+          <span class="step-no">{{ workflowStep > step.value ? "✓" : step.value }}</span>
+          <span>{{ step.label }}</span>
+        </div>
+      </div>
+
       <div v-if="outlineLoading" class="loading-strip">
         <span class="loading-dot"></span>
         <span>正在生成大纲,请稍候</span>
@@ -27,75 +38,90 @@
         <div class="progress-value">{{ progressValue }}%</div>
       </div>
 
-      <div class="extra">
-        <div class="label">补充要求</div>
-        <TextArea v-model:value="extraPrompt" :rows="2" placeholder="可选,例如:多一些节奏互动,语言更适合小学低年级" :disabled="working" />
-      </div>
+      <div v-if="workflowStep === 1" class="setup-stage">
+        <div class="stage-intro">
+          <div>
+            <div class="stage-title">设置这份课件的生成方式</div>
+            <div class="stage-desc">先确定课时、视觉和素材偏好,下一步再确认每页内容与视觉样张。</div>
+          </div>
+          <span v-if="outline" class="saved-state">已有大纲,可继续确认</span>
+        </div>
 
-      <div class="style-selector">
-        <div class="label">视觉方向</div>
-        <div class="style-options">
-          <button
-            type="button"
-            :class="['style-option', { active: selectedStyle === 'cartoon' }]"
-            :disabled="working"
-            @click="selectedStyle = 'cartoon'"
-          >
-            <strong>卡通课堂</strong>
-            <span>适合小学和互动课堂</span>
-          </button>
-          <button type="button" :class="['style-option', { active: selectedStyle === 'clean' }]" :disabled="working" @click="selectedStyle = 'clean'">
-            <strong>简洁教学</strong>
-            <span>适合高年级和知识讲解</span>
-          </button>
+        <div class="extra">
+          <div class="label">补充要求</div>
+          <TextArea v-model:value="extraPrompt" :rows="2" placeholder="可选,例如:多一些节奏互动,语言更适合小学低年级" :disabled="working" />
         </div>
-      </div>
 
-      <div class="generation-preferences">
-        <div class="label">生成偏好</div>
-        <div class="preference-grid">
-          <label
-            ><span>课时</span
-            ><select v-model.number="preferences.lessonMinutes" :disabled="working">
-              <option :value="20">20分钟</option>
-              <option :value="30">30分钟</option>
-              <option :value="40">40分钟</option>
-              <option :value="45">45分钟</option>
-            </select></label
-          >
-          <label
-            ><span>页数</span
-            ><select v-model="pageRange" :disabled="working">
-              <option value="9-12">9~12页</option>
-              <option value="12-15">12~15页</option>
-              <option value="9-15">9~15页</option>
-            </select></label
-          >
-          <label
-            ><span>素材</span
-            ><select v-model="preferences.assetStyle" :disabled="working">
-              <option value="illustration">统一插画</option>
-              <option value="photo">真实照片</option>
-              <option value="mixed">插画与照片混合</option>
-            </select></label
-          >
-          <label
-            ><span>教材原图</span
-            ><select v-model="preferences.sourceImagePolicy" :disabled="working">
-              <option value="strict">优先保留</option>
-              <option value="balanced">适度使用</option>
-              <option value="creative">自由创作</option>
-            </select></label
-          >
+        <div class="style-selector">
+          <div class="label">视觉方向</div>
+          <div class="style-options">
+            <button
+              type="button"
+              :class="['style-option', { active: selectedStyle === 'cartoon' }]"
+              :disabled="working"
+              @click="selectedStyle = 'cartoon'"
+            >
+              <strong>卡通课堂</strong>
+              <span>适合小学和互动课堂</span>
+            </button>
+            <button
+              type="button"
+              :class="['style-option', { active: selectedStyle === 'clean' }]"
+              :disabled="working"
+              @click="selectedStyle = 'clean'"
+            >
+              <strong>简洁教学</strong>
+              <span>适合高年级和知识讲解</span>
+            </button>
+          </div>
         </div>
-        <div class="media-options">
-          <label><input v-model="preferences.requireAudio" type="checkbox" :disabled="working" />需要音频</label>
-          <label><input v-model="preferences.requireVideo" type="checkbox" :disabled="working" />需要视频</label>
-          <label><input v-model="preferences.enableAnimations" type="checkbox" :disabled="working" />启用克制动画</label>
+
+        <div class="generation-preferences">
+          <div class="label">生成偏好</div>
+          <div class="preference-grid">
+            <label
+              ><span>课时</span
+              ><select v-model.number="preferences.lessonMinutes" :disabled="working">
+                <option :value="20">20分钟</option>
+                <option :value="30">30分钟</option>
+                <option :value="40">40分钟</option>
+                <option :value="45">45分钟</option>
+              </select></label
+            >
+            <label
+              ><span>页数</span
+              ><select v-model="pageRange" :disabled="working">
+                <option value="9-12">9~12页</option>
+                <option value="12-15">12~15页</option>
+                <option value="9-15">9~15页</option>
+              </select></label
+            >
+            <label
+              ><span>素材</span
+              ><select v-model="preferences.assetStyle" :disabled="working">
+                <option value="illustration">统一插画</option>
+                <option value="photo">真实照片</option>
+                <option value="mixed">插画与照片混合</option>
+              </select></label
+            >
+            <label
+              ><span>教材原图</span
+              ><select v-model="preferences.sourceImagePolicy" :disabled="working">
+                <option value="strict">优先保留</option>
+                <option value="balanced">适度使用</option>
+                <option value="creative">自由创作</option>
+              </select></label
+            >
+          </div>
+          <div class="media-options">
+            <label><input v-model="preferences.requireAudio" type="checkbox" :disabled="working" />需要音频</label>
+            <label><input v-model="preferences.requireVideo" type="checkbox" :disabled="working" />需要视频</label>
+            <label><input v-model="preferences.enableAnimations" type="checkbox" :disabled="working" />启用克制动画</label>
+          </div>
         </div>
       </div>
 
-      <div v-if="draftConverted" class="result-preview">
+      <div v-if="workflowStep === 3 && draftConverted" class="result-preview">
         <div class="preview-head">
           <div>
             <div class="preview-title">全稿预览</div>
@@ -139,18 +165,53 @@
         </div>
       </div>
 
-      <div v-else-if="outline" class="outline">
+      <div v-else-if="workflowStep === 2 && outline" class="outline">
         <div v-if="sampleSlide" class="sample-review">
           <div class="sample-copy">
-            <div class="field-title">视觉样张</div>
+            <div class="sample-eyebrow">视觉样张 · 当前同步左侧页面</div>
             <div class="sample-title">第{{ samplePageNo }}页 · {{ samplePageTitle }}</div>
-            <div class="sample-hint">样张使用最终模板和当前视觉方向渲染。修改大纲或视觉方向后需要重新确认。</div>
-            <Button type="primary" @click="sampleApproved = !sampleApproved" :disabled="working">
-              {{ sampleApproved ? "已确认样张" : "确认这个视觉方向" }}
-            </Button>
+            <div class="sample-hint">{{ samplePageReason }}。你可以切换任意页面查看,也可以只重新生成当前页样张。</div>
+
+            <div class="sample-page-control">
+              <button type="button" :disabled="working || samplePageIndex <= 0" @click="previewPreviousPage">上一页</button>
+              <select v-model.number="selectedPageNo" :disabled="working">
+                <option v-for="page in outline.pages" :key="page.pageNo" :value="page.pageNo">第{{ page.pageNo }}页 · {{ page.title }}</option>
+              </select>
+              <button type="button" :disabled="working || samplePageIndex >= outline.pages.length - 1" @click="previewNextPage">下一页</button>
+            </div>
+
+            <div class="sample-feedback">
+              <div class="feedback-label">哪里不满意?选择后可重新生成当前样张</div>
+              <div class="feedback-chips">
+                <button
+                  v-for="item in sampleFeedbackOptions"
+                  :key="item"
+                  type="button"
+                  :class="{ active: sampleFeedbackTags.includes(item) }"
+                  :disabled="working"
+                  @click="toggleSampleFeedback(item)"
+                >
+                  {{ item }}
+                </button>
+              </div>
+              <textarea
+                v-model="sampleFeedbackText"
+                :disabled="working"
+                placeholder="还可以补充,例如:希望画面更童趣、减少卡片、突出山谷主视觉"
+                @input="sampleApproved = false"
+              ></textarea>
+            </div>
+
+            <div class="sample-actions">
+              <Button @click="regenerateSamplePage" :disabled="working">重新生成当前样张</Button>
+              <Button type="primary" @click="approveSample" :disabled="working">
+                {{ sampleApproved ? "视觉方向已确认" : "确认这个视觉方向" }}
+              </Button>
+            </div>
           </div>
           <div :class="['sample-canvas', { approved: sampleApproved }]">
             <ThumbnailSlide :slide="sampleSlide" :size="420" />
+            <div v-if="sampleApproved" class="sample-approved-mark">已确认</div>
           </div>
         </div>
         <div class="page-nav">
@@ -262,19 +323,24 @@
         </div>
       </div>
 
-      <div v-else class="empty">生成大纲后,可在这里确认课件结构、页面内容和设计要求。</div>
+      <div v-else-if="workflowStep === 1 && !outline" class="empty">完成生成设置后,系统会先生成大纲和可调整的视觉样张。</div>
 
       <div class="footer">
         <Button @click="emit('update:visible', false)" :disabled="working">取消</Button>
-        <template v-if="draftConverted">
+        <template v-if="workflowStep === 1">
+          <Button v-if="outline" @click="workflowStep = 2" :disabled="working">继续确认现有大纲</Button>
+          <Button type="primary" @click="handleOutline" :disabled="working">{{ outline ? "按新设置重新生成大纲" : "生成大纲并进入下一步" }}</Button>
+        </template>
+        <template v-else-if="workflowStep === 2">
+          <Button @click="workflowStep = 1" :disabled="working">返回生成设置</Button>
+          <Button @click="regenerateSamplePage" :disabled="working">重新生成当前样张</Button>
+          <Button type="primary" @click="handleGenerate" :disabled="!sampleApproved || working">确认视觉并生成完整课件</Button>
+        </template>
+        <template v-else>
           <Button @click="backToOutline" :disabled="working">返回修改</Button>
           <Button type="primary" @click="handleGenerate" :disabled="working">重新生成</Button>
           <Button type="primary" @click="applyDraft" :disabled="working || !draftQuality?.passed">应用到当前课件</Button>
         </template>
-        <template v-else>
-          <Button type="primary" @click="handleOutline" :disabled="working">{{ outline ? "重新生成大纲" : "生成大纲" }}</Button>
-          <Button type="primary" @click="handleGenerate" :disabled="!outline || !sampleApproved || working">确认并生成PPT</Button>
-        </template>
       </div>
     </div>
   </Modal>
@@ -322,7 +388,15 @@ const { addHistorySnapshot } = useHistorySnapshot()
 const extraPrompt = ref("")
 const outline = ref<AiCoursewareOutline>()
 const taskInfo = ref<AiCoursewareTaskInfo>()
+const workflowStep = ref<1 | 2 | 3>(1)
+const workflowSteps = [
+  { value: 1, label: "生成设置" },
+  { value: 2, label: "大纲与样张" },
+  { value: 3, label: "全稿预览" }
+] as const
+const taskMode = ref<"full" | "sample">("full")
 const selectedStyle = ref<"cartoon" | "clean">("cartoon")
+const paletteVariant = ref(0)
 const pageRange = ref("9-12")
 const preferences = reactive<AiCoursewarePreferences>({
   lessonMinutes: 40,
@@ -335,6 +409,9 @@ const preferences = reactive<AiCoursewarePreferences>({
   enableAnimations: true
 })
 const sampleApproved = ref(false)
+const sampleFeedbackOptions = ["配色不合适", "版式太单一", "图片不合适", "文字太多"]
+const sampleFeedbackTags = ref<string[]>([])
+const sampleFeedbackText = ref("")
 const draftOutline = ref<AiCoursewareOutline>()
 const draftConverted = ref<{ title: string; theme: Partial<SlideTheme>; slides: Slide[] }>()
 const draftQuality = ref<AiPptQualityReport>()
@@ -371,17 +448,25 @@ const studentPoints = computed(() => {
 const activityTask = computed(() => selectedPage.value?.classroomAction?.studentTask || selectedPage.value?.activity?.task || "")
 const samplePageIndex = computed(() => {
   const pages = outline.value?.pages || []
-  const preferred = pages.findIndex(
-    (page, index) => index > 0 && ["goal_cards", "activity_steps", "compare_listen", "summary_checklist"].includes(page.visualPlan?.layout || "")
-  )
-  return preferred >= 0 ? preferred : Math.min(1, Math.max(0, pages.length - 1))
+  const selectedIndex = pages.findIndex(page => page.pageNo === selectedPageNo.value)
+  return selectedIndex >= 0 ? selectedIndex : 0
 })
 const samplePageNo = computed(() => outline.value?.pages[samplePageIndex.value]?.pageNo || samplePageIndex.value + 1)
 const samplePageTitle = computed(() => outline.value?.pages[samplePageIndex.value]?.title || "代表页面")
+const samplePageReason = computed(() => {
+  const layout = outline.value?.pages[samplePageIndex.value]?.visualPlan?.layout || ""
+  if (["activity_steps", "action_demo", "group_mission", "performance_stage"].includes(layout)) return "这是课堂活动页,适合判断互动方式和信息层级"
+  if (["cover_scene", "story_stage", "full_image_question", "observe_question"].includes(layout))
+    return "这是主视觉页面,适合判断画风、配色和场景表现"
+  if (["summary_checklist", "visual_summary", "practice_check"].includes(layout)) return "这是总结检测页,适合判断文字密度和阅读效率"
+  return "这是你当前选中的页面,可用来判断版式和学生阅读体验"
+})
 const sampleSlide = computed(() => {
   if (!outline.value?.pages.length) return undefined
   const previewOutline = { ...outline.value, style: selectedStyle.value }
-  return aiCoursewareToSlides(previewOutline, { enableAnimations: preferences.enableAnimations }).slides[samplePageIndex.value]
+  return aiCoursewareToSlides(previewOutline, { enableAnimations: preferences.enableAnimations, paletteVariant: paletteVariant.value }).slides[
+    samplePageIndex.value
+  ]
 })
 
 const layoutOptions = [
@@ -411,6 +496,7 @@ const layoutOptions = [
 ]
 
 watch(selectedStyle, style => {
+  paletteVariant.value = 0
   if (outline.value) outline.value.style = style
   invalidateVisualApproval()
 })
@@ -457,6 +543,9 @@ async function handleOutline() {
     outline.value = res.data as AiCoursewareOutline
     outline.value.style = selectedStyle.value
     selectedPageNo.value = outline.value.pages[0]?.pageNo || 1
+    sampleFeedbackTags.value = []
+    sampleFeedbackText.value = ""
+    workflowStep.value = 2
   }
 }
 
@@ -475,6 +564,7 @@ async function handleGenerate() {
   draftConverted.value = undefined
   draftQuality.value = undefined
   sourceOutline.style = selectedStyle.value
+  taskMode.value = "full"
   const res = await httpAjaxErrMsg(aiCoursewareGenerateApi, {
     id: pptWork.id,
     fromType: queryParams.pptResourcesType || queryParams.fromType,
@@ -488,6 +578,25 @@ async function handleGenerate() {
   await pollTask(taskInfo.value.taskId)
 }
 
+async function regenerateSamplePage() {
+  if (!outline.value || !pptWork.id) return
+  sampleApproved.value = false
+  taskMode.value = "sample"
+  if (sampleFeedbackTags.value.includes("配色不合适")) paletteVariant.value = (paletteVariant.value + 1) % 3
+  const feedback = [...sampleFeedbackTags.value, sampleFeedbackText.value.trim()].filter(Boolean).join(";")
+  const res = await httpAjaxErrMsg(aiCoursewareGenerateApi, {
+    id: pptWork.id,
+    fromType: queryParams.pptResourcesType || queryParams.fromType,
+    outline: outline.value,
+    visualPrompt: `${buildVisualPrompt()}当前只调整第${samplePageNo.value}页样张。${feedback ? `用户反馈:${feedback}。` : "请更换视觉组织方式,保持教学目标不变。"}`,
+    preferences: { ...preferences },
+    regeneratePageNo: samplePageNo.value
+  })
+  if (res.code !== 200) return
+  taskInfo.value = res.data as AiCoursewareTaskInfo
+  await pollTask(taskInfo.value.taskId)
+}
+
 async function pollTask(taskId: string) {
   if (pollTimer.value) window.clearInterval(pollTimer.value)
   await fetchTask(taskId)
@@ -505,7 +614,8 @@ async function fetchTask(taskId: string) {
   taskInfo.value = res.data as AiCoursewareTaskInfo
   if (taskInfo.value.status === "DONE") {
     clearPollTimer()
-    await loadDraft(taskId)
+    if (taskMode.value === "sample") await loadSampleResult(taskId)
+    else await loadDraft(taskId)
   } else if (taskInfo.value.status === "FAILED") {
     clearPollTimer()
     ElMessage.error(taskInfo.value.message || "AI课件生成失败")
@@ -526,15 +636,36 @@ function clearPollTimer() {
 
 async function loadDraft(taskId: string) {
   const res = await httpAjaxErrMsg(aiCoursewareResultApi, taskId)
-  if (res.code !== 200) return
+  if (res.code !== 200) {
+    clearTask()
+    return
+  }
 
   const result = res.data as AiCoursewareGenerateResult
   draftOutline.value = result.outline
   rebuildDraftPreview()
+  workflowStep.value = 3
+  clearTask()
   if (draftQuality.value?.passed) ElMessage.success("课件草稿已生成,请预览后确认应用")
   else ElMessage.error(`课件草稿存在${draftQuality.value?.errors.length || 0}项阻止应用的问题`)
 }
 
+async function loadSampleResult(taskId: string) {
+  const res = await httpAjaxErrMsg(aiCoursewareResultApi, taskId)
+  if (res.code !== 200) {
+    clearTask()
+    return
+  }
+  const result = res.data as AiCoursewareGenerateResult
+  outline.value = result.outline
+  outline.value.style = selectedStyle.value
+  sampleFeedbackTags.value = []
+  sampleFeedbackText.value = ""
+  workflowStep.value = 2
+  clearTask()
+  ElMessage.success(`第${samplePageNo.value}页样张已重新生成,请再次确认视觉方向`)
+}
+
 function rebuildDraftPreview() {
   if (!draftOutline.value) {
     draftConverted.value = undefined
@@ -542,7 +673,10 @@ function rebuildDraftPreview() {
     return
   }
   draftOutline.value.style = selectedStyle.value
-  draftConverted.value = aiCoursewareToSlides(draftOutline.value, { enableAnimations: preferences.enableAnimations })
+  draftConverted.value = aiCoursewareToSlides(draftOutline.value, {
+    enableAnimations: preferences.enableAnimations,
+    paletteVariant: paletteVariant.value
+  })
   draftQuality.value = inspectAiPptQuality(draftOutline.value, draftConverted.value.slides, preferences)
 }
 
@@ -604,6 +738,7 @@ function removeDraftImage(index: number) {
 
 async function regenerateDraftPage(index: number) {
   if (!draftOutline.value || !pptWork.id) return
+  taskMode.value = "full"
   const res = await httpAjaxErrMsg(aiCoursewareGenerateApi, {
     id: pptWork.id,
     fromType: queryParams.pptResourcesType || queryParams.fromType,
@@ -634,14 +769,33 @@ function backToOutline() {
   draftOutline.value = undefined
   draftConverted.value = undefined
   draftQuality.value = undefined
+  workflowStep.value = 2
 }
 
 function invalidateVisualApproval() {
   sampleApproved.value = false
-  if (!draftConverted.value) return
-  draftOutline.value = undefined
-  draftConverted.value = undefined
-  draftQuality.value = undefined
+}
+
+function approveSample() {
+  sampleApproved.value = true
+  ElMessage.success("视觉方向已确认,可以生成完整课件")
+}
+
+function toggleSampleFeedback(item: string) {
+  sampleApproved.value = false
+  sampleFeedbackTags.value = sampleFeedbackTags.value.includes(item)
+    ? sampleFeedbackTags.value.filter(value => value !== item)
+    : [...sampleFeedbackTags.value, item]
+}
+
+function previewPreviousPage() {
+  const page = outline.value?.pages[samplePageIndex.value - 1]
+  if (page) selectedPageNo.value = page.pageNo
+}
+
+function previewNextPage() {
+  const page = outline.value?.pages[samplePageIndex.value + 1]
+  if (page) selectedPageNo.value = page.pageNo
 }
 
 function updateStudentHeadline(page: AiCoursewarePage, event: Event) {
@@ -704,7 +858,13 @@ function buildVisualPrompt() {
       : preferences.sourceImagePolicy === "creative"
         ? "允许根据教学目标重新创作主视觉。"
         : "教材原图与新视觉保持平衡。"
-  return `${base}${asset}${source}`
+  const palette =
+    paletteVariant.value === 1
+      ? "配色使用清新蓝绿体系。"
+      : paletteVariant.value === 2
+        ? "配色使用柔和紫色与珊瑚色体系。"
+        : "配色保持明快且有清晰对比。"
+  return `${base}${asset}${source}${palette}`
 }
 
 function formatPageNo(pageNo?: number) {
@@ -713,6 +873,7 @@ function formatPageNo(pageNo?: number) {
 
 function shortProgressLabel(message?: string) {
   if (!message) return ""
+  if (message.includes("重新生成第")) return "重做样张"
   if (message.includes("读取") || message.includes("等待")) return "准备中"
   if (message.includes("页面") || message.includes("内容")) return "生成内容"
   if (message.includes("优化") || message.includes("教学设计")) return "优化设计"
@@ -762,6 +923,87 @@ function resourceTypeName(type: string) {
   color: #7b8794;
   font-size: 14px;
 }
+.workflow-steps {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  margin-top: 16px;
+  padding: 4px;
+  border-radius: 8px;
+  background: #f4f7fa;
+}
+.workflow-step {
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  height: 38px;
+  color: #7b8794;
+  font-size: 13px;
+  font-weight: 600;
+  &.active {
+    border-radius: 6px;
+    background: #fff;
+    color: $themeColor;
+    box-shadow: 0 2px 8px rgba(31, 41, 51, 0.08);
+  }
+  &.done {
+    color: #168f86;
+  }
+}
+.step-no {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 22px;
+  height: 22px;
+  border-radius: 50%;
+  background: #e7edf5;
+  font-size: 12px;
+}
+.workflow-step.active .step-no {
+  background: $themeColor;
+  color: #fff;
+}
+.workflow-step.done .step-no {
+  background: #dff3ed;
+  color: #168f86;
+}
+.setup-stage {
+  min-height: 0;
+  flex: 1;
+  overflow: auto;
+  padding-right: 4px;
+}
+.stage-intro {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 20px;
+  margin-top: 16px;
+  padding: 14px 16px;
+  border-radius: 8px;
+  background: #f7faff;
+}
+.stage-title {
+  color: #1f2933;
+  font-size: 16px;
+  font-weight: 700;
+}
+.stage-desc {
+  margin-top: 4px;
+  color: #7b8794;
+  font-size: 12px;
+}
+.saved-state {
+  flex-shrink: 0;
+  padding: 5px 9px;
+  border-radius: 999px;
+  background: #eaf7f4;
+  color: #168f86;
+  font-size: 12px;
+  font-weight: 600;
+}
 .progress-strip,
 .loading-strip {
   margin-top: 12px;
@@ -903,26 +1145,107 @@ function resourceTypeName(type: string) {
 .sample-review {
   grid-column: 1 / -1;
   display: grid;
-  grid-template-columns: minmax(240px, 1fr) 420px;
+  grid-template-columns: minmax(360px, 1fr) 420px;
   align-items: center;
   gap: 20px;
-  padding: 14px;
+  padding: 16px;
   border: 1px solid #e5eaf0;
   border-radius: 8px;
   background: #fafcff;
 }
+.sample-eyebrow {
+  color: $themeColor;
+  font-size: 12px;
+  font-weight: 700;
+}
 .sample-title {
-  margin: 8px 0;
-  font-size: 16px;
+  margin: 7px 0 4px;
+  font-size: 18px;
   font-weight: 700;
 }
 .sample-hint {
-  margin-bottom: 14px;
+  margin-bottom: 10px;
   color: #7b8794;
   font-size: 12px;
   line-height: 1.6;
 }
+.sample-page-control {
+  display: grid;
+  grid-template-columns: auto minmax(0, 1fr) auto;
+  gap: 7px;
+  button,
+  select {
+    height: 32px;
+    border: 1px solid #dfe5ec;
+    border-radius: 6px;
+    background: #fff;
+    color: #52616b;
+    font-family: inherit;
+    font-size: 12px;
+  }
+  button {
+    padding: 0 10px;
+    cursor: pointer;
+  }
+  button:disabled {
+    cursor: not-allowed;
+    opacity: 0.45;
+  }
+  select {
+    min-width: 0;
+    padding: 0 8px;
+  }
+}
+.sample-feedback {
+  margin-top: 10px;
+}
+.feedback-label {
+  color: #52616b;
+  font-size: 12px;
+  font-weight: 600;
+}
+.feedback-chips {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 6px;
+  margin-top: 7px;
+  button {
+    height: 28px;
+    padding: 0 10px;
+    border: 1px solid #dfe5ec;
+    border-radius: 999px;
+    background: #fff;
+    color: #52616b;
+    font-family: inherit;
+    font-size: 11px;
+    cursor: pointer;
+  }
+  button.active {
+    border-color: $themeColor;
+    background: rgba($themeColor, 0.08);
+    color: $themeColor;
+  }
+}
+.sample-feedback textarea {
+  width: 100%;
+  height: 54px;
+  margin-top: 7px;
+  padding: 8px 10px;
+  border: 1px solid #dfe5ec;
+  border-radius: 6px;
+  box-sizing: border-box;
+  resize: none;
+  font-family: inherit;
+  font-size: 12px;
+  line-height: 1.5;
+}
+.sample-actions {
+  display: flex;
+  gap: 8px;
+  margin-top: 10px;
+}
 .sample-canvas {
+  position: relative;
   overflow: hidden;
   border: 2px solid transparent;
   border-radius: 6px;
@@ -931,6 +1254,18 @@ function resourceTypeName(type: string) {
 .sample-canvas.approved {
   border-color: #168f86;
 }
+.sample-approved-mark {
+  position: absolute;
+  top: 10px;
+  right: 10px;
+  padding: 5px 9px;
+  border-radius: 999px;
+  background: #168f86;
+  color: #fff;
+  font-size: 11px;
+  font-weight: 700;
+  box-shadow: 0 4px 12px rgba(22, 143, 134, 0.25);
+}
 .result-preview {
   min-height: 0;
   flex: 1;

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