@lemonppt/renderer 1.0.6 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/dist/browser-export-driver.d.ts +28 -0
  2. package/dist/browser-export-driver.d.ts.map +1 -0
  3. package/dist/browser-export-driver.js +115 -0
  4. package/dist/browser-export-driver.js.map +1 -0
  5. package/dist/browser-export.d.ts +66 -0
  6. package/dist/browser-export.d.ts.map +1 -0
  7. package/dist/browser-export.js +176 -0
  8. package/dist/browser-export.js.map +1 -0
  9. package/dist/client/browser-export.js +1 -0
  10. package/dist/client/client-render.js +21 -21
  11. package/dist/client/editor-script.js +1 -2
  12. package/dist/client/theme-echarts.js +27 -27
  13. package/dist/editor-script.d.ts +1 -1
  14. package/dist/editor-script.d.ts.map +1 -1
  15. package/dist/editor-script.js +1 -2
  16. package/dist/editor-script.js.map +1 -1
  17. package/dist/export-pdf.d.ts.map +1 -1
  18. package/dist/export-pdf.js +7 -4
  19. package/dist/export-pdf.js.map +1 -1
  20. package/dist/export-pptx-screenshot.d.ts +36 -0
  21. package/dist/export-pptx-screenshot.d.ts.map +1 -0
  22. package/dist/export-pptx-screenshot.js +74 -0
  23. package/dist/export-pptx-screenshot.js.map +1 -0
  24. package/dist/index.d.ts +3 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +3 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/normalize-goal.d.ts +6 -0
  29. package/dist/normalize-goal.d.ts.map +1 -0
  30. package/dist/normalize-goal.js +12 -0
  31. package/dist/normalize-goal.js.map +1 -0
  32. package/dist/preview-server.d.ts +9 -0
  33. package/dist/preview-server.d.ts.map +1 -0
  34. package/dist/preview-server.js +81 -0
  35. package/dist/preview-server.js.map +1 -0
  36. package/dist/render-editor-data.d.ts.map +1 -1
  37. package/dist/render-editor-data.js +29 -2
  38. package/dist/render-editor-data.js.map +1 -1
  39. package/dist/render.d.ts.map +1 -1
  40. package/dist/render.js +25 -5
  41. package/dist/render.js.map +1 -1
  42. package/package.json +8 -5
  43. package/templates/editor.html +26 -0
  44. package/templates/editor.js +1 -0
  45. package/dist/export-pptx.d.ts +0 -1167
  46. package/dist/export-pptx.d.ts.map +0 -1
  47. package/dist/export-pptx.js +0 -3007
  48. package/dist/export-pptx.js.map +0 -1
  49. package/dist/theme01-pptx.d.ts +0 -12
  50. package/dist/theme01-pptx.d.ts.map +0 -1
  51. package/dist/theme01-pptx.js +0 -2957
  52. package/dist/theme01-pptx.js.map +0 -1
  53. package/dist/theme02-pptx.d.ts +0 -5
  54. package/dist/theme02-pptx.d.ts.map +0 -1
  55. package/dist/theme02-pptx.js +0 -1063
  56. package/dist/theme02-pptx.js.map +0 -1
  57. package/dist/theme03-pptx.d.ts +0 -5
  58. package/dist/theme03-pptx.d.ts.map +0 -1
  59. package/dist/theme03-pptx.js +0 -3279
  60. package/dist/theme03-pptx.js.map +0 -1
  61. package/dist/theme04-pptx.d.ts +0 -6
  62. package/dist/theme04-pptx.d.ts.map +0 -1
  63. package/dist/theme04-pptx.js +0 -5496
  64. package/dist/theme04-pptx.js.map +0 -1
  65. package/dist/theme05-pptx.d.ts +0 -5
  66. package/dist/theme05-pptx.d.ts.map +0 -1
  67. package/dist/theme05-pptx.js +0 -3152
  68. package/dist/theme05-pptx.js.map +0 -1
  69. package/dist/theme06-pptx.d.ts +0 -124
  70. package/dist/theme06-pptx.d.ts.map +0 -1
  71. package/dist/theme06-pptx.js +0 -8154
  72. package/dist/theme06-pptx.js.map +0 -1
  73. package/dist/theme07-pptx.d.ts +0 -49
  74. package/dist/theme07-pptx.d.ts.map +0 -1
  75. package/dist/theme07-pptx.js +0 -2579
  76. package/dist/theme07-pptx.js.map +0 -1
  77. package/dist/theme08-pptx.d.ts +0 -19
  78. package/dist/theme08-pptx.d.ts.map +0 -1
  79. package/dist/theme08-pptx.js +0 -1411
  80. package/dist/theme08-pptx.js.map +0 -1
  81. package/dist/theme09-pptx.d.ts +0 -18
  82. package/dist/theme09-pptx.d.ts.map +0 -1
  83. package/dist/theme09-pptx.js +0 -1513
  84. package/dist/theme09-pptx.js.map +0 -1
  85. package/dist/theme10-pptx.d.ts +0 -18
  86. package/dist/theme10-pptx.d.ts.map +0 -1
  87. package/dist/theme10-pptx.js +0 -2889
  88. package/dist/theme10-pptx.js.map +0 -1
  89. package/dist/theme11-pptx.d.ts +0 -17
  90. package/dist/theme11-pptx.d.ts.map +0 -1
  91. package/dist/theme11-pptx.js +0 -908
  92. package/dist/theme11-pptx.js.map +0 -1
@@ -1,3152 +0,0 @@
1
- // lemonPPT - AI-powered presentation generation
2
- // Copyright (c) 2026 lemonforme
3
- // SPDX-License-Identifier: AGPL-3.0-or-later
4
- //
5
- // theme05 PPTX 版式专属渲染器(从 export-pptx.ts 抽出)。
6
- // 共享基础设施(全局 helper + 通用渲染器 + 类型 + theme05 局部 helper/const)从 export-pptx.ts 导入;
7
- // theme01 通用渲染器从 theme01-pptx.ts 复用;其他已抽出主题的渲染器从各自 theme<K>-pptx.ts 复用。
8
- // 注册通过 registerTheme05Renderers 回调注入。
9
- import { COLORS, FONTS, addTheme05Card, addTheme05SpectrumBar, CHART_COLORS, addImageMaybe, theme05SchemeColor, addTheme05Pill, addTheme05ConclusionCard, layoutTreemap } from './export-pptx.js';
10
- function renderTheme05CoverV1(slide, props) {
11
- if (props.tag) {
12
- slide.addText(props.tag, {
13
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
14
- fontSize: 12, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
15
- });
16
- }
17
- slide.addText(props.title ?? '', {
18
- x: 0.65, y: 1.3, w: 6.5, h: 1.4,
19
- fontSize: 52, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
20
- });
21
- if (props.subtitle) {
22
- slide.addText(props.subtitle, {
23
- x: 0.65, y: 2.75, w: 6.5, h: 0.6,
24
- fontSize: 18, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
25
- });
26
- }
27
- const metrics = (props.metrics || []).slice(0, 4);
28
- if (metrics.length > 0) {
29
- const cardW = 2.05;
30
- const startX = 7.05;
31
- metrics.forEach((m, idx) => {
32
- const y = 0.9 + idx * 1.05;
33
- addTheme05Card(slide, startX, y, cardW, 0.9, !!m.accent);
34
- slide.addText(`${m.value ?? ''}${m.unit ?? ''}`, {
35
- x: startX + 0.12, y: y + 0.12, w: cardW - 0.24, h: 0.4,
36
- fontSize: 26, color: m.accent ? COLORS.white : COLORS.primary, bold: true, fontFace: FONTS.heading,
37
- });
38
- slide.addText(m.label ?? '', {
39
- x: startX + 0.12, y: y + 0.52, w: cardW - 0.24, h: 0.22,
40
- fontSize: 10, color: m.accent ? 'FFFFFF' : COLORS.secondary, fontFace: FONTS.body,
41
- });
42
- });
43
- }
44
- addTheme05SpectrumBar(slide);
45
- const footerText = [props.footnoteLeft, props.footnoteRight].filter(Boolean).join(' ');
46
- if (footerText) {
47
- slide.addText(footerText, {
48
- x: 0.65, y: 5.32, w: 8.7, h: 0.2,
49
- fontSize: 9, color: COLORS.secondary, align: 'center', fontFace: FONTS.mono,
50
- });
51
- }
52
- }
53
- function renderTheme05ChapterV1(slide, props) {
54
- if (props.tag) {
55
- slide.addText(props.tag, {
56
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
57
- fontSize: 12, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
58
- });
59
- }
60
- slide.addText(props.number ?? '', {
61
- x: 0.65, y: 1.6, w: 8.7, h: 1.4,
62
- fontSize: 110, color: COLORS.accent, bold: true, valign: 'top', fontFace: FONTS.heading,
63
- });
64
- slide.addText(props.title ?? '', {
65
- x: 0.65, y: 3.1, w: 8.7, h: 0.9,
66
- fontSize: 44, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
67
- });
68
- if (props.subtitle) {
69
- slide.addText(props.subtitle, {
70
- x: 0.65, y: 4.05, w: 8.7, h: 0.5,
71
- fontSize: 16, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
72
- });
73
- }
74
- addTheme05SpectrumBar(slide);
75
- }
76
- function renderTheme05ContentV1(slide, props) {
77
- if (props.kicker) {
78
- slide.addText(props.kicker, {
79
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
80
- fontSize: 11, color: COLORS.secondary, fontFace: FONTS.mono,
81
- });
82
- }
83
- slide.addText(props.title ?? '', {
84
- x: 0.65, y: 1.2, w: 4.2, h: 0.9,
85
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
86
- });
87
- slide.addShape('rect', {
88
- x: 0.65, y: 2.12, w: 1.4, h: 0.05,
89
- fill: { color: COLORS.accent },
90
- });
91
- if (props.subtitle) {
92
- slide.addText(props.subtitle, {
93
- x: 0.65, y: 2.35, w: 4.2, h: 0.7,
94
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
95
- });
96
- }
97
- const bullets = (props.bullets || []).slice(0, 8);
98
- if (bullets.length > 0) {
99
- const startX = 5.05;
100
- const cardW = 4.1;
101
- bullets.forEach((bullet, idx) => {
102
- const text = typeof bullet === 'string' ? bullet : bullet.item ?? '';
103
- const y = 0.85 + idx * 0.6;
104
- slide.addShape('ellipse', {
105
- x: startX + 0.05, y: y + 0.16, w: 0.1, h: 0.1,
106
- fill: { color: COLORS.accent },
107
- });
108
- slide.addText(text, {
109
- x: startX + 0.3, y, w: cardW - 0.3, h: 0.55,
110
- fontSize: 16, color: COLORS.primary, align: 'left', valign: 'top', fontFace: FONTS.body,
111
- });
112
- });
113
- }
114
- const showConclusion = props.showConclusion !== false;
115
- const conclusion = props.conclusion;
116
- const hasConclusion = showConclusion && conclusion && (conclusion.value || conclusion.label || conclusion.description);
117
- if (hasConclusion) {
118
- const y = 4.55;
119
- addTheme05Card(slide, 5.05, y, 4.1, 0.85, true);
120
- let cursorY = y + 0.12;
121
- if (conclusion.value) {
122
- slide.addText(conclusion.value, {
123
- x: 5.25, y: cursorY, w: 3.7, h: 0.35,
124
- fontSize: 24, color: COLORS.white, bold: true, fontFace: FONTS.heading,
125
- });
126
- cursorY += 0.36;
127
- }
128
- if (conclusion.label) {
129
- slide.addText(conclusion.label, {
130
- x: 5.25, y: cursorY, w: 3.7, h: 0.2,
131
- fontSize: 10, color: 'FFFFFF', fontFace: FONTS.body,
132
- });
133
- cursorY += 0.22;
134
- }
135
- if (conclusion.description) {
136
- slide.addText(conclusion.description, {
137
- x: 5.25, y: cursorY, w: 3.7, h: 0.35,
138
- fontSize: 10, color: 'FFFFFF', valign: 'top', fontFace: FONTS.body,
139
- });
140
- }
141
- }
142
- addTheme05SpectrumBar(slide);
143
- }
144
- function renderTheme05MetricV1(slide, props) {
145
- if (props.kicker) {
146
- slide.addText(props.kicker, {
147
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
148
- fontSize: 11, color: COLORS.secondary, fontFace: FONTS.mono,
149
- });
150
- }
151
- slide.addText(props.title ?? '', {
152
- x: 0.65, y: 1.15, w: 8.7, h: 0.7,
153
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
154
- });
155
- if (props.subtitle) {
156
- slide.addText(props.subtitle, {
157
- x: 0.65, y: 1.85, w: 8.7, h: 0.35,
158
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
159
- });
160
- }
161
- slide.addText(props.value ?? '', {
162
- x: 0.65, y: 2.45, w: 4.0, h: 1.0,
163
- fontSize: 80, color: COLORS.accent, bold: true, valign: 'top', fontFace: FONTS.heading,
164
- });
165
- if (props.unit) {
166
- slide.addText(props.unit, {
167
- x: 3.2, y: 2.95, w: 1.5, h: 0.35,
168
- fontSize: 22, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
169
- });
170
- }
171
- const metrics = (props.metrics || []).slice(0, 4);
172
- if (metrics.length > 0) {
173
- const cardW = 2.05;
174
- const gap = 0.18;
175
- const startX = 0.65;
176
- metrics.forEach((m, idx) => {
177
- const x = startX + idx * (cardW + gap);
178
- const y = 4.05;
179
- addTheme05Card(slide, x, y, cardW, 1.1, !!m.accent);
180
- slide.addText(`${m.value ?? ''}${m.unit ?? ''}`, {
181
- x, y: y + 0.15, w: cardW, h: 0.45,
182
- fontSize: 28, color: m.accent ? COLORS.white : COLORS.primary, bold: true, align: 'center', fontFace: FONTS.heading,
183
- });
184
- slide.addText(m.label ?? '', {
185
- x, y: y + 0.62, w: cardW, h: 0.25,
186
- fontSize: 10, color: m.accent ? 'FFFFFF' : COLORS.secondary, align: 'center', fontFace: FONTS.body,
187
- });
188
- });
189
- }
190
- addTheme05SpectrumBar(slide);
191
- }
192
- function renderTheme05ChartV1(slide, props) {
193
- if (props.kicker) {
194
- slide.addText(props.kicker, {
195
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
196
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
197
- });
198
- }
199
- slide.addText(props.title ?? '', {
200
- x: 0.65, y: 1.15, w: 8.7, h: 0.7,
201
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
202
- });
203
- if (props.subtitle) {
204
- slide.addText(props.subtitle, {
205
- x: 0.65, y: 1.85, w: 8.7, h: 0.3,
206
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
207
- });
208
- }
209
- const labels = props.labels || [];
210
- const data = props.data || [];
211
- const showConclusion = props.showConclusion !== false;
212
- const conclusion = props.conclusion;
213
- const hasConclusion = showConclusion && conclusion && (conclusion.value || conclusion.label || conclusion.description);
214
- const chartW = hasConclusion ? 5.6 : 8.7;
215
- if (labels.length > 0 && data.length > 0) {
216
- const chartType = props.type === 'line' ? 'line' : 'bar';
217
- slide.addChart(chartType, [{
218
- name: props.title || '',
219
- labels: labels.map((l) => (typeof l === 'string' ? l : l.item ?? '')),
220
- values: data.map((d) => (typeof d === 'number' ? d : Number(d.item ?? 0) || 0)),
221
- }], {
222
- x: 0.65, y: 2.25, w: chartW, h: 3.0,
223
- chartColors: [COLORS.accent],
224
- showValue: true,
225
- dataLabelColor: COLORS.primary,
226
- dataLabelFontSize: 10,
227
- });
228
- }
229
- else {
230
- slide.addText('(暂无图表数据)', {
231
- x: 0.65, y: 3.0, w: chartW, h: 0.6,
232
- fontSize: 16, color: COLORS.secondary, align: 'center', valign: 'middle', fontFace: FONTS.body,
233
- });
234
- }
235
- if (props.unit) {
236
- addTheme05Card(slide, 6.45, 2.25, 2.9, 0.7);
237
- slide.addText('单位', {
238
- x: 6.6, y: 2.32, w: 2.6, h: 0.2,
239
- fontSize: 10, color: COLORS.secondary, fontFace: FONTS.body,
240
- });
241
- slide.addText(props.unit, {
242
- x: 6.6, y: 2.52, w: 2.6, h: 0.35,
243
- fontSize: 18, color: COLORS.primary, bold: true, fontFace: FONTS.heading,
244
- });
245
- }
246
- if (hasConclusion) {
247
- const panelY = props.unit ? 3.05 : 2.25;
248
- const panelH = props.unit ? 2.2 : 3.0;
249
- addTheme05Card(slide, 6.45, panelY, 2.9, panelH, true);
250
- let cursorY = panelY + 0.18;
251
- if (conclusion.value) {
252
- slide.addText(conclusion.value, {
253
- x: 6.65, y: cursorY, w: 2.5, h: 0.55,
254
- fontSize: 32, color: COLORS.white, bold: true, fontFace: FONTS.heading,
255
- });
256
- cursorY += 0.58;
257
- }
258
- if (conclusion.label) {
259
- slide.addText(conclusion.label, {
260
- x: 6.65, y: cursorY, w: 2.5, h: 0.25,
261
- fontSize: 10, color: 'FFFFFF', fontFace: FONTS.body,
262
- });
263
- cursorY += 0.32;
264
- }
265
- if (conclusion.description) {
266
- slide.addText(conclusion.description, {
267
- x: 6.65, y: cursorY, w: 2.5, h: panelH - (cursorY - panelY) - 0.25,
268
- fontSize: 10, color: 'FFFFFF', valign: 'top', fontFace: FONTS.body,
269
- });
270
- }
271
- }
272
- addTheme05SpectrumBar(slide);
273
- }
274
- function renderTheme05RankV1(slide, props) {
275
- if (props.kicker) {
276
- slide.addText(props.kicker, {
277
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
278
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
279
- });
280
- }
281
- slide.addText(props.title ?? '', {
282
- x: 0.65, y: 1.15, w: 8.7, h: 0.7,
283
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
284
- });
285
- if (props.subtitle) {
286
- slide.addText(props.subtitle, {
287
- x: 0.65, y: 1.85, w: 8.7, h: 0.3,
288
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
289
- });
290
- }
291
- const rows = (props.rows || []).slice(0, 8);
292
- if (rows.length > 0) {
293
- const rowH = 0.48;
294
- const startY = 2.35;
295
- rows.forEach((row, idx) => {
296
- const y = startY + idx * (rowH + 0.1);
297
- const rankColor = idx === 0 ? COLORS.accent : COLORS.surfaceElevated;
298
- slide.addShape('roundRect', {
299
- x: 0.65, y, w: 0.5, h: rowH,
300
- fill: { color: rankColor },
301
- rectRadius: 0.06,
302
- });
303
- slide.addText(String(row.rank ?? idx + 1), {
304
- x: 0.65, y, w: 0.5, h: rowH,
305
- fontSize: 14, color: idx === 0 ? COLORS.white : COLORS.primary, bold: true, align: 'center', valign: 'middle', fontFace: FONTS.heading,
306
- });
307
- slide.addText(row.name ?? '', {
308
- x: 1.3, y, w: 4.0, h: rowH,
309
- fontSize: 15, color: COLORS.primary, bold: true, valign: 'middle', fontFace: FONTS.body,
310
- });
311
- slide.addText(row.value ?? '', {
312
- x: 5.6, y, w: 1.8, h: rowH,
313
- fontSize: 16, color: COLORS.primary, bold: true, align: 'right', valign: 'middle', fontFace: FONTS.mono,
314
- });
315
- if (row.change) {
316
- slide.addText(row.change, {
317
- x: 7.55, y: y + 0.08, w: 1.5, h: 0.32,
318
- fontSize: 11, color: row.positive === false ? 'E85D4E' : '22C55E', bold: true, align: 'right', valign: 'middle', fontFace: FONTS.mono,
319
- });
320
- }
321
- });
322
- }
323
- addTheme05SpectrumBar(slide);
324
- }
325
- function renderTheme05HeatmapV1(slide, props) {
326
- if (props.kicker) {
327
- slide.addText(props.kicker, {
328
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
329
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
330
- });
331
- }
332
- slide.addText(props.title ?? '', {
333
- x: 0.65, y: 1.15, w: 8.7, h: 0.7,
334
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
335
- });
336
- if (props.subtitle) {
337
- slide.addText(props.subtitle, {
338
- x: 0.65, y: 1.85, w: 8.7, h: 0.3,
339
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
340
- });
341
- }
342
- const values = (props.values || []).slice(0, 12);
343
- const months = (props.months || []).slice(0, 12);
344
- if (values.length > 0) {
345
- const max = Math.max(...values.map((v) => (typeof v === 'number' ? v : Number(v.item ?? 0) || 0)), 1);
346
- const cols = Math.min(values.length, 6);
347
- const cellW = 1.35;
348
- const cellH = 0.9;
349
- const totalW = cols * cellW + (cols - 1) * 0.1;
350
- const startX = (10 - totalW) / 2;
351
- const startY = 2.4;
352
- const heatColor = (value) => {
353
- const r = value / max;
354
- if (r > 0.75)
355
- return COLORS.accent;
356
- if (r > 0.5)
357
- return CHART_COLORS[1] || 'F5A623';
358
- if (r > 0.25)
359
- return CHART_COLORS[2] || '0FA3B1';
360
- return COLORS.surfaceElevated;
361
- };
362
- values.forEach((v, idx) => {
363
- const value = typeof v === 'number' ? v : Number(v.item ?? 0) || 0;
364
- const r = value / max;
365
- const col = idx % cols;
366
- const row = Math.floor(idx / cols);
367
- const x = startX + col * (cellW + 0.1);
368
- const y = startY + row * (cellH + 0.12);
369
- slide.addShape('roundRect', {
370
- x, y, w: cellW, h: cellH,
371
- fill: { color: heatColor(value) },
372
- rectRadius: 0.06,
373
- });
374
- slide.addText(String(value), {
375
- x, y: y + 0.05, w: cellW, h: 0.4,
376
- fontSize: 18, color: r > 0.5 ? COLORS.white : COLORS.primary, bold: true, align: 'center', valign: 'middle', fontFace: FONTS.heading,
377
- });
378
- if (months[idx]) {
379
- const monthText = typeof months[idx] === 'string' ? months[idx] : months[idx].item ?? '';
380
- slide.addText(monthText, {
381
- x, y: y + 0.48, w: cellW, h: 0.3,
382
- fontSize: 10, color: r > 0.5 ? 'FFFFFF' : COLORS.secondary, align: 'center', valign: 'middle', fontFace: FONTS.body,
383
- });
384
- }
385
- });
386
- }
387
- if (props.peakLabel) {
388
- addTheme05Card(slide, 0.65, 4.6, 4.0, 0.55, true);
389
- slide.addText(props.peakLabel, {
390
- x: 0.8, y: 4.68, w: 3.7, h: 0.38,
391
- fontSize: 14, color: COLORS.white, bold: true, valign: 'middle', fontFace: FONTS.body,
392
- });
393
- }
394
- addTheme05SpectrumBar(slide);
395
- }
396
- function renderTheme05WaterfallV1(slide, props) {
397
- if (props.kicker) {
398
- slide.addText(props.kicker, {
399
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
400
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
401
- });
402
- }
403
- slide.addText(props.title ?? '', {
404
- x: 0.65, y: 1.15, w: 8.7, h: 0.7,
405
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
406
- });
407
- if (props.subtitle) {
408
- slide.addText(props.subtitle, {
409
- x: 0.65, y: 1.85, w: 8.7, h: 0.3,
410
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
411
- });
412
- }
413
- const items = (props.items || []).slice(0, 8);
414
- if (items.length > 0) {
415
- const labels = items.map((item) => item.name ?? '');
416
- const values = items.map((item) => Number(item.value ?? 0) || 0);
417
- let cumulative = 0;
418
- const baseData = [];
419
- const changeData = [];
420
- const changeColors = [];
421
- values.forEach((v) => {
422
- baseData.push(cumulative);
423
- changeData.push(v);
424
- changeColors.push(v >= 0 ? COLORS.accent : 'E85D4E');
425
- cumulative += v;
426
- });
427
- const total = cumulative;
428
- // 总计柱:从 0 开始到 total
429
- baseData.push(0);
430
- changeData.push(total);
431
- changeColors.push(CHART_COLORS[2] || '0FA3B1');
432
- const chartLabels = [...labels, props.totalLabel ?? '总计'];
433
- slide.addChart('bar', [
434
- {
435
- name: '基础',
436
- labels: chartLabels,
437
- values: baseData,
438
- },
439
- {
440
- name: '变化',
441
- labels: chartLabels,
442
- values: changeData,
443
- },
444
- ], {
445
- x: 0.65, y: 2.35, w: 5.6, h: 3.0,
446
- chartColors: [COLORS.surface, COLORS.accent],
447
- showValue: true,
448
- dataLabelColor: COLORS.primary,
449
- dataLabelFontSize: 9,
450
- barGrouping: 'stacked',
451
- barDir: 'col',
452
- dataLabelPosition: 'outEnd',
453
- });
454
- addTheme05Card(slide, 6.45, 2.35, 2.9, 3.0);
455
- items.forEach((item, idx) => {
456
- const y = 2.5 + idx * 0.36;
457
- const color = (Number(item.value ?? 0) || 0) >= 0 ? COLORS.accent : 'E85D4E';
458
- slide.addText(item.name ?? '', {
459
- x: 6.6, y, w: 1.6, h: 0.28,
460
- fontSize: 11, color: COLORS.primary, valign: 'middle', fontFace: FONTS.body,
461
- });
462
- slide.addText(`${(Number(item.value ?? 0) || 0) >= 0 ? '+' : ''}${item.value ?? 0}`, {
463
- x: 8.2, y, w: 1.0, h: 0.28,
464
- fontSize: 11, color, bold: true, align: 'right', valign: 'middle', fontFace: FONTS.mono,
465
- });
466
- });
467
- slide.addShape('rect', {
468
- x: 6.6, y: 2.5 + items.length * 0.36 + 0.08, w: 2.6, h: 0.02,
469
- fill: { color: COLORS.border },
470
- });
471
- slide.addText(props.totalLabel ?? '总计', {
472
- x: 6.6, y: 2.5 + items.length * 0.36 + 0.16, w: 1.6, h: 0.28,
473
- fontSize: 12, color: COLORS.primary, bold: true, valign: 'middle', fontFace: FONTS.body,
474
- });
475
- slide.addText(String(total), {
476
- x: 8.2, y: 2.5 + items.length * 0.36 + 0.16, w: 1.0, h: 0.28,
477
- fontSize: 12, color: CHART_COLORS[2] || '0FA3B1', bold: true, align: 'right', valign: 'middle', fontFace: FONTS.mono,
478
- });
479
- }
480
- else {
481
- slide.addText('(暂无瀑布图数据)', {
482
- x: 0.65, y: 3.0, w: 8.7, h: 0.6,
483
- fontSize: 16, color: COLORS.secondary, align: 'center', valign: 'middle', fontFace: FONTS.body,
484
- });
485
- }
486
- addTheme05SpectrumBar(slide);
487
- }
488
- function renderTheme05QuoteV1(slide, props) {
489
- slide.addShape('rect', {
490
- x: 0.65, y: 0.9, w: 0.12, h: 4.0,
491
- fill: { color: COLORS.accent },
492
- });
493
- slide.addText('“', {
494
- x: 1.0, y: 1.0, w: 1.0, h: 0.7,
495
- fontSize: 80, color: COLORS.accent, bold: true, valign: 'top', fontFace: FONTS.heading,
496
- });
497
- slide.addText(props.quote ?? '', {
498
- x: 1.0, y: 1.9, w: 7.5, h: 2.4,
499
- fontSize: 32, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
500
- });
501
- if (props.source) {
502
- slide.addText(props.source, {
503
- x: 1.0, y: 4.4, w: 7.5, h: 0.3,
504
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
505
- });
506
- }
507
- addTheme05SpectrumBar(slide);
508
- }
509
- function renderTheme05ImageV1(slide, props) {
510
- if (props.image) {
511
- addImageMaybe(slide, props.image, 0.65, 0.65, 5.4, 4.45);
512
- }
513
- else {
514
- slide.addShape('roundRect', {
515
- x: 0.65, y: 0.65, w: 5.4, h: 4.45,
516
- fill: { color: COLORS.surfaceElevated },
517
- line: { color: COLORS.border, width: 1 },
518
- rectRadius: 0.08,
519
- });
520
- slide.addText('点击上传图片', {
521
- x: 0.65, y: 2.6, w: 5.4, h: 0.4,
522
- fontSize: 14, color: COLORS.secondary, align: 'center', valign: 'middle', fontFace: FONTS.body,
523
- });
524
- }
525
- const annotation = props.annotation;
526
- if (annotation && (annotation.value || annotation.label)) {
527
- addTheme05Card(slide, 4.8, 3.8, 1.9, 1.0, true);
528
- if (annotation.value) {
529
- slide.addText(annotation.value, {
530
- x: 4.95, y: 3.9, w: 1.6, h: 0.4,
531
- fontSize: 24, color: COLORS.white, bold: true, align: 'center', valign: 'middle', fontFace: FONTS.heading,
532
- });
533
- }
534
- if (annotation.label) {
535
- slide.addText(annotation.label, {
536
- x: 4.95, y: 4.3, w: 1.6, h: 0.4,
537
- fontSize: 10, color: 'FFFFFF', align: 'center', valign: 'middle', fontFace: FONTS.body,
538
- });
539
- }
540
- }
541
- if (props.kicker) {
542
- slide.addText(props.kicker, {
543
- x: 6.35, y: 0.78, w: 3.0, h: 0.25,
544
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
545
- });
546
- }
547
- slide.addText(props.title ?? '', {
548
- x: 6.35, y: 1.2, w: 3.0, h: 1.0,
549
- fontSize: 30, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
550
- });
551
- slide.addShape('rect', {
552
- x: 6.35, y: 2.25, w: 1.0, h: 0.05,
553
- fill: { color: COLORS.accent },
554
- });
555
- if (props.subtitle) {
556
- slide.addText(props.subtitle, {
557
- x: 6.35, y: 2.4, w: 3.0, h: 0.9,
558
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
559
- });
560
- }
561
- addTheme05SpectrumBar(slide);
562
- }
563
- function renderTheme05VersusV1(slide, props) {
564
- const schemeColors = {
565
- coral: 'E85D4E',
566
- amber: 'F5A623',
567
- teal: '0FA3B1',
568
- indigo: '4A58D9',
569
- violet: '7C3AED',
570
- };
571
- if (props.kicker) {
572
- slide.addText(props.kicker, {
573
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
574
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
575
- });
576
- }
577
- slide.addText(props.title ?? '', {
578
- x: 0.65, y: 1.15, w: 8.7, h: 0.7,
579
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
580
- });
581
- if (props.subtitle) {
582
- slide.addText(props.subtitle, {
583
- x: 0.65, y: 1.85, w: 8.7, h: 0.3,
584
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
585
- });
586
- }
587
- const cardW = 3.7;
588
- const cardH = 2.4;
589
- const cardY = 2.4;
590
- const badgeW = 0.9;
591
- const gap = 0.25;
592
- const leftX = (10 - cardW * 2 - badgeW - gap * 2) / 2;
593
- const rightX = leftX + cardW + gap + badgeW + gap;
594
- const badgeX = leftX + cardW + gap;
595
- const sides = [
596
- { key: 'left', x: leftX },
597
- { key: 'right', x: rightX },
598
- ];
599
- sides.forEach(({ key, x }) => {
600
- const side = props[key] || {};
601
- const scheme = schemeColors[side.scheme] || COLORS.accent;
602
- slide.addShape('roundRect', {
603
- x, y: cardY, w: cardW, h: cardH,
604
- fill: { color: COLORS.surfaceElevated },
605
- line: { color: scheme, width: 2 },
606
- rectRadius: 0.08,
607
- });
608
- slide.addText(side.label ?? '', {
609
- x: x + 0.25, y: cardY + 0.25, w: cardW - 0.5, h: 0.35,
610
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
611
- });
612
- slide.addText(side.value ?? '', {
613
- x: x + 0.25, y: cardY + 0.75, w: cardW - 0.5, h: 0.9,
614
- fontSize: 52, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
615
- });
616
- if (side.unit) {
617
- slide.addText(side.unit, {
618
- x: x + 0.25, y: cardY + 1.55, w: cardW - 0.5, h: 0.3,
619
- fontSize: 16, color: COLORS.secondary, valign: 'top', fontFace: FONTS.heading,
620
- });
621
- }
622
- });
623
- slide.addShape('ellipse', {
624
- x: badgeX, y: cardY + (cardH - badgeW) / 2, w: badgeW, h: badgeW,
625
- fill: { color: COLORS.light },
626
- line: { color: COLORS.border, width: 1 },
627
- });
628
- slide.addText('VS', {
629
- x: badgeX, y: cardY + (cardH - badgeW) / 2, w: badgeW, h: badgeW,
630
- fontSize: 20, color: COLORS.secondary, bold: true, align: 'center', valign: 'middle', fontFace: FONTS.mono,
631
- });
632
- if (props.footnote) {
633
- slide.addText(props.footnote, {
634
- x: 0.65, y: 5.0, w: 8.7, h: 0.25,
635
- fontSize: 10, color: COLORS.secondary, align: 'center', fontFace: FONTS.body,
636
- });
637
- }
638
- addTheme05SpectrumBar(slide);
639
- }
640
- function renderTheme05ProcessV1(slide, props) {
641
- if (props.tag || props.tagLabel) {
642
- const tagText = [props.tag, props.tagLabel].filter(Boolean).join(' · ');
643
- slide.addText(tagText, {
644
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
645
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
646
- });
647
- }
648
- if (props.topRightMeta) {
649
- slide.addText(props.topRightMeta, {
650
- x: 5.5, y: 0.78, w: 3.85, h: 0.25,
651
- fontSize: 10, color: COLORS.secondary, align: 'right', fontFace: FONTS.mono,
652
- });
653
- }
654
- slide.addText(props.title ?? '', {
655
- x: 0.65, y: 1.15, w: 8.7, h: 0.7,
656
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
657
- });
658
- if (props.subtitle) {
659
- slide.addText(props.subtitle, {
660
- x: 0.65, y: 1.85, w: 8.7, h: 0.3,
661
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
662
- });
663
- }
664
- const steps = (props.steps || []).filter((s) => s != null).slice(0, 6);
665
- if (steps.length > 0) {
666
- const cardW = Math.min(2.0, (8.7 - (steps.length - 1) * 0.18) / steps.length);
667
- const gap = 0.18;
668
- const totalW = steps.length * cardW + (steps.length - 1) * gap;
669
- const startX = (10 - totalW) / 2;
670
- const cardY = 2.55;
671
- const cardH = 2.2;
672
- steps.forEach((step, idx) => {
673
- const x = startX + idx * (cardW + gap);
674
- addTheme05Card(slide, x, cardY, cardW, cardH);
675
- slide.addShape('ellipse', {
676
- x: x + 0.15, y: cardY + 0.18, w: 0.42, h: 0.42,
677
- fill: { color: COLORS.accent },
678
- });
679
- slide.addText(String(idx + 1).padStart(2, '0'), {
680
- x: x + 0.15, y: cardY + 0.18, w: 0.42, h: 0.42,
681
- fontSize: 12, color: COLORS.white, bold: true, align: 'center', valign: 'middle', fontFace: FONTS.mono,
682
- });
683
- slide.addText(step.title ?? '', {
684
- x: x + 0.65, y: cardY + 0.2, w: cardW - 0.8, h: 0.45,
685
- fontSize: 14, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
686
- });
687
- if (step.description) {
688
- slide.addText(step.description, {
689
- x: x + 0.15, y: cardY + 0.75, w: cardW - 0.3, h: cardH - 0.95,
690
- fontSize: 10, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
691
- });
692
- }
693
- if (idx < steps.length - 1) {
694
- const arrowX = x + cardW + 0.02;
695
- const arrowY = cardY + cardH / 2;
696
- slide.addShape('rect', {
697
- x: arrowX, y: arrowY - 0.01, w: gap - 0.04, h: 0.02,
698
- fill: { color: COLORS.border },
699
- });
700
- slide.addShape('triangle', {
701
- x: arrowX + gap - 0.1, y: arrowY - 0.05, w: 0.08, h: 0.1,
702
- fill: { color: COLORS.border },
703
- });
704
- }
705
- });
706
- }
707
- const footerText = [props.footnoteLeft, props.footnoteRight].filter(Boolean).join(' ');
708
- if (footerText) {
709
- slide.addText(footerText, {
710
- x: 0.65, y: 5.32, w: 8.7, h: 0.2,
711
- fontSize: 9, color: COLORS.secondary, align: 'center', fontFace: FONTS.mono,
712
- });
713
- }
714
- addTheme05SpectrumBar(slide);
715
- }
716
- function renderTheme05TimelineV1(slide, props) {
717
- const schemeColors = {
718
- coral: 'E85D4E',
719
- amber: 'F5A623',
720
- teal: '0FA3B1',
721
- indigo: '4A58D9',
722
- violet: '7C3AED',
723
- };
724
- if (props.kicker) {
725
- slide.addText(props.kicker, {
726
- x: 0.65, y: 0.78, w: 5.0, h: 0.25,
727
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
728
- });
729
- }
730
- slide.addText(props.title ?? '', {
731
- x: 0.65, y: 1.15, w: 8.7, h: 0.7,
732
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
733
- });
734
- if (props.subtitle) {
735
- slide.addText(props.subtitle, {
736
- x: 0.65, y: 1.85, w: 8.7, h: 0.3,
737
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
738
- });
739
- }
740
- const phases = (props.phases || []).slice(0, 3);
741
- if (phases.length > 0) {
742
- const cardW = 2.8;
743
- const gap = 0.2;
744
- const totalW = phases.length * cardW + (phases.length - 1) * gap;
745
- const startX = (10 - totalW) / 2;
746
- const trackY = 2.55;
747
- const cardY = 3.15;
748
- const cardH = 2.15;
749
- const dotY = trackY + 0.05;
750
- const lineY = trackY + 0.24;
751
- // timeline line
752
- slide.addShape('rect', {
753
- x: startX + 0.4, y: lineY, w: totalW - 0.8, h: 0.02,
754
- fill: { color: COLORS.border },
755
- });
756
- phases.forEach((phase, idx) => {
757
- const x = startX + idx * (cardW + gap);
758
- const scheme = schemeColors[phase.scheme] || COLORS.accent;
759
- // node dot
760
- slide.addShape('ellipse', {
761
- x: x + cardW / 2 - 0.18, y: dotY, w: 0.36, h: 0.36,
762
- fill: { color: scheme },
763
- });
764
- slide.addText(String(idx + 1), {
765
- x: x + cardW / 2 - 0.18, y: dotY, w: 0.36, h: 0.36,
766
- fontSize: 12, color: COLORS.white, bold: true, align: 'center', valign: 'middle', fontFace: FONTS.mono,
767
- });
768
- // period
769
- slide.addText(phase.period ?? '', {
770
- x, y: trackY + 0.5, w: cardW, h: 0.22,
771
- fontSize: 10, color: COLORS.secondary, align: 'center', fontFace: FONTS.mono,
772
- });
773
- // card
774
- addTheme05Card(slide, x, cardY, cardW, cardH);
775
- slide.addText(String(idx + 1).padStart(2, '0'), {
776
- x: x + 0.12, y: cardY + 0.12, w: 0.5, h: 0.35,
777
- fontSize: 18, color: COLORS.secondary, bold: true, valign: 'top', fontFace: FONTS.mono,
778
- });
779
- if (phase.badge) {
780
- slide.addText(phase.badge, {
781
- x: x + 0.55, y: cardY + 0.14, w: cardW - 0.7, h: 0.22,
782
- fontSize: 9, color: scheme, fontFace: FONTS.mono, bold: true,
783
- });
784
- }
785
- slide.addText(phase.title ?? '', {
786
- x: x + 0.12, y: cardY + 0.55, w: cardW - 0.24, h: 0.4,
787
- fontSize: 14, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
788
- });
789
- if (phase.description) {
790
- slide.addText(phase.description, {
791
- x: x + 0.12, y: cardY + 0.95, w: cardW - 0.24, h: cardH - 1.05,
792
- fontSize: 9, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
793
- });
794
- }
795
- });
796
- }
797
- if (props.footnote) {
798
- slide.addText(props.footnote, {
799
- x: 0.65, y: 5.32, w: 8.7, h: 0.2,
800
- fontSize: 9, color: COLORS.secondary, align: 'center', fontFace: FONTS.mono,
801
- });
802
- }
803
- addTheme05SpectrumBar(slide);
804
- }
805
- function renderTheme05MatrixV1(slide, props) {
806
- const schemeColors = {
807
- coral: 'E85D4E',
808
- amber: 'F5A623',
809
- teal: '0FA3B1',
810
- indigo: '4A58D9',
811
- violet: '7C3AED',
812
- };
813
- if (props.kicker) {
814
- slide.addText(props.kicker, {
815
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
816
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
817
- });
818
- }
819
- slide.addText(props.title ?? '', {
820
- x: 0.65, y: 1.15, w: 8.7, h: 0.7,
821
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
822
- });
823
- if (props.subtitle) {
824
- slide.addText(props.subtitle, {
825
- x: 0.65, y: 1.85, w: 8.7, h: 0.3,
826
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
827
- });
828
- }
829
- const gridX = 1.1;
830
- const gridY = 2.45;
831
- const gridW = 7.4;
832
- const gridH = 2.65;
833
- // y-axis
834
- const yAxis = props.yAxis || { low: '低', high: '高' };
835
- slide.addText(yAxis.high ?? '高', {
836
- x: 0.35, y: gridY, w: 0.5, h: 0.25,
837
- fontSize: 10, color: COLORS.secondary, align: 'center', valign: 'top', fontFace: FONTS.body,
838
- });
839
- slide.addShape('rect', {
840
- x: 0.57, y: gridY + 0.3, w: 0.02, h: gridH - 0.55,
841
- fill: { color: COLORS.border },
842
- });
843
- slide.addText(yAxis.low ?? '低', {
844
- x: 0.35, y: gridY + gridH - 0.5, w: 0.5, h: 0.25,
845
- fontSize: 10, color: COLORS.secondary, align: 'center', valign: 'bottom', fontFace: FONTS.body,
846
- });
847
- const items = (props.items || []).slice(0, 4);
848
- if (items.length > 0) {
849
- const cols = 2;
850
- const rows = 2;
851
- const cardGap = 0.14;
852
- const cardW = (gridW - cardGap) / cols;
853
- const cardH = (gridH - cardGap) / rows;
854
- items.forEach((item, idx) => {
855
- const col = idx % cols;
856
- const row = Math.floor(idx / cols);
857
- const x = gridX + col * (cardW + cardGap);
858
- const y = gridY + row * (cardH + cardGap);
859
- const scheme = schemeColors[item.scheme] || COLORS.accent;
860
- addTheme05Card(slide, x, y, cardW, cardH);
861
- slide.addShape('rect', {
862
- x, y, w: cardW, h: 0.04,
863
- fill: { color: scheme },
864
- });
865
- slide.addText(item.title ?? '', {
866
- x: x + 0.12, y: y + 0.18, w: cardW - 0.24, h: 0.35,
867
- fontSize: 14, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
868
- });
869
- if (item.description) {
870
- slide.addText(item.description, {
871
- x: x + 0.12, y: y + 0.55, w: cardW - 0.24, h: cardH - 0.65,
872
- fontSize: 9, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
873
- });
874
- }
875
- });
876
- }
877
- // x-axis
878
- const xAxis = props.xAxis || { low: '低', high: '高' };
879
- slide.addText(xAxis.low ?? '低', {
880
- x: gridX, y: gridY + gridH + 0.12, w: 0.5, h: 0.22,
881
- fontSize: 10, color: COLORS.secondary, align: 'left', fontFace: FONTS.body,
882
- });
883
- slide.addShape('rect', {
884
- x: gridX + 0.55, y: gridY + gridH + 0.22, w: gridW - 1.1, h: 0.02,
885
- fill: { color: COLORS.border },
886
- });
887
- slide.addText(xAxis.high ?? '高', {
888
- x: gridX + gridW - 0.5, y: gridY + gridH + 0.12, w: 0.5, h: 0.22,
889
- fontSize: 10, color: COLORS.secondary, align: 'right', fontFace: FONTS.body,
890
- });
891
- if (props.footnote) {
892
- slide.addText(props.footnote, {
893
- x: 0.65, y: 5.32, w: 8.7, h: 0.2,
894
- fontSize: 9, color: COLORS.secondary, align: 'center', fontFace: FONTS.mono,
895
- });
896
- }
897
- addTheme05SpectrumBar(slide);
898
- }
899
- function renderTheme05QuadrantV1(slide, props) {
900
- const schemeColors = {
901
- coral: 'E85D4E',
902
- amber: 'F5A623',
903
- teal: '0FA3B1',
904
- indigo: '4A58D9',
905
- violet: '7C3AED',
906
- };
907
- if (props.kicker) {
908
- slide.addText(props.kicker, {
909
- x: 0.65, y: 0.78, w: 5.0, h: 0.25,
910
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
911
- });
912
- }
913
- slide.addText(props.title ?? '', {
914
- x: 0.65, y: 1.15, w: 8.7, h: 0.7,
915
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
916
- });
917
- if (props.subtitle) {
918
- slide.addText(props.subtitle, {
919
- x: 0.65, y: 1.85, w: 8.7, h: 0.3,
920
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
921
- });
922
- }
923
- const gridX = 1.1;
924
- const gridY = 2.45;
925
- const gridW = 7.4;
926
- const gridH = 2.65;
927
- // y-axis
928
- const yAxisLabel = props.yAxisLabel ?? '资本热度';
929
- const yLabels = props.yAxisLabels ?? ['低', '高'];
930
- slide.addText(yAxisLabel, {
931
- x: 0.05, y: gridY + gridH / 2 - 0.5, w: 0.9, h: 1.0,
932
- fontSize: 10, color: COLORS.accent, align: 'center', valign: 'middle', fontFace: FONTS.mono, bold: true,
933
- });
934
- slide.addShape('rect', {
935
- x: 0.57, y: gridY + 0.15, w: 0.02, h: gridH - 0.3,
936
- fill: { color: COLORS.border },
937
- });
938
- slide.addText(yLabels[1] ?? '高', {
939
- x: 0.35, y: gridY, w: 0.5, h: 0.25,
940
- fontSize: 10, color: COLORS.secondary, align: 'center', valign: 'top', fontFace: FONTS.body,
941
- });
942
- slide.addText(yLabels[0] ?? '低', {
943
- x: 0.35, y: gridY + gridH - 0.25, w: 0.5, h: 0.25,
944
- fontSize: 10, color: COLORS.secondary, align: 'center', valign: 'bottom', fontFace: FONTS.body,
945
- });
946
- const quadrants = (props.quadrants || []).slice(0, 4);
947
- if (quadrants.length > 0) {
948
- const cols = 2;
949
- const rows = 2;
950
- const cardGap = 0.14;
951
- const cardW = (gridW - cardGap) / cols;
952
- const cardH = (gridH - cardGap) / rows;
953
- quadrants.forEach((q, idx) => {
954
- const col = idx % cols;
955
- const row = Math.floor(idx / cols);
956
- const x = gridX + col * (cardW + cardGap);
957
- const y = gridY + row * (cardH + cardGap);
958
- const scheme = schemeColors[q.scheme] || COLORS.accent;
959
- addTheme05Card(slide, x, y, cardW, cardH);
960
- slide.addShape('rect', {
961
- x, y, w: cardW, h: 0.04,
962
- fill: { color: scheme },
963
- });
964
- slide.addShape('ellipse', {
965
- x: x + 0.12, y: y + 0.16, w: 0.1, h: 0.1,
966
- fill: { color: scheme },
967
- });
968
- slide.addText(q.title ?? '', {
969
- x: x + 0.28, y: y + 0.12, w: cardW - 0.8, h: 0.35,
970
- fontSize: 14, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
971
- });
972
- slide.addText(String(idx + 1).padStart(2, '0'), {
973
- x: x + cardW - 0.45, y: y + 0.12, w: 0.35, h: 0.3,
974
- fontSize: 14, color: COLORS.secondary, bold: true, align: 'right', fontFace: FONTS.mono,
975
- });
976
- if (q.description) {
977
- slide.addText(q.description, {
978
- x: x + 0.12, y: y + 0.5, w: cardW - 0.24, h: cardH - 0.95,
979
- fontSize: 9, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
980
- });
981
- }
982
- const tags = (q.tags || []).slice(0, 5);
983
- if (tags.length > 0) {
984
- slide.addText(tags.join(' · '), {
985
- x: x + 0.12, y: y + cardH - 0.42, w: cardW - 0.24, h: 0.3,
986
- fontSize: 8, color: COLORS.secondary, valign: 'bottom', fontFace: FONTS.mono,
987
- });
988
- }
989
- });
990
- }
991
- // x-axis
992
- const xAxisLabel = props.xAxisLabel ?? '商业兑现度';
993
- const xLabels = props.xAxisLabels ?? ['低 / 待验证', '高'];
994
- slide.addShape('rect', {
995
- x: gridX + 0.2, y: gridY + gridH + 0.2, w: gridW - 0.4, h: 0.02,
996
- fill: { color: COLORS.border },
997
- });
998
- slide.addText(xLabels[0] ?? '低', {
999
- x: gridX, y: gridY + gridH + 0.28, w: 0.8, h: 0.22,
1000
- fontSize: 10, color: COLORS.secondary, align: 'left', fontFace: FONTS.body,
1001
- });
1002
- slide.addText(xLabels[1] ?? '高', {
1003
- x: gridX + gridW - 0.8, y: gridY + gridH + 0.28, w: 0.8, h: 0.22,
1004
- fontSize: 10, color: COLORS.secondary, align: 'right', fontFace: FONTS.body,
1005
- });
1006
- slide.addText(xAxisLabel, {
1007
- x: gridX + gridW / 2 - 1.0, y: gridY + gridH + 0.28, w: 2.0, h: 0.22,
1008
- fontSize: 10, color: COLORS.accent, align: 'center', fontFace: FONTS.mono, bold: true,
1009
- });
1010
- if (props.footnote) {
1011
- slide.addText(props.footnote, {
1012
- x: 0.65, y: 5.32, w: 8.7, h: 0.2,
1013
- fontSize: 9, color: COLORS.secondary, align: 'center', fontFace: FONTS.mono,
1014
- });
1015
- }
1016
- addTheme05SpectrumBar(slide);
1017
- }
1018
- function renderTheme05RiskV1(slide, props) {
1019
- const schemeColors = {
1020
- coral: 'E85D4E',
1021
- amber: 'F5A623',
1022
- teal: '0FA3B1',
1023
- indigo: '4A58D9',
1024
- violet: '7C3AED',
1025
- };
1026
- if (props.kicker) {
1027
- slide.addText(props.kicker, {
1028
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
1029
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
1030
- });
1031
- }
1032
- slide.addText(props.title ?? '关键风险与应对', {
1033
- x: 0.65, y: 1.15, w: 8.7, h: 0.7,
1034
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
1035
- });
1036
- if (props.subtitle) {
1037
- slide.addText(props.subtitle, {
1038
- x: 0.65, y: 1.85, w: 8.7, h: 0.3,
1039
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
1040
- });
1041
- }
1042
- const items = (props.items || []).slice(0, 4);
1043
- if (items.length > 0) {
1044
- const cols = 2;
1045
- const rows = 2;
1046
- const cardGap = 0.16;
1047
- const cardW = (8.7 - cardGap) / cols;
1048
- const cardH = (2.85 - cardGap) / rows;
1049
- const startX = 0.65;
1050
- const startY = 2.4;
1051
- items.forEach((item, idx) => {
1052
- const col = idx % cols;
1053
- const row = Math.floor(idx / cols);
1054
- const x = startX + col * (cardW + cardGap);
1055
- const y = startY + row * (cardH + cardGap);
1056
- const scheme = schemeColors[item.scheme] || COLORS.accent;
1057
- addTheme05Card(slide, x, y, cardW, cardH);
1058
- slide.addShape('rect', {
1059
- x, y, w: cardW, h: 0.04,
1060
- fill: { color: scheme },
1061
- });
1062
- slide.addText(item.risk ?? '', {
1063
- x: x + 0.12, y: y + 0.14, w: cardW - 0.6, h: 0.4,
1064
- fontSize: 15, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
1065
- });
1066
- slide.addText(String(idx + 1).padStart(2, '0'), {
1067
- x: x + cardW - 0.45, y: y + 0.14, w: 0.35, h: 0.3,
1068
- fontSize: 14, color: COLORS.secondary, bold: true, align: 'right', fontFace: FONTS.mono,
1069
- });
1070
- if (item.impact) {
1071
- slide.addText('影响程度', {
1072
- x: x + 0.12, y: y + 0.62, w: 0.9, h: 0.2,
1073
- fontSize: 8, color: COLORS.secondary, fontFace: FONTS.mono, bold: true,
1074
- });
1075
- slide.addText(item.impact, {
1076
- x: x + 1.05, y: y + 0.62, w: cardW - 1.2, h: 0.22,
1077
- fontSize: 11, color: scheme, bold: true, valign: 'top', fontFace: FONTS.body,
1078
- });
1079
- }
1080
- if (item.response) {
1081
- slide.addText('应对策略', {
1082
- x: x + 0.12, y: y + 0.92, w: 0.9, h: 0.2,
1083
- fontSize: 8, color: COLORS.secondary, fontFace: FONTS.mono, bold: true,
1084
- });
1085
- slide.addText(item.response, {
1086
- x: x + 1.05, y: y + 0.92, w: cardW - 1.2, h: cardH - 1.05,
1087
- fontSize: 9, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
1088
- });
1089
- }
1090
- });
1091
- }
1092
- if (props.footnote) {
1093
- slide.addText(props.footnote, {
1094
- x: 0.65, y: 5.32, w: 8.7, h: 0.2,
1095
- fontSize: 9, color: COLORS.secondary, align: 'center', fontFace: FONTS.mono,
1096
- });
1097
- }
1098
- addTheme05SpectrumBar(slide);
1099
- }
1100
- function renderTheme05BubbleV1(slide, props) {
1101
- const schemeColors = {
1102
- coral: 'E85D4E',
1103
- amber: 'F5A623',
1104
- teal: '0FA3B1',
1105
- indigo: '4A58D9',
1106
- violet: '7C3AED',
1107
- };
1108
- if (props.kicker) {
1109
- slide.addText(props.kicker, {
1110
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
1111
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
1112
- });
1113
- }
1114
- slide.addText(props.title ?? '', {
1115
- x: 0.65, y: 1.15, w: 8.7, h: 0.7,
1116
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
1117
- });
1118
- if (props.subtitle) {
1119
- slide.addText(props.subtitle, {
1120
- x: 0.65, y: 1.85, w: 8.7, h: 0.3,
1121
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
1122
- });
1123
- }
1124
- const items = (props.items || []).slice(0, 16);
1125
- if (items.length === 0) {
1126
- addTheme05SpectrumBar(slide);
1127
- return;
1128
- }
1129
- const chartX = 0.65;
1130
- const chartY = 2.45;
1131
- const chartW = 8.7;
1132
- const chartH = 2.85;
1133
- addTheme05Card(slide, chartX, chartY, chartW, chartH);
1134
- const plotMargin = { top: 0.3, right: 0.3, bottom: 0.5, left: 0.7 };
1135
- const plotX = chartX + plotMargin.left;
1136
- const plotY = chartY + plotMargin.top;
1137
- const plotW = chartW - plotMargin.left - plotMargin.right;
1138
- const plotH = chartH - plotMargin.top - plotMargin.bottom;
1139
- const xs = items.map((item) => Number(item.x) || 0);
1140
- const ys = items.map((item) => Number(item.y) || 0);
1141
- const maxX = Math.max(1, ...xs);
1142
- const maxY = Math.max(1, ...ys);
1143
- for (let i = 0; i <= 4; i++) {
1144
- const x = plotX + (i / 4) * plotW;
1145
- const y = plotY + (i / 4) * plotH;
1146
- slide.addShape('line', {
1147
- x1: x, y1: plotY, x2: x, y2: plotY + plotH,
1148
- line: { color: COLORS.border, width: 0.5, dash: 'dash' },
1149
- });
1150
- slide.addShape('line', {
1151
- x1: plotX, y1: y, x2: plotX + plotW, y2: y,
1152
- line: { color: COLORS.border, width: 0.5, dash: 'dash' },
1153
- });
1154
- }
1155
- slide.addText(props.xAxisLabel || '', {
1156
- x: plotX + plotW / 2 - 1, y: chartY + chartH - 0.38, w: 2, h: 0.2,
1157
- fontSize: 9, color: COLORS.secondary, align: 'center', fontFace: FONTS.body,
1158
- });
1159
- slide.addText(props.yAxisLabel || '', {
1160
- x: chartX + 0.05, y: plotY + plotH / 2 - 0.25, w: 0.5, h: 0.5,
1161
- fontSize: 9, color: COLORS.secondary, align: 'center', valign: 'middle', fontFace: FONTS.body,
1162
- });
1163
- const maxValue = Math.max(1, ...items.map((item) => Number(item.value) || 0));
1164
- items.forEach((item) => {
1165
- const x = plotX + ((Number(item.x) || 0) / maxX) * plotW;
1166
- const y = plotY + plotH - ((Number(item.y) || 0) / maxY) * plotH;
1167
- const scheme = item.scheme || 'coral';
1168
- const color = schemeColors[scheme] ?? COLORS.accent;
1169
- const size = Math.max(0.12, Math.min(0.34, ((Number(item.value) || 0) / maxValue) * 0.28 + 0.1));
1170
- slide.addShape('ellipse', {
1171
- x: x - size / 2, y: y - size / 2, w: size, h: size,
1172
- fill: { color },
1173
- });
1174
- slide.addText(item.name ?? '', {
1175
- x: x - 0.6, y: y - size / 2 - 0.22, w: 1.2, h: 0.18,
1176
- fontSize: 8, color: COLORS.primary, bold: true, align: 'center', fontFace: FONTS.body,
1177
- });
1178
- });
1179
- addTheme05SpectrumBar(slide);
1180
- }
1181
- function renderTheme05MapV1(slide, props) {
1182
- const schemeColors = {
1183
- coral: 'E85D4E',
1184
- amber: 'F5A623',
1185
- teal: '0FA3B1',
1186
- indigo: '4A58D9',
1187
- violet: '7C3AED',
1188
- };
1189
- if (props.kicker) {
1190
- slide.addText(props.kicker, {
1191
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
1192
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
1193
- });
1194
- }
1195
- slide.addText(props.title ?? '', {
1196
- x: 0.65, y: 1.15, w: 8.7, h: 0.7,
1197
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
1198
- });
1199
- if (props.subtitle) {
1200
- slide.addText(props.subtitle, {
1201
- x: 0.65, y: 1.85, w: 8.7, h: 0.3,
1202
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
1203
- });
1204
- }
1205
- const items = (props.items || []).slice(0, 10);
1206
- if (items.length === 0) {
1207
- addTheme05SpectrumBar(slide);
1208
- return;
1209
- }
1210
- const chartX = 0.65;
1211
- const chartY = 2.45;
1212
- const chartW = 8.7;
1213
- const chartH = 2.85;
1214
- addTheme05Card(slide, chartX, chartY, chartW, chartH);
1215
- const sorted = [...items].sort((a, b) => (Number(b.value) || 0) - (Number(a.value) || 0));
1216
- const maxValue = Math.max(1, ...sorted.map((item) => Number(item.value) || 0));
1217
- const rowH = chartH / sorted.length;
1218
- const labelW = 1.4;
1219
- const plotX = chartX + labelW + 0.2;
1220
- const plotW = chartW - labelW - 1.2;
1221
- sorted.forEach((item, idx) => {
1222
- const y = chartY + idx * rowH;
1223
- const value = Number(item.value) || 0;
1224
- const barW = (value / maxValue) * plotW;
1225
- const scheme = item.scheme || 'coral';
1226
- const color = schemeColors[scheme] ?? COLORS.accent;
1227
- slide.addText(item.name ?? '', {
1228
- x: chartX + 0.15, y, w: labelW, h: rowH,
1229
- fontSize: 11, color: COLORS.primary, bold: true, valign: 'middle', fontFace: FONTS.body,
1230
- });
1231
- slide.addShape('roundRect', {
1232
- x: plotX, y: y + rowH * 0.22, w: Math.max(0.04, barW), h: rowH * 0.56,
1233
- fill: { color }, rectRadius: 0.04,
1234
- });
1235
- slide.addText(`${value} ${props.unit ?? ''}`, {
1236
- x: plotX + barW + 0.08, y, w: 1.0, h: rowH,
1237
- fontSize: 10, color: COLORS.secondary, bold: true, valign: 'middle', fontFace: FONTS.body,
1238
- });
1239
- });
1240
- addTheme05SpectrumBar(slide);
1241
- }
1242
- function renderTheme05TableOfContentsV1(slide, props) {
1243
- if (props.subtitle) {
1244
- slide.addText(props.subtitle, {
1245
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
1246
- fontSize: 11, color: COLORS.secondary, fontFace: FONTS.mono,
1247
- });
1248
- }
1249
- slide.addText(props.title ?? '', {
1250
- x: 0.65, y: 1.2, w: 4.2, h: 0.8,
1251
- fontSize: 42, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
1252
- });
1253
- slide.addShape('rect', {
1254
- x: 0.65, y: 2.05, w: 1.4, h: 0.05,
1255
- fill: { color: COLORS.accent },
1256
- });
1257
- const items = (props.items || []).slice(0, 6);
1258
- if (items.length > 0) {
1259
- const cols = Math.min(items.length, 2);
1260
- const cardW = 4.0;
1261
- const cardH = 1.05;
1262
- const gapX = 0.2;
1263
- const gapY = 0.18;
1264
- const startX = 5.05;
1265
- const startY = 0.85;
1266
- items.forEach((item, idx) => {
1267
- const col = idx % cols;
1268
- const row = Math.floor(idx / cols);
1269
- const x = startX + col * (cardW + gapX);
1270
- const y = startY + row * (cardH + gapY);
1271
- addTheme05Card(slide, x, y, cardW, cardH);
1272
- slide.addText(String(idx + 1).padStart(2, '0'), {
1273
- x: x + 0.15, y: y + 0.18, w: 0.6, h: 0.6,
1274
- fontSize: 28, color: COLORS.accent, bold: true, valign: 'top', fontFace: FONTS.heading,
1275
- });
1276
- slide.addText(item.title ?? '', {
1277
- x: x + 0.85, y: y + 0.2, w: cardW - 1.1, h: 0.35,
1278
- fontSize: 15, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.body,
1279
- });
1280
- if (item.page) {
1281
- slide.addText(`P.${item.page}`, {
1282
- x: x + 0.85, y: y + 0.55, w: cardW - 1.1, h: 0.25,
1283
- fontSize: 11, color: COLORS.secondary, valign: 'top', fontFace: FONTS.mono,
1284
- });
1285
- }
1286
- });
1287
- }
1288
- addTheme05SpectrumBar(slide);
1289
- }
1290
- function renderTheme05ClosingV1(slide, props) {
1291
- if (props.kicker) {
1292
- slide.addText(props.kicker, {
1293
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
1294
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
1295
- });
1296
- }
1297
- slide.addText(props.claim ?? '', {
1298
- x: 0.65, y: 1.6, w: 8.7, h: 1.4,
1299
- fontSize: 42, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
1300
- });
1301
- const points = (props.points || []).slice(0, 4);
1302
- if (points.length > 0) {
1303
- const cardW = 2.05;
1304
- const gap = 0.18;
1305
- const startX = (10 - points.length * cardW - (points.length - 1) * gap) / 2;
1306
- points.forEach((p, idx) => {
1307
- const x = startX + idx * (cardW + gap);
1308
- const y = 3.4;
1309
- addTheme05Card(slide, x, y, cardW, 1.1, idx === 0);
1310
- slide.addText(p.value ?? '', {
1311
- x, y: y + 0.15, w: cardW, h: 0.45,
1312
- fontSize: 28, color: idx === 0 ? COLORS.white : COLORS.primary, bold: true, align: 'center', fontFace: FONTS.heading,
1313
- });
1314
- slide.addText(p.label ?? '', {
1315
- x, y: y + 0.62, w: cardW, h: 0.25,
1316
- fontSize: 10, color: idx === 0 ? 'FFFFFF' : COLORS.secondary, align: 'center', fontFace: FONTS.body,
1317
- });
1318
- });
1319
- }
1320
- addTheme05SpectrumBar(slide);
1321
- const footerText = [props.footnoteLeft, props.footnoteRight].filter(Boolean).join(' ');
1322
- if (footerText) {
1323
- slide.addText(footerText, {
1324
- x: 0.65, y: 5.32, w: 8.7, h: 0.2,
1325
- fontSize: 9, color: COLORS.secondary, align: 'center', fontFace: FONTS.mono,
1326
- });
1327
- }
1328
- }
1329
- function renderTheme05CoverExV1(slide, props) {
1330
- if (props.tag) {
1331
- slide.addText(props.tag, {
1332
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
1333
- fontSize: 12, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
1334
- });
1335
- }
1336
- slide.addText(props.title ?? '', {
1337
- x: 0.65, y: 1.2, w: 5.0, h: 1.6,
1338
- fontSize: 48, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
1339
- });
1340
- if (props.subtitle) {
1341
- slide.addText(props.subtitle, {
1342
- x: 0.65, y: 2.85, w: 5.0, h: 0.6,
1343
- fontSize: 16, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
1344
- });
1345
- }
1346
- addTheme05SpectrumBar(slide);
1347
- const stats = (props.stats || []).slice(0, 4);
1348
- const cardW = 2.15;
1349
- const cardH = 1.05;
1350
- const gapX = 0.18;
1351
- const gapY = 0.18;
1352
- const startX = 5.85;
1353
- const startY = 0.95;
1354
- stats.forEach((s, idx) => {
1355
- const col = idx % 2;
1356
- const row = Math.floor(idx / 2);
1357
- const x = startX + col * (cardW + gapX);
1358
- const y = startY + row * (cardH + gapY);
1359
- const color = theme05SchemeColor(s.scheme);
1360
- slide.addShape('roundRect', {
1361
- x, y, w: cardW, h: cardH,
1362
- fill: { color: COLORS.surfaceElevated },
1363
- line: { color: COLORS.border, width: 1 },
1364
- rectRadius: 0.08,
1365
- });
1366
- slide.addShape('rect', {
1367
- x, y, w: cardW, h: 0.06,
1368
- fill: { color },
1369
- });
1370
- slide.addText(`${s.value ?? ''}${s.unit ?? ''}`, {
1371
- x: x + 0.12, y: y + 0.18, w: cardW - 0.24, h: 0.42,
1372
- fontSize: 26, color: COLORS.primary, bold: true, fontFace: FONTS.heading,
1373
- });
1374
- slide.addText(s.label ?? '', {
1375
- x: x + 0.12, y: y + 0.62, w: cardW - 0.24, h: 0.28,
1376
- fontSize: 10, color: COLORS.secondary, fontFace: FONTS.body,
1377
- });
1378
- });
1379
- const footerText = [props.footnoteLeft, props.footnoteRight].filter(Boolean).join(' ');
1380
- if (footerText) {
1381
- slide.addText(footerText, {
1382
- x: 0.65, y: 5.32, w: 8.7, h: 0.2,
1383
- fontSize: 9, color: COLORS.secondary, align: 'center', fontFace: FONTS.mono,
1384
- });
1385
- }
1386
- }
1387
- function renderTheme05CoverExV2(slide, props) {
1388
- if (props.backgroundNumber) {
1389
- slide.addText(props.backgroundNumber, {
1390
- x: 4.5, y: 0.6, w: 5.5, h: 2.0,
1391
- fontSize: 160, color: COLORS.primary, bold: true, align: 'right', valign: 'top', fontFace: FONTS.heading,
1392
- transparency: 92,
1393
- });
1394
- }
1395
- if (props.tag) {
1396
- slide.addText(props.tag, {
1397
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
1398
- fontSize: 12, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
1399
- });
1400
- }
1401
- const highlights = (props.highlights || []).slice(0, 5);
1402
- if (highlights.length > 0) {
1403
- let pillX = 0.65;
1404
- const pillY = 1.2;
1405
- highlights.forEach((h) => {
1406
- const text = typeof h === 'string' ? h : h.text ?? '';
1407
- if (!text)
1408
- return;
1409
- const color = theme05SchemeColor(typeof h === 'string' ? undefined : h.scheme);
1410
- const approxW = Math.min(2.2, 0.24 + text.length * 0.11);
1411
- addTheme05Pill(slide, pillX, pillY, approxW, 0.32, text, color);
1412
- pillX += approxW + 0.12;
1413
- });
1414
- }
1415
- slide.addText(props.title ?? '', {
1416
- x: 0.65, y: 3.1, w: 8.7, h: 1.2,
1417
- fontSize: 54, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
1418
- });
1419
- if (props.subtitle) {
1420
- slide.addText(props.subtitle, {
1421
- x: 0.65, y: 4.3, w: 8.7, h: 0.45,
1422
- fontSize: 16, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
1423
- });
1424
- }
1425
- addTheme05SpectrumBar(slide);
1426
- const footerText = [props.footnoteLeft, props.footnoteRight].filter(Boolean).join(' ');
1427
- if (footerText) {
1428
- slide.addText(footerText, {
1429
- x: 0.65, y: 5.32, w: 8.7, h: 0.2,
1430
- fontSize: 9, color: COLORS.secondary, align: 'center', fontFace: FONTS.mono,
1431
- });
1432
- }
1433
- }
1434
- function renderTheme05CoverHeroV1(slide, props) {
1435
- const imageUrl = props.image;
1436
- if (imageUrl) {
1437
- addImageMaybe(slide, imageUrl, 0, 0, 10, 5.625);
1438
- }
1439
- else {
1440
- slide.addShape('rect', {
1441
- x: 0, y: 0, w: 10, h: 5.625,
1442
- fill: { color: COLORS.light },
1443
- });
1444
- slide.addShape('rect', {
1445
- x: 4.2, y: 2.0, w: 1.6, h: 1.6,
1446
- fill: { color: COLORS.border },
1447
- line: { color: COLORS.secondary, width: 1, dashType: 'dash' },
1448
- });
1449
- slide.addText('[图片]', {
1450
- x: 4.2, y: 2.6, w: 1.6, h: 0.3,
1451
- fontSize: 12, color: COLORS.secondary, align: 'center', fontFace: FONTS.body,
1452
- });
1453
- }
1454
- // 左侧渐变遮罩:用半透明矩形从白到透明模拟
1455
- slide.addShape('rect', {
1456
- x: 0, y: 0, w: 5.2, h: 5.625,
1457
- fill: {
1458
- color: COLORS.white,
1459
- gradient: {
1460
- type: 'linear',
1461
- angle: 0,
1462
- stops: [
1463
- { position: 0, color: COLORS.white },
1464
- { position: 1, color: `${COLORS.white}00` },
1465
- ],
1466
- },
1467
- },
1468
- });
1469
- if (props.tag) {
1470
- slide.addText(props.tag, {
1471
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
1472
- fontSize: 12, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
1473
- });
1474
- }
1475
- slide.addText(props.title ?? '', {
1476
- x: 0.65, y: 3.0, w: 6.5, h: 1.2,
1477
- fontSize: 50, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
1478
- });
1479
- if (props.subtitle) {
1480
- slide.addText(props.subtitle, {
1481
- x: 0.65, y: 4.2, w: 6.5, h: 0.5,
1482
- fontSize: 16, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
1483
- });
1484
- }
1485
- addTheme05SpectrumBar(slide);
1486
- const footerText = [props.footnoteLeft, props.footnoteRight].filter(Boolean).join(' ');
1487
- if (footerText) {
1488
- slide.addText(footerText, {
1489
- x: 0.65, y: 5.32, w: 8.7, h: 0.2,
1490
- fontSize: 9, color: COLORS.secondary, align: 'center', fontFace: FONTS.mono,
1491
- });
1492
- }
1493
- }
1494
- function renderTheme05ChapterBigV1(slide, props) {
1495
- if (props.tag) {
1496
- slide.addText(props.tag, {
1497
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
1498
- fontSize: 12, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
1499
- });
1500
- }
1501
- slide.addText(props.title ?? '', {
1502
- x: 0.65, y: 1.8, w: 8.7, h: 1.5,
1503
- fontSize: 64, color: COLORS.primary, bold: true, align: 'center', valign: 'middle', fontFace: FONTS.heading,
1504
- });
1505
- if (props.subtitle) {
1506
- slide.addText(props.subtitle, {
1507
- x: 0.65, y: 3.4, w: 8.7, h: 0.6,
1508
- fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'top', fontFace: FONTS.body,
1509
- });
1510
- }
1511
- addTheme05SpectrumBar(slide);
1512
- }
1513
- function renderTheme05ChapterSplitV1(slide, props) {
1514
- slide.addShape('rect', {
1515
- x: 0, y: 0, w: 3.0, h: 5.625,
1516
- fill: { color: COLORS.accent },
1517
- });
1518
- slide.addText(props.number ?? '', {
1519
- x: 0, y: 1.9, w: 3.0, h: 1.4,
1520
- fontSize: 110, color: COLORS.white, bold: true, align: 'center', valign: 'middle', fontFace: FONTS.heading,
1521
- });
1522
- slide.addText(props.title ?? '', {
1523
- x: 3.4, y: 1.8, w: 6.2, h: 1.2,
1524
- fontSize: 48, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
1525
- });
1526
- if (props.subtitle) {
1527
- slide.addText(props.subtitle, {
1528
- x: 3.4, y: 3.05, w: 6.2, h: 0.6,
1529
- fontSize: 16, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
1530
- });
1531
- }
1532
- addTheme05SpectrumBar(slide);
1533
- }
1534
- function renderTheme05ChapterNumberedV1(slide, props) {
1535
- slide.addText(props.number ?? '', {
1536
- x: 0.65, y: 0.9, w: 8.7, h: 0.9,
1537
- fontSize: 80, color: COLORS.accent, bold: true, align: 'center', valign: 'top', fontFace: FONTS.heading,
1538
- });
1539
- slide.addText(props.title ?? '', {
1540
- x: 0.65, y: 2.0, w: 8.7, h: 1.2,
1541
- fontSize: 54, color: COLORS.primary, bold: true, align: 'center', valign: 'top', fontFace: FONTS.heading,
1542
- });
1543
- if (props.subtitle) {
1544
- slide.addText(props.subtitle, {
1545
- x: 0.65, y: 3.25, w: 8.7, h: 0.5,
1546
- fontSize: 16, color: COLORS.secondary, align: 'center', valign: 'top', fontFace: FONTS.body,
1547
- });
1548
- }
1549
- slide.addShape('rect', {
1550
- x: 4.5, y: 3.9, w: 1.0, h: 0.05,
1551
- fill: { color: COLORS.accent },
1552
- });
1553
- addTheme05SpectrumBar(slide);
1554
- }
1555
- function renderTheme05ChapterImageV1(slide, props) {
1556
- const imageUrl = props.image;
1557
- if (imageUrl) {
1558
- addImageMaybe(slide, imageUrl, 0, 0, 10, 5.625);
1559
- }
1560
- else {
1561
- slide.addShape('rect', {
1562
- x: 0, y: 0, w: 10, h: 5.625,
1563
- fill: { color: COLORS.light },
1564
- });
1565
- slide.addShape('rect', {
1566
- x: 4.2, y: 2.0, w: 1.6, h: 1.6,
1567
- fill: { color: COLORS.border },
1568
- line: { color: COLORS.secondary, width: 1, dashType: 'dash' },
1569
- });
1570
- slide.addText('[图片]', {
1571
- x: 4.2, y: 2.6, w: 1.6, h: 0.3,
1572
- fontSize: 12, color: COLORS.secondary, align: 'center', fontFace: FONTS.body,
1573
- });
1574
- }
1575
- // 底部渐变遮罩
1576
- slide.addShape('rect', {
1577
- x: 0, y: 2.8, w: 10, h: 2.825,
1578
- fill: {
1579
- color: COLORS.white,
1580
- gradient: {
1581
- type: 'linear',
1582
- angle: 270,
1583
- stops: [
1584
- { position: 0, color: `${COLORS.white}00` },
1585
- { position: 1, color: COLORS.white },
1586
- ],
1587
- },
1588
- },
1589
- });
1590
- slide.addText(props.number ?? '', {
1591
- x: 0.65, y: 3.2, w: 8.7, h: 0.6,
1592
- fontSize: 48, color: COLORS.accent, bold: true, fontFace: FONTS.heading,
1593
- });
1594
- slide.addText(props.title ?? '', {
1595
- x: 0.65, y: 3.85, w: 8.7, h: 0.9,
1596
- fontSize: 42, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
1597
- });
1598
- if (props.subtitle) {
1599
- slide.addText(props.subtitle, {
1600
- x: 0.65, y: 4.75, w: 8.7, h: 0.35,
1601
- fontSize: 14, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
1602
- });
1603
- }
1604
- addTheme05SpectrumBar(slide);
1605
- }
1606
- function renderTheme05MetricHeroV1(slide, props) {
1607
- if (props.kicker) {
1608
- slide.addText(props.kicker, {
1609
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
1610
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
1611
- });
1612
- }
1613
- slide.addText(props.title ?? '', {
1614
- x: 0.65, y: 1.15, w: 8.7, h: 0.8,
1615
- fontSize: 38, color: COLORS.primary, bold: true, align: 'center', valign: 'top', fontFace: FONTS.heading,
1616
- });
1617
- if (props.subtitle) {
1618
- slide.addText(props.subtitle, {
1619
- x: 0.65, y: 1.95, w: 8.7, h: 0.35,
1620
- fontSize: 13, color: COLORS.secondary, align: 'center', valign: 'top', fontFace: FONTS.body,
1621
- });
1622
- }
1623
- slide.addText(props.value ?? '', {
1624
- x: 0.65, y: 2.6, w: 8.7, h: 1.2,
1625
- fontSize: 84, color: COLORS.accent, bold: true, align: 'center', valign: 'top', fontFace: FONTS.heading,
1626
- });
1627
- if (props.unit) {
1628
- slide.addText(props.unit, {
1629
- x: 0.65, y: 3.6, w: 8.7, h: 0.35,
1630
- fontSize: 22, color: COLORS.secondary, align: 'center', valign: 'top', fontFace: FONTS.heading,
1631
- });
1632
- }
1633
- if (props.change || props.changeLabel) {
1634
- const badgeW = 2.4;
1635
- const badgeX = (10 - badgeW) / 2;
1636
- slide.addShape('roundRect', {
1637
- x: badgeX, y: 4.05, w: badgeW, h: 0.42,
1638
- fill: { color: COLORS.accent },
1639
- rectRadius: 0.21,
1640
- });
1641
- const text = [props.change, props.changeLabel].filter(Boolean).join(' ');
1642
- slide.addText(text, {
1643
- x: badgeX, y: 4.07, w: badgeW, h: 0.38,
1644
- fontSize: 12, color: COLORS.white, bold: true, align: 'center', valign: 'middle', fontFace: FONTS.body,
1645
- });
1646
- }
1647
- addTheme05SpectrumBar(slide);
1648
- }
1649
- function renderTheme05MetricCapacityV1(slide, props) {
1650
- if (props.kicker) {
1651
- slide.addText(props.kicker, {
1652
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
1653
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
1654
- });
1655
- }
1656
- slide.addText(props.title ?? '', {
1657
- x: 0.65, y: 1.15, w: 8.7, h: 0.8,
1658
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
1659
- });
1660
- if (props.subtitle) {
1661
- slide.addText(props.subtitle, {
1662
- x: 0.65, y: 1.95, w: 8.7, h: 0.35,
1663
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
1664
- });
1665
- }
1666
- const items = (props.items || []).slice(0, 6);
1667
- const startY = 2.55;
1668
- const rowH = 0.55;
1669
- items.forEach((item, idx) => {
1670
- const y = startY + idx * rowH;
1671
- const name = item.name ?? '';
1672
- const value = Number(item.value) || 0;
1673
- const max = Math.max(Number(item.max) || 100, value);
1674
- const unit = item.unit ?? '';
1675
- const color = theme05SchemeColor(item.scheme);
1676
- slide.addText(name, {
1677
- x: 0.65, y, w: 2.0, h: 0.3,
1678
- fontSize: 12, color: COLORS.primary, bold: true, valign: 'middle', fontFace: FONTS.body,
1679
- });
1680
- slide.addText(`${value}${unit}`, {
1681
- x: 8.05, y, w: 1.3, h: 0.3,
1682
- fontSize: 12, color: COLORS.primary, bold: true, align: 'right', valign: 'middle', fontFace: FONTS.heading,
1683
- });
1684
- const trackX = 2.75;
1685
- const trackW = 5.15;
1686
- slide.addShape('roundRect', {
1687
- x: trackX, y: y + 0.08, w: trackW, h: 0.14,
1688
- fill: { color: COLORS.border },
1689
- rectRadius: 0.07,
1690
- });
1691
- const fillW = max > 0 ? (value / max) * trackW : 0;
1692
- if (fillW > 0) {
1693
- slide.addShape('roundRect', {
1694
- x: trackX, y: y + 0.08, w: fillW, h: 0.14,
1695
- fill: { color },
1696
- rectRadius: 0.07,
1697
- });
1698
- }
1699
- });
1700
- addTheme05SpectrumBar(slide);
1701
- }
1702
- function renderTheme05MetricDeltaV1(slide, props) {
1703
- if (props.kicker) {
1704
- slide.addText(props.kicker, {
1705
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
1706
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
1707
- });
1708
- }
1709
- slide.addText(props.title ?? '', {
1710
- x: 0.65, y: 1.15, w: 8.7, h: 0.7,
1711
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
1712
- });
1713
- if (props.subtitle) {
1714
- slide.addText(props.subtitle, {
1715
- x: 0.65, y: 1.85, w: 8.7, h: 0.35,
1716
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
1717
- });
1718
- }
1719
- const colW = 2.6;
1720
- const startX = (10 - colW * 3 - 0.6) / 2;
1721
- // 当前值
1722
- slide.addShape('roundRect', {
1723
- x: startX, y: 2.4, w: colW, h: 1.4,
1724
- fill: { color: COLORS.surfaceElevated },
1725
- line: { color: COLORS.border, width: 1 },
1726
- rectRadius: 0.1,
1727
- });
1728
- slide.addText(`${props.currentValue ?? ''}${props.currentUnit ?? ''}`, {
1729
- x: startX, y: 2.55, w: colW, h: 0.55,
1730
- fontSize: 34, color: COLORS.accent, bold: true, align: 'center', fontFace: FONTS.heading,
1731
- });
1732
- slide.addText(props.currentLabel ?? '当前值', {
1733
- x: startX, y: 3.15, w: colW, h: 0.25,
1734
- fontSize: 10, color: COLORS.secondary, align: 'center', fontFace: FONTS.body,
1735
- });
1736
- // Delta 徽章
1737
- const deltaX = startX + colW + 0.3;
1738
- slide.addShape('roundRect', {
1739
- x: deltaX, y: 2.65, w: colW, h: 0.9,
1740
- fill: { color: COLORS.accent },
1741
- rectRadius: 0.1,
1742
- });
1743
- slide.addText(props.delta ?? '', {
1744
- x: deltaX, y: 2.72, w: colW, h: 0.4,
1745
- fontSize: 28, color: COLORS.white, bold: true, align: 'center', fontFace: FONTS.heading,
1746
- });
1747
- slide.addText(props.deltaLabel ?? '变化', {
1748
- x: deltaX, y: 3.15, w: colW, h: 0.25,
1749
- fontSize: 10, color: COLORS.white, align: 'center', fontFace: FONTS.body,
1750
- });
1751
- // 对比值
1752
- const prevX = deltaX + colW + 0.3;
1753
- slide.addShape('roundRect', {
1754
- x: prevX, y: 2.4, w: colW, h: 1.4,
1755
- fill: { color: COLORS.surfaceElevated },
1756
- line: { color: COLORS.border, width: 1 },
1757
- rectRadius: 0.1,
1758
- });
1759
- slide.addText(`${props.previousValue ?? ''}${props.previousUnit ?? ''}`, {
1760
- x: prevX, y: 2.55, w: colW, h: 0.55,
1761
- fontSize: 34, color: COLORS.primary, bold: true, align: 'center', fontFace: FONTS.heading,
1762
- });
1763
- slide.addText(props.previousLabel ?? '对比值', {
1764
- x: prevX, y: 3.15, w: colW, h: 0.25,
1765
- fontSize: 10, color: COLORS.secondary, align: 'center', fontFace: FONTS.body,
1766
- });
1767
- // 迷你折线
1768
- const labels = (props.labels || []).slice(0, 12);
1769
- const data = (props.data || []).slice(0, 12).map((d) => Number(d) || 0);
1770
- if (labels.length > 1 && data.length > 1) {
1771
- slide.addChart('line', [{ name: props.title || '', labels, values: data }], {
1772
- x: 0.85, y: 4.05, w: 8.3, h: 1.25,
1773
- chartColors: [COLORS.accent],
1774
- lineDataSymbol: 'none',
1775
- lineSmooth: true,
1776
- showLegend: false,
1777
- showValue: false,
1778
- catAxisHidden: true,
1779
- valAxisHidden: true,
1780
- gridLineColor: COLORS.surface,
1781
- });
1782
- }
1783
- addTheme05SpectrumBar(slide);
1784
- }
1785
- function renderTheme05ChartShareV1(slide, props) {
1786
- if (props.kicker) {
1787
- slide.addText(props.kicker, {
1788
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
1789
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
1790
- });
1791
- }
1792
- slide.addText(props.title ?? '', {
1793
- x: 0.65, y: 1.15, w: 8.7, h: 0.7,
1794
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
1795
- });
1796
- if (props.subtitle) {
1797
- slide.addText(props.subtitle, {
1798
- x: 0.65, y: 1.85, w: 8.7, h: 0.35,
1799
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
1800
- });
1801
- }
1802
- const items = (props.items || []).slice(0, 8);
1803
- const hasConclusion = props.showConclusion !== false && !!props.conclusion && (!!props.conclusion.value || !!props.conclusion.label || !!props.conclusion.description);
1804
- const chartW = hasConclusion ? 5.7 : 8.7;
1805
- if (items.length > 0) {
1806
- const data = items.map((item) => ({
1807
- name: item.name ?? '',
1808
- labels: [item.name ?? ''],
1809
- values: [Number(item.value) || 0],
1810
- }));
1811
- slide.addChart('pie', data, {
1812
- x: 0.65, y: 2.05, w: chartW, h: 3.0,
1813
- chartColors: items.map((item) => theme05SchemeColor(item.scheme)),
1814
- showValue: false,
1815
- showLegend: false,
1816
- dataLabelColor: COLORS.primary,
1817
- dataLabelFontSize: 10,
1818
- });
1819
- }
1820
- else {
1821
- slide.addText('(暂无图表数据)', {
1822
- x: 0.65, y: 3.0, w: chartW, h: 0.6,
1823
- fontSize: 16, color: COLORS.secondary, align: 'center', valign: 'middle', fontFace: FONTS.body,
1824
- });
1825
- }
1826
- if (hasConclusion) {
1827
- addTheme05ConclusionCard(slide, 6.55, 2.05, 2.8, 3.0, props.conclusion);
1828
- }
1829
- addTheme05SpectrumBar(slide);
1830
- }
1831
- function renderTheme05ChartStackedV1(slide, props) {
1832
- if (props.kicker) {
1833
- slide.addText(props.kicker, {
1834
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
1835
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
1836
- });
1837
- }
1838
- slide.addText(props.title ?? '', {
1839
- x: 0.65, y: 1.15, w: 8.7, h: 0.7,
1840
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
1841
- });
1842
- if (props.subtitle) {
1843
- slide.addText(props.subtitle, {
1844
- x: 0.65, y: 1.85, w: 8.7, h: 0.35,
1845
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
1846
- });
1847
- }
1848
- const labels = (props.labels || []).slice(0, 12);
1849
- const rawSeries = (props.series || []).slice(0, 5);
1850
- const series = rawSeries.map((s) => ({
1851
- name: s.name ?? '',
1852
- labels,
1853
- values: (s.data || []).slice(0, labels.length).map((d) => Number(d) || 0),
1854
- }));
1855
- const hasConclusion = props.showConclusion !== false && !!props.conclusion && (!!props.conclusion.value || !!props.conclusion.label || !!props.conclusion.description);
1856
- const chartW = hasConclusion ? 5.7 : 8.7;
1857
- if (labels.length > 0 && series.length > 0) {
1858
- slide.addChart('bar', series, {
1859
- x: 0.65, y: 2.05, w: chartW, h: 3.0,
1860
- chartColors: rawSeries.map((s) => theme05SchemeColor(s.scheme)),
1861
- barGrouping: 'stacked',
1862
- barDir: 'col',
1863
- showValue: false,
1864
- });
1865
- }
1866
- else {
1867
- slide.addText('(暂无图表数据)', {
1868
- x: 0.65, y: 3.0, w: chartW, h: 0.6,
1869
- fontSize: 16, color: COLORS.secondary, align: 'center', valign: 'middle', fontFace: FONTS.body,
1870
- });
1871
- }
1872
- if (hasConclusion) {
1873
- addTheme05ConclusionCard(slide, 6.55, 2.05, 2.8, 3.0, props.conclusion);
1874
- }
1875
- addTheme05SpectrumBar(slide);
1876
- }
1877
- function renderTheme05ChartCurveV1(slide, props) {
1878
- if (props.kicker) {
1879
- slide.addText(props.kicker, {
1880
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
1881
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
1882
- });
1883
- }
1884
- slide.addText(props.title ?? '', {
1885
- x: 0.65, y: 1.15, w: 8.7, h: 0.7,
1886
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
1887
- });
1888
- if (props.subtitle) {
1889
- slide.addText(props.subtitle, {
1890
- x: 0.65, y: 1.85, w: 8.7, h: 0.35,
1891
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
1892
- });
1893
- }
1894
- const labels = (props.labels || []).slice(0, 12);
1895
- const values = (props.data || []).slice(0, 12).map((d) => Number(d) || 0);
1896
- const hasConclusion = props.showConclusion !== false && !!props.conclusion && (!!props.conclusion.value || !!props.conclusion.label || !!props.conclusion.description);
1897
- const chartW = hasConclusion ? 5.7 : 8.7;
1898
- if (labels.length > 0 && values.length > 0) {
1899
- slide.addChart('line', [{ name: props.title || '', labels, values }], {
1900
- x: 0.65, y: 2.05, w: chartW, h: 3.0,
1901
- chartColors: [COLORS.accent],
1902
- lineDataSymbol: 'circle',
1903
- lineSmooth: props.smooth !== false,
1904
- showLegend: false,
1905
- showValue: false,
1906
- });
1907
- }
1908
- else {
1909
- slide.addText('(暂无图表数据)', {
1910
- x: 0.65, y: 3.0, w: chartW, h: 0.6,
1911
- fontSize: 16, color: COLORS.secondary, align: 'center', valign: 'middle', fontFace: FONTS.body,
1912
- });
1913
- }
1914
- if (hasConclusion) {
1915
- addTheme05ConclusionCard(slide, 6.55, 2.05, 2.8, 3.0, props.conclusion);
1916
- }
1917
- addTheme05SpectrumBar(slide);
1918
- }
1919
- function renderTheme05ChartPeakV1(slide, props) {
1920
- if (props.kicker) {
1921
- slide.addText(props.kicker, {
1922
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
1923
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
1924
- });
1925
- }
1926
- slide.addText(props.title ?? '', {
1927
- x: 0.65, y: 1.15, w: 8.7, h: 0.7,
1928
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
1929
- });
1930
- if (props.subtitle) {
1931
- slide.addText(props.subtitle, {
1932
- x: 0.65, y: 1.85, w: 8.7, h: 0.35,
1933
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
1934
- });
1935
- }
1936
- const labels = (props.labels || []).slice(0, 12);
1937
- const values = (props.data || []).slice(0, 12).map((d) => Number(d) || 0);
1938
- const peakIndex = Number(props.peakIndex) || 0;
1939
- const hasConclusion = props.showConclusion !== false && !!props.conclusion && (!!props.conclusion.value || !!props.conclusion.label || !!props.conclusion.description);
1940
- const chartW = hasConclusion ? 5.7 : 8.7;
1941
- if (labels.length > 0 && values.length > 0) {
1942
- const baseColor = CHART_COLORS[2] || COLORS.secondary;
1943
- const peakColor = COLORS.accent;
1944
- const chartColors = values.map((_, idx) => (idx === peakIndex ? peakColor : baseColor));
1945
- slide.addChart('bar', [{ name: props.title || '', labels, values }], {
1946
- x: 0.65, y: 2.05, w: chartW, h: 3.0,
1947
- chartColors,
1948
- barGrouping: 'clustered',
1949
- barDir: 'col',
1950
- showValue: false,
1951
- });
1952
- // PEAK 标注
1953
- const barW = chartW / Math.max(labels.length, 1);
1954
- const maxVal = Math.max(...values, 1);
1955
- const peakVal = values[peakIndex] ?? 0;
1956
- const peakX = 0.65 + (peakIndex + 0.5) * barW;
1957
- const peakY = 2.05 + 3.0 * (1 - peakVal / maxVal) - 0.35;
1958
- slide.addText('PEAK', {
1959
- x: peakX - 0.5, y: Math.max(2.05, peakY), w: 1.0, h: 0.22,
1960
- fontSize: 10, color: COLORS.accent, bold: true, align: 'center', fontFace: FONTS.mono,
1961
- });
1962
- }
1963
- else {
1964
- slide.addText('(暂无图表数据)', {
1965
- x: 0.65, y: 3.0, w: chartW, h: 0.6,
1966
- fontSize: 16, color: COLORS.secondary, align: 'center', valign: 'middle', fontFace: FONTS.body,
1967
- });
1968
- }
1969
- if (hasConclusion) {
1970
- addTheme05ConclusionCard(slide, 6.55, 2.05, 2.8, 3.0, props.conclusion);
1971
- }
1972
- addTheme05SpectrumBar(slide);
1973
- }
1974
- function renderTheme05ChartPeaktroughV1(slide, props) {
1975
- if (props.kicker) {
1976
- slide.addText(props.kicker, {
1977
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
1978
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
1979
- });
1980
- }
1981
- slide.addText(props.title ?? '', {
1982
- x: 0.65, y: 1.15, w: 8.7, h: 0.7,
1983
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
1984
- });
1985
- if (props.subtitle) {
1986
- slide.addText(props.subtitle, {
1987
- x: 0.65, y: 1.85, w: 8.7, h: 0.35,
1988
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
1989
- });
1990
- }
1991
- const labels = (props.labels || []).slice(0, 12);
1992
- const values = (props.data || []).slice(0, 12).map((d) => Number(d) || 0);
1993
- const peakIndex = Number(props.peakIndex) || 0;
1994
- const troughIndex = Number(props.troughIndex) || 0;
1995
- const hasConclusion = props.showConclusion !== false && !!props.conclusion && (!!props.conclusion.value || !!props.conclusion.label || !!props.conclusion.description);
1996
- const chartW = hasConclusion ? 5.7 : 8.7;
1997
- if (labels.length > 0 && values.length > 0) {
1998
- slide.addChart('line', [{ name: props.title || '', labels, values }], {
1999
- x: 0.65, y: 2.05, w: chartW, h: 3.0,
2000
- chartColors: [COLORS.accent],
2001
- lineDataSymbol: 'circle',
2002
- lineSmooth: true,
2003
- showLegend: false,
2004
- showValue: false,
2005
- });
2006
- // 峰值/谷值椭圆标注
2007
- const maxVal = Math.max(...values, 1);
2008
- const minVal = Math.min(...values);
2009
- const plotH = 3.0;
2010
- const plotY = 2.05;
2011
- const pointX = (idx) => 0.65 + (chartW / Math.max(labels.length - 1, 1)) * idx;
2012
- const pointY = (idx) => plotY + plotH * (1 - (values[idx] - minVal) / Math.max(maxVal - minVal, 1));
2013
- if (values[peakIndex] !== undefined) {
2014
- const px = pointX(peakIndex);
2015
- const py = pointY(peakIndex);
2016
- slide.addShape('ellipse', {
2017
- x: px - 0.2, y: py - 0.12, w: 0.4, h: 0.24,
2018
- fill: { color: COLORS.surface },
2019
- line: { color: COLORS.accent, width: 2 },
2020
- });
2021
- slide.addText('PEAK', {
2022
- x: px - 0.4, y: py - 0.4, w: 0.8, h: 0.2,
2023
- fontSize: 9, color: COLORS.accent, bold: true, align: 'center', fontFace: FONTS.mono,
2024
- });
2025
- }
2026
- if (values[troughIndex] !== undefined) {
2027
- const tx = pointX(troughIndex);
2028
- const ty = pointY(troughIndex);
2029
- slide.addShape('ellipse', {
2030
- x: tx - 0.2, y: ty - 0.12, w: 0.4, h: 0.24,
2031
- fill: { color: COLORS.surface },
2032
- line: { color: COLORS.secondary, width: 2 },
2033
- });
2034
- slide.addText('TROUGH', {
2035
- x: tx - 0.45, y: ty + 0.2, w: 0.9, h: 0.2,
2036
- fontSize: 9, color: COLORS.secondary, bold: true, align: 'center', fontFace: FONTS.mono,
2037
- });
2038
- }
2039
- }
2040
- else {
2041
- slide.addText('(暂无图表数据)', {
2042
- x: 0.65, y: 3.0, w: chartW, h: 0.6,
2043
- fontSize: 16, color: COLORS.secondary, align: 'center', valign: 'middle', fontFace: FONTS.body,
2044
- });
2045
- }
2046
- if (hasConclusion) {
2047
- addTheme05ConclusionCard(slide, 6.55, 2.05, 2.8, 3.0, props.conclusion);
2048
- }
2049
- addTheme05SpectrumBar(slide);
2050
- }
2051
- function renderTheme05ChartCumulativeV1(slide, props) {
2052
- if (props.kicker) {
2053
- slide.addText(props.kicker, {
2054
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
2055
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
2056
- });
2057
- }
2058
- slide.addText(props.title ?? '', {
2059
- x: 0.65, y: 1.15, w: 8.7, h: 0.7,
2060
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
2061
- });
2062
- if (props.subtitle) {
2063
- slide.addText(props.subtitle, {
2064
- x: 0.65, y: 1.85, w: 8.7, h: 0.35,
2065
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
2066
- });
2067
- }
2068
- const labels = (props.labels || []).slice(0, 12);
2069
- const rawValues = (props.data || []).slice(0, 12).map((d) => Number(d) || 0);
2070
- const cumulative = [];
2071
- rawValues.reduce((sum, v) => {
2072
- const next = sum + v;
2073
- cumulative.push(next);
2074
- return next;
2075
- }, 0);
2076
- const hasConclusion = props.showConclusion !== false && !!props.conclusion && (!!props.conclusion.value || !!props.conclusion.label || !!props.conclusion.description);
2077
- const chartW = hasConclusion ? 5.7 : 8.7;
2078
- if (labels.length > 0 && cumulative.length > 0) {
2079
- slide.addChart('line', [{ name: props.title || '', labels, values: cumulative }], {
2080
- x: 0.65, y: 2.05, w: chartW, h: 3.0,
2081
- chartColors: [COLORS.accent],
2082
- lineDataSymbol: 'circle',
2083
- lineSmooth: false,
2084
- showLegend: false,
2085
- showValue: false,
2086
- });
2087
- }
2088
- else {
2089
- slide.addText('(暂无图表数据)', {
2090
- x: 0.65, y: 3.0, w: chartW, h: 0.6,
2091
- fontSize: 16, color: COLORS.secondary, align: 'center', valign: 'middle', fontFace: FONTS.body,
2092
- });
2093
- }
2094
- if (hasConclusion) {
2095
- addTheme05ConclusionCard(slide, 6.55, 2.05, 2.8, 3.0, props.conclusion);
2096
- }
2097
- addTheme05SpectrumBar(slide);
2098
- }
2099
- function renderTheme05TableOfContentsV2(slide, props) {
2100
- slide.addText(props.title ?? '', {
2101
- x: 0.65, y: 1.2, w: 4.0, h: 0.9,
2102
- fontSize: 42, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
2103
- });
2104
- if (props.subtitle) {
2105
- slide.addText(props.subtitle, {
2106
- x: 0.65, y: 2.1, w: 4.0, h: 0.4,
2107
- fontSize: 14, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
2108
- });
2109
- }
2110
- slide.addShape('rect', {
2111
- x: 0.65, y: 2.6, w: 1.2, h: 0.05,
2112
- fill: { color: COLORS.accent },
2113
- });
2114
- const items = (props.items || []).slice(0, 6);
2115
- const startX = 5.2;
2116
- const startY = 1.05;
2117
- const rowH = 0.72;
2118
- items.forEach((item, idx) => {
2119
- const y = startY + idx * rowH;
2120
- const num = String(idx + 1).padStart(2, '0');
2121
- slide.addText(num, {
2122
- x: startX, y, w: 0.7, h: 0.5,
2123
- fontSize: 28, color: COLORS.accent, bold: true, valign: 'top', fontFace: FONTS.heading,
2124
- });
2125
- slide.addText(item.title ?? '', {
2126
- x: startX + 0.9, y, w: 3.2, h: 0.32,
2127
- fontSize: 16, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.body,
2128
- });
2129
- if (item.page) {
2130
- slide.addText(`P.${item.page}`, {
2131
- x: startX + 0.9, y: y + 0.28, w: 3.2, h: 0.22,
2132
- fontSize: 10, color: COLORS.secondary, valign: 'top', fontFace: FONTS.mono,
2133
- });
2134
- }
2135
- });
2136
- addTheme05SpectrumBar(slide);
2137
- }
2138
- function renderTheme05ProcessV2(slide, props) {
2139
- if (props.kicker) {
2140
- slide.addText(props.kicker, {
2141
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
2142
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
2143
- });
2144
- }
2145
- slide.addText(props.title ?? '', {
2146
- x: 0.65, y: 1.15, w: 8.7, h: 0.8,
2147
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
2148
- });
2149
- if (props.subtitle) {
2150
- slide.addText(props.subtitle, {
2151
- x: 0.65, y: 1.95, w: 8.7, h: 0.35,
2152
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
2153
- });
2154
- }
2155
- const steps = (props.steps || []).slice(0, 6);
2156
- const cardW = 8.7;
2157
- const cardH = 0.62;
2158
- const gapY = 0.18;
2159
- const startX = 0.65;
2160
- const startY = 2.5;
2161
- steps.forEach((step, idx) => {
2162
- const y = startY + idx * (cardH + gapY);
2163
- const color = theme05SchemeColor(step.scheme);
2164
- slide.addShape('roundRect', {
2165
- x: startX, y, w: cardW, h: cardH,
2166
- fill: { color: COLORS.surfaceElevated },
2167
- line: { color: COLORS.border, width: 1 },
2168
- rectRadius: 0.08,
2169
- });
2170
- slide.addShape('roundRect', {
2171
- x: startX + 0.12, y: y + 0.11, w: 0.6, h: 0.4,
2172
- fill: { color },
2173
- rectRadius: 0.06,
2174
- });
2175
- slide.addText(String(idx + 1).padStart(2, '0'), {
2176
- x: startX + 0.12, y: y + 0.13, w: 0.6, h: 0.36,
2177
- fontSize: 14, color: COLORS.white, bold: true, align: 'center', valign: 'middle', fontFace: FONTS.mono,
2178
- });
2179
- slide.addText(step.title ?? '', {
2180
- x: startX + 0.9, y: y + 0.1, w: 3.5, h: 0.25,
2181
- fontSize: 14, color: COLORS.primary, bold: true, valign: 'middle', fontFace: FONTS.body,
2182
- });
2183
- if (step.description) {
2184
- slide.addText(step.description, {
2185
- x: startX + 0.9, y: y + 0.32, w: 7.4, h: 0.25,
2186
- fontSize: 10, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
2187
- });
2188
- }
2189
- if (idx < steps.length - 1) {
2190
- const arrowY = y + cardH + gapY / 2;
2191
- slide.addShape('triangle', {
2192
- x: 4.9, y: arrowY - 0.08, w: 0.16, h: 0.16,
2193
- fill: { color: COLORS.border },
2194
- });
2195
- }
2196
- });
2197
- addTheme05SpectrumBar(slide);
2198
- }
2199
- function renderTheme05ComparisonV1(slide, props) {
2200
- if (props.kicker) {
2201
- slide.addText(props.kicker, {
2202
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
2203
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
2204
- });
2205
- }
2206
- slide.addText(props.title ?? '', {
2207
- x: 0.65, y: 1.15, w: 8.7, h: 0.8,
2208
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
2209
- });
2210
- if (props.subtitle) {
2211
- slide.addText(props.subtitle, {
2212
- x: 0.65, y: 1.95, w: 8.7, h: 0.35,
2213
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
2214
- });
2215
- }
2216
- const leftColor = theme05SchemeColor(props.leftScheme);
2217
- const rightColor = theme05SchemeColor(props.rightScheme);
2218
- const cardW = 3.6;
2219
- const cardH = 3.0;
2220
- const cardY = 2.25;
2221
- const gap = 0.9;
2222
- const startX = (10 - cardW * 2 - gap) / 2;
2223
- // 左栏
2224
- slide.addShape('roundRect', {
2225
- x: startX, y: cardY, w: cardW, h: cardH,
2226
- fill: { color: COLORS.surfaceElevated },
2227
- line: { color: COLORS.border, width: 1 },
2228
- rectRadius: 0.12,
2229
- });
2230
- slide.addShape('rect', {
2231
- x: startX, y: cardY, w: cardW, h: 0.08,
2232
- fill: { color: leftColor },
2233
- });
2234
- slide.addText(props.leftTitle ?? '', {
2235
- x: startX + 0.2, y: cardY + 0.25, w: cardW - 0.4, h: 0.35,
2236
- fontSize: 18, color: leftColor, bold: true, valign: 'top', fontFace: FONTS.heading,
2237
- });
2238
- const leftItems = (props.leftItems || []).slice(0, 6);
2239
- leftItems.forEach((item, idx) => {
2240
- slide.addText(item, {
2241
- x: startX + 0.3, y: cardY + 0.75 + idx * 0.38, w: cardW - 0.5, h: 0.34,
2242
- fontSize: 12, color: COLORS.primary, valign: 'top', fontFace: FONTS.body,
2243
- });
2244
- });
2245
- // VS 徽章
2246
- const vsX = startX + cardW + gap / 2;
2247
- slide.addShape('ellipse', {
2248
- x: vsX - 0.35, y: cardY + cardH / 2 - 0.35, w: 0.7, h: 0.7,
2249
- fill: { color: COLORS.surfaceElevated },
2250
- line: { color: COLORS.border, width: 1 },
2251
- });
2252
- slide.addText(props.vsLabel ?? 'VS', {
2253
- x: vsX - 0.35, y: cardY + cardH / 2 - 0.15, w: 0.7, h: 0.3,
2254
- fontSize: 16, color: COLORS.secondary, bold: true, align: 'center', valign: 'middle', fontFace: FONTS.mono,
2255
- });
2256
- // 右栏
2257
- const rightX = startX + cardW + gap;
2258
- slide.addShape('roundRect', {
2259
- x: rightX, y: cardY, w: cardW, h: cardH,
2260
- fill: { color: COLORS.surfaceElevated },
2261
- line: { color: COLORS.border, width: 1 },
2262
- rectRadius: 0.12,
2263
- });
2264
- slide.addShape('rect', {
2265
- x: rightX, y: cardY, w: cardW, h: 0.08,
2266
- fill: { color: rightColor },
2267
- });
2268
- slide.addText(props.rightTitle ?? '', {
2269
- x: rightX + 0.2, y: cardY + 0.25, w: cardW - 0.4, h: 0.35,
2270
- fontSize: 18, color: rightColor, bold: true, valign: 'top', fontFace: FONTS.heading,
2271
- });
2272
- const rightItems = (props.rightItems || []).slice(0, 6);
2273
- rightItems.forEach((item, idx) => {
2274
- slide.addText(item, {
2275
- x: rightX + 0.3, y: cardY + 0.75 + idx * 0.38, w: cardW - 0.5, h: 0.34,
2276
- fontSize: 12, color: COLORS.primary, valign: 'top', fontFace: FONTS.body,
2277
- });
2278
- });
2279
- addTheme05SpectrumBar(slide);
2280
- }
2281
- function renderTheme05ChartFunnelV1(slide, props) {
2282
- if (props.kicker) {
2283
- slide.addText(props.kicker, {
2284
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
2285
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
2286
- });
2287
- }
2288
- slide.addText(props.title ?? '', {
2289
- x: 0.65, y: 1.15, w: 8.7, h: 0.7,
2290
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
2291
- });
2292
- if (props.subtitle) {
2293
- slide.addText(props.subtitle, {
2294
- x: 0.65, y: 1.85, w: 8.7, h: 0.35,
2295
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
2296
- });
2297
- }
2298
- const stages = (props.stages || []).slice(0, 8);
2299
- const hasConclusion = props.showConclusion !== false && !!props.conclusion && (!!props.conclusion.value || !!props.conclusion.label || !!props.conclusion.description);
2300
- const funnelW = hasConclusion ? 5.7 : 8.7;
2301
- const startX = 0.65;
2302
- const startY = 2.15;
2303
- const maxH = 3.0;
2304
- if (stages.length > 0) {
2305
- const maxValue = Math.max(...stages.map((s) => Number(s.value) || 0), 1);
2306
- const layerH = maxH / stages.length;
2307
- const topW = funnelW;
2308
- stages.forEach((stage, idx) => {
2309
- const value = Number(stage.value) || 0;
2310
- const ratio = value / maxValue;
2311
- const upperRatio = idx === 0 ? 1 : (Number(stages[idx - 1].value) || 0) / maxValue;
2312
- const y = startY + idx * layerH;
2313
- const topLayerW = topW * upperRatio;
2314
- const bottomLayerW = topW * ratio;
2315
- const xTop = startX + (funnelW - topLayerW) / 2;
2316
- const xBottom = startX + (funnelW - bottomLayerW) / 2;
2317
- const color = theme05SchemeColor(stage.scheme);
2318
- // 用梯形路径模拟漏斗层
2319
- const path = [
2320
- `M ${xTop} ${y}`,
2321
- `L ${xTop + topLayerW} ${y}`,
2322
- `L ${xBottom + bottomLayerW} ${y + layerH - 0.04}`,
2323
- `L ${xBottom} ${y + layerH - 0.04}`,
2324
- 'Z',
2325
- ].join(' ');
2326
- slide.addShape('custGeom', {
2327
- x: startX, y, w: funnelW, h: layerH,
2328
- path,
2329
- fill: { color },
2330
- });
2331
- slide.addText(stage.name ?? '', {
2332
- x: startX, y: y + 0.05, w: funnelW, h: 0.22,
2333
- fontSize: 10, color: COLORS.white, bold: true, align: 'center', valign: 'top', fontFace: FONTS.body,
2334
- });
2335
- slide.addText(String(value), {
2336
- x: startX, y: y + layerH - 0.28, w: funnelW, h: 0.2,
2337
- fontSize: 10, color: COLORS.white, align: 'center', valign: 'bottom', fontFace: FONTS.heading,
2338
- });
2339
- });
2340
- }
2341
- else {
2342
- slide.addText('(暂无漏斗数据)', {
2343
- x: startX, y: 3.0, w: funnelW, h: 0.6,
2344
- fontSize: 16, color: COLORS.secondary, align: 'center', valign: 'middle', fontFace: FONTS.body,
2345
- });
2346
- }
2347
- if (hasConclusion) {
2348
- addTheme05ConclusionCard(slide, 6.55, 2.05, 2.8, 3.0, props.conclusion);
2349
- }
2350
- addTheme05SpectrumBar(slide);
2351
- }
2352
- function renderTheme05QuoteV2(slide, props) {
2353
- const color = theme05SchemeColor(props.accentScheme);
2354
- slide.addShape('roundRect', {
2355
- x: 0.65, y: 1.8, w: 0.1, h: 2.0,
2356
- fill: { color },
2357
- rectRadius: 0.05,
2358
- });
2359
- slide.addText('“', {
2360
- x: 1.0, y: 1.2, w: 1.0, h: 0.6,
2361
- fontSize: 80, color: COLORS.border, bold: true, valign: 'top', fontFace: FONTS.heading,
2362
- });
2363
- slide.addText(props.quote ?? '', {
2364
- x: 1.0, y: 2.0, w: 8.35, h: 1.6,
2365
- fontSize: 32, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
2366
- });
2367
- const source = [props.author, props.source].filter(Boolean).join(' · ');
2368
- if (source) {
2369
- slide.addText(source, {
2370
- x: 1.0, y: 3.75, w: 8.35, h: 0.25,
2371
- fontSize: 12, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
2372
- });
2373
- }
2374
- addTheme05SpectrumBar(slide);
2375
- }
2376
- function renderTheme05ChartGaugeV1(slide, props) {
2377
- if (props.kicker) {
2378
- slide.addText(props.kicker, {
2379
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
2380
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
2381
- });
2382
- }
2383
- slide.addText(props.title ?? '', {
2384
- x: 0.65, y: 1.15, w: 8.7, h: 0.7,
2385
- fontSize: 38, color: COLORS.primary, bold: true, align: 'center', valign: 'top', fontFace: FONTS.heading,
2386
- });
2387
- if (props.subtitle) {
2388
- slide.addText(props.subtitle, {
2389
- x: 0.65, y: 1.85, w: 8.7, h: 0.35,
2390
- fontSize: 13, color: COLORS.secondary, align: 'center', valign: 'top', fontFace: FONTS.body,
2391
- });
2392
- }
2393
- const value = Number(props.value) || 0;
2394
- const min = Number(props.min) || 0;
2395
- const max = Math.max(Number(props.max) || 100, value, min + 1);
2396
- const chartX = 2.5;
2397
- const chartY = 2.25;
2398
- const chartW = 5.0;
2399
- const chartH = 2.8;
2400
- const color = theme05SchemeColor(props.scheme);
2401
- slide.addChart('doughnut', [
2402
- { name: props.label || '已完成', labels: ['已完成'], values: [value - min] },
2403
- { name: '剩余', labels: ['剩余'], values: [max - value] },
2404
- ], {
2405
- x: chartX, y: chartY, w: chartW, h: chartH,
2406
- chartColors: [color, COLORS.border],
2407
- holeSize: 65,
2408
- showValue: false,
2409
- showLegend: false,
2410
- });
2411
- slide.addText(`${value}${props.unit ?? ''}`, {
2412
- x: chartX, y: chartY + 1.05, w: chartW, h: 0.55,
2413
- fontSize: 36, color: COLORS.primary, bold: true, align: 'center', valign: 'middle', fontFace: FONTS.heading,
2414
- });
2415
- slide.addText(props.label ?? '完成度', {
2416
- x: chartX, y: chartY + 1.55, w: chartW, h: 0.25,
2417
- fontSize: 11, color: COLORS.secondary, align: 'center', valign: 'top', fontFace: FONTS.body,
2418
- });
2419
- addTheme05SpectrumBar(slide);
2420
- }
2421
- function renderTheme05BentoV1(slide, props) {
2422
- if (props.kicker) {
2423
- slide.addText(props.kicker, {
2424
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
2425
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
2426
- });
2427
- }
2428
- slide.addText(props.title ?? '', {
2429
- x: 0.65, y: 1.15, w: 8.7, h: 0.7,
2430
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
2431
- });
2432
- if (props.subtitle) {
2433
- slide.addText(props.subtitle, {
2434
- x: 0.65, y: 1.85, w: 8.7, h: 0.35,
2435
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
2436
- });
2437
- }
2438
- const items = (props.items || []).slice(0, 6);
2439
- if (items.length > 0) {
2440
- const startY = 2.35;
2441
- const areaH = 2.85;
2442
- const gap = 0.16;
2443
- if (items.length === 1) {
2444
- renderTheme05BentoCard(slide, items[0], 0.65, startY, 8.7, areaH);
2445
- }
2446
- else if (items.length === 2) {
2447
- const cardW = (8.7 - gap) / 2;
2448
- items.forEach((item, idx) => {
2449
- renderTheme05BentoCard(slide, item, 0.65 + idx * (cardW + gap), startY, cardW, areaH);
2450
- });
2451
- }
2452
- else if (items.length === 3) {
2453
- renderTheme05BentoCard(slide, items[0], 0.65, startY, 4.25, areaH);
2454
- const h2 = (areaH - gap) / 2;
2455
- renderTheme05BentoCard(slide, items[1], 5.08, startY, 4.25, h2);
2456
- renderTheme05BentoCard(slide, items[2], 5.08, startY + h2 + gap, 4.25, h2);
2457
- }
2458
- else if (items.length === 4) {
2459
- const cardW = (8.7 - gap) / 2;
2460
- const cardH = (areaH - gap) / 2;
2461
- items.forEach((item, idx) => {
2462
- const col = idx % 2;
2463
- const row = Math.floor(idx / 2);
2464
- renderTheme05BentoCard(slide, item, 0.65 + col * (cardW + gap), startY + row * (cardH + gap), cardW, cardH);
2465
- });
2466
- }
2467
- else {
2468
- const cols = 3;
2469
- const cardW = (8.7 - gap * (cols - 1)) / cols;
2470
- const cardH = (areaH - gap) / 2;
2471
- items.forEach((item, idx) => {
2472
- const col = idx % cols;
2473
- const row = Math.floor(idx / cols);
2474
- renderTheme05BentoCard(slide, item, 0.65 + col * (cardW + gap), startY + row * (cardH + gap), cardW, cardH);
2475
- });
2476
- }
2477
- }
2478
- addTheme05SpectrumBar(slide);
2479
- }
2480
- function renderTheme05BentoCard(slide, item, x, y, w, h) {
2481
- const color = theme05SchemeColor(item.scheme);
2482
- addTheme05Card(slide, x, y, w, h);
2483
- slide.addShape('rect', {
2484
- x, y, w, h: 0.06,
2485
- fill: { color },
2486
- });
2487
- slide.addText(item.value ?? '', {
2488
- x: x + 0.14, y: y + 0.18, w: w - 0.28, h: 0.55,
2489
- fontSize: 30, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
2490
- });
2491
- slide.addText(item.title ?? '', {
2492
- x: x + 0.14, y: y + h - 0.55, w: w - 0.28, h: 0.25,
2493
- fontSize: 11, color: COLORS.secondary, valign: 'bottom', fontFace: FONTS.body,
2494
- });
2495
- if (item.description) {
2496
- slide.addText(item.description, {
2497
- x: x + 0.14, y: y + 0.78, w: w - 0.28, h: Math.max(0.2, h - 1.05),
2498
- fontSize: 10, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
2499
- });
2500
- }
2501
- }
2502
- function renderTheme05CaseV1(slide, props) {
2503
- if (props.kicker) {
2504
- slide.addText(props.kicker, {
2505
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
2506
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
2507
- });
2508
- }
2509
- slide.addText(props.title ?? '', {
2510
- x: 0.65, y: 1.15, w: 8.7, h: 0.7,
2511
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
2512
- });
2513
- if (props.subtitle) {
2514
- slide.addText(props.subtitle, {
2515
- x: 0.65, y: 1.85, w: 8.7, h: 0.35,
2516
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
2517
- });
2518
- }
2519
- const metric = props.metric || {};
2520
- const leftX = 0.65;
2521
- const leftW = 3.6;
2522
- const rightX = 4.55;
2523
- const rightW = 4.8;
2524
- const startY = 2.35;
2525
- slide.addText(props.company ?? '', {
2526
- x: leftX, y: startY, w: leftW, h: 0.55,
2527
- fontSize: 26, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
2528
- });
2529
- slide.addText(`${metric.value ?? ''}`, {
2530
- x: leftX, y: startY + 0.7, w: leftW, h: 0.8,
2531
- fontSize: 56, color: COLORS.accent, bold: true, valign: 'top', fontFace: FONTS.heading,
2532
- });
2533
- if (metric.label) {
2534
- slide.addText(metric.label, {
2535
- x: leftX, y: startY + 1.45, w: leftW, h: 0.3,
2536
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
2537
- });
2538
- }
2539
- const sections = [
2540
- { key: 'challenge', label: '挑战' },
2541
- { key: 'solution', label: '方案' },
2542
- { key: 'result', label: '成果' },
2543
- ].filter((s) => props[s.key]);
2544
- const cardH = sections.length > 0 ? (2.8 - 0.16 * (sections.length - 1)) / sections.length : 0;
2545
- sections.forEach((s, idx) => {
2546
- const y = startY + idx * (cardH + 0.16);
2547
- addTheme05Card(slide, rightX, y, rightW, cardH, true);
2548
- slide.addText(s.label, {
2549
- x: rightX + 0.14, y: y + 0.1, w: 0.8, h: 0.22,
2550
- fontSize: 10, color: COLORS.white, bold: true, fontFace: FONTS.mono,
2551
- });
2552
- slide.addText(props[s.key], {
2553
- x: rightX + 0.14, y: y + 0.34, w: rightW - 0.28, h: cardH - 0.48,
2554
- fontSize: 12, color: COLORS.white, valign: 'top', fontFace: FONTS.body,
2555
- });
2556
- });
2557
- addTheme05SpectrumBar(slide);
2558
- }
2559
- function renderTheme05EditorialV1(slide, props) {
2560
- if (props.kicker) {
2561
- slide.addText(props.kicker, {
2562
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
2563
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
2564
- });
2565
- }
2566
- slide.addText(props.title ?? '', {
2567
- x: 0.65, y: 1.15, w: 8.7, h: 0.7,
2568
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
2569
- });
2570
- if (props.subtitle) {
2571
- slide.addText(props.subtitle, {
2572
- x: 0.65, y: 1.85, w: 8.7, h: 0.35,
2573
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
2574
- });
2575
- }
2576
- const isRight = props.imageSide === 'right';
2577
- const imgX = isRight ? 5.05 : 0.65;
2578
- const textX = isRight ? 0.65 : 5.05;
2579
- const imgW = 4.3;
2580
- const textW = 4.3;
2581
- const bodyY = 2.35;
2582
- const imgH = 2.85;
2583
- if (props.imageUrl) {
2584
- addImageMaybe(slide, props.imageUrl, imgX, bodyY, imgW, imgH);
2585
- }
2586
- else {
2587
- slide.addShape('roundRect', {
2588
- x: imgX, y: bodyY, w: imgW, h: imgH,
2589
- fill: { color: COLORS.surfaceElevated },
2590
- line: { color: COLORS.border, width: 1 },
2591
- rectRadius: 0.12,
2592
- });
2593
- slide.addText('点击上传大图', {
2594
- x: imgX, y: bodyY + 1.2, w: imgW, h: 0.4,
2595
- fontSize: 14, color: COLORS.secondary, align: 'center', valign: 'middle', fontFace: FONTS.body,
2596
- });
2597
- }
2598
- if (props.body) {
2599
- slide.addText(props.body, {
2600
- x: textX, y: bodyY, w: textW, h: props.pullQuote ? 1.75 : 2.75,
2601
- fontSize: 13, color: COLORS.primary, valign: 'top', fontFace: FONTS.body,
2602
- });
2603
- }
2604
- if (props.pullQuote) {
2605
- const quoteY = props.body ? bodyY + 1.9 : bodyY;
2606
- slide.addShape('roundRect', {
2607
- x: textX, y: quoteY, w: textW, h: 1.3,
2608
- fill: { color: COLORS.surfaceElevated },
2609
- line: { color: COLORS.border, width: 1 },
2610
- rectRadius: 0.12,
2611
- });
2612
- slide.addShape('rect', {
2613
- x: textX, y: quoteY, w: 0.08, h: 1.3,
2614
- fill: { color: COLORS.accent },
2615
- });
2616
- slide.addText(props.pullQuote, {
2617
- x: textX + 0.18, y: quoteY + 0.16, w: textW - 0.3, h: 0.95,
2618
- fontSize: 13, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.body,
2619
- });
2620
- }
2621
- addTheme05SpectrumBar(slide);
2622
- }
2623
- function renderTheme05GalleryV1(slide, props) {
2624
- if (props.kicker) {
2625
- slide.addText(props.kicker, {
2626
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
2627
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
2628
- });
2629
- }
2630
- slide.addText(props.title ?? '', {
2631
- x: 0.65, y: 1.15, w: 8.7, h: 0.7,
2632
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
2633
- });
2634
- if (props.subtitle) {
2635
- slide.addText(props.subtitle, {
2636
- x: 0.65, y: 1.85, w: 8.7, h: 0.35,
2637
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
2638
- });
2639
- }
2640
- const items = (props.items || []).slice(0, 4);
2641
- if (items.length > 0) {
2642
- const startY = 2.35;
2643
- const areaH = 2.6;
2644
- const gap = 0.16;
2645
- if (items.length === 1) {
2646
- addImageMaybe(slide, items[0].imageUrl, 0.65, startY, 8.7, areaH);
2647
- if (items[0].caption) {
2648
- slide.addText(items[0].caption, {
2649
- x: 0.65, y: startY + areaH + 0.08, w: 8.7, h: 0.25,
2650
- fontSize: 12, color: COLORS.secondary, align: 'center', fontFace: FONTS.body,
2651
- });
2652
- }
2653
- }
2654
- else if (items.length === 2) {
2655
- const w = (8.7 - gap) / 2;
2656
- items.forEach((item, idx) => {
2657
- addImageMaybe(slide, item.imageUrl, 0.65 + idx * (w + gap), startY, w, areaH);
2658
- if (item.caption) {
2659
- slide.addText(item.caption, {
2660
- x: 0.65 + idx * (w + gap), y: startY + areaH + 0.08, w, h: 0.25,
2661
- fontSize: 12, color: COLORS.secondary, align: 'center', fontFace: FONTS.body,
2662
- });
2663
- }
2664
- });
2665
- }
2666
- else if (items.length === 3) {
2667
- const w = (8.7 - gap) / 2;
2668
- const h = (areaH - gap) / 2;
2669
- addImageMaybe(slide, items[0].imageUrl, 0.65, startY, w, areaH);
2670
- addImageMaybe(slide, items[1].imageUrl, 0.65 + w + gap, startY, w, h);
2671
- addImageMaybe(slide, items[2].imageUrl, 0.65 + w + gap, startY + h + gap, w, h);
2672
- [0, 1, 2].forEach((idx) => {
2673
- if (items[idx].caption) {
2674
- const cx = idx === 0 ? 0.65 : 0.65 + w + gap;
2675
- const cy = idx === 0 ? startY + areaH + 0.08 : startY + h + 0.08;
2676
- slide.addText(items[idx].caption, {
2677
- x: cx, y: cy, w, h: 0.25,
2678
- fontSize: 12, color: COLORS.secondary, align: 'center', fontFace: FONTS.body,
2679
- });
2680
- }
2681
- });
2682
- }
2683
- else {
2684
- const w = (8.7 - gap) / 2;
2685
- const h = (areaH - gap) / 2;
2686
- items.forEach((item, idx) => {
2687
- const col = idx % 2;
2688
- const row = Math.floor(idx / 2);
2689
- addImageMaybe(slide, item.imageUrl, 0.65 + col * (w + gap), startY + row * (h + gap), w, h);
2690
- if (item.caption) {
2691
- slide.addText(item.caption, {
2692
- x: 0.65 + col * (w + gap), y: startY + row * (h + gap) + h + 0.08, w, h: 0.25,
2693
- fontSize: 12, color: COLORS.secondary, align: 'center', fontFace: FONTS.body,
2694
- });
2695
- }
2696
- });
2697
- }
2698
- }
2699
- addTheme05SpectrumBar(slide);
2700
- }
2701
- function renderTheme05ProfileV1(slide, props) {
2702
- if (props.kicker) {
2703
- slide.addText(props.kicker, {
2704
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
2705
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
2706
- });
2707
- }
2708
- slide.addText(props.title ?? '', {
2709
- x: 0.65, y: 1.15, w: 8.7, h: 0.7,
2710
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
2711
- });
2712
- if (props.subtitle) {
2713
- slide.addText(props.subtitle, {
2714
- x: 0.65, y: 1.85, w: 8.7, h: 0.35,
2715
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
2716
- });
2717
- }
2718
- const avatarX = 0.85;
2719
- const avatarY = 2.45;
2720
- const avatarSize = 2.2;
2721
- addTheme05Card(slide, avatarX - 0.2, avatarY - 0.2, avatarSize + 0.4, 2.9);
2722
- if (props.avatarUrl) {
2723
- addImageMaybe(slide, props.avatarUrl, avatarX, avatarY, avatarSize, avatarSize);
2724
- }
2725
- else {
2726
- slide.addShape('ellipse', {
2727
- x: avatarX, y: avatarY, w: avatarSize, h: avatarSize,
2728
- fill: { color: COLORS.border },
2729
- });
2730
- slide.addText('+ 头像', {
2731
- x: avatarX, y: avatarY + 0.85, w: avatarSize, h: 0.4,
2732
- fontSize: 14, color: COLORS.secondary, align: 'center', valign: 'middle', fontFace: FONTS.body,
2733
- });
2734
- }
2735
- const mainX = 3.55;
2736
- const mainW = 5.8;
2737
- let cursorY = 2.45;
2738
- slide.addText(props.name ?? '', {
2739
- x: mainX, y: cursorY, w: mainW, h: 0.5,
2740
- fontSize: 28, color: COLORS.primary, bold: true, fontFace: FONTS.heading,
2741
- });
2742
- cursorY += 0.55;
2743
- slide.addText(`${props.role ?? ''} · ${props.company ?? ''}`, {
2744
- x: mainX, y: cursorY, w: mainW, h: 0.28,
2745
- fontSize: 12, color: COLORS.secondary, fontFace: FONTS.body,
2746
- });
2747
- cursorY += 0.45;
2748
- if (props.quote) {
2749
- slide.addShape('roundRect', {
2750
- x: mainX, y: cursorY, w: mainW, h: 0.9,
2751
- fill: { color: COLORS.surfaceElevated },
2752
- line: { color: COLORS.border, width: 1 },
2753
- rectRadius: 0.12,
2754
- });
2755
- slide.addText(`"${props.quote}"`, {
2756
- x: mainX + 0.14, y: cursorY + 0.12, w: mainW - 0.28, h: 0.65,
2757
- fontSize: 13, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.body,
2758
- });
2759
- cursorY += 1.05;
2760
- }
2761
- const facts = (props.facts || []).slice(0, 4);
2762
- if (facts.length > 0) {
2763
- const cols = facts.length <= 2 ? 2 : 2;
2764
- const rows = Math.ceil(facts.length / cols);
2765
- const gap = 0.12;
2766
- const cardW = (mainW - gap * (cols - 1)) / cols;
2767
- const cardH = rows === 1 ? 0.7 : 0.55;
2768
- facts.forEach((fact, idx) => {
2769
- const col = idx % cols;
2770
- const row = Math.floor(idx / cols);
2771
- const x = mainX + col * (cardW + gap);
2772
- const y = cursorY + row * (cardH + gap);
2773
- slide.addText(fact.label ?? '', {
2774
- x, y, w: cardW, h: 0.18,
2775
- fontSize: 9, color: COLORS.secondary, fontFace: FONTS.body,
2776
- });
2777
- slide.addText(fact.value ?? '', {
2778
- x, y: y + 0.2, w: cardW, h: 0.28,
2779
- fontSize: 13, color: COLORS.primary, bold: true, fontFace: FONTS.heading,
2780
- });
2781
- });
2782
- }
2783
- addTheme05SpectrumBar(slide);
2784
- }
2785
- function renderTheme05RoadmapV1(slide, props) {
2786
- if (props.kicker) {
2787
- slide.addText(props.kicker, {
2788
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
2789
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
2790
- });
2791
- }
2792
- slide.addText(props.title ?? '', {
2793
- x: 0.65, y: 1.15, w: 8.7, h: 0.7,
2794
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
2795
- });
2796
- if (props.subtitle) {
2797
- slide.addText(props.subtitle, {
2798
- x: 0.65, y: 1.85, w: 8.7, h: 0.35,
2799
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
2800
- });
2801
- }
2802
- const phases = (props.phases || []).slice(0, 4);
2803
- if (phases.length > 0) {
2804
- const startY = 2.45;
2805
- const cardH = 2.5;
2806
- const gap = 0.2;
2807
- const cardW = (8.7 - gap * (phases.length - 1)) / phases.length;
2808
- const lineY = startY + 0.35;
2809
- slide.addShape('rect', {
2810
- x: 0.65, y: lineY, w: 8.7, h: 0.04,
2811
- fill: { color: COLORS.border },
2812
- });
2813
- phases.forEach((phase, idx) => {
2814
- const x = 0.65 + idx * (cardW + gap);
2815
- const color = theme05SchemeColor(phase.scheme);
2816
- const nodeSize = 0.36;
2817
- const nodeX = x + (cardW - nodeSize) / 2;
2818
- slide.addShape('ellipse', {
2819
- x: nodeX, y: lineY - nodeSize / 2 + 0.02, w: nodeSize, h: nodeSize,
2820
- fill: { color },
2821
- });
2822
- slide.addText(String(idx + 1).padStart(2, '0'), {
2823
- x: nodeX, y: lineY - nodeSize / 2 + 0.02, w: nodeSize, h: nodeSize,
2824
- fontSize: 11, color: 'FFFFFF', bold: true, align: 'center', valign: 'middle', fontFace: FONTS.heading,
2825
- });
2826
- addTheme05Card(slide, x, startY + 0.7, cardW, cardH - 0.7);
2827
- slide.addShape('rect', {
2828
- x, y: startY + 0.7, w: cardW, h: 0.06,
2829
- fill: { color },
2830
- });
2831
- slide.addText(phase.period ?? '', {
2832
- x: x + 0.12, y: startY + 0.88, w: cardW - 0.24, h: 0.2,
2833
- fontSize: 10, color: COLORS.secondary, fontFace: FONTS.mono,
2834
- });
2835
- slide.addText(phase.title ?? '', {
2836
- x: x + 0.12, y: startY + 1.15, w: cardW - 0.24, h: 0.35,
2837
- fontSize: 15, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
2838
- });
2839
- if (phase.description) {
2840
- slide.addText(phase.description, {
2841
- x: x + 0.12, y: startY + 1.55, w: cardW - 0.24, h: 1.1,
2842
- fontSize: 11, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
2843
- });
2844
- }
2845
- });
2846
- }
2847
- addTheme05SpectrumBar(slide);
2848
- }
2849
- function renderTheme05ScorecardsV1(slide, props) {
2850
- if (props.kicker) {
2851
- slide.addText(props.kicker, {
2852
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
2853
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
2854
- });
2855
- }
2856
- slide.addText(props.title ?? '', {
2857
- x: 0.65, y: 1.15, w: 8.7, h: 0.7,
2858
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
2859
- });
2860
- if (props.subtitle) {
2861
- slide.addText(props.subtitle, {
2862
- x: 0.65, y: 1.85, w: 8.7, h: 0.35,
2863
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
2864
- });
2865
- }
2866
- const cards = (props.cards || []).slice(0, 4);
2867
- if (cards.length > 0) {
2868
- const startY = 2.45;
2869
- const gap = 0.14;
2870
- const cardH = (2.85 - gap * (cards.length - 1)) / cards.length;
2871
- cards.forEach((card, idx) => {
2872
- const y = startY + idx * (cardH + gap);
2873
- const color = theme05SchemeColor(card.scheme);
2874
- addTheme05Card(slide, 0.65, y, 8.7, cardH);
2875
- slide.addShape('rect', {
2876
- x: 0.65, y, w: 0.1, h: cardH,
2877
- fill: { color },
2878
- });
2879
- slide.addText(card.label ?? '', {
2880
- x: 0.9, y: y + 0.12, w: 3.8, h: cardH - 0.24,
2881
- fontSize: 15, color: COLORS.primary, bold: true, valign: 'middle', fontFace: FONTS.heading,
2882
- });
2883
- const valueText = `${card.value ?? ''}${card.unit ?? ''}`;
2884
- slide.addText(valueText, {
2885
- x: 4.9, y: y + 0.1, w: 2.2, h: cardH - 0.2,
2886
- fontSize: 28, color: COLORS.primary, bold: true, align: 'right', valign: 'middle', fontFace: FONTS.heading,
2887
- });
2888
- if (card.change) {
2889
- const changeColor = card.positive === false ? 'E85D4E' : '22C55E';
2890
- slide.addShape('roundRect', {
2891
- x: 7.25, y: y + (cardH - 0.42) / 2, w: 1.0, h: 0.42,
2892
- fill: { color: changeColor }, rectRadius: 0.21,
2893
- });
2894
- slide.addText(card.change, {
2895
- x: 7.25, y: y + (cardH - 0.42) / 2 + 0.02, w: 1.0, h: 0.38,
2896
- fontSize: 12, color: 'FFFFFF', bold: true, align: 'center', valign: 'middle', fontFace: FONTS.body,
2897
- });
2898
- }
2899
- });
2900
- }
2901
- addTheme05SpectrumBar(slide);
2902
- }
2903
- function renderTheme05DonutV1(slide, props) {
2904
- if (props.kicker) {
2905
- slide.addText(props.kicker, {
2906
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
2907
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
2908
- });
2909
- }
2910
- slide.addText(props.title ?? '', {
2911
- x: 0.65, y: 1.15, w: 8.7, h: 0.7,
2912
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
2913
- });
2914
- if (props.subtitle) {
2915
- slide.addText(props.subtitle, {
2916
- x: 0.65, y: 1.85, w: 8.7, h: 0.35,
2917
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
2918
- });
2919
- }
2920
- const items = (props.items || []).slice(0, 8).filter((it) => it && it.name);
2921
- const hasConclusion = props.showConclusion !== false && !!props.conclusion && (!!props.conclusion.value || !!props.conclusion.label || !!props.conclusion.description);
2922
- const chartW = hasConclusion ? 5.4 : 8.0;
2923
- const chartX = 0.65;
2924
- const chartY = 2.15;
2925
- if (items.length > 0) {
2926
- const total = items.reduce((sum, it) => sum + (Number(it.value) || 0), 0);
2927
- const data = items.map((it) => ({
2928
- name: it.name,
2929
- labels: [it.name],
2930
- values: [Number(it.value) || 0],
2931
- }));
2932
- slide.addChart('doughnut', data, {
2933
- x: chartX, y: chartY, w: chartW, h: 3.1,
2934
- chartColors: items.map((it) => theme05SchemeColor(it.scheme)),
2935
- holeSize: 55,
2936
- showValue: false,
2937
- showLegend: false,
2938
- });
2939
- if (total > 0) {
2940
- slide.addText(String(total), {
2941
- x: chartX, y: chartY + 1.2, w: chartW, h: 0.55,
2942
- fontSize: 34, color: COLORS.primary, bold: true, align: 'center', valign: 'middle', fontFace: FONTS.heading,
2943
- });
2944
- if (props.unit) {
2945
- slide.addText(props.unit, {
2946
- x: chartX, y: chartY + 1.7, w: chartW, h: 0.25,
2947
- fontSize: 11, color: COLORS.secondary, align: 'center', valign: 'top', fontFace: FONTS.body,
2948
- });
2949
- }
2950
- }
2951
- const legendX = chartX + chartW + 0.25;
2952
- const legendW = hasConclusion ? 2.7 : 3.0;
2953
- items.slice(0, 6).forEach((it, idx) => {
2954
- const y = chartY + idx * 0.48;
2955
- const color = theme05SchemeColor(it.scheme);
2956
- slide.addShape('ellipse', {
2957
- x: legendX, y: y + 0.1, w: 0.16, h: 0.16,
2958
- fill: { color },
2959
- });
2960
- slide.addText(it.name, {
2961
- x: legendX + 0.24, y, w: legendW - 0.24, h: 0.22,
2962
- fontSize: 11, color: COLORS.primary, bold: true, fontFace: FONTS.body,
2963
- });
2964
- const pct = total > 0 ? Math.round(((Number(it.value) || 0) / total) * 100) : 0;
2965
- slide.addText(`${pct}%`, {
2966
- x: legendX + 0.24, y: y + 0.22, w: legendW - 0.24, h: 0.18,
2967
- fontSize: 10, color: COLORS.secondary, fontFace: FONTS.body,
2968
- });
2969
- });
2970
- }
2971
- else {
2972
- slide.addText('(暂无图表数据)', {
2973
- x: chartX, y: chartY + 1.2, w: chartW, h: 0.6,
2974
- fontSize: 16, color: COLORS.secondary, align: 'center', valign: 'middle', fontFace: FONTS.body,
2975
- });
2976
- }
2977
- if (hasConclusion) {
2978
- addTheme05ConclusionCard(slide, 6.55, chartY, 2.8, 3.1, props.conclusion);
2979
- }
2980
- addTheme05SpectrumBar(slide);
2981
- }
2982
- function renderTheme05RadarV1(slide, props) {
2983
- if (props.kicker) {
2984
- slide.addText(props.kicker, {
2985
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
2986
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
2987
- });
2988
- }
2989
- slide.addText(props.title ?? '', {
2990
- x: 0.65, y: 1.15, w: 8.7, h: 0.7,
2991
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
2992
- });
2993
- if (props.subtitle) {
2994
- slide.addText(props.subtitle, {
2995
- x: 0.65, y: 1.85, w: 8.7, h: 0.35,
2996
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
2997
- });
2998
- }
2999
- const indicators = (props.indicators || []).slice(0, 10).filter((it) => it && it.name);
3000
- const rawSeries = (props.series || []).slice(0, 5).filter((it) => it && it.name);
3001
- const chartX = 0.65;
3002
- const chartY = 2.15;
3003
- const chartW = 6.0;
3004
- const chartH = 3.15;
3005
- if (indicators.length > 0 && rawSeries.length > 0) {
3006
- const labels = indicators.map((it) => it.name);
3007
- const data = rawSeries.map((s) => ({
3008
- name: s.name,
3009
- labels,
3010
- values: (Array.isArray(s.values) ? s.values : String(s.values || '').split(/[,,]/).map((v) => parseFloat(v.trim())).filter((v) => !Number.isNaN(v))).slice(0, labels.length),
3011
- })).filter((s) => s.values.length > 0);
3012
- if (data.length > 0) {
3013
- slide.addChart('radar', data, {
3014
- x: chartX, y: chartY, w: chartW, h: chartH,
3015
- chartColors: rawSeries.map((s) => theme05SchemeColor(s.scheme)),
3016
- showValue: false,
3017
- showLegend: false,
3018
- });
3019
- }
3020
- const legendX = chartX + chartW + 0.3;
3021
- const legendW = 2.8;
3022
- rawSeries.forEach((s, idx) => {
3023
- const y = chartY + idx * 0.32;
3024
- const color = theme05SchemeColor(s.scheme);
3025
- slide.addShape('ellipse', {
3026
- x: legendX, y, w: 0.14, h: 0.14,
3027
- fill: { color },
3028
- });
3029
- slide.addText(s.name, {
3030
- x: legendX + 0.22, y: y - 0.02, w: legendW - 0.22, h: 0.2,
3031
- fontSize: 12, color: COLORS.primary, bold: true, fontFace: FONTS.body,
3032
- });
3033
- });
3034
- }
3035
- else {
3036
- slide.addText('(暂无图表数据)', {
3037
- x: chartX, y: chartY + 1.2, w: chartW, h: 0.6,
3038
- fontSize: 16, color: COLORS.secondary, align: 'center', valign: 'middle', fontFace: FONTS.body,
3039
- });
3040
- }
3041
- addTheme05SpectrumBar(slide);
3042
- }
3043
- function renderTheme05TreemapV1(slide, props) {
3044
- if (props.kicker) {
3045
- slide.addText(props.kicker, {
3046
- x: 0.65, y: 0.78, w: 4.0, h: 0.25,
3047
- fontSize: 11, color: COLORS.accent, fontFace: FONTS.mono, bold: true,
3048
- });
3049
- }
3050
- slide.addText(props.title ?? '', {
3051
- x: 0.65, y: 1.15, w: 8.7, h: 0.7,
3052
- fontSize: 38, color: COLORS.primary, bold: true, valign: 'top', fontFace: FONTS.heading,
3053
- });
3054
- if (props.subtitle) {
3055
- slide.addText(props.subtitle, {
3056
- x: 0.65, y: 1.85, w: 8.7, h: 0.35,
3057
- fontSize: 13, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
3058
- });
3059
- }
3060
- const items = (props.items || []).slice(0, 16).filter((it) => it && it.name);
3061
- const chartX = 0.65;
3062
- const chartY = 2.35;
3063
- const chartW = 8.7;
3064
- const chartH = 2.85;
3065
- if (items.length > 0) {
3066
- slide.addShape('roundRect', {
3067
- x: chartX, y: chartY, w: chartW, h: chartH,
3068
- fill: { color: COLORS.surfaceElevated },
3069
- line: { color: COLORS.border, width: 1 },
3070
- rectRadius: 0.12,
3071
- });
3072
- const rects = layoutTreemap(items, chartX + 0.06, chartY + 0.06, chartW - 0.12, chartH - 0.12);
3073
- rects.forEach(({ x, y, w, h, item }) => {
3074
- const color = theme05SchemeColor(item.scheme);
3075
- if (w < 0.15 || h < 0.15)
3076
- return;
3077
- slide.addShape('roundRect', {
3078
- x, y, w, h,
3079
- fill: { color },
3080
- rectRadius: 0.06,
3081
- });
3082
- const label = `${item.name ?? ''}\n${item.value ?? ''}`;
3083
- const fontSize = Math.min(13, Math.max(8, Math.min(w, h) * 6));
3084
- slide.addText(label, {
3085
- x: x + 0.04, y: y + 0.04, w: w - 0.08, h: h - 0.08,
3086
- fontSize, color: 'FFFFFF', bold: true, align: 'center', valign: 'middle', fontFace: FONTS.body,
3087
- });
3088
- });
3089
- }
3090
- else {
3091
- slide.addText('(暂无图表数据)', {
3092
- x: chartX, y: chartY + 1.1, w: chartW, h: 0.6,
3093
- fontSize: 16, color: COLORS.secondary, align: 'center', valign: 'middle', fontFace: FONTS.body,
3094
- });
3095
- }
3096
- addTheme05SpectrumBar(slide);
3097
- }
3098
- export function registerTheme05Renderers(registerPptxLayoutRenderer) {
3099
- registerPptxLayoutRenderer('theme05_cover_v1', renderTheme05CoverV1);
3100
- registerPptxLayoutRenderer('theme05_table_of_contents_v1', renderTheme05TableOfContentsV1);
3101
- registerPptxLayoutRenderer('theme05_chapter_v1', renderTheme05ChapterV1);
3102
- registerPptxLayoutRenderer('theme05_content_v1', renderTheme05ContentV1);
3103
- registerPptxLayoutRenderer('theme05_metric_v1', renderTheme05MetricV1);
3104
- registerPptxLayoutRenderer('theme05_chart_v1', renderTheme05ChartV1);
3105
- registerPptxLayoutRenderer('theme05_bubble_v1', renderTheme05BubbleV1);
3106
- registerPptxLayoutRenderer('theme05_map_v1', renderTheme05MapV1);
3107
- registerPptxLayoutRenderer('theme05_rank_v1', renderTheme05RankV1);
3108
- registerPptxLayoutRenderer('theme05_heatmap_v1', renderTheme05HeatmapV1);
3109
- registerPptxLayoutRenderer('theme05_waterfall_v1', renderTheme05WaterfallV1);
3110
- registerPptxLayoutRenderer('theme05_quote_v1', renderTheme05QuoteV1);
3111
- registerPptxLayoutRenderer('theme05_image_v1', renderTheme05ImageV1);
3112
- registerPptxLayoutRenderer('theme05_versus_v1', renderTheme05VersusV1);
3113
- registerPptxLayoutRenderer('theme05_process_v1', renderTheme05ProcessV1);
3114
- registerPptxLayoutRenderer('theme05_timeline_v1', renderTheme05TimelineV1);
3115
- registerPptxLayoutRenderer('theme05_matrix_v1', renderTheme05MatrixV1);
3116
- registerPptxLayoutRenderer('theme05_quadrant_v1', renderTheme05QuadrantV1);
3117
- registerPptxLayoutRenderer('theme05_risk_v1', renderTheme05RiskV1);
3118
- registerPptxLayoutRenderer('theme05_closing_v1', renderTheme05ClosingV1);
3119
- registerPptxLayoutRenderer('theme05_cover_ex_v1', renderTheme05CoverExV1);
3120
- registerPptxLayoutRenderer('theme05_cover_ex_v2', renderTheme05CoverExV2);
3121
- registerPptxLayoutRenderer('theme05_cover_hero_v1', renderTheme05CoverHeroV1);
3122
- registerPptxLayoutRenderer('theme05_chapter_big_v1', renderTheme05ChapterBigV1);
3123
- registerPptxLayoutRenderer('theme05_chapter_split_v1', renderTheme05ChapterSplitV1);
3124
- registerPptxLayoutRenderer('theme05_chapter_numbered_v1', renderTheme05ChapterNumberedV1);
3125
- registerPptxLayoutRenderer('theme05_chapter_image_v1', renderTheme05ChapterImageV1);
3126
- registerPptxLayoutRenderer('theme05_metric_hero_v1', renderTheme05MetricHeroV1);
3127
- registerPptxLayoutRenderer('theme05_metric_capacity_v1', renderTheme05MetricCapacityV1);
3128
- registerPptxLayoutRenderer('theme05_metric_delta_v1', renderTheme05MetricDeltaV1);
3129
- registerPptxLayoutRenderer('theme05_chart_share_v1', renderTheme05ChartShareV1);
3130
- registerPptxLayoutRenderer('theme05_chart_stacked_v1', renderTheme05ChartStackedV1);
3131
- registerPptxLayoutRenderer('theme05_chart_curve_v1', renderTheme05ChartCurveV1);
3132
- registerPptxLayoutRenderer('theme05_chart_peak_v1', renderTheme05ChartPeakV1);
3133
- registerPptxLayoutRenderer('theme05_chart_peaktrough_v1', renderTheme05ChartPeaktroughV1);
3134
- registerPptxLayoutRenderer('theme05_chart_cumulative_v1', renderTheme05ChartCumulativeV1);
3135
- registerPptxLayoutRenderer('theme05_table_of_contents_v2', renderTheme05TableOfContentsV2);
3136
- registerPptxLayoutRenderer('theme05_process_v2', renderTheme05ProcessV2);
3137
- registerPptxLayoutRenderer('theme05_comparison_v1', renderTheme05ComparisonV1);
3138
- registerPptxLayoutRenderer('theme05_chart_funnel_v1', renderTheme05ChartFunnelV1);
3139
- registerPptxLayoutRenderer('theme05_quote_v2', renderTheme05QuoteV2);
3140
- registerPptxLayoutRenderer('theme05_chart_gauge_v1', renderTheme05ChartGaugeV1);
3141
- registerPptxLayoutRenderer('theme05_bento_v1', renderTheme05BentoV1);
3142
- registerPptxLayoutRenderer('theme05_case_v1', renderTheme05CaseV1);
3143
- registerPptxLayoutRenderer('theme05_donut_v1', renderTheme05DonutV1);
3144
- registerPptxLayoutRenderer('theme05_editorial_v1', renderTheme05EditorialV1);
3145
- registerPptxLayoutRenderer('theme05_gallery_v1', renderTheme05GalleryV1);
3146
- registerPptxLayoutRenderer('theme05_profile_v1', renderTheme05ProfileV1);
3147
- registerPptxLayoutRenderer('theme05_radar_v1', renderTheme05RadarV1);
3148
- registerPptxLayoutRenderer('theme05_roadmap_v1', renderTheme05RoadmapV1);
3149
- registerPptxLayoutRenderer('theme05_scorecards_v1', renderTheme05ScorecardsV1);
3150
- registerPptxLayoutRenderer('theme05_treemap_v1', renderTheme05TreemapV1);
3151
- }
3152
- //# sourceMappingURL=theme05-pptx.js.map