import { nanoid } from "nanoid" import type { Slide, PPTElement, SlideTheme, PPTTextElement, PPTImageElement } from "@/types/slides" import type { AiCoursewareOutline, AiCoursewarePage } from "@/api/aiCourseware" export function aiCoursewareToSlides(outline: AiCoursewareOutline): { title: string; theme: Partial; slides: Slide[] } { const primary = outline.style === "clean" ? "#2D6CDF" : "#FF8A3D" const background = outline.style === "clean" ? "#F7F9FC" : "#FFF8EF" const fontColor = "#263238" const theme: Partial = { themeColor: primary, backgroundColor: background, fontColor, fontName: "Microsoft Yahei", fontSize: "36px" } return { title: outline.title || outline.chapterName || "AI音乐课件", theme, slides: outline.pages.map(page => buildSlide(page, outline, primary, background, fontColor)) } } function buildSlide(page: AiCoursewarePage, outline: AiCoursewareOutline, primary: string, background: string, fontColor: string): Slide { const isCover = page.type === "cover" || page.pageNo === 1 const elements: PPTElement[] = [] elements.push( textElement({ left: 110, top: isCover ? 130 : 70, width: isCover ? 980 : 1040, height: isCover ? 160 : 82, content: page.title || outline.title, fontSize: isCover ? 58 : 38, color: primary, bold: true }) ) const subtitle = isCover ? [outline.gradeName, outline.textbookName, outline.unitName].filter(Boolean).join(" · ") : page.subtitle if (subtitle) { elements.push( textElement({ left: 116, top: isCover ? 312 : 150, width: 920, height: 48, content: subtitle, fontSize: isCover ? 28 : 20, color: fontColor }) ) } if (page.image?.url) { elements.push( imageElement({ left: isCover ? 1080 : 1180, top: isCover ? 150 : 150, width: isCover ? 650 : 590, height: isCover ? 620 : 480, src: page.image.url }) ) } const bulletTop = isCover ? 430 : 250 const bulletWidth = page.image?.url ? 900 : 1320 const bullets = normalizeBullets(page) elements.push( textElement({ left: 120, top: bulletTop, width: bulletWidth, height: Math.min(430, 72 + bullets.length * 58), content: bullets.map(item => `• ${escapeHtml(item)}`).join("
"), fontSize: isCover ? 30 : 28, color: fontColor, lineHeight: 1.45 }) ) if (page.interaction?.question) { elements.push( textElement({ left: 120, top: 760, width: 1200, height: 105, content: `互动:${escapeHtml(page.interaction.question)}`, fontSize: 28, color: "#FFFFFF", fill: primary, lineHeight: 1.35 }) ) } else if (page.teacherTips) { elements.push( textElement({ left: 120, top: 780, width: 1200, height: 86, content: `提示:${escapeHtml(page.teacherTips)}`, fontSize: 22, color: "#52616B", fill: "#FFFFFF", lineHeight: 1.35 }) ) } elements.push( textElement({ left: 1700, top: 995, width: 120, height: 36, content: `${page.pageNo || ""}/15`, fontSize: 18, color: "#90A4AE" }) ) return { id: nanoid(10), elements, background: { type: "solid", color: background }, remark: page.teacherTips || "" } } 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 textElement(params: { left: number top: number width: number height: number content: string fontSize: number color: string bold?: boolean fill?: string lineHeight?: number }): PPTTextElement { return { id: nanoid(10), type: "text", left: params.left, top: params.top, width: params.width, height: params.height, rotate: 0, content: `
${params.content}
`, defaultFontName: "Microsoft Yahei", defaultColor: params.color, fill: params.fill, lineHeight: params.lineHeight || 1.25, wordSpace: 0, opacity: 1 } } function imageElement(params: { left: number; top: number; width: number; height: number; src: string }): PPTImageElement { return { id: nanoid(10), type: "image", left: params.left, top: params.top, width: params.width, height: params.height, rotate: 0, src: params.src, fixedRatio: false, radius: 12, shadow: { h: 0, v: 8, blur: 18, color: "rgba(38, 50, 56, 0.18)" } } } function escapeHtml(value: string) { return value.replace(/&/g, "&").replace(//g, ">") }