|
|
@@ -4,6 +4,12 @@ import type {
|
|
|
PPTElement,
|
|
|
PPTElementShadow,
|
|
|
PPTImageElement,
|
|
|
+ PPTVideoElement,
|
|
|
+ PPTEnjoyElement,
|
|
|
+ PPTCloudCoachElement,
|
|
|
+ PPTListeningPracticeElement,
|
|
|
+ PPTRhythmPracticeElement,
|
|
|
+ PPTMusicResourcesElement,
|
|
|
PPTLineElement,
|
|
|
PPTShapeElement,
|
|
|
PPTTextElement,
|
|
|
@@ -65,7 +71,12 @@ export function buildAiPptSlides(outline: AiCoursewareOutline, options: AiPptOpt
|
|
|
|
|
|
function buildSlide(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette, options: AiPptOptions, index: number): Slide {
|
|
|
const role = pageRole(page, index)
|
|
|
- const elements = templateFor(role)(page, outline, palette, index)
|
|
|
+ const dedicatedResource = dedicatedResourceElement(page)
|
|
|
+ const elements = dedicatedResource
|
|
|
+ ? [dedicatedResource]
|
|
|
+ : videoBinding(page)
|
|
|
+ ? videoResourceTemplate(page, outline, palette)
|
|
|
+ : [...templateFor(role)(page, outline, palette, index), ...inlineResourceElements(page, palette)]
|
|
|
return {
|
|
|
id: nanoid(10),
|
|
|
elements: keepInCanvas(elements),
|
|
|
@@ -117,7 +128,7 @@ function coverTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, pal
|
|
|
elements.push(textElement({ left: 92, top: 112, width: 380, height: 70, content: "音乐课堂", fontSize: 30, color: "#FFFFFF", bold: true }))
|
|
|
elements.push(textElement({ left: 650, top: 170, width: 780, height: 170, content: title, fontSize: fitFont(title, 780, 170, 70, 50), color: palette.primary, bold: true }))
|
|
|
if (subtitle) elements.push(textElement({ left: 656, top: 370, width: 740, height: 44, content: subtitle, fontSize: 24, color: palette.muted }))
|
|
|
- elements.push(...melodyStaff(120, 350, 360, palette))
|
|
|
+ elements.push(textElement({ left: 92, top: 360, width: 380, height: 180, content: "听 · 唱 · 动", fontSize: 52, color: "#FFFFFF", bold: true }))
|
|
|
elements.push(shapeElement({ left: 654, top: 486, width: 120, height: 10, fill: palette.secondary, radius: 4 }))
|
|
|
points.forEach((point, index) => elements.push(textElement({ left: 656, top: 548 + index * 66, width: 720, height: 46, content: point, fontSize: fitFont(point, 720, 46, 30, 23), color: palette.text, bold: index === 0 })))
|
|
|
return elements
|
|
|
@@ -166,7 +177,7 @@ function listenTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, pa
|
|
|
} else {
|
|
|
elements.push(shapeElement({ left: 92, top: 210, width: 1416, height: 220, fill: palette.dark, radius: 8 }))
|
|
|
elements.push(textElement({ left: 150, top: 270, width: 1300, height: 92, content: task, fontSize: fitFont(task, 1300, 92, 44, 30), color: "#FFFFFF", bold: true }))
|
|
|
- elements.push(...soundWave(250, 520, palette, 1080, 150))
|
|
|
+ elements.push(...studentPanel(page, palette, 170, 500, 1260))
|
|
|
}
|
|
|
addClassroomCue(elements, page, palette)
|
|
|
return elements
|
|
|
@@ -189,15 +200,7 @@ function notationTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline,
|
|
|
const elements = slideHeader(page, outline, palette, "旋律与谱例")
|
|
|
const headline = studentHeadline(page) || "看一看,唱一唱"
|
|
|
elements.push(textElement({ left: 92, top: 190, width: 1416, height: 72, content: headline, fontSize: fitFont(headline, 1416, 72, 40, 30), color: palette.primary, bold: true }))
|
|
|
- elements.push(shapeElement({ left: 92, top: 300, width: 1416, height: 300, fill: palette.surface, radius: 8, shadow: softShadow() }))
|
|
|
- for (let i = 0; i < 5; i++) elements.push(lineElement(160, 370 + i * 42, 1440, 370 + i * 42, palette.muted, 2))
|
|
|
- studentPoints(page, 4).forEach((point, index) => {
|
|
|
- const left = 180 + index * 315
|
|
|
- const top = 392 + (index % 2) * 44
|
|
|
- elements.push(shapeElement({ left, top, width: 42, height: 34, fill: index % 2 ? palette.secondary : palette.primary, radius: 8 }))
|
|
|
- elements.push(lineElement(left + 38, top - 52, left + 38, top + 12, index % 2 ? palette.secondary : palette.primary, 5))
|
|
|
- elements.push(textElement({ left: left - 26, top: 610, width: 250, height: 50, content: point, fontSize: fitFont(point, 250, 50, 27, 21), color: palette.text, bold: true }))
|
|
|
- })
|
|
|
+ elements.push(...knowledgeList(studentPoints(page, 3), palette, 92, 310, 1416))
|
|
|
addClassroomCue(elements, page, palette)
|
|
|
return elements
|
|
|
}
|
|
|
@@ -216,7 +219,7 @@ function instrumentTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline
|
|
|
} else {
|
|
|
elements.push(shapeElement({ left: 92, top: 210, width: 1416, height: 190, fill: palette.primary, radius: 8 }))
|
|
|
elements.push(textElement({ left: 150, top: 260, width: 1300, height: 90, content: studentHeadline(page) || page.title, fontSize: fitFont(studentHeadline(page) || page.title, 1300, 90, 44, 30), color: "#FFFFFF", bold: true }))
|
|
|
- elements.push(...soundWave(210, 500, palette, 1180, 150))
|
|
|
+ elements.push(...knowledgeList(points, palette, 170, 470, 1260))
|
|
|
}
|
|
|
addClassroomCue(elements, page, palette)
|
|
|
return elements
|
|
|
@@ -400,30 +403,76 @@ function knowledgeList(items: string[], palette: Palette, left: number, top: num
|
|
|
return elements
|
|
|
}
|
|
|
|
|
|
-function melodyStaff(left: number, top: number, width: number, palette: Palette): PPTElement[] {
|
|
|
- const elements: PPTElement[] = []
|
|
|
- for (let i = 0; i < 5; i++) elements.push(lineElement(left, top + i * 34, left + width, top + i * 34, "rgba(255,255,255,0.55)"))
|
|
|
- ;[0.12, 0.36, 0.62, 0.84].forEach((position, index) => {
|
|
|
- elements.push(shapeElement({ left: left + width * position, top: top + 48 + (index % 2) * 30, width: 34, height: 28, fill: index % 2 ? palette.accent : palette.warm, radius: 8 }))
|
|
|
- elements.push(lineElement(left + width * position + 30, top + 10 + (index % 2) * 30, left + width * position + 30, top + 60 + (index % 2) * 30, index % 2 ? palette.accent : palette.warm, 5))
|
|
|
- })
|
|
|
- return elements
|
|
|
+function addClassroomCue(elements: PPTElement[], page: AiCoursewarePage, palette: Palette) {
|
|
|
+ const cue = page.interaction?.question || activityTask(page)
|
|
|
+ if (!cue) return
|
|
|
+ elements.push(shapeElement({ left: 92, top: 824, width: 1416, height: 48, fill: palette.soft, radius: 8 }))
|
|
|
+ elements.push(textElement({ left: 120, top: 834, width: 1360, height: 28, content: cue, fontSize: fitFont(cue, 1360, 28, 20, 17), color: palette.text, bold: true }))
|
|
|
}
|
|
|
|
|
|
-function soundWave(left: number, top: number, palette: Palette, width: number, maxHeight: number): PPTElement[] {
|
|
|
- const heights = [0.28, 0.55, 0.82, 0.45, 1, 0.7, 0.38, 0.64, 0.32]
|
|
|
- return heights.map((scale, index) => shapeElement({
|
|
|
- left: left + index * (width / heights.length), top: top + (maxHeight - maxHeight * scale) / 2,
|
|
|
- width: Math.max(18, width / 24), height: maxHeight * scale,
|
|
|
- fill: index % 3 === 0 ? palette.accent : index % 2 === 0 ? palette.secondary : palette.primary, radius: 8,
|
|
|
- }))
|
|
|
+function dedicatedResourceElement(page: AiCoursewarePage): PPTElement | undefined {
|
|
|
+ const binding = (page.resourceBindings || []).find(item => item.verified !== false && item.dedicatedPage)
|
|
|
+ if (!binding) return undefined
|
|
|
+ const base = { id: nanoid(10), left: 0, top: 0, width: SLIDE_WIDTH, height: SLIDE_HEIGHT, rotate: 0 }
|
|
|
+ if (binding.resourceType === "MUSIC") {
|
|
|
+ return { ...base, type: "elf", subtype: "elf-sing-play", sid: binding.resourceId, title: binding.name || page.title } as PPTCloudCoachElement
|
|
|
+ }
|
|
|
+ if (binding.resourceType === "RHYTHM" && binding.dataJson) {
|
|
|
+ return { ...base, type: "elf", subtype: "elf-rhythm-practice", dataJson: binding.dataJson } as PPTRhythmPracticeElement
|
|
|
+ }
|
|
|
+ if (binding.resourceType === "LISTEN") {
|
|
|
+ const metadata = parseResourceJson(binding.dataJson)
|
|
|
+ const code = String(metadata.code || metadata.instrumentCode || binding.contentUrl || "")
|
|
|
+ if (code) {
|
|
|
+ return {
|
|
|
+ ...base, type: "elf", subtype: "elf-listening-practice", code,
|
|
|
+ name: binding.name || page.title, instrumentImg: binding.coverUrl || String(metadata.img || ""),
|
|
|
+ } as PPTListeningPracticeElement
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const typeMap: Record<string, "INSTRUMENT" | "MUSICIAN" | "MUSIC" | "THEORY"> = {
|
|
|
+ INSTRUMENT: "INSTRUMENT", MUSICIAN: "MUSICIAN", MUSIC_WIKI: "MUSIC", THEORY: "THEORY",
|
|
|
+ }
|
|
|
+ const sType = typeMap[binding.resourceType]
|
|
|
+ if (sType) {
|
|
|
+ return { ...base, type: "elf", subtype: "elf-music-resources", sid: binding.resourceId, name: binding.name || page.title, sType } as PPTMusicResourcesElement
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-function addClassroomCue(elements: PPTElement[], page: AiCoursewarePage, palette: Palette) {
|
|
|
- const cue = page.interaction?.question || activityTask(page)
|
|
|
- if (!cue) return
|
|
|
- elements.push(shapeElement({ left: 92, top: 754, width: 1416, height: 62, fill: palette.soft, radius: 8 }))
|
|
|
- elements.push(textElement({ left: 120, top: 772, width: 1360, height: 30, content: cue, fontSize: fitFont(cue, 1360, 30, 22, 18), color: palette.text, bold: true }))
|
|
|
+function inlineResourceElements(page: AiCoursewarePage, palette: Palette): PPTElement[] {
|
|
|
+ const binding = (page.resourceBindings || []).find(item => item.verified !== false && item.resourceType === "SONG" && item.contentUrl)
|
|
|
+ if (!binding) return []
|
|
|
+ return [{
|
|
|
+ id: nanoid(10), type: "elf", subtype: "elf-enjoy", left: 110, top: 700, width: 1380, height: 100, rotate: 0,
|
|
|
+ sid: binding.resourceId, title: binding.name || page.title, src: binding.contentUrl || "",
|
|
|
+ enjoyList: [{ id: binding.resourceId, title: binding.name || page.title, src: binding.contentUrl || "" }],
|
|
|
+ } as PPTEnjoyElement]
|
|
|
+}
|
|
|
+
|
|
|
+function videoBinding(page: AiCoursewarePage) {
|
|
|
+ return (page.resourceBindings || []).find(item => item.verified !== false && item.resourceType === "VIDEO" && item.contentUrl)
|
|
|
+}
|
|
|
+
|
|
|
+function videoResourceTemplate(page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette): PPTElement[] {
|
|
|
+ const binding = videoBinding(page)!
|
|
|
+ const elements = slideHeader(page, outline, palette, page.teachingStage || "观看与发现")
|
|
|
+ elements.push({
|
|
|
+ id: nanoid(10), type: "elf", subtype: "elf-video", left: 190, top: 190, width: 1220, height: 570, rotate: 0,
|
|
|
+ src: binding.contentUrl || "", poster: binding.coverUrl, autoplay: false,
|
|
|
+ } as PPTVideoElement)
|
|
|
+ addClassroomCue(elements, page, palette)
|
|
|
+ return elements
|
|
|
+}
|
|
|
+
|
|
|
+function parseResourceJson(value?: string): Record<string, unknown> {
|
|
|
+ if (!value) return {}
|
|
|
+ try {
|
|
|
+ const parsed = JSON.parse(value)
|
|
|
+ return parsed && typeof parsed === "object" ? parsed : {}
|
|
|
+ }
|
|
|
+ catch {
|
|
|
+ return {}
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
function normalizeOutline(outline: AiCoursewareOutline): AiCoursewareOutline {
|
|
|
@@ -503,7 +552,9 @@ function activitySteps(page: AiCoursewarePage, limit: number) {
|
|
|
}
|
|
|
|
|
|
function slideRemark(page: AiCoursewarePage) {
|
|
|
- return [page.speakerNotes, page.teacherTips, page.classroomAction?.teacherInstruction, page.teachingPurpose]
|
|
|
+ const lessonMeta = [page.teachingStage, page.durationMinutes ? `建议用时:${page.durationMinutes}分钟` : ""].filter(Boolean).join(" · ")
|
|
|
+ const evidence = page.learningEvidence ? `学习表现:${page.learningEvidence}` : ""
|
|
|
+ return [lessonMeta, page.teachingPurpose, page.speakerNotes, page.teacherTips, page.classroomAction?.teacherInstruction, evidence]
|
|
|
.map(cleanText).filter(Boolean).filter((item, index, all) => all.indexOf(item) === index).join("\n")
|
|
|
}
|
|
|
|