|
|
@@ -43,8 +43,8 @@
|
|
|
<div v-if="workflowStep === 1" class="setup-stage simplified-setup">
|
|
|
<div class="stage-intro">
|
|
|
<div>
|
|
|
- <div class="stage-title">告诉系统这节课怎么上</div>
|
|
|
- <div class="stage-desc">填写必要信息即可,模板和素材由系统自动匹配。</div>
|
|
|
+ <div class="stage-title">设置这节课的基本需求</div>
|
|
|
+ <div class="stage-desc">只需确认课时和篇幅,样式、素材与质量检查由系统完成。</div>
|
|
|
</div>
|
|
|
<span v-if="outline" class="saved-state">已有大纲,可继续确认</span>
|
|
|
</div>
|
|
|
@@ -105,60 +105,48 @@
|
|
|
</div>
|
|
|
|
|
|
<div v-if="workflowStep === 3 && draftConverted" class="result-preview">
|
|
|
- <div class="preview-head">
|
|
|
+ <div :class="['draft-status', draftPreparationState]">
|
|
|
+ <span class="draft-status-icon">{{ draftPreparationState === "ready" ? "✓" : draftPreparationState === "failed" ? "!" : "" }}</span>
|
|
|
<div>
|
|
|
- <div class="preview-title">全稿预览</div>
|
|
|
- <div class="preview-sub">共{{ draftConverted.slides.length }}页,确认后才会替换当前课件</div>
|
|
|
- </div>
|
|
|
- <div :class="['quality-badge', { passed: draftQuality?.passed }]">
|
|
|
- {{ draftQuality?.passed ? "可以直接使用" : `还有${draftQuality?.errors.length || 0}页需要优化` }}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div v-if="draftQuality?.issues.length" class="quality-list">
|
|
|
- <div
|
|
|
- v-for="issue in draftQuality.issues"
|
|
|
- :key="`${issue.code}-${issue.pageNo || 0}-${issue.message}`"
|
|
|
- :class="['quality-item', issue.severity]"
|
|
|
- >
|
|
|
- <span>{{ issue.severity === "error" ? "阻止应用" : "建议检查" }}</span>
|
|
|
- <span>{{ issue.pageNo ? `第${issue.pageNo}页:` : "" }}{{ issue.message }}</span>
|
|
|
+ <strong>{{ draftStatusTitle }}</strong>
|
|
|
+ <span>{{ draftStatusDescription }}</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div v-if="draftQuality && !draftQuality.passed" class="quality-actions">
|
|
|
- <button type="button" :disabled="working" @click="autoOptimizeDraft">自动优化全部页面</button>
|
|
|
- <button v-if="selectedTemplateFit.level === 'low'" type="button" :disabled="working" @click="useRecommendedTemplateForDraft">
|
|
|
- 切换推荐模板
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- <div v-if="selectedDraftPage" class="selected-draft-actions">
|
|
|
- <div>
|
|
|
- <span>当前页面</span><strong>第{{ selectedDraftPageIndex + 1 }}页 · {{ selectedDraftPage.title }}</strong>
|
|
|
+
|
|
|
+ <div class="draft-workspace">
|
|
|
+ <div class="draft-page-list" aria-label="课件页面">
|
|
|
+ <button
|
|
|
+ v-for="(slide, index) in draftConverted.slides"
|
|
|
+ :key="slide.id"
|
|
|
+ type="button"
|
|
|
+ :class="['draft-page-item', { active: selectedDraftPageIndex === index }]"
|
|
|
+ @click="selectedDraftPageIndex = index"
|
|
|
+ >
|
|
|
+ <ThumbnailSlide :slide="slide" :size="148" />
|
|
|
+ <span>{{ formatPageNo(index + 1) }} · {{ draftOutline?.pages[index]?.title || "未命名页面" }}</span>
|
|
|
+ </button>
|
|
|
</div>
|
|
|
- <div>
|
|
|
- <button type="button" :disabled="working" @click="compactDraftPage(selectedDraftPageIndex)">精简文字</button>
|
|
|
- <button type="button" :disabled="working || !selectedDraftPage.image" @click="removeDraftImage(selectedDraftPageIndex)">移除图片</button>
|
|
|
- <button type="button" :disabled="working" @click="regenerateDraftPage(selectedDraftPageIndex)">重新生成本页</button>
|
|
|
+
|
|
|
+ <div v-if="selectedDraftSlide" class="draft-preview-pane">
|
|
|
+ <div class="draft-preview-toolbar">
|
|
|
+ <div>
|
|
|
+ <span>第{{ selectedDraftPageIndex + 1 }} / {{ draftConverted.slides.length }}页</span>
|
|
|
+ <strong>{{ selectedDraftPage?.title || "课件页面" }}</strong>
|
|
|
+ </div>
|
|
|
+ <button type="button" :disabled="working" @click="regenerateDraftPage(selectedDraftPageIndex)">重新生成本页</button>
|
|
|
+ </div>
|
|
|
+ <div class="draft-canvas-shell">
|
|
|
+ <ThumbnailSlide :slide="selectedDraftSlide" :size="680" />
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div class="preview-grid">
|
|
|
- <button
|
|
|
- v-for="(slide, index) in draftConverted.slides"
|
|
|
- :key="slide.id"
|
|
|
- type="button"
|
|
|
- :class="['preview-card', { active: selectedDraftPageIndex === index }]"
|
|
|
- @click="selectedDraftPageIndex = index"
|
|
|
- >
|
|
|
- <ThumbnailSlide :slide="slide" :size="220" />
|
|
|
- <div class="preview-page-no">{{ formatPageNo(index + 1) }}</div>
|
|
|
- </button>
|
|
|
- </div>
|
|
|
</div>
|
|
|
|
|
|
<div v-else-if="workflowStep === 2 && outline" class="outline">
|
|
|
<div class="sample-stage-head">
|
|
|
<div>
|
|
|
- <strong>确认课件的整体样式</strong>
|
|
|
- <span>当前模板:{{ selectedTemplate.name }}。查看三类代表页面,一次确认即可。</span>
|
|
|
+ <strong>选择喜欢的课件样式</strong>
|
|
|
+ <span>已推荐“{{ selectedTemplate.name }}”,查看代表页面后可直接生成完整课件。</span>
|
|
|
</div>
|
|
|
<button type="button" :disabled="working" @click="templatePickerExpanded = !templatePickerExpanded">
|
|
|
{{ templatePickerExpanded ? "收起模板" : "更换模板" }}
|
|
|
@@ -210,7 +198,7 @@
|
|
|
<div class="sample-hint">{{ samplePageReason }}</div>
|
|
|
<div v-if="sampleBackgroundState.status === 'error'" class="template-load-error">模板背景加载失败,请更换模板或稍后重试</div>
|
|
|
<div class="sample-feedback">
|
|
|
- <div class="feedback-label">这一页哪里需要调整?</div>
|
|
|
+ <div class="feedback-label">不满意?告诉系统怎么调整</div>
|
|
|
<div class="feedback-chips">
|
|
|
<button
|
|
|
v-for="item in sampleFeedbackOptions"
|
|
|
@@ -287,17 +275,19 @@
|
|
|
<Button @click="emit('update:visible', false)" :disabled="working">取消</Button>
|
|
|
<template v-if="workflowStep === 1">
|
|
|
<Button v-if="outline" @click="workflowStep = 2" :disabled="working">继续选择样式</Button>
|
|
|
- <Button type="primary" @click="handleOutline" :disabled="working">{{ outline ? "按当前设置重新生成" : "生成大纲并选择样式" }}</Button>
|
|
|
+ <Button type="primary" @click="handleOutline" :disabled="working">{{ outline ? "按当前设置重新生成" : "开始生成课件" }}</Button>
|
|
|
</template>
|
|
|
<template v-else-if="workflowStep === 2">
|
|
|
<Button @click="workflowStep = 1" :disabled="working">返回修改需求</Button>
|
|
|
<Button type="primary" @click="confirmSamplesAndGenerate" :disabled="working || sampleBackgroundState.status === 'error'"
|
|
|
- >确认样式并生成课件</Button
|
|
|
+ >使用此样式并生成课件</Button
|
|
|
>
|
|
|
</template>
|
|
|
<template v-else>
|
|
|
- <Button @click="backToOutline" :disabled="working">返回调整</Button>
|
|
|
- <Button type="primary" @click="applyDraft" :disabled="working || !draftQuality?.passed">确认并应用到当前课件</Button>
|
|
|
+ <Button v-if="draftPreparationState !== 'optimizing'" @click="backToOutline">返回调整</Button>
|
|
|
+ <span v-if="draftPreparationState === 'optimizing'" class="footer-progress">正在准备可用课件…</span>
|
|
|
+ <Button v-else-if="draftPreparationState === 'failed'" type="primary" @click="retryAutomaticOptimization">重新自动优化</Button>
|
|
|
+ <Button v-else-if="draftPreparationState === 'ready'" type="primary" @click="applyDraft">应用到当前课件</Button>
|
|
|
</template>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -357,9 +347,9 @@ const outline = ref<AiCoursewareOutline>()
|
|
|
const taskInfo = ref<AiCoursewareTaskInfo>()
|
|
|
const workflowStep = ref<1 | 2 | 3>(1)
|
|
|
const workflowSteps = [
|
|
|
- { value: 1, label: "设置课程" },
|
|
|
- { value: 2, label: "确认样式" },
|
|
|
- { value: 3, label: "检查课件" }
|
|
|
+ { value: 1, label: "设置需求" },
|
|
|
+ { value: 2, label: "选择样式" },
|
|
|
+ { value: 3, label: "预览并应用" }
|
|
|
] as const
|
|
|
const taskMode = ref<"full" | "sample">("full")
|
|
|
const coursewareTemplates = AI_COURSEWARE_TEMPLATES
|
|
|
@@ -390,11 +380,22 @@ const sampleFeedbackText = ref("")
|
|
|
const draftOutline = ref<AiCoursewareOutline>()
|
|
|
const draftConverted = ref<{ title: string; theme: Partial<SlideTheme>; slides: Slide[] }>()
|
|
|
const draftQuality = ref<AiPptQualityReport>()
|
|
|
+const draftPreparationState = ref<"idle" | "optimizing" | "ready" | "failed">("idle")
|
|
|
+const automaticRepairAttempts = ref(0)
|
|
|
+const MAX_AUTOMATIC_REPAIR_ATTEMPTS = 1
|
|
|
+const TEACHER_VISIBLE_PATTERN = /(?:教师|老师)(?:讲述|提问|引导|指导|组织|巡视|示范)|(?:引导|指导|组织)学生/
|
|
|
+const PLACEHOLDER_PATTERN = /(?:待补充|待完善|请输入|可参考教材|placeholder|lorem ipsum|\{\{.+?\}\}|\[.+?占位.+?\])/i
|
|
|
const pollTimer = ref<number>()
|
|
|
const selectedPageNo = ref(1)
|
|
|
const selectedDraftPageIndex = ref(0)
|
|
|
const outlineLoading = ref(false)
|
|
|
-const working = computed(() => outlineLoading.value || taskInfo.value?.status === "PENDING" || taskInfo.value?.status === "RUNNING")
|
|
|
+const working = computed(
|
|
|
+ () =>
|
|
|
+ outlineLoading.value ||
|
|
|
+ draftPreparationState.value === "optimizing" ||
|
|
|
+ taskInfo.value?.status === "PENDING" ||
|
|
|
+ taskInfo.value?.status === "RUNNING"
|
|
|
+)
|
|
|
const selectedTemplate = computed(() => getAiCoursewareTemplate(selectedTemplateId.value))
|
|
|
const selectedStyle = computed(() => selectedTemplate.value.baseStyle)
|
|
|
const templateRecommendation = computed(() =>
|
|
|
@@ -488,6 +489,17 @@ const representativeSamples = computed(() => {
|
|
|
return selected.slice(0, 3).map((item, index) => ({ ...item, order: index + 1 }))
|
|
|
})
|
|
|
const selectedDraftPage = computed(() => draftOutline.value?.pages[selectedDraftPageIndex.value])
|
|
|
+const selectedDraftSlide = computed(() => draftConverted.value?.slides[selectedDraftPageIndex.value])
|
|
|
+const draftStatusTitle = computed(() => {
|
|
|
+ if (draftPreparationState.value === "ready") return "课件已准备完成"
|
|
|
+ if (draftPreparationState.value === "failed") return "部分页面还未处理完成"
|
|
|
+ return "正在自动检查和优化课件"
|
|
|
+})
|
|
|
+const draftStatusDescription = computed(() => {
|
|
|
+ if (draftPreparationState.value === "ready") return "系统已完成内容、排版和教学可用性检查,可以直接预览并应用。"
|
|
|
+ if (draftPreparationState.value === "failed") return "无需逐项修改,点击重新自动优化即可继续。"
|
|
|
+ return "正在清理重复内容、调整版式并修复问题页面,请稍候。"
|
|
|
+})
|
|
|
const requiredSamplePages = computed(() => representativeSamples.value.map(item => item.pageNo))
|
|
|
const allSamplesApproved = computed(
|
|
|
() => requiredSamplePages.value.length === 3 && requiredSamplePages.value.every(pageNo => approvedSamplePages.value.includes(pageNo))
|
|
|
@@ -584,6 +596,8 @@ async function handleGenerate() {
|
|
|
|
|
|
draftConverted.value = undefined
|
|
|
draftQuality.value = undefined
|
|
|
+ draftPreparationState.value = "idle"
|
|
|
+ automaticRepairAttempts.value = 0
|
|
|
sourceOutline.style = selectedStyle.value
|
|
|
sourceOutline.templateId = selectedTemplateId.value
|
|
|
taskMode.value = "full"
|
|
|
@@ -644,7 +658,13 @@ async function fetchTask(taskId: string) {
|
|
|
clearPollTimer()
|
|
|
const message = taskInfo.value.message || "AI课件生成失败"
|
|
|
clearTask()
|
|
|
- ElMessage.error(message)
|
|
|
+ if (workflowStep.value === 3) {
|
|
|
+ draftPreparationState.value = "failed"
|
|
|
+ console.error("AI课件自动优化失败", message)
|
|
|
+ ElMessage.error("自动优化未完成,请重新尝试")
|
|
|
+ } else {
|
|
|
+ ElMessage.error(message)
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -670,12 +690,10 @@ async function loadDraft(taskId: string) {
|
|
|
const result = res.data as AiCoursewareGenerateResult
|
|
|
draftOutline.value = result.outline
|
|
|
draftOutline.value.templateId = selectedTemplateId.value
|
|
|
- rebuildDraftPreview()
|
|
|
selectedDraftPageIndex.value = 0
|
|
|
workflowStep.value = 3
|
|
|
clearTask()
|
|
|
- if (draftQuality.value?.passed) ElMessage.success("课件草稿已生成,请预览后确认应用")
|
|
|
- else ElMessage.error(`课件草稿存在${draftQuality.value?.errors.length || 0}项阻止应用的问题`)
|
|
|
+ await prepareDraftForUse()
|
|
|
}
|
|
|
|
|
|
async function loadSampleResult(taskId: string) {
|
|
|
@@ -711,53 +729,24 @@ function rebuildDraftPreview() {
|
|
|
draftQuality.value = inspectAiPptQuality(draftOutline.value, draftConverted.value.slides, preferences)
|
|
|
}
|
|
|
|
|
|
-function compactDraftPage(index: number) {
|
|
|
- const page = draftOutline.value?.pages[index]
|
|
|
- if (!page) return
|
|
|
- const compact = (value: string | undefined, limit: number) =>
|
|
|
- String(value || "")
|
|
|
- .replace(/\s+/g, " ")
|
|
|
- .trim()
|
|
|
- .slice(0, limit)
|
|
|
- if (page.studentContent) {
|
|
|
- page.studentContent.headline = compact(page.studentContent.headline, 18)
|
|
|
- page.studentContent.points = [...new Set(page.studentContent.points || [])]
|
|
|
- .slice(0, 3)
|
|
|
- .map(item => compact(item, 20))
|
|
|
- .filter(Boolean)
|
|
|
- page.bullets = [...page.studentContent.points]
|
|
|
- }
|
|
|
- if (page.activity) {
|
|
|
- page.activity.task = compact(page.activity.task, 22)
|
|
|
- page.activity.steps = [...new Set(page.activity.steps || [])]
|
|
|
- .slice(0, 3)
|
|
|
- .map(item => compact(item, 20))
|
|
|
- .filter(Boolean)
|
|
|
- }
|
|
|
- rebuildDraftPreview()
|
|
|
-}
|
|
|
-
|
|
|
-function removeDraftImage(index: number) {
|
|
|
- const page = draftOutline.value?.pages[index]
|
|
|
- if (!page) return
|
|
|
- page.image = undefined
|
|
|
- page.resourceBindings = (page.resourceBindings || []).filter(binding => binding.resourceType !== "IMG")
|
|
|
- rebuildDraftPreview()
|
|
|
-}
|
|
|
-
|
|
|
async function regenerateDraftPage(index: number) {
|
|
|
if (!draftOutline.value || !pptWork.id) return
|
|
|
+ draftPreparationState.value = "optimizing"
|
|
|
+ automaticRepairAttempts.value = 0
|
|
|
taskMode.value = "full"
|
|
|
const res = await httpAjaxErrMsg(aiCoursewareGenerateApi, {
|
|
|
id: pptWork.id,
|
|
|
fromType: queryParams.pptResourcesType || queryParams.fromType,
|
|
|
outline: draftOutline.value,
|
|
|
templateId: selectedTemplateId.value,
|
|
|
- visualPrompt: buildVisualPrompt(),
|
|
|
+ visualPrompt: `${buildVisualPrompt()}请重新生成本页并主动修正学生可见内容、文字密度和版式问题。`,
|
|
|
preferences: { ...preferences },
|
|
|
regeneratePageNo: index + 1
|
|
|
})
|
|
|
- if (res.code !== 200) return
|
|
|
+ if (res.code !== 200) {
|
|
|
+ draftPreparationState.value = "failed"
|
|
|
+ return
|
|
|
+ }
|
|
|
taskInfo.value = res.data as AiCoursewareTaskInfo
|
|
|
await pollTask(taskInfo.value.taskId)
|
|
|
}
|
|
|
@@ -779,6 +768,8 @@ function backToOutline() {
|
|
|
draftOutline.value = undefined
|
|
|
draftConverted.value = undefined
|
|
|
draftQuality.value = undefined
|
|
|
+ draftPreparationState.value = "idle"
|
|
|
+ automaticRepairAttempts.value = 0
|
|
|
workflowStep.value = 2
|
|
|
}
|
|
|
|
|
|
@@ -871,44 +862,149 @@ function templateFit(templateId: AiCoursewareTemplateId) {
|
|
|
: { templateId, score: 80, level: "usable" as const, label: "可以使用" as const, reason: "生成大纲后评估" }
|
|
|
}
|
|
|
|
|
|
-function useRecommendedTemplateForDraft() {
|
|
|
- useRecommendedTemplate()
|
|
|
- if (draftOutline.value) {
|
|
|
- draftOutline.value.templateId = selectedTemplateId.value
|
|
|
- rebuildDraftPreview()
|
|
|
+async function prepareDraftForUse() {
|
|
|
+ if (!draftOutline.value) return
|
|
|
+ draftPreparationState.value = "optimizing"
|
|
|
+ normalizeDraftLocally()
|
|
|
+ rebuildDraftPreview()
|
|
|
+
|
|
|
+ if (draftQuality.value?.passed) {
|
|
|
+ draftPreparationState.value = "ready"
|
|
|
+ ElMessage.success("课件已完成自动检查,可以直接应用")
|
|
|
+ return
|
|
|
}
|
|
|
+
|
|
|
+ console.warn("AI课件自动检查发现待修复项", draftQuality.value?.errors)
|
|
|
+ if (automaticRepairAttempts.value >= MAX_AUTOMATIC_REPAIR_ATTEMPTS) {
|
|
|
+ draftPreparationState.value = "failed"
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ automaticRepairAttempts.value += 1
|
|
|
+ await requestAutomaticRepair()
|
|
|
}
|
|
|
|
|
|
-function autoOptimizeDraft() {
|
|
|
+async function requestAutomaticRepair() {
|
|
|
+ if (!draftOutline.value || !pptWork.id || !draftQuality.value) {
|
|
|
+ draftPreparationState.value = "failed"
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const repairSummary = draftQuality.value.errors
|
|
|
+ .slice(0, 12)
|
|
|
+ .map(issue => `${issue.pageNo ? `第${issue.pageNo}页` : "整套课件"}:${issue.message}`)
|
|
|
+ .join(";")
|
|
|
+ taskMode.value = "full"
|
|
|
+ const res = await httpAjaxErrMsg(aiCoursewareGenerateApi, {
|
|
|
+ id: pptWork.id,
|
|
|
+ fromType: queryParams.pptResourcesType || queryParams.fromType,
|
|
|
+ outline: draftOutline.value,
|
|
|
+ templateId: selectedTemplateId.value,
|
|
|
+ visualPrompt: `${buildVisualPrompt()}系统检查发现以下问题,请在保持教学目标和页数不变的前提下全部修正:${repairSummary}。`,
|
|
|
+ preferences: { ...preferences }
|
|
|
+ })
|
|
|
+ if (res.code !== 200) {
|
|
|
+ draftPreparationState.value = "failed"
|
|
|
+ return
|
|
|
+ }
|
|
|
+ taskInfo.value = res.data as AiCoursewareTaskInfo
|
|
|
+ await pollTask(taskInfo.value.taskId)
|
|
|
+}
|
|
|
+
|
|
|
+async function retryAutomaticOptimization() {
|
|
|
+ automaticRepairAttempts.value = 0
|
|
|
+ await prepareDraftForUse()
|
|
|
+}
|
|
|
+
|
|
|
+function normalizeDraftLocally() {
|
|
|
if (!draftOutline.value) return
|
|
|
+ const imageUseCounts = new Map<string, number>()
|
|
|
draftOutline.value.pages.forEach((page, index) => {
|
|
|
if (!page.visualPlan) page.visualPlan = {}
|
|
|
- const points = [...new Set(page.studentContent?.points || page.bullets || [])]
|
|
|
- .map(item =>
|
|
|
- String(item || "")
|
|
|
- .replace(/\s+/g, " ")
|
|
|
- .trim()
|
|
|
- .slice(0, 24)
|
|
|
- )
|
|
|
- .filter(Boolean)
|
|
|
- .slice(0, 4)
|
|
|
if (!page.studentContent) page.studentContent = { headline: "", points: [] }
|
|
|
+
|
|
|
+ const movedTeacherNotes: string[] = []
|
|
|
+ const headline = cleanDraftText(page.studentContent.headline, 24)
|
|
|
+ if (isTeacherOnlyText(headline)) movedTeacherNotes.push(headline)
|
|
|
+ page.studentContent.headline = isTeacherOnlyText(headline) ? "" : headline
|
|
|
+
|
|
|
+ const excluded = new Set([page.title, page.studentContent.headline].map(normalizeDraftText).filter(Boolean))
|
|
|
+ const pointKeys = new Set<string>()
|
|
|
+ const points = (page.studentContent.points?.length ? page.studentContent.points : page.bullets || [])
|
|
|
+ .map(item => cleanDraftText(item, 22))
|
|
|
+ .filter(item => {
|
|
|
+ if (!item) return false
|
|
|
+ if (isTeacherOnlyText(item)) {
|
|
|
+ movedTeacherNotes.push(item)
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ const key = normalizeDraftText(item)
|
|
|
+ if (!key || excluded.has(key) || pointKeys.has(key)) return false
|
|
|
+ pointKeys.add(key)
|
|
|
+ return true
|
|
|
+ })
|
|
|
+ .slice(0, 4)
|
|
|
page.studentContent.points = points
|
|
|
page.bullets = [...points]
|
|
|
page.visualPlan.visualDensity = points.length <= 2 ? "low" : points.length >= 4 ? "high" : "medium"
|
|
|
+
|
|
|
+ if (movedTeacherNotes.length) {
|
|
|
+ if (!page.activity) page.activity = { task: "", steps: [] }
|
|
|
+ page.activity.steps = uniqueDraftTexts([...(page.activity.steps || []), ...movedTeacherNotes])
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!page.classroomAction) {
|
|
|
+ page.classroomAction = { teacherInstruction: "", studentTask: page.studentContent.headline || page.title, interactionType: "" }
|
|
|
+ }
|
|
|
+ const studentTask = cleanDraftText(page.classroomAction.studentTask, 24)
|
|
|
+ page.classroomAction.studentTask = !studentTask || isTeacherOnlyText(studentTask) ? page.studentContent.headline || page.title : studentTask
|
|
|
+
|
|
|
const layout = page.visualPlan.layout || ""
|
|
|
if (["hero_focus", "media_focus", "split_content", "diagram_focus"].includes(layout)) {
|
|
|
page.visualPlan.focalContent ||= page.title
|
|
|
page.visualPlan.mainVisual ||= page.imageKeywords?.[0] || `${page.title}的课堂主视觉`
|
|
|
}
|
|
|
- if (index > 0 && !page.classroomAction?.teacherInstruction && !page.teacherTips && !page.speakerNotes) {
|
|
|
- if (!page.classroomAction)
|
|
|
- page.classroomAction = { teacherInstruction: "", studentTask: page.studentContent.headline || page.title, interactionType: "" }
|
|
|
- page.classroomAction.teacherInstruction = `引导学生完成“${page.classroomAction.studentTask || page.title}”,根据学生的语言、动作或声音表现及时反馈。`
|
|
|
+
|
|
|
+ if (page.image?.url) {
|
|
|
+ const useCount = (imageUseCounts.get(page.image.url) || 0) + 1
|
|
|
+ imageUseCounts.set(page.image.url, useCount)
|
|
|
+ if (useCount > 2) {
|
|
|
+ page.image = undefined
|
|
|
+ page.resourceBindings = (page.resourceBindings || []).filter(binding => binding.resourceType !== "IMG")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (index > 0) {
|
|
|
+ const fallbackInstruction = `引导学生完成“${page.classroomAction.studentTask || page.title}”,根据学生的语言、动作或声音表现及时反馈。`
|
|
|
+ const instruction = cleanDraftText(page.classroomAction.teacherInstruction || page.teacherTips || page.speakerNotes) || fallbackInstruction
|
|
|
+ page.classroomAction.teacherInstruction = instruction
|
|
|
+ page.teacherTips ||= instruction
|
|
|
+ page.speakerNotes ||= instruction
|
|
|
}
|
|
|
})
|
|
|
- rebuildDraftPreview()
|
|
|
- ElMessage.success("已按投屏字号、内容密度、主视觉和教师备注重新优化全稿")
|
|
|
+}
|
|
|
+
|
|
|
+function cleanDraftText(value?: string, limit?: number) {
|
|
|
+ const cleaned = String(value || "").replace(/\s+/g, " ").trim()
|
|
|
+ return limit ? cleaned.slice(0, limit) : cleaned
|
|
|
+}
|
|
|
+
|
|
|
+function normalizeDraftText(value?: string) {
|
|
|
+ return cleanDraftText(value).replace(/[,。!?、;:,.!?;:\s]+/g, "").toLowerCase()
|
|
|
+}
|
|
|
+
|
|
|
+function uniqueDraftTexts(values: string[]) {
|
|
|
+ const keys = new Set<string>()
|
|
|
+ return values.filter(value => {
|
|
|
+ const key = normalizeDraftText(value)
|
|
|
+ if (!key || keys.has(key)) return false
|
|
|
+ keys.add(key)
|
|
|
+ return true
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function isTeacherOnlyText(value?: string) {
|
|
|
+ const text = cleanDraftText(value)
|
|
|
+ return Boolean(text && (TEACHER_VISIBLE_PATTERN.test(text) || PLACEHOLDER_PATTERN.test(text)))
|
|
|
}
|
|
|
|
|
|
function updateStudentHeadline(page: AiCoursewarePage, event: Event) {
|
|
|
@@ -1848,137 +1944,123 @@ function shortProgressLabel(message?: string) {
|
|
|
min-height: 0;
|
|
|
flex: 1;
|
|
|
margin-top: 14px;
|
|
|
- overflow: auto;
|
|
|
-}
|
|
|
-.preview-head {
|
|
|
display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
-}
|
|
|
-.preview-title {
|
|
|
- font-size: 18px;
|
|
|
- font-weight: 700;
|
|
|
-}
|
|
|
-.preview-sub {
|
|
|
- margin-top: 4px;
|
|
|
- color: #7b8794;
|
|
|
- font-size: 12px;
|
|
|
-}
|
|
|
-.quality-badge {
|
|
|
- padding: 7px 12px;
|
|
|
- border-radius: 999px;
|
|
|
- background: #fff1f0;
|
|
|
- color: #cf1322;
|
|
|
- font-size: 12px;
|
|
|
- font-weight: 700;
|
|
|
-}
|
|
|
-.quality-badge.passed {
|
|
|
- background: #eaf7f4;
|
|
|
- color: #168f86;
|
|
|
-}
|
|
|
-.quality-list {
|
|
|
- margin-top: 12px;
|
|
|
- border: 1px solid #edf1f5;
|
|
|
- border-radius: 8px;
|
|
|
+ flex-direction: column;
|
|
|
+ overflow: hidden;
|
|
|
}
|
|
|
-.quality-actions {
|
|
|
+.draft-status {
|
|
|
display: flex;
|
|
|
- gap: 8px;
|
|
|
- margin-top: 10px;
|
|
|
- button {
|
|
|
- padding: 7px 11px;
|
|
|
- border: 1px solid #d9e2ec;
|
|
|
- border-radius: 6px;
|
|
|
- background: #fff;
|
|
|
- color: $themeColor;
|
|
|
- cursor: pointer;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ padding: 12px 14px;
|
|
|
+ border: 1px solid #dce8f7;
|
|
|
+ border-radius: 10px;
|
|
|
+ background: #f6f9fe;
|
|
|
+ color: #35516f;
|
|
|
+ &.ready {
|
|
|
+ border-color: #cce8df;
|
|
|
+ background: #f0faf7;
|
|
|
+ color: #16776f;
|
|
|
+ }
|
|
|
+ &.failed {
|
|
|
+ border-color: #f3d7b2;
|
|
|
+ background: #fff8ee;
|
|
|
+ color: #9a5b08;
|
|
|
}
|
|
|
-}
|
|
|
-.quality-scores {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: repeat(4, minmax(0, 1fr)) auto auto;
|
|
|
- gap: 8px;
|
|
|
- margin-top: 12px;
|
|
|
> div {
|
|
|
- padding: 9px 10px;
|
|
|
- border: 1px solid #e5eaf0;
|
|
|
- border-radius: 7px;
|
|
|
- background: #fafcff;
|
|
|
+ min-width: 0;
|
|
|
+ flex: 1;
|
|
|
}
|
|
|
+ strong,
|
|
|
span {
|
|
|
display: block;
|
|
|
- color: #7b8794;
|
|
|
- font-size: 11px;
|
|
|
}
|
|
|
strong {
|
|
|
- display: block;
|
|
|
- margin-top: 2px;
|
|
|
color: #1f2933;
|
|
|
- font-size: 18px;
|
|
|
+ font-size: 14px;
|
|
|
}
|
|
|
- button {
|
|
|
- padding: 0 12px;
|
|
|
- border: 1px solid #d9e2ec;
|
|
|
- border-radius: 7px;
|
|
|
- background: #fff;
|
|
|
- color: #2563eb;
|
|
|
- cursor: pointer;
|
|
|
+ > div > span {
|
|
|
+ margin-top: 3px;
|
|
|
+ font-size: 12px;
|
|
|
}
|
|
|
}
|
|
|
-.quality-item {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: 70px 1fr;
|
|
|
- gap: 10px;
|
|
|
- padding: 8px 10px;
|
|
|
- font-size: 12px;
|
|
|
-}
|
|
|
-.quality-item + .quality-item {
|
|
|
- border-top: 1px solid #edf1f5;
|
|
|
-}
|
|
|
-.quality-item.error > span:first-child {
|
|
|
- color: #cf1322;
|
|
|
- font-weight: 700;
|
|
|
-}
|
|
|
-.quality-item.warning > span:first-child {
|
|
|
- color: #d97706;
|
|
|
- font-weight: 700;
|
|
|
+.draft-status-icon {
|
|
|
+ width: 28px;
|
|
|
+ height: 28px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ display: inline-flex !important;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ border: 2px solid currentColor;
|
|
|
+ border-radius: 50%;
|
|
|
+ font-size: 15px !important;
|
|
|
+ font-weight: 800;
|
|
|
+ .optimizing & {
|
|
|
+ border-color: #c6d7ee;
|
|
|
+ border-top-color: $themeColor;
|
|
|
+ animation: loading-rotate 0.8s linear infinite;
|
|
|
+ }
|
|
|
}
|
|
|
-.preview-grid {
|
|
|
+.draft-workspace {
|
|
|
+ min-height: 0;
|
|
|
+ flex: 1;
|
|
|
display: grid;
|
|
|
- grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
|
- gap: 14px;
|
|
|
- margin-top: 14px;
|
|
|
+ grid-template-columns: 170px minmax(0, 1fr);
|
|
|
+ gap: 16px;
|
|
|
+ margin-top: 12px;
|
|
|
}
|
|
|
-.preview-card {
|
|
|
- position: relative;
|
|
|
- width: 220px;
|
|
|
- padding: 0;
|
|
|
- overflow: hidden;
|
|
|
- border: 1px solid #e5eaf0;
|
|
|
- border-radius: 6px;
|
|
|
- background: #fff;
|
|
|
+.draft-page-list {
|
|
|
+ min-height: 0;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding: 3px 7px 3px 3px;
|
|
|
+}
|
|
|
+.draft-page-item {
|
|
|
+ width: 100%;
|
|
|
+ padding: 6px;
|
|
|
+ border: 1px solid transparent;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: transparent;
|
|
|
+ color: #52616b;
|
|
|
text-align: left;
|
|
|
cursor: pointer;
|
|
|
- box-shadow: 0 4px 12px rgba(31, 41, 51, 0.08);
|
|
|
+ transition: border-color 0.16s ease, background 0.16s ease;
|
|
|
+ + .draft-page-item {
|
|
|
+ margin-top: 7px;
|
|
|
+ }
|
|
|
+ &:hover {
|
|
|
+ background: #f5f8fc;
|
|
|
+ }
|
|
|
&.active {
|
|
|
border-color: $themeColor;
|
|
|
- box-shadow: 0 0 0 2px rgba($themeColor, 0.12);
|
|
|
+ background: rgba($themeColor, 0.06);
|
|
|
+ box-shadow: 0 0 0 2px rgba($themeColor, 0.08);
|
|
|
+ }
|
|
|
+ :deep(.thumbnail-slide) {
|
|
|
+ border-radius: 5px;
|
|
|
+ box-shadow: 0 3px 10px rgba(31, 41, 51, 0.1);
|
|
|
+ }
|
|
|
+ > span {
|
|
|
+ display: block;
|
|
|
+ margin-top: 6px;
|
|
|
+ overflow: hidden;
|
|
|
+ font-size: 11px;
|
|
|
+ line-height: 1.4;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
}
|
|
|
}
|
|
|
-.selected-draft-actions {
|
|
|
- position: sticky;
|
|
|
- top: 0;
|
|
|
- z-index: 2;
|
|
|
+.draft-preview-pane {
|
|
|
+ min-width: 0;
|
|
|
+ min-height: 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+.draft-preview-toolbar {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
gap: 14px;
|
|
|
- margin-top: 12px;
|
|
|
- padding: 10px 12px;
|
|
|
- border: 1px solid #dbe7f5;
|
|
|
- border-radius: 8px;
|
|
|
- background: #f7faff;
|
|
|
- box-shadow: 0 4px 12px rgba(31, 41, 51, 0.06);
|
|
|
+ margin-bottom: 8px;
|
|
|
span,
|
|
|
strong {
|
|
|
display: block;
|
|
|
@@ -1989,15 +2071,11 @@ function shortProgressLabel(message?: string) {
|
|
|
}
|
|
|
strong {
|
|
|
margin-top: 2px;
|
|
|
- font-size: 13px;
|
|
|
- }
|
|
|
- > div:last-child {
|
|
|
- display: flex;
|
|
|
- flex-wrap: wrap;
|
|
|
- gap: 6px;
|
|
|
+ color: #1f2933;
|
|
|
+ font-size: 14px;
|
|
|
}
|
|
|
button {
|
|
|
- padding: 6px 9px;
|
|
|
+ padding: 7px 11px;
|
|
|
border: 1px solid #d9e2ec;
|
|
|
border-radius: 6px;
|
|
|
background: #fff;
|
|
|
@@ -2005,6 +2083,24 @@ function shortProgressLabel(message?: string) {
|
|
|
cursor: pointer;
|
|
|
}
|
|
|
}
|
|
|
+.draft-canvas-shell {
|
|
|
+ min-width: 0;
|
|
|
+ min-height: 0;
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ overflow: auto;
|
|
|
+ padding: 18px;
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
+ border-radius: 10px;
|
|
|
+ background: #eef2f7;
|
|
|
+ :deep(.thumbnail-slide) {
|
|
|
+ flex-shrink: 0;
|
|
|
+ border-radius: 7px;
|
|
|
+ box-shadow: 0 14px 34px rgba(31, 41, 51, 0.16);
|
|
|
+ }
|
|
|
+}
|
|
|
.page-actions {
|
|
|
display: grid;
|
|
|
gap: 7px;
|
|
|
@@ -2037,16 +2133,6 @@ function shortProgressLabel(message?: string) {
|
|
|
opacity: 0.5;
|
|
|
}
|
|
|
}
|
|
|
-.preview-page-no {
|
|
|
- position: absolute;
|
|
|
- right: 6px;
|
|
|
- bottom: 6px;
|
|
|
- padding: 2px 5px;
|
|
|
- border-radius: 4px;
|
|
|
- background: rgba(31, 41, 51, 0.72);
|
|
|
- color: #fff;
|
|
|
- font-size: 11px;
|
|
|
-}
|
|
|
.page-nav {
|
|
|
overflow: auto;
|
|
|
border: 1px solid #edf1f5;
|
|
|
@@ -2281,6 +2367,20 @@ function shortProgressLabel(message?: string) {
|
|
|
background: #fff;
|
|
|
justify-content: flex-end;
|
|
|
gap: 10px;
|
|
|
+ :deep(.button.primary) {
|
|
|
+ min-width: 154px;
|
|
|
+ height: 40px;
|
|
|
+ line-height: 40px;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 700;
|
|
|
+ }
|
|
|
+}
|
|
|
+.footer-progress {
|
|
|
+ min-width: 154px;
|
|
|
+ color: $themeColor;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 600;
|
|
|
+ text-align: center;
|
|
|
}
|
|
|
|
|
|
@media (max-width: 900px) {
|
|
|
@@ -2304,9 +2404,22 @@ function shortProgressLabel(message?: string) {
|
|
|
.compact-template-panel .sample-template-picker {
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
}
|
|
|
- .selected-draft-actions {
|
|
|
- align-items: flex-start;
|
|
|
- flex-direction: column;
|
|
|
+ .draft-workspace {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ }
|
|
|
+ .draft-page-list {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ overflow-x: auto;
|
|
|
+ overflow-y: hidden;
|
|
|
+ padding: 3px 3px 8px;
|
|
|
+ }
|
|
|
+ .draft-page-item {
|
|
|
+ width: 160px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ + .draft-page-item {
|
|
|
+ margin-top: 0;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -2325,7 +2438,8 @@ function shortProgressLabel(message?: string) {
|
|
|
.compact-template-panel .sample-template-picker {
|
|
|
grid-template-columns: 1fr;
|
|
|
}
|
|
|
- .preview-head,
|
|
|
+ .draft-status,
|
|
|
+ .draft-preview-toolbar,
|
|
|
.sample-stage-head {
|
|
|
align-items: flex-start;
|
|
|
flex-direction: column;
|