yonge 4 days ago
parent
commit
ae0f2b840a

+ 1 - 1
dist/assets/index-DMxwym03.js → dist/assets/index-B649_9g_.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-CTPkOFAg.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-CBwkUkeK.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};

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


+ 1 - 1
dist/assets/index-DuAGIZys.js → dist/assets/index-BMUOVq-x.js

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

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


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


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


+ 1 - 1
dist/assets/index-Dux_OJAL.js → dist/assets/index-CLw40505.js

@@ -1 +1 @@
-import{u as s,ah as r}from"./index-CcCGo14p.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-CTPkOFAg.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-CVNp4jiL.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-CBwkUkeK.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};

+ 1 - 1
dist/assets/index-CcCGo14p.js → dist/assets/index-CVNp4jiL.js

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


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


+ 1 - 1
dist/assets/index-DLngAGwa.js → dist/assets/index-rc4192rB.js

@@ -1 +1 @@
-import{_ as t}from"./404-R0y5dkvZ.js";import{E as c}from"./index-BdyAxVy4.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-CTPkOFAg.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-Bl97WSgm.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-CBwkUkeK.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/shapes-CiFnAZ1O.js → dist/assets/shapes-BiL_Idk1.js

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

+ 288 - 165
src/views/Editor/EditorHeader/AiCoursewareDialog.vue

@@ -146,24 +146,45 @@
         <div class="sample-stage-head">
           <div>
             <strong>选择喜欢的课件样式</strong>
-            <span>已推荐“{{ selectedTemplate.name }}”,查看代表页面后可直接生成完整课件。</span>
+            <span>已推荐“{{ selectedTemplate.name }}”,可以逐页查看效果后再生成完整课件。</span>
           </div>
           <button type="button" :disabled="working" @click="templatePickerExpanded = !templatePickerExpanded">
             {{ templatePickerExpanded ? "收起模板" : "更换模板" }}
           </button>
         </div>
 
-        <div class="sample-thumbnail-grid">
+        <div class="sample-carousel-shell">
           <button
-            v-for="sample in representativeSampleSlides"
-            :key="sample.pageNo"
             type="button"
-            :class="{ active: selectedPageNo === sample.pageNo }"
-            :disabled="working"
-            @click="selectedPageNo = sample.pageNo"
+            class="sample-carousel-arrow previous"
+            aria-label="查看前面的页面"
+            :disabled="!canScrollSamplePrevious"
+            @click="scrollSampleCarousel(-1)"
           >
-            <ThumbnailSlide v-if="sample.slide" :slide="sample.slide" :size="170" />
-            <span>{{ sample.label }} · 第{{ sample.pageNo }}页</span>
+            ‹
+          </button>
+          <div ref="sampleCarouselRef" class="sample-thumbnail-track" @scroll="updateSampleCarouselNavigation">
+            <button
+              v-for="(slide, index) in previewSlides"
+              :key="slide.id"
+              type="button"
+              :class="['sample-thumbnail-item', { active: selectedPageNo === outline.pages[index]?.pageNo }]"
+              :disabled="working"
+              :title="outline.pages[index]?.title || `第${index + 1}页`"
+              @click="selectSamplePage(outline.pages[index]?.pageNo || index + 1, index)"
+            >
+              <ThumbnailSlide :slide="slide" :size="132" />
+              <span>{{ outline.pages[index]?.title || `第${index + 1}页` }}</span>
+            </button>
+          </div>
+          <button
+            type="button"
+            class="sample-carousel-arrow next"
+            aria-label="查看后面的页面"
+            :disabled="!canScrollSampleNext"
+            @click="scrollSampleCarousel(1)"
+          >
+            ›
           </button>
         </div>
 
@@ -191,63 +212,18 @@
           </div>
         </div>
 
-        <div v-if="sampleSlide" class="sample-review simplified-sample-review">
-          <div class="sample-copy">
-            <div class="sample-eyebrow">当前查看</div>
-            <div class="sample-title">第{{ samplePageNo }}页 · {{ samplePageTitle }}</div>
-            <div class="sample-hint">{{ samplePageReason }}</div>
-            <div v-if="sampleBackgroundState.status === 'error'" class="template-load-error">模板背景加载失败,请更换模板或稍后重试</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="invalidateVisualApproval"
-              ></textarea>
+        <div v-if="sampleSlide && selectedPage" class="sample-detail-layout">
+          <div class="sample-outline-panel">
+            <div class="sample-panel-heading">
+              <span>当前页大纲</span>
+              <strong>第{{ samplePageNo }}页</strong>
             </div>
-            <Button @click="regenerateSamplePage" :disabled="working">调整当前页面</Button>
-          </div>
-          <div class="sample-canvas">
-            <ThumbnailSlide :slide="sampleSlide" :size="420" :show-background-error="true" @background-load-state="handleSampleBackgroundLoadState" />
-          </div>
-        </div>
-
-        <button type="button" class="outline-toggle" @click="outlineExpanded = !outlineExpanded">
-          {{ outlineExpanded ? "收起课件大纲" : `查看或修改课件大纲(共${outline.pages.length}页)` }}
-        </button>
-        <div v-if="outlineExpanded" class="outline-editor-panel">
-          <div class="page-nav">
-            <button
-              v-for="page in outline.pages"
-              :key="page.pageNo"
-              class="nav-item"
-              :class="{ active: selectedPageNo === page.pageNo }"
-              type="button"
-              @click="selectedPageNo = page.pageNo"
-            >
-              <span class="nav-no">{{ formatPageNo(page.pageNo) }}</span>
-              <span class="nav-title">{{ page.title || "未命名页面" }}</span>
-            </button>
-          </div>
-
-          <div v-if="selectedPage" class="page-editor simplified-page-editor">
-            <div class="editor-head">
-              <div class="title-editor">
-                <input v-model="selectedPage.title" class="page-title" :disabled="working" />
-              </div>
+            <div class="page-editor simplified-page-editor">
+              <div class="editor-head">
+                <div class="title-editor">
+                  <label class="outline-field-label">页面主题</label>
+                  <input v-model="selectedPage.title" class="page-title" :disabled="working" />
+                </div>
             </div>
 
             <div class="field-card">
@@ -268,16 +244,60 @@
               ></textarea>
             </div>
           </div>
+          </div>
+
+          <div class="sample-preview-column">
+            <div class="sample-preview-heading">
+              <div>
+                <span>页面预览</span>
+                <strong>{{ samplePageTitle }}</strong>
+              </div>
+              <small>{{ samplePageReason }}</small>
+            </div>
+            <div class="sample-canvas">
+              <ThumbnailSlide
+                :slide="sampleSlide"
+                :size="600"
+                :show-background-error="true"
+                @background-load-state="handleSampleBackgroundLoadState"
+              />
+            </div>
+            <div v-if="sampleBackgroundState.status === 'error'" class="template-load-error">模板背景加载失败,请更换模板或稍后重试</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>
+              <div class="sample-feedback-action">
+                <textarea
+                  v-model="sampleFeedbackText"
+                  :disabled="working"
+                  placeholder="也可以直接说明你希望怎么调整"
+                  @input="invalidateVisualApproval"
+                ></textarea>
+                <Button @click="regenerateSamplePage" :disabled="working">调整当前页面</Button>
+              </div>
+            </div>
+          </div>
         </div>
       </div>
 
       <div class="footer">
         <Button @click="emit('update:visible', false)" :disabled="working">取消</Button>
-        <template v-if="workflowStep === 1">
+        <template v-if="isSetupStep">
           <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">
+        <template v-else-if="isStyleStep">
           <Button @click="workflowStep = 1" :disabled="working">返回修改需求</Button>
           <Button type="primary" @click="confirmSamplesAndGenerate" :disabled="working || sampleBackgroundState.status === 'error'"
             >使用此样式并生成课件</Button
@@ -295,7 +315,7 @@
 </template>
 
 <script lang="ts" setup>
-import { ref, reactive, computed, watch } from "vue"
+import { ref, reactive, computed, watch, nextTick } from "vue"
 import { ElMessage } from "element-plus"
 import Modal from "@/components/Modal.vue"
 import Button from "@/components/Button.vue"
@@ -346,6 +366,8 @@ const extraPrompt = ref("")
 const outline = ref<AiCoursewareOutline>()
 const taskInfo = ref<AiCoursewareTaskInfo>()
 const workflowStep = ref<1 | 2 | 3>(1)
+const isSetupStep = computed(() => workflowStep.value === 1)
+const isStyleStep = computed(() => workflowStep.value === 2)
 const workflowSteps = [
   { value: 1, label: "设置需求" },
   { value: 2, label: "选择样式" },
@@ -359,7 +381,6 @@ const templateSelectionMode = ref<"auto" | "manual">("auto")
 const templatePickerExpanded = ref(false)
 const advancedSettingsExpanded = ref(false)
 const showAllTemplates = ref(false)
-const outlineExpanded = ref(false)
 const paletteVariant = ref(0)
 const pageRange = ref("9-12")
 const preferences = reactive<AiCoursewarePreferences>({
@@ -387,6 +408,9 @@ const TEACHER_VISIBLE_PATTERN = /(?:教师|老师)(?:讲述|提问|引导|指导
 const PLACEHOLDER_PATTERN = /(?:待补充|待完善|请输入|可参考教材|placeholder|lorem ipsum|\{\{.+?\}\}|\[.+?占位.+?\])/i
 const pollTimer = ref<number>()
 const selectedPageNo = ref(1)
+const sampleCarouselRef = ref<HTMLElement>()
+const canScrollSamplePrevious = ref(false)
+const canScrollSampleNext = ref(false)
 const selectedDraftPageIndex = ref(0)
 const outlineLoading = ref(false)
 const working = computed(
@@ -522,12 +546,33 @@ const previewSlides = computed(() => {
   }).slides
 })
 const sampleSlide = computed(() => previewSlides.value?.[samplePageIndex.value])
-const representativeSampleSlides = computed(() =>
-  representativeSamples.value.map(sample => {
-    const pageIndex = outline.value?.pages.findIndex(page => page.pageNo === sample.pageNo) ?? -1
-    return { ...sample, slide: pageIndex >= 0 ? previewSlides.value?.[pageIndex] : undefined }
-  })
-)
+
+function updateSampleCarouselNavigation() {
+  const carousel = sampleCarouselRef.value
+  if (!carousel) {
+    canScrollSamplePrevious.value = false
+    canScrollSampleNext.value = false
+    return
+  }
+  canScrollSamplePrevious.value = carousel.scrollLeft > 2
+  canScrollSampleNext.value = carousel.scrollLeft + carousel.clientWidth < carousel.scrollWidth - 2
+}
+
+function scrollSampleCarousel(direction: -1 | 1) {
+  const carousel = sampleCarouselRef.value
+  if (!carousel) return
+  carousel.scrollBy({ left: direction * Math.max(320, carousel.clientWidth * 0.72), behavior: "smooth" })
+}
+
+function selectSamplePage(pageNo: number, index: number) {
+  if (selectedPageNo.value !== pageNo) {
+    sampleFeedbackTags.value = []
+    sampleFeedbackText.value = ""
+  }
+  selectedPageNo.value = pageNo
+  const item = sampleCarouselRef.value?.children.item(index) as HTMLElement | null
+  item?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" })
+}
 
 watch(selectedTemplateId, () => {
   paletteVariant.value = 0
@@ -560,6 +605,15 @@ watch(
   }
 )
 
+watch(
+  [workflowStep, selectedTemplateId, () => outline.value?.pages.length],
+  async () => {
+    await nextTick()
+    updateSampleCarouselNavigation()
+  },
+  { immediate: true }
+)
+
 async function handleOutline() {
   if (!pptWork.id) {
     ElMessage.error("当前课件ID为空")
@@ -778,15 +832,16 @@ function invalidateVisualApproval() {
 }
 
 async function confirmSamplesAndGenerate() {
-  if (representativeSampleSlides.value.length !== 3 || representativeSampleSlides.value.some(sample => !sample.slide)) {
-    ElMessage.error("代表样稿尚未准备完成,请稍后重试")
+  const slides = previewSlides.value || []
+  if (!outline.value || slides.length !== outline.value.pages.length) {
+    ElMessage.error("页面预览尚未准备完成,请稍后重试")
     return
   }
 
   const backgroundUrls = Array.from(
     new Set(
-      representativeSampleSlides.value
-        .map(sample => (sample.slide?.background?.type === "image" ? resolveTemplateAssetUrl(sample.slide.background.image) : ""))
+      slides
+        .map(slide => (slide.background?.type === "image" ? resolveTemplateAssetUrl(slide.background.image) : ""))
         .filter(Boolean)
     )
   )
@@ -1511,31 +1566,73 @@ function shortProgressLabel(message?: string) {
     cursor: pointer;
   }
 }
-.sample-thumbnail-grid {
+.sample-carousel-shell {
   display: grid;
-  grid-template-columns: repeat(3, 170px);
-  justify-content: center;
-  gap: 16px;
+  grid-template-columns: 34px minmax(0, 1fr) 34px;
+  align-items: center;
+  gap: 8px;
   margin-top: 12px;
-  button {
-    padding: 0;
-    overflow: hidden;
-    border: 2px solid transparent;
-    border-radius: 7px;
-    background: #fff;
-    color: #52616b;
-    cursor: pointer;
-    box-shadow: 0 4px 12px rgba(31, 41, 51, 0.08);
+}
+.sample-carousel-arrow {
+  width: 34px;
+  height: 34px;
+  padding: 0;
+  border: 1px solid #d8e1eb;
+  border-radius: 50%;
+  background: #fff;
+  color: #52616b;
+  font-size: 24px;
+  line-height: 30px;
+  cursor: pointer;
+  box-shadow: 0 3px 10px rgba(31, 41, 51, 0.08);
+  &:hover:not(:disabled) {
+    border-color: $themeColor;
+    color: $themeColor;
   }
-  button.active {
+  &:disabled {
+    opacity: 0.28;
+    cursor: default;
+  }
+}
+.sample-thumbnail-track {
+  display: flex;
+  gap: 10px;
+  min-width: 0;
+  overflow-x: auto;
+  padding: 3px;
+  scroll-behavior: smooth;
+  scroll-snap-type: x proximity;
+  scrollbar-width: none;
+  &::-webkit-scrollbar {
+    display: none;
+  }
+}
+.sample-thumbnail-item {
+  width: 136px;
+  flex: 0 0 136px;
+  padding: 0;
+  overflow: hidden;
+  border: 2px solid transparent;
+  border-radius: 7px;
+  background: #fff;
+  color: #52616b;
+  text-align: left;
+  cursor: pointer;
+  scroll-snap-align: start;
+  box-shadow: 0 3px 10px rgba(31, 41, 51, 0.08);
+  transition: border-color 0.16s ease, box-shadow 0.16s ease;
+  &.active {
     border-color: $themeColor;
+    box-shadow: 0 0 0 2px rgba($themeColor, 0.1);
   }
-  span {
+  > span {
     display: block;
-    padding: 7px 8px;
+    padding: 6px 7px 7px;
+    overflow: hidden;
     font-size: 11px;
     font-weight: 600;
-    text-align: left;
+    text-overflow: ellipsis;
+    white-space: nowrap;
   }
 }
 .compact-template-panel {
@@ -1553,50 +1650,57 @@ function shortProgressLabel(message?: string) {
     margin: 0;
   }
 }
-.simplified-sample-review {
-  grid-column: auto;
-  margin-top: 12px;
-}
-.outline-toggle {
-  display: block;
-  margin: 14px auto 0;
-}
-.outline-editor-panel {
+.sample-detail-layout {
   display: grid;
-  grid-template-columns: 260px minmax(0, 1fr);
-  gap: 12px;
-  min-height: 280px;
-  margin-top: 10px;
-}
-.simplified-page-editor .field-card {
-  margin-top: 12px;
+  grid-template-columns: minmax(280px, 0.72fr) minmax(0, 1.45fr);
+  align-items: start;
+  gap: 16px;
+  margin-top: 14px;
 }
-.sample-review {
-  grid-column: 1 / -1;
-  display: grid;
-  grid-template-columns: minmax(360px, 1fr) 420px;
-  align-items: center;
-  gap: 20px;
-  padding: 16px;
+.sample-outline-panel,
+.sample-preview-column {
+  min-width: 0;
+  padding: 14px;
   border: 1px solid #e5eaf0;
-  border-radius: 8px;
+  border-radius: 10px;
   background: #fafcff;
 }
-.sample-eyebrow {
-  color: $themeColor;
-  font-size: 12px;
-  font-weight: 700;
-}
-.sample-title {
-  margin: 7px 0 4px;
-  font-size: 18px;
-  font-weight: 700;
-}
-.sample-hint {
+.sample-panel-heading,
+.sample-preview-heading {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  gap: 12px;
   margin-bottom: 10px;
+  span,
+  strong {
+    display: block;
+  }
+  span {
+    color: $themeColor;
+    font-size: 12px;
+    font-weight: 700;
+  }
+  strong {
+    margin-top: 3px;
+    color: #1f2933;
+    font-size: 15px;
+  }
+}
+.sample-panel-heading > strong {
+  margin-top: 0;
   color: #7b8794;
   font-size: 12px;
-  line-height: 1.6;
+}
+.sample-preview-heading small {
+  max-width: 52%;
+  color: #7b8794;
+  font-size: 11px;
+  line-height: 1.5;
+  text-align: right;
+}
+.simplified-page-editor .field-card {
+  margin-top: 12px;
 }
 .representative-samples {
   display: grid;
@@ -1871,7 +1975,9 @@ function shortProgressLabel(message?: string) {
   }
 }
 .sample-feedback {
-  margin-top: 10px;
+  margin-top: 12px;
+  padding-top: 12px;
+  border-top: 1px solid #e5eaf0;
 }
 .feedback-label {
   color: #52616b;
@@ -1902,8 +2008,8 @@ function shortProgressLabel(message?: string) {
 }
 .sample-feedback textarea {
   width: 100%;
-  height: 54px;
-  margin-top: 7px;
+  height: 58px;
+  margin: 0;
   padding: 8px 10px;
   border: 1px solid #dfe5ec;
   border-radius: 6px;
@@ -1913,32 +2019,33 @@ function shortProgressLabel(message?: string) {
   font-size: 12px;
   line-height: 1.5;
 }
-.sample-actions {
-  display: flex;
+.sample-feedback-action {
+  display: grid;
+  grid-template-columns: minmax(0, 1fr) auto;
+  align-items: end;
   gap: 8px;
-  margin-top: 10px;
+  margin-top: 8px;
+  :deep(.button) {
+    height: 34px;
+    line-height: 34px;
+    white-space: nowrap;
+  }
 }
 .sample-canvas {
   position: relative;
-  overflow: hidden;
-  border: 2px solid transparent;
-  border-radius: 6px;
-  box-shadow: 0 6px 20px rgba(31, 41, 51, 0.12);
-}
-.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);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  overflow: auto;
+  padding: 12px;
+  border: 1px solid #e0e7ef;
+  border-radius: 8px;
+  background: #eef2f7;
+  :deep(.thumbnail-slide) {
+    flex-shrink: 0;
+    border-radius: 6px;
+    box-shadow: 0 8px 24px rgba(31, 41, 51, 0.14);
+  }
 }
 .result-preview {
   min-height: 0;
@@ -2196,6 +2303,13 @@ function shortProgressLabel(message?: string) {
 .title-editor {
   max-width: 460px;
 }
+.outline-field-label {
+  display: block;
+  margin-bottom: 8px;
+  color: #52616b;
+  font-size: 13px;
+  font-weight: 600;
+}
 .purpose-card {
   margin-top: 12px;
 }
@@ -2388,19 +2502,13 @@ function shortProgressLabel(message?: string) {
     height: calc(100vh - 64px);
   }
   .setup-primary-grid,
-  .outline-editor-panel {
-    grid-template-columns: 1fr;
-  }
-  .sample-review {
+  .sample-detail-layout {
     grid-template-columns: 1fr;
   }
   .sample-canvas {
     justify-self: center;
     max-width: 100%;
   }
-  .sample-thumbnail-grid {
-    grid-template-columns: repeat(3, minmax(0, 1fr));
-  }
   .compact-template-panel .sample-template-picker {
     grid-template-columns: repeat(2, minmax(0, 1fr));
   }
@@ -2432,9 +2540,24 @@ function shortProgressLabel(message?: string) {
     width: 20px;
     height: 20px;
   }
-  .sample-thumbnail-grid {
+  .sample-carousel-shell {
+    grid-template-columns: 30px minmax(0, 1fr) 30px;
+    gap: 5px;
+  }
+  .sample-carousel-arrow {
+    width: 30px;
+    height: 30px;
+  }
+  .sample-feedback-action {
     grid-template-columns: 1fr;
   }
+  .sample-preview-heading {
+    flex-direction: column;
+    small {
+      max-width: none;
+      text-align: left;
+    }
+  }
   .compact-template-panel .sample-template-picker {
     grid-template-columns: 1fr;
   }

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