yonge 1 هفته پیش
والد
کامیت
93c1b93f3a

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
dist/assets/index-BB0o1k4b.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
dist/assets/index-BJ5h6qFI.css


+ 1 - 1
dist/assets/index-CPZgOZAF.js → dist/assets/index-BcPIKJfq.js

@@ -1 +1 @@
-import{_ as t}from"./404-R0y5dkvZ.js";import{E as c}from"./index-DMDnLNls.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-D4K6rP7T.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-BB0o1k4b.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-DCMcs6Y_.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-CHmIuqCA.js


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

+ 1 - 1
dist/assets/index-5BeD2U-1.js → dist/assets/index-CuHZpN0g.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-D4K6rP7T.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-DCMcs6Y_.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-DCMcs6Y_.js


+ 1 - 1
dist/assets/index-B5igi-FK.js → dist/assets/index-DUGyxAXa.js

@@ -1 +1 @@
-import{u as s,ag as r}from"./index-BsLRVkES.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-D4K6rP7T.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-CgoX14-6.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-DCMcs6Y_.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};

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
dist/assets/index-VSwSeyQH.js


+ 1 - 1
dist/assets/index-BbUBOdOI.js → dist/assets/index-VYVp_B8-.js

@@ -1 +1 @@
-import{u as c,ag as l}from"./index-BsLRVkES.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-D4K6rP7T.js";import"./shapes-Br2iv8LA.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-CgoX14-6.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-DCMcs6Y_.js";import"./shapes-DN4jr45j.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/shapes-Br2iv8LA.js → dist/assets/shapes-DN4jr45j.js

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

+ 10 - 1
scripts/aiPptDialogTest.html

@@ -101,6 +101,14 @@ httpAxios.axioseRquest = async config => {
       page.bullets = [...page.studentContent.points]
       page.image = { url: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', width: 1, height: 1 }
     }
+    if (scenario.startsWith('image-completion')) {
+      data.outline.pages.slice(0, 2).forEach((page, index) => {
+        page.requiredVisual = true
+        page.visualPlan = { layout: index ? 'hero_focus' : 'cover_scene', composition: 'full_scene', visualMode: 'generated_image', textPresentation: 'keywords', visualShare: 0.65, imageNecessity: 'essential', imagePurpose: 'scene', mainVisual: '公园里的声音' }
+        page.image = index === 0 || repaired ? { url: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', width: 1, height: 1 } : undefined
+        page.imageStatus = page.image ? 'ready' : 'generation_failed'
+      })
+    }
   }
   else if (config.url.endsWith("/task/historical-failed-task")) {
     historicalPolls.value++
@@ -110,7 +118,8 @@ httpAxios.axioseRquest = async config => {
     if (scenario === 'page-repair') await new Promise(resolve => setTimeout(resolve, 1500))
     pollCount++
     if (scenario.includes("network") && pollCount === 1) throw new Error("测试网络中断")
-    data={taskId:`local-task-${generated.value}`,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") ? "测试:生成服务暂不可用" : "正在完善课堂任务"}
+    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页配图尚未完成;已保留生成结果,可继续完成课件' })
   }
   else throw new Error("测试禁止未声明的业务请求")
   return {data:{code:200,data}}

+ 4 - 0
scripts/aiPptOutlineTest.mjs

@@ -6,6 +6,10 @@ try {
   const { editableOutlinePages, confirmedCoursewareOutline, inspectConfirmedOutline } = await vite.ssrLoadModule('/src/utils/aiCoursewareOutline.ts')
   const { aiCoursewareToSlides } = await vite.ssrLoadModule('/src/utils/aiCoursewareToSlides.ts')
   const { aiCoursewareHttpError } = await vite.ssrLoadModule('/src/utils/aiCoursewareHttpError.ts')
+  const { teachingReviewIssues, blockingImageIssues } = await vite.ssrLoadModule('/src/utils/aiPptFeedback.ts')
+  assert.equal(teachingReviewIssues('整课一致性复核暂未完成,请核对跨页的角色、动作、乐器与练习安排')[0].severity, 'warning')
+  assert.equal(teachingReviewIssues('第2页:缺少听辨活动')[0].severity, 'error')
+  assert.equal(blockingImageIssues([{code:'missing_required_visual'}, {code:'resource_unavailable',message:'图片无法加载'}, {code:'missing_page_audio'}, {code:'missing_score'}]).length, 2)
   assert.match(aiCoursewareHttpError({ response: { status: 504, data: {} } }).message, /超时/)
   assert.match(aiCoursewareHttpError({ code: 'ECONNABORTED' }).message, /超时/)
   assert.match(aiCoursewareHttpError({ code: 'ERR_NETWORK' }).message, /网络/)

+ 2 - 2
scripts/aiPptSelfTest.mjs

@@ -374,7 +374,7 @@ try {
   const classroomReport = inspectAiPptQuality(classroom, classroomSlides)
   assert(!classroomReport.errors.some(issue => ["out_of_bounds", "text_overlap", "text_overflow", "rendered_text_geometry", "missing_content", "small_font"].includes(issue.code)), JSON.stringify(classroomReport.errors))
   assert.equal(classroomReport.issues.filter(issue => issue.code === "missing_required_visual").length, 6)
-  assert(classroomReport.issues.filter(issue => issue.code === "missing_required_visual").every(issue => /重新生成|补充/.test(issue.message) && !/模型说明|评分|mustInclude/.test(issue.message)))
+  assert(classroomReport.issues.filter(issue => issue.code === "missing_required_visual").every(issue => issue.severity === 'error' && /重新生成|继续完成|尚未取得/.test(issue.message) && !/手动插图|补充正确图片|模型说明|评分|mustInclude/.test(issue.message)))
   // 配图不能迫使对比、活动、总结、目标页统一分栏,也不能在保留版式后丢图。
   for (const layout of ["compare", "activity", "summary", "objectives"]) {
     for (const template of AI_COURSEWARE_TEMPLATES) {
@@ -436,7 +436,7 @@ try {
   const missingPlannedImage = structuredClone(modelFixture)
   missingPlannedImage.pages[1].image = undefined
   missingPlannedImage.pages[1].imageStatus = 'budget_exhausted'
-  assert(inspectAiPptQuality(missingPlannedImage, render(missingPlannedImage)).issues.some(issue => issue.pageNo === 2 && issue.message.includes('单独重新生成')))
+  assert(inspectAiPptQuality(missingPlannedImage, render(missingPlannedImage)).issues.some(issue => issue.pageNo === 2 && issue.severity === 'error' && issue.message.includes('继续完成课件')))
   const repeatedSlides = structuredClone(modelSlides)
   repeatedSlides[2] = structuredClone(modelSlides[1])
   repeatedSlides[3] = structuredClone(modelSlides[1])

+ 8 - 1
src/hooks/useAiCoursewarePageRepair.ts

@@ -11,7 +11,7 @@ import { getAiPptImageAspectRatios } from "@/utils/aiPptTemplateEngine"
 import { DEFAULT_AI_COURSEWARE_TEMPLATE_ID } from "@/config/aiCoursewareTemplates"
 import { inspectAiPptQuality, blockingAiPptLayoutIssues, type AiPptQualityIssue } from "@/utils/aiPptQuality"
 import { inspectAiPptResources } from "@/utils/aiPptResources"
-import { teachingReviewIssues } from '@/utils/aiPptFeedback'
+import { teachingReviewIssues, blockingImageIssues } from '@/utils/aiPptFeedback'
 
 const clone = <T,>(value: T): T => JSON.parse(JSON.stringify(value))
 
@@ -88,6 +88,11 @@ export default function useAiCoursewarePageRepair() {
       // 忽略后端返回的其他页面,保留本地原始教学安排。
       const nextOutline = clone(whole ? resultOutline : sourceOutline)
       if (!whole) nextOutline.pages[pageNo - 1] = resultOutline.pages[pageNo - 1]
+      if (draft) {
+        // 保留服务端已经完成的配图供下次续作,画布保持不变。
+        outline = nextOutline
+        throw new Error('课件尚未全部完成,已保留生成内容和成功配图,请继续完成。当前页面未被替换。')
+      }
       await assertAiPptClientCurrent()
       if (!active()) return
       const converted = aiCoursewareToSlides(nextOutline, { templateId, enableAnimations: prefs.enableAnimations })
@@ -96,6 +101,8 @@ export default function useAiCoursewarePageRepair() {
       if (blockingAiPptLayoutIssues(report).some(issue => whole || issue.pageNo === pageNo)) throw new Error('生成页的文字仍然过密,已保留当前课件,请再次精简后生成。')
       const resourceIssues = await inspectAiPptResources(whole ? nextOutline : { ...nextOutline, pages: [nextOutline.pages[pageNo - 1]] }, whole ? converted.slides : [target])
       if (!active()) return
+      const imageIssues = blockingImageIssues([...report.issues.filter(issue => whole || issue.pageNo === pageNo), ...resourceIssues])
+      if (imageIssues.length) throw new Error('配图尚未全部完成或加载失败,当前课件已保留,请继续完成。')
       const unchanged = () => whole ? JSON.stringify(slides.slides) === before : JSON.stringify(slides.slides.find(slide => slide.id === slideId)) === before
       if (!unchanged()) throw new Error("课件在生成期间已被修改或删除,未替换当前内容,避免丢失你的编辑。")
       await snapshots.addSnapshot()

+ 2 - 0
src/utils/aiCoursewareDraft.ts

@@ -16,6 +16,8 @@ export interface AiDraftRecord {
   taskId?: string
   outlineTaskId?: string
   outlineTaskSettings?: string
+  completionSource?: AiCoursewareOutline
+  completionMessage?: string
   reviewMessage?: string
   resources?: AiCoursewareResourceBinding[]
   protectedContent?: Array<[number, ProtectedPageContent]>

+ 10 - 6
src/utils/aiPptFeedback.ts

@@ -4,13 +4,13 @@ import type { AiPptQualityIssue } from '@/utils/aiPptQuality'
 /** 教师只需知道待补内容和处理方法,审核原文保留在生成结果中供开发诊断。 */
 export function imagePreparationMessage(page: AiCoursewarePage) {
   const messages: Record<string, string> = {
-    budget_exhausted: '本页图片尚未生成。可单独重新生成本页,或插入适合本课的图片。',
-    review_rejected: '候选图片与本页教学要求不符,已移除。请重新生成本页或补充正确图片。',
-    reference_missing: '本页需要准确的教材或实物图片,请补充已核实的图片。',
-    generation_failed: '本页图片生成未成功,可稍后重新生成本页或手动插图。',
+    budget_exhausted: '本页配图尚未完成,继续完成课件时将补齐。',
+    review_rejected: '本页配图审核未通过,系统需重新生成符合教学要求的图片。',
+    reference_missing: '本页所需的准确教材或实物图片尚未取得,课件尚未完成。',
+    generation_failed: '本页配图多次尝试后仍未成功,已保留内容,可继续完成课件。',
     review_unavailable: '本页图片尚未完成核对,请检查角色外观、数量与动作是否符合教学要求。'
   }
-  return messages[page.imageStatus || ''] || '请核对本页图片是否与教学内容一致;缺少图片时可重新生成本页或手动补充。'
+  return messages[page.imageStatus || ''] || '本页配图尚未完成,已保留内容,可继续完成课件。'
 }
 
 export function teacherReviewMessage(message: string) {
@@ -31,7 +31,11 @@ export function teacherReviewMessage(message: string) {
 export function teachingReviewIssues(message?: string): AiPptQualityIssue[] {
   if (!message?.trim()) return []
   return [...new Set(message.split(/;(?=第\d+页|课堂|整课|“)/).map(text => text.trim()).filter(Boolean))].map(text => ({
-    code: 'teaching-review', severity: 'error', pageNo: text.match(/^第(\d+)页/) ? Number(text.match(/^第(\d+)页/)![1]) : undefined,
+    code: 'teaching-review', severity: text === '整课一致性复核暂未完成,请核对跨页的角色、动作、乐器与练习安排' ? 'warning' : 'error', pageNo: text.match(/^第(\d+)页/) ? Number(text.match(/^第(\d+)页/)![1]) : undefined,
     message: teacherReviewMessage(text)
   }))
 }
+
+export const blockingImageIssues = (issues: AiPptQualityIssue[]) => issues.filter(issue =>
+  ['missing_required_visual', 'missing_visual_render'].includes(issue.code)
+  || (issue.code === 'resource_unavailable' && issue.message.startsWith('图片')))

+ 4 - 3
src/utils/aiPptQuality.ts

@@ -293,12 +293,13 @@ function inspectTeachingContract(outline: AiCoursewareOutline, slides: Slide[],
       }
     }
     const plannedImage = hasModelPageDesign(page) ? externalVisual(page) : ["scene", "observation", "comparison", "factual", "demonstration"].includes(page.visualPlan?.imagePurpose || "") && Boolean(page.visualPlan?.mainVisual || page.visualPlan?.mustInclude?.length)
-    const missingImage = (page.requiredVisual || plannedImage) && !usableResourceUrl(page.image?.url) && !scoreBinding(page)
-    if (missingImage) push(issues, "missing_required_visual", page.requiredVisual ? "error" : "warning", no,
+    const manualScore = /score|melody|lyric/.test(page.type || '')
+    const missingImage = !manualScore && (page.requiredVisual || plannedImage) && !usableResourceUrl(page.image?.url) && !scoreBinding(page)
+    if (missingImage) push(issues, "missing_required_visual", "error", no,
       imagePreparationMessage(page))
     if (page.imageReview && !missingImage) push(issues, "image_review", "warning", no, imagePreparationMessage(page))
     else if (page.image?.reviewStatus === "unverified") push(issues, "image_review", "warning", no, '配图尚未完成核对,请检查角色、数量、动作与课堂任务是否一致。')
-    if (page.requiredVisual && usableResourceUrl(page.image?.url) && !slide.elements.some(el => el.type === "image" && el.src === page.image?.url)) push(issues, "missing_visual_render", "error", no, "必要教学图片没有进入页面")
+    if ((page.requiredVisual || plannedImage) && usableResourceUrl(page.image?.url) && !slide.elements.some(el => el.type === "image" && el.src === page.image?.url)) push(issues, "missing_visual_render", "error", no, "教学配图没有进入页面")
     for (const binding of (page.resourceBindings || []).filter(item => item.resourceType === "VIDEO")) {
       if (binding.verified !== true || !usableResourceUrl(binding.contentUrl) || !slide.elements.some(el => el.type === "elf" && el.subtype === "elf-video" && el.src === binding.contentUrl)) push(issues, "missing_video_render", "error", no, "已选择的视频不可用或没有进入页面")
     }

+ 84 - 6
src/views/Editor/EditorHeader/AiCoursewareDialog.vue

@@ -167,9 +167,9 @@
         <section v-else class="generation" aria-live="polite">
           <template v-if="generationError"
             ><span class="error-icon">!</span>
-            <h3>{{ task ? "课件进度暂时无法继续" : "这次生成未完成" }}</h3>
+            <h3>{{ completionSource ? "课件尚未全部完成" : task ? "课件进度暂时无法继续" : "这次生成未完成" }}</h3>
             <p>{{ generationError }}</p>
-            <p class="hint">已保留确认的大纲,当前课件没有被替换。</p></template
+            <p class="hint">{{ completionSource ? '已保留生成内容和成功配图,可接着完成;当前课件没有被替换。' : '已保留确认的大纲,当前课件没有被替换。' }}</p></template
           ><template v-else
             ><span class="spinner"></span>
             <h3>正在生成课件</h3>
@@ -193,7 +193,7 @@
         </div>
         <div v-else-if="generationError">
           <Button v-if="!task" @click="step = 2">返回修改大纲</Button
-          ><Button type="primary" :disabled="transferring" @click="retry">{{ layoutRepairSource ? "精简并重新排版" : task ? "继续查看课件" : "重试生成" }}</Button>
+          ><Button type="primary" :disabled="transferring" @click="retry">{{ completionSource ? "继续完成课件" : layoutRepairSource ? "精简并重新排版" : task ? "继续查看课件" : "重试生成" }}</Button>
         </div>
       </footer>
     </div>
@@ -228,7 +228,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 } from '@/utils/aiPptFeedback'
+import { teachingReviewIssues, blockingImageIssues } from '@/utils/aiPptFeedback'
 import { templateAssetPath } from "@/utils/templateAssetUrl"
 import { draftKey, loadAiDraft, saveAiDraft } from "@/utils/aiCoursewareDraft"
 import {
@@ -280,6 +280,7 @@ const outlineLoading = ref(false),
 const task = ref<AiCoursewareTaskInfo>(),
   confirmed = ref<AiCoursewareOutline>()
 const layoutRepairSource = ref<AiCoursewareOutline>()
+const completionSource = ref<AiCoursewareOutline>()
 const outlineTask = ref<AiCoursewareTaskInfo>(), outlineTaskSettings = ref('')
 let pollTimer: ReturnType<typeof setTimeout> | undefined,
   version = 0,
@@ -331,6 +332,7 @@ watch(
     outlineTaskSettings.value = ''
     confirmed.value = undefined
     layoutRepairSource.value = undefined
+    completionSource.value = undefined
     outline.value = undefined
     pages.value = []
     extraPrompt.value = ""
@@ -358,6 +360,8 @@ function remember() {
     taskId = task.value?.taskId,
     outlineTaskId = outlineTask.value?.taskId,
     pendingOutlineSettings = outlineTaskSettings.value,
+    pendingCompletion = completionSource.value ? clone(completionSource.value) : undefined,
+    completionMessage = generationError.value,
     savedSettings = outlineSettings.value
   if (!id) return
   saveQueue = saveQueue
@@ -371,6 +375,8 @@ function remember() {
         taskId,
         outlineTaskId,
         outlineTaskSettings: pendingOutlineSettings,
+        completionSource: pendingCompletion,
+        completionMessage,
         outlineSettings: savedSettings
       })
     })
@@ -408,6 +414,15 @@ async function restore() {
       outlineTaskSettings.value = saved.outlineTaskSettings || settingsSignature()
       step.value = 1
       await pollOutlineTask()
+    } else if (saved.completionSource) {
+      confirmed.value = currentPlan()
+      completionSource.value = clone(saved.completionSource)
+      generationError.value = saved.completionMessage || '课件尚未全部完成,可接着完成。'
+      step.value = 3
+      if (saved.taskId) {
+        task.value = { taskId: saved.taskId, status: 'RUNNING', progress: 0, message: '正在恢复课件进度' }
+        await pollTask()
+      }
     } else if (saved.taskId && outline.value) {
       confirmed.value = currentPlan()
       task.value = { taskId: saved.taskId, status: "RUNNING", progress: 0, message: "正在获取生成进度" }
@@ -556,6 +571,7 @@ async function confirmAndGenerate() {
 async function startGeneration() {
   if (!confirmed.value || !pptWork.id || transferring.value || task.value) return
   layoutRepairSource.value = undefined
+  completionSource.value = undefined
   const requestVersion = ++version
   step.value = 3
   generationError.value = ""
@@ -589,6 +605,11 @@ async function startGeneration() {
   if (props.visible && task.value) await pollTask()
 }
 async function retry() {
+  if (completionSource.value) {
+    if (task.value && ['PENDING', 'RUNNING'].includes(task.value.status)) { generationError.value = ''; await pollTask() }
+    else await continueCompletion()
+    return
+  }
   if (layoutRepairSource.value) { await repairBlockedLayout(); return }
   generationError.value = ""
   if (task.value) await pollTask()
@@ -623,8 +644,35 @@ async function repairBlockedLayout() {
   }
   if (requestVersion === version && !generationError.value && props.visible) await pollTask()
 }
+async function continueCompletion() {
+  if (transferring.value || !completionSource.value) return
+  const requestVersion = ++version
+  stopPolling()
+  transferring.value = true
+  generationError.value = ''
+  try {
+    await assertAiPptClientCurrent()
+    if (requestVersion !== version) return
+    const plan = clone(completionSource.value)
+    const response = await httpAjax(aiCoursewareGenerateApi, {
+      id: pptWork.id, fromType: source(), outline: plan, templateId: templateId.value, repairOnly: true,
+      imageAspectRatios: getAiPptImageAspectRatios(templateId.value),
+      preferences: { ...clone(preferences), minPages: plan.pages.length, maxPages: plan.pages.length }
+    })
+    if (requestVersion !== version) return
+    if (response.code !== 200 || !response.data?.taskId) throw new Error(response.message || '暂时无法继续完成课件,请重试')
+    task.value = response.data
+    remember()
+  } catch (error) {
+    if (requestVersion === version) generationError.value = error instanceof Error ? error.message : '暂时无法继续完成课件'
+  } finally {
+    if (requestVersion === version) transferring.value = false
+  }
+  if (requestVersion === version && task.value && !generationError.value && props.visible) await pollTask()
+}
 async function pollTask() {
   stopPolling()
+  generationError.value = ''
   const taskId = task.value?.taskId,
     requestVersion = version,
     currentPoll = pollVersion
@@ -633,6 +681,12 @@ async function pollTask() {
   try {
     const response = await httpAjax(aiCoursewareTaskApi, taskId)
     if (!active()) return
+    if (completionSource.value && response.code !== 200 && /任务不存在|任务已过期/.test(response.message || '')) {
+      task.value = undefined
+      generationError.value = '任务记录已过期,已保留生成内容,可继续完成课件。'
+      remember()
+      return
+    }
     if (response.code !== 200) throw new Error(response.message || "暂时无法获取进度,请稍后继续查看")
     task.value = response.data
     if (task.value?.status === "FAILED") {
@@ -664,6 +718,13 @@ async function enterEditor(result: AiCoursewareGenerateResult, active: () => boo
   if (!result?.outline?.pages?.length) throw new Error("尚未获得课件内容,请继续查看")
   await assertAiPptClientCurrent()
   if (!active()) return
+  if (task.value?.status === 'DRAFT') {
+    completionSource.value = clone(result.outline)
+    generationError.value = task.value.message || '课件尚未全部完成,可接着完成。'
+    remember()
+    return
+  }
+  completionSource.value = undefined
   layoutRepairSource.value = clone(result.outline)
   let converted: ReturnType<typeof aiCoursewareToSlides>
   try {
@@ -686,8 +747,24 @@ async function enterEditor(result: AiCoursewareGenerateResult, active: () => boo
     ...(await inspectAiPptResources(result.outline, converted.slides))
   ]
   issues.unshift(...teachingReviewIssues(result.reviewMessage))
-  const draft = task.value?.status === 'DRAFT' || issues.some(issue => issue.severity === 'error')
-  if (draft && !issues.some(issue => issue.severity === 'error')) issues.unshift({ code: 'teaching-review', severity: 'error', message: '课堂内容尚未通过复核,请继续完善草稿。' })
+  const imageIssues = blockingImageIssues(issues)
+  if (imageIssues.length) {
+    if (!active()) return
+    completionSource.value = clone(result.outline)
+    // 地址存在但实际加载失败的图片不能在续作时误当作成功配图复用。
+    imageIssues.filter(issue => issue.code === 'resource_unavailable').forEach(issue => {
+      const page = completionSource.value?.pages.find(page => page.pageNo === issue.pageNo)
+      if (page) {
+        const failedUrl = page.image?.url
+        page.image = undefined; page.imageStatus = 'generation_failed'
+        page.resourceBindings = page.resourceBindings?.filter(binding => binding.resourceType !== 'IMG' || binding.contentUrl !== failedUrl)
+      }
+    })
+    generationError.value = `第${[...new Set(imageIssues.map(issue => issue.pageNo))].join('、')}页配图尚未完成,已保留生成内容。`
+    remember()
+    return
+  }
+  const draft = issues.some(issue => issue.severity === 'error')
   if (!active()) return
   if (
     replacementSnapshot !== editorSnapshot() &&
@@ -722,6 +799,7 @@ async function enterEditor(result: AiCoursewareGenerateResult, active: () => boo
     await snapshots.addSnapshot()
     pptWork.isSave = false
     task.value = undefined
+    completionSource.value = undefined
     generationError.value = ""
     step.value = 2
     remember()

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است