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

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


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


+ 1 - 1
dist/assets/index-BdVynuBN.js → dist/assets/index-DOGzR9zm.js

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

+ 1 - 1
dist/assets/index-CRZ2xJMi.js → dist/assets/index-DQMEoHCs.js

@@ -1 +1 @@
-import{_ as a}from"./404-R0y5dkvZ.js";import{a5 as o,eo as t,ae as r,af as c,bP as _,bQ as d,ak as s,bH as p}from"./index-DH7_VOUf.js";const i=e=>(_("data-v-dda25179"),e=e(),d(),e),n={class:"errorPage"},m=i(()=>s("div",{class:"error"},[s("img",{src:a,class:"img",alt:""}),s("div",{class:"tit"},"页面找不到了~")],-1)),l=[m],f=o({__name:"errorPage",setup(e){return t(),(u,h)=>(r(),c("div",n,l))}}),I=p(f,[["__scopeId","data-v-dda25179"]]);export{I as default};
+import{_ as a}from"./404-R0y5dkvZ.js";import{a5 as o,eo as t,ae as r,af as c,bP as _,bQ as d,ak as s,bH as p}from"./index-Dr9yIelq.js";const i=e=>(_("data-v-dda25179"),e=e(),d(),e),n={class:"errorPage"},m=i(()=>s("div",{class:"error"},[s("img",{src:a,class:"img",alt:""}),s("div",{class:"tit"},"页面找不到了~")],-1)),l=[m],f=o({__name:"errorPage",setup(e){return t(),(u,h)=>(r(),c("div",n,l))}}),I=p(f,[["__scopeId","data-v-dda25179"]]);export{I as default};

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


+ 1 - 1
dist/assets/index-DhfYccPL.js → dist/assets/index-DiOeXnXo.js

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

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


+ 1 - 1
dist/assets/index-XP6acjPQ.js → dist/assets/index-XNZ3x88t.js

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

@@ -1 +1 @@
-import{_ as t}from"./404-R0y5dkvZ.js";import{E as c}from"./index-CfXkJUZO.js";import{a5 as n,ae as i,af as r,ak as o,aq as _,am as d,P as l,aS as p,bP as m,bQ as f,bH as u}from"./index-DH7_VOUf.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-CmIQ720b.js";import{a5 as n,ae as i,af as r,ak as o,aq as _,am as d,P as l,aS as p,bP as m,bQ as f,bH as u}from"./index-Dr9yIelq.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-avtaqPKf.js


+ 1 - 1
dist/assets/shapes-DOZLoOQa.js → dist/assets/shapes-DFq5iAv_.js

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

+ 9 - 0
scripts/aiPptDialogTest.html

@@ -98,6 +98,14 @@ httpAxios.axioseRquest = async config => {
       data.reviewMessage = '第2页:建议增加同伴交流;建议:先讨论再举手'
       data.blockingMessage = ''
     }
+    if (scenario.endsWith('step-advice')) {
+      const page = data.outline.pages[7]
+      page.type = 'performance_activity'
+      page.visualPlan = { layout: 'activity', composition: 'steps', visualMode: 'text_only', textPresentation: 'steps', imagePurpose: 'none', imageNecessity: 'none' }
+      page.studentContent = { points: ['第一组轻拍手', '第二组重拍手'], steps: ['各组先练习自己的声音', '看指挥轮流进入', '握拳时一起停止', '交换分工再试一次'] }
+      data.blockingMessage = '第8页:学生操作步骤过多,请合并同一动作,将教师组织细节放入备注,节奏与例子放points'
+      data.reviewMessage = data.blockingMessage
+    }
     if (scenario === 'completion-blocked') {
       data.reviewMessage = '第2页:建议增加同伴交流'
       data.blockingMessage = '第5页:故事内容未完成,请提供本页可讲述的storyScript'
@@ -132,6 +140,7 @@ httpAxios.axioseRquest = async config => {
     data={taskId:config.url.split("/").pop(),status:scenario.includes("failure") && generated.value === 1 ? "FAILED" : scenario.includes("slow") && pollCount < 6 ? "RUNNING" : scenario.includes("draft") ? "DRAFT" : "DONE",progress:pollCount < 6 && scenario.includes("slow") ? 50 : 100,message:scenario.includes("failure") ? "测试:生成服务暂不可用" : "正在完善课堂任务"}
     if (scenario === 'image-completion' && !repaired) Object.assign(data, { status: 'DRAFT', progress: 95, message: '第2页配图尚未完成;已保留生成结果,可继续完成课件' })
     if (scenario === 'completion-blocked') Object.assign(data, { status: 'DRAFT', progress: 95, message: '课堂内容尚未完成修正' })
+    if (scenario.endsWith('step-advice')) Object.assign(data, { status: 'DRAFT', progress: 95, message: '第8页:学生操作步骤过多' })
   }
   else throw new Error("测试禁止未声明的业务请求")
   return {data:{code:200,data}}

+ 7 - 1
scripts/aiPptSelfTest.mjs

@@ -375,8 +375,14 @@ try {
   assert.deepEqual(responsePageContent(repairedInstruction).steps, [completeStep, '听到结束信号后停下'], '续作修复后保留完整步骤和顺序')
   assert.equal(responsePageContent(repairedInstruction).task, '', '同一句任务不在步骤上方重复绘制')
   assert.equal(responsePageContent(repairedInstruction).heading, '', '步骤不能被移到标题区')
-  const { coursewareCompletionMessage: completionMessage } = await vite.ssrLoadModule('/src/utils/aiPptFeedback.ts')
+  const { coursewareCompletionMessage: completionMessage, isAdvisoryCompletion } = await vite.ssrLoadModule('/src/utils/aiPptFeedback.ts')
   assert(completionMessage({ blockingMessage: '第1页配图尚未完成(图片生成配置不完整)' }).includes('请管理员恢复配图服务'), '配置错误必须解释为何重试无效')
+  const stepAdvice = '第8页:学生操作步骤过多,请合并同一动作,将教师组织细节放入备注,节奏与例子放points'
+  assert(isAdvisoryCompletion(stepAdvice), '旧草稿仅有步骤数量建议时应能打开已有课件')
+  assert(isAdvisoryCompletion(`${stepAdvice};第2页:学生文字超过180字,请精简`))
+  for (const blocking of ['', '第8页:内容尚未生成', '第1页配图尚未完成', '第8页:教学范例错误', `${stepAdvice};第8页:缺少具体教学示范`])
+    assert(!isAdvisoryCompletion(blocking), '不能用密度建议掩盖必需内容或图片缺失')
+  assert(!teacherReviewMessage(stepAdvice).includes('points'), '教师提示不能泄露内部字段名')
   const withVideo = structuredClone(valid)
   withVideo.pages[7].resourceBindings = [{resourceType:"VIDEO",resourceId:"test-video",contentUrl:"https://example.com/test.mp4",verified:true}]
   assert(render(withVideo)[7].elements.some(element => element.subtype === "elf-video"))

+ 9 - 2
src/utils/aiPptFeedback.ts

@@ -25,11 +25,18 @@ export function teacherReviewMessage(message: string) {
     'visualPlan.composition': '页面构图', 'visualPlan': '页面设计', 'composition': '页面构图',
     'visualMode': '图片或图示形式', 'visualShare': '主图比例', 'textPresentation': '文字排版',
     'imageNecessity': '配图用途', 'imageReason': '配图理由', 'essential': '教学必需',
-    'headline': '引导问题', 'studentTask': '学生任务', 'objectiveIds': '教学目标关联'
+    'headline': '引导问题', 'studentTask': '学生任务', 'objectiveIds': '教学目标关联',
+    'points': '正文要点', 'steps': '操作步骤'
   }
   return Object.entries(names).reduce((text, [key, value]) => text.split(key).join(value), message)
 }
 
+/** 兼容旧任务:仅明确的密度/表达建议可以继续打开;仍要经过实际渲染与资源校验。 */
+export function isAdvisoryCompletion(message?: string) {
+  const issues = message?.trim().split(/[;;]/).map(text => text.trim()).filter(Boolean) || []
+  return issues.length > 0 && issues.every(text => /^第\d+页[::]\s*(学生操作步骤过多|学生步骤过长|学生文字超过180字|学生画面的任务、步骤或材料存在重复|教师演示过程混入学生范例|节奏示例混入步骤编号|低年级指令过于抽象)/.test(text))
+}
+
 export function teachingReviewIssues(message?: string, blockingMessage?: string): AiPptQualityIssue[] {
   const split = (value?: string) => value?.split(/;(?=第\d+页|课堂|整课|“)/).map(text => text.trim()).filter(Boolean) || []
   const blocking = split(blockingMessage)
@@ -47,7 +54,7 @@ export function coursewareCompletionMessage(result: Pick<AiCoursewareGenerateRes
   if (detail && /配置不完整|配置关闭|配置不受支持/.test(detail))
     return teacherReviewMessage(`${detail};已保留课件内容,请管理员恢复配图服务后再继续完成,重复点击不会修复配置。`)
   return teacherReviewMessage(detail
-    ? `${detail};已保留生成内容和成功配图,可继续完成课件。`
+    ? detail
     : taskMessage || '课件尚未全部完成,可接着完成。')
 }
 

+ 57 - 9
src/views/Editor/EditorHeader/AiCoursewareDialog.vue

@@ -167,9 +167,30 @@
         <section v-else class="generation" aria-live="polite">
           <template v-if="generationError"
             ><span class="error-icon">!</span>
-            <h3>{{ completionSource ? "课件尚未全部完成" : task ? "课件进度暂时无法继续" : "这次生成未完成" }}</h3>
+            <h3>{{ completionSource ? `已保留 ${completionSource.pages.length} 页,部分内容待完善` : task ? "课件进度暂时无法继续" : "这次生成未完成" }}</h3>
             <p>{{ generationError }}</p>
-            <p class="hint">{{ completionSource ? '已保留生成内容和成功配图,可接着完成;当前课件没有被替换。' : '已保留确认的大纲,当前课件没有被替换。' }}</p></template
+            <p class="hint">{{ completionSource ? /配置不完整|配置关闭|配置不受支持/.test(generationError) ? '可以先查看已生成内容;配图服务恢复后再点击“继续自动完善”。' : '可以先查看已生成内容,再点击“继续自动完善”。系统会沿用当前结果处理,当前课件没有被替换。' : '已保留确认的大纲,当前课件没有被替换。' }}</p>
+            <div v-if="completionSource && showCompletionContent" class="completion-content">
+              <h4>已生成内容</h4>
+              <p>这里可查看各页文字和教学备注。</p>
+              <details v-for="page in completionSource.pages" :key="page.pageNo">
+                <summary>第 {{ page.pageNo }} 页 · {{ page.title }}</summary>
+                <p v-if="page.studentContent?.headline">{{ page.studentContent.headline }}</p>
+                <p v-if="page.classroomAction?.studentTask">{{ page.classroomAction.studentTask }}</p>
+                <ul><li v-for="(point, index) in (page.studentContent?.points || page.bullets || [])" :key="index">{{ point }}</li></ul>
+                <ol><li v-for="(item, index) in page.studentContent?.steps" :key="index">{{ item }}</li></ol>
+                <p v-if="page.studentContent?.example">示例:{{ page.studentContent.example }}</p>
+                <div v-for="(group, index) in page.studentContent?.comparisonGroups" :key="`group-${index}`">
+                  <p>{{ group.label }}</p>
+                  <ul><li v-for="(point, pointIndex) in group.points" :key="pointIndex">{{ point }}</li></ul>
+                </div>
+                <p v-if="page.interaction?.question">课堂提问:{{ page.interaction.question }}</p>
+                <ul><li v-for="(option, index) in page.interaction?.options" :key="index">{{ option }}</li></ul>
+                <p v-if="page.classroomAction?.modelExample" class="completion-notes">教学示范:{{ page.classroomAction.modelExample }}</p>
+                <p v-if="page.classroomAction?.storyScript" class="completion-notes">故事讲稿:{{ page.classroomAction.storyScript }}</p>
+                <p v-if="page.speakerNotes || page.teacherTips" class="completion-notes">教学备注:{{ page.speakerNotes || page.teacherTips }}</p>
+              </details>
+            </div></template
           ><template v-else
             ><span class="spinner"></span>
             <h3>正在生成课件</h3>
@@ -192,8 +213,9 @@
           ><Button type="primary" :disabled="!reliableResearch || transferring" @click="confirmAndGenerate">确认大纲并生成</Button>
         </div>
         <div v-else-if="generationError">
+          <Button v-if="completionSource" :aria-expanded="showCompletionContent" @click="showCompletionContent = !showCompletionContent">{{ showCompletionContent ? "收起已生成内容" : "查看已生成内容" }}</Button>
           <Button v-if="!task" @click="step = 2">返回修改大纲</Button
-          ><Button type="primary" :disabled="transferring" @click="retry">{{ completionSource ? "继续完成课件" : layoutRepairSource ? "精简并重新排版" : task ? "继续查看课件" : "重试生成" }}</Button>
+          ><Button type="primary" :disabled="transferring" @click="retry">{{ completionSource ? "继续自动完善" : layoutRepairSource ? "精简并重新排版" : task ? "继续查看课件" : "重试生成" }}</Button>
         </div>
       </footer>
     </div>
@@ -228,7 +250,7 @@ import { aiCoursewareToSlides } from "@/utils/aiCoursewareToSlides"
 import { getAiPptImageAspectRatios } from "@/utils/aiPptTemplateEngine"
 import { inspectAiPptQuality, type AiPptQualityIssue } from "@/utils/aiPptQuality"
 import { inspectAiPptResources } from "@/utils/aiPptResources"
-import { teachingReviewIssues, blockingImageIssues, coursewareCompletionMessage } from '@/utils/aiPptFeedback'
+import { teachingReviewIssues, blockingImageIssues, coursewareCompletionMessage, isAdvisoryCompletion } from '@/utils/aiPptFeedback'
 import { templateAssetPath } from "@/utils/templateAssetUrl"
 import { draftKey, loadAiDraft, saveAiDraft } from "@/utils/aiCoursewareDraft"
 import {
@@ -281,6 +303,8 @@ const task = ref<AiCoursewareTaskInfo>(),
   confirmed = ref<AiCoursewareOutline>()
 const layoutRepairSource = ref<AiCoursewareOutline>()
 const completionSource = ref<AiCoursewareOutline>()
+const showCompletionContent = ref(false)
+watch(completionSource, () => { showCompletionContent.value = false })
 const outlineTask = ref<AiCoursewareTaskInfo>(), outlineTaskSettings = ref('')
 let pollTimer: ReturnType<typeof setTimeout> | undefined,
   version = 0,
@@ -606,7 +630,8 @@ async function startGeneration() {
 }
 async function retry() {
   if (completionSource.value) {
-    if (task.value && ['PENDING', 'RUNNING'].includes(task.value.status)) { generationError.value = ''; await pollTask() }
+    if (task.value && (['PENDING', 'RUNNING'].includes(task.value.status)
+        || isAdvisoryCompletion(generationError.value.split(';已保留')[0]))) { generationError.value = ''; await pollTask() }
     else await continueCompletion()
     return
   }
@@ -719,10 +744,15 @@ async function enterEditor(result: AiCoursewareGenerateResult, active: () => boo
   await assertAiPptClientCurrent()
   if (!active()) return
   if (task.value?.status === 'DRAFT') {
-    completionSource.value = clone(result.outline)
-    generationError.value = coursewareCompletionMessage(result, task.value.message)
-    remember()
-    return
+    if (isAdvisoryCompletion(result.blockingMessage)) {
+      // 历史任务保留DRAFT状态时无需再次调用生成;让正常渲染与缺图校验决定能否进入。
+      result = { ...result, reviewMessage: [result.reviewMessage, result.blockingMessage].filter(Boolean).join(';'), blockingMessage: '' }
+    } else {
+      completionSource.value = clone(result.outline)
+      generationError.value = coursewareCompletionMessage(result, task.value.message)
+      remember()
+      return
+    }
   }
   completionSource.value = undefined
   layoutRepairSource.value = clone(result.outline)
@@ -1073,6 +1103,24 @@ button:focus-visible {
 .generation p {
   max-width: 650px;
 }
+.completion-content {
+  width: 100%;
+  max-width: 850px;
+  margin-top: 20px;
+  padding: 18px;
+  text-align: left;
+  background: #fff;
+  border: 1px solid #e2e9f1;
+  border-radius: 8px;
+}
+.completion-content details { padding: 12px 0; border-top: 1px solid #e2e9f1; }
+.completion-content summary { cursor: pointer; font-weight: 600; }
+.completion-content p { max-width: none; white-space: pre-wrap; }
+.completion-content ul, .completion-content ol { padding-left: 22px; }
+.completion-content ul { list-style: disc; }
+.completion-content ol { list-style: decimal; }
+.completion-content li { margin: 7px 0; }
+.completion-notes { color: #65758a; }
 progress {
   width: 320px;
   max-width: 100%;

+ 1 - 1
src/views/Editor/EditorHeader/index.vue

@@ -77,7 +77,7 @@
                 <button v-if="issue.pageNo" @click="locatePage(issue.pageNo)">
                   第 {{ issue.pageNo }} 页</button
                 >{{ issue.message }}
-                <button v-if="issue.pageNo && (issue.severity === 'error' || ['missing_required_visual', 'image_review', 'resource_unavailable'].includes(issue.code))" :disabled="repairingPage !== undefined" @click="repairPage(issue.pageNo)">
+                <button v-if="issue.pageNo && (issue.severity === 'error' || ['teaching-review', 'missing_required_visual', 'image_review', 'resource_unavailable'].includes(issue.code))" :disabled="repairingPage !== undefined" @click="repairPage(issue.pageNo)">
                   {{ repairingPage === issue.pageNo ? '正在生成…' : '重新生成本页' }}
                 </button>
               </li>

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