|
|
@@ -1,172 +1,307 @@
|
|
|
import { nanoid } from "nanoid"
|
|
|
-import type { Slide, PPTElement, SlideTheme, PPTTextElement, PPTImageElement, PPTShapeElement, SlideBackground } from "@/types/slides"
|
|
|
+import type { Slide, PPTElement, SlideTheme, PPTTextElement, PPTImageElement, PPTShapeElement, SlideBackground, PPTElementShadow } from "@/types/slides"
|
|
|
import type { AiCoursewareOutline, AiCoursewarePage } from "@/api/aiCourseware"
|
|
|
|
|
|
interface AiCoursewareVisualOptions {
|
|
|
backgroundImage?: string
|
|
|
}
|
|
|
|
|
|
+interface Palette {
|
|
|
+ primary: string
|
|
|
+ secondary: string
|
|
|
+ accent: string
|
|
|
+ warm: string
|
|
|
+ background: string
|
|
|
+ panelAlt: string
|
|
|
+ text: string
|
|
|
+ muted: string
|
|
|
+}
|
|
|
+
|
|
|
const SLIDE_WIDTH = 1600
|
|
|
const SLIDE_HEIGHT = 900
|
|
|
const FONT_NAME = "Microsoft Yahei"
|
|
|
|
|
|
export function aiCoursewareToSlides(outline: AiCoursewareOutline, options: AiCoursewareVisualOptions = {}): { title: string; theme: Partial<SlideTheme>; slides: Slide[] } {
|
|
|
- const primary = outline.style === "clean" ? "#2D6CDF" : "#FF8A3D"
|
|
|
- const background = outline.style === "clean" ? "#F7F9FC" : "#FFF8EF"
|
|
|
- const fontColor = "#263238"
|
|
|
+ const palette = buildPalette(outline.style)
|
|
|
const theme: Partial<SlideTheme> = {
|
|
|
- themeColor: primary,
|
|
|
- backgroundColor: background,
|
|
|
- fontColor,
|
|
|
+ themeColor: palette.primary,
|
|
|
+ backgroundColor: palette.background,
|
|
|
+ fontColor: palette.text,
|
|
|
fontName: FONT_NAME,
|
|
|
fontSize: "36px"
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
- title: outline.title || outline.chapterName || "AI音乐课件",
|
|
|
+ title: outline.title || outline.chapterName || "音乐课件",
|
|
|
theme,
|
|
|
- slides: outline.pages.map(page => buildSlide(page, outline, primary, background, fontColor, options))
|
|
|
+ slides: outline.pages.map(page => buildSlide(page, outline, palette, options))
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-function buildSlide(
|
|
|
- page: AiCoursewarePage,
|
|
|
- outline: AiCoursewareOutline,
|
|
|
- primary: string,
|
|
|
- background: string,
|
|
|
- fontColor: string,
|
|
|
- options: AiCoursewareVisualOptions
|
|
|
-): Slide {
|
|
|
- const isCover = page.type === "cover" || page.pageNo === 1
|
|
|
- const hasCustomBackground = !!options.backgroundImage
|
|
|
- const elements: PPTElement[] = []
|
|
|
- const visual = visualConfig(page, primary, outline.style)
|
|
|
- const backgroundConfig: SlideBackground = hasCustomBackground
|
|
|
- ? {
|
|
|
- type: "image",
|
|
|
- image: options.backgroundImage,
|
|
|
- imageSize: "cover"
|
|
|
- }
|
|
|
- : {
|
|
|
- type: "solid",
|
|
|
- color: background
|
|
|
- }
|
|
|
-
|
|
|
- if (hasCustomBackground) {
|
|
|
- elements.push(shapeElement({ left: 0, top: 0, width: SLIDE_WIDTH, height: SLIDE_HEIGHT, fill: "rgba(255, 255, 255, 0.74)", opacity: 0.92 }))
|
|
|
+function buildSlide(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette, options: AiCoursewareVisualOptions): Slide {
|
|
|
+ const type = page.type || ""
|
|
|
+ const elements = baseElements(palette, options)
|
|
|
+
|
|
|
+ if (type.includes("cover") || page.pageNo === 1) {
|
|
|
+ elements.push(...coverLayout(page, outline, palette))
|
|
|
+ } else if (type.includes("goal")) {
|
|
|
+ elements.push(...goalLayout(page, outline, palette))
|
|
|
+ } else if (type.includes("listen")) {
|
|
|
+ elements.push(...listenLayout(page, outline, palette))
|
|
|
+ } else if (type.includes("interaction") || type.includes("activity")) {
|
|
|
+ elements.push(...interactionLayout(page, outline, palette))
|
|
|
+ } else if (type.includes("summary")) {
|
|
|
+ elements.push(...summaryLayout(page, outline, palette))
|
|
|
+ } else if (type.includes("homework")) {
|
|
|
+ elements.push(...homeworkLayout(page, outline, palette))
|
|
|
+ } else if (type.includes("knowledge") || type.includes("skill")) {
|
|
|
+ elements.push(...knowledgeLayout(page, outline, palette))
|
|
|
+ } else {
|
|
|
+ elements.push(...contentLayout(page, outline, palette))
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ id: nanoid(10),
|
|
|
+ elements,
|
|
|
+ background: backgroundConfig(palette, options),
|
|
|
+ remark: page.teacherTips || ""
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- elements.push(shapeElement({ left: 0, top: 0, width: 18, height: SLIDE_HEIGHT, fill: primary, opacity: 0.95 }))
|
|
|
- elements.push(shapeElement({ left: 86, top: isCover ? 142 : 54, width: 70, height: 8, fill: primary, opacity: 0.9 }))
|
|
|
-
|
|
|
- elements.push(
|
|
|
- textElement({
|
|
|
- left: 100,
|
|
|
- top: isCover ? 172 : 72,
|
|
|
- width: isCover ? 850 : 980,
|
|
|
- height: isCover ? 98 : 64,
|
|
|
- content: page.title || outline.title,
|
|
|
- fontSize: isCover ? 48 : 34,
|
|
|
- color: primary,
|
|
|
- bold: true
|
|
|
- })
|
|
|
- )
|
|
|
-
|
|
|
- const subtitle = isCover ? [outline.gradeName, outline.textbookName, outline.unitName].filter(Boolean).join(" · ") : page.subtitle
|
|
|
+function coverLayout(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
|
|
|
+ const elements: PPTElement[] = []
|
|
|
+ const subtitle = [outline.gradeName, outline.textbookName, outline.unitName].filter(Boolean).join(" · ")
|
|
|
+ elements.push(shapeElement({ left: 84, top: 96, width: 94, height: 10, fill: palette.primary, radius: 6 }))
|
|
|
+ elements.push(textElement({ left: 84, top: 138, width: 820, height: 106, content: page.title || outline.title, fontSize: 58, color: palette.primary, bold: true }))
|
|
|
if (subtitle) {
|
|
|
- elements.push(
|
|
|
- textElement({
|
|
|
- left: 104,
|
|
|
- top: isCover ? 288 : 132,
|
|
|
- width: 840,
|
|
|
- height: 40,
|
|
|
- content: subtitle,
|
|
|
- fontSize: isCover ? 24 : 18,
|
|
|
- color: fontColor
|
|
|
- })
|
|
|
- )
|
|
|
+ elements.push(textElement({ left: 88, top: 264, width: 820, height: 38, content: escapeHtml(subtitle), fontSize: 23, color: palette.text }))
|
|
|
}
|
|
|
+ elements.push(...mainVisual(page, palette, { left: 940, top: 112, width: 520, height: 470, mode: "hero" }))
|
|
|
+ elements.push(...bulletCards(normalizeBullets(page, 3), palette, { left: 88, top: 390, width: 720, itemHeight: 72, fontSize: 28, mark: "♪" }))
|
|
|
+ elements.push(textElement({ left: 92, top: 746, width: 900, height: 42, content: "课堂导入:从声音、画面和问题进入音乐情境", fontSize: 22, color: palette.muted }))
|
|
|
+ return elements
|
|
|
+}
|
|
|
|
|
|
- if (page.image?.url) {
|
|
|
- elements.push(
|
|
|
- imageElement({
|
|
|
- left: isCover ? 980 : 1040,
|
|
|
- top: isCover ? 138 : 150,
|
|
|
- width: isCover ? 480 : 430,
|
|
|
- height: isCover ? 520 : 360,
|
|
|
- src: page.image.url
|
|
|
- })
|
|
|
- )
|
|
|
- } else {
|
|
|
- elements.push(...fallbackVisualElements(page, visual, isCover))
|
|
|
+function contentLayout(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
|
|
|
+ const elements: PPTElement[] = []
|
|
|
+ addTitle(elements, page, outline, palette)
|
|
|
+ elements.push(...mainVisual(page, palette, { left: 960, top: 178, width: 470, height: 470, mode: "photo" }))
|
|
|
+ elements.push(...bulletCards(normalizeBullets(page, 4), palette, { left: 98, top: 190, width: 760, itemHeight: 86, fontSize: 27, mark: "•" }))
|
|
|
+ addPrompt(elements, page, palette, 98, 706, 1220)
|
|
|
+ return elements
|
|
|
+}
|
|
|
+
|
|
|
+function goalLayout(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
|
|
|
+ const elements: PPTElement[] = []
|
|
|
+ addTitle(elements, page, outline, palette)
|
|
|
+ const goals = normalizeBullets(page, 3)
|
|
|
+ const colors = [palette.primary, palette.accent, palette.secondary]
|
|
|
+ goals.forEach((goal, index) => {
|
|
|
+ const left = 100 + index * 470
|
|
|
+ elements.push(shapeElement({ left, top: 238, width: 392, height: 300, fill: "#FFFFFF", radius: 24, shadow: softShadow() }))
|
|
|
+ elements.push(shapeElement({ left: left + 30, top: 270, width: 58, height: 58, fill: colors[index % colors.length], radius: 20 }))
|
|
|
+ elements.push(textElement({ left: left + 45, top: 284, width: 34, height: 34, content: `${index + 1}`, fontSize: 28, color: "#FFFFFF", bold: true }))
|
|
|
+ elements.push(textElement({ left: left + 32, top: 362, width: 328, height: 132, content: escapeHtml(goal), fontSize: 30, color: palette.text, bold: true, lineHeight: 1.35 }))
|
|
|
+ })
|
|
|
+ addPrompt(elements, page, palette, 112, 650, 1180)
|
|
|
+ return elements
|
|
|
+}
|
|
|
+
|
|
|
+function listenLayout(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
|
|
|
+ const elements: PPTElement[] = []
|
|
|
+ addTitle(elements, page, outline, palette)
|
|
|
+ elements.push(shapeElement({ left: 92, top: 190, width: 640, height: 438, fill: "#FFFFFF", radius: 28, shadow: softShadow() }))
|
|
|
+ elements.push(textElement({ left: 132, top: 232, width: 360, height: 46, content: "听赏任务", fontSize: 34, color: palette.primary, bold: true }))
|
|
|
+ elements.push(...soundWave(150, 328, palette))
|
|
|
+ elements.push(...bulletCards(normalizeBullets(page, 3), palette, { left: 132, top: 442, width: 520, itemHeight: 58, fontSize: 24, mark: "听" }))
|
|
|
+ elements.push(...mainVisual(page, palette, { left: 835, top: 178, width: 560, height: 500, mode: "photo" }))
|
|
|
+ addPrompt(elements, page, palette, 112, 716, 1200, page.interaction?.question || "听一听:音乐中出现了哪些有特点的声音?")
|
|
|
+ return elements
|
|
|
+}
|
|
|
+
|
|
|
+function knowledgeLayout(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
|
|
|
+ const elements: PPTElement[] = []
|
|
|
+ addTitle(elements, page, outline, palette)
|
|
|
+ const items = normalizeBullets(page, 4)
|
|
|
+ items.forEach((item, index) => {
|
|
|
+ const left = 100 + (index % 2) * 520
|
|
|
+ const top = 210 + Math.floor(index / 2) * 190
|
|
|
+ elements.push(shapeElement({ left, top, width: 460, height: 142, fill: index % 2 === 0 ? "#FFFFFF" : palette.panelAlt, radius: 22, shadow: softShadow() }))
|
|
|
+ elements.push(textElement({ left: left + 34, top: top + 36, width: 390, height: 68, content: `${index + 1}. ${escapeHtml(item)}`, fontSize: 27, color: palette.text, bold: true, lineHeight: 1.32 }))
|
|
|
+ })
|
|
|
+ elements.push(...mainVisual(page, palette, { left: 1132, top: 230, width: 300, height: 300, mode: "compact" }))
|
|
|
+ addPrompt(elements, page, palette, 100, 672, 1150)
|
|
|
+ return elements
|
|
|
+}
|
|
|
+
|
|
|
+function interactionLayout(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
|
|
|
+ const elements: PPTElement[] = []
|
|
|
+ addTitle(elements, page, outline, palette)
|
|
|
+ const question = page.interaction?.question || normalizeBullets(page, 1)[0] || "请同学们参与课堂互动"
|
|
|
+ elements.push(shapeElement({ left: 96, top: 178, width: 930, height: 210, fill: palette.primary, radius: 28, shadow: softShadow() }))
|
|
|
+ elements.push(textElement({ left: 140, top: 222, width: 820, height: 104, content: escapeHtml(question), fontSize: 38, color: "#FFFFFF", bold: true, lineHeight: 1.28 }))
|
|
|
+ elements.push(...mainVisual(page, palette, { left: 1090, top: 170, width: 330, height: 330, mode: "compact" }))
|
|
|
+ elements.push(...stepCards(normalizeBullets(page, 3), palette, 112, 486))
|
|
|
+ addPrompt(elements, page, palette, 112, 732, 1200, page.interaction?.answer ? `参考:${page.interaction.answer}` : "")
|
|
|
+ return elements
|
|
|
+}
|
|
|
+
|
|
|
+function summaryLayout(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
|
|
|
+ const elements: PPTElement[] = []
|
|
|
+ addTitle(elements, page, outline, palette)
|
|
|
+ const items = normalizeBullets(page, 4)
|
|
|
+ elements.push(shapeElement({ left: 110, top: 190, width: 960, height: 470, fill: "#FFFFFF", radius: 26, shadow: softShadow() }))
|
|
|
+ items.forEach((item, index) => {
|
|
|
+ const top = 236 + index * 96
|
|
|
+ elements.push(shapeElement({ left: 154, top, width: 48, height: 48, fill: palette.accent, radius: 16 }))
|
|
|
+ elements.push(textElement({ left: 168, top: top + 8, width: 22, height: 24, content: "✓", fontSize: 24, color: "#FFFFFF", bold: true }))
|
|
|
+ elements.push(textElement({ left: 228, top: top + 4, width: 760, height: 48, content: escapeHtml(item), fontSize: 28, color: palette.text, bold: true }))
|
|
|
+ })
|
|
|
+ elements.push(...mainVisual(page, palette, { left: 1150, top: 250, width: 300, height: 300, mode: "compact" }))
|
|
|
+ return elements
|
|
|
+}
|
|
|
+
|
|
|
+function homeworkLayout(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
|
|
|
+ const elements: PPTElement[] = []
|
|
|
+ addTitle(elements, page, outline, palette)
|
|
|
+ elements.push(shapeElement({ left: 110, top: 188, width: 590, height: 500, fill: "#FFFFFF", radius: 28, shadow: softShadow() }))
|
|
|
+ elements.push(textElement({ left: 150, top: 236, width: 460, height: 44, content: "课后练习", fontSize: 36, color: palette.primary, bold: true }))
|
|
|
+ elements.push(...bulletCards(normalizeBullets(page, 4), palette, { left: 150, top: 324, width: 490, itemHeight: 66, fontSize: 25, mark: "做" }))
|
|
|
+ elements.push(...mainVisual(page, palette, { left: 825, top: 188, width: 520, height: 500, mode: "photo" }))
|
|
|
+ return elements
|
|
|
+}
|
|
|
+
|
|
|
+function baseElements(palette: Palette, options: AiCoursewareVisualOptions): PPTElement[] {
|
|
|
+ const elements: PPTElement[] = []
|
|
|
+ if (options.backgroundImage) {
|
|
|
+ elements.push(shapeElement({ left: 0, top: 0, width: SLIDE_WIDTH, height: SLIDE_HEIGHT, fill: "rgba(255, 255, 255, 0.78)", opacity: 0.94 }))
|
|
|
}
|
|
|
+ elements.push(shapeElement({ left: 0, top: 0, width: 18, height: SLIDE_HEIGHT, fill: palette.primary, radius: 0 }))
|
|
|
+ elements.push(shapeElement({ left: 1260, top: 78, width: 250, height: 250, fill: palette.panelAlt, opacity: 0.7, radius: 100 }))
|
|
|
+ elements.push(shapeElement({ left: 1370, top: 675, width: 120, height: 120, fill: palette.warm, opacity: 0.5, radius: 60 }))
|
|
|
+ return elements
|
|
|
+}
|
|
|
|
|
|
- const bulletTop = isCover ? 410 : 230
|
|
|
- const bulletWidth = page.image?.url ? 820 : 960
|
|
|
- const bullets = normalizeBullets(page)
|
|
|
- elements.push(
|
|
|
- shapeElement({
|
|
|
- left: 92,
|
|
|
- top: bulletTop - 20,
|
|
|
- width: bulletWidth + 38,
|
|
|
- height: Math.min(330, 78 + bullets.length * 58),
|
|
|
- fill: hasCustomBackground ? "rgba(255, 255, 255, 0.82)" : "#FFFFFF",
|
|
|
- opacity: 0.96
|
|
|
- })
|
|
|
- )
|
|
|
- elements.push(
|
|
|
- textElement({
|
|
|
- left: 120,
|
|
|
- top: bulletTop,
|
|
|
- width: bulletWidth,
|
|
|
- height: Math.min(290, 66 + bullets.length * 50),
|
|
|
- content: bullets.map(item => `<div style="margin-bottom:12px;">${visual.bulletMark} ${escapeHtml(item)}</div>`).join(""),
|
|
|
- fontSize: isCover ? 26 : 24,
|
|
|
- color: fontColor,
|
|
|
- lineHeight: 1.36
|
|
|
- })
|
|
|
- )
|
|
|
-
|
|
|
- if (page.interaction?.question) {
|
|
|
- elements.push(shapeElement({ left: 104, top: 704, width: 1090, height: 88, fill: primary, opacity: 0.92 }))
|
|
|
- elements.push(
|
|
|
- textElement({
|
|
|
- left: 134,
|
|
|
- top: 722,
|
|
|
- width: 1020,
|
|
|
- height: 56,
|
|
|
- content: `互动:${escapeHtml(page.interaction.question)}`,
|
|
|
- fontSize: 24,
|
|
|
- color: "#FFFFFF",
|
|
|
- lineHeight: 1.35
|
|
|
- })
|
|
|
- )
|
|
|
+function addTitle(elements: PPTElement[], page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette) {
|
|
|
+ elements.push(shapeElement({ left: 90, top: 68, width: 76, height: 8, fill: palette.primary, radius: 5 }))
|
|
|
+ elements.push(textElement({ left: 92, top: 96, width: 1120, height: 62, content: page.title || outline.title || outline.chapterName, fontSize: 42, color: palette.primary, bold: true }))
|
|
|
+}
|
|
|
+
|
|
|
+function mainVisual(page: AiCoursewarePage, palette: Palette, rect: { left: number; top: number; width: number; height: number; mode: "hero" | "photo" | "compact" }): PPTElement[] {
|
|
|
+ if (page.image?.url) {
|
|
|
+ return [
|
|
|
+ shapeElement({ left: rect.left - 18, top: rect.top + 20, width: rect.width, height: rect.height, fill: palette.warm, opacity: 0.62, radius: 34 }),
|
|
|
+ imageElement({ left: rect.left, top: rect.top, width: rect.width, height: rect.height, src: page.image.url, radius: rect.mode === "compact" ? 28 : 34 })
|
|
|
+ ]
|
|
|
}
|
|
|
+ return fallbackVisualElements(page, palette, rect)
|
|
|
+}
|
|
|
+
|
|
|
+function fallbackVisualElements(page: AiCoursewarePage, palette: Palette, rect: { left: number; top: number; width: number; height: number; mode: "hero" | "photo" | "compact" }): PPTElement[] {
|
|
|
+ const elements: PPTElement[] = []
|
|
|
+ const visual = visualConfig(page, palette)
|
|
|
+ elements.push(shapeElement({ left: rect.left, top: rect.top, width: rect.width, height: rect.height, fill: visual.light, radius: 34, shadow: softShadow() }))
|
|
|
+ elements.push(shapeElement({ left: rect.left + rect.width * 0.18, top: rect.top + rect.height * 0.13, width: rect.width * 0.64, height: rect.height * 0.64, fill: "#FFFFFF", opacity: 0.88, radius: 90 }))
|
|
|
+ elements.push(textElement({ left: rect.left + rect.width * 0.36, top: rect.top + rect.height * 0.23, width: rect.width * 0.28, height: 100, content: visual.icon, fontSize: rect.mode === "compact" ? 76 : 96, color: visual.color, bold: true }))
|
|
|
+ elements.push(...soundWave(rect.left + rect.width * 0.22, rect.top + rect.height * 0.62, palette, rect.width * 0.5))
|
|
|
+ elements.push(textElement({ left: rect.left + rect.width * 0.22, top: rect.top + rect.height * 0.76, width: rect.width * 0.56, height: 42, content: visual.label, fontSize: rect.mode === "compact" ? 22 : 28, color: visual.color, bold: true }))
|
|
|
+ return elements
|
|
|
+}
|
|
|
+
|
|
|
+function bulletCards(items: string[], palette: Palette, config: { left: number; top: number; width: number; itemHeight: number; fontSize: number; mark: string }): PPTElement[] {
|
|
|
+ const elements: PPTElement[] = []
|
|
|
+ items.forEach((item, index) => {
|
|
|
+ const top = config.top + index * (config.itemHeight + 16)
|
|
|
+ elements.push(shapeElement({ left: config.left, top, width: config.width, height: config.itemHeight, fill: "#FFFFFF", radius: 20, shadow: softShadow() }))
|
|
|
+ elements.push(shapeElement({ left: config.left + 24, top: top + config.itemHeight / 2 - 18, width: 36, height: 36, fill: palette.primary, radius: 14 }))
|
|
|
+ elements.push(textElement({ left: config.left + 32, top: top + config.itemHeight / 2 - 13, width: 24, height: 22, content: escapeHtml(config.mark), fontSize: 18, color: "#FFFFFF", bold: true }))
|
|
|
+ elements.push(textElement({ left: config.left + 78, top: top + 18, width: config.width - 104, height: config.itemHeight - 28, content: escapeHtml(item), fontSize: config.fontSize, color: palette.text, lineHeight: 1.25 }))
|
|
|
+ })
|
|
|
+ return elements
|
|
|
+}
|
|
|
|
|
|
- elements.push(
|
|
|
- textElement({
|
|
|
- left: 1450,
|
|
|
- top: 838,
|
|
|
- width: 120,
|
|
|
- height: 36,
|
|
|
- content: `${page.pageNo || ""}/15`,
|
|
|
- fontSize: 18,
|
|
|
- color: "#90A4AE"
|
|
|
- })
|
|
|
- )
|
|
|
+function stepCards(items: string[], palette: Palette, left: number, top: number): PPTElement[] {
|
|
|
+ const elements: PPTElement[] = []
|
|
|
+ items.forEach((item, index) => {
|
|
|
+ const itemLeft = left + index * 405
|
|
|
+ elements.push(shapeElement({ left: itemLeft, top, width: 350, height: 160, fill: "#FFFFFF", radius: 24, shadow: softShadow() }))
|
|
|
+ elements.push(textElement({ left: itemLeft + 30, top: top + 26, width: 80, height: 32, content: `步骤${index + 1}`, fontSize: 22, color: palette.primary, bold: true }))
|
|
|
+ elements.push(textElement({ left: itemLeft + 30, top: top + 72, width: 290, height: 68, content: escapeHtml(item), fontSize: 25, color: palette.text, lineHeight: 1.3 }))
|
|
|
+ })
|
|
|
+ return elements
|
|
|
+}
|
|
|
+
|
|
|
+function addPrompt(elements: PPTElement[], page: AiCoursewarePage, palette: Palette, left: number, top: number, width: number, fallback = "") {
|
|
|
+ const text = fallback || page.interaction?.question || ""
|
|
|
+ if (!text) return
|
|
|
+ elements.push(shapeElement({ left, top, width, height: 76, fill: palette.panelAlt, radius: 20 }))
|
|
|
+ elements.push(textElement({ left: left + 28, top: top + 20, width: width - 56, height: 34, content: escapeHtml(text), fontSize: 23, color: palette.text, lineHeight: 1.3 }))
|
|
|
+}
|
|
|
+
|
|
|
+function soundWave(left: number, top: number, palette: Palette, width = 330): PPTElement[] {
|
|
|
+ const elements: PPTElement[] = []
|
|
|
+ const heights = [46, 76, 112, 70, 132, 92, 54]
|
|
|
+ heights.forEach((height, index) => {
|
|
|
+ const barWidth = Math.max(18, width / 18)
|
|
|
+ const gap = Math.max(20, width / 10)
|
|
|
+ elements.push(shapeElement({ left: left + index * gap, top: top + (140 - height) / 2, width: barWidth, height, fill: index % 2 === 0 ? palette.primary : palette.accent, radius: 10 }))
|
|
|
+ })
|
|
|
+ return elements
|
|
|
+}
|
|
|
|
|
|
+function normalizeBullets(page: AiCoursewarePage, limit: number): string[] {
|
|
|
+ const bullets = (page.bullets || []).filter(Boolean).map(item => item.trim()).filter(Boolean)
|
|
|
+ const result = bullets.length ? bullets.slice(0, limit) : [page.subtitle || page.title || "课堂内容"]
|
|
|
+ return result.map(item => trimText(item, 46))
|
|
|
+}
|
|
|
+
|
|
|
+function backgroundConfig(palette: Palette, options: AiCoursewareVisualOptions): SlideBackground {
|
|
|
+ if (options.backgroundImage) {
|
|
|
+ return {
|
|
|
+ type: "image",
|
|
|
+ image: options.backgroundImage,
|
|
|
+ imageSize: "cover"
|
|
|
+ }
|
|
|
+ }
|
|
|
return {
|
|
|
- id: nanoid(10),
|
|
|
- elements,
|
|
|
- background: backgroundConfig,
|
|
|
- remark: page.teacherTips || ""
|
|
|
+ type: "solid",
|
|
|
+ color: palette.background
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-function normalizeBullets(page: AiCoursewarePage): string[] {
|
|
|
- const bullets = (page.bullets || []).filter(Boolean)
|
|
|
- if (bullets.length) return bullets.slice(0, 5)
|
|
|
- return [page.subtitle || page.title || "课堂内容"]
|
|
|
+function buildPalette(style: string): Palette {
|
|
|
+ if (style === "clean") {
|
|
|
+ return {
|
|
|
+ primary: "#2563EB",
|
|
|
+ secondary: "#0F766E",
|
|
|
+ accent: "#7C3AED",
|
|
|
+ warm: "#FEF3C7",
|
|
|
+ background: "#F7FAFC",
|
|
|
+ panelAlt: "#EAF2FF",
|
|
|
+ text: "#1F2933",
|
|
|
+ muted: "#64748B"
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ primary: "#FF7A3D",
|
|
|
+ secondary: "#2BAE9F",
|
|
|
+ accent: "#5B8DEF",
|
|
|
+ warm: "#FFE8B8",
|
|
|
+ background: "#FFF7EC",
|
|
|
+ panelAlt: "#EAF6F3",
|
|
|
+ text: "#263238",
|
|
|
+ muted: "#6B7C8F"
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function visualConfig(page: AiCoursewarePage, palette: Palette) {
|
|
|
+ const type = page.type || ""
|
|
|
+ if (type.includes("interaction") || type.includes("activity")) return { icon: "?", label: "课堂互动", color: palette.accent, light: "#EEF2FF" }
|
|
|
+ if (type.includes("summary")) return { icon: "✓", label: "课堂小结", color: "#2E7D32", light: "#EAF7EC" }
|
|
|
+ if (type.includes("goal")) return { icon: "◎", label: "学习目标", color: "#7B1FA2", light: "#F3E8FF" }
|
|
|
+ if (type.includes("listen")) return { icon: "♪", label: "听一听", color: palette.secondary, light: "#E5F6F3" }
|
|
|
+ if (type.includes("homework")) return { icon: "✎", label: "课后练习", color: "#6D4C41", light: "#F3ECE8" }
|
|
|
+ return { icon: "♫", label: "音乐课堂", color: palette.primary, light: palette.warm }
|
|
|
}
|
|
|
|
|
|
function textElement(params: {
|
|
|
@@ -199,7 +334,7 @@ function textElement(params: {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-function imageElement(params: { left: number; top: number; width: number; height: number; src: string }): PPTImageElement {
|
|
|
+function imageElement(params: { left: number; top: number; width: number; height: number; src: string; radius?: number }): PPTImageElement {
|
|
|
return {
|
|
|
id: nanoid(10),
|
|
|
type: "image",
|
|
|
@@ -210,13 +345,8 @@ function imageElement(params: { left: number; top: number; width: number; height
|
|
|
rotate: 0,
|
|
|
src: params.src,
|
|
|
fixedRatio: false,
|
|
|
- radius: 8,
|
|
|
- shadow: {
|
|
|
- h: 0,
|
|
|
- v: 8,
|
|
|
- blur: 18,
|
|
|
- color: "rgba(38, 50, 56, 0.18)"
|
|
|
- }
|
|
|
+ radius: params.radius || 18,
|
|
|
+ shadow: softShadow()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -228,6 +358,7 @@ function shapeElement(params: {
|
|
|
fill: string
|
|
|
opacity?: number
|
|
|
radius?: number
|
|
|
+ shadow?: PPTElementShadow
|
|
|
}): PPTShapeElement {
|
|
|
return {
|
|
|
id: nanoid(10),
|
|
|
@@ -238,52 +369,21 @@ function shapeElement(params: {
|
|
|
height: params.height,
|
|
|
rotate: 0,
|
|
|
viewBox: [200, 200],
|
|
|
- path: roundedRectPath(params.radius || 18),
|
|
|
+ path: roundedRectPath(params.radius ?? 18),
|
|
|
fixedRatio: false,
|
|
|
fill: params.fill,
|
|
|
- opacity: params.opacity ?? 1
|
|
|
+ opacity: params.opacity ?? 1,
|
|
|
+ shadow: params.shadow
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-function fallbackVisualElements(page: AiCoursewarePage, visual: ReturnType<typeof visualConfig>, isCover: boolean): PPTElement[] {
|
|
|
- const left = isCover ? 1010 : 1080
|
|
|
- const top = isCover ? 168 : 180
|
|
|
- const size = isCover ? 360 : 280
|
|
|
- return [
|
|
|
- shapeElement({ left, top, width: size, height: size, fill: visual.light, opacity: 0.94, radius: 80 }),
|
|
|
- shapeElement({ left: left + 34, top: top + 34, width: size - 68, height: size - 68, fill: "#FFFFFF", opacity: 0.9, radius: 70 }),
|
|
|
- textElement({
|
|
|
- left: left + 64,
|
|
|
- top: top + (isCover ? 112 : 84),
|
|
|
- width: size - 128,
|
|
|
- height: 100,
|
|
|
- content: visual.icon,
|
|
|
- fontSize: isCover ? 76 : 62,
|
|
|
- color: visual.color,
|
|
|
- bold: true
|
|
|
- }),
|
|
|
- textElement({
|
|
|
- left: left + 54,
|
|
|
- top: top + (isCover ? 230 : 186),
|
|
|
- width: size - 108,
|
|
|
- height: 44,
|
|
|
- content: visual.label,
|
|
|
- fontSize: isCover ? 24 : 20,
|
|
|
- color: visual.color,
|
|
|
- bold: true
|
|
|
- })
|
|
|
- ]
|
|
|
-}
|
|
|
-
|
|
|
-function visualConfig(page: AiCoursewarePage, primary: string, style: string) {
|
|
|
- const type = page.type || ""
|
|
|
- const cartoonLight = style === "clean" ? "#E8F1FF" : "#FFF0D9"
|
|
|
- if (type.includes("interaction") || type.includes("activity")) return { icon: "?", label: "课堂互动", color: "#1E88E5", light: "#EAF4FF", bulletMark: "◆" }
|
|
|
- if (type.includes("summary")) return { icon: "✓", label: "课堂小结", color: "#2E7D32", light: "#EAF7EC", bulletMark: "✓" }
|
|
|
- if (type.includes("goal")) return { icon: "◎", label: "学习目标", color: "#7B1FA2", light: "#F3E8FF", bulletMark: "◎" }
|
|
|
- if (type.includes("listen")) return { icon: "♪", label: "听一听", color: "#00897B", light: "#E5F6F3", bulletMark: "♪" }
|
|
|
- if (type.includes("homework")) return { icon: "✎", label: "课后练习", color: "#6D4C41", light: "#F3ECE8", bulletMark: "•" }
|
|
|
- return { icon: "♫", label: "音乐课堂", color: primary, light: cartoonLight, bulletMark: "•" }
|
|
|
+function softShadow(): PPTElementShadow {
|
|
|
+ return {
|
|
|
+ h: 0,
|
|
|
+ v: 8,
|
|
|
+ blur: 22,
|
|
|
+ color: "rgba(38, 50, 56, 0.14)"
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
function roundedRectPath(radius: number) {
|
|
|
@@ -291,6 +391,10 @@ function roundedRectPath(radius: number) {
|
|
|
return `M ${r} 0 L ${200 - r} 0 Q 200 0 200 ${r} L 200 ${200 - r} Q 200 200 ${200 - r} 200 L ${r} 200 Q 0 200 0 ${200 - r} L 0 ${r} Q 0 0 ${r} 0 Z`
|
|
|
}
|
|
|
|
|
|
+function trimText(value: string, maxLength: number) {
|
|
|
+ return value.length > maxLength ? `${value.slice(0, maxLength - 1)}...` : value
|
|
|
+}
|
|
|
+
|
|
|
function escapeHtml(value: string) {
|
|
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
|
}
|