|
@@ -62,10 +62,6 @@
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div class="extra">
|
|
<div class="extra">
|
|
|
- <label class="label">本节课范围</label>
|
|
|
|
|
- <TextArea :value="preferences.lessonScope || ''" @update:value="preferences.lessonScope = $event" :rows="2" placeholder="例如:第一课时,重点学习第一段,不安排整曲演唱" :disabled="working" />
|
|
|
|
|
- <label class="label">学生基础与课堂条件</label>
|
|
|
|
|
- <TextArea :value="preferences.learningContext || ''" @update:value="preferences.learningContext = $event" :rows="2" placeholder="例如:一年级,尚不识谱,有打击乐器,需离线播放" :disabled="working" />
|
|
|
|
|
<div class="label">还有什么特别要求?<span>选填</span></div>
|
|
<div class="label">还有什么特别要求?<span>选填</span></div>
|
|
|
<TextArea v-model:value="extraPrompt" :rows="3" placeholder="例如:多安排节奏游戏,语言适合一年级学生" :disabled="working" />
|
|
<TextArea v-model:value="extraPrompt" :rows="3" placeholder="例如:多安排节奏游戏,语言适合一年级学生" :disabled="working" />
|
|
|
</div>
|
|
</div>
|
|
@@ -74,6 +70,10 @@
|
|
|
{{ advancedSettingsExpanded ? "收起高级设置" : "高级设置" }}
|
|
{{ advancedSettingsExpanded ? "收起高级设置" : "高级设置" }}
|
|
|
</button>
|
|
</button>
|
|
|
<div v-if="advancedSettingsExpanded" class="generation-preferences advanced-settings">
|
|
<div v-if="advancedSettingsExpanded" class="generation-preferences advanced-settings">
|
|
|
|
|
+ <label class="label">本节课范围</label>
|
|
|
|
|
+ <TextArea :value="preferences.lessonScope || ''" @update:value="preferences.lessonScope = $event" :rows="2" placeholder="例如:第一课时,重点学习第一段" :disabled="working" />
|
|
|
|
|
+ <label class="label">学生基础与课堂条件</label>
|
|
|
|
|
+ <TextArea :value="preferences.learningContext || ''" @update:value="preferences.learningContext = $event" :rows="2" placeholder="例如:尚不识谱,有打击乐器" :disabled="working" />
|
|
|
<div class="preference-grid">
|
|
<div class="preference-grid">
|
|
|
<label
|
|
<label
|
|
|
><span>素材</span
|
|
><span>素材</span
|
|
@@ -101,9 +101,10 @@
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div v-else-if="workflowStep === 2 && outline" class="outline unified-review">
|
|
<div v-else-if="workflowStep === 2 && outline" class="outline unified-review">
|
|
|
- <details class="teaching-review" :open="!outlineConfirmed">
|
|
|
|
|
|
|
+ <details v-if="teachingDetailsExpanded || !outlineConfirmed" class="teaching-review" open>
|
|
|
<summary>教材依据与教学主线</summary>
|
|
<summary>教材依据与教学主线</summary>
|
|
|
- <p>{{ reliableResearch ? "已检索到匹配教材的依据,请确认本课教学范围。" : "教材尚未精确匹配,暂不能生成授课成品。请返回核对课程信息后重新检索。" }}</p>
|
|
|
|
|
|
|
+ <p>{{ reliableResearch ? "已检索到匹配教材的依据,可在这里查看教学安排。" : "教材尚未精确匹配,暂不能生成授课成品。请返回核对课程信息后重新检索。" }}</p>
|
|
|
|
|
+ <p v-if="displayedConverted">必讲内容 {{ plannedMinutes }} 分钟 · 课时设置 {{ preferences.lessonMinutes }} 分钟</p>
|
|
|
<p>{{ outline.textbookResearch?.contentSummary }}</p>
|
|
<p>{{ outline.textbookResearch?.contentSummary }}</p>
|
|
|
<p v-for="item in outline.textbookResearch?.missingEvidence || []" :key="item">待核对:{{ item }}</p>
|
|
<p v-for="item in outline.textbookResearch?.missingEvidence || []" :key="item">待核对:{{ item }}</p>
|
|
|
<a v-for="source in researchSources" :key="source.url" :href="source.url" target="_blank" rel="noopener noreferrer">{{ source.title || "教材来源" }}</a>
|
|
<a v-for="source in researchSources" :key="source.url" :href="source.url" target="_blank" rel="noopener noreferrer">{{ source.title || "教材来源" }}</a>
|
|
@@ -116,22 +117,25 @@
|
|
|
</label>
|
|
</label>
|
|
|
</div>
|
|
</div>
|
|
|
</details>
|
|
</details>
|
|
|
- <section v-if="displayedConverted && draftQuality" class="quality-panel" aria-live="polite">
|
|
|
|
|
- <strong>{{ draftQuality.passed ? "结构与资源检查通过 · 请试讲确认" : `待修正 ${draftQuality.errors.length} 项,暂不能作为授课成品应用` }}</strong>
|
|
|
|
|
- <span>必讲 {{ plannedMinutes }} / {{ preferences.lessonMinutes }} 分钟</span>
|
|
|
|
|
- <details v-if="draftQuality.issues.length" :open="!draftQuality.passed">
|
|
|
|
|
- <summary>查看检查结果(规则检查,不代表教学准确率)</summary>
|
|
|
|
|
- <button v-for="(issue, index) in draftQuality.issues" :key="index" type="button" @click="issue.pageNo && selectWorkspacePage(issue.pageNo - 1)">
|
|
|
|
|
- {{ issue.severity === 'error' ? '需修正' : '建议' }} · {{ issue.pageNo ? `第${issue.pageNo}页:` : '' }}{{ issue.message }}
|
|
|
|
|
- </button>
|
|
|
|
|
- </details>
|
|
|
|
|
|
|
+ <div v-if="displayedConverted && working" class="loading-strip" aria-live="polite"><span class="loading-dot"></span>正在完善课件,已有内容会保留</div>
|
|
|
|
|
+ <section v-else-if="displayedConverted && (draftQuality?.errors.length || serverReviewMessage)" class="draft-notice" aria-live="polite">
|
|
|
|
|
+ <span>初稿已保留,{{ draftQuality?.errors.length ? `有${draftQuality.errors.length}处待完善` : '还有内容待复核' }}</span>
|
|
|
|
|
+ <button type="button" @click="qualityDetailsExpanded = !qualityDetailsExpanded">{{ qualityDetailsExpanded ? '收起' : '查看问题' }}</button>
|
|
|
|
|
+ <Button :disabled="working" @click="repairDraft">自动完善</Button>
|
|
|
</section>
|
|
</section>
|
|
|
|
|
+ <div v-if="qualityDetailsExpanded && displayedConverted" class="quality-panel">
|
|
|
|
|
+ <p v-if="serverReviewMessage">{{ serverReviewMessage }}</p>
|
|
|
|
|
+ <button v-for="(issue, index) in draftQuality?.errors || []" :key="index" type="button" @click="issue.pageNo && selectWorkspacePage(issue.pageNo - 1)">
|
|
|
|
|
+ {{ issue.pageNo ? `第${issue.pageNo}页:` : '' }}{{ issue.message }}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
<div class="sample-stage-head">
|
|
<div class="sample-stage-head">
|
|
|
<div class="sample-template-current">
|
|
<div class="sample-template-current">
|
|
|
<span>当前样式</span>
|
|
<span>当前样式</span>
|
|
|
<strong>{{ selectedTemplate.name }}</strong>
|
|
<strong>{{ selectedTemplate.name }}</strong>
|
|
|
<small v-if="selectedTemplate.id === recommendedTemplate.id">系统推荐</small>
|
|
<small v-if="selectedTemplate.id === recommendedTemplate.id">系统推荐</small>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <button type="button" @click="teachingDetailsExpanded = !teachingDetailsExpanded">{{ teachingDetailsExpanded ? '收起说明' : '教学说明' }}</button>
|
|
|
<button type="button" :disabled="working" @click="templatePickerExpanded = !templatePickerExpanded">
|
|
<button type="button" :disabled="working" @click="templatePickerExpanded = !templatePickerExpanded">
|
|
|
{{ templatePickerExpanded ? "收起模板" : "更换模板" }}
|
|
{{ templatePickerExpanded ? "收起模板" : "更换模板" }}
|
|
|
</button>
|
|
</button>
|
|
@@ -164,9 +168,9 @@
|
|
|
<div v-if="!workspaceSlide && outlineConfirmed" :class="['final-preview-placeholder', draftPreparationState]">
|
|
<div v-if="!workspaceSlide && outlineConfirmed" :class="['final-preview-placeholder', draftPreparationState]">
|
|
|
<span v-if="draftPreparationState === 'optimizing'" class="loading-dot"></span>
|
|
<span v-if="draftPreparationState === 'optimizing'" class="loading-dot"></span>
|
|
|
<span v-else class="placeholder-icon">!</span>
|
|
<span v-else class="placeholder-icon">!</span>
|
|
|
- <strong>{{ draftPreparationState === "failed" ? "课件生成失败" : generationStatusTitle }}</strong>
|
|
|
|
|
|
|
+ <strong>{{ draftPreparationState === "failed" ? "这次生成未完成" : generationStatusTitle }}</strong>
|
|
|
<small>{{ draftPreparationState === "failed" ? draftFailureMessage : "完成后可直接预览、编辑和应用" }}</small>
|
|
<small>{{ draftPreparationState === "failed" ? draftFailureMessage : "完成后可直接预览、编辑和应用" }}</small>
|
|
|
- <Button v-if="draftPreparationState === 'failed'" type="primary" @click="regenerateFinalPreview">重新生成</Button>
|
|
|
|
|
|
|
+ <Button v-if="draftPreparationState === 'failed'" type="primary" @click="regenerateFinalPreview">重试生成</Button>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div v-if="workspaceSlide" class="unified-review-workspace">
|
|
<div v-if="workspaceSlide" class="unified-review-workspace">
|
|
@@ -192,10 +196,10 @@
|
|
|
</div>
|
|
</div>
|
|
|
<small>{{ canEditWorkspaceText ? "双击预览中的文字即可编辑" : "系统处理完成后可直接编辑文字" }}</small>
|
|
<small>{{ canEditWorkspaceText ? "双击预览中的文字即可编辑" : "系统处理完成后可直接编辑文字" }}</small>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="workspace-canvas-shell">
|
|
|
|
|
|
|
+ <div ref="workspaceCanvas" class="workspace-canvas-shell">
|
|
|
<ThumbnailSlide
|
|
<ThumbnailSlide
|
|
|
:slide="workspaceSlide"
|
|
:slide="workspaceSlide"
|
|
|
- :size="720"
|
|
|
|
|
|
|
+ :size="workspaceCanvasSize"
|
|
|
:show-background-error="true"
|
|
:show-background-error="true"
|
|
|
:editable-text="true"
|
|
:editable-text="true"
|
|
|
:edit-disabled="!canEditWorkspaceText"
|
|
:edit-disabled="!canEditWorkspaceText"
|
|
@@ -209,7 +213,8 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<div v-if="sampleBackgroundState.status === 'error'" class="template-load-error">模板背景加载失败,请更换模板或稍后重试</div>
|
|
<div v-if="sampleBackgroundState.status === 'error'" class="template-load-error">模板背景加载失败,请更换模板或稍后重试</div>
|
|
|
- <div class="sample-feedback workspace-feedback">
|
|
|
|
|
|
|
+ <details class="sample-feedback workspace-feedback">
|
|
|
|
|
+ <summary>调整当前页</summary>
|
|
|
<label><input v-model="editTeachingContent" type="checkbox" :disabled="working" />同时修改本页教学内容(保留其他页面)</label>
|
|
<label><input v-model="editTeachingContent" type="checkbox" :disabled="working" />同时修改本页教学内容(保留其他页面)</label>
|
|
|
<div class="feedback-label">图片或版式需要调整?</div>
|
|
<div class="feedback-label">图片或版式需要调整?</div>
|
|
|
<div class="feedback-chips">
|
|
<div class="feedback-chips">
|
|
@@ -228,7 +233,7 @@
|
|
|
<textarea v-model="sampleFeedbackText" :disabled="working" placeholder="说明希望如何调整,文字可直接在上方预览中双击修改"></textarea>
|
|
<textarea v-model="sampleFeedbackText" :disabled="working" placeholder="说明希望如何调整,文字可直接在上方预览中双击修改"></textarea>
|
|
|
<Button @click="adjustCurrentPage" :disabled="working">调整当前页</Button>
|
|
<Button @click="adjustCurrentPage" :disabled="working">调整当前页</Button>
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
|
|
+ </details>
|
|
|
<details class="teaching-review" v-if="workspacePage">
|
|
<details class="teaching-review" v-if="workspacePage">
|
|
|
<summary>教师备注、用时与课堂资源</summary>
|
|
<summary>教师备注、用时与课堂资源</summary>
|
|
|
<label>用时(分钟)<input type="number" min="1" max="60" v-model.number="workspacePage.durationMinutes" :disabled="working" @input="refreshEditedDraft" /></label>
|
|
<label>用时(分钟)<input type="number" min="1" max="60" v-model.number="workspacePage.durationMinutes" :disabled="working" @input="refreshEditedDraft" /></label>
|
|
@@ -244,7 +249,7 @@
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div class="footer">
|
|
<div class="footer">
|
|
|
- <Button @click="emit('update:visible', false)" :disabled="working">取消</Button>
|
|
|
|
|
|
|
+ <Button @click="emit('update:visible', false)" :disabled="working">关闭</Button>
|
|
|
<template v-if="isSetupStep">
|
|
<template v-if="isSetupStep">
|
|
|
<Button v-if="outline" @click="continueToFinalPreview" :disabled="working">{{ displayedConverted ? "继续预览" : "继续生成" }}</Button>
|
|
<Button v-if="outline" @click="continueToFinalPreview" :disabled="working">{{ displayedConverted ? "继续预览" : "继续生成" }}</Button>
|
|
|
<Button type="primary" @click="handleOutline" :disabled="working">{{ outline ? "按当前设置重新生成" : "开始生成课件" }}</Button>
|
|
<Button type="primary" @click="handleOutline" :disabled="working">{{ outline ? "按当前设置重新生成" : "开始生成课件" }}</Button>
|
|
@@ -252,12 +257,15 @@
|
|
|
<template v-else-if="isStyleStep">
|
|
<template v-else-if="isStyleStep">
|
|
|
<Button @click="workflowStep = 1" :disabled="working">返回修改需求</Button>
|
|
<Button @click="workflowStep = 1" :disabled="working">返回修改需求</Button>
|
|
|
<Button v-if="!outlineConfirmed" type="primary" :disabled="working || !reliableResearch" @click="confirmOutline">确认教学安排并生成</Button>
|
|
<Button v-if="!outlineConfirmed" type="primary" :disabled="working || !reliableResearch" @click="confirmOutline">确认教学安排并生成</Button>
|
|
|
- <Button v-if="displayedConverted" :disabled="working" @click="saveDraftExplicitly">保存初稿</Button>
|
|
|
|
|
- <Button v-if="previousCourseware" :disabled="working" @click="restorePreviousCourseware">恢复应用前课件</Button>
|
|
|
|
|
|
|
+ <span v-if="displayedConverted" class="draft-save-status">{{ draftSaved ? '草稿已保存' : '草稿未保存' }}</span>
|
|
|
|
|
+ <details v-if="displayedConverted" class="more-actions"><summary>更多</summary><div class="more-actions-menu">
|
|
|
|
|
+ <Button :disabled="working" @click="saveDraftExplicitly">保存初稿</Button>
|
|
|
|
|
+ <Button v-if="previousCourseware" :disabled="working" @click="restorePreviousCourseware">恢复应用前课件</Button>
|
|
|
|
|
+ </div></details>
|
|
|
<Button
|
|
<Button
|
|
|
v-if="displayedConverted && draftPreparationState === 'ready'"
|
|
v-if="displayedConverted && draftPreparationState === 'ready'"
|
|
|
type="primary"
|
|
type="primary"
|
|
|
- :disabled="working || !draftQuality?.passed || sampleBackgroundState.status === 'error'"
|
|
|
|
|
|
|
+ :disabled="working || !draftQuality?.passed || Boolean(serverReviewMessage) || sampleBackgroundState.status === 'error'"
|
|
|
@click="applyDraft"
|
|
@click="applyDraft"
|
|
|
>应用到当前课件</Button
|
|
>应用到当前课件</Button
|
|
|
>
|
|
>
|
|
@@ -322,6 +330,24 @@ const { addHistorySnapshot } = useHistorySnapshot()
|
|
|
const extraPrompt = ref("")
|
|
const extraPrompt = ref("")
|
|
|
const outline = ref<AiCoursewareOutline>()
|
|
const outline = ref<AiCoursewareOutline>()
|
|
|
const outlineConfirmed = ref(false)
|
|
const outlineConfirmed = ref(false)
|
|
|
|
|
+const teachingDetailsExpanded = ref(false)
|
|
|
|
|
+const qualityDetailsExpanded = ref(false)
|
|
|
|
|
+const serverReviewMessage = ref("")
|
|
|
|
|
+const draftSaved = ref(false)
|
|
|
|
|
+const workspaceCanvas = ref<HTMLElement>()
|
|
|
|
|
+const canvasWidth = ref(720)
|
|
|
|
|
+const canvasHeight = ref(405)
|
|
|
|
|
+watch(workspaceCanvas, (element, _, onCleanup) => {
|
|
|
|
|
+ if (!element) return
|
|
|
|
|
+ const observer = new ResizeObserver(([entry]) => {
|
|
|
|
|
+ canvasWidth.value = entry.contentRect.width
|
|
|
|
|
+ canvasHeight.value = entry.contentRect.height
|
|
|
|
|
+ })
|
|
|
|
|
+ observer.observe(element)
|
|
|
|
|
+ onCleanup(() => observer.disconnect())
|
|
|
|
|
+})
|
|
|
|
|
+// 同时约束宽高,避免小窗口里只显示幻灯片中间的一截。
|
|
|
|
|
+const workspaceCanvasSize = computed(() => Math.max(1, Math.min(canvasWidth.value, canvasHeight.value / slidesStore.viewportRatio)))
|
|
|
const editTeachingContent = ref(false)
|
|
const editTeachingContent = ref(false)
|
|
|
const availableResources = ref<AiCoursewareResourceBinding[]>([])
|
|
const availableResources = ref<AiCoursewareResourceBinding[]>([])
|
|
|
const previousCourseware = ref<AiDraftRecord["backup"]>()
|
|
const previousCourseware = ref<AiDraftRecord["backup"]>()
|
|
@@ -573,6 +599,8 @@ async function handleOutline() {
|
|
|
displayedConverted.value = undefined
|
|
displayedConverted.value = undefined
|
|
|
displayedTemplateId.value = undefined
|
|
displayedTemplateId.value = undefined
|
|
|
draftQuality.value = undefined
|
|
draftQuality.value = undefined
|
|
|
|
|
+ serverReviewMessage.value = ""
|
|
|
|
|
+ qualityDetailsExpanded.value = false
|
|
|
draftPreparationState.value = "idle"
|
|
draftPreparationState.value = "idle"
|
|
|
draftFailureMessage.value = "请稍后重试"
|
|
draftFailureMessage.value = "请稍后重试"
|
|
|
outline.value = res.data as AiCoursewareOutline
|
|
outline.value = res.data as AiCoursewareOutline
|
|
@@ -586,6 +614,7 @@ async function handleOutline() {
|
|
|
draftPreparationState.value = "idle"
|
|
draftPreparationState.value = "idle"
|
|
|
workflowStep.value = 2
|
|
workflowStep.value = 2
|
|
|
await persistDraft()
|
|
await persistDraft()
|
|
|
|
|
+ if (reliableResearch.value) await confirmOutline()
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -595,7 +624,7 @@ async function continueToFinalPreview() {
|
|
|
draftPreparationState.value = "ready"
|
|
draftPreparationState.value = "ready"
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
- if (outlineConfirmed.value) await confirmSamplesAndGenerate()
|
|
|
|
|
|
|
+ if (outlineConfirmed.value) await handleGenerate()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async function regenerateFinalPreview() {
|
|
async function regenerateFinalPreview() {
|
|
@@ -631,16 +660,22 @@ function handleFinalPreparationFailure(message = "自动生成和优化未能完
|
|
|
draftPreparationState.value = "failed"
|
|
draftPreparationState.value = "failed"
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+async function repairDraft() {
|
|
|
|
|
+ if (working.value || !displayedOutline.value) return
|
|
|
|
|
+ draftOutline.value = cloneCoursewareData(displayedOutline.value)
|
|
|
|
|
+ await handleGenerate(true)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function cloneCoursewareData<T>(value: T): T {
|
|
function cloneCoursewareData<T>(value: T): T {
|
|
|
return JSON.parse(JSON.stringify(value)) as T
|
|
return JSON.parse(JSON.stringify(value)) as T
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-async function handleGenerate() {
|
|
|
|
|
|
|
+async function handleGenerate(repairOnly = false) {
|
|
|
const sourceOutline = draftOutline.value || outline.value
|
|
const sourceOutline = draftOutline.value || outline.value
|
|
|
if (!sourceOutline || !pptWork.id) return
|
|
if (!sourceOutline || !pptWork.id) return
|
|
|
|
|
|
|
|
draftConverted.value = undefined
|
|
draftConverted.value = undefined
|
|
|
- draftQuality.value = undefined
|
|
|
|
|
|
|
+ if (!displayedConverted.value) draftQuality.value = undefined
|
|
|
draftPreparationState.value = "optimizing"
|
|
draftPreparationState.value = "optimizing"
|
|
|
draftFailureMessage.value = "请稍后重试"
|
|
draftFailureMessage.value = "请稍后重试"
|
|
|
sourceOutline.style = selectedStyle.value
|
|
sourceOutline.style = selectedStyle.value
|
|
@@ -651,7 +686,8 @@ async function handleGenerate() {
|
|
|
fromType: queryParams.pptResourcesType || queryParams.fromType,
|
|
fromType: queryParams.pptResourcesType || queryParams.fromType,
|
|
|
outline: sourceOutline,
|
|
outline: sourceOutline,
|
|
|
templateId: selectedTemplateId.value,
|
|
templateId: selectedTemplateId.value,
|
|
|
- visualPrompt: buildVisualPrompt(),
|
|
|
|
|
|
|
+ visualPrompt: buildVisualPrompt() + (repairOnly ? `\n请在现有初稿上修复以下问题,保留无关内容及教师修改:${[serverReviewMessage.value, ...(draftQuality.value?.errors || []).map(issue => issue.message)].filter(Boolean).join(';')}` : ''),
|
|
|
|
|
+ repairOnly,
|
|
|
preferences: { ...preferences }
|
|
preferences: { ...preferences }
|
|
|
})
|
|
})
|
|
|
if (res.code !== 200) {
|
|
if (res.code !== 200) {
|
|
@@ -679,7 +715,7 @@ async function fetchTask(taskId: string) {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
taskInfo.value = res.data as AiCoursewareTaskInfo
|
|
taskInfo.value = res.data as AiCoursewareTaskInfo
|
|
|
- if (taskInfo.value.status === "DONE") {
|
|
|
|
|
|
|
+ if (taskInfo.value.status === "DONE" || taskInfo.value.status === "DRAFT") {
|
|
|
clearPollTimer()
|
|
clearPollTimer()
|
|
|
await loadDraft(taskId)
|
|
await loadDraft(taskId)
|
|
|
} else if (taskInfo.value.status === "FAILED") {
|
|
} else if (taskInfo.value.status === "FAILED") {
|
|
@@ -724,6 +760,7 @@ async function loadDraft(taskId: string) {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
draftOutline.value = result.outline
|
|
draftOutline.value = result.outline
|
|
|
|
|
+ serverReviewMessage.value = result.reviewMessage || ""
|
|
|
availableResources.value = Array.isArray(result.context?.resources) ? result.context.resources : []
|
|
availableResources.value = Array.isArray(result.context?.resources) ? result.context.resources : []
|
|
|
restoreProtectedPageContent(draftOutline.value)
|
|
restoreProtectedPageContent(draftOutline.value)
|
|
|
draftOutline.value.templateId = selectedTemplateId.value
|
|
draftOutline.value.templateId = selectedTemplateId.value
|
|
@@ -783,7 +820,7 @@ async function applyDraft() {
|
|
|
draftQuality.value = inspectAiPptQuality(displayedOutline.value, displayedConverted.value.slides, preferences)
|
|
draftQuality.value = inspectAiPptQuality(displayedOutline.value, displayedConverted.value.slides, preferences)
|
|
|
appendResourceIssues(resourceIssues)
|
|
appendResourceIssues(resourceIssues)
|
|
|
draftPreparationState.value = "ready"
|
|
draftPreparationState.value = "ready"
|
|
|
- if (!draftQuality.value.passed) return
|
|
|
|
|
|
|
+ if (!draftQuality.value.passed || serverReviewMessage.value) return
|
|
|
previousCourseware.value = cloneCoursewareData({ title: slidesStore.title, theme: slidesStore.theme, slides: slidesStore.slides, width: slidesStore.viewportSize, ratio: slidesStore.viewportRatio })
|
|
previousCourseware.value = cloneCoursewareData({ title: slidesStore.title, theme: slidesStore.theme, slides: slidesStore.slides, width: slidesStore.viewportSize, ratio: slidesStore.viewportRatio })
|
|
|
if (!await persistDraft()) return
|
|
if (!await persistDraft()) return
|
|
|
slidesStore.setViewportSize(1920)
|
|
slidesStore.setViewportSize(1920)
|
|
@@ -802,27 +839,6 @@ async function adjustCurrentPage() {
|
|
|
await regenerateDraftPage(selectedDraftPageIndex.value)
|
|
await regenerateDraftPage(selectedDraftPageIndex.value)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-async function confirmSamplesAndGenerate() {
|
|
|
|
|
- const slides = previewSlides.value || []
|
|
|
|
|
- if (!outline.value || slides.length !== outline.value.pages.length) {
|
|
|
|
|
- ElMessage.error("页面预览尚未准备完成,请稍后重试")
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const backgroundUrls = Array.from(
|
|
|
|
|
- new Set(slides.map(slide => (slide.background?.type === "image" ? resolveTemplateAssetUrl(slide.background.image) : "")).filter(Boolean))
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- try {
|
|
|
|
|
- await Promise.all(backgroundUrls.map(preloadImage))
|
|
|
|
|
- } catch {
|
|
|
|
|
- handleFinalPreparationFailure("模板背景加载失败")
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- await handleGenerate()
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
function handleSampleBackgroundLoadState(payload: { status: "none" | "loading" | "loaded" | "error"; url: string }) {
|
|
function handleSampleBackgroundLoadState(payload: { status: "none" | "loading" | "loaded" | "error"; url: string }) {
|
|
|
sampleBackgroundState.value = payload
|
|
sampleBackgroundState.value = payload
|
|
|
if (payload.status === "error") {
|
|
if (payload.status === "error") {
|
|
@@ -876,17 +892,6 @@ function templateCoverStyle(templateId: AiCoursewareTemplateId): Record<string,
|
|
|
return { background: cover?.background || selectedTemplate.value.palettes[0].background }
|
|
return { background: cover?.background || selectedTemplate.value.palettes[0].background }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function preloadImage(url: string) {
|
|
|
|
|
- if (!url || typeof Image === "undefined") return Promise.resolve()
|
|
|
|
|
- return new Promise<void>((resolve, reject) => {
|
|
|
|
|
- const image = new Image()
|
|
|
|
|
- const timer = window.setTimeout(() => reject(new Error("图片加载超时")), 15000)
|
|
|
|
|
- image.onload = () => { window.clearTimeout(timer); resolve() }
|
|
|
|
|
- image.onerror = () => { window.clearTimeout(timer); reject(new Error(`failed to load ${url}`)) }
|
|
|
|
|
- image.src = url
|
|
|
|
|
- })
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
function templateFit(templateId: AiCoursewareTemplateId) {
|
|
function templateFit(templateId: AiCoursewareTemplateId) {
|
|
|
return outline.value
|
|
return outline.value
|
|
|
? scoreAiCoursewareTemplate(outline.value, templateId)
|
|
? scoreAiCoursewareTemplate(outline.value, templateId)
|
|
@@ -1009,7 +1014,7 @@ function buildExtraPrompt() {
|
|
|
async function confirmOutline() {
|
|
async function confirmOutline() {
|
|
|
if (!reliableResearch.value || working.value) return
|
|
if (!reliableResearch.value || working.value) return
|
|
|
outlineConfirmed.value = true
|
|
outlineConfirmed.value = true
|
|
|
- await confirmSamplesAndGenerate()
|
|
|
|
|
|
|
+ await handleGenerate()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async function refreshEditedDraft() {
|
|
async function refreshEditedDraft() {
|
|
@@ -1045,9 +1050,11 @@ function toggleResource(resource: AiCoursewareResourceBinding) {
|
|
|
let saveQueue: Promise<unknown> = Promise.resolve()
|
|
let saveQueue: Promise<unknown> = Promise.resolve()
|
|
|
async function persistDraft() {
|
|
async function persistDraft() {
|
|
|
if (!pptWork.id) return false
|
|
if (!pptWork.id) return false
|
|
|
|
|
+ draftSaved.value = false
|
|
|
const snapshot = cloneCoursewareData({ outline: displayedOutline.value || draftOutline.value || outline.value,
|
|
const snapshot = cloneCoursewareData({ outline: displayedOutline.value || draftOutline.value || outline.value,
|
|
|
converted: displayedConverted.value, preferences: { ...preferences }, extraPrompt: extraPrompt.value,
|
|
converted: displayedConverted.value, preferences: { ...preferences }, extraPrompt: extraPrompt.value,
|
|
|
- taskId: taskInfo.value?.taskId, resources: availableResources.value, backup: previousCourseware.value,
|
|
|
|
|
|
|
+ taskId: taskInfo.value && ['PENDING', 'RUNNING'].includes(taskInfo.value.status) ? taskInfo.value.taskId : undefined,
|
|
|
|
|
+ reviewMessage: serverReviewMessage.value, resources: availableResources.value, backup: previousCourseware.value,
|
|
|
protectedContent: Array.from(protectedPageContent.entries()) })
|
|
protectedContent: Array.from(protectedPageContent.entries()) })
|
|
|
const id = String(pptWork.id)
|
|
const id = String(pptWork.id)
|
|
|
const source = queryParams.pptResourcesType || queryParams.fromType || "TEACHER"
|
|
const source = queryParams.pptResourcesType || queryParams.fromType || "TEACHER"
|
|
@@ -1055,8 +1062,10 @@ async function persistDraft() {
|
|
|
const key = await draftKey(id, source)
|
|
const key = await draftKey(id, source)
|
|
|
saveQueue = saveQueue.catch(() => undefined).then(() => saveAiDraft({ ...snapshot, key, updatedAt: Date.now() }))
|
|
saveQueue = saveQueue.catch(() => undefined).then(() => saveAiDraft({ ...snapshot, key, updatedAt: Date.now() }))
|
|
|
await saveQueue
|
|
await saveQueue
|
|
|
|
|
+ draftSaved.value = true
|
|
|
return true
|
|
return true
|
|
|
} catch {
|
|
} catch {
|
|
|
|
|
+ draftSaved.value = false
|
|
|
ElMessage.warning("本机草稿保存失败,请勿刷新页面,并及时手动保存课件")
|
|
ElMessage.warning("本机草稿保存失败,请勿刷新页面,并及时手动保存课件")
|
|
|
return false
|
|
return false
|
|
|
}
|
|
}
|
|
@@ -1079,6 +1088,8 @@ async function restoreSavedDraft() {
|
|
|
protectedPageContent.clear()
|
|
protectedPageContent.clear()
|
|
|
for (const [pageNo, content] of saved.protectedContent || []) protectedPageContent.set(pageNo, content)
|
|
for (const [pageNo, content] of saved.protectedContent || []) protectedPageContent.set(pageNo, content)
|
|
|
previousCourseware.value = saved.backup
|
|
previousCourseware.value = saved.backup
|
|
|
|
|
+ serverReviewMessage.value = saved.reviewMessage || ""
|
|
|
|
|
+ draftSaved.value = true
|
|
|
outline.value = saved.outline
|
|
outline.value = saved.outline
|
|
|
selectedTemplateId.value = getAiCoursewareTemplate(saved.outline.templateId).id
|
|
selectedTemplateId.value = getAiCoursewareTemplate(saved.outline.templateId).id
|
|
|
draftOutline.value = cloneCoursewareData(saved.outline)
|
|
draftOutline.value = cloneCoursewareData(saved.outline)
|
|
@@ -1157,6 +1168,41 @@ function shortProgressLabel(message?: string) {
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
<style lang="scss" scoped>
|
|
|
|
|
+.draft-notice {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ padding: 8px 12px;
|
|
|
|
|
+ margin: 8px 0;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ background: #fff7e8;
|
|
|
|
|
+ color: #76552b;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ > span { flex: 1; }
|
|
|
|
|
+ > button:not(.button) { color: #1677d2; border: 0; background: none; cursor: pointer; }
|
|
|
|
|
+}
|
|
|
|
|
+.draft-save-status { font-size: 12px; color: #7b8794; margin-right: auto; }
|
|
|
|
|
+.more-actions {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ summary { cursor: pointer; color: #64748b; }
|
|
|
|
|
+}
|
|
|
|
|
+.more-actions-menu {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ bottom: 26px;
|
|
|
|
|
+ z-index: 10;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ width: 160px;
|
|
|
|
|
+ padding: 8px;
|
|
|
|
|
+ border: 1px solid #e1e8f0;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ box-shadow: 0 4px 18px rgba(31, 41, 51, 0.12);
|
|
|
|
|
+}
|
|
|
.teaching-review, .quality-panel {
|
|
.teaching-review, .quality-panel {
|
|
|
margin: 10px 0;
|
|
margin: 10px 0;
|
|
|
padding: 12px 16px;
|
|
padding: 12px 16px;
|
|
@@ -1683,6 +1729,7 @@ function shortProgressLabel(message?: string) {
|
|
|
min-height: 0;
|
|
min-height: 0;
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
|
|
+ overflow: auto;
|
|
|
padding: 10px;
|
|
padding: 10px;
|
|
|
border: 1px solid #dfe6ee;
|
|
border: 1px solid #dfe6ee;
|
|
|
border-radius: 8px;
|
|
border-radius: 8px;
|
|
@@ -1718,12 +1765,12 @@ function shortProgressLabel(message?: string) {
|
|
|
}
|
|
}
|
|
|
.workspace-canvas-shell {
|
|
.workspace-canvas-shell {
|
|
|
position: relative;
|
|
position: relative;
|
|
|
- min-height: 0;
|
|
|
|
|
- flex: 1 1 auto;
|
|
|
|
|
|
|
+ min-height: 180px;
|
|
|
|
|
+ flex: 1 0 180px;
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
justify-content: center;
|
|
|
- overflow: auto;
|
|
|
|
|
|
|
+ overflow: hidden;
|
|
|
padding: 10px;
|
|
padding: 10px;
|
|
|
border-radius: 7px;
|
|
border-radius: 7px;
|
|
|
background: #eef2f7;
|
|
background: #eef2f7;
|
|
@@ -1759,6 +1806,14 @@ function shortProgressLabel(message?: string) {
|
|
|
flex: 0 0 auto;
|
|
flex: 0 0 auto;
|
|
|
margin-top: 8px;
|
|
margin-top: 8px;
|
|
|
padding-top: 8px;
|
|
padding-top: 8px;
|
|
|
|
|
+ summary { color: #52616b; font-size: 13px; cursor: pointer; }
|
|
|
|
|
+ &[open] summary { margin-bottom: 10px; }
|
|
|
|
|
+}
|
|
|
|
|
+.workspace-preview-column > .teaching-review {
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ margin-bottom: 0;
|
|
|
|
|
+ padding: 8px 12px;
|
|
|
|
|
+ summary { font-size: 13px; }
|
|
|
}
|
|
}
|
|
|
.sample-stage-head {
|
|
.sample-stage-head {
|
|
|
display: flex;
|
|
display: flex;
|
|
@@ -1780,6 +1835,7 @@ function shortProgressLabel(message?: string) {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
min-width: 0;
|
|
min-width: 0;
|
|
|
|
|
+ margin-right: auto;
|
|
|
gap: 7px;
|
|
gap: 7px;
|
|
|
color: #657381;
|
|
color: #657381;
|
|
|
font-size: 12px;
|
|
font-size: 12px;
|