yonge 2 дней назад
Родитель
Сommit
9f21487ba7

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


+ 1 - 1
dist/assets/index-TQOGWCvg.js → dist/assets/index-BRruopgM.js

@@ -1 +1 @@
-import{_ as t}from"./404-R0y5dkvZ.js";import{E as c}from"./index-BWhBrdxR.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,bK as m,bL as f,bH as u}from"./index-CQIh-Q03.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-DjhJ3cLi.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,bK as m,bL as f,bH as u}from"./index-BYQICn-d.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-BYQICn-d.js


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


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


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


+ 1 - 1
dist/assets/index-BGor5NqM.js → dist/assets/index-CYUaulEP.js

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

+ 1 - 1
dist/assets/index-DuI25JxZ.js → dist/assets/index-D-9C7kq9.js

@@ -1 +1 @@
-import{_ as a}from"./404-R0y5dkvZ.js";import{a5 as o,ep as t,ae as r,af as c,bK as _,bL as d,ak as s,bH as p}from"./index-CQIh-Q03.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,ep as t,ae as r,af as c,bK as _,bL as d,ak as s,bH as p}from"./index-BYQICn-d.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/index-CK0-ZPB_.js → dist/assets/index-DB6-Hacn.js

@@ -1 +1 @@
-import{u as c,ag as l}from"./index-BGor5NqM.js";import{w as p,bA as d,a5 as g,cE as m,R as t,ae as r,af as u,al as f,ag as w,bH as k}from"./index-CQIh-Q03.js";import"./shapes-BzawXFTG.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}))]))}}),E=k(_,[["__scopeId","data-v-90b2692a"]]);export{E as default};
+import{u as c,ag as l}from"./index-CYUaulEP.js";import{w as p,bA as d,a5 as g,cE as m,R as t,ae as r,af as u,al as f,ag as w,bH as k}from"./index-BYQICn-d.js";import"./shapes-BDwgiUOt.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}))]))}}),E=k(_,[["__scopeId","data-v-90b2692a"]]);export{E as default};

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


+ 1 - 1
dist/assets/index-bA5aKz9y.js → dist/assets/index-_1Pe1YyP.js

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

+ 1 - 1
dist/assets/shapes-BzawXFTG.js → dist/assets/shapes-BDwgiUOt.js

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

+ 29 - 0
scripts/aiPptDialogTest.html

@@ -19,6 +19,8 @@ import { aiCoursewareToSlides } from "/src/utils/aiCoursewareToSlides.ts"
 import { liveRegressionFixture } from './fixtures/aiPptLiveRegression.mjs'
 import { storyVisualFixture } from './fixtures/aiPptStoryVisual.mjs'
 import { nativeVisualFixture } from './fixtures/aiPptNativeVisual.mjs'
+import { slideDesignFixture } from './fixtures/aiPptSlideDesign.mjs'
+import ThumbnailSlide from '/src/views/components/ThumbnailSlide/index.vue'
 import "/src/assets/styles/global.scss"
 import "/src/assets/styles/elementTheme.scss"
 import "/src/assets/styles/prosemirror.scss"
@@ -37,6 +39,7 @@ const historicalPolls = ref(0)
 let repaired = false
 const resourceCheck = ref("检查中")
 const runtimeError = ref("")
+const invalidTarget = ref(false)
 const types = ["cover", "scene_intro", "objectives", "music_knowledge", "rhythm_practice", "performance_activity", "practice_check", "creative_activity", "summary"]
 const layouts = ["cover_scene", "hero_focus", "objectives", "split_content", "diagram_focus", "activity", "text_focus", "activity", "summary"]
 const outline = {
@@ -84,6 +87,11 @@ httpAxios.axioseRquest = async config => {
     if (config.data.repairOnly || config.data.regeneratePageNo) { repairs.value++; repaired=true }
     data={taskId:`local-task-${generated.value}`,status:"PENDING",progress:0}
   }
+  else if (config.url.endsWith('/result') && scenario === 'page-design-isolation') {
+    data = { outline: JSON.parse(JSON.stringify(submitted.value.outline)), context: { resources: [] }, blockingMessage: '' }
+    data.outline.pages[1].studentContent.points[0] = '认真聆听,再表达声音的特点'
+    if (invalidTarget.value) data.outline.pages[1].pageDesign.elements[1].height = 12
+  }
   else if (config.url.endsWith("/result")) {
     data={outline:JSON.parse(JSON.stringify(submitted.value?.outline || outline)),context:{resources:[]}}
     data.outline.pages.forEach((page, index) => {
@@ -157,6 +165,27 @@ const app=createApp({setup(){
   const slidesStore=useSlidesStore()
   const pptWork=usePptWork()
   const repair = useAiCoursewarePageRepair()
+  if (scenario === 'page-design-isolation') {
+    const fixture = slideDesignFixture()
+    fixture.pages[0].pageDesign = { version:1, elements:[] }
+    const result = aiCoursewareToSlides(fixture, { templateId:'blank', recoverDraft:true })
+    slidesStore.setViewportSize(1920); slidesStore.setViewportRatio(.5625)
+    slidesStore.setTheme({ ...slidesStore.theme, ...result.theme }); slidesStore.setSlides(result.slides)
+    const otherBefore = JSON.stringify(slidesStore.slides[0])
+    repair.generated([], fixture, {lessonMinutes:40,minPages:2,maxPages:2,resourceMode:'teacher_supplement',enableAnimations:false})
+    return () => h('main',{style:'padding:24px;font-family:Microsoft Yahei;background:#edf1f5;min-height:100vh'},[
+      h('h1','单页生成隔离回归'), h('p','本地模拟接口:第1页版式未完成,仅重新生成第2页;不修改线上课件。'),
+      h('button',{disabled:repair.repairingPage.value!==undefined,onClick:()=>repair.repair(2)},'重新生成第2页'),
+      h('button',{onClick:()=>{invalidTarget.value=!invalidTarget.value}},invalidTarget.value?'恢复有效目标页':'模拟目标页版式失败'),
+      h('p',{id:'repair-state'},`状态:${repair.repairingPage.value===undefined?'空闲':'生成中'};生成请求:${repairs.value}`),
+      h('p',{id:'repair-error'},repair.error.value),
+      h('p',{id:'repair-content'},slidesStore.slides[1].elements.filter(e=>e.type==='text').map(e=>e.content.replace(/<[^>]+>/g,'')).join(';')),
+      h('p',{id:'other-pages'},`第1页${JSON.stringify(slidesStore.slides[0])===otherBefore?'保持不变':'被修改'}`),
+      ...slidesStore.slides.map((slide,i)=>h('section',{style:'margin:16px 0;background:white;padding:12px'},[
+        h('p',`第${i+1}页`),h(ThumbnailSlide,{slide,size:Math.min(900,innerWidth-72),viewportSize:1920,viewportRatio:.5625})
+      ]))
+    ])
+  }
   if (['editor-live', 'screen-live', 'editor-story', 'screen-story', 'editor-native', 'screen-native'].includes(scenario)) {
     const result = aiCoursewareToSlides(scenario.endsWith('native') ? nativeVisualFixture() : scenario.endsWith('story') ? storyVisualFixture() : liveRegressionFixture(), { templateId: 'blank' })
     slidesStore.setViewportSize(1920)

+ 47 - 0
scripts/aiPptDraftRecoveryTest.html

@@ -0,0 +1,47 @@
+<!doctype html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>草稿恢复回归</title>
+  <style>
+    body{margin:0;background:#eef1f5;font-family:"Microsoft Yahei",sans-serif;color:#283544}main{max-width:1000px;margin:24px auto;padding:24px;background:white;border-radius:8px}h1{font-size:26px;margin:0 0 16px}.test-note{color:#65758a;margin-bottom:16px}button.action{margin-top:20px;padding:12px 24px;background:#1989fa;color:white;border:0;border-radius:4px;cursor:pointer}.editor-pages{display:grid;gap:20px;margin-top:16px}.editor-pages>div{background:#eef1f5;padding:12px}#conversion-status{margin-top:16px;color:#34475c}@media(max-width:1048px){main{margin:12px;padding:16px}}
+  </style>
+</head>
+<body><div id="app"></div>
+<script type="module">
+  import { createApp, h, ref, nextTick } from 'vue'
+  import { createPinia } from 'pinia'
+  import DraftPreview from '/src/views/Editor/EditorHeader/AiCoursewareDraftPreview.vue'
+  import ThumbnailSlide from '/src/views/components/ThumbnailSlide/index.vue'
+  import { aiCoursewareToSlides } from '/src/utils/aiCoursewareToSlides.ts'
+  import { inspectAiPptRenderedText } from '/src/utils/aiPptRenderedQuality.ts'
+  import '/src/assets/styles/global.scss'
+  import '/src/assets/styles/prosemirror.scss'
+
+  // 模拟十页均有无效模型版式;只验证生产预览组件/草稿转换,不访问线上课件。
+  const titles = ['凤鸣山谷的故事','听听山谷的声音','今天我们要学会','第一次聆听','山谷里的声音','声音有什么特点','模仿声音','一起比较声音','山谷音乐会','把山谷声音带回家']
+  const outline = { title: titles[0], chapterName: titles[0], templateId:'blank', style:'fresh', pages: titles.map((title,index) => ({
+    pageNo:index+1,type:index===0?'cover':'knowledge',title,bullets:[],
+    studentContent:{headline:index===9?'你最喜欢山谷里的哪种声音?':'仔细听,说说声音的特点。',points:['凤凰的歌声:又轻又长。','麒麟的蹄声:短而快。','山谷的风声:长而轻。']},
+    classroomAction:{studentTask:index===9?'用一句话说出你最喜欢的声音,再说出它的一个特点。':'选一种声音,用动作表现它的长短和强弱。'},
+    visualPlan:{composition:'text_focus',visualMode:'text_only',imageNecessity:'none'},
+    pageDesign:{version:1,elements:[]}
+  })) }
+  const converted = ref(), status = ref(''), diagnostics = ref([])
+  const app = createApp({ render:()=>h('main',[
+    h('h1','凤鸣山谷的故事'),
+    h('p',{class:'test-note'},'草稿恢复回归 · 模拟数据 · 不调用生成服务、不修改线上课件'),
+    h(DraftPreview,{outline,templateId:'blank',message:'10页原版式未完成,完整内容已保留。',onLayoutErrors:value=>{diagnostics.value=value}}),
+    h('button',{class:'action',onClick:async()=>{
+      converted.value=aiCoursewareToSlides(outline,{templateId:'blank',recoverDraft:true});await nextTick();
+      const errors=await inspectAiPptRenderedText(converted.value.slides)
+      status.value=`已转换 ${converted.value.slides.length} 页原生可编辑草稿;${converted.value.recoveredPages.length} 页原版式问题已保留;基础排版实际文字检查:${errors.length?errors.map(e=>e.message).join(';'):'通过'}`
+    }},'验证草稿转换'),
+    h('p',{id:'conversion-status',role:'status'},status.value),
+    converted.value?h('section',{class:'editor-pages'},converted.value.slides.map((slide,index)=>h('div',[
+      h('p',`第${index+1}页 · ${slide.elements.filter(e=>e.type==='text').length}个可编辑文本元素`),
+      h(ThumbnailSlide,{slide,size:Math.min(960,innerWidth-80),viewportSize:1920,viewportRatio:0.5625})
+    ]))):null
+  ]) })
+  app.use(createPinia());app.mount('#app')
+</script></body></html>

+ 43 - 1
scripts/aiPptDraftSelfTest.mjs

@@ -234,7 +234,49 @@ try {
   assert.equal(repeated.payload.designOnly, true)
   assert.deepEqual(repeated.payload.renderFeedback, state.result.renderFeedback, '不能丢失失败页的浏览器诊断而重复普通内容生成')
 
-  console.log('PASS: 草稿预览与续作回归;模型渲染诊断仅修对应页布局、保留正文,两轮失败后保留草稿且不应用课件')
+  ;({ state, dialog } = mount())
+  state.result = { outline: editorialFixture(), context: {}, blockingMessage: '' }
+  state.renderedIssues = [{ code: 'rendered_text_overlap', pageNo: 1, severity: 'error', message: '浏览器实际正文重叠' }]
+  dialog.loadOutline(clone(state.result.outline))
+  dialog.task = { taskId: 'completed-but-unrenderable', status: 'DONE' }
+  await assert.rejects(() => dialog.enterEditor(clone(state.result), () => true))
+  assert(dialog.completionSource, '后端DONE但前端渲染失败时,完整结果仍必须可预览和打开草稿')
+  assert.equal(state.slides.slides[0].id, 'old', '不能把渲染失败的课件当成成功应用')
+  await dialog.openCompletionDraft()
+  assert.equal(state.slides.slides.length, 6, '无需重新生成即可恢复已生成的正文')
+
+  const { default: usePageRepair } = await vite.ssrLoadModule('/src/hooks/useAiCoursewarePageRepair.ts')
+  const { aiCoursewareToSlides } = await vite.ssrLoadModule('/src/utils/aiCoursewareToSlides.ts')
+  ;({ state } = mount())
+  const source = slideDesignFixture()
+  source.pages[0].pageDesign = { version: 1, elements: [] }
+  state.slides.setSlides(aiCoursewareToSlides(source, { recoverDraft: true }).slides)
+  const firstBefore = JSON.stringify(state.slides.slides[0])
+  const updated = clone(source)
+  updated.pages[1].studentContent.points[0] = '认真聆听,再表达声音的特点'
+  state.result = { outline: updated, context: {}, blockingMessage: '' }
+  state.request = async function(kind, payload) {
+    this.calls.push({ kind, payload: clone(payload) })
+    return { code: 200, data: kind === 'generate' ? { taskId: 'single-page', status: 'PENDING' }
+      : kind === 'task' ? { taskId: 'single-page', status: 'DONE' } : clone(this.result) }
+  }
+  const repairApp = renderer.createApp({ setup: () => usePageRepair(), render: () => null })
+  repairApp.mount({})
+  try {
+    const repair = repairApp._instance.setupState
+    repair.generated([], source, { minPages: 2, maxPages: 2, enableAnimations: false })
+    await repair.repair(2)
+    assert.equal(repair.error, '', '生成本页不能被另一页的坏版式阻断')
+    assert.equal(JSON.stringify(state.slides.slides[0]), firstBefore, '另一页草稿不被重排或替换')
+    assert(state.slides.slides[1].elements.some(e => e.type === 'text' && e.content.includes('认真聆听,再表达声音的特点')))
+    const targetBefore = JSON.stringify(state.slides.slides[1])
+    state.result.outline.pages[1].pageDesign.elements[1].height = 12
+    await repair.repair(2)
+    assert(repair.error, '目标页自身版式无效时,仍必须拒绝替换')
+    assert.equal(JSON.stringify(state.slides.slides[1]), targetBefore)
+  } finally { repairApp.unmount() }
+
+  console.log('PASS: DONE后渲染失败仍保留完整草稿;单页生成不受其他坏版式阻断,目标页自身无效时不替换;模型修复两轮失败保留草稿')
 } finally {
   app?.unmount()
   await vite.close()

+ 5 - 1
src/hooks/useAiCoursewarePageRepair.ts

@@ -98,7 +98,11 @@ export default function useAiCoursewarePageRepair() {
       }
       await assertAiPptClientCurrent()
       if (!active()) return
-      const converted = aiCoursewareToSlides(nextOutline, { templateId, enableAnimations: prefs.enableAnimations })
+      // 单页修复只应用目标页;其他未完成草稿不得阻断,也不得被重新应用到画布。
+      const converted = aiCoursewareToSlides(nextOutline, { templateId, enableAnimations: prefs.enableAnimations,
+        recoverDraft: !whole, fallbackPageNos: whole ? undefined : nextOutline.pages.map((_, i) => i + 1).filter(no => no !== pageNo) })
+      const targetRecovery = converted.recoveredPages.find(page => page.pageNo === pageNo)
+      if (targetRecovery) throw new Error(`${targetRecovery.error};目标页排版仍未通过,已保留当前页面。`)
       const target = converted.slides[pageNo - 1]
       const report = inspectAiPptQuality(nextOutline, converted.slides, { ...prefs, minPages: nextOutline.pages.length, maxPages: nextOutline.pages.length })
       if (blockingAiPptLayoutIssues(report).some(issue => whole || issue.pageNo === pageNo)) throw new Error('生成页的文字仍然过密,已保留当前课件,请再次精简后生成。')

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

@@ -827,6 +827,10 @@ async function enterEditor(result: AiCoursewareGenerateResult, active: () => boo
   if (!matchesRequestedTopic(result.outline)) throw new Error('生成结果与本次教材或课题不符,原课件已保留,请重新生成大纲。')
   await assertAiPptClientCurrent()
   if (!active()) return
+  // 已收到的正文先保留为草稿,任意后续渲染/资源检查失败都不能只剩错误页。
+  completionSource.value = clone(result.outline)
+  completionReviewMessage.value = result.reviewMessage || ''
+  remember()
   if (task.value?.status === 'DRAFT' && !allowIncompleteDraft) {
     if (isAdvisoryCompletion(result.blockingMessage)) {
       // 历史任务保留DRAFT状态时无需再次调用生成;让正常渲染与缺图校验决定能否进入。

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