yonge 4 days ago
parent
commit
32a735968c

+ 1 - 1
dist/assets/index-Bpy1Ietx.js → dist/assets/index-BLanZz14.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,bP as ea,$ as Tc,dE as NT,bO 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,bL as hI,d_ as vI,d$ as pI,bK 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,cv as oh,bI as _I,bH as Ve,bu as sh,bv as Ev,bx as jt,bB as Ic,bN as di,e9 as Tu,bE as hi,ea as AI,bJ as ra,bC as EI,bw as Lt,eb as PI,ec as ui,cw as MI,ed as RI,ee as DI,ef as LI,eg as kI,bM as Jp,eh as OI,bF as qp,cD as _c,cC as BI,ei as Vu,ej as _l}from"./index-TCQxKpEQ.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,bP as ea,$ as Tc,dE as NT,bO 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,bL as hI,d_ as vI,d$ as pI,bK 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,cv as oh,bI as _I,bH as Ve,bu as sh,bv as Ev,bx as jt,bB as Ic,bN as di,e9 as Tu,bE as hi,ea as AI,bJ as ra,bC as EI,bw as Lt,eb as PI,ec as ui,cw as MI,ed as RI,ee as DI,ef as LI,eg as kI,bM as Jp,eh as OI,bF as qp,cD as _c,cC as BI,ei as Vu,ej as _l}from"./index-DBM-UbfS.js";/**
 * vue v3.4.34
 * (c) 2018-present Yuxi (Evan) You and Vue contributors
 * @license MIT

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


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


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


+ 1 - 1
dist/assets/index-C-Aaa1Hr.js → dist/assets/index-BmSNSE_0.js

@@ -1 +1 @@
-import{u as s,ah as r}from"./index-Bpy1Ietx.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-TCQxKpEQ.js";const m={class:"pptScreen"},d=c({__name:"pptScreen",setup(f){const a=n(),e=p(!0),o=s();return a.setScreenMode("pptScreen"),o.initPPTData().then(()=>{e.value=!1}),(S,k)=>(t(),_("div",m,[e.value?i("",!0):(t(),u(r,{key:0}))]))}}),P=l(d,[["__scopeId","data-v-678905e9"]]);export{P as default};
+import{u as s,ah as r}from"./index-BLanZz14.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-DBM-UbfS.js";const m={class:"pptScreen"},d=c({__name:"pptScreen",setup(f){const a=n(),e=p(!0),o=s();return a.setScreenMode("pptScreen"),o.initPPTData().then(()=>{e.value=!1}),(S,k)=>(t(),_("div",m,[e.value?i("",!0):(t(),u(r,{key:0}))]))}}),P=l(d,[["__scopeId","data-v-678905e9"]]);export{P as default};

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


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


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


+ 1 - 1
dist/assets/index-Bc2STCKN.js → dist/assets/index-Dm89N0o3.js

@@ -1 +1 @@
-import{_ as t}from"./404-R0y5dkvZ.js";import{E as c}from"./index-Cb6xgMRB.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-TCQxKpEQ.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-DXNQHm98.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-DBM-UbfS.js";const s=a=>(m("data-v-340bbdaa"),a=a(),f(),a),h={class:"login"},b={class:"error"},g=s(()=>o("img",{src:t,class:"img",alt:""},null,-1)),k=s(()=>o("div",{class:"tit"},"登录已过期或服务器错误!",-1)),v=n({__name:"login",setup(a){function e(){window.close()}return(x,B)=>(i(),r("div",h,[o("div",b,[g,k,_(l(c),{class:"backBtn",type:"primary",plain:"",onClick:e},{default:d(()=>[p("关闭页面")]),_:1})])]))}}),C=u(v,[["__scopeId","data-v-340bbdaa"]]);export{C as default};

+ 1 - 1
dist/assets/index-DXxFpfzQ.js → dist/assets/index-VFagXLE3.js

@@ -1 +1 @@
-import{u as c,ah as l}from"./index-Bpy1Ietx.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-TCQxKpEQ.js";import"./shapes-CDadNhDF.js";const S=()=>{function o(a){if(window.parent===window)return;const{api:e,playState:n}=a.data||{};e==="headerTogge"?window.parent.postMessage({api:"headerTogge",playState:n},"*"):e==="changeTogge"?window.parent.postMessage({api:"changeTogge"},"*"):["clickTempo","clickViewFigner"].includes(e)&&window.parent.postMessage({api:e},"*")}p(()=>{window.addEventListener("message",o)}),d(()=>{window.removeEventListener("message",o)})},_=g({__name:"mobileScreen",setup(o){S();const a=m(),e=t(!0),n=t(!1),i=c();return a.setScreenMode("mobileScreen"),i.initPPTData().then(()=>{e.value=!1}),(v,s)=>(r(),u("div",{class:"mobileScreen",onClick:s[0]||(s[0]=b=>n.value=!1)},[e.value?w("",!0):(r(),f(l,{key:0}))]))}}),P=k(_,[["__scopeId","data-v-90b2692a"]]);export{P as default};
+import{u as c,ah as l}from"./index-BLanZz14.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-DBM-UbfS.js";import"./shapes-KZZr0Zhq.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-B4-jzq4g.js → dist/assets/index-rcv5Xrtm.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-TCQxKpEQ.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-DBM-UbfS.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-CDadNhDF.js → dist/assets/shapes-KZZr0Zhq.js

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

+ 181 - 127
src/views/Editor/EditorHeader/AiCoursewareDialog.vue

@@ -140,7 +140,7 @@
           </div>
         </div>
 
-        <div v-if="draftConverted" :class="['compact-process-state', draftPreparationState]">
+        <div v-if="draftPreparationState !== 'idle'" :class="['compact-process-state', draftPreparationState]">
           <span v-if="draftPreparationState === 'optimizing'" class="loading-dot"></span>
           <span v-else class="process-state-icon">{{ draftPreparationState === "ready" ? "✓" : "!" }}</span>
           <div>
@@ -149,6 +149,13 @@
           </div>
         </div>
 
+        <div v-if="!workspaceSlide" :class="['final-preview-placeholder', draftPreparationState]">
+          <span v-if="draftPreparationState === 'optimizing'" class="loading-dot"></span>
+          <span v-else class="placeholder-icon">!</span>
+          <strong>{{ draftPreparationState === "failed" ? "最终课件暂未生成成功" : "正在生成并优化最终课件" }}</strong>
+          <small>{{ draftPreparationState === "failed" ? "请更换模板或返回修改需求后重试" : "完成后将直接展示可应用的最终效果" }}</small>
+        </div>
+
         <div v-if="workspaceSlide" class="unified-review-workspace">
           <aside class="workspace-page-list" aria-label="课件页面">
             <button
@@ -182,6 +189,11 @@
                 @background-load-state="handleSampleBackgroundLoadState"
                 @text-change="handleWorkspaceTextChange"
               />
+              <div v-if="working" class="workspace-processing-overlay">
+                <span class="loading-dot"></span>
+                <strong>正在生成并优化新版本</strong>
+                <small>完成后会自动更新预览</small>
+              </div>
             </div>
             <div v-if="sampleBackgroundState.status === 'error'" class="template-load-error">模板背景加载失败,请更换模板或稍后重试</div>
             <div class="sample-feedback workspace-feedback">
@@ -199,12 +211,7 @@
                 </button>
               </div>
               <div class="sample-feedback-action">
-                <textarea
-                  v-model="sampleFeedbackText"
-                  :disabled="working"
-                  placeholder="说明希望如何调整,文字可直接在上方预览中双击修改"
-                  @input="invalidateVisualApproval"
-                ></textarea>
+                <textarea v-model="sampleFeedbackText" :disabled="working" placeholder="说明希望如何调整,文字可直接在上方预览中双击修改"></textarea>
                 <Button @click="adjustCurrentPage" :disabled="working">调整当前页</Button>
               </div>
             </div>
@@ -215,20 +222,19 @@
       <div class="footer">
         <Button @click="emit('update:visible', false)" :disabled="working">取消</Button>
         <template v-if="isSetupStep">
-          <Button v-if="outline" @click="workflowStep = 2" :disabled="working">继续选择样式</Button>
+          <Button v-if="outline" @click="continueToFinalPreview" :disabled="working">{{ displayedConverted ? "继续预览" : "继续生成" }}</Button>
           <Button type="primary" @click="handleOutline" :disabled="working">{{ outline ? "按当前设置重新生成" : "开始生成课件" }}</Button>
         </template>
         <template v-else-if="isStyleStep">
           <Button @click="workflowStep = 1" :disabled="working">返回修改需求</Button>
           <span v-if="draftPreparationState === 'optimizing'" class="footer-progress">正在自动检查并优化,无需操作…</span>
           <Button
-            v-if="!draftConverted"
+            v-if="displayedConverted && draftPreparationState === 'ready'"
             type="primary"
-            @click="confirmSamplesAndGenerate"
-            :disabled="working || sampleBackgroundState.status === 'error'"
-            >生成并自动优化</Button
+            :disabled="sampleBackgroundState.status === 'error'"
+            @click="applyDraft"
+            >应用到当前课件</Button
           >
-          <Button v-else-if="draftPreparationState === 'ready'" type="primary" @click="applyDraft">应用到当前课件</Button>
         </template>
       </div>
     </div>
@@ -293,7 +299,6 @@ const workflowSteps = [
   { value: 1, label: "设置需求" },
   { value: 2, label: "预览、编辑并应用" }
 ] as const
-const taskMode = ref<"full" | "sample">("full")
 const coursewareTemplates = AI_COURSEWARE_TEMPLATES
 const defaultTemplateId = AI_COURSEWARE_TEMPLATES[0].id
 const selectedTemplateId = ref<AiCoursewareTemplateId>(defaultTemplateId)
@@ -313,13 +318,15 @@ const preferences = reactive<AiCoursewarePreferences>({
   requireVideo: false,
   enableAnimations: true
 })
-const approvedSamplePages = ref<number[]>([])
 const sampleBackgroundState = ref<{ status: "none" | "loading" | "loaded" | "error"; url: string }>({ status: "none", url: "" })
 const sampleFeedbackOptions = ["画风不合适", "页面太空", "图片不合适", "层级不清晰"]
 const sampleFeedbackTags = ref<string[]>([])
 const sampleFeedbackText = ref("")
 const draftOutline = ref<AiCoursewareOutline>()
 const draftConverted = ref<{ title: string; theme: Partial<SlideTheme>; slides: Slide[] }>()
+const displayedOutline = ref<AiCoursewareOutline>()
+const displayedConverted = ref<{ title: string; theme: Partial<SlideTheme>; slides: Slide[] }>()
+const displayedTemplateId = ref<AiCoursewareTemplateId>()
 const draftQuality = ref<AiPptQualityReport>()
 const draftPreparationState = ref<"idle" | "optimizing" | "ready" | "failed">("idle")
 const automaticRepairAttempts = ref(0)
@@ -337,7 +344,6 @@ const protectedPageContent = new Map<number, ProtectedPageContent>()
 const TEACHER_VISIBLE_PATTERN = /(?:教师|老师)(?:讲述|提问|引导|指导|组织|巡视|示范)|(?:引导|指导|组织)学生/
 const PLACEHOLDER_PATTERN = /(?:待补充|待完善|请输入|可参考教材|placeholder|lorem ipsum|\{\{.+?\}\}|\[.+?占位.+?\])/i
 const pollTimer = ref<number>()
-const selectedPageNo = ref(1)
 const selectedDraftPageIndex = ref(0)
 const outlineLoading = ref(false)
 const working = computed(
@@ -386,26 +392,21 @@ const pageMeta = computed(() => {
   if (!outline.value) return `根据当前教材生成${preferences.minPages}至${preferences.maxPages}页音乐课件`
   return [...[outline.value.gradeName, outline.value.unitName].filter(Boolean), `共${outline.value.pages.length}页`].join(" · ")
 })
-const samplePageIndex = computed(() => {
-  const pages = outline.value?.pages || []
-  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 workspaceSlides = computed(() => draftConverted.value?.slides || previewSlides.value || [])
-const workspacePages = computed(() => draftOutline.value?.pages || outline.value?.pages || [])
-const selectedWorkspacePageIndex = computed(() => (draftConverted.value ? selectedDraftPageIndex.value : samplePageIndex.value))
+const workspaceSlides = computed(() => displayedConverted.value?.slides || [])
+const workspacePages = computed(() => displayedOutline.value?.pages || [])
+const selectedWorkspacePageIndex = computed(() => selectedDraftPageIndex.value)
 const workspacePage = computed(() => workspacePages.value[selectedWorkspacePageIndex.value])
 const workspaceSlide = computed(() => workspaceSlides.value[selectedWorkspacePageIndex.value])
-const canEditWorkspaceText = computed(() => !working.value && (!draftConverted.value || draftPreparationState.value === "ready"))
+const canEditWorkspaceText = computed(() => !working.value && Boolean(displayedConverted.value) && draftPreparationState.value === "ready")
 const draftStatusTitle = computed(() => {
   if (draftPreparationState.value === "ready") return "课件已准备完成"
   if (draftPreparationState.value === "failed") return "自动优化未能完成"
   return "正在自动检查和优化课件"
 })
 const draftStatusDescription = computed(() => {
-  if (draftPreparationState.value === "ready") return "可双击预览中的文字修改,确认后直接应用。"
-  if (draftPreparationState.value === "failed") return "请更换模板或返回修改需求后重新生成。"
+  if (draftPreparationState.value === "ready") return "当前展示的就是优化后的最终课件,可直接编辑或应用。"
+  if (draftPreparationState.value === "failed")
+    return displayedConverted.value ? "新版本未生成成功,已保留上一次最终预览。" : "请更换模板或返回修改需求后重新生成。"
   return `系统正在自动处理问题页面(${Math.max(1, automaticRepairAttempts.value)}/${MAX_AUTOMATIC_REPAIR_ATTEMPTS})。`
 })
 const previewSlides = computed(() => {
@@ -420,11 +421,7 @@ const previewSlides = computed(() => {
 function selectWorkspacePage(index: number) {
   sampleFeedbackTags.value = []
   sampleFeedbackText.value = ""
-  if (draftConverted.value) {
-    selectedDraftPageIndex.value = index
-    return
-  }
-  selectedPageNo.value = outline.value?.pages[index]?.pageNo || index + 1
+  selectedDraftPageIndex.value = index
 }
 
 function handleWorkspaceTextChange(payload: { elementId: string; content: string; previousContent: string; text: string; previousText: string }) {
@@ -441,7 +438,8 @@ function handleWorkspaceTextChange(payload: { elementId: string; content: string
   }
 
   protectPageContent(page)
-  invalidateVisualApproval()
+  restoreProtectedPageContent(outline.value)
+  restoreProtectedPageContent(draftOutline.value)
   ElMessage.success("文字已更新")
 }
 
@@ -507,7 +505,6 @@ function restoreProtectedPageContent(targetOutline?: AiCoursewareOutline) {
 watch(selectedTemplateId, () => {
   paletteVariant.value = 0
   if (outline.value) outline.value.style = selectedStyle.value
-  invalidateVisualApproval()
 })
 
 watch(pageRange, value => {
@@ -516,14 +513,11 @@ watch(pageRange, value => {
   preferences.maxPages = maxPages
 })
 
-watch(preferences, () => invalidateVisualApproval(), { deep: true })
-
 watch(
   outline,
   () => {
     if (!outline.value) return
     if (templateSelectionMode.value === "auto") selectedTemplateId.value = templateRecommendation.value.id
-    invalidateVisualApproval()
   },
   { deep: true }
 )
@@ -553,17 +547,68 @@ async function handleOutline() {
     protectedPageContent.clear()
     draftOutline.value = undefined
     draftConverted.value = undefined
+    displayedOutline.value = undefined
+    displayedConverted.value = undefined
+    displayedTemplateId.value = undefined
     draftQuality.value = undefined
     draftPreparationState.value = "idle"
     outline.value = res.data as AiCoursewareOutline
     if (templateSelectionMode.value === "auto") selectedTemplateId.value = recommendAiCoursewareTemplate(outline.value).id
     outline.value.style = selectedStyle.value
     outline.value.templateId = selectedTemplateId.value
-    selectedPageNo.value = outline.value.pages[0]?.pageNo || 1
+    selectedDraftPageIndex.value = 0
     sampleFeedbackTags.value = []
     sampleFeedbackText.value = ""
+    draftPreparationState.value = "optimizing"
     workflowStep.value = 2
+    await confirmSamplesAndGenerate()
+  }
+}
+
+async function continueToFinalPreview() {
+  workflowStep.value = 2
+  if (displayedConverted.value) {
+    draftPreparationState.value = "ready"
+    return
+  }
+  draftPreparationState.value = "optimizing"
+  await confirmSamplesAndGenerate()
+}
+
+async function regenerateFinalPreview() {
+  const sourceOutline = displayedOutline.value || draftOutline.value || outline.value
+  if (!sourceOutline) return
+  draftOutline.value = cloneCoursewareData(sourceOutline)
+  restoreProtectedPageContent(draftOutline.value)
+  workflowStep.value = 2
+  await handleGenerate()
+}
+
+function publishFinalPreview() {
+  if (!draftOutline.value || !draftConverted.value) return
+  displayedOutline.value = cloneCoursewareData(draftOutline.value)
+  displayedConverted.value = cloneCoursewareData(draftConverted.value)
+  displayedTemplateId.value = selectedTemplateId.value
+  selectedDraftPageIndex.value = Math.max(0, Math.min(selectedDraftPageIndex.value, displayedConverted.value.slides.length - 1))
+  sampleFeedbackTags.value = []
+  sampleFeedbackText.value = ""
+}
+
+function handleFinalPreparationFailure(message = "自动生成和优化未能完成") {
+  if (displayedOutline.value && displayedConverted.value && displayedTemplateId.value) {
+    selectedTemplateId.value = displayedTemplateId.value
+    draftOutline.value = cloneCoursewareData(displayedOutline.value)
+    draftConverted.value = cloneCoursewareData(displayedConverted.value)
+    draftQuality.value = undefined
+    draftPreparationState.value = "ready"
+    ElMessage.warning(`${message},已保留上一次最终预览`)
+    return
   }
+  draftPreparationState.value = "failed"
+}
+
+function cloneCoursewareData<T>(value: T): T {
+  return JSON.parse(JSON.stringify(value)) as T
 }
 
 async function handleGenerate() {
@@ -572,12 +617,11 @@ async function handleGenerate() {
 
   draftConverted.value = undefined
   draftQuality.value = undefined
-  draftPreparationState.value = "idle"
+  draftPreparationState.value = "optimizing"
   automaticRepairAttempts.value = 0
   sourceOutline.style = selectedStyle.value
   sourceOutline.templateId = selectedTemplateId.value
   restoreProtectedPageContent(sourceOutline)
-  taskMode.value = "full"
   const res = await httpAjaxErrMsg(aiCoursewareGenerateApi, {
     id: pptWork.id,
     fromType: queryParams.pptResourcesType || queryParams.fromType,
@@ -586,28 +630,11 @@ async function handleGenerate() {
     visualPrompt: buildVisualPrompt(),
     preferences: { ...preferences }
   })
-  if (res.code !== 200) return
-
-  taskInfo.value = res.data as AiCoursewareTaskInfo
-  await pollTask(taskInfo.value.taskId)
-}
+  if (res.code !== 200) {
+    handleFinalPreparationFailure()
+    return
+  }
 
-async function regenerateSamplePage() {
-  if (!outline.value || !pptWork.id) return
-  approvedSamplePages.value = approvedSamplePages.value.filter(pageNo => pageNo !== samplePageNo.value)
-  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,
-    templateId: selectedTemplateId.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)
 }
@@ -629,18 +656,17 @@ async function fetchTask(taskId: string) {
   taskInfo.value = res.data as AiCoursewareTaskInfo
   if (taskInfo.value.status === "DONE") {
     clearPollTimer()
-    if (taskMode.value === "sample") await loadSampleResult(taskId)
-    else await loadDraft(taskId)
+    await loadDraft(taskId)
   } else if (taskInfo.value.status === "FAILED") {
     clearPollTimer()
     const message = taskInfo.value.message || "AI课件生成失败"
     clearTask()
-    if (taskMode.value === "full" && draftOutline.value) {
-      draftPreparationState.value = "failed"
+    if (draftOutline.value && draftQuality.value) {
       console.error("AI课件自动优化失败", message)
       await continueAutomaticRepairAfterFailure()
     } else {
-      ElMessage.error(message)
+      console.error("AI课件生成失败", message)
+      handleFinalPreparationFailure(message)
     }
   }
 }
@@ -661,6 +687,7 @@ async function loadDraft(taskId: string) {
   const res = await httpAjaxErrMsg(aiCoursewareResultApi, taskId)
   if (res.code !== 200) {
     clearTask()
+    handleFinalPreparationFailure("未能取得最终课件")
     return
   }
 
@@ -674,24 +701,6 @@ async function loadDraft(taskId: string) {
   await prepareDraftForUse()
 }
 
-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
-  restoreProtectedPageContent(outline.value)
-  outline.value.style = selectedStyle.value
-  outline.value.templateId = selectedTemplateId.value
-  sampleFeedbackTags.value = []
-  sampleFeedbackText.value = ""
-  workflowStep.value = 2
-  clearTask()
-  ElMessage.success(`第${samplePageNo.value}页样张已重新生成,请再次确认视觉方向`)
-}
-
 function rebuildDraftPreview() {
   if (!draftOutline.value) {
     draftConverted.value = undefined
@@ -712,7 +721,6 @@ async function regenerateDraftPage(index: number) {
   if (!draftOutline.value || !pptWork.id) return
   draftPreparationState.value = "optimizing"
   automaticRepairAttempts.value = 0
-  taskMode.value = "full"
   restoreProtectedPageContent(draftOutline.value)
   if (sampleFeedbackTags.value.includes("画风不合适")) paletteVariant.value = (paletteVariant.value + 1) % 3
   const feedback = [...sampleFeedbackTags.value, sampleFeedbackText.value.trim()].filter(Boolean).join(";")
@@ -734,37 +742,19 @@ async function regenerateDraftPage(index: number) {
 }
 
 function applyDraft() {
-  if (!draftConverted.value || !draftQuality.value?.passed) return
-  slidesStore.setTitle(draftConverted.value.title)
-  slidesStore.setTheme(draftConverted.value.theme)
-  slidesStore.setSlides(draftConverted.value.slides)
+  if (!displayedConverted.value || draftPreparationState.value !== "ready") return
+  slidesStore.setTitle(displayedConverted.value.title)
+  slidesStore.setTheme(displayedConverted.value.theme)
+  slidesStore.setSlides(displayedConverted.value.slides)
   slidesStore.updateSlideIndex(0)
   addHistorySnapshot()
   ElMessage.success("PPT已应用,请检查后手动保存")
   emit("update:visible", false)
 }
 
-function backToOutline() {
-  clearTask()
-  if (draftOutline.value) outline.value = draftOutline.value
-  draftOutline.value = undefined
-  draftConverted.value = undefined
-  draftQuality.value = undefined
-  draftPreparationState.value = "idle"
-  automaticRepairAttempts.value = 0
-  workflowStep.value = 2
-}
-
 async function adjustCurrentPage() {
-  if (draftConverted.value) {
-    await regenerateDraftPage(selectedDraftPageIndex.value)
-    return
-  }
-  await regenerateSamplePage()
-}
-
-function invalidateVisualApproval() {
-  approvedSamplePages.value = []
+  if (!displayedConverted.value) return
+  await regenerateDraftPage(selectedDraftPageIndex.value)
 }
 
 async function confirmSamplesAndGenerate() {
@@ -781,50 +771,52 @@ async function confirmSamplesAndGenerate() {
   try {
     await Promise.all(backgroundUrls.map(preloadImage))
   } catch {
-    ElMessage.error("模板背景加载失败,请更换模板或稍后重试")
+    handleFinalPreparationFailure("模板背景加载失败")
     return
   }
 
-  approvedSamplePages.value = outline.value.pages.map(page => page.pageNo)
   await handleGenerate()
 }
 
 function handleSampleBackgroundLoadState(payload: { status: "none" | "loading" | "loaded" | "error"; url: string }) {
   sampleBackgroundState.value = payload
   if (payload.status === "error") {
-    approvedSamplePages.value = approvedSamplePages.value.filter(pageNo => pageNo !== samplePageNo.value)
+    const page = workspacePage.value || draftOutline.value?.pages[selectedDraftPageIndex.value] || outline.value?.pages[selectedDraftPageIndex.value]
     console.error("AI课件模板背景加载失败", {
       templateId: selectedTemplateId.value,
-      pageNo: samplePageNo.value,
-      layout: outline.value?.pages[samplePageIndex.value]?.visualPlan?.layout,
+      pageNo: page?.pageNo || selectedDraftPageIndex.value + 1,
+      layout: page?.visualPlan?.layout,
       url: payload.url
     })
   }
 }
 
 function toggleSampleFeedback(item: string) {
-  approvedSamplePages.value = approvedSamplePages.value.filter(pageNo => pageNo !== samplePageNo.value)
   sampleFeedbackTags.value = sampleFeedbackTags.value.includes(item)
     ? sampleFeedbackTags.value.filter(value => value !== item)
     : [...sampleFeedbackTags.value, item]
 }
 
-function selectTemplate(templateId: AiCoursewareTemplateId, fromSample = false) {
-  if (draftOutline.value) backToOutline()
+async function selectTemplate(templateId: AiCoursewareTemplateId, fromSample = false) {
+  if (templateId === selectedTemplateId.value && displayedConverted.value) {
+    templatePickerExpanded.value = false
+    return
+  }
   templateSelectionMode.value = "manual"
   selectedTemplateId.value = templateId
   if (fromSample) {
     templatePickerExpanded.value = false
     showAllTemplates.value = false
-    ElMessage.success(`已切换为${getAiCoursewareTemplate(templateId).name},已同步重排样张结构`)
+    ElMessage.info(`正在生成并优化${getAiCoursewareTemplate(templateId).name}`)
   }
+  await regenerateFinalPreview()
 }
 
-function useRecommendedTemplate() {
-  if (draftOutline.value) backToOutline()
+async function useRecommendedTemplate() {
   templateSelectionMode.value = "auto"
   selectedTemplateId.value = templateRecommendation.value.id
-  ElMessage.success(`已使用系统推荐的${recommendedTemplate.value.name}`)
+  ElMessage.info(`正在生成并优化${recommendedTemplate.value.name}`)
+  await regenerateFinalPreview()
 }
 
 function templateCoverStyle(templateId: AiCoursewareTemplateId): Record<string, string> {
@@ -859,14 +851,15 @@ async function prepareDraftForUse() {
   rebuildDraftPreview()
 
   if (draftQuality.value?.passed) {
+    publishFinalPreview()
     draftPreparationState.value = "ready"
-    ElMessage.success("课件已完成自动检查,可以直接应用")
+    ElMessage.success("最终课件已完成优化,可以直接预览并应用")
     return
   }
 
   console.warn("AI课件自动检查发现待修复项", draftQuality.value?.errors)
   if (automaticRepairAttempts.value >= MAX_AUTOMATIC_REPAIR_ATTEMPTS) {
-    draftPreparationState.value = "failed"
+    handleFinalPreparationFailure()
     return
   }
 
@@ -876,14 +869,13 @@ async function prepareDraftForUse() {
 
 async function requestAutomaticRepair() {
   if (!draftOutline.value || !pptWork.id || !draftQuality.value) {
-    draftPreparationState.value = "failed"
+    handleFinalPreparationFailure()
     return
   }
   const repairSummary = draftQuality.value.errors
     .slice(0, 12)
     .map(issue => `${issue.pageNo ? `第${issue.pageNo}页` : "整套课件"}:${issue.message}`)
     .join(";")
-  taskMode.value = "full"
   const res = await httpAjaxErrMsg(aiCoursewareGenerateApi, {
     id: pptWork.id,
     fromType: queryParams.pptResourcesType || queryParams.fromType,
@@ -903,7 +895,7 @@ async function requestAutomaticRepair() {
 async function continueAutomaticRepairAfterFailure() {
   clearTask()
   if (automaticRepairAttempts.value >= MAX_AUTOMATIC_REPAIR_ATTEMPTS) {
-    draftPreparationState.value = "failed"
+    handleFinalPreparationFailure()
     return
   }
   automaticRepairAttempts.value += 1
@@ -1537,6 +1529,45 @@ function shortProgressLabel(message?: string) {
   font-size: 11px;
   font-weight: 700;
 }
+.final-preview-placeholder {
+  min-height: 320px;
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-direction: column;
+  gap: 8px;
+  margin-top: 8px;
+  border: 1px solid #e1e8f0;
+  border-radius: 8px;
+  background: #f7f9fc;
+  color: #52616b;
+  strong {
+    color: #263442;
+    font-size: 15px;
+  }
+  small {
+    color: #7b8794;
+    font-size: 12px;
+  }
+  &.failed {
+    background: #fffaf3;
+  }
+  .loading-dot {
+    width: 20px;
+    height: 20px;
+  }
+}
+.placeholder-icon {
+  display: grid;
+  width: 28px;
+  height: 28px;
+  place-items: center;
+  border: 1px solid #e5b66d;
+  border-radius: 50%;
+  color: #a46108;
+  font-weight: 700;
+}
 .unified-review-workspace {
   min-height: 0;
   flex: 1;
@@ -1629,6 +1660,7 @@ function shortProgressLabel(message?: string) {
   }
 }
 .workspace-canvas-shell {
+  position: relative;
   min-height: 0;
   flex: 1 1 auto;
   display: flex;
@@ -1644,6 +1676,28 @@ function shortProgressLabel(message?: string) {
     box-shadow: 0 10px 28px rgba(31, 41, 51, 0.15);
   }
 }
+.workspace-processing-overlay {
+  position: absolute;
+  inset: 10px;
+  z-index: 5;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-direction: column;
+  gap: 7px;
+  border-radius: 6px;
+  background: rgba(245, 248, 252, 0.92);
+  color: #52616b;
+  backdrop-filter: blur(2px);
+  strong {
+    color: #263442;
+    font-size: 14px;
+  }
+  small {
+    color: #7b8794;
+    font-size: 11px;
+  }
+}
 .workspace-feedback {
   flex: 0 0 auto;
   margin-top: 8px;

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