|
@@ -140,7 +140,7 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <div v-if="draftConverted" :class="['compact-process-state', draftPreparationState]">
|
|
|
|
|
|
|
+ <div v-if="draftPreparationState !== 'idle'" :class="['compact-process-state', draftPreparationState]">
|
|
|
<span v-if="draftPreparationState === 'optimizing'" class="loading-dot"></span>
|
|
<span v-if="draftPreparationState === 'optimizing'" class="loading-dot"></span>
|
|
|
<span v-else class="process-state-icon">{{ draftPreparationState === "ready" ? "✓" : "!" }}</span>
|
|
<span v-else class="process-state-icon">{{ draftPreparationState === "ready" ? "✓" : "!" }}</span>
|
|
|
<div>
|
|
<div>
|
|
@@ -149,6 +149,13 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
+ <div v-if="!workspaceSlide" :class="['final-preview-placeholder', draftPreparationState]">
|
|
|
|
|
+ <span v-if="draftPreparationState === 'optimizing'" class="loading-dot"></span>
|
|
|
|
|
+ <span v-else class="placeholder-icon">!</span>
|
|
|
|
|
+ <strong>{{ draftPreparationState === "failed" ? "最终课件暂未生成成功" : "正在生成并优化最终课件" }}</strong>
|
|
|
|
|
+ <small>{{ draftPreparationState === "failed" ? "请更换模板或返回修改需求后重试" : "完成后将直接展示可应用的最终效果" }}</small>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
<div v-if="workspaceSlide" class="unified-review-workspace">
|
|
<div v-if="workspaceSlide" class="unified-review-workspace">
|
|
|
<aside class="workspace-page-list" aria-label="课件页面">
|
|
<aside class="workspace-page-list" aria-label="课件页面">
|
|
|
<button
|
|
<button
|
|
@@ -182,6 +189,11 @@
|
|
|
@background-load-state="handleSampleBackgroundLoadState"
|
|
@background-load-state="handleSampleBackgroundLoadState"
|
|
|
@text-change="handleWorkspaceTextChange"
|
|
@text-change="handleWorkspaceTextChange"
|
|
|
/>
|
|
/>
|
|
|
|
|
+ <div v-if="working" class="workspace-processing-overlay">
|
|
|
|
|
+ <span class="loading-dot"></span>
|
|
|
|
|
+ <strong>正在生成并优化新版本</strong>
|
|
|
|
|
+ <small>完成后会自动更新预览</small>
|
|
|
|
|
+ </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">
|
|
<div class="sample-feedback workspace-feedback">
|
|
@@ -199,12 +211,7 @@
|
|
|
</button>
|
|
</button>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="sample-feedback-action">
|
|
<div class="sample-feedback-action">
|
|
|
- <textarea
|
|
|
|
|
- v-model="sampleFeedbackText"
|
|
|
|
|
- :disabled="working"
|
|
|
|
|
- placeholder="说明希望如何调整,文字可直接在上方预览中双击修改"
|
|
|
|
|
- @input="invalidateVisualApproval"
|
|
|
|
|
- ></textarea>
|
|
|
|
|
|
|
+ <textarea v-model="sampleFeedbackText" :disabled="working" placeholder="说明希望如何调整,文字可直接在上方预览中双击修改"></textarea>
|
|
|
<Button @click="adjustCurrentPage" :disabled="working">调整当前页</Button>
|
|
<Button @click="adjustCurrentPage" :disabled="working">调整当前页</Button>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -215,20 +222,19 @@
|
|
|
<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="workflowStep = 2" :disabled="working">继续选择样式</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>
|
|
|
</template>
|
|
</template>
|
|
|
<template v-else-if="isStyleStep">
|
|
<template v-else-if="isStyleStep">
|
|
|
<Button @click="workflowStep = 1" :disabled="working">返回修改需求</Button>
|
|
<Button @click="workflowStep = 1" :disabled="working">返回修改需求</Button>
|
|
|
<span v-if="draftPreparationState === 'optimizing'" class="footer-progress">正在自动检查并优化,无需操作…</span>
|
|
<span v-if="draftPreparationState === 'optimizing'" class="footer-progress">正在自动检查并优化,无需操作…</span>
|
|
|
<Button
|
|
<Button
|
|
|
- v-if="!draftConverted"
|
|
|
|
|
|
|
+ v-if="displayedConverted && draftPreparationState === 'ready'"
|
|
|
type="primary"
|
|
type="primary"
|
|
|
- @click="confirmSamplesAndGenerate"
|
|
|
|
|
- :disabled="working || sampleBackgroundState.status === 'error'"
|
|
|
|
|
- >生成并自动优化</Button
|
|
|
|
|
|
|
+ :disabled="sampleBackgroundState.status === 'error'"
|
|
|
|
|
+ @click="applyDraft"
|
|
|
|
|
+ >应用到当前课件</Button
|
|
|
>
|
|
>
|
|
|
- <Button v-else-if="draftPreparationState === 'ready'" type="primary" @click="applyDraft">应用到当前课件</Button>
|
|
|
|
|
</template>
|
|
</template>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -293,7 +299,6 @@ const workflowSteps = [
|
|
|
{ value: 1, label: "设置需求" },
|
|
{ value: 1, label: "设置需求" },
|
|
|
{ value: 2, label: "预览、编辑并应用" }
|
|
{ value: 2, label: "预览、编辑并应用" }
|
|
|
] as const
|
|
] as const
|
|
|
-const taskMode = ref<"full" | "sample">("full")
|
|
|
|
|
const coursewareTemplates = AI_COURSEWARE_TEMPLATES
|
|
const coursewareTemplates = AI_COURSEWARE_TEMPLATES
|
|
|
const defaultTemplateId = AI_COURSEWARE_TEMPLATES[0].id
|
|
const defaultTemplateId = AI_COURSEWARE_TEMPLATES[0].id
|
|
|
const selectedTemplateId = ref<AiCoursewareTemplateId>(defaultTemplateId)
|
|
const selectedTemplateId = ref<AiCoursewareTemplateId>(defaultTemplateId)
|
|
@@ -313,13 +318,15 @@ const preferences = reactive<AiCoursewarePreferences>({
|
|
|
requireVideo: false,
|
|
requireVideo: false,
|
|
|
enableAnimations: true
|
|
enableAnimations: true
|
|
|
})
|
|
})
|
|
|
-const approvedSamplePages = ref<number[]>([])
|
|
|
|
|
const sampleBackgroundState = ref<{ status: "none" | "loading" | "loaded" | "error"; url: string }>({ status: "none", url: "" })
|
|
const sampleBackgroundState = ref<{ status: "none" | "loading" | "loaded" | "error"; url: string }>({ status: "none", url: "" })
|
|
|
const sampleFeedbackOptions = ["画风不合适", "页面太空", "图片不合适", "层级不清晰"]
|
|
const sampleFeedbackOptions = ["画风不合适", "页面太空", "图片不合适", "层级不清晰"]
|
|
|
const sampleFeedbackTags = ref<string[]>([])
|
|
const sampleFeedbackTags = ref<string[]>([])
|
|
|
const sampleFeedbackText = ref("")
|
|
const sampleFeedbackText = ref("")
|
|
|
const draftOutline = ref<AiCoursewareOutline>()
|
|
const draftOutline = ref<AiCoursewareOutline>()
|
|
|
const draftConverted = ref<{ title: string; theme: Partial<SlideTheme>; slides: Slide[] }>()
|
|
const draftConverted = ref<{ title: string; theme: Partial<SlideTheme>; slides: Slide[] }>()
|
|
|
|
|
+const displayedOutline = ref<AiCoursewareOutline>()
|
|
|
|
|
+const displayedConverted = ref<{ title: string; theme: Partial<SlideTheme>; slides: Slide[] }>()
|
|
|
|
|
+const displayedTemplateId = ref<AiCoursewareTemplateId>()
|
|
|
const draftQuality = ref<AiPptQualityReport>()
|
|
const draftQuality = ref<AiPptQualityReport>()
|
|
|
const draftPreparationState = ref<"idle" | "optimizing" | "ready" | "failed">("idle")
|
|
const draftPreparationState = ref<"idle" | "optimizing" | "ready" | "failed">("idle")
|
|
|
const automaticRepairAttempts = ref(0)
|
|
const automaticRepairAttempts = ref(0)
|
|
@@ -337,7 +344,6 @@ const protectedPageContent = new Map<number, ProtectedPageContent>()
|
|
|
const TEACHER_VISIBLE_PATTERN = /(?:教师|老师)(?:讲述|提问|引导|指导|组织|巡视|示范)|(?:引导|指导|组织)学生/
|
|
const TEACHER_VISIBLE_PATTERN = /(?:教师|老师)(?:讲述|提问|引导|指导|组织|巡视|示范)|(?:引导|指导|组织)学生/
|
|
|
const PLACEHOLDER_PATTERN = /(?:待补充|待完善|请输入|可参考教材|placeholder|lorem ipsum|\{\{.+?\}\}|\[.+?占位.+?\])/i
|
|
const PLACEHOLDER_PATTERN = /(?:待补充|待完善|请输入|可参考教材|placeholder|lorem ipsum|\{\{.+?\}\}|\[.+?占位.+?\])/i
|
|
|
const pollTimer = ref<number>()
|
|
const pollTimer = ref<number>()
|
|
|
-const selectedPageNo = ref(1)
|
|
|
|
|
const selectedDraftPageIndex = ref(0)
|
|
const selectedDraftPageIndex = ref(0)
|
|
|
const outlineLoading = ref(false)
|
|
const outlineLoading = ref(false)
|
|
|
const working = computed(
|
|
const working = computed(
|
|
@@ -386,26 +392,21 @@ const pageMeta = computed(() => {
|
|
|
if (!outline.value) return `根据当前教材生成${preferences.minPages}至${preferences.maxPages}页音乐课件`
|
|
if (!outline.value) return `根据当前教材生成${preferences.minPages}至${preferences.maxPages}页音乐课件`
|
|
|
return [...[outline.value.gradeName, outline.value.unitName].filter(Boolean), `共${outline.value.pages.length}页`].join(" · ")
|
|
return [...[outline.value.gradeName, outline.value.unitName].filter(Boolean), `共${outline.value.pages.length}页`].join(" · ")
|
|
|
})
|
|
})
|
|
|
-const samplePageIndex = computed(() => {
|
|
|
|
|
- const pages = outline.value?.pages || []
|
|
|
|
|
- const selectedIndex = pages.findIndex(page => page.pageNo === selectedPageNo.value)
|
|
|
|
|
- return selectedIndex >= 0 ? selectedIndex : 0
|
|
|
|
|
-})
|
|
|
|
|
-const samplePageNo = computed(() => outline.value?.pages[samplePageIndex.value]?.pageNo || samplePageIndex.value + 1)
|
|
|
|
|
-const workspaceSlides = computed(() => draftConverted.value?.slides || previewSlides.value || [])
|
|
|
|
|
-const workspacePages = computed(() => draftOutline.value?.pages || outline.value?.pages || [])
|
|
|
|
|
-const selectedWorkspacePageIndex = computed(() => (draftConverted.value ? selectedDraftPageIndex.value : samplePageIndex.value))
|
|
|
|
|
|
|
+const workspaceSlides = computed(() => displayedConverted.value?.slides || [])
|
|
|
|
|
+const workspacePages = computed(() => displayedOutline.value?.pages || [])
|
|
|
|
|
+const selectedWorkspacePageIndex = computed(() => selectedDraftPageIndex.value)
|
|
|
const workspacePage = computed(() => workspacePages.value[selectedWorkspacePageIndex.value])
|
|
const workspacePage = computed(() => workspacePages.value[selectedWorkspacePageIndex.value])
|
|
|
const workspaceSlide = computed(() => workspaceSlides.value[selectedWorkspacePageIndex.value])
|
|
const workspaceSlide = computed(() => workspaceSlides.value[selectedWorkspacePageIndex.value])
|
|
|
-const canEditWorkspaceText = computed(() => !working.value && (!draftConverted.value || draftPreparationState.value === "ready"))
|
|
|
|
|
|
|
+const canEditWorkspaceText = computed(() => !working.value && Boolean(displayedConverted.value) && draftPreparationState.value === "ready")
|
|
|
const draftStatusTitle = computed(() => {
|
|
const draftStatusTitle = computed(() => {
|
|
|
if (draftPreparationState.value === "ready") return "课件已准备完成"
|
|
if (draftPreparationState.value === "ready") return "课件已准备完成"
|
|
|
if (draftPreparationState.value === "failed") return "自动优化未能完成"
|
|
if (draftPreparationState.value === "failed") return "自动优化未能完成"
|
|
|
return "正在自动检查和优化课件"
|
|
return "正在自动检查和优化课件"
|
|
|
})
|
|
})
|
|
|
const draftStatusDescription = computed(() => {
|
|
const draftStatusDescription = computed(() => {
|
|
|
- if (draftPreparationState.value === "ready") return "可双击预览中的文字修改,确认后直接应用。"
|
|
|
|
|
- if (draftPreparationState.value === "failed") return "请更换模板或返回修改需求后重新生成。"
|
|
|
|
|
|
|
+ if (draftPreparationState.value === "ready") return "当前展示的就是优化后的最终课件,可直接编辑或应用。"
|
|
|
|
|
+ if (draftPreparationState.value === "failed")
|
|
|
|
|
+ return displayedConverted.value ? "新版本未生成成功,已保留上一次最终预览。" : "请更换模板或返回修改需求后重新生成。"
|
|
|
return `系统正在自动处理问题页面(${Math.max(1, automaticRepairAttempts.value)}/${MAX_AUTOMATIC_REPAIR_ATTEMPTS})。`
|
|
return `系统正在自动处理问题页面(${Math.max(1, automaticRepairAttempts.value)}/${MAX_AUTOMATIC_REPAIR_ATTEMPTS})。`
|
|
|
})
|
|
})
|
|
|
const previewSlides = computed(() => {
|
|
const previewSlides = computed(() => {
|
|
@@ -420,11 +421,7 @@ const previewSlides = computed(() => {
|
|
|
function selectWorkspacePage(index: number) {
|
|
function selectWorkspacePage(index: number) {
|
|
|
sampleFeedbackTags.value = []
|
|
sampleFeedbackTags.value = []
|
|
|
sampleFeedbackText.value = ""
|
|
sampleFeedbackText.value = ""
|
|
|
- if (draftConverted.value) {
|
|
|
|
|
- selectedDraftPageIndex.value = index
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
- selectedPageNo.value = outline.value?.pages[index]?.pageNo || index + 1
|
|
|
|
|
|
|
+ selectedDraftPageIndex.value = index
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function handleWorkspaceTextChange(payload: { elementId: string; content: string; previousContent: string; text: string; previousText: string }) {
|
|
function handleWorkspaceTextChange(payload: { elementId: string; content: string; previousContent: string; text: string; previousText: string }) {
|
|
@@ -441,7 +438,8 @@ function handleWorkspaceTextChange(payload: { elementId: string; content: string
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
protectPageContent(page)
|
|
protectPageContent(page)
|
|
|
- invalidateVisualApproval()
|
|
|
|
|
|
|
+ restoreProtectedPageContent(outline.value)
|
|
|
|
|
+ restoreProtectedPageContent(draftOutline.value)
|
|
|
ElMessage.success("文字已更新")
|
|
ElMessage.success("文字已更新")
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -507,7 +505,6 @@ function restoreProtectedPageContent(targetOutline?: AiCoursewareOutline) {
|
|
|
watch(selectedTemplateId, () => {
|
|
watch(selectedTemplateId, () => {
|
|
|
paletteVariant.value = 0
|
|
paletteVariant.value = 0
|
|
|
if (outline.value) outline.value.style = selectedStyle.value
|
|
if (outline.value) outline.value.style = selectedStyle.value
|
|
|
- invalidateVisualApproval()
|
|
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
watch(pageRange, value => {
|
|
watch(pageRange, value => {
|
|
@@ -516,14 +513,11 @@ watch(pageRange, value => {
|
|
|
preferences.maxPages = maxPages
|
|
preferences.maxPages = maxPages
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
-watch(preferences, () => invalidateVisualApproval(), { deep: true })
|
|
|
|
|
-
|
|
|
|
|
watch(
|
|
watch(
|
|
|
outline,
|
|
outline,
|
|
|
() => {
|
|
() => {
|
|
|
if (!outline.value) return
|
|
if (!outline.value) return
|
|
|
if (templateSelectionMode.value === "auto") selectedTemplateId.value = templateRecommendation.value.id
|
|
if (templateSelectionMode.value === "auto") selectedTemplateId.value = templateRecommendation.value.id
|
|
|
- invalidateVisualApproval()
|
|
|
|
|
},
|
|
},
|
|
|
{ deep: true }
|
|
{ deep: true }
|
|
|
)
|
|
)
|
|
@@ -553,17 +547,68 @@ async function handleOutline() {
|
|
|
protectedPageContent.clear()
|
|
protectedPageContent.clear()
|
|
|
draftOutline.value = undefined
|
|
draftOutline.value = undefined
|
|
|
draftConverted.value = undefined
|
|
draftConverted.value = undefined
|
|
|
|
|
+ displayedOutline.value = undefined
|
|
|
|
|
+ displayedConverted.value = undefined
|
|
|
|
|
+ displayedTemplateId.value = undefined
|
|
|
draftQuality.value = undefined
|
|
draftQuality.value = undefined
|
|
|
draftPreparationState.value = "idle"
|
|
draftPreparationState.value = "idle"
|
|
|
outline.value = res.data as AiCoursewareOutline
|
|
outline.value = res.data as AiCoursewareOutline
|
|
|
if (templateSelectionMode.value === "auto") selectedTemplateId.value = recommendAiCoursewareTemplate(outline.value).id
|
|
if (templateSelectionMode.value === "auto") selectedTemplateId.value = recommendAiCoursewareTemplate(outline.value).id
|
|
|
outline.value.style = selectedStyle.value
|
|
outline.value.style = selectedStyle.value
|
|
|
outline.value.templateId = selectedTemplateId.value
|
|
outline.value.templateId = selectedTemplateId.value
|
|
|
- selectedPageNo.value = outline.value.pages[0]?.pageNo || 1
|
|
|
|
|
|
|
+ selectedDraftPageIndex.value = 0
|
|
|
sampleFeedbackTags.value = []
|
|
sampleFeedbackTags.value = []
|
|
|
sampleFeedbackText.value = ""
|
|
sampleFeedbackText.value = ""
|
|
|
|
|
+ draftPreparationState.value = "optimizing"
|
|
|
workflowStep.value = 2
|
|
workflowStep.value = 2
|
|
|
|
|
+ await confirmSamplesAndGenerate()
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function continueToFinalPreview() {
|
|
|
|
|
+ workflowStep.value = 2
|
|
|
|
|
+ if (displayedConverted.value) {
|
|
|
|
|
+ draftPreparationState.value = "ready"
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ draftPreparationState.value = "optimizing"
|
|
|
|
|
+ await confirmSamplesAndGenerate()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function regenerateFinalPreview() {
|
|
|
|
|
+ const sourceOutline = displayedOutline.value || draftOutline.value || outline.value
|
|
|
|
|
+ if (!sourceOutline) return
|
|
|
|
|
+ draftOutline.value = cloneCoursewareData(sourceOutline)
|
|
|
|
|
+ restoreProtectedPageContent(draftOutline.value)
|
|
|
|
|
+ workflowStep.value = 2
|
|
|
|
|
+ await handleGenerate()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function publishFinalPreview() {
|
|
|
|
|
+ if (!draftOutline.value || !draftConverted.value) return
|
|
|
|
|
+ displayedOutline.value = cloneCoursewareData(draftOutline.value)
|
|
|
|
|
+ displayedConverted.value = cloneCoursewareData(draftConverted.value)
|
|
|
|
|
+ displayedTemplateId.value = selectedTemplateId.value
|
|
|
|
|
+ selectedDraftPageIndex.value = Math.max(0, Math.min(selectedDraftPageIndex.value, displayedConverted.value.slides.length - 1))
|
|
|
|
|
+ sampleFeedbackTags.value = []
|
|
|
|
|
+ sampleFeedbackText.value = ""
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function handleFinalPreparationFailure(message = "自动生成和优化未能完成") {
|
|
|
|
|
+ if (displayedOutline.value && displayedConverted.value && displayedTemplateId.value) {
|
|
|
|
|
+ selectedTemplateId.value = displayedTemplateId.value
|
|
|
|
|
+ draftOutline.value = cloneCoursewareData(displayedOutline.value)
|
|
|
|
|
+ draftConverted.value = cloneCoursewareData(displayedConverted.value)
|
|
|
|
|
+ draftQuality.value = undefined
|
|
|
|
|
+ draftPreparationState.value = "ready"
|
|
|
|
|
+ ElMessage.warning(`${message},已保留上一次最终预览`)
|
|
|
|
|
+ return
|
|
|
}
|
|
}
|
|
|
|
|
+ draftPreparationState.value = "failed"
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function cloneCoursewareData<T>(value: T): T {
|
|
|
|
|
+ return JSON.parse(JSON.stringify(value)) as T
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async function handleGenerate() {
|
|
async function handleGenerate() {
|
|
@@ -572,12 +617,11 @@ async function handleGenerate() {
|
|
|
|
|
|
|
|
draftConverted.value = undefined
|
|
draftConverted.value = undefined
|
|
|
draftQuality.value = undefined
|
|
draftQuality.value = undefined
|
|
|
- draftPreparationState.value = "idle"
|
|
|
|
|
|
|
+ draftPreparationState.value = "optimizing"
|
|
|
automaticRepairAttempts.value = 0
|
|
automaticRepairAttempts.value = 0
|
|
|
sourceOutline.style = selectedStyle.value
|
|
sourceOutline.style = selectedStyle.value
|
|
|
sourceOutline.templateId = selectedTemplateId.value
|
|
sourceOutline.templateId = selectedTemplateId.value
|
|
|
restoreProtectedPageContent(sourceOutline)
|
|
restoreProtectedPageContent(sourceOutline)
|
|
|
- taskMode.value = "full"
|
|
|
|
|
const res = await httpAjaxErrMsg(aiCoursewareGenerateApi, {
|
|
const res = await httpAjaxErrMsg(aiCoursewareGenerateApi, {
|
|
|
id: pptWork.id,
|
|
id: pptWork.id,
|
|
|
fromType: queryParams.pptResourcesType || queryParams.fromType,
|
|
fromType: queryParams.pptResourcesType || queryParams.fromType,
|
|
@@ -586,28 +630,11 @@ async function handleGenerate() {
|
|
|
visualPrompt: buildVisualPrompt(),
|
|
visualPrompt: buildVisualPrompt(),
|
|
|
preferences: { ...preferences }
|
|
preferences: { ...preferences }
|
|
|
})
|
|
})
|
|
|
- if (res.code !== 200) return
|
|
|
|
|
-
|
|
|
|
|
- taskInfo.value = res.data as AiCoursewareTaskInfo
|
|
|
|
|
- await pollTask(taskInfo.value.taskId)
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ if (res.code !== 200) {
|
|
|
|
|
+ handleFinalPreparationFailure()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
-async function regenerateSamplePage() {
|
|
|
|
|
- if (!outline.value || !pptWork.id) return
|
|
|
|
|
- approvedSamplePages.value = approvedSamplePages.value.filter(pageNo => pageNo !== samplePageNo.value)
|
|
|
|
|
- taskMode.value = "sample"
|
|
|
|
|
- if (sampleFeedbackTags.value.includes("画风不合适")) paletteVariant.value = (paletteVariant.value + 1) % 3
|
|
|
|
|
- const feedback = [...sampleFeedbackTags.value, sampleFeedbackText.value.trim()].filter(Boolean).join(";")
|
|
|
|
|
- const res = await httpAjaxErrMsg(aiCoursewareGenerateApi, {
|
|
|
|
|
- id: pptWork.id,
|
|
|
|
|
- fromType: queryParams.pptResourcesType || queryParams.fromType,
|
|
|
|
|
- outline: outline.value,
|
|
|
|
|
- templateId: selectedTemplateId.value,
|
|
|
|
|
- visualPrompt: `${buildVisualPrompt()}当前只调整第${samplePageNo.value}页样张。${feedback ? `用户反馈:${feedback}。` : "请更换视觉组织方式,保持教学目标不变。"}`,
|
|
|
|
|
- preferences: { ...preferences },
|
|
|
|
|
- regeneratePageNo: samplePageNo.value
|
|
|
|
|
- })
|
|
|
|
|
- if (res.code !== 200) return
|
|
|
|
|
taskInfo.value = res.data as AiCoursewareTaskInfo
|
|
taskInfo.value = res.data as AiCoursewareTaskInfo
|
|
|
await pollTask(taskInfo.value.taskId)
|
|
await pollTask(taskInfo.value.taskId)
|
|
|
}
|
|
}
|
|
@@ -629,18 +656,17 @@ async function fetchTask(taskId: string) {
|
|
|
taskInfo.value = res.data as AiCoursewareTaskInfo
|
|
taskInfo.value = res.data as AiCoursewareTaskInfo
|
|
|
if (taskInfo.value.status === "DONE") {
|
|
if (taskInfo.value.status === "DONE") {
|
|
|
clearPollTimer()
|
|
clearPollTimer()
|
|
|
- if (taskMode.value === "sample") await loadSampleResult(taskId)
|
|
|
|
|
- else await loadDraft(taskId)
|
|
|
|
|
|
|
+ await loadDraft(taskId)
|
|
|
} else if (taskInfo.value.status === "FAILED") {
|
|
} else if (taskInfo.value.status === "FAILED") {
|
|
|
clearPollTimer()
|
|
clearPollTimer()
|
|
|
const message = taskInfo.value.message || "AI课件生成失败"
|
|
const message = taskInfo.value.message || "AI课件生成失败"
|
|
|
clearTask()
|
|
clearTask()
|
|
|
- if (taskMode.value === "full" && draftOutline.value) {
|
|
|
|
|
- draftPreparationState.value = "failed"
|
|
|
|
|
|
|
+ if (draftOutline.value && draftQuality.value) {
|
|
|
console.error("AI课件自动优化失败", message)
|
|
console.error("AI课件自动优化失败", message)
|
|
|
await continueAutomaticRepairAfterFailure()
|
|
await continueAutomaticRepairAfterFailure()
|
|
|
} else {
|
|
} else {
|
|
|
- ElMessage.error(message)
|
|
|
|
|
|
|
+ console.error("AI课件生成失败", message)
|
|
|
|
|
+ handleFinalPreparationFailure(message)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -661,6 +687,7 @@ async function loadDraft(taskId: string) {
|
|
|
const res = await httpAjaxErrMsg(aiCoursewareResultApi, taskId)
|
|
const res = await httpAjaxErrMsg(aiCoursewareResultApi, taskId)
|
|
|
if (res.code !== 200) {
|
|
if (res.code !== 200) {
|
|
|
clearTask()
|
|
clearTask()
|
|
|
|
|
+ handleFinalPreparationFailure("未能取得最终课件")
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -674,24 +701,6 @@ async function loadDraft(taskId: string) {
|
|
|
await prepareDraftForUse()
|
|
await prepareDraftForUse()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-async function loadSampleResult(taskId: string) {
|
|
|
|
|
- const res = await httpAjaxErrMsg(aiCoursewareResultApi, taskId)
|
|
|
|
|
- if (res.code !== 200) {
|
|
|
|
|
- clearTask()
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
- const result = res.data as AiCoursewareGenerateResult
|
|
|
|
|
- outline.value = result.outline
|
|
|
|
|
- restoreProtectedPageContent(outline.value)
|
|
|
|
|
- outline.value.style = selectedStyle.value
|
|
|
|
|
- outline.value.templateId = selectedTemplateId.value
|
|
|
|
|
- sampleFeedbackTags.value = []
|
|
|
|
|
- sampleFeedbackText.value = ""
|
|
|
|
|
- workflowStep.value = 2
|
|
|
|
|
- clearTask()
|
|
|
|
|
- ElMessage.success(`第${samplePageNo.value}页样张已重新生成,请再次确认视觉方向`)
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
function rebuildDraftPreview() {
|
|
function rebuildDraftPreview() {
|
|
|
if (!draftOutline.value) {
|
|
if (!draftOutline.value) {
|
|
|
draftConverted.value = undefined
|
|
draftConverted.value = undefined
|
|
@@ -712,7 +721,6 @@ async function regenerateDraftPage(index: number) {
|
|
|
if (!draftOutline.value || !pptWork.id) return
|
|
if (!draftOutline.value || !pptWork.id) return
|
|
|
draftPreparationState.value = "optimizing"
|
|
draftPreparationState.value = "optimizing"
|
|
|
automaticRepairAttempts.value = 0
|
|
automaticRepairAttempts.value = 0
|
|
|
- taskMode.value = "full"
|
|
|
|
|
restoreProtectedPageContent(draftOutline.value)
|
|
restoreProtectedPageContent(draftOutline.value)
|
|
|
if (sampleFeedbackTags.value.includes("画风不合适")) paletteVariant.value = (paletteVariant.value + 1) % 3
|
|
if (sampleFeedbackTags.value.includes("画风不合适")) paletteVariant.value = (paletteVariant.value + 1) % 3
|
|
|
const feedback = [...sampleFeedbackTags.value, sampleFeedbackText.value.trim()].filter(Boolean).join(";")
|
|
const feedback = [...sampleFeedbackTags.value, sampleFeedbackText.value.trim()].filter(Boolean).join(";")
|
|
@@ -734,37 +742,19 @@ async function regenerateDraftPage(index: number) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function applyDraft() {
|
|
function applyDraft() {
|
|
|
- if (!draftConverted.value || !draftQuality.value?.passed) return
|
|
|
|
|
- slidesStore.setTitle(draftConverted.value.title)
|
|
|
|
|
- slidesStore.setTheme(draftConverted.value.theme)
|
|
|
|
|
- slidesStore.setSlides(draftConverted.value.slides)
|
|
|
|
|
|
|
+ if (!displayedConverted.value || draftPreparationState.value !== "ready") return
|
|
|
|
|
+ slidesStore.setTitle(displayedConverted.value.title)
|
|
|
|
|
+ slidesStore.setTheme(displayedConverted.value.theme)
|
|
|
|
|
+ slidesStore.setSlides(displayedConverted.value.slides)
|
|
|
slidesStore.updateSlideIndex(0)
|
|
slidesStore.updateSlideIndex(0)
|
|
|
addHistorySnapshot()
|
|
addHistorySnapshot()
|
|
|
ElMessage.success("PPT已应用,请检查后手动保存")
|
|
ElMessage.success("PPT已应用,请检查后手动保存")
|
|
|
emit("update:visible", false)
|
|
emit("update:visible", false)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function backToOutline() {
|
|
|
|
|
- clearTask()
|
|
|
|
|
- if (draftOutline.value) outline.value = draftOutline.value
|
|
|
|
|
- draftOutline.value = undefined
|
|
|
|
|
- draftConverted.value = undefined
|
|
|
|
|
- draftQuality.value = undefined
|
|
|
|
|
- draftPreparationState.value = "idle"
|
|
|
|
|
- automaticRepairAttempts.value = 0
|
|
|
|
|
- workflowStep.value = 2
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
async function adjustCurrentPage() {
|
|
async function adjustCurrentPage() {
|
|
|
- if (draftConverted.value) {
|
|
|
|
|
- await regenerateDraftPage(selectedDraftPageIndex.value)
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
- await regenerateSamplePage()
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function invalidateVisualApproval() {
|
|
|
|
|
- approvedSamplePages.value = []
|
|
|
|
|
|
|
+ if (!displayedConverted.value) return
|
|
|
|
|
+ await regenerateDraftPage(selectedDraftPageIndex.value)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async function confirmSamplesAndGenerate() {
|
|
async function confirmSamplesAndGenerate() {
|
|
@@ -781,50 +771,52 @@ async function confirmSamplesAndGenerate() {
|
|
|
try {
|
|
try {
|
|
|
await Promise.all(backgroundUrls.map(preloadImage))
|
|
await Promise.all(backgroundUrls.map(preloadImage))
|
|
|
} catch {
|
|
} catch {
|
|
|
- ElMessage.error("模板背景加载失败,请更换模板或稍后重试")
|
|
|
|
|
|
|
+ handleFinalPreparationFailure("模板背景加载失败")
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- approvedSamplePages.value = outline.value.pages.map(page => page.pageNo)
|
|
|
|
|
await handleGenerate()
|
|
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") {
|
|
|
- approvedSamplePages.value = approvedSamplePages.value.filter(pageNo => pageNo !== samplePageNo.value)
|
|
|
|
|
|
|
+ const page = workspacePage.value || draftOutline.value?.pages[selectedDraftPageIndex.value] || outline.value?.pages[selectedDraftPageIndex.value]
|
|
|
console.error("AI课件模板背景加载失败", {
|
|
console.error("AI课件模板背景加载失败", {
|
|
|
templateId: selectedTemplateId.value,
|
|
templateId: selectedTemplateId.value,
|
|
|
- pageNo: samplePageNo.value,
|
|
|
|
|
- layout: outline.value?.pages[samplePageIndex.value]?.visualPlan?.layout,
|
|
|
|
|
|
|
+ pageNo: page?.pageNo || selectedDraftPageIndex.value + 1,
|
|
|
|
|
+ layout: page?.visualPlan?.layout,
|
|
|
url: payload.url
|
|
url: payload.url
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function toggleSampleFeedback(item: string) {
|
|
function toggleSampleFeedback(item: string) {
|
|
|
- approvedSamplePages.value = approvedSamplePages.value.filter(pageNo => pageNo !== samplePageNo.value)
|
|
|
|
|
sampleFeedbackTags.value = sampleFeedbackTags.value.includes(item)
|
|
sampleFeedbackTags.value = sampleFeedbackTags.value.includes(item)
|
|
|
? sampleFeedbackTags.value.filter(value => value !== item)
|
|
? sampleFeedbackTags.value.filter(value => value !== item)
|
|
|
: [...sampleFeedbackTags.value, item]
|
|
: [...sampleFeedbackTags.value, item]
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function selectTemplate(templateId: AiCoursewareTemplateId, fromSample = false) {
|
|
|
|
|
- if (draftOutline.value) backToOutline()
|
|
|
|
|
|
|
+async function selectTemplate(templateId: AiCoursewareTemplateId, fromSample = false) {
|
|
|
|
|
+ if (templateId === selectedTemplateId.value && displayedConverted.value) {
|
|
|
|
|
+ templatePickerExpanded.value = false
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
templateSelectionMode.value = "manual"
|
|
templateSelectionMode.value = "manual"
|
|
|
selectedTemplateId.value = templateId
|
|
selectedTemplateId.value = templateId
|
|
|
if (fromSample) {
|
|
if (fromSample) {
|
|
|
templatePickerExpanded.value = false
|
|
templatePickerExpanded.value = false
|
|
|
showAllTemplates.value = false
|
|
showAllTemplates.value = false
|
|
|
- ElMessage.success(`已切换为${getAiCoursewareTemplate(templateId).name},已同步重排样张结构`)
|
|
|
|
|
|
|
+ ElMessage.info(`正在生成并优化${getAiCoursewareTemplate(templateId).name}`)
|
|
|
}
|
|
}
|
|
|
|
|
+ await regenerateFinalPreview()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function useRecommendedTemplate() {
|
|
|
|
|
- if (draftOutline.value) backToOutline()
|
|
|
|
|
|
|
+async function useRecommendedTemplate() {
|
|
|
templateSelectionMode.value = "auto"
|
|
templateSelectionMode.value = "auto"
|
|
|
selectedTemplateId.value = templateRecommendation.value.id
|
|
selectedTemplateId.value = templateRecommendation.value.id
|
|
|
- ElMessage.success(`已使用系统推荐的${recommendedTemplate.value.name}`)
|
|
|
|
|
|
|
+ ElMessage.info(`正在生成并优化${recommendedTemplate.value.name}`)
|
|
|
|
|
+ await regenerateFinalPreview()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function templateCoverStyle(templateId: AiCoursewareTemplateId): Record<string, string> {
|
|
function templateCoverStyle(templateId: AiCoursewareTemplateId): Record<string, string> {
|
|
@@ -859,14 +851,15 @@ async function prepareDraftForUse() {
|
|
|
rebuildDraftPreview()
|
|
rebuildDraftPreview()
|
|
|
|
|
|
|
|
if (draftQuality.value?.passed) {
|
|
if (draftQuality.value?.passed) {
|
|
|
|
|
+ publishFinalPreview()
|
|
|
draftPreparationState.value = "ready"
|
|
draftPreparationState.value = "ready"
|
|
|
- ElMessage.success("课件已完成自动检查,可以直接应用")
|
|
|
|
|
|
|
+ ElMessage.success("最终课件已完成优化,可以直接预览并应用")
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
console.warn("AI课件自动检查发现待修复项", draftQuality.value?.errors)
|
|
console.warn("AI课件自动检查发现待修复项", draftQuality.value?.errors)
|
|
|
if (automaticRepairAttempts.value >= MAX_AUTOMATIC_REPAIR_ATTEMPTS) {
|
|
if (automaticRepairAttempts.value >= MAX_AUTOMATIC_REPAIR_ATTEMPTS) {
|
|
|
- draftPreparationState.value = "failed"
|
|
|
|
|
|
|
+ handleFinalPreparationFailure()
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -876,14 +869,13 @@ async function prepareDraftForUse() {
|
|
|
|
|
|
|
|
async function requestAutomaticRepair() {
|
|
async function requestAutomaticRepair() {
|
|
|
if (!draftOutline.value || !pptWork.id || !draftQuality.value) {
|
|
if (!draftOutline.value || !pptWork.id || !draftQuality.value) {
|
|
|
- draftPreparationState.value = "failed"
|
|
|
|
|
|
|
+ handleFinalPreparationFailure()
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
const repairSummary = draftQuality.value.errors
|
|
const repairSummary = draftQuality.value.errors
|
|
|
.slice(0, 12)
|
|
.slice(0, 12)
|
|
|
.map(issue => `${issue.pageNo ? `第${issue.pageNo}页` : "整套课件"}:${issue.message}`)
|
|
.map(issue => `${issue.pageNo ? `第${issue.pageNo}页` : "整套课件"}:${issue.message}`)
|
|
|
.join(";")
|
|
.join(";")
|
|
|
- taskMode.value = "full"
|
|
|
|
|
const res = await httpAjaxErrMsg(aiCoursewareGenerateApi, {
|
|
const res = await httpAjaxErrMsg(aiCoursewareGenerateApi, {
|
|
|
id: pptWork.id,
|
|
id: pptWork.id,
|
|
|
fromType: queryParams.pptResourcesType || queryParams.fromType,
|
|
fromType: queryParams.pptResourcesType || queryParams.fromType,
|
|
@@ -903,7 +895,7 @@ async function requestAutomaticRepair() {
|
|
|
async function continueAutomaticRepairAfterFailure() {
|
|
async function continueAutomaticRepairAfterFailure() {
|
|
|
clearTask()
|
|
clearTask()
|
|
|
if (automaticRepairAttempts.value >= MAX_AUTOMATIC_REPAIR_ATTEMPTS) {
|
|
if (automaticRepairAttempts.value >= MAX_AUTOMATIC_REPAIR_ATTEMPTS) {
|
|
|
- draftPreparationState.value = "failed"
|
|
|
|
|
|
|
+ handleFinalPreparationFailure()
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
automaticRepairAttempts.value += 1
|
|
automaticRepairAttempts.value += 1
|
|
@@ -1537,6 +1529,45 @@ function shortProgressLabel(message?: string) {
|
|
|
font-size: 11px;
|
|
font-size: 11px;
|
|
|
font-weight: 700;
|
|
font-weight: 700;
|
|
|
}
|
|
}
|
|
|
|
|
+.final-preview-placeholder {
|
|
|
|
|
+ min-height: 320px;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ margin-top: 8px;
|
|
|
|
|
+ border: 1px solid #e1e8f0;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ background: #f7f9fc;
|
|
|
|
|
+ color: #52616b;
|
|
|
|
|
+ strong {
|
|
|
|
|
+ color: #263442;
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ }
|
|
|
|
|
+ small {
|
|
|
|
|
+ color: #7b8794;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+ &.failed {
|
|
|
|
|
+ background: #fffaf3;
|
|
|
|
|
+ }
|
|
|
|
|
+ .loading-dot {
|
|
|
|
|
+ width: 20px;
|
|
|
|
|
+ height: 20px;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+.placeholder-icon {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ width: 28px;
|
|
|
|
|
+ height: 28px;
|
|
|
|
|
+ place-items: center;
|
|
|
|
|
+ border: 1px solid #e5b66d;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ color: #a46108;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+}
|
|
|
.unified-review-workspace {
|
|
.unified-review-workspace {
|
|
|
min-height: 0;
|
|
min-height: 0;
|
|
|
flex: 1;
|
|
flex: 1;
|
|
@@ -1629,6 +1660,7 @@ function shortProgressLabel(message?: string) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
.workspace-canvas-shell {
|
|
.workspace-canvas-shell {
|
|
|
|
|
+ position: relative;
|
|
|
min-height: 0;
|
|
min-height: 0;
|
|
|
flex: 1 1 auto;
|
|
flex: 1 1 auto;
|
|
|
display: flex;
|
|
display: flex;
|
|
@@ -1644,6 +1676,28 @@ function shortProgressLabel(message?: string) {
|
|
|
box-shadow: 0 10px 28px rgba(31, 41, 51, 0.15);
|
|
box-shadow: 0 10px 28px rgba(31, 41, 51, 0.15);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+.workspace-processing-overlay {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ inset: 10px;
|
|
|
|
|
+ z-index: 5;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 7px;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ background: rgba(245, 248, 252, 0.92);
|
|
|
|
|
+ color: #52616b;
|
|
|
|
|
+ backdrop-filter: blur(2px);
|
|
|
|
|
+ strong {
|
|
|
|
|
+ color: #263442;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ }
|
|
|
|
|
+ small {
|
|
|
|
|
+ color: #7b8794;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
.workspace-feedback {
|
|
.workspace-feedback {
|
|
|
flex: 0 0 auto;
|
|
flex: 0 0 auto;
|
|
|
margin-top: 8px;
|
|
margin-top: 8px;
|