yonge il y a 3 jours
Parent
commit
8a8109b783

+ 14 - 2
src/config/aiCoursewareTemplateMasters.ts

@@ -1,4 +1,4 @@
-import { AI_COURSEWARE_TEMPLATES, type AiCoursewareTemplate, type AiCoursewareTemplateId } from "@/config/aiCoursewareTemplates"
+import { AI_COURSEWARE_TEMPLATES, DEFAULT_AI_COURSEWARE_TEMPLATE_ID, type AiCoursewareTemplate, type AiCoursewareTemplateId } from "@/config/aiCoursewareTemplates"
 import { templateAssetPath } from "@/utils/templateAssetUrl"
 
 export type TemplatePageRole = "cover" | "intro" | "goal" | "story" | "listen" | "lyric" | "notation" | "instrument" | "compare" | "knowledge" | "rhythm" | "interaction" | "performance" | "practice" | "activity" | "summary" | "homework" | "content"
@@ -350,6 +350,18 @@ function createExtractedMaster(template: AiCoursewareTemplate): TemplateMaster {
     contentBase: [],
     contentChrome: []
   })
+  if (template.id === DEFAULT_AI_COURSEWARE_TEMPLATE_ID) {
+    return {
+      ...master,
+      pages: master.pages.map(page => ({
+        ...page,
+        backgroundKind: "decorative" as const,
+        backgroundVisual: false,
+        safeArea: region("content", "教学内容", 64, 64, 1472, 800),
+        reservedZones: []
+      }))
+    }
+  }
   const backgroundContract = EXTRACTED_BACKGROUND_CONTRACTS[template.id]
   if (!backgroundContract) throw new Error(`课件模板 ${template.id} 缺少背景版式契约`)
   const registeredFiles = new Set(template.backgroundFiles)
@@ -481,7 +493,7 @@ export function validateTemplateMasterRegistry(templateIds: AiCoursewareTemplate
       const slotKinds = new Set(page.slots.map(item => item.kind))
       const absentSlots = requiredSlots[page.layout].filter(kind => !slotKinds.has(kind))
       if (absentSlots.length) problems.push(`${registryId}/${page.id}: slots=${absentSlots.join(",")}`)
-      if (!page.backgroundImage || !page.backgroundImages?.length) problems.push(`${registryId}/${page.id}: missing-background`)
+      if (registryId !== DEFAULT_AI_COURSEWARE_TEMPLATE_ID && (!page.backgroundImage || !page.backgroundImages?.length)) problems.push(`${registryId}/${page.id}: missing-background`)
       if (!page.backgroundKind) problems.push(`${registryId}/${page.id}: missing-background-kind`)
       if ((page.backgroundKind === "hero") !== Boolean(page.backgroundVisual)) problems.push(`${registryId}/${page.id}: inconsistent-background-kind`)
       const imageSlot = page.slots.find(item => item.kind === "image")

+ 21 - 0
src/config/aiCoursewareTemplates.ts

@@ -2,6 +2,7 @@ import type { AiCoursewareOutline } from "@/api/aiCourseware"
 import { templateAssetPath } from "@/utils/templateAssetUrl"
 
 export type AiCoursewareTemplateId = string
+export const DEFAULT_AI_COURSEWARE_TEMPLATE_ID = "blank"
 
 export type AiCoursewareTemplatePageRole =
   | "cover"
@@ -141,6 +142,26 @@ function extractedTemplate(
 }
 
 export const AI_COURSEWARE_TEMPLATES: AiCoursewareTemplate[] = [
+  {
+    id: DEFAULT_AI_COURSEWARE_TEMPLATE_ID,
+    name: "空白模板",
+    sourceName: "空白模板",
+    scene: "通用课堂",
+    description: "纯白背景,无预置装饰,展示教学内容与生成配图",
+    templateFile: "",
+    templateSlides: 10,
+    backgroundFiles: [],
+    baseStyle: "clean",
+    layoutFamily: "education",
+    typography: "深色大字,适合教室投屏",
+    backgroundStyle: "纯白背景,不添加预置人物、边饰或背景图片",
+    illustrationStyle: "配图依据教学内容、学生年级和教师要求生成",
+    layoutCapabilities: ["cover_scene", "hero_focus", "objectives", "media_focus", "split_content", "text_focus", "diagram_focus", "compare", "activity", "summary"],
+    pageLayouts: extractedPageLayouts,
+    visualPrompt: "纯白空白母版,清晰排版教学文字和课程配图,不添加主题背景和装饰;配图风格遵循学生年级与教师要求。",
+    matching: { keywords: [], matchedReason: "适用于各类课程", unmatchedReason: "纯白背景,适用于各类课程" },
+    palettes: extractedPalettes("#334155", "#64748B", "#475569", "#94A3B8", "#FFFFFF")
+  },
   extractedTemplate("ethnic-flower", "民族花园", "《彝家娃娃真幸福》", "民族歌曲与律动", "民族人物、花草边饰与低年级互动", "culture", ["#DF7A2B", "#4C9A71", "#E45667", "#F2C84B", "#FFFDF6"], {
     keywords: ["一年级", "二年级", "低年级", "故事", "童话", "儿歌", "律动", "节奏", "活动", "民族", "民歌"],
     strongKeywords: ["低年级", "律动", "民族", "民歌"],

+ 3 - 3
src/utils/aiPptTemplateEngine.ts

@@ -21,7 +21,7 @@ import {
   AI_COURSEWARE_TEMPLATES,
   getAiCoursewarePalette,
   getAiCoursewareTemplate,
-  recommendAiCoursewareTemplate,
+  DEFAULT_AI_COURSEWARE_TEMPLATE_ID,
   type AiCoursewareTemplateId,
   type AiCoursewareThemePalette
 } from "@/config/aiCoursewareTemplates"
@@ -80,7 +80,7 @@ export function buildAiPptSlides(
   const requestedTemplateId = options.templateId || normalized.templateId
   const templateId = requestedTemplateId
     ? getAiCoursewareTemplate(requestedTemplateId).id
-    : recommendAiCoursewareTemplate(normalized).id
+    : DEFAULT_AI_COURSEWARE_TEMPLATE_ID
   const palette = buildPalette(normalized, options.paletteVariant || 0, templateId)
   const resolvedOptions = { ...options, templateId }
   const layoutOccurrences = new Map<string, number>()
@@ -810,7 +810,7 @@ function slideRemark(page: AiCoursewarePage) {
 }
 
 function buildPalette(outline: AiCoursewareOutline, variant: number, templateId?: AiCoursewareTemplateId): Palette {
-  const resolvedTemplateId = templateId || recommendAiCoursewareTemplate(outline).id
+  const resolvedTemplateId = templateId || DEFAULT_AI_COURSEWARE_TEMPLATE_ID
   return { ...getAiCoursewarePalette(resolvedTemplateId, variant), ...(resolvedTemplateId === "dark-lute" ? { soft: "#16485C" } : {}) }
 }
 

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

@@ -143,7 +143,8 @@
               :aria-pressed="templateId === template.id"
               @click="selectTemplate(template.id)"
             >
-              <img :src="templatePreview(template.id)" :alt="template.name" /><span>{{ template.name }}</span>
+              <span v-if="template.id === DEFAULT_AI_COURSEWARE_TEMPLATE_ID" class="blank-template-preview" aria-hidden="true"></span>
+              <img v-else :src="templatePreview(template.id)" :alt="template.name" /><span>{{ template.name }}</span>
             </button>
           </div>
         </section>
@@ -204,7 +205,7 @@ import {
   type AiCoursewareTaskInfo,
   type AiCoursewareGenerateResult
 } from "@/api/aiCourseware"
-import { AI_COURSEWARE_TEMPLATES, recommendAiCoursewareTemplate, getAiCoursewareTemplate } from "@/config/aiCoursewareTemplates"
+import { AI_COURSEWARE_TEMPLATES, DEFAULT_AI_COURSEWARE_TEMPLATE_ID, getAiCoursewareTemplate } from "@/config/aiCoursewareTemplates"
 import { aiCoursewareToSlides } from "@/utils/aiCoursewareToSlides"
 import { getAiPptImageAspectRatios } from "@/utils/aiPptTemplateEngine"
 import { inspectAiPptQuality, type AiPptQualityIssue } from "@/utils/aiPptQuality"
@@ -243,13 +244,13 @@ const expandedPage = ref(""),
 const settingsSignature = () => JSON.stringify([preferences, extraPrompt.value, pageRange.value])
 const settingsChanged = computed(() => !!outline.value && outlineSettings.value !== settingsSignature())
 const templatePreview = (id: string) => templateAssetPath(id, getAiCoursewareTemplate(id).backgroundFiles[0])
-const steps = ["设置需求", "确认大纲", "生成课件", "编辑课件", "保存课件"]
+const steps = ["设置需求", "确认大纲", "生成课件"]
 const step = ref(1),
   outline = ref<AiCoursewareOutline>(),
   pages = ref<EditableOutlinePage[]>([])
 const extraPrompt = ref(""),
   pageRange = ref("9-12"),
-  templateId = ref("")
+  templateId = ref(DEFAULT_AI_COURSEWARE_TEMPLATE_ID)
 const outlineLoading = ref(false),
   transferring = ref(false),
   setupError = ref(""),
@@ -384,7 +385,7 @@ async function restore() {
 function loadOutline(value: AiCoursewareOutline) {
   outline.value = clone(value)
   pages.value = editableOutlinePages(value)
-  templateId.value = value.templateId || recommendAiCoursewareTemplate(value).id
+  templateId.value = value.templateId || DEFAULT_AI_COURSEWARE_TEMPLATE_ID
   validationErrors.value = []
   expandedPage.value = ""
   outlineSettings.value = settingsSignature()
@@ -1025,13 +1026,18 @@ footer > div {
 .template-options .selected {
   border-color: #1989fa;
 }
-.template-options img {
+.template-options img,
+.blank-template-preview {
   display: block;
   width: 100%;
   aspect-ratio: 16/9;
   object-fit: cover;
   margin-bottom: 5px;
 }
+.blank-template-preview {
+  background: #fff;
+  box-shadow: inset 0 0 0 1px #e5e7eb;
+}
 @media (max-width: 740px) {
   .page-heading {
     flex-wrap: wrap;