|
|
@@ -1,4 +1,5 @@
|
|
|
import { AI_COURSEWARE_TEMPLATES, 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"
|
|
|
/** 纯视觉布局能力,不承载“歌词课/欣赏课”等教学语义。 */
|
|
|
@@ -6,6 +7,7 @@ export type TemplatePageLayout = "cover" | "hero" | "objectives" | "media" | "sp
|
|
|
export type TemplateSlotKind = "title" | "meta" | "headline" | "body" | "image" | "media" | "left" | "right" | "steps"
|
|
|
export type TemplateCardStyle = "bubble" | "paper" | "cinematic" | "ink" | "grid" | "sticker"
|
|
|
export type TemplateComposition = "left-visual" | "right-visual" | "wide-stage" | "scroll" | "asymmetric" | "playful"
|
|
|
+export type TemplateBackgroundKind = "hero" | "decorative"
|
|
|
|
|
|
export interface TemplateMasterElement {
|
|
|
type: "shape" | "text"
|
|
|
@@ -53,6 +55,8 @@ export interface TemplateMasterPage {
|
|
|
backgroundImage?: string
|
|
|
/** 同一逻辑版式可轮换的已验收背景,避免长课件反复出现同一画面。 */
|
|
|
backgroundImages?: string[]
|
|
|
+ /** hero 表示背景本身承担主视觉,decorative 表示仍需内容图片。 */
|
|
|
+ backgroundKind?: TemplateBackgroundKind
|
|
|
/** 背景已自带主题人物或乐器时,仅在有真实素材时覆盖 image 槽位。 */
|
|
|
backgroundVisual?: boolean
|
|
|
titleColor?: string
|
|
|
@@ -68,6 +72,58 @@ export interface TemplateMasterPage {
|
|
|
reservedZones?: TemplateRegion[]
|
|
|
}
|
|
|
|
|
|
+interface ExtractedBackgroundContract {
|
|
|
+ byLayout: Record<TemplatePageLayout, string[]>
|
|
|
+ backgroundVisualLayouts: TemplatePageLayout[]
|
|
|
+}
|
|
|
+
|
|
|
+const EXTRACTED_BACKGROUND_CONTRACTS: Record<string, ExtractedBackgroundContract> = {
|
|
|
+ "ethnic-flower": {
|
|
|
+ byLayout: { cover: ["background-01.jpg", "background-10.jpg"], hero: ["background-02.jpg", "background-03.jpg", "background-08.jpg"], objectives: ["background-04.jpg"], media: ["background-05.jpg"], split: ["background-06.jpg", "background-07.jpg"], text: ["background-04.jpg"], diagram: ["background-06.jpg"], compare: ["background-08.jpg", "background-09.jpg"], activity: ["background-09.jpg"], summary: ["background-10.jpg"] },
|
|
|
+ backgroundVisualLayouts: ["cover", "hero", "media"]
|
|
|
+ },
|
|
|
+ "opera-fan": {
|
|
|
+ byLayout: { cover: ["background-01.jpg"], hero: ["background-02.jpg", "background-04.jpg", "background-06.jpg"], objectives: ["background-03.jpg", "background-08.jpg"], media: ["background-04.jpg", "background-06.jpg"], split: ["background-05.jpg", "background-07.jpg"], text: ["background-03.jpg", "background-09.jpg"], diagram: ["background-05.jpg", "background-07.jpg"], compare: ["background-08.jpg", "background-09.jpg"], activity: ["background-06.jpg"], summary: ["background-10.jpg"] },
|
|
|
+ backgroundVisualLayouts: ["cover"]
|
|
|
+ },
|
|
|
+ "dark-lute": {
|
|
|
+ byLayout: { cover: ["background-01.jpg", "background-10.jpg"], hero: ["background-04.jpg", "background-09.jpg"], objectives: ["background-04.jpg"], media: ["background-01.jpg"], split: ["background-04.jpg"], text: ["background-09.jpg"], diagram: ["background-04.jpg"], compare: ["background-09.jpg"], activity: ["background-01.jpg"], summary: ["background-10.jpg"] },
|
|
|
+ backgroundVisualLayouts: ["cover", "summary"]
|
|
|
+ },
|
|
|
+ "spring-outing": {
|
|
|
+ byLayout: { cover: ["background-01.jpg"], hero: ["background-10.jpg"], objectives: ["background-02.jpg"], media: ["background-03.jpg"], split: ["background-04.jpg"], text: ["background-05.jpg"], diagram: ["background-06.jpg"], compare: ["background-02.jpg", "background-04.jpg"], activity: ["background-10.jpg"], summary: ["background-01.jpg"] },
|
|
|
+ backgroundVisualLayouts: ["cover"]
|
|
|
+ },
|
|
|
+ "bird-garden": {
|
|
|
+ byLayout: { cover: ["background-01.jpg", "background-10.jpg"], hero: ["background-01.jpg", "background-10.jpg"], objectives: ["background-02.jpg", "background-05.jpg"], media: ["background-04.jpg", "background-06.jpg"], split: ["background-07.jpg", "background-08.jpg"], text: ["background-03.jpg", "background-05.jpg"], diagram: ["background-06.jpg", "background-09.jpg"], compare: ["background-07.jpg", "background-08.jpg"], activity: ["background-09.jpg"], summary: ["background-10.jpg"] },
|
|
|
+ backgroundVisualLayouts: ["cover", "hero"]
|
|
|
+ },
|
|
|
+ "rainy-cat": {
|
|
|
+ byLayout: { cover: ["background-01.jpg", "background-10.jpg"], hero: ["background-02.jpg", "background-04.jpg"], objectives: ["background-03.jpg"], media: ["background-04.jpg", "background-06.jpg"], split: ["background-05.jpg", "background-07.jpg"], text: ["background-06.jpg", "background-08.jpg"], diagram: ["background-07.jpg"], compare: ["background-08.jpg", "background-09.jpg"], activity: ["background-09.jpg"], summary: ["background-10.jpg"] },
|
|
|
+ backgroundVisualLayouts: []
|
|
|
+ },
|
|
|
+ "ink-jiangnan": {
|
|
|
+ byLayout: { cover: ["background-02.jpg"], hero: ["background-03.jpg"], objectives: ["background-04.jpg"], media: ["background-05.jpg"], split: ["background-03.jpg"], text: ["background-04.jpg"], diagram: ["background-05.jpg"], compare: ["background-06.jpg"], activity: ["background-03.jpg", "background-05.jpg"], summary: ["background-06.jpg"] },
|
|
|
+ backgroundVisualLayouts: []
|
|
|
+ },
|
|
|
+ "rhythm-train": {
|
|
|
+ byLayout: { cover: ["background-01.jpg", "background-10.jpg"], hero: ["background-10.jpg"], objectives: ["background-04.jpg"], media: ["background-06.jpg"], split: ["background-07.jpg"], text: ["background-08.jpg"], diagram: ["background-09.jpg"], compare: ["background-06.jpg", "background-08.jpg"], activity: ["background-10.jpg"], summary: ["background-01.jpg"] },
|
|
|
+ backgroundVisualLayouts: []
|
|
|
+ },
|
|
|
+ "sunset-drum": {
|
|
|
+ byLayout: { cover: ["background-01.jpg", "background-02.jpg"], hero: ["background-03.jpg", "background-05.jpg"], objectives: ["background-04.jpg"], media: ["background-05.jpg"], split: ["background-06.jpg"], text: ["background-07.jpg"], diagram: ["background-08.jpg"], compare: ["background-06.jpg", "background-08.jpg"], activity: ["background-03.jpg"], summary: ["background-09.jpg"] },
|
|
|
+ backgroundVisualLayouts: ["cover", "hero"]
|
|
|
+ },
|
|
|
+ "ballroom-silhouette": {
|
|
|
+ byLayout: { cover: ["background-02.jpg"], hero: ["background-03.jpg", "background-05.jpg"], objectives: ["background-04.jpg"], media: ["background-06.jpg"], split: ["background-07.jpg"], text: ["background-08.jpg"], diagram: ["background-09.jpg"], compare: ["background-04.jpg", "background-08.jpg"], activity: ["background-06.jpg"], summary: ["background-10.jpg"] },
|
|
|
+ backgroundVisualLayouts: ["cover"]
|
|
|
+ },
|
|
|
+ "color-theory": {
|
|
|
+ byLayout: { cover: ["background-10.jpg"], hero: ["background-03.jpg", "background-05.jpg"], objectives: ["background-02.jpg"], media: ["background-04.jpg"], split: ["background-06.jpg"], text: ["background-07.jpg"], diagram: ["background-08.jpg"], compare: ["background-04.jpg", "background-07.jpg"], activity: ["background-09.jpg"], summary: ["background-10.jpg"] },
|
|
|
+ backgroundVisualLayouts: []
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
export interface TemplateMaster {
|
|
|
id: AiCoursewareTemplateId
|
|
|
label: string
|
|
|
@@ -294,26 +350,42 @@ function createExtractedMaster(template: AiCoursewareTemplate): TemplateMaster {
|
|
|
contentBase: [],
|
|
|
contentChrome: []
|
|
|
})
|
|
|
- const backgroundUrls = template.backgroundFiles.map(file => `/ai-courseware-templates/extracted/${template.id}/${file}`)
|
|
|
- if (!backgroundUrls.length) throw new Error(`课件模板 ${template.id} 没有可用背景资产`)
|
|
|
- const rotateBackgrounds = !["ethnic-flower", "opera-fan", "dark-lute", "ballroom-silhouette"].includes(template.id)
|
|
|
+ const backgroundContract = EXTRACTED_BACKGROUND_CONTRACTS[template.id]
|
|
|
+ if (!backgroundContract) throw new Error(`课件模板 ${template.id} 缺少背景版式契约`)
|
|
|
+ const registeredFiles = new Set(template.backgroundFiles)
|
|
|
+ Object.entries(backgroundContract.byLayout).forEach(([layout, files]) => {
|
|
|
+ if (!files.length) throw new Error(`课件模板 ${template.id} 的 ${layout} 版式没有背景资产`)
|
|
|
+ const missing = files.filter(file => !registeredFiles.has(file))
|
|
|
+ if (missing.length) throw new Error(`课件模板 ${template.id} 的 ${layout} 版式引用未注册背景:${missing.join(",")}`)
|
|
|
+ })
|
|
|
return {
|
|
|
...master,
|
|
|
- pages: master.pages.map((page, index) => {
|
|
|
- const primaryIndex = index % backgroundUrls.length
|
|
|
- const primary = backgroundUrls[primaryIndex]
|
|
|
- const alternative = backgroundUrls[(primaryIndex + Math.max(1, Math.floor(backgroundUrls.length / 2))) % backgroundUrls.length]
|
|
|
- const backgroundImages = rotateBackgrounds && alternative !== primary ? [primary, alternative] : [primary]
|
|
|
+ pages: master.pages.map(page => {
|
|
|
+ const backgroundImages = backgroundContract.byLayout[page.layout].map(file => templateAssetPath(template.id, file))
|
|
|
+ const primary = backgroundImages[0]
|
|
|
+ const backgroundVisual = backgroundContract.backgroundVisualLayouts.includes(page.layout)
|
|
|
+ let slots = pilotSlots(template.id, page.layout, page.slots).map(item =>
|
|
|
+ item.kind === "image" && !backgroundVisual ? { ...item, enabled: true } : item
|
|
|
+ )
|
|
|
+ if (page.layout === "cover" && !backgroundVisual) {
|
|
|
+ slots = [
|
|
|
+ slot("title", "title", 110, 180, 760, 160, 1, 66, 44),
|
|
|
+ slot("meta", "meta", 115, 375, 700, 48, 1, 24, 20),
|
|
|
+ slot("headline", "headline", 115, 475, 700, 120, 2, 28, 22),
|
|
|
+ slot("image", "image", 950, 170, 500, 500)
|
|
|
+ ]
|
|
|
+ }
|
|
|
return {
|
|
|
...page,
|
|
|
...pilotContract(template.id, page.layout),
|
|
|
backgroundImage: primary,
|
|
|
backgroundImages,
|
|
|
- backgroundVisual: true,
|
|
|
+ backgroundKind: backgroundVisual ? "hero" as const : "decorative" as const,
|
|
|
+ backgroundVisual,
|
|
|
base: template.id === "dark-lute" && !["cover", "media", "activity", "summary"].includes(page.layout)
|
|
|
? [shape(0, 0, 1600, 86, "#06364B")]
|
|
|
: page.base,
|
|
|
- slots: pilotSlots(template.id, page.layout, page.slots),
|
|
|
+ slots,
|
|
|
...(template.id === "dark-lute" && ["cover", "media", "activity", "summary"].includes(page.layout)
|
|
|
? {
|
|
|
titleColor: "#D3B270",
|
|
|
@@ -409,6 +481,11 @@ 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 (!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")
|
|
|
+ if (page.backgroundKind === "decorative" && imageSlot?.enabled === false) problems.push(`${registryId}/${page.id}: decorative-background-disabled-image`)
|
|
|
page.slots.forEach(item => {
|
|
|
if (outsideCanvas(item)) problems.push(`${registryId}/${page.id}/${item.id}: out-of-canvas`)
|
|
|
})
|