aiCoursewareToSlides.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. import { nanoid } from "nanoid"
  2. import type { Slide, PPTElement, SlideTheme, PPTTextElement, PPTImageElement } from "@/types/slides"
  3. import type { AiCoursewareOutline, AiCoursewarePage } from "@/api/aiCourseware"
  4. export function aiCoursewareToSlides(outline: AiCoursewareOutline): { title: string; theme: Partial<SlideTheme>; slides: Slide[] } {
  5. const primary = outline.style === "clean" ? "#2D6CDF" : "#FF8A3D"
  6. const background = outline.style === "clean" ? "#F7F9FC" : "#FFF8EF"
  7. const fontColor = "#263238"
  8. const theme: Partial<SlideTheme> = {
  9. themeColor: primary,
  10. backgroundColor: background,
  11. fontColor,
  12. fontName: "Microsoft Yahei",
  13. fontSize: "36px"
  14. }
  15. return {
  16. title: outline.title || outline.chapterName || "AI音乐课件",
  17. theme,
  18. slides: outline.pages.map(page => buildSlide(page, outline, primary, background, fontColor))
  19. }
  20. }
  21. function buildSlide(page: AiCoursewarePage, outline: AiCoursewareOutline, primary: string, background: string, fontColor: string): Slide {
  22. const isCover = page.type === "cover" || page.pageNo === 1
  23. const elements: PPTElement[] = []
  24. elements.push(
  25. textElement({
  26. left: 110,
  27. top: isCover ? 130 : 70,
  28. width: isCover ? 980 : 1040,
  29. height: isCover ? 160 : 82,
  30. content: page.title || outline.title,
  31. fontSize: isCover ? 58 : 38,
  32. color: primary,
  33. bold: true
  34. })
  35. )
  36. const subtitle = isCover ? [outline.gradeName, outline.textbookName, outline.unitName].filter(Boolean).join(" · ") : page.subtitle
  37. if (subtitle) {
  38. elements.push(
  39. textElement({
  40. left: 116,
  41. top: isCover ? 312 : 150,
  42. width: 920,
  43. height: 48,
  44. content: subtitle,
  45. fontSize: isCover ? 28 : 20,
  46. color: fontColor
  47. })
  48. )
  49. }
  50. if (page.image?.url) {
  51. elements.push(
  52. imageElement({
  53. left: isCover ? 1080 : 1180,
  54. top: isCover ? 150 : 150,
  55. width: isCover ? 650 : 590,
  56. height: isCover ? 620 : 480,
  57. src: page.image.url
  58. })
  59. )
  60. }
  61. const bulletTop = isCover ? 430 : 250
  62. const bulletWidth = page.image?.url ? 900 : 1320
  63. const bullets = normalizeBullets(page)
  64. elements.push(
  65. textElement({
  66. left: 120,
  67. top: bulletTop,
  68. width: bulletWidth,
  69. height: Math.min(430, 72 + bullets.length * 58),
  70. content: bullets.map(item => `• ${escapeHtml(item)}`).join("<br>"),
  71. fontSize: isCover ? 30 : 28,
  72. color: fontColor,
  73. lineHeight: 1.45
  74. })
  75. )
  76. if (page.interaction?.question) {
  77. elements.push(
  78. textElement({
  79. left: 120,
  80. top: 760,
  81. width: 1200,
  82. height: 105,
  83. content: `互动:${escapeHtml(page.interaction.question)}`,
  84. fontSize: 28,
  85. color: "#FFFFFF",
  86. fill: primary,
  87. lineHeight: 1.35
  88. })
  89. )
  90. } else if (page.teacherTips) {
  91. elements.push(
  92. textElement({
  93. left: 120,
  94. top: 780,
  95. width: 1200,
  96. height: 86,
  97. content: `提示:${escapeHtml(page.teacherTips)}`,
  98. fontSize: 22,
  99. color: "#52616B",
  100. fill: "#FFFFFF",
  101. lineHeight: 1.35
  102. })
  103. )
  104. }
  105. elements.push(
  106. textElement({
  107. left: 1700,
  108. top: 995,
  109. width: 120,
  110. height: 36,
  111. content: `${page.pageNo || ""}/15`,
  112. fontSize: 18,
  113. color: "#90A4AE"
  114. })
  115. )
  116. return {
  117. id: nanoid(10),
  118. elements,
  119. background: {
  120. type: "solid",
  121. color: background
  122. },
  123. remark: page.teacherTips || ""
  124. }
  125. }
  126. function normalizeBullets(page: AiCoursewarePage): string[] {
  127. const bullets = (page.bullets || []).filter(Boolean)
  128. if (bullets.length) return bullets.slice(0, 5)
  129. return [page.subtitle || page.title || "课堂内容"]
  130. }
  131. function textElement(params: {
  132. left: number
  133. top: number
  134. width: number
  135. height: number
  136. content: string
  137. fontSize: number
  138. color: string
  139. bold?: boolean
  140. fill?: string
  141. lineHeight?: number
  142. }): PPTTextElement {
  143. return {
  144. id: nanoid(10),
  145. type: "text",
  146. left: params.left,
  147. top: params.top,
  148. width: params.width,
  149. height: params.height,
  150. rotate: 0,
  151. content: `<div style="font-size:${params.fontSize}px;${params.bold ? "font-weight:700;" : ""}">${params.content}</div>`,
  152. defaultFontName: "Microsoft Yahei",
  153. defaultColor: params.color,
  154. fill: params.fill,
  155. lineHeight: params.lineHeight || 1.25,
  156. wordSpace: 0,
  157. opacity: 1
  158. }
  159. }
  160. function imageElement(params: { left: number; top: number; width: number; height: number; src: string }): PPTImageElement {
  161. return {
  162. id: nanoid(10),
  163. type: "image",
  164. left: params.left,
  165. top: params.top,
  166. width: params.width,
  167. height: params.height,
  168. rotate: 0,
  169. src: params.src,
  170. fixedRatio: false,
  171. radius: 12,
  172. shadow: {
  173. h: 0,
  174. v: 8,
  175. blur: 18,
  176. color: "rgba(38, 50, 56, 0.18)"
  177. }
  178. }
  179. }
  180. function escapeHtml(value: string) {
  181. return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
  182. }