@lemonppt/renderer 0.1.8 → 0.2.1

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 (78) hide show
  1. package/dist/client/client-render.js +136 -0
  2. package/dist/client/editor-script.js +3355 -0
  3. package/dist/client/theme-echarts.js +122 -0
  4. package/dist/client-render.d.ts +39 -0
  5. package/dist/client-render.d.ts.map +1 -0
  6. package/dist/client-render.js +65 -0
  7. package/dist/client-render.js.map +1 -0
  8. package/dist/editor-script.d.ts +1 -1
  9. package/dist/editor-script.d.ts.map +1 -1
  10. package/dist/editor-script.js +2791 -310
  11. package/dist/editor-script.js.map +1 -1
  12. package/dist/export-pdf.d.ts +5 -0
  13. package/dist/export-pdf.d.ts.map +1 -1
  14. package/dist/export-pdf.js +22 -2
  15. package/dist/export-pdf.js.map +1 -1
  16. package/dist/export-pptx.d.ts +1157 -4
  17. package/dist/export-pptx.d.ts.map +1 -1
  18. package/dist/export-pptx.js +2436 -1356
  19. package/dist/export-pptx.js.map +1 -1
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/render-editor-data.d.ts +21 -0
  25. package/dist/render-editor-data.d.ts.map +1 -0
  26. package/dist/render-editor-data.js +1068 -0
  27. package/dist/render-editor-data.js.map +1 -0
  28. package/dist/render.d.ts +0 -2
  29. package/dist/render.d.ts.map +1 -1
  30. package/dist/render.js +946 -160
  31. package/dist/render.js.map +1 -1
  32. package/dist/theme-echarts.d.ts +9 -0
  33. package/dist/theme-echarts.d.ts.map +1 -0
  34. package/dist/theme-echarts.js +14 -0
  35. package/dist/theme-echarts.js.map +1 -0
  36. package/dist/theme01-pptx.d.ts +12 -0
  37. package/dist/theme01-pptx.d.ts.map +1 -0
  38. package/dist/theme01-pptx.js +2954 -0
  39. package/dist/theme01-pptx.js.map +1 -0
  40. package/dist/theme02-pptx.d.ts +5 -0
  41. package/dist/theme02-pptx.d.ts.map +1 -0
  42. package/dist/theme02-pptx.js +1063 -0
  43. package/dist/theme02-pptx.js.map +1 -0
  44. package/dist/theme03-pptx.d.ts +5 -0
  45. package/dist/theme03-pptx.d.ts.map +1 -0
  46. package/dist/theme03-pptx.js +3279 -0
  47. package/dist/theme03-pptx.js.map +1 -0
  48. package/dist/theme04-pptx.d.ts +6 -0
  49. package/dist/theme04-pptx.d.ts.map +1 -0
  50. package/dist/theme04-pptx.js +5496 -0
  51. package/dist/theme04-pptx.js.map +1 -0
  52. package/dist/theme05-pptx.d.ts +5 -0
  53. package/dist/theme05-pptx.d.ts.map +1 -0
  54. package/dist/theme05-pptx.js +3152 -0
  55. package/dist/theme05-pptx.js.map +1 -0
  56. package/dist/theme06-pptx.d.ts +124 -0
  57. package/dist/theme06-pptx.d.ts.map +1 -0
  58. package/dist/theme06-pptx.js +8154 -0
  59. package/dist/theme06-pptx.js.map +1 -0
  60. package/dist/theme07-pptx.d.ts +49 -0
  61. package/dist/theme07-pptx.d.ts.map +1 -0
  62. package/dist/theme07-pptx.js +2579 -0
  63. package/dist/theme07-pptx.js.map +1 -0
  64. package/dist/theme08-pptx.d.ts +19 -0
  65. package/dist/theme08-pptx.d.ts.map +1 -0
  66. package/dist/theme08-pptx.js +1411 -0
  67. package/dist/theme08-pptx.js.map +1 -0
  68. package/dist/theme09-pptx.d.ts +18 -0
  69. package/dist/theme09-pptx.d.ts.map +1 -0
  70. package/dist/theme09-pptx.js +1513 -0
  71. package/dist/theme09-pptx.js.map +1 -0
  72. package/dist/theme10-pptx.d.ts +18 -0
  73. package/dist/theme10-pptx.d.ts.map +1 -0
  74. package/dist/theme10-pptx.js +2889 -0
  75. package/dist/theme10-pptx.js.map +1 -0
  76. package/package.json +8 -6
  77. package/templates/editor.html +580 -0
  78. package/templates/editor.js +222 -0
@@ -3,9 +3,188 @@
3
3
  // SPDX-License-Identifier: AGPL-3.0-or-later
4
4
  import { normalizeDeckGoal } from '@lemonppt/core';
5
5
  import PptxGenJS from 'pptxgenjs';
6
+ import { mkdtempSync, writeFileSync, rmSync, readFileSync } from 'node:fs';
7
+ import { tmpdir } from 'node:os';
8
+ import path from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import crypto from 'node:crypto';
11
+ import { configureTheme08, registerTheme08Renderers } from './theme08-pptx.js';
12
+ import { configureTheme09, registerTheme09Renderers } from './theme09-pptx.js';
13
+ import { configureTheme10, registerTheme10Renderers } from './theme10-pptx.js';
14
+ import { registerTheme01Renderers, cleanupTheme01TempImages } from './theme01-pptx.js';
15
+ // 重新导出 theme01 通用渲染器,供 theme02-pptx.ts 等复用,避免 theme02→theme01→export-pptx 的循环依赖。
16
+ export { renderChartV1, renderContentV1, renderCoverV1, renderClosing, renderComparisonV1 } from './theme01-pptx.js';
17
+ import { registerTheme02Renderers } from './theme02-pptx.js';
18
+ import { registerTheme03Renderers } from './theme03-pptx.js';
19
+ import { registerTheme04Renderers } from './theme04-pptx.js';
20
+ import { registerTheme05Renderers } from './theme05-pptx.js';
21
+ import { registerTheme06Renderers } from './theme06-pptx.js';
22
+ import { registerTheme07Renderers } from './theme07-pptx.js';
23
+ import { renderTheme07SectorV1, renderTheme07TableV1 } from './theme07-pptx.js';
24
+ import { renderTheme06Bento, renderTheme06BigNumber, renderTheme06Case, renderTheme06ChainFlow, renderTheme06Chapter, renderTheme06Closing, renderTheme06CompanyProfile, renderTheme06CompanySpotlight, renderTheme06Comparison, renderTheme06ContentNumbered, renderTheme06Cover, renderTheme06CoverVariant, renderTheme06DealMap, renderTheme06ListBased, renderTheme06Matrix, renderTheme06MetricShowcase, renderTheme06Milestone, renderTheme06Process, renderTheme06Quote, renderTheme06Rank, renderTheme06RiskMatrix, renderTheme06SectorSpotlight, renderTheme06Sources, renderTheme06Statement, renderTheme06Summary, renderTheme06TechLandscape, renderTheme06Timeline, renderTheme06Toc, renderTheme06Triad } from './theme06-pptx.js';
25
+ // Load theme snapshot for PPTX export colors and gradients
26
+ // Use ESM-compatible path resolution
27
+ const _currentDir = path.dirname(fileURLToPath(import.meta.url));
28
+ function loadThemeSnapshot() {
29
+ const possiblePaths = [
30
+ path.resolve(_currentDir, 'theme-snapshot.json'),
31
+ path.resolve(_currentDir, '../src/theme-snapshot.json'),
32
+ path.resolve(_currentDir, '../theme-snapshot.json'),
33
+ ];
34
+ for (const snapshotPath of possiblePaths) {
35
+ try {
36
+ const content = readFileSync(snapshotPath, 'utf-8');
37
+ console.log(`Loading theme snapshot from: ${snapshotPath}`);
38
+ return JSON.parse(content);
39
+ }
40
+ catch {
41
+ // Continue to next path
42
+ }
43
+ }
44
+ console.warn('Warning: theme-snapshot.json not found in any location, using default theme configs');
45
+ return {};
46
+ }
47
+ const THEME_SNAPSHOT = loadThemeSnapshot();
48
+ // Load layout coordinates for PPTX export
49
+ function loadLayoutCoordinates() {
50
+ const possiblePaths = [
51
+ path.resolve(_currentDir, 'pptx-layout-coordinates.json'),
52
+ path.resolve(_currentDir, '../src/pptx-layout-coordinates.json'),
53
+ path.resolve(_currentDir, '../pptx-layout-coordinates.json'),
54
+ ];
55
+ for (const coordPath of possiblePaths) {
56
+ try {
57
+ const content = readFileSync(coordPath, 'utf-8');
58
+ console.log(`Loading layout coordinates from: ${coordPath}`);
59
+ return JSON.parse(content);
60
+ }
61
+ catch {
62
+ // Continue to next path
63
+ }
64
+ }
65
+ console.warn('Warning: pptx-layout-coordinates.json not found, using default coordinates');
66
+ return {};
67
+ }
68
+ const LAYOUT_COORDINATES = loadLayoutCoordinates();
69
+ // Load theme06-specific layout coordinates
70
+ function loadTheme06LayoutCoordinates() {
71
+ const possiblePaths = [
72
+ path.resolve(_currentDir, 'theme06-layout-coordinates.json'),
73
+ path.resolve(_currentDir, '../src/theme06-layout-coordinates.json'),
74
+ path.resolve(_currentDir, '../theme06-layout-coordinates.json'),
75
+ ];
76
+ for (const coordPath of possiblePaths) {
77
+ try {
78
+ const content = readFileSync(coordPath, 'utf-8');
79
+ console.log(`Loading theme06 layout coordinates from: ${coordPath}`);
80
+ return JSON.parse(content);
81
+ }
82
+ catch {
83
+ // Continue to next path
84
+ }
85
+ }
86
+ console.warn('Warning: theme06-layout-coordinates.json not found');
87
+ return {};
88
+ }
89
+ const THEME06_LAYOUT_COORDINATES = loadTheme06LayoutCoordinates();
90
+ // Load theme07-specific layout coordinates (v2 coordinate system)
91
+ function loadTheme07LayoutCoordinates() {
92
+ const possiblePaths = [
93
+ path.resolve(_currentDir, 'theme07-layout-coordinates.json'),
94
+ path.resolve(_currentDir, '../src/theme07-layout-coordinates.json'),
95
+ path.resolve(_currentDir, '../theme07-layout-coordinates.json'),
96
+ ];
97
+ for (const coordPath of possiblePaths) {
98
+ try {
99
+ const content = readFileSync(coordPath, 'utf-8');
100
+ console.log(`Loading theme07 layout coordinates from: ${coordPath}`);
101
+ return JSON.parse(content);
102
+ }
103
+ catch {
104
+ // Continue to next path
105
+ }
106
+ }
107
+ console.warn('Warning: theme07-layout-coordinates.json not found');
108
+ return {};
109
+ }
110
+ const THEME07_LAYOUT_COORDINATES = loadTheme07LayoutCoordinates();
111
+ // Load theme08-specific layout coordinates
112
+ function loadTheme08LayoutCoordinates() {
113
+ const possiblePaths = [
114
+ path.resolve(_currentDir, 'theme08-layout-coordinates.json'),
115
+ path.resolve(_currentDir, '../src/theme08-layout-coordinates.json'),
116
+ path.resolve(_currentDir, '../theme08-layout-coordinates.json'),
117
+ ];
118
+ for (const coordPath of possiblePaths) {
119
+ try {
120
+ const content = readFileSync(coordPath, 'utf-8');
121
+ console.log(`Loading theme08 layout coordinates from: ${coordPath}`);
122
+ return JSON.parse(content);
123
+ }
124
+ catch {
125
+ // Continue to next path
126
+ }
127
+ }
128
+ console.warn('Warning: theme08-layout-coordinates.json not found');
129
+ return {};
130
+ }
131
+ const THEME08_LAYOUT_COORDINATES = loadTheme08LayoutCoordinates();
132
+ /** Convert CSS layout ID to coordinate config key */
133
+ function normalizeLayoutId(layoutId) {
134
+ // theme01_cover_v1 -> cover_v1
135
+ const parts = layoutId.split('_');
136
+ // Find the layout role (cover, toc, metric, stats, chart, content, comparison, timeline, table)
137
+ const roles = ['cover', 'toc', 'metric', 'stats', 'chart', 'content', 'comparison', 'timeline', 'table', 'chapter', 'closing', 'quote', 'feature', 'gallery', 'image', 'team', 'pricing', 'faq', 'roadmap', 'process', 'quadrant', 'matrix', 'bento', 'risk', 'trend', 'scorecard', 'outlook', 'statement', 'conclusion'];
138
+ for (const role of roles) {
139
+ if (layoutId.includes(role)) {
140
+ const version = parts[parts.length - 1] || 'v1';
141
+ return `${role}_${version}`;
142
+ }
143
+ }
144
+ return 'default';
145
+ }
146
+ /** Get layout coordinates by layout ID */
147
+ export function getLayoutCoordinates(layoutId) {
148
+ // First check theme-specific coordinates (e.g., theme06_cover_v1)
149
+ const themeId = layoutId.split('_')[0] + '_' + layoutId.split('_')[1]; // e.g., "theme06"
150
+ if (themeId === 'theme06') {
151
+ const theme06Layouts = THEME06_LAYOUT_COORDINATES.layouts;
152
+ if (theme06Layouts && theme06Layouts[layoutId]) {
153
+ return { ...theme06Layouts[layoutId], _themeSpecific: true };
154
+ }
155
+ }
156
+ if (themeId === 'theme07') {
157
+ const theme07Layouts = THEME07_LAYOUT_COORDINATES.layouts;
158
+ if (theme07Layouts && theme07Layouts[layoutId]) {
159
+ return { ...theme07Layouts[layoutId], _themeSpecific: true };
160
+ }
161
+ }
162
+ if (themeId === 'theme08') {
163
+ const theme08Layouts = THEME08_LAYOUT_COORDINATES.layouts;
164
+ if (theme08Layouts && theme08Layouts[layoutId]) {
165
+ return { ...theme08Layouts[layoutId], _themeSpecific: true };
166
+ }
167
+ }
168
+ // Then check generic coordinates
169
+ const normalizedId = normalizeLayoutId(layoutId);
170
+ const layouts = LAYOUT_COORDINATES.layouts;
171
+ if (layouts && layouts[normalizedId]) {
172
+ return layouts[normalizedId];
173
+ }
174
+ // Fallback: try broader match (just the role without version)
175
+ if (layouts) {
176
+ for (const [key, value] of Object.entries(layouts)) {
177
+ const roleKey = key.split('_')[0];
178
+ if (normalizedId.startsWith(roleKey)) {
179
+ return value;
180
+ }
181
+ }
182
+ }
183
+ return {};
184
+ }
6
185
  const THEME_CONFIGS = {
7
- base: {
8
- colors: {
186
+ theme01: {
187
+ colorsLight: {
9
188
  primary: '1D1D1F',
10
189
  secondary: '6E6E73',
11
190
  accent: '2563EB',
@@ -15,63 +194,604 @@ const THEME_CONFIGS = {
15
194
  surface: 'FFFFFF',
16
195
  surfaceElevated: 'F9FAFB',
17
196
  },
197
+ colorsDark: {
198
+ primary: 'F0F0F5',
199
+ secondary: 'C0C0CC',
200
+ accent: '3B82F6',
201
+ white: '1A1A20',
202
+ light: '25252C',
203
+ border: '3A3A44',
204
+ surface: '1A1A20',
205
+ surfaceElevated: '25252C',
206
+ },
18
207
  fonts: { heading: 'Inter', body: 'Inter', mono: 'JetBrains Mono' },
19
208
  chartColors: ['2563EB', '10B981', 'F59E0B', 'EF4444', '8B5CF6', 'EC4899'],
209
+ chartColorsDark: ['3B82F6', '34D399', 'FBBF24', 'F87171', 'A78BFA', 'F472B6'],
20
210
  },
21
- 'dark-tech': {
22
- colors: {
23
- primary: 'F3F4F6',
24
- secondary: '94A3B8',
25
- accent: '06B6D4',
26
- white: 'FFFFFF',
27
- light: '1F2937',
28
- border: '374151',
29
- surface: '111827',
30
- surfaceElevated: '1F2937',
211
+ theme02: {
212
+ colorsLight: {
213
+ primary: 'F0F4F8',
214
+ secondary: 'A0A8B0',
215
+ accent: '00E5B0',
216
+ white: '080A0E',
217
+ light: '0F1218',
218
+ border: '2A2E36',
219
+ surface: '0F1218',
220
+ surfaceElevated: '161A22',
221
+ },
222
+ colorsDark: {
223
+ primary: 'F0F4F8',
224
+ secondary: 'A0A8B0',
225
+ accent: '00E5B0',
226
+ white: '080A0E',
227
+ light: '0F1218',
228
+ border: '2A2E36',
229
+ surface: '0F1218',
230
+ surfaceElevated: '161A22',
231
+ },
232
+ fonts: { heading: 'Space Grotesk', body: 'Noto Sans SC', mono: 'Space Mono' },
233
+ chartColors: ['00E5B0', 'FFD166', '00B4FF', 'FF6B6B', 'A78BFA', 'F472B6'],
234
+ },
235
+ theme03: {
236
+ colorsLight: {
237
+ primary: '05080D',
238
+ secondary: '5A5E68',
239
+ accent: '0077B6',
240
+ white: 'F1F3F5',
241
+ light: 'E5E8EB',
242
+ border: 'D0D2D6',
243
+ surface: 'FFFFFF',
244
+ surfaceElevated: 'F1F3F5',
245
+ },
246
+ colorsDark: {
247
+ primary: 'E8EDF3',
248
+ secondary: '8A8F99',
249
+ accent: '00B4FF',
250
+ white: '05080D',
251
+ light: '0D1118',
252
+ border: '22262D',
253
+ surface: '05080D',
254
+ surfaceElevated: '0D1118',
255
+ },
256
+ fonts: { heading: 'Space Grotesk', body: 'Noto Sans SC', mono: 'Space Mono' },
257
+ chartColors: ['00B4FF', 'FF2A6D', '8B5CF6', 'FF7B54', 'FF9F1C', '22D3EE'],
258
+ },
259
+ theme04: {
260
+ colorsLight: {
261
+ primary: '1A1A1A',
262
+ secondary: '6B6B73',
263
+ accent: '22A55C',
264
+ white: 'FAFAF8',
265
+ light: 'F1F1ED',
266
+ border: 'E5E5E0',
267
+ surface: 'FFFFFF',
268
+ surfaceElevated: 'FAFAF8',
269
+ },
270
+ colorsDark: {
271
+ primary: 'F4F4F5',
272
+ secondary: 'A0A0A8',
273
+ accent: '3ADE80',
274
+ white: '0A0A0A',
275
+ light: '121212',
276
+ border: '2A2A2E',
277
+ surface: '0A0A0A',
278
+ surfaceElevated: '121212',
31
279
  },
32
280
  fonts: { heading: 'Space Grotesk', body: 'Inter', mono: 'Space Mono' },
33
- chartColors: ['06B6D4', '3B82F6', '10B981', 'F59E0B', '8B5CF6'],
281
+ chartColors: ['3ADE80', 'FF6B9D', '4ECDC4', 'FFD166', 'A78BFA', 'FF8A5B'],
282
+ },
283
+ theme05: {
284
+ colorsLight: {
285
+ primary: '1A1A1A',
286
+ secondary: '6B6B73',
287
+ accent: 'C73E2F',
288
+ white: 'F7F4EF',
289
+ light: 'EDE9E2',
290
+ border: 'DCD6CC',
291
+ surface: 'FFFFFF',
292
+ surfaceElevated: 'F7F4EF',
293
+ },
294
+ colorsDark: {
295
+ primary: 'F4F4F5',
296
+ secondary: 'A0A0A8',
297
+ accent: 'E85D4E',
298
+ white: '15151A',
299
+ light: '1E1E24',
300
+ border: '2A2A30',
301
+ surface: '15151A',
302
+ surfaceElevated: '1E1E24',
303
+ },
304
+ fonts: { heading: 'Inter', body: 'Inter', mono: 'Space Mono' },
305
+ chartColors: ['E85D4E', 'F5A623', '0FA3B1', '4A58D9', '7C3AED', '22C55E'],
306
+ },
307
+ theme06: {
308
+ colorsLight: {
309
+ primary: '0F172A',
310
+ secondary: '5A5E68',
311
+ accent: '7BC800',
312
+ white: 'F7F8FA',
313
+ light: 'ECEFF3',
314
+ border: 'D0D2D6',
315
+ surface: 'FFFFFF',
316
+ surfaceElevated: 'F1F3F5',
317
+ },
318
+ colorsDark: {
319
+ primary: 'F2F4F7',
320
+ secondary: 'A0A0A8',
321
+ accent: 'B6FF2B',
322
+ white: '0B0F17',
323
+ light: '121926',
324
+ border: '2A2E36',
325
+ surface: '0B0F17',
326
+ surfaceElevated: '151C27',
327
+ },
328
+ fonts: { heading: 'Inter', body: 'Inter', mono: 'Space Mono' },
329
+ chartColors: ['B6FF2B', '00E5B0', '2E9FFF', 'FF6B45', 'A855F7', 'FFD100'],
34
330
  },
35
- 'warm-business': {
36
- colors: {
37
- primary: '3D2C24',
38
- secondary: '8C7B70',
39
- accent: 'E07B39',
331
+ theme07: {
332
+ colorsLight: {
333
+ primary: '1A1D23',
334
+ secondary: '5A5E68',
335
+ accent: '0E86E6',
336
+ accent2: '12B886',
337
+ accentCool: 'FF7A33',
40
338
  white: 'FFFFFF',
41
- light: 'F5EFE8',
42
- border: 'EFE8E0',
43
- surface: 'FFFBF7',
339
+ light: 'F2F5F8',
340
+ border: 'E2E5EA',
341
+ surface: 'FFFFFF',
342
+ surfaceElevated: 'F4F6F8',
343
+ },
344
+ colorsDark: {
345
+ primary: 'F2F4F7',
346
+ secondary: 'A0A0A8',
347
+ accent: '5B9BD5',
348
+ white: '0D100F',
349
+ light: '151C27',
350
+ border: '2A2E36',
351
+ surface: '151C27',
352
+ surfaceElevated: '1A2330',
353
+ },
354
+ fonts: { heading: 'Source Serif Pro', body: 'Inter', mono: 'JetBrains Mono' },
355
+ chartColors: ['0E86E6', '12B886', 'FF7A33', '7C5CFC', '16BFD9', 'F24E7D'],
356
+ },
357
+ theme08: {
358
+ colorsLight: {
359
+ primary: '1A1812',
360
+ secondary: '524C3D',
361
+ accent: '8F6608',
362
+ white: 'FDFBF4',
363
+ light: 'FAF7EE',
364
+ border: 'D9CFB8',
365
+ surface: 'FFFFFF',
366
+ surfaceElevated: 'FFFFFF',
367
+ },
368
+ colorsDark: {
369
+ primary: 'F7F4EA',
370
+ secondary: 'B8AE98',
371
+ accent: 'FFD23F',
372
+ white: '14130D',
373
+ light: '1C1A12',
374
+ border: '512810',
375
+ surface: '211F17',
376
+ surfaceElevated: '2C281E',
377
+ },
378
+ fonts: { heading: 'Noto Sans SC', body: 'Noto Sans SC', mono: 'JetBrains Mono' },
379
+ chartColors: ['FFD23F', 'FF4D4D', '2BD2FF', 'C44DFF', '3DDC84', 'FF9F1C'],
380
+ },
381
+ theme09: {
382
+ // 墨韵专色 · 杂志印刷风:双基底(纸底/墨底)按版式预分配,
383
+ // appearance(primary/muted) 仅调专色浓度,不翻转明暗。
384
+ colorsLight: {
385
+ primary: '14161C',
386
+ secondary: '3A3F4A',
387
+ accent: 'C8102E',
388
+ white: 'F4F1EA',
389
+ light: 'EDE9DE',
390
+ border: 'D5CFC0',
391
+ surface: 'FDFCF8',
44
392
  surfaceElevated: 'FFFFFF',
45
393
  },
46
- fonts: { heading: 'Newsreader', body: 'IBM Plex Sans', mono: 'JetBrains Mono' },
47
- chartColors: ['E07B39', 'D97706', 'B45309', 'DC2626', 'A16207'],
394
+ colorsDark: {
395
+ primary: 'F4F1EA',
396
+ secondary: 'C9C6BE',
397
+ accent: 'F04A62',
398
+ white: '14161C',
399
+ light: '0D0F14',
400
+ border: '2E323C',
401
+ surface: '1C2027',
402
+ surfaceElevated: '242932',
403
+ },
404
+ fonts: { heading: 'Noto Serif SC', body: 'Noto Sans SC', mono: 'Space Mono' },
405
+ chartColors: ['C8102E', '2B4A8B', '8F6410', '3F6B5C', '6D4C7D', 'A8562A'],
406
+ },
407
+ theme10: {
408
+ // 金色指数 · 金融编辑风:墨黑金线底(始终深色),冰蓝主强调 + 香槟金/铜点睛。
409
+ // 与 theme09 双基底不同,theme10 仅一套深色基底;具体强调色由专属渲染器按 SUBSTRATE 取色。
410
+ colorsLight: {
411
+ primary: 'EEF1F6',
412
+ secondary: 'A9B2C0',
413
+ accent: '4A7FD4',
414
+ white: '0A0E14',
415
+ light: '121823',
416
+ border: '2A3340',
417
+ surface: '121823',
418
+ surfaceElevated: '1A2230',
419
+ },
420
+ colorsDark: {
421
+ primary: 'EEF1F6',
422
+ secondary: 'A9B2C0',
423
+ accent: '4A7FD4',
424
+ white: '0A0E14',
425
+ light: '121823',
426
+ border: '2A3340',
427
+ surface: '121823',
428
+ surfaceElevated: '1A2230',
429
+ },
430
+ fonts: { heading: 'Noto Sans SC', body: 'Noto Sans SC', mono: 'Space Mono' },
431
+ chartColors: ['4A7FD4', 'D9B977', 'E8A23A', 'C97A52', '9A82DC', '6F9BD8'],
48
432
  },
49
433
  };
50
- function resolveThemeConfig(theme, language) {
51
- const config = THEME_CONFIGS[theme ?? ''] ?? THEME_CONFIGS.base;
52
- // 中文场景优先使用 Noto 中文字体,同时保留主题字体作为备选
434
+ // Helper to load theme colors from snapshot
435
+ function getSnapshotThemeColors(themeId, appearance) {
436
+ const snapshotData = THEME_SNAPSHOT[themeId];
437
+ if (!snapshotData)
438
+ return null;
439
+ const mode = appearance === 'light' ? 'light' : 'dark';
440
+ const data = snapshotData[mode];
441
+ if (!data)
442
+ return null;
443
+ return {
444
+ colors: data.colors,
445
+ fonts: data.fonts,
446
+ chartColors: data.chartColors,
447
+ };
448
+ }
449
+ // Helper to get gradient from snapshot
450
+ function getSnapshotGradient(themeId) {
451
+ const snapshotData = THEME_SNAPSHOT[themeId];
452
+ if (!snapshotData || !snapshotData.gradients)
453
+ return null;
454
+ // Return the first gradient available, or the theme-specific one
455
+ const gradientKeys = Object.keys(snapshotData.gradients);
456
+ if (gradientKeys.length === 0)
457
+ return null;
458
+ // Try to find a gradient specific to this theme
459
+ if (snapshotData.gradients[themeId]) {
460
+ return snapshotData.gradients[themeId];
461
+ }
462
+ // Return first available
463
+ return snapshotData.gradients[gradientKeys[0]];
464
+ }
465
+ function resolveThemeConfig(theme, language, colorScheme = 'light', appearance = 'dark') {
466
+ const themeId = theme ?? 'theme01';
467
+ const snapshotColors = getSnapshotThemeColors(themeId, appearance);
468
+ // Fallback to hardcoded config if snapshot is not available
469
+ const config = THEME_CONFIGS[themeId] ?? THEME_CONFIGS.theme01;
470
+ const effectiveColorScheme = colorScheme || (theme === 'theme02' || theme === 'theme03' ? 'scheme-a' : theme === 'theme04' ? 'green' : 'light');
471
+ let isDark = false;
472
+ let colors;
473
+ let chartColors;
474
+ let resolved;
475
+ if (theme === 'theme02') {
476
+ isDark = true;
477
+ colors = config.colorsDark;
478
+ chartColors = config.chartColors;
479
+ }
480
+ else if (theme === 'theme03') {
481
+ isDark = appearance !== 'light';
482
+ colors = isDark ? config.colorsDark : config.colorsLight;
483
+ const isSchemeB = effectiveColorScheme === 'scheme-b';
484
+ const isLight03 = appearance === 'light';
485
+ const accent = isSchemeB
486
+ ? (isLight03 ? 'B45309' : 'FF9F1C')
487
+ : (isLight03 ? '0077B6' : '00B4FF');
488
+ const accent2 = isSchemeB
489
+ ? (isLight03 ? '0077B6' : '00B4FF')
490
+ : (isLight03 ? 'C2185B' : 'FF2A6D');
491
+ const accentCool = isSchemeB
492
+ ? (isLight03 ? '6B21A8' : '8B5CF6')
493
+ : (isLight03 ? '6B21A8' : '8B5CF6');
494
+ colors = {
495
+ ...colors,
496
+ accent,
497
+ secondary: accent2,
498
+ };
499
+ chartColors = [accent, accent2, accentCool, isLight03 ? 'C2410C' : 'FF7B54', isLight03 ? 'B45309' : 'FF9F1C', isLight03 ? '0077B6' : '22D3EE'];
500
+ }
501
+ else if (theme === 'theme04') {
502
+ isDark = appearance !== 'light';
503
+ colors = isDark ? config.colorsDark : config.colorsLight;
504
+ const isLight04 = appearance === 'light';
505
+ const tone = ['green', 'yellow', 'blue', 'pink'].includes(colorScheme) ? colorScheme : 'green';
506
+ const toneAccents = {
507
+ green: { dark: '3ADE80', light: '22A55C' },
508
+ yellow: { dark: 'FFD166', light: 'D97706' },
509
+ blue: { dark: '4ECDC4', light: '0D9488' },
510
+ pink: { dark: 'FF6B9D', light: 'DB2777' },
511
+ };
512
+ const accent = toneAccents[tone][isLight04 ? 'light' : 'dark'];
513
+ const accent2 = isLight04 ? 'DB2777' : 'FF6B9D';
514
+ const accentCool = isLight04 ? '0D9488' : '4ECDC4';
515
+ colors = {
516
+ ...colors,
517
+ accent,
518
+ secondary: accent2,
519
+ };
520
+ chartColors = [accent, accent2, accentCool, isLight04 ? 'D97706' : 'FFD166', isLight04 ? '7C3AED' : 'A78BFA', isLight04 ? 'EA580C' : 'FF8A5B'];
521
+ }
522
+ else if (theme === 'theme05') {
523
+ isDark = appearance !== 'light';
524
+ colors = isDark ? config.colorsDark : config.colorsLight;
525
+ const isLight05 = appearance === 'light';
526
+ const scheme = ['coral', 'amber', 'teal', 'indigo', 'violet'].includes(colorScheme) ? colorScheme : 'coral';
527
+ const schemeAccents = {
528
+ coral: { dark: 'E85D4E', light: 'C73E2F', accent2: { dark: 'F5A623', light: 'C47B08' }, accentCool: { dark: '4A58D9', light: '3A46B0' } },
529
+ amber: { dark: 'F5A623', light: 'C47B08', accent2: { dark: 'E85D4E', light: 'C73E2F' }, accentCool: { dark: '0FA3B1', light: '0B7A85' } },
530
+ teal: { dark: '0FA3B1', light: '0B7A85', accent2: { dark: '4A58D9', light: '3A46B0' }, accentCool: { dark: 'F5A623', light: 'C47B08' } },
531
+ indigo: { dark: '4A58D9', light: '3A46B0', accent2: { dark: '0FA3B1', light: '0B7A85' }, accentCool: { dark: 'F5A623', light: 'C47B08' } },
532
+ violet: { dark: '7C3AED', light: '5B25C1', accent2: { dark: 'F5A623', light: 'C47B08' }, accentCool: { dark: '0FA3B1', light: '0B7A85' } },
533
+ };
534
+ const accent = schemeAccents[scheme][isLight05 ? 'light' : 'dark'];
535
+ const accent2 = schemeAccents[scheme].accent2[isLight05 ? 'light' : 'dark'];
536
+ const accentCool = schemeAccents[scheme].accentCool[isLight05 ? 'light' : 'dark'];
537
+ colors = {
538
+ ...colors,
539
+ accent,
540
+ secondary: accent2,
541
+ };
542
+ chartColors = [accent, accent2, accentCool, isLight05 ? 'C73E2F' : 'E85D4E', isLight05 ? '0B7A85' : '0FA3B1', isLight05 ? '5B25C1' : '7C3AED'];
543
+ }
544
+ else if (theme === 'theme06') {
545
+ isDark = appearance !== 'light';
546
+ colors = isDark ? config.colorsDark : config.colorsLight;
547
+ const isLight06 = appearance === 'light';
548
+ const scheme = ['volt', 'magma', 'nebula', 'nova'].includes(colorScheme) ? colorScheme : 'volt';
549
+ const schemeAccents = {
550
+ volt: { dark: 'B6FF2B', light: '7BC800', accent2: { dark: '00E5B0', light: '009E7A' }, accentCool: { dark: '2E9FFF', light: '1A6FD6' } },
551
+ magma: { dark: 'FF6B45', light: 'D94E2B', accent2: { dark: 'FFC700', light: 'C78F00' }, accentCool: { dark: '00C2FF', light: '008FBF' } },
552
+ nebula: { dark: '2E9FFF', light: '1A6FD6', accent2: { dark: 'A855F7', light: '7C2ED3' }, accentCool: { dark: '22D3EE', light: '0B9FB8' } },
553
+ nova: { dark: 'FFD100', light: 'C78F00', accent2: { dark: 'FF6B45', light: 'D94E2B' }, accentCool: { dark: '00E5B0', light: '009E7A' } },
554
+ };
555
+ const accent = schemeAccents[scheme][isLight06 ? 'light' : 'dark'];
556
+ const accent2 = schemeAccents[scheme].accent2[isLight06 ? 'light' : 'dark'];
557
+ const accentCool = schemeAccents[scheme].accentCool[isLight06 ? 'light' : 'dark'];
558
+ colors = {
559
+ ...colors,
560
+ accent,
561
+ secondary: accent2,
562
+ accent2,
563
+ accentCool,
564
+ borderStrong: isLight06 ? 'D0D2D6' : '2A2E36',
565
+ surfaceSolid: isLight06 ? 'FFFFFF' : '151C27',
566
+ edge: isLight06 ? 'C8CDD4' : '2A2E36',
567
+ };
568
+ chartColors = [accent, accent2, accentCool, isLight06 ? 'D94E2B' : 'FF6B45', isLight06 ? '009E7A' : '00E5B0', isLight06 ? '7C2ED3' : 'A855F7'];
569
+ }
570
+ else if (theme === 'theme07') {
571
+ // theme07 冷白调研风:单一 light 外观,电光蓝主色 + 翠绿辅色 + 暖橙警示(明亮鲜艳)
572
+ isDark = false;
573
+ const accent2 = '12B886';
574
+ const accentCool = 'FF7A33';
575
+ colors = {
576
+ ...config.colorsLight,
577
+ accent2,
578
+ accentCool,
579
+ borderStrong: 'D0D2D6',
580
+ surfaceSolid: 'FFFFFF',
581
+ };
582
+ chartColors = config.chartColors;
583
+ }
584
+ else if (theme === 'theme08') {
585
+ // theme08 黑金实验风:primary(深黑/荧光金) / muted(象牙暖白/古铜金)
586
+ const isMuted = appearance === 'muted' || appearance === 'light';
587
+ isDark = !isMuted;
588
+ colors = isMuted ? config.colorsLight : config.colorsDark;
589
+ chartColors = config.chartColors;
590
+ }
591
+ else if (theme === 'theme09') {
592
+ // theme09 墨韵专色:双基底按版式预分配,全局仅给默认纸底;
593
+ // 墨底由专属渲染器在每一页内部按 layout 覆盖背景。
594
+ const isMuted = appearance === 'muted' || appearance === 'light';
595
+ isDark = false; // 默认纸底;墨底页由 theme09 渲染器覆盖
596
+ colors = isMuted ? config.colorsLight : config.colorsLight;
597
+ chartColors = config.chartColors;
598
+ }
599
+ else if (theme === 'theme10') {
600
+ // theme10 金色指数:始终深色墨黑金线底,专属渲染器内部按 layout 取强调色/基底。
601
+ isDark = true;
602
+ colors = config.colorsDark;
603
+ chartColors = config.chartColors;
604
+ }
605
+ else {
606
+ isDark = effectiveColorScheme === 'dark';
607
+ colors = isDark ? config.colorsDark : config.colorsLight;
608
+ chartColors = isDark ? (config.chartColorsDark ?? config.chartColors) : config.chartColors;
609
+ }
610
+ // If snapshot has data, use it as the base and overlay scheme-specific colors
611
+ if (snapshotColors) {
612
+ // Use snapshot colors as base
613
+ const snapshotColorBase = snapshotColors.colors;
614
+ // For themes with scheme/variant support, apply the accent overrides
615
+ if (themeId === 'theme03') {
616
+ const isSchemeB = effectiveColorScheme === 'scheme-b';
617
+ const isLight03 = appearance === 'light';
618
+ const accent = isSchemeB
619
+ ? (isLight03 ? 'B45309' : 'FF9F1C')
620
+ : (isLight03 ? '0077B6' : '00B4FF');
621
+ const accent2 = isSchemeB
622
+ ? (isLight03 ? '0077B6' : '00B4FF')
623
+ : (isLight03 ? 'C2185B' : 'FF2A6D');
624
+ colors = {
625
+ ...snapshotColorBase,
626
+ accent,
627
+ secondary: accent2,
628
+ };
629
+ chartColors = snapshotColors.chartColors;
630
+ }
631
+ else if (themeId === 'theme04') {
632
+ const isLight04 = appearance === 'light';
633
+ const tone = ['green', 'yellow', 'blue', 'pink'].includes(colorScheme) ? colorScheme : 'green';
634
+ const toneAccents = {
635
+ green: { dark: '3ADE80', light: '22A55C' },
636
+ yellow: { dark: 'FFD166', light: 'D97706' },
637
+ blue: { dark: '4ECDC4', light: '0D9488' },
638
+ pink: { dark: 'FF6B9D', light: 'DB2777' },
639
+ };
640
+ const accent = toneAccents[tone][isLight04 ? 'light' : 'dark'];
641
+ const accent2 = isLight04 ? 'DB2777' : 'FF6B9D';
642
+ colors = {
643
+ ...snapshotColorBase,
644
+ accent,
645
+ secondary: accent2,
646
+ };
647
+ chartColors = snapshotColors.chartColors;
648
+ }
649
+ else if (themeId === 'theme05') {
650
+ const isLight05 = appearance === 'light';
651
+ const scheme = ['coral', 'amber', 'teal', 'indigo', 'violet'].includes(colorScheme) ? colorScheme : 'coral';
652
+ const schemeAccents = {
653
+ coral: { dark: 'E85D4E', light: 'C73E2F', accent2: { dark: 'F5A623', light: 'C47B08' } },
654
+ amber: { dark: 'F5A623', light: 'C47B08', accent2: { dark: 'E85D4E', light: 'C73E2F' } },
655
+ teal: { dark: '0FA3B1', light: '0B7A85', accent2: { dark: '4A58D9', light: '3A46B0' } },
656
+ indigo: { dark: '4A58D9', light: '3A46B0', accent2: { dark: '0FA3B1', light: '0B7A85' } },
657
+ violet: { dark: '7C3AED', light: '5B25C1', accent2: { dark: 'F5A623', light: 'C47B08' } },
658
+ };
659
+ const accent = schemeAccents[scheme][isLight05 ? 'light' : 'dark'];
660
+ const accent2 = schemeAccents[scheme].accent2[isLight05 ? 'light' : 'dark'];
661
+ colors = {
662
+ ...snapshotColorBase,
663
+ accent,
664
+ secondary: accent2,
665
+ };
666
+ chartColors = snapshotColors.chartColors;
667
+ }
668
+ else if (themeId === 'theme06') {
669
+ const isLight06 = appearance === 'light';
670
+ const scheme = ['volt', 'magma', 'nebula', 'nova'].includes(colorScheme) ? colorScheme : 'volt';
671
+ const schemeAccents = {
672
+ volt: { dark: 'B6FF2B', light: '7BC800', accent2: { dark: '00E5B0', light: '009E7A' }, accentCool: { dark: '2E9FFF', light: '1A6FD6' } },
673
+ magma: { dark: 'FF6B45', light: 'D94E2B', accent2: { dark: 'FFC700', light: 'C78F00' }, accentCool: { dark: '00C2FF', light: '008FBF' } },
674
+ nebula: { dark: '2E9FFF', light: '1A6FD6', accent2: { dark: 'A855F7', light: '7C2ED3' }, accentCool: { dark: '22D3EE', light: '0B9FB8' } },
675
+ nova: { dark: 'FFD100', light: 'C78F00', accent2: { dark: 'FF6B45', light: 'D94E2B' }, accentCool: { dark: '00E5B0', light: '009E7A' } },
676
+ };
677
+ const accent = schemeAccents[scheme][isLight06 ? 'light' : 'dark'];
678
+ const accent2 = schemeAccents[scheme].accent2[isLight06 ? 'light' : 'dark'];
679
+ const accentCool = schemeAccents[scheme].accentCool[isLight06 ? 'light' : 'dark'];
680
+ colors = {
681
+ ...snapshotColorBase,
682
+ accent,
683
+ secondary: accent2,
684
+ accent2,
685
+ accentCool,
686
+ };
687
+ chartColors = snapshotColors.chartColors;
688
+ }
689
+ else {
690
+ // Simple themes (theme01) - just use snapshot colors directly
691
+ colors = snapshotColorBase;
692
+ chartColors = snapshotColors.chartColors;
693
+ }
694
+ resolved = {
695
+ colors,
696
+ fonts: snapshotColors.fonts,
697
+ chartColors,
698
+ };
699
+ }
700
+ else {
701
+ // Fallback to the original logic
702
+ resolved = {
703
+ colors,
704
+ fonts: config.fonts,
705
+ chartColors,
706
+ };
707
+ }
708
+ // 中文场景优先使用 Noto 中文字体
53
709
  if (language === 'zh') {
54
- return {
55
- ...config,
56
- fonts: {
57
- heading: theme === 'warm-business' ? 'Noto Serif SC' : 'Noto Sans SC',
58
- body: 'Noto Sans SC',
59
- mono: 'Noto Sans SC',
60
- },
710
+ resolved.fonts = {
711
+ heading: 'Noto Sans SC',
712
+ body: 'Noto Sans SC',
713
+ mono: 'Noto Sans SC',
61
714
  };
62
715
  }
63
- return config;
716
+ return resolved;
717
+ }
718
+ export let COLORS = THEME_CONFIGS.theme01.colorsLight;
719
+ export let FONTS = THEME_CONFIGS.theme01.fonts;
720
+ export let CHART_COLORS = THEME_CONFIGS.theme01.chartColors;
721
+ // Store current gradient for background functions
722
+ let CURRENT_GRADIENT = null;
723
+ /** 调试辅助:拦截 slide 的绘制调用,检测空颜色 */
724
+ function wrapSlideForColorDebug(slide, index) {
725
+ const methodsToWrap = ['addShape', 'addText', 'addChart', 'addImage'];
726
+ return new Proxy(slide, {
727
+ get(target, prop) {
728
+ const value = target[prop];
729
+ if (methodsToWrap.includes(prop) && typeof value === 'function') {
730
+ return function (...args) {
731
+ // args[0] is shapeType/text/chartType; args[1] is options for shape/chart/image, text options is args[1] for addText
732
+ const optionsArg = prop === 'addText' ? args[1] : args[1];
733
+ if (optionsArg && typeof optionsArg === 'object') {
734
+ findEmptyColor(optionsArg, `${String(prop)}(slide ${index + 1})`);
735
+ }
736
+ return value.apply(this === undefined ? target : this, args);
737
+ };
738
+ }
739
+ return value;
740
+ },
741
+ });
742
+ }
743
+ function findEmptyColor(obj, path) {
744
+ if (!obj || typeof obj !== 'object')
745
+ return;
746
+ for (const [key, val] of Object.entries(obj)) {
747
+ const currentPath = `${path}.${key}`;
748
+ if ((key === 'color' || key === 'fill' || key === 'line' || key === 'chartColors' || key === 'dataLabelColor') && (val === '' || val === undefined || val === null)) {
749
+ console.warn(`[PPTX color debug] 空颜色 @ ${currentPath}`, val, new Error().stack?.split('\n').slice(2, 6).join('\n'));
750
+ continue;
751
+ }
752
+ if (key === 'color' && typeof val === 'string' && val.startsWith('var(--')) {
753
+ console.warn(`[PPTX color debug] CSS 变量颜色未解析 @ ${currentPath}`, val, new Error().stack?.split('\n').slice(2, 6).join('\n'));
754
+ continue;
755
+ }
756
+ if (typeof val === 'object') {
757
+ findEmptyColor(val, currentPath);
758
+ }
759
+ }
64
760
  }
65
- let COLORS = THEME_CONFIGS.base.colors;
66
- let FONTS = THEME_CONFIGS.base.fonts;
67
- let CHART_COLORS = THEME_CONFIGS.base.chartColors;
68
761
  export async function exportDeckToPptx(goal, options) {
69
762
  goal = normalizeDeckGoal(goal);
70
763
  const { outFile, title = goal.title, subject, author } = options;
71
- const theme = resolveThemeConfig(goal.theme, goal.language);
764
+ const theme = resolveThemeConfig(goal.theme, goal.language, goal.colorScheme, goal.appearance);
72
765
  COLORS = theme.colors;
73
766
  FONTS = theme.fonts;
74
767
  CHART_COLORS = theme.chartColors;
768
+ // 注入 theme08 专属渲染器所需的状态(深浅模式随 appearance 切换)
769
+ if (goal.theme === 'theme08') {
770
+ configureTheme08({
771
+ colors: COLORS,
772
+ fonts: FONTS,
773
+ chartColors: CHART_COLORS,
774
+ muted: goal.appearance === 'muted' || goal.appearance === 'light',
775
+ });
776
+ }
777
+ // 注入 theme09 专属渲染器所需的状态(appearance 决定专色浓度)
778
+ if (goal.theme === 'theme09') {
779
+ configureTheme09({
780
+ appearance: goal.appearance === 'muted' || goal.appearance === 'light' ? 'muted' : 'primary',
781
+ fonts: FONTS,
782
+ chartColors: CHART_COLORS,
783
+ });
784
+ }
785
+ // 注入 theme10 专属渲染器所需的状态(appearance 决定强调色浓度)
786
+ if (goal.theme === 'theme10') {
787
+ configureTheme10({
788
+ appearance: goal.appearance === 'muted' || goal.appearance === 'light' ? 'muted' : 'primary',
789
+ fonts: FONTS,
790
+ chartColors: CHART_COLORS,
791
+ });
792
+ }
793
+ // Load gradient from snapshot
794
+ CURRENT_GRADIENT = getSnapshotGradient(goal.theme ?? 'theme01');
75
795
  const pptx = new PptxGenJS();
76
796
  pptx.layout = 'LAYOUT_16x9';
77
797
  pptx.title = title;
@@ -79,415 +799,1245 @@ export async function exportDeckToPptx(goal, options) {
79
799
  pptx.subject = subject;
80
800
  if (author)
81
801
  pptx.author = author;
82
- for (const slide of goal.slides) {
83
- const pptxSlide = pptx.addSlide();
802
+ for (const [slideIndex, slide] of goal.slides.entries()) {
803
+ let pptxSlide = pptx.addSlide();
804
+ if (process.env.DEBUG_PPTX_COLOR === '1') {
805
+ pptxSlide = wrapSlideForColorDebug(pptxSlide, slideIndex);
806
+ }
84
807
  pptxSlide.background = { color: COLORS.surface };
85
- renderSlideToPptx(pptxSlide, slide, goal.theme);
808
+ // theme07 冷白调研风:纯白底 + 亮色弥散光晕(优先,覆盖快照渐变)
809
+ if (goal.theme === 'theme07') {
810
+ addTheme07Background(pptxSlide);
811
+ }
812
+ else if (CURRENT_GRADIENT) {
813
+ addGradientBackground(pptxSlide, CURRENT_GRADIENT);
814
+ }
815
+ else if (goal.theme === 'theme02' || goal.theme === 'theme03') {
816
+ addTheme02Background(pptxSlide);
817
+ }
818
+ else if (goal.theme === 'theme04') {
819
+ addTheme04Background(pptxSlide);
820
+ }
821
+ else if (goal.theme === 'theme05') {
822
+ addTheme05Background(pptxSlide);
823
+ }
824
+ else if (goal.theme === 'theme06') {
825
+ addTheme06Background(pptxSlide);
826
+ }
827
+ renderSlideToPptx(pptxSlide, slide);
828
+ }
829
+ try {
830
+ await pptx.writeFile({ fileName: outFile });
831
+ }
832
+ finally {
833
+ cleanupTempImages();
834
+ cleanupTheme01TempImages();
835
+ }
836
+ }
837
+ /** 按 layoutId 索引的 PPTX 渲染器注册表。 */
838
+ const pptxRenderersByLayout = new Map();
839
+ function registerPptxLayoutRenderer(layoutId, renderFn) {
840
+ pptxRenderersByLayout.set(layoutId, renderFn);
841
+ }
842
+ function resolvePptxRenderer(slide) {
843
+ return pptxRenderersByLayout.get(slide.layout);
844
+ }
845
+ function renderSlideToPptx(pptxSlide, slide) {
846
+ // theme08 拥有专属 PPTX 渲染器,优先按 layoutId 精确分发,避免被下方 theme06 的 includes() 兜底误路由。
847
+ if (slide.layout.startsWith('theme08_')) {
848
+ const t08 = resolvePptxRenderer(slide);
849
+ if (t08) {
850
+ t08(pptxSlide, slide.props);
851
+ return;
852
+ }
853
+ }
854
+ // theme09 墨韵专色:双基底(纸/墨)按版式预分配,专属渲染器内部按 layout 取基底。
855
+ if (slide.layout.startsWith('theme09_')) {
856
+ const t09 = resolvePptxRenderer(slide);
857
+ if (t09) {
858
+ t09(pptxSlide, slide.props);
859
+ return;
860
+ }
861
+ }
862
+ // theme10 金色指数:墨黑金线底,专属渲染器内部按 layout 取强调色/基底。
863
+ if (slide.layout.startsWith('theme10_')) {
864
+ const t10 = resolvePptxRenderer(slide);
865
+ if (t10) {
866
+ t10(pptxSlide, slide.props);
867
+ return;
868
+ }
869
+ }
870
+ // theme01 浅色玻璃:双基底(light / tint)按版式预分配,专属渲染器内部按 layout 取基底。
871
+ if (slide.layout.startsWith('theme01_')) {
872
+ const t01 = resolvePptxRenderer(slide);
873
+ if (t01) {
874
+ t01(pptxSlide, slide.props);
875
+ return;
876
+ }
877
+ }
878
+ // theme06 所有子页面统一支持可选背景图(已有专属图片处理的版式除外)
879
+ const theme06ExplicitImageLayouts = new Set([
880
+ 'theme06_cover_v1',
881
+ 'theme06_chapter_v1',
882
+ 'theme06_closing_v1',
883
+ 'theme06_chapter_image_v1',
884
+ ]);
885
+ if (slide.layout.startsWith('theme06_') &&
886
+ slide.props?.imageUrl &&
887
+ !theme06ExplicitImageLayouts.has(slide.layout)) {
888
+ addImageMaybe(pptxSlide, slide.props.imageUrl, 0, 0, 10, 5.625);
889
+ }
890
+ // 检查是否有坐标配置 - 如果有,使用 v2 坐标系统
891
+ // 支持 theme06 专属版式坐标(带 _themeSpecific 标记)
892
+ const coords = getLayoutCoordinates(slide.layout);
893
+ const hasCoords = Object.keys(coords).length > 0;
894
+ const isTheme06Specific = coords._themeSpecific === true;
895
+ const layoutId = slide.layout;
896
+ // 仅对"标准 layout ID"或"theme06 专属版式"启用 v2 渲染
897
+ const standardLayoutPattern = /^theme\d+_(cover|content|metric|table_of_contents)_v\d+$/;
898
+ const isStandardLayout = standardLayoutPattern.test(layoutId);
899
+ const shouldUseV2 = (hasCoords && isStandardLayout) || (hasCoords && isTheme06Specific);
900
+ if (shouldUseV2) {
901
+ // 根据布局类型选择对应的 v2 渲染函数
902
+ if (layoutId.includes('cover_product') || layoutId.includes('cover_business') || layoutId.includes('cover_manufacturing')) {
903
+ renderTheme06CoverVariant(pptxSlide, slide.props, layoutId);
904
+ return;
905
+ }
906
+ if (layoutId.includes('cover')) {
907
+ // Theme06 cover layouts use their own renderer with different prop structure
908
+ if (layoutId.startsWith('theme06_')) {
909
+ renderTheme06Cover(pptxSlide, slide.props, layoutId);
910
+ }
911
+ else {
912
+ renderCoverV2(pptxSlide, slide.props, layoutId);
913
+ }
914
+ return;
915
+ }
916
+ else if (layoutId.includes('content_numbered')) {
917
+ renderTheme06ContentNumbered(pptxSlide, slide.props, layoutId);
918
+ return;
919
+ }
920
+ else if (layoutId.includes('content')) {
921
+ renderContentWithCoords(pptxSlide, slide.props, layoutId);
922
+ return;
923
+ }
924
+ else if (layoutId.includes('metric_showcase')) {
925
+ renderTheme06MetricShowcase(pptxSlide, slide.props, layoutId);
926
+ return;
927
+ }
928
+ else if (layoutId.includes('metric')) {
929
+ renderMetricWithCoords(pptxSlide, slide.props, layoutId);
930
+ return;
931
+ }
932
+ else if (layoutId.includes('table_of_contents')) {
933
+ renderTheme06Toc(pptxSlide, slide.props, layoutId);
934
+ return;
935
+ }
936
+ else if (layoutId.includes('toc')) {
937
+ renderTheme06Toc(pptxSlide, slide.props, layoutId);
938
+ return;
939
+ }
940
+ else if (layoutId.includes('big_number')) {
941
+ renderTheme06BigNumber(pptxSlide, slide.props, layoutId);
942
+ return;
943
+ }
944
+ else if (layoutId.includes('chapter_numbered') || layoutId.includes('chapter_split') || layoutId.includes('chapter_focus') || layoutId.includes('chapter_image') || layoutId.includes('chapter_minimal')) {
945
+ renderTheme06Chapter(pptxSlide, slide.props, layoutId);
946
+ return;
947
+ }
948
+ else if (layoutId.includes('chapter')) {
949
+ renderTheme06Chapter(pptxSlide, slide.props, layoutId);
950
+ return;
951
+ }
952
+ else if (layoutId.includes('closing')) {
953
+ renderTheme06Closing(pptxSlide, slide.props, layoutId);
954
+ return;
955
+ }
956
+ else if (layoutId.includes('timeline')) {
957
+ renderTheme06Timeline(pptxSlide, slide.props, layoutId);
958
+ return;
959
+ }
960
+ else if (layoutId.includes('milestone')) {
961
+ renderTheme06Milestone(pptxSlide, slide.props, layoutId);
962
+ return;
963
+ }
964
+ else if (layoutId.includes('risk_matrix')) {
965
+ renderTheme06RiskMatrix(pptxSlide, slide.props, layoutId);
966
+ return;
967
+ }
968
+ else if (layoutId.includes('summary')) {
969
+ renderTheme06Summary(pptxSlide, slide.props, layoutId);
970
+ return;
971
+ }
972
+ else if (layoutId.includes('quote')) {
973
+ renderTheme06Quote(pptxSlide, slide.props, layoutId);
974
+ return;
975
+ }
976
+ else if (layoutId.includes('process')) {
977
+ renderTheme06Process(pptxSlide, slide.props, layoutId);
978
+ return;
979
+ }
980
+ else if (layoutId.includes('method')) {
981
+ renderTheme06Process(pptxSlide, slide.props, layoutId);
982
+ return;
983
+ }
984
+ else if (layoutId.includes('outlook')) {
985
+ renderTheme06ListBased(pptxSlide, slide.props, layoutId);
986
+ return;
987
+ }
988
+ else if (layoutId.includes('recap')) {
989
+ renderTheme06ListBased(pptxSlide, slide.props, layoutId);
990
+ return;
991
+ }
992
+ else if (layoutId.includes('statement')) {
993
+ renderTheme06Statement(pptxSlide, slide.props, layoutId);
994
+ return;
995
+ }
996
+ else if (layoutId.includes('sources')) {
997
+ renderTheme06Sources(pptxSlide, slide.props, layoutId);
998
+ return;
999
+ }
1000
+ else if (layoutId.includes('matrix')) {
1001
+ renderTheme06Matrix(pptxSlide, slide.props, layoutId);
1002
+ return;
1003
+ }
1004
+ else if (layoutId.includes('rank')) {
1005
+ renderTheme06Rank(pptxSlide, slide.props, layoutId);
1006
+ return;
1007
+ }
1008
+ else if (layoutId.includes('comparison')) {
1009
+ renderTheme06Comparison(pptxSlide, slide.props, layoutId);
1010
+ return;
1011
+ }
1012
+ else if (layoutId.includes('bento')) {
1013
+ renderTheme06Bento(pptxSlide, slide.props, layoutId);
1014
+ return;
1015
+ }
1016
+ else if (layoutId.includes('triad')) {
1017
+ renderTheme06Triad(pptxSlide, slide.props, layoutId);
1018
+ return;
1019
+ }
1020
+ else if (layoutId.includes('tech_landscape')) {
1021
+ renderTheme06TechLandscape(pptxSlide, slide.props, layoutId);
1022
+ return;
1023
+ }
1024
+ else if (layoutId.includes('chain_flow')) {
1025
+ renderTheme06ChainFlow(pptxSlide, slide.props, layoutId);
1026
+ return;
1027
+ }
1028
+ else if (layoutId.includes('deal_map')) {
1029
+ renderTheme06DealMap(pptxSlide, slide.props, layoutId);
1030
+ return;
1031
+ }
1032
+ else if (layoutId.includes('sector_spotlight')) {
1033
+ renderTheme06SectorSpotlight(pptxSlide, slide.props, layoutId);
1034
+ return;
1035
+ }
1036
+ else if (layoutId.includes('company_profile')) {
1037
+ renderTheme06CompanyProfile(pptxSlide, slide.props, layoutId);
1038
+ return;
1039
+ }
1040
+ else if (layoutId.includes('company_spotlight')) {
1041
+ renderTheme06CompanySpotlight(pptxSlide, slide.props, layoutId);
1042
+ return;
1043
+ }
1044
+ else if (layoutId.includes('case')) {
1045
+ renderTheme06Case(pptxSlide, slide.props, layoutId);
1046
+ return;
1047
+ }
1048
+ }
1049
+ // 否则使用原有渲染器
1050
+ const renderer = resolvePptxRenderer(slide);
1051
+ if (renderer) {
1052
+ renderer(pptxSlide, slide.props);
1053
+ return;
1054
+ }
1055
+ pptxSlide.addText(`Unknown layout: ${slide.layout}`, {
1056
+ x: 0.5, y: 3.5, w: 9, h: 1,
1057
+ fontSize: 18, color: 'EF4444', align: 'center',
1058
+ });
1059
+ }
1060
+ // ---- Layout Coordinate System (v2) ---------------------------------------
1061
+ /**
1062
+ * 通用元素渲染函数 - 从坐标配置读取位置和样式
1063
+ * 这是解决版式错位问题的核心:所有布局坐标都从配置文件读取,
1064
+ * 而不是在渲染函数中硬编码。
1065
+ */
1066
+ export function renderElementFromConfig(slide, text, config, options) {
1067
+ if (!text)
1068
+ return false;
1069
+ const textOptions = {
1070
+ x: config.x,
1071
+ y: config.y,
1072
+ w: config.w || 8,
1073
+ h: config.h || 0.4,
1074
+ fontSize: config.fontSize || 18,
1075
+ align: config.align || 'left',
1076
+ bold: config.bold || false,
1077
+ color: options?.color || COLORS.primary,
1078
+ fontFace: options?.fontFace || FONTS.body,
1079
+ };
1080
+ slide.addText(text, textOptions);
1081
+ return true;
1082
+ }
1083
+ /**
1084
+ * 使用坐标配置渲染封面布局
1085
+ * 这是新系统的示例实现
1086
+ */
1087
+ function renderCoverV2(slide, props, layoutId) {
1088
+ const coords = getLayoutCoordinates(layoutId);
1089
+ // 背景图
1090
+ if (props.image) {
1091
+ const imgConfig = coords.image || { x: 0, y: 0, w: 10, h: 5.625 };
1092
+ addImageMaybe(slide, props.image, imgConfig.x, imgConfig.y, imgConfig.w, imgConfig.h);
1093
+ }
1094
+ // Kicker
1095
+ if (props.kicker && coords.kicker) {
1096
+ renderElementFromConfig(slide, props.kicker, coords.kicker, {
1097
+ color: COLORS.accent,
1098
+ fontFace: FONTS.mono,
1099
+ });
1100
+ }
1101
+ // Title
1102
+ if (coords.title) {
1103
+ renderElementFromConfig(slide, props.title, coords.title, {
1104
+ color: COLORS.primary,
1105
+ fontFace: FONTS.heading,
1106
+ });
1107
+ }
1108
+ // Subtitle
1109
+ if (props.subtitle && coords.subtitle) {
1110
+ renderElementFromConfig(slide, props.subtitle, coords.subtitle, {
1111
+ color: COLORS.secondary,
1112
+ fontFace: FONTS.body,
1113
+ });
1114
+ }
1115
+ // Date
1116
+ if (props.date && coords.date) {
1117
+ renderElementFromConfig(slide, props.date, coords.date, {
1118
+ color: COLORS.secondary,
1119
+ fontFace: FONTS.mono,
1120
+ });
1121
+ }
1122
+ }
1123
+ /** 使用坐标配置渲染内容布局 (v2 坐标系统) */
1124
+ function renderContentWithCoords(slide, props, layoutId) {
1125
+ const coords = getLayoutCoordinates(layoutId);
1126
+ // Kicker
1127
+ if (props.kicker && coords.kicker) {
1128
+ renderElementFromConfig(slide, props.kicker, coords.kicker, {
1129
+ color: COLORS.accent,
1130
+ fontFace: FONTS.mono,
1131
+ });
1132
+ }
1133
+ // Title
1134
+ if (coords.title) {
1135
+ renderElementFromConfig(slide, props.title, coords.title, {
1136
+ color: COLORS.primary,
1137
+ fontFace: FONTS.heading,
1138
+ });
1139
+ }
1140
+ // Body text
1141
+ if (props.body && coords.body) {
1142
+ renderElementFromConfig(slide, props.body, coords.body, {
1143
+ color: COLORS.primary,
1144
+ fontFace: FONTS.body,
1145
+ });
1146
+ }
1147
+ // Bullets
1148
+ if (props.bullets && props.bullets.length > 0 && coords.bulletItem) {
1149
+ const bulletConfig = coords.bulletItem;
1150
+ let y = (coords.body?.y || 2.5) + (coords.body?.h || 2.5) + 0.3;
1151
+ props.bullets.forEach((bullet) => {
1152
+ // Bullet number/marker
1153
+ slide.addText('•', {
1154
+ x: bulletConfig.indentX,
1155
+ y,
1156
+ w: 0.3,
1157
+ h: bulletConfig.height,
1158
+ fontSize: bulletConfig.fontSize,
1159
+ color: COLORS.accent,
1160
+ align: 'left',
1161
+ });
1162
+ // Bullet text
1163
+ slide.addText(bullet, {
1164
+ x: bulletConfig.textStartX,
1165
+ y,
1166
+ w: 8,
1167
+ h: bulletConfig.height,
1168
+ fontSize: bulletConfig.fontSize,
1169
+ color: COLORS.primary,
1170
+ align: 'left',
1171
+ fontFace: FONTS.body,
1172
+ });
1173
+ y += bulletConfig.height + 0.1;
1174
+ });
1175
+ }
1176
+ }
1177
+ /** 使用坐标配置渲染指标布局 (v2 坐标系统) */
1178
+ function renderMetricWithCoords(slide, props, layoutId) {
1179
+ const coords = getLayoutCoordinates(layoutId);
1180
+ // Label/Kicker
1181
+ if (props.kicker && coords.label) {
1182
+ renderElementFromConfig(slide, props.kicker, coords.label, {
1183
+ color: COLORS.accent,
1184
+ fontFace: FONTS.mono,
1185
+ });
1186
+ }
1187
+ // Title
1188
+ if (props.title && coords.title) {
1189
+ renderElementFromConfig(slide, props.title, coords.title, {
1190
+ color: COLORS.primary,
1191
+ fontFace: FONTS.heading,
1192
+ });
1193
+ }
1194
+ // Main value
1195
+ if (props.value && coords.value) {
1196
+ const valueText = props.unit ? `${props.value}${props.unit}` : props.value;
1197
+ renderElementFromConfig(slide, valueText, coords.value, {
1198
+ color: COLORS.accent,
1199
+ fontFace: FONTS.heading,
1200
+ });
1201
+ }
1202
+ // Description
1203
+ if (props.description && coords.description) {
1204
+ renderElementFromConfig(slide, props.description, coords.description, {
1205
+ color: COLORS.secondary,
1206
+ fontFace: FONTS.body,
1207
+ });
1208
+ }
1209
+ }
1210
+ // ---- Shared helpers --------------------------------------------------------
1211
+ export function addKicker(slide, kicker, x = 0.8, y = 0.8) {
1212
+ if (!kicker)
1213
+ return;
1214
+ slide.addText(kicker, {
1215
+ x, y, w: 8.4, h: 0.4,
1216
+ fontSize: 14, color: COLORS.accent, align: 'left',
1217
+ fontFace: FONTS.mono,
1218
+ });
1219
+ }
1220
+ /** 通用渐变背景函数:从快照配置中读取渐变参数 */
1221
+ function addGradientBackground(slide, gradient) {
1222
+ slide.addShape('rect', {
1223
+ x: 0, y: 0, w: 10, h: 5.625,
1224
+ fill: {
1225
+ color: COLORS.surface,
1226
+ gradient: {
1227
+ type: 'linear',
1228
+ angle: gradient.angle,
1229
+ stops: gradient.stops,
1230
+ },
1231
+ },
1232
+ });
1233
+ }
1234
+ /** 为 theme02 幻灯片添加深色霓虹渐变背景。 */
1235
+ function addTheme02Background(slide) {
1236
+ slide.addShape('rect', {
1237
+ x: 0, y: 0, w: 10, h: 5.625,
1238
+ fill: {
1239
+ color: COLORS.surface,
1240
+ gradient: {
1241
+ type: 'linear',
1242
+ angle: 160,
1243
+ stops: [
1244
+ { position: 0, color: COLORS.white },
1245
+ { position: 1, color: COLORS.light },
1246
+ ],
1247
+ },
1248
+ },
1249
+ });
1250
+ }
1251
+ /** 为 theme04 幻灯片添加玻璃糖果渐变背景。 */
1252
+ export function addTheme04Background(slide) {
1253
+ slide.addShape('rect', {
1254
+ x: 0, y: 0, w: 10, h: 5.625,
1255
+ fill: {
1256
+ color: COLORS.surface,
1257
+ gradient: {
1258
+ type: 'linear',
1259
+ angle: 155,
1260
+ stops: [
1261
+ { position: 0, color: COLORS.white },
1262
+ { position: 1, color: COLORS.light },
1263
+ ],
1264
+ },
1265
+ },
1266
+ });
1267
+ // 右上角糖果色弥散光晕
1268
+ slide.addShape('ellipse', {
1269
+ x: 7.2, y: -0.8, w: 3.8, h: 2.6,
1270
+ fill: {
1271
+ color: COLORS.accent,
1272
+ transparency: 88,
1273
+ },
1274
+ });
1275
+ // 左下角冷色光晕
1276
+ slide.addShape('ellipse', {
1277
+ x: -1.2, y: 4.0, w: 3.4, h: 2.2,
1278
+ fill: {
1279
+ color: '4ECDC4',
1280
+ transparency: 90,
1281
+ },
1282
+ });
1283
+ }
1284
+ /** 为 theme05 幻灯片添加纸白/深色渐变背景与光谱色带。 */
1285
+ function addTheme05Background(slide) {
1286
+ slide.addShape('rect', {
1287
+ x: 0, y: 0, w: 10, h: 5.625,
1288
+ fill: {
1289
+ color: COLORS.surface,
1290
+ gradient: {
1291
+ type: 'linear',
1292
+ angle: 170,
1293
+ stops: [
1294
+ { position: 0, color: COLORS.white },
1295
+ { position: 1, color: COLORS.light },
1296
+ ],
1297
+ },
1298
+ },
1299
+ });
1300
+ }
1301
+ /** 在 theme05 幻灯片底部绘制光谱色带。 */
1302
+ export function addTheme05SpectrumBar(slide) {
1303
+ const colors = ['E85D4E', 'F5A623', '0FA3B1', '4A58D9', '7C3AED'];
1304
+ const barY = 5.18;
1305
+ const barH = 0.06;
1306
+ const segmentW = 10 / colors.length;
1307
+ colors.forEach((color, idx) => {
1308
+ slide.addShape('rect', {
1309
+ x: idx * segmentW, y: barY, w: segmentW, h: barH,
1310
+ fill: { color },
1311
+ });
1312
+ });
1313
+ }
1314
+ /** 为 theme06 幻灯片添加深色图谱渐变背景。 */
1315
+ function addTheme06Background(slide) {
1316
+ slide.addShape('rect', {
1317
+ x: 0, y: 0, w: 10, h: 5.625,
1318
+ fill: {
1319
+ color: COLORS.surface,
1320
+ gradient: {
1321
+ type: 'linear',
1322
+ angle: 160,
1323
+ stops: [
1324
+ { position: 0, color: COLORS.white },
1325
+ { position: 1, color: COLORS.light },
1326
+ ],
1327
+ },
1328
+ },
1329
+ });
1330
+ }
1331
+ /** theme07 冷白调研风:纯白底 + 亮色弥散光晕背景。 */
1332
+ function addTheme07Background(slide) {
1333
+ const W = 10;
1334
+ const H = 5.625;
1335
+ // 纯白底(覆盖默认 surface,确保无灰偏)
1336
+ slide.addShape('rect', { x: 0, y: 0, w: W, h: H, fill: { color: 'FFFFFF' }, line: { type: 'none' } });
1337
+ // 右上角蓝色弥散光晕
1338
+ slide.addShape('ellipse', {
1339
+ x: 7.2, y: -1.7, w: 5.0, h: 3.9,
1340
+ fill: { color: COLORS.accent, transparency: 82 },
1341
+ line: { type: 'none' },
1342
+ });
1343
+ // 右侧绿色弥散光晕
1344
+ slide.addShape('ellipse', {
1345
+ x: 6.4, y: 1.0, w: 3.6, h: 3.1,
1346
+ fill: { color: COLORS.accent2, transparency: 86 },
1347
+ line: { type: 'none' },
1348
+ });
1349
+ // 暖橙色小光晕
1350
+ slide.addShape('ellipse', {
1351
+ x: 5.4, y: 2.6, w: 2.9, h: 2.5,
1352
+ fill: { color: COLORS.accentCool, transparency: 88 },
1353
+ line: { type: 'none' },
1354
+ });
1355
+ }
1356
+ /** 在 theme06 幻灯片底部绘制霓虹装饰线。 */
1357
+ export function addTheme06GlowLine(slide) {
1358
+ const barY = 5.18;
1359
+ const barH = 0.04;
1360
+ const stops = [
1361
+ { position: 0, color: COLORS.white },
1362
+ { position: 0.35, color: COLORS.accent },
1363
+ { position: 0.65, color: COLORS.secondary },
1364
+ { position: 1, color: COLORS.white },
1365
+ ];
1366
+ slide.addShape('rect', {
1367
+ x: 0, y: barY, w: 10, h: barH,
1368
+ fill: {
1369
+ color: COLORS.accent,
1370
+ gradient: {
1371
+ type: 'linear',
1372
+ angle: 90,
1373
+ stops,
1374
+ },
1375
+ },
1376
+ });
1377
+ }
1378
+ function splitBilingual(text) {
1379
+ if (!text)
1380
+ return {};
1381
+ const parts = text.split(' / ');
1382
+ if (parts.length >= 2)
1383
+ return { cn: parts[0], en: parts.slice(1).join(' / ') };
1384
+ return { cn: text };
1385
+ }
1386
+ export function addTheme06FooterBilingual(slide, footnote) {
1387
+ const parts = splitBilingual(footnote);
1388
+ if (parts.cn) {
1389
+ slide.addText(parts.cn, {
1390
+ x: 0.65, y: 5.08, w: 4.5, h: 0.22,
1391
+ fontSize: 10, color: COLORS.secondary, fontFace: FONTS.heading, bold: true,
1392
+ });
1393
+ }
1394
+ if (parts.en) {
1395
+ slide.addText(parts.en, {
1396
+ x: 5.15, y: 5.08, w: 4.2, h: 0.22,
1397
+ fontSize: 10, color: COLORS.secondary, fontFace: FONTS.mono, bold: true, align: 'right',
1398
+ });
1399
+ }
1400
+ }
1401
+ /** theme06 标准数据卡片:实色表面 + 细边框。 */
1402
+ export function addTheme06Card(slide, x, y, w, h, accent = false) {
1403
+ slide.addShape('roundRect', {
1404
+ x, y, w, h,
1405
+ fill: { color: accent ? COLORS.accent : COLORS.surfaceElevated },
1406
+ line: { color: accent ? COLORS.accent : COLORS.border, width: 1 },
1407
+ rectRadius: 0.08,
1408
+ });
1409
+ }
1410
+ /** theme06 霓虹徽章(圆角胶囊)。 */
1411
+ export function addTheme06Badge(slide, x, y, w, h, text) {
1412
+ slide.addShape('roundRect', {
1413
+ x, y, w, h,
1414
+ fill: { color: COLORS.accent },
1415
+ rectRadius: h / 2,
1416
+ });
1417
+ slide.addText(text, {
1418
+ x, y, w, h,
1419
+ fontSize: 10, color: COLORS.white, bold: true, align: 'center', valign: 'middle', fontFace: FONTS.mono,
1420
+ });
1421
+ }
1422
+ /** theme06 进度条卡片。 */
1423
+ export function addTheme06ProgressCard(slide, x, y, w, h, label, value, progress) {
1424
+ addTheme06Card(slide, x, y, w, h);
1425
+ slide.addText(label, {
1426
+ x: x + 0.12, y: y + 0.1, w: w - 0.24, h: 0.24,
1427
+ fontSize: 11, color: COLORS.primary, bold: true, fontFace: FONTS.body,
1428
+ });
1429
+ slide.addText(value, {
1430
+ x: x + 0.12, y: y + 0.32, w: w - 0.24, h: 0.32,
1431
+ fontSize: 20, color: COLORS.accent, bold: true, align: 'right', fontFace: FONTS.heading,
1432
+ });
1433
+ const trackY = y + h - 0.18;
1434
+ slide.addShape('roundRect', {
1435
+ x: x + 0.12, y: trackY, w: w - 0.24, h: 0.06,
1436
+ fill: { color: COLORS.white },
1437
+ rectRadius: 0.03,
1438
+ });
1439
+ const fillW = Math.max(0, Math.min(1, progress)) * (w - 0.24);
1440
+ if (fillW > 0) {
1441
+ slide.addShape('roundRect', {
1442
+ x: x + 0.12, y: trackY, w: fillW, h: 0.06,
1443
+ fill: { color: COLORS.accent },
1444
+ rectRadius: 0.03,
1445
+ });
1446
+ }
1447
+ }
1448
+ /** theme06 时间轴节点与连线。 */
1449
+ export function addTheme06TimelineNode(slide, cx, cy, label, date, focus = false) {
1450
+ slide.addShape('ellipse', {
1451
+ x: cx - 0.15, y: cy - 0.15, w: 0.3, h: 0.3,
1452
+ fill: { color: focus ? COLORS.accent : COLORS.surfaceElevated },
1453
+ line: { color: COLORS.accent, width: 2 },
1454
+ });
1455
+ slide.addText(date, {
1456
+ x: cx - 0.5, y: cy - 0.55, w: 1.0, h: 0.22,
1457
+ fontSize: 9, color: COLORS.accent, bold: true, align: 'center', fontFace: FONTS.mono,
1458
+ });
1459
+ slide.addText(label, {
1460
+ x: cx - 0.55, y: cy + 0.22, w: 1.1, h: 0.22,
1461
+ fontSize: 10, color: COLORS.primary, bold: true, align: 'center', fontFace: FONTS.body,
1462
+ });
1463
+ }
1464
+ /** theme06 2×2 象限网格(虚线十字)。 */
1465
+ export function addTheme06QuadrantGrid(slide, x, y, w, h) {
1466
+ addTheme06Card(slide, x, y, w, h);
1467
+ const midX = x + w / 2;
1468
+ const midY = y + h / 2;
1469
+ const dash = 0.06;
1470
+ const gap = 0.04;
1471
+ for (let cy = y + 0.1; cy < y + h - 0.1; cy += dash + gap) {
1472
+ slide.addShape('rect', {
1473
+ x: midX - 0.01, y: cy, w: 0.02, h: Math.min(dash, y + h - 0.1 - cy),
1474
+ fill: { color: COLORS.border },
1475
+ });
1476
+ }
1477
+ for (let cx = x + 0.1; cx < x + w - 0.1; cx += dash + gap) {
1478
+ slide.addShape('rect', {
1479
+ x: cx, y: midY - 0.01, w: Math.min(dash, x + w - 0.1 - cx), h: 0.02,
1480
+ fill: { color: COLORS.border },
1481
+ });
1482
+ }
1483
+ }
1484
+ /** theme06 气泡节点。 */
1485
+ export function addTheme06Bubble(slide, x, y, size, label, color) {
1486
+ slide.addShape('ellipse', {
1487
+ x: x - size / 2, y: y - size / 2, w: size, h: size,
1488
+ fill: { color, transparency: 84 },
1489
+ line: { color, width: 1 },
1490
+ });
1491
+ slide.addText(label, {
1492
+ x: x - size / 2, y: y - 0.15, w: size, h: 0.3,
1493
+ fontSize: 8, color: COLORS.primary, bold: true, align: 'center', valign: 'middle', fontFace: FONTS.body,
1494
+ });
1495
+ }
1496
+ /** theme06 流程步骤卡片(带输入/输出标签)。 */
1497
+ export function addTheme06StepCard(slide, x, y, w, h, step, accent = false) {
1498
+ addTheme06Card(slide, x, y, w, h, accent);
1499
+ slide.addText(step.title ?? '', {
1500
+ x: x + 0.1, y: y + 0.1, w: w - 0.2, h: 0.26,
1501
+ fontSize: 11, color: accent ? COLORS.white : COLORS.primary, bold: true, align: 'center', fontFace: FONTS.body,
1502
+ });
1503
+ if (step.description) {
1504
+ slide.addText(step.description, {
1505
+ x: x + 0.1, y: y + 0.36, w: w - 0.2, h: 0.5,
1506
+ fontSize: 9, color: accent ? 'FFFFFF' : COLORS.secondary, align: 'center', valign: 'top', fontFace: FONTS.body,
1507
+ });
1508
+ }
1509
+ if (step.input || step.output) {
1510
+ const ioText = [step.input ? `IN: ${step.input}` : '', step.output ? `OUT: ${step.output}` : ''].filter(Boolean).join(' / ');
1511
+ slide.addText(ioText, {
1512
+ x: x + 0.1, y: y + h - 0.32, w: w - 0.2, h: 0.22,
1513
+ fontSize: 8, color: accent ? 'FFFFFF' : COLORS.secondary, align: 'center', valign: 'middle', fontFace: FONTS.mono,
1514
+ });
86
1515
  }
87
- await pptx.writeFile({ fileName: outFile });
88
1516
  }
89
- /** 通用共享版式的主题标识;旧版式中仍可能使用 'base',两者等价。 */
90
- const UNIVERSAL_THEME = '*';
91
- /** 与 UNIVERSAL_THEME 等价的历史主题标识,用于兼容旧版式。 */
92
- const LEGACY_UNIVERSAL_THEME = 'base';
93
- function normalizeUniversalTheme(theme) {
94
- return theme === LEGACY_UNIVERSAL_THEME ? UNIVERSAL_THEME : theme;
1517
+ /** theme05 标准数据卡片:实色表面 + 细边框。 */
1518
+ export function addTheme05Card(slide, x, y, w, h, accent = false) {
1519
+ slide.addShape('roundRect', {
1520
+ x, y, w, h,
1521
+ fill: { color: accent ? COLORS.accent : COLORS.surfaceElevated },
1522
+ line: { color: accent ? COLORS.accent : COLORS.border, width: 1 },
1523
+ rectRadius: 0.08,
1524
+ });
1525
+ }
1526
+ /** theme05 scheme 颜色到固定 hex 的映射(与运行时 CSS 变量对应)。 */
1527
+ const THEME05_SCHEME_COLORS = {
1528
+ coral: 'E85D4E',
1529
+ amber: 'F5A623',
1530
+ teal: '0FA3B1',
1531
+ indigo: '4A58D9',
1532
+ violet: '7C3AED',
1533
+ };
1534
+ export function theme05SchemeColor(scheme) {
1535
+ return THEME05_SCHEME_COLORS[scheme || 'coral'] || COLORS.accent;
95
1536
  }
96
- /** layoutId 索引的 PPTX 渲染器注册表。 */
97
- const pptxRenderersByLayout = new Map();
98
- /** 按 (role, theme) 二维索引的 PPTX 渲染器注册表,支持主题专属覆盖。 */
99
- const pptxRenderersByRoleTheme = new Map();
100
- function registerPptxLayoutRenderer(layoutId, renderFn) {
101
- pptxRenderersByLayout.set(layoutId, renderFn);
1537
+ /** theme05 小胶囊(用于高亮标签)。 */
1538
+ export function addTheme05Pill(slide, x, y, w, h, text, color) {
1539
+ slide.addShape('roundRect', {
1540
+ x, y, w, h,
1541
+ fill: { color },
1542
+ rectRadius: h / 2,
1543
+ });
1544
+ slide.addText(text, {
1545
+ x, y: y + 0.02, w, h: h - 0.02,
1546
+ fontSize: 11, color: 'FFFFFF', bold: true, align: 'center', valign: 'middle', fontFace: FONTS.body,
1547
+ });
102
1548
  }
103
- /** 注册主题专属 PPTX 渲染器(按 role + theme 覆盖默认 layout 渲染器)。 */
104
- export function registerPptxRoleRenderer(role, theme, renderFn) {
105
- const normalizedTheme = normalizeUniversalTheme(theme);
106
- if (!pptxRenderersByRoleTheme.has(role)) {
107
- pptxRenderersByRoleTheme.set(role, new Map());
1549
+ /** theme05 结论卡片(右侧)。 */
1550
+ export function addTheme05ConclusionCard(slide, x, y, w, h, conclusion) {
1551
+ slide.addShape('roundRect', {
1552
+ x, y, w, h,
1553
+ fill: { color: COLORS.surfaceElevated },
1554
+ line: { color: COLORS.border, width: 1 },
1555
+ rectRadius: 0.12,
1556
+ });
1557
+ let cursorY = y + 0.18;
1558
+ if (conclusion?.value) {
1559
+ slide.addText(conclusion.value, {
1560
+ x: x + 0.16, y: cursorY, w: w - 0.32, h: 0.55,
1561
+ fontSize: 30, color: COLORS.accent, bold: true, fontFace: FONTS.heading,
1562
+ });
1563
+ cursorY += 0.55;
108
1564
  }
109
- pptxRenderersByRoleTheme.get(role).set(normalizedTheme, renderFn);
110
- }
111
- function resolvePptxRenderer(slide, theme) {
112
- // 1. 优先查找主题专属覆盖
113
- if (theme) {
114
- const byTheme = pptxRenderersByRoleTheme.get(slide.role);
115
- const normalized = normalizeUniversalTheme(theme);
116
- const exact = byTheme?.get(normalized);
117
- if (exact) {
118
- return exact;
119
- }
1565
+ if (conclusion?.label) {
1566
+ slide.addText(conclusion.label, {
1567
+ x: x + 0.16, y: cursorY, w: w - 0.32, h: 0.22,
1568
+ fontSize: 10, color: COLORS.secondary, fontFace: FONTS.body,
1569
+ });
1570
+ cursorY += 0.26;
120
1571
  }
121
- // 2. 否则按具体 layoutId 查找通用渲染器
122
- return pptxRenderersByLayout.get(slide.layout);
123
- }
124
- function renderSlideToPptx(pptxSlide, slide, theme) {
125
- const renderer = resolvePptxRenderer(slide, theme);
126
- if (renderer) {
127
- renderer(pptxSlide, slide.props);
128
- return;
1572
+ if (conclusion?.description) {
1573
+ slide.addText(conclusion.description, {
1574
+ x: x + 0.16, y: cursorY, w: w - 0.32, h: h - (cursorY - y) - 0.2,
1575
+ fontSize: 10, color: COLORS.secondary, valign: 'top', fontFace: FONTS.body,
1576
+ });
129
1577
  }
130
- pptxSlide.addText(`Unknown layout: ${slide.layout}`, {
131
- x: 0.5, y: 3.5, w: 9, h: 1,
132
- fontSize: 18, color: 'EF4444', align: 'center',
133
- });
134
1578
  }
135
- // ---- Shared helpers --------------------------------------------------------
136
- function addKicker(slide, kicker, x = 0.8, y = 0.8) {
137
- if (!kicker)
138
- return;
139
- slide.addText(kicker, {
140
- x, y, w: 8.4, h: 0.4,
141
- fontSize: 14, color: COLORS.accent, align: 'left',
142
- fontFace: FONTS.mono,
1579
+ /** theme02 玻璃卡片:深色表面 + 细边框 + 柔和阴影。 */
1580
+ export function addTheme02Card(slide, x, y, w, h, radius = 0.15) {
1581
+ slide.addShape('roundRect', {
1582
+ x, y, w, h,
1583
+ fill: { color: COLORS.surfaceElevated },
1584
+ line: { color: COLORS.border, width: 1 },
1585
+ rectRadius: radius,
1586
+ shadow: {
1587
+ type: 'outer',
1588
+ blur: 8,
1589
+ offset: 4,
1590
+ angle: 45,
1591
+ color: '000000',
1592
+ opacity: 0.35,
1593
+ },
143
1594
  });
144
1595
  }
145
- function addTitle(slide, title, x = 0.8, y = 1.3, w = 8.4, h = 0.9, fontSize = 44) {
1596
+ export function addTitle(slide, title, x = 0.8, y = 1.3, w = 8.4, h = 0.9, fontSize = 44) {
146
1597
  slide.addText(title, {
147
1598
  x, y, w, h,
148
1599
  fontSize, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
149
1600
  fontFace: FONTS.heading,
150
1601
  });
151
1602
  }
152
- function addBulletList(slide, items, x, y, w, maxItems = 8) {
153
- const list = items.slice(0, maxItems);
154
- let cy = y;
155
- for (const item of list) {
156
- slide.addShape('ellipse', {
157
- x: x + 0.05, y: cy + 0.16, w: 0.1, h: 0.1,
158
- fill: { color: COLORS.accent },
1603
+ export function renderInsightPanel(slide, insight, x, y, w, h) {
1604
+ if (!insight || (!insight.value && !insight.label && !insight.description))
1605
+ return;
1606
+ slide.addShape('roundRect', {
1607
+ x, y, w, h,
1608
+ fill: { color: COLORS.surfaceElevated },
1609
+ line: { color: COLORS.border, width: 1 },
1610
+ rectRadius: 0.15,
1611
+ });
1612
+ let cursorY = y + 0.25;
1613
+ if (insight.value) {
1614
+ slide.addText(insight.value, {
1615
+ x: x + 0.2, y: cursorY, w: w - 0.4, h: 0.65,
1616
+ fontSize: 36, color: COLORS.accent, bold: true, align: 'left', valign: 'top',
1617
+ fontFace: FONTS.heading,
159
1618
  });
160
- slide.addText(item, {
161
- x: x + 0.3, y: cy, w: w - 0.3, h: 0.55,
162
- fontSize: 20, color: COLORS.primary, align: 'left', valign: 'top',
1619
+ cursorY += 0.62;
1620
+ }
1621
+ if (insight.label) {
1622
+ slide.addText(insight.label, {
1623
+ x: x + 0.2, y: cursorY, w: w - 0.4, h: 0.3,
1624
+ fontSize: 11, color: COLORS.secondary, align: 'left', valign: 'top',
1625
+ fontFace: FONTS.body,
1626
+ });
1627
+ cursorY += 0.38;
1628
+ }
1629
+ if (insight.description) {
1630
+ slide.addText(insight.description, {
1631
+ x: x + 0.2, y: cursorY, w: w - 0.4, h: h - (cursorY - y) - 0.25,
1632
+ fontSize: 12, color: COLORS.secondary, align: 'left', valign: 'top',
163
1633
  fontFace: FONTS.body,
164
1634
  });
165
- cy += 0.7;
166
1635
  }
167
- return cy;
168
1636
  }
169
- function addImageMaybe(slide, url, x, y, w, h) {
170
- if (!url)
171
- return;
172
- // pptxgenjs 在 Node 环境无法直接下载远程 URL,仅当为本地文件或 base64 时可用。
1637
+ let exportTmpDir = null;
1638
+ const dataUriToTempPath = new Map();
1639
+ function getExportTmpDir() {
1640
+ if (!exportTmpDir) {
1641
+ try {
1642
+ exportTmpDir = mkdtempSync(path.join(tmpdir(), 'lemonppt-'));
1643
+ }
1644
+ catch {
1645
+ return null;
1646
+ }
1647
+ }
1648
+ return exportTmpDir;
1649
+ }
1650
+ export function dataUriToTempFile(url) {
1651
+ const cached = dataUriToTempPath.get(url);
1652
+ if (cached)
1653
+ return cached;
1654
+ const match = url.match(/^data:([^;]+);base64,(.+)$/);
1655
+ if (!match)
1656
+ return undefined;
1657
+ const mime = match[1];
1658
+ const base64 = match[2];
1659
+ const ext = mime === 'image/svg+xml' ? 'svg' :
1660
+ mime === 'image/png' ? 'png' :
1661
+ mime === 'image/jpeg' ? 'jpg' :
1662
+ mime === 'image/webp' ? 'webp' : 'bin';
173
1663
  try {
174
- slide.addImage({ path: url, x, y, w, h, sizing: { type: 'crop', w, h } });
1664
+ const buffer = Buffer.from(base64, 'base64');
1665
+ const tmpDir = getExportTmpDir();
1666
+ if (!tmpDir)
1667
+ return undefined;
1668
+ const fileName = `img-${crypto.createHash('sha256').update(buffer).digest('hex').slice(0, 16)}.${ext}`;
1669
+ const filePath = path.join(tmpDir, fileName);
1670
+ writeFileSync(filePath, buffer);
1671
+ dataUriToTempPath.set(url, filePath);
1672
+ return filePath;
175
1673
  }
176
1674
  catch {
177
- slide.addText('[图片]', { x, y, w, h, fontSize: 14, color: COLORS.secondary, align: 'center', valign: 'middle' });
1675
+ return undefined;
178
1676
  }
179
1677
  }
180
- function renderCoverV1(slide, props) {
181
- if (props.image)
182
- addImageMaybe(slide, props.image, 0, 0, 10, 5.625);
183
- if (props.kicker) {
184
- slide.addText(props.kicker, {
185
- x: 1, y: 2.0, w: 8, h: 0.4,
186
- fontSize: 14, color: COLORS.accent, align: 'center',
187
- fontFace: FONTS.mono,
188
- });
1678
+ export function addImageMaybe(slide, url, x, y, w, h) {
1679
+ if (!url)
1680
+ return;
1681
+ let pathOrUrl = url;
1682
+ if (url.startsWith('data:')) {
1683
+ const tempPath = dataUriToTempFile(url);
1684
+ if (!tempPath) {
1685
+ slide.addText('[图片]', { x, y, w, h, fontSize: 14, color: COLORS.secondary, align: 'center', valign: 'middle' });
1686
+ return;
1687
+ }
1688
+ pathOrUrl = tempPath;
189
1689
  }
190
- slide.addText(props.title, {
191
- x: 1, y: 2.5, w: 8, h: 1.2,
192
- fontSize: 54, color: COLORS.primary, bold: true, align: 'center',
193
- fontFace: FONTS.heading,
194
- });
195
- if (props.subtitle) {
196
- slide.addText(props.subtitle, {
197
- x: 1.5, y: 3.8, w: 7, h: 0.8,
198
- fontSize: 22, color: COLORS.secondary, align: 'center',
199
- fontFace: FONTS.body,
200
- });
1690
+ // pptxgenjs 在 Node 环境无法直接下载远程 URL,仅当为本地文件或 base64 时可用。
1691
+ try {
1692
+ slide.addImage({ path: pathOrUrl, x, y, w, h, sizing: { type: 'crop', w, h } });
201
1693
  }
202
- if (props.date) {
203
- slide.addText(props.date, {
204
- x: 1, y: 5.0, w: 8, h: 0.3,
205
- fontSize: 14, color: COLORS.secondary, align: 'center',
206
- fontFace: FONTS.mono,
207
- });
1694
+ catch {
1695
+ slide.addText('[图片]', { x, y, w, h, fontSize: 14, color: COLORS.secondary, align: 'center', valign: 'middle' });
208
1696
  }
209
1697
  }
210
- function renderDarkTechCoverV1(slide, props) {
211
- // 深色科技风背景:径向光晕 + 网格线简化示意
212
- slide.background = { color: '0A0F1C' };
213
- slide.addShape('rect', {
214
- x: 0, y: 0, w: 10, h: 5.625,
215
- fill: { color: '0A0F1C' },
216
- });
217
- slide.addShape('ellipse', {
218
- x: 2.5, y: 0.5, w: 5, h: 4.5,
219
- fill: { color: '06B6D4' },
220
- });
221
- if (props.image) {
1698
+ function cleanupTempImages() {
1699
+ dataUriToTempPath.clear();
1700
+ if (exportTmpDir) {
222
1701
  try {
223
- slide.addImage({ path: props.image, x: 0, y: 0, w: 10, h: 5.625 });
1702
+ rmSync(exportTmpDir, { recursive: true, force: true });
224
1703
  }
225
1704
  catch {
226
- // ignore
1705
+ // ignore cleanup errors
227
1706
  }
228
- }
229
- if (props.kicker) {
230
- slide.addText(props.kicker, {
231
- x: 1, y: 1.8, w: 8, h: 0.35,
232
- fontSize: 13, color: '06B6D4', align: 'center',
233
- fontFace: FONTS.mono,
234
- });
235
- }
236
- slide.addText(props.title, {
237
- x: 1, y: 2.2, w: 8, h: 1.2,
238
- fontSize: 56, color: 'FFFFFF', bold: true, align: 'center',
239
- fontFace: FONTS.heading,
240
- });
241
- if (props.subtitle) {
242
- slide.addText(props.subtitle, {
243
- x: 1.5, y: 3.5, w: 7, h: 0.7,
244
- fontSize: 22, color: '94A3B8', align: 'center',
245
- fontFace: FONTS.body,
246
- });
247
- }
248
- if (props.date) {
249
- slide.addText(props.date, {
250
- x: 1, y: 4.6, w: 8, h: 0.3,
251
- fontSize: 14, color: '94A3B8', align: 'center',
252
- fontFace: FONTS.mono,
253
- });
1707
+ exportTmpDir = null;
254
1708
  }
255
1709
  }
256
- function renderTableOfContentsV1(slide, props) {
1710
+ export function renderTableOfContentsV1(slide, props) {
257
1711
  addKicker(slide, props.kicker);
258
1712
  addTitle(slide, props.title);
259
- const items = props.items || [];
260
- let y = 2.6;
1713
+ const items = (props.items || []).slice(0, 4);
1714
+ let y = 2.4;
261
1715
  items.forEach((item, index) => {
262
1716
  const number = String(index + 1).padStart(2, '0');
263
1717
  slide.addText(number, {
264
- x: 0.8, y, w: 0.8, h: 0.5,
1718
+ x: 0.8, y, w: 0.8, h: 0.45,
265
1719
  fontSize: 18, color: COLORS.accent, align: 'left', valign: 'top',
266
1720
  fontFace: FONTS.mono,
267
1721
  });
268
1722
  slide.addText(item, {
269
- x: 1.7, y, w: 7.8, h: 0.5,
1723
+ x: 1.7, y, w: 7.8, h: 0.45,
270
1724
  fontSize: 26, color: COLORS.primary, align: 'left', valign: 'top',
271
1725
  fontFace: FONTS.body,
272
1726
  });
273
- y += 0.9;
274
- });
275
- }
276
- function renderMetricV1(slide, props) {
277
- if (props.label) {
278
- slide.addText(props.label, {
279
- x: 1, y: 1.8, w: 8, h: 0.4,
280
- fontSize: 16, color: COLORS.secondary, align: 'center',
281
- fontFace: FONTS.body,
282
- });
283
- }
284
- const metricText = props.unit ? `${props.value} ${props.unit}` : props.value;
285
- slide.addText(metricText, {
286
- x: 1, y: 2.4, w: 8, h: 1.4,
287
- fontSize: 90, color: COLORS.primary, bold: true, align: 'center',
288
- fontFace: FONTS.heading,
289
- });
290
- if (props.description) {
291
- slide.addText(props.description, {
292
- x: 1.5, y: 4.1, w: 7, h: 0.8,
293
- fontSize: 20, color: COLORS.secondary, align: 'center',
294
- fontFace: FONTS.body,
295
- });
296
- }
297
- }
298
- function renderDarkTechMetricV1(slide, props) {
299
- slide.background = { color: '0A0F1C' };
300
- slide.addShape('ellipse', {
301
- x: 2.5, y: 0.5, w: 5, h: 4.5,
302
- fill: { color: '06B6D4' },
303
- });
304
- if (props.label) {
305
- slide.addText(props.label, {
306
- x: 1, y: 1.6, w: 8, h: 0.4,
307
- fontSize: 15, color: '06B6D4', align: 'center',
308
- fontFace: FONTS.mono,
309
- });
310
- }
311
- const metricText = props.unit ? `${props.value} ${props.unit}` : props.value;
312
- slide.addText(metricText, {
313
- x: 1, y: 2.2, w: 8, h: 1.5,
314
- fontSize: 96, color: 'FFFFFF', bold: true, align: 'center',
315
- fontFace: FONTS.heading,
316
- });
317
- if (props.description) {
318
- slide.addText(props.description, {
319
- x: 1.5, y: 4.0, w: 7, h: 0.8,
320
- fontSize: 20, color: '94A3B8', align: 'center',
321
- fontFace: FONTS.body,
322
- });
323
- }
324
- }
325
- function renderMetricV2(slide, props) {
326
- if (props.kicker) {
327
- slide.addText(props.kicker, {
328
- x: 1, y: 1.8, w: 8, h: 0.4,
329
- fontSize: 14, color: COLORS.accent, align: 'center',
330
- fontFace: FONTS.mono,
331
- });
332
- }
333
- const metricText = props.unit ? `${props.value} ${props.unit}` : props.value;
334
- slide.addText(metricText, {
335
- x: 1, y: 2.5, w: 8, h: 1.4,
336
- fontSize: 80, color: COLORS.primary, bold: true, align: 'center',
337
- fontFace: FONTS.heading,
1727
+ y += 0.75;
338
1728
  });
339
- if (props.description) {
340
- slide.addText(props.description, {
341
- x: 1.5, y: 4.1, w: 7, h: 0.8,
342
- fontSize: 20, color: COLORS.secondary, align: 'center',
343
- fontFace: FONTS.body,
344
- });
345
- }
346
1729
  }
347
- function renderStatsV1(slide, props) {
1730
+ export function renderStatsV1(slide, props) {
1731
+ const hasInsight = props.showInsight !== false && !!props.insight && (!!props.insight.value || !!props.insight.label || !!props.insight.description);
1732
+ const mainW = hasInsight ? 6.8 : 8.4;
1733
+ const cardW = (mainW - 0.4) / 2;
1734
+ const startX = 0.8;
348
1735
  addKicker(slide, props.kicker);
349
- addTitle(slide, props.title);
1736
+ addTitle(slide, props.title, startX, 1.3, mainW);
350
1737
  const stats = (props.stats || []).slice(0, 4);
351
1738
  const positions = [
352
- { x: 0.8, y: 2.5 },
353
- { x: 5.2, y: 2.5 },
354
- { x: 0.8, y: 4.0 },
355
- { x: 5.2, y: 4.0 },
1739
+ { x: startX, y: 2.5 },
1740
+ { x: startX + cardW + 0.2, y: 2.5 },
1741
+ { x: startX, y: 4.0 },
1742
+ { x: startX + cardW + 0.2, y: 4.0 },
356
1743
  ];
357
1744
  stats.forEach((stat, index) => {
358
1745
  const pos = positions[index];
359
1746
  if (!pos)
360
1747
  return;
361
1748
  slide.addShape('rect', {
362
- x: pos.x, y: pos.y, w: 4.2, h: 1.3,
1749
+ x: pos.x, y: pos.y, w: cardW, h: 1.3,
363
1750
  fill: { color: COLORS.light },
364
1751
  line: { color: COLORS.border, width: 1 },
365
1752
  });
366
1753
  const valueText = [stat.value, stat.unit].filter(Boolean).join(' ');
367
1754
  slide.addText(valueText || '-', {
368
- x: pos.x + 0.2, y: pos.y + 0.15, w: 3.8, h: 0.55,
1755
+ x: pos.x + 0.2, y: pos.y + 0.15, w: cardW - 0.4, h: 0.55,
369
1756
  fontSize: 30, color: COLORS.primary, bold: true, align: 'left',
370
1757
  fontFace: FONTS.heading,
371
1758
  });
372
1759
  slide.addText(stat.label || '', {
373
- x: pos.x + 0.2, y: pos.y + 0.75, w: 2.6, h: 0.35,
1760
+ x: pos.x + 0.2, y: pos.y + 0.75, w: cardW - 1.0, h: 0.35,
374
1761
  fontSize: 14, color: COLORS.secondary, align: 'left',
375
1762
  fontFace: FONTS.body,
376
1763
  });
377
1764
  if (stat.change) {
378
1765
  slide.addText(stat.change, {
379
- x: pos.x + 2.8, y: pos.y + 0.75, w: 1.2, h: 0.35,
1766
+ x: pos.x + cardW - 1.0, y: pos.y + 0.75, w: 0.8, h: 0.35,
380
1767
  fontSize: 14, color: COLORS.accent, align: 'right',
381
1768
  fontFace: FONTS.mono,
382
1769
  });
383
1770
  }
384
1771
  });
1772
+ if (hasInsight) {
1773
+ renderInsightPanel(slide, props.insight, 8.0, 2.3, 2.4, 3.2);
1774
+ }
385
1775
  }
386
- function renderChartV1(slide, props) {
1776
+ export function renderChartGauge(slide, props) {
387
1777
  addKicker(slide, props.kicker);
388
- addTitle(slide, props.title);
389
- const labels = props.labels || [];
390
- const data = props.data || [];
391
- if (labels.length === 0 || data.length === 0)
1778
+ if (props.title)
1779
+ addTitle(slide, props.title);
1780
+ const value = props.value ?? 0;
1781
+ const min = props.min ?? 0;
1782
+ const max = Math.max(props.max ?? 100, min + 1);
1783
+ const unit = props.unit || '';
1784
+ const percentage = Math.min(100, Math.max(0, ((value - min) / (max - min)) * 100));
1785
+ slide.addText(`${value}${unit}`, {
1786
+ x: 0, y: 2.4, w: 10, h: 1.0,
1787
+ fontSize: 48, bold: true, color: COLORS.accent,
1788
+ align: 'center', valign: 'middle', fontFace: FONTS.heading,
1789
+ });
1790
+ slide.addText(`${percentage.toFixed(1)}%`, {
1791
+ x: 0, y: 3.4, w: 10, h: 0.5,
1792
+ fontSize: 18, color: COLORS.secondary,
1793
+ align: 'center', valign: 'middle', fontFace: FONTS.body,
1794
+ });
1795
+ // pptxgenjs 3.12 运行时支持 doughnut,但当前类型定义较旧,使用 as any 绕过。
1796
+ slide.addChart('doughnut', [
1797
+ { name: '已完成', labels: ['已完成', '未完成'], values: [percentage, 100 - percentage] },
1798
+ ], {
1799
+ x: 3, y: 4.0, w: 4, h: 1.5,
1800
+ chartColors: [COLORS.accent, COLORS.border],
1801
+ showValue: false,
1802
+ holeSize: 60,
1803
+ });
1804
+ const insight = props.insight;
1805
+ const hasInsight = !!insight && (!!insight.value || !!insight.label || !!insight.description);
1806
+ if (hasInsight) {
1807
+ slide.addShape('roundRect', {
1808
+ x: 6.5, y: 2.3, w: 3.0, h: 3.2,
1809
+ fill: { color: COLORS.surfaceElevated },
1810
+ line: { color: COLORS.border, width: 1 },
1811
+ rectRadius: 0.15,
1812
+ });
1813
+ let cursorY = 2.6;
1814
+ if (insight.value) {
1815
+ slide.addText(insight.value, {
1816
+ x: 6.7, y: cursorY, w: 2.6, h: 0.6,
1817
+ fontSize: 34, fontFace: FONTS.heading, bold: true, color: COLORS.accent,
1818
+ });
1819
+ cursorY += 0.6;
1820
+ }
1821
+ if (insight.label) {
1822
+ slide.addText(insight.label, {
1823
+ x: 6.7, y: cursorY, w: 2.6, h: 0.3,
1824
+ fontSize: 10, fontFace: FONTS.body, color: COLORS.secondary,
1825
+ });
1826
+ cursorY += 0.45;
1827
+ }
1828
+ if (insight.description) {
1829
+ cursorY += 0.15;
1830
+ slide.addText(insight.description, {
1831
+ x: 6.7, y: cursorY, w: 2.6, h: 1.6,
1832
+ fontSize: 11, fontFace: FONTS.body, color: COLORS.secondary,
1833
+ valign: 'top',
1834
+ });
1835
+ }
1836
+ }
1837
+ }
1838
+ export function renderChartHeatmap(slide, props) {
1839
+ addKicker(slide, props.kicker);
1840
+ if (props.title)
1841
+ addTitle(slide, props.title);
1842
+ const raw = props.data || [];
1843
+ const flat = raw.map((cell) => ({ name: `${cell[0]} / ${cell[1]}`, value: cell[2] }));
1844
+ const sorted = flat.sort((a, b) => b.value - a.value).slice(0, 12);
1845
+ const insight = props.insight;
1846
+ const hasInsight = !!insight && (!!insight.value || !!insight.label || !!insight.description);
1847
+ if (sorted.length === 0) {
1848
+ slide.addText('(暂无图表数据)', {
1849
+ x: 2, y: 3.2, w: 6, h: 0.8,
1850
+ fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
1851
+ fontFace: FONTS.body,
1852
+ });
392
1853
  return;
393
- const chartType = props.type || 'bar';
394
- const chartData = [
395
- {
396
- name: props.title,
397
- labels,
398
- values: data,
399
- },
400
- ];
401
- slide.addChart(chartType, chartData, {
402
- x: 0.8, y: 2.3, w: 8.4, h: 3.2,
403
- chartColors: [COLORS.accent],
1854
+ }
1855
+ const labels = sorted.map((d) => d.name);
1856
+ const values = sorted.map((d) => d.value);
1857
+ const chartColors = sorted.map((_, i) => CHART_COLORS[i % CHART_COLORS.length]);
1858
+ const chartW = hasInsight ? 5.6 : 8.4;
1859
+ slide.addChart('bar', [{ name: props.title || '热度', labels, values }], {
1860
+ x: 0.8, y: 2.3, w: chartW, h: 3.2,
1861
+ chartColors,
404
1862
  showValue: true,
405
1863
  dataLabelColor: COLORS.primary,
406
1864
  dataLabelFontSize: 10,
407
1865
  });
1866
+ if (hasInsight) {
1867
+ slide.addShape('roundRect', {
1868
+ x: 6.5, y: 2.3, w: 3.0, h: 3.2,
1869
+ fill: { color: COLORS.surfaceElevated },
1870
+ line: { color: COLORS.border, width: 1 },
1871
+ rectRadius: 0.15,
1872
+ });
1873
+ let cursorY = 2.6;
1874
+ if (insight.value) {
1875
+ slide.addText(insight.value, {
1876
+ x: 6.7, y: cursorY, w: 2.6, h: 0.6,
1877
+ fontSize: 34, fontFace: FONTS.heading, bold: true, color: COLORS.accent,
1878
+ });
1879
+ cursorY += 0.6;
1880
+ }
1881
+ if (insight.label) {
1882
+ slide.addText(insight.label, {
1883
+ x: 6.7, y: cursorY, w: 2.6, h: 0.3,
1884
+ fontSize: 10, fontFace: FONTS.body, color: COLORS.secondary,
1885
+ });
1886
+ cursorY += 0.45;
1887
+ }
1888
+ if (insight.description) {
1889
+ cursorY += 0.15;
1890
+ slide.addText(insight.description, {
1891
+ x: 6.7, y: cursorY, w: 2.6, h: 1.6,
1892
+ fontSize: 11, fontFace: FONTS.body, color: COLORS.secondary,
1893
+ valign: 'top',
1894
+ });
1895
+ }
1896
+ }
408
1897
  }
409
- function renderChartV2(slide, props) {
410
- addKicker(slide, props.kicker);
411
- addTitle(slide, props.title);
412
- const labels = props.labels || [];
413
- const datasets = (props.datasets || []).filter((d) => (d.data ?? []).length > 0);
414
- if (labels.length === 0 || datasets.length === 0)
1898
+ export function renderChartFunnel(slide, props) {
1899
+ if (props.kicker)
1900
+ addKicker(slide, props.kicker);
1901
+ if (props.title)
1902
+ addTitle(slide, props.title);
1903
+ const raw = props.data || [];
1904
+ const insight = props.insight || {};
1905
+ const hasInsight = !!insight.value || !!insight.label || !!insight.description;
1906
+ if (raw.length === 0) {
1907
+ slide.addText('(暂无图表数据)', {
1908
+ x: 2, y: 3.2, w: 6, h: 0.8,
1909
+ fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
1910
+ fontFace: FONTS.body,
1911
+ });
415
1912
  return;
416
- const chartData = datasets.map((dataset) => ({
417
- name: dataset.label || '数据',
418
- labels,
419
- values: dataset.data || [],
420
- }));
421
- const chartColors = datasets.map((d, i) => d.color || CHART_COLORS[i % CHART_COLORS.length]);
422
- slide.addChart('bar', chartData, {
423
- x: 0.8, y: 2.3, w: 8.4, h: 3.2,
1913
+ }
1914
+ const labels = raw.map((d) => d.name);
1915
+ const values = raw.map((d) => d.value);
1916
+ const chartColors = raw.map((_, i) => CHART_COLORS[i % CHART_COLORS.length]);
1917
+ const chartW = hasInsight ? 5.6 : 8.4;
1918
+ // PptxGenJS 没有真正的漏斗图,使用横向柱状图模拟漏斗层级
1919
+ slide.addChart('bar', [{ name: props.title || '漏斗', labels, values }], {
1920
+ x: 0.6, y: 2.3, w: chartW, h: 3.2,
424
1921
  chartColors,
425
1922
  showValue: true,
426
1923
  dataLabelColor: COLORS.primary,
427
1924
  dataLabelFontSize: 10,
428
1925
  });
1926
+ if (hasInsight) {
1927
+ slide.addShape('roundRect', {
1928
+ x: 6.5, y: 2.3, w: 3.0, h: 3.2,
1929
+ fill: { color: 'FFFFFF', transparency: 40 },
1930
+ line: { color: 'E5E7EB', width: 1 },
1931
+ rectRadius: 0.15,
1932
+ });
1933
+ let cursorY = 2.6;
1934
+ if (insight.value) {
1935
+ slide.addText(insight.value, {
1936
+ x: 6.7, y: cursorY, w: 2.6, h: 0.6,
1937
+ fontSize: 34, fontFace: FONTS.heading, bold: true, color: COLORS.primary,
1938
+ });
1939
+ cursorY += 0.6;
1940
+ }
1941
+ if (insight.label) {
1942
+ slide.addText(insight.label, {
1943
+ x: 6.7, y: cursorY, w: 2.6, h: 0.3,
1944
+ fontSize: 10, fontFace: FONTS.body, color: COLORS.secondary,
1945
+ });
1946
+ cursorY += 0.45;
1947
+ }
1948
+ if (insight.description) {
1949
+ cursorY += 0.15;
1950
+ slide.addText(insight.description, {
1951
+ x: 6.7, y: cursorY, w: 2.6, h: 1.6,
1952
+ fontSize: 11, fontFace: FONTS.body, color: COLORS.secondary,
1953
+ valign: 'top',
1954
+ });
1955
+ }
1956
+ }
1957
+ if (props.footnote) {
1958
+ slide.addText(props.footnote, {
1959
+ x: 0.6, y: 5.75, w: 8.9, h: 0.3,
1960
+ fontSize: 11, fontFace: FONTS.body, color: COLORS.secondary,
1961
+ });
1962
+ }
429
1963
  }
430
- function renderContentV1(slide, props) {
431
- addKicker(slide, props.kicker);
432
- addTitle(slide, props.title);
433
- addBulletList(slide, props.points || [], 0.8, 2.8, 8.4);
434
- }
435
- function renderContentV2(slide, props) {
436
- addKicker(slide, props.kicker);
437
- addTitle(slide, props.title);
438
- slide.addText('左栏', {
439
- x: 0.8, y: 2.4, w: 4.0, h: 0.4,
440
- fontSize: 18, color: COLORS.accent, bold: true, align: 'left',
441
- fontFace: FONTS.heading,
442
- });
443
- slide.addText('右栏', {
444
- x: 5.2, y: 2.4, w: 4.0, h: 0.4,
445
- fontSize: 18, color: COLORS.accent, bold: true, align: 'left',
446
- fontFace: FONTS.heading,
447
- });
448
- addBulletList(slide, props.leftPoints || [], 0.8, 2.9, 4.0);
449
- addBulletList(slide, props.rightPoints || [], 5.2, 2.9, 4.0);
450
- }
451
- function renderContentV3(slide, props) {
452
- addKicker(slide, props.kicker);
453
- addTitle(slide, props.title, 0.8, 1.3, 4.5);
454
- addBulletList(slide, props.points || [], 0.8, 2.5, 4.2);
455
- addImageMaybe(slide, props.imageUrl, 5.3, 1.5, 4.0, 3.5);
456
- }
457
- function renderSplitV1(slide, props) {
458
- renderContentV3(slide, props);
459
- }
460
- function renderComparisonV1(slide, props) {
461
- addKicker(slide, props.kicker);
462
- addTitle(slide, props.title);
463
- renderComparisonColumns(slide, props.leftTitle || '方案 A', props.leftPoints || [], props.rightTitle || '方案 B', props.rightPoints || []);
464
- }
465
- function renderComparisonV2(slide, props) {
1964
+ export function renderChartRadar(slide, props) {
466
1965
  addKicker(slide, props.kicker);
467
- addTitle(slide, props.title);
468
- renderComparisonColumns(slide, props.leftTitle || '优点', props.leftPoints || [], props.rightTitle || '缺点', props.rightPoints || []);
469
- }
470
- function renderComparisonColumns(slide, leftTitle, leftPoints, rightTitle, rightPoints) {
471
- slide.addText(leftTitle, {
472
- x: 0.8, y: 2.4, w: 4.0, h: 0.5,
473
- fontSize: 24, color: COLORS.primary, bold: true, align: 'left',
474
- fontFace: FONTS.heading,
475
- });
476
- slide.addText(rightTitle, {
477
- x: 5.2, y: 2.4, w: 4.0, h: 0.5,
478
- fontSize: 24, color: COLORS.primary, bold: true, align: 'left',
479
- fontFace: FONTS.heading,
1966
+ if (props.title)
1967
+ addTitle(slide, props.title);
1968
+ const indicators = props.indicators || [];
1969
+ const datasets = props.data || [];
1970
+ const insight = props.insight;
1971
+ const hasInsight = !!insight && (!!insight.value || !!insight.label || !!insight.description);
1972
+ if (indicators.length === 0 || datasets.length === 0) {
1973
+ slide.addText('(暂无图表数据)', {
1974
+ x: 2, y: 3.2, w: 6, h: 0.8,
1975
+ fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
1976
+ fontFace: FONTS.body,
1977
+ });
1978
+ return;
1979
+ }
1980
+ const labels = indicators.map((i) => i.name);
1981
+ const chartData = datasets.map((d) => ({
1982
+ name: d.name,
1983
+ labels,
1984
+ values: d.value,
1985
+ }));
1986
+ const chartColors = datasets.map((_, i) => CHART_COLORS[i % CHART_COLORS.length]);
1987
+ const chartW = hasInsight ? 5.6 : 8.4;
1988
+ // pptxgenjs 3.12 运行时支持 radar,但当前类型定义较旧,使用 as any 绕过。
1989
+ slide.addChart('radar', chartData, {
1990
+ x: 0.8, y: 2.3, w: chartW, h: 3.2,
1991
+ chartColors,
1992
+ showValue: true,
1993
+ dataLabelColor: COLORS.primary,
1994
+ dataLabelFontSize: 10,
480
1995
  });
481
- addBulletList(slide, leftPoints, 0.8, 3.0, 4.0);
482
- addBulletList(slide, rightPoints, 5.2, 3.0, 4.0);
1996
+ if (hasInsight) {
1997
+ slide.addShape('roundRect', {
1998
+ x: 6.5, y: 2.3, w: 3.0, h: 3.2,
1999
+ fill: { color: COLORS.surfaceElevated },
2000
+ line: { color: COLORS.border, width: 1 },
2001
+ rectRadius: 0.15,
2002
+ });
2003
+ let cursorY = 2.6;
2004
+ if (insight.value) {
2005
+ slide.addText(insight.value, {
2006
+ x: 6.7, y: cursorY, w: 2.6, h: 0.6,
2007
+ fontSize: 34, fontFace: FONTS.heading, bold: true, color: COLORS.accent,
2008
+ });
2009
+ cursorY += 0.6;
2010
+ }
2011
+ if (insight.label) {
2012
+ slide.addText(insight.label, {
2013
+ x: 6.7, y: cursorY, w: 2.6, h: 0.3,
2014
+ fontSize: 10, fontFace: FONTS.body, color: COLORS.secondary,
2015
+ });
2016
+ cursorY += 0.45;
2017
+ }
2018
+ if (insight.description) {
2019
+ cursorY += 0.15;
2020
+ slide.addText(insight.description, {
2021
+ x: 6.7, y: cursorY, w: 2.6, h: 1.6,
2022
+ fontSize: 11, fontFace: FONTS.body, color: COLORS.secondary,
2023
+ valign: 'top',
2024
+ });
2025
+ }
2026
+ }
483
2027
  }
484
- function renderProcessV1(slide, props) {
2028
+ export function renderProcessV1(slide, props) {
485
2029
  addKicker(slide, props.kicker);
486
2030
  addTitle(slide, props.title);
487
2031
  const steps = props.steps || [];
488
2032
  const count = steps.length;
489
- if (count === 0)
2033
+ if (count === 0) {
2034
+ slide.addText('(暂无流程步骤)', {
2035
+ x: 2, y: 3.2, w: 6, h: 0.8,
2036
+ fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
2037
+ fontFace: FONTS.body,
2038
+ });
490
2039
  return;
2040
+ }
491
2041
  const stepWidth = 8.4 / count;
492
2042
  steps.forEach((step, index) => {
493
2043
  const x = 0.8 + index * stepWidth;
@@ -502,40 +2052,12 @@ function renderProcessV1(slide, props) {
502
2052
  });
503
2053
  slide.addText(step, {
504
2054
  x: x + 0.1, y: 3.5, w: stepWidth - 0.2, h: 1.2,
505
- fontSize: 18, color: COLORS.primary, align: 'center', valign: 'top',
506
- fontFace: FONTS.body,
507
- });
508
- });
509
- }
510
- function renderProcessV2(slide, props) {
511
- addKicker(slide, props.kicker);
512
- addTitle(slide, props.title);
513
- const steps = (props.steps || []).slice(0, 4);
514
- let y = 2.5;
515
- steps.forEach((step, index) => {
516
- slide.addShape('ellipse', {
517
- x: 0.9, y: y + 0.12, w: 0.4, h: 0.4,
518
- fill: { color: COLORS.accent },
519
- });
520
- slide.addText(String(index + 1), {
521
- x: 0.9, y: y + 0.12, w: 0.4, h: 0.4,
522
- fontSize: 16, color: COLORS.white, bold: true, align: 'center', valign: 'middle',
523
- fontFace: FONTS.heading,
524
- });
525
- slide.addText(step.title || '', {
526
- x: 1.6, y, w: 7.6, h: 0.35,
527
- fontSize: 20, color: COLORS.primary, bold: true, align: 'left',
528
- fontFace: FONTS.heading,
529
- });
530
- slide.addText(step.description || '', {
531
- x: 1.6, y: y + 0.38, w: 7.6, h: 0.5,
532
- fontSize: 16, color: COLORS.secondary, align: 'left', valign: 'top',
2055
+ fontSize: 18, color: COLORS.primary, align: 'center', valign: 'top',
533
2056
  fontFace: FONTS.body,
534
2057
  });
535
- y += 1.0;
536
2058
  });
537
2059
  }
538
- function renderTimelineV1(slide, props) {
2060
+ export function renderTimelineV1(slide, props) {
539
2061
  addKicker(slide, props.kicker);
540
2062
  addTitle(slide, props.title);
541
2063
  const milestones = (props.milestones || []).slice(0, 4);
@@ -568,65 +2090,11 @@ function renderTimelineV1(slide, props) {
568
2090
  });
569
2091
  });
570
2092
  }
571
- function renderWarmBusinessTimelineV1(slide, props) {
572
- addKicker(slide, props.kicker);
573
- addTitle(slide, props.title);
574
- const milestones = (props.milestones || []).slice(0, 4);
575
- const count = milestones.length || 1;
576
- const gap = 0.25;
577
- const cardW = (8.4 - gap * (count - 1)) / count;
578
- const startX = 0.8;
579
- const y = 2.7;
580
- const h = 3.2;
581
- const warmAccent = 'D97706';
582
- const warmText = '3E2723';
583
- const warmSecondary = '6D4C41';
584
- milestones.forEach((m, index) => {
585
- const x = startX + index * (cardW + gap);
586
- slide.addShape('rect', {
587
- x, y, w: cardW, h,
588
- fill: { color: 'FFF8F0' },
589
- line: { color: 'F3E5D8', width: 1 },
590
- rectRadius: 0.12,
591
- });
592
- if (index < count - 1) {
593
- slide.addShape('rect', {
594
- x: x + cardW, y: y + 0.35, w: gap, h: 0.04,
595
- fill: { color: warmAccent },
596
- });
597
- slide.addShape('ellipse', {
598
- x: x + cardW + gap - 0.06, y: y + 0.30, w: 0.12, h: 0.12,
599
- fill: { color: warmAccent },
600
- });
601
- }
602
- if (m.date) {
603
- slide.addText(m.date, {
604
- x: x + 0.16, y: y + 0.22, w: cardW - 0.32, h: 0.3,
605
- fontSize: 12, color: warmAccent, align: 'left', valign: 'top',
606
- fontFace: FONTS.mono,
607
- });
608
- }
609
- if (m.title) {
610
- slide.addText(m.title, {
611
- x: x + 0.16, y: y + 0.6, w: cardW - 0.32, h: 0.45,
612
- fontSize: 17, color: warmText, bold: true, align: 'left', valign: 'top',
613
- fontFace: FONTS.heading,
614
- });
615
- }
616
- if (m.description) {
617
- slide.addText(m.description, {
618
- x: x + 0.16, y: y + 1.1, w: cardW - 0.32, h: 1.6,
619
- fontSize: 13, color: warmSecondary, align: 'left', valign: 'top',
620
- fontFace: FONTS.body,
621
- });
622
- }
623
- });
624
- }
625
2093
  function renderRoadmapV1(slide, props) {
626
2094
  addKicker(slide, props.kicker);
627
2095
  addTitle(slide, props.title);
628
2096
  const phases = (props.phases || []).slice(0, 4);
629
- let y = 2.5;
2097
+ let y = 2.1;
630
2098
  phases.forEach((phase, index) => {
631
2099
  const statusColor = phase.status === '已完成' ? COLORS.accent : COLORS.secondary;
632
2100
  slide.addShape('rect', {
@@ -639,48 +2107,14 @@ function renderRoadmapV1(slide, props) {
639
2107
  fontFace: FONTS.heading,
640
2108
  });
641
2109
  slide.addText(phase.description || '', {
642
- x: 1.1, y: y + 0.38, w: 7.8, h: 0.35,
2110
+ x: 1.1, y: y + 0.38, w: 7.8, h: 0.45,
643
2111
  fontSize: 14, color: COLORS.secondary, align: 'left', valign: 'top',
644
2112
  fontFace: FONTS.body,
645
2113
  });
646
- y += 1.0;
647
- });
648
- }
649
- function renderQuoteV1(slide, props) {
650
- slide.addText(`“${props.quote}”`, {
651
- x: 1, y: 2.0, w: 8, h: 2.2,
652
- fontSize: 36, color: COLORS.primary, bold: true, align: 'center', valign: 'middle',
653
- fontFace: FONTS.heading,
654
- });
655
- const attribution = [props.author, props.source].filter(Boolean).join(' · ');
656
- if (attribution) {
657
- slide.addText(attribution, {
658
- x: 1, y: 4.4, w: 8, h: 0.4,
659
- fontSize: 18, color: COLORS.secondary, align: 'center',
660
- fontFace: FONTS.mono,
661
- });
662
- }
663
- }
664
- function renderQuoteV2(slide, props) {
665
- slide.addShape('rect', {
666
- x: 0.6, y: 1.5, w: 0.15, h: 2.8,
667
- fill: { color: COLORS.accent },
668
- });
669
- slide.addText(`“${props.quote}”`, {
670
- x: 1.0, y: 1.8, w: 8.0, h: 2.0,
671
- fontSize: 34, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
672
- fontFace: FONTS.heading,
2114
+ y += 0.8;
673
2115
  });
674
- const attribution = [props.author, props.role].filter(Boolean).join(' · ');
675
- if (attribution) {
676
- slide.addText(attribution, {
677
- x: 1.0, y: 4.0, w: 8.0, h: 0.4,
678
- fontSize: 18, color: COLORS.secondary, align: 'left',
679
- fontFace: FONTS.mono,
680
- });
681
- }
682
2116
  }
683
- function renderTestimonialV1(slide, props) {
2117
+ export function renderTestimonialV1(slide, props) {
684
2118
  slide.addText(`“${props.quote}”`, {
685
2119
  x: 1, y: 1.8, w: 8, h: 2.2,
686
2120
  fontSize: 34, color: COLORS.primary, bold: true, align: 'center', valign: 'middle',
@@ -697,107 +2131,26 @@ function renderTestimonialV1(slide, props) {
697
2131
  });
698
2132
  }
699
2133
  }
700
- function renderWarmBusinessTestimonialV1(slide, props) {
701
- const warmAccent = 'D97706';
702
- const warmText = '3E2723';
703
- const warmSecondary = '6D4C41';
704
- const warmSurface = 'FFF8F0';
705
- slide.addShape('rect', {
706
- x: 0.8, y: 1.4, w: 8.4, h: 4.2,
707
- fill: { color: warmSurface },
708
- line: { color: 'F3E5D8', width: 1 },
709
- rectRadius: 0.12,
710
- });
711
- slide.addText('“', {
712
- x: 1.0, y: 1.5, w: 0.8, h: 0.8,
713
- fontSize: 64, color: warmAccent, align: 'left', valign: 'top',
714
- fontFace: FONTS.heading,
715
- });
716
- slide.addText(props.quote, {
717
- x: 1.2, y: 2.0, w: 7.6, h: 2.0,
718
- fontSize: 32, color: warmText, bold: true, align: 'center', valign: 'middle',
719
- fontFace: FONTS.heading,
720
- });
721
- if (props.avatarUrl)
722
- addImageMaybe(slide, props.avatarUrl, 1.4, 4.15, 0.7, 0.7);
723
- const attribution = [props.author, props.role, props.company].filter(Boolean).join(' · ');
724
- if (attribution) {
725
- slide.addText(attribution, {
726
- x: 2.3, y: 4.3, w: 6.5, h: 0.4,
727
- fontSize: 15, color: warmSecondary, align: 'left', valign: 'middle',
728
- fontFace: FONTS.mono,
729
- });
730
- }
731
- }
732
- function renderTestimonialV2(slide, props) {
733
- // 卡片背景
734
- slide.addShape('rect', {
735
- x: 1, y: 1.2, w: 8, h: 4.6,
736
- fill: { color: COLORS.light },
737
- line: { color: COLORS.border, width: 1 },
738
- });
739
- // 公司/Logo
740
- const header = props.company || '';
741
- if (header) {
742
- slide.addText(header, {
743
- x: 1.4, y: 1.5, w: 7.2, h: 0.4,
744
- fontSize: 14, color: COLORS.secondary, bold: true,
745
- fontFace: FONTS.mono,
746
- });
747
- }
748
- if (props.logoUrl)
749
- addImageMaybe(slide, props.logoUrl, 7.2, 1.4, 1.2, 0.6);
750
- // 引用
751
- slide.addText(`“${props.quote}”`, {
752
- x: 1.4, y: 2.1, w: 7.2, h: 2.2,
753
- fontSize: 28, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
754
- fontFace: FONTS.heading,
755
- });
756
- // 作者信息
757
- const attribution = [props.author, props.role].filter(Boolean).join(' · ');
758
- if (attribution) {
759
- slide.addText(attribution, {
760
- x: 1.4, y: 4.5, w: 4.5, h: 0.4,
761
- fontSize: 16, color: COLORS.secondary, align: 'left',
762
- fontFace: FONTS.body,
763
- });
764
- }
765
- // 指标
766
- if (props.metric) {
767
- slide.addText(props.metric, {
768
- x: 6.2, y: 4.3, w: 2, h: 0.6,
769
- fontSize: 32, color: COLORS.accent, bold: true, align: 'right',
770
- fontFace: FONTS.heading,
771
- });
772
- if (props.metricLabel) {
773
- slide.addText(props.metricLabel, {
774
- x: 6.2, y: 4.85, w: 2, h: 0.3,
775
- fontSize: 12, color: COLORS.secondary, align: 'right',
776
- fontFace: FONTS.mono,
777
- });
778
- }
779
- }
780
- }
781
- function renderFaqV1(slide, props) {
2134
+ export function renderFaqV1(slide, props) {
782
2135
  addKicker(slide, props.kicker);
783
2136
  addTitle(slide, props.title);
784
2137
  const items = (props.items || []).slice(0, 4);
785
- let y = 2.5;
2138
+ let y = 2.0;
786
2139
  items.forEach((item) => {
787
2140
  slide.addText(`Q: ${item.q || ''}`, {
788
- x: 0.8, y, w: 8.4, h: 0.35,
2141
+ x: 0.8, y, w: 8.4, h: 0.32,
789
2142
  fontSize: 18, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
790
2143
  fontFace: FONTS.heading,
791
2144
  });
792
2145
  slide.addText(`A: ${item.a || ''}`, {
793
- x: 0.8, y: y + 0.4, w: 8.4, h: 0.55,
2146
+ x: 0.8, y: y + 0.37, w: 8.4, h: 0.43,
794
2147
  fontSize: 16, color: COLORS.secondary, align: 'left', valign: 'top',
795
2148
  fontFace: FONTS.body,
796
2149
  });
797
- y += 1.1;
2150
+ y += 0.85;
798
2151
  });
799
2152
  }
800
- function renderFeatureV1(slide, props) {
2153
+ export function renderFeatureV1(slide, props) {
801
2154
  addKicker(slide, props.kicker);
802
2155
  addTitle(slide, props.title);
803
2156
  const features = (props.features || []).slice(0, 3);
@@ -823,39 +2176,7 @@ function renderFeatureV1(slide, props) {
823
2176
  });
824
2177
  });
825
2178
  }
826
- function renderDarkTechFeatureV1(slide, props) {
827
- addKicker(slide, props.kicker);
828
- addTitle(slide, props.title);
829
- const features = (props.features || []).slice(0, 3);
830
- const cardWidth = 2.6;
831
- const gap = 0.3;
832
- const startX = (10 - (features.length * cardWidth + (features.length - 1) * gap)) / 2;
833
- features.forEach((feature, index) => {
834
- const x = startX + index * (cardWidth + gap);
835
- slide.addShape('rect', {
836
- x, y: 2.8, w: cardWidth, h: 2.2,
837
- fill: { color: '111827' },
838
- line: { color: '06B6D4', width: 1 },
839
- rectRadius: 0.1,
840
- });
841
- slide.addText(String(index + 1).padStart(2, '0'), {
842
- x: x + 0.15, y: 2.95, w: cardWidth - 0.3, h: 0.35,
843
- fontSize: 14, color: '06B6D4', align: 'left', valign: 'top',
844
- fontFace: FONTS.mono,
845
- });
846
- slide.addText(feature.title || '', {
847
- x: x + 0.15, y: 3.35, w: cardWidth - 0.3, h: 0.45,
848
- fontSize: 20, color: 'FFFFFF', bold: true, align: 'left', valign: 'top',
849
- fontFace: FONTS.heading,
850
- });
851
- slide.addText(feature.description || '', {
852
- x: x + 0.15, y: 3.85, w: cardWidth - 0.3, h: 0.95,
853
- fontSize: 14, color: '94A3B8', align: 'left', valign: 'top',
854
- fontFace: FONTS.body,
855
- });
856
- });
857
- }
858
- function renderTeamV1(slide, props) {
2179
+ export function renderTeamV1(slide, props) {
859
2180
  addKicker(slide, props.kicker);
860
2181
  addTitle(slide, props.title);
861
2182
  const members = (props.members || []).slice(0, 4);
@@ -883,7 +2204,7 @@ function renderTeamV1(slide, props) {
883
2204
  });
884
2205
  });
885
2206
  }
886
- function renderPartnersV1(slide, props) {
2207
+ export function renderPartnersV1(slide, props) {
887
2208
  addKicker(slide, props.kicker);
888
2209
  addTitle(slide, props.title);
889
2210
  const partners = (props.partners || []).slice(0, 8);
@@ -912,7 +2233,7 @@ function renderPartnersV1(slide, props) {
912
2233
  });
913
2234
  });
914
2235
  }
915
- function renderPricingV1(slide, props) {
2236
+ export function renderPricingV1(slide, props) {
916
2237
  addKicker(slide, props.kicker);
917
2238
  addTitle(slide, props.title);
918
2239
  const tiers = (props.tiers || []).slice(0, 3);
@@ -958,58 +2279,7 @@ function renderPricingV1(slide, props) {
958
2279
  }
959
2280
  });
960
2281
  }
961
- function renderGalleryV1(slide, props) {
962
- addKicker(slide, props.kicker);
963
- addTitle(slide, props.title);
964
- const images = (props.images || []).slice(0, 4);
965
- const positions = [
966
- { x: 0.8, y: 2.5 },
967
- { x: 5.2, y: 2.5 },
968
- { x: 0.8, y: 4.0 },
969
- { x: 5.2, y: 4.0 },
970
- ];
971
- images.forEach((image, index) => {
972
- const pos = positions[index];
973
- if (!pos)
974
- return;
975
- if (image.url)
976
- addImageMaybe(slide, image.url, pos.x, pos.y, 4.0, 1.3);
977
- if (image.caption) {
978
- slide.addText(image.caption, {
979
- x: pos.x, y: pos.y + 1.35, w: 4.0, h: 0.25,
980
- fontSize: 12, color: COLORS.secondary, align: 'center',
981
- fontFace: FONTS.body,
982
- });
983
- }
984
- });
985
- }
986
- function renderGalleryV2(slide, props) {
987
- addKicker(slide, props.kicker);
988
- addTitle(slide, props.title);
989
- const images = (props.images || []).slice(0, 6);
990
- const cols = 3;
991
- const itemWidth = 2.6;
992
- const itemHeight = 1.5;
993
- const gapX = 0.33;
994
- const startX = 0.8;
995
- const startY = 2.4;
996
- images.forEach((image, index) => {
997
- const col = index % cols;
998
- const row = Math.floor(index / cols);
999
- const x = startX + col * (itemWidth + gapX);
1000
- const y = startY + row * (itemHeight + 0.45);
1001
- if (image.url)
1002
- addImageMaybe(slide, image.url, x, y, itemWidth, itemHeight);
1003
- if (image.caption) {
1004
- slide.addText(image.caption, {
1005
- x, y: y + itemHeight + 0.05, w: itemWidth, h: 0.25,
1006
- fontSize: 12, color: COLORS.secondary, align: 'center',
1007
- fontFace: FONTS.body,
1008
- });
1009
- }
1010
- });
1011
- }
1012
- function renderImageV1(slide, props) {
2282
+ export function renderImageV1(slide, props) {
1013
2283
  if (props.imageUrl) {
1014
2284
  addImageMaybe(slide, props.imageUrl, 0, 0, 10, 5.625);
1015
2285
  slide.addShape('rect', {
@@ -1030,7 +2300,7 @@ function renderImageV1(slide, props) {
1030
2300
  });
1031
2301
  }
1032
2302
  }
1033
- function renderSwotV1(slide, props) {
2303
+ export function renderSwotV1(slide, props) {
1034
2304
  addKicker(slide, props.kicker);
1035
2305
  addTitle(slide, props.title);
1036
2306
  render2x2Grid(slide, [
@@ -1040,7 +2310,7 @@ function renderSwotV1(slide, props) {
1040
2310
  { title: '威胁 Threats', text: props.threat || '' },
1041
2311
  ]);
1042
2312
  }
1043
- function renderPestV1(slide, props) {
2313
+ export function renderPestV1(slide, props) {
1044
2314
  addKicker(slide, props.kicker);
1045
2315
  addTitle(slide, props.title);
1046
2316
  render2x2Grid(slide, [
@@ -1078,800 +2348,610 @@ function render2x2Grid(slide, cells) {
1078
2348
  });
1079
2349
  });
1080
2350
  }
1081
- function renderClosingV1(slide, props) {
1082
- if (props.kicker) {
1083
- slide.addText(props.kicker, {
1084
- x: 1, y: 2.0, w: 8, h: 0.4,
1085
- fontSize: 14, color: COLORS.accent, align: 'center',
1086
- fontFace: FONTS.mono,
1087
- });
1088
- }
1089
- slide.addText(props.title, {
1090
- x: 1, y: 2.5, w: 8, h: 1.0,
1091
- fontSize: 48, color: COLORS.primary, bold: true, align: 'center',
1092
- fontFace: FONTS.heading,
1093
- });
1094
- if (props.subtitle) {
1095
- slide.addText(props.subtitle, {
1096
- x: 1.5, y: 3.6, w: 7, h: 0.7,
1097
- fontSize: 22, color: COLORS.secondary, align: 'center',
1098
- fontFace: FONTS.body,
1099
- });
1100
- }
1101
- if (props.cta) {
1102
- slide.addShape('roundRect', {
1103
- x: 3.5, y: 4.6, w: 3, h: 0.6,
1104
- fill: { color: COLORS.accent },
1105
- rectRadius: 0.3,
1106
- });
1107
- slide.addText(props.cta, {
1108
- x: 3.5, y: 4.6, w: 3, h: 0.6,
1109
- fontSize: 16, color: COLORS.white, bold: true, align: 'center', valign: 'middle',
1110
- fontFace: FONTS.body,
1111
- });
1112
- }
1113
- }
1114
- function renderClosingV2(slide, props) {
1115
- slide.addText(props.title, {
1116
- x: 1, y: 2.2, w: 8, h: 1.0,
1117
- fontSize: 48, color: COLORS.primary, bold: true, align: 'center',
1118
- fontFace: FONTS.heading,
1119
- });
1120
- if (props.subtitle) {
1121
- slide.addText(props.subtitle, {
1122
- x: 1.5, y: 3.3, w: 7, h: 0.6,
1123
- fontSize: 22, color: COLORS.secondary, align: 'center',
1124
- fontFace: FONTS.body,
1125
- });
1126
- }
1127
- const lines = [props.contact, props.email, props.link].filter(Boolean);
1128
- let y = 4.2;
1129
- lines.forEach((line) => {
1130
- slide.addText(line, {
1131
- x: 1, y, w: 8, h: 0.35,
1132
- fontSize: 16, color: COLORS.accent, align: 'center',
1133
- fontFace: FONTS.mono,
1134
- });
1135
- y += 0.45;
1136
- });
1137
- }
1138
- function renderTimelineV2(slide, props) {
1139
- addKicker(slide, props.kicker);
1140
- addTitle(slide, props.title ?? 'Timeline');
1141
- const milestones = props.milestones ?? [];
1142
- const maxItems = 5;
1143
- let y = 2.6;
1144
- milestones.slice(0, maxItems).forEach((milestone, index) => {
1145
- const isLast = index === Math.min(milestones.length, maxItems) - 1;
1146
- // marker dot
1147
- slide.addShape('ellipse', {
1148
- x: 0.9, y: y + 0.16, w: 0.16, h: 0.16,
1149
- fill: { color: COLORS.accent },
1150
- });
1151
- if (!isLast) {
1152
- slide.addShape('rect', {
1153
- x: 0.97, y: y + 0.42, w: 0.02, h: 1.0,
1154
- fill: { color: COLORS.border },
1155
- });
1156
- }
1157
- const dateText = milestone.date ?? '';
1158
- if (dateText) {
1159
- slide.addText(dateText, {
1160
- x: 1.3, y, w: 2.0, h: 0.35,
1161
- fontSize: 14, color: COLORS.accent, align: 'left', valign: 'top',
1162
- fontFace: FONTS.mono,
1163
- });
1164
- }
1165
- if (milestone.title) {
1166
- slide.addText(milestone.title, {
1167
- x: 1.3, y: y + 0.35, w: 7.8, h: 0.4,
1168
- fontSize: 20, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
1169
- fontFace: FONTS.heading,
1170
- });
1171
- }
1172
- if (milestone.description) {
1173
- slide.addText(milestone.description, {
1174
- x: 1.3, y: y + 0.75, w: 7.8, h: 0.55,
1175
- fontSize: 16, color: COLORS.secondary, align: 'left', valign: 'top',
1176
- fontFace: FONTS.body,
1177
- });
1178
- }
1179
- y += 1.35;
1180
- });
1181
- }
1182
- function renderRoadmapV2(slide, props) {
1183
- addKicker(slide, props.kicker);
1184
- addTitle(slide, props.title ?? 'Roadmap');
1185
- const phases = props.phases ?? [];
1186
- const maxPhases = 4;
1187
- const cardW = 2.1;
1188
- const startX = 0.7;
1189
- const y = 2.6;
1190
- const h = 3.4;
1191
- phases.slice(0, maxPhases).forEach((phase, index) => {
1192
- const x = startX + index * (cardW + 0.25);
1193
- slide.addShape('rect', {
1194
- x, y, w: cardW, h,
1195
- fill: { color: COLORS.surfaceElevated },
1196
- line: { color: COLORS.border, width: 1 },
1197
- rectRadius: 0.08,
1198
- });
1199
- slide.addText(phase.phase ?? `Phase ${index + 1}`, {
1200
- x: x + 0.12, y: y + 0.2, w: cardW - 0.24, h: 0.4,
1201
- fontSize: 18, color: COLORS.accent, bold: true, align: 'left', valign: 'top',
1202
- fontFace: FONTS.heading,
1203
- });
1204
- const goals = (phase.goals ?? []).slice(0, 5);
1205
- let goalY = y + 0.7;
1206
- goals.forEach((goal) => {
1207
- slide.addText(`• ${goal}`, {
1208
- x: x + 0.16, y: goalY, w: cardW - 0.32, h: 0.45,
1209
- fontSize: 13, color: COLORS.secondary, align: 'left', valign: 'top',
1210
- fontFace: FONTS.body,
1211
- });
1212
- goalY += 0.45;
1213
- });
1214
- });
1215
- }
1216
- function renderPricingV2(slide, props) {
1217
- addKicker(slide, props.kicker);
1218
- addTitle(slide, props.title ?? 'Pricing');
1219
- const plans = props.plans ?? [];
1220
- const maxPlans = 3;
1221
- const cardW = 2.6;
1222
- const startX = 0.9;
1223
- const y = 2.6;
1224
- const h = 3.8;
1225
- plans.slice(0, maxPlans).forEach((plan, index) => {
1226
- const x = startX + index * (cardW + 0.25);
1227
- const isHighlighted = plan.highlighted;
1228
- slide.addShape('rect', {
1229
- x, y, w: cardW, h,
1230
- fill: { color: isHighlighted ? COLORS.accent : COLORS.surfaceElevated },
1231
- line: { color: isHighlighted ? COLORS.accent : COLORS.border, width: 1 },
1232
- rectRadius: 0.08,
1233
- });
1234
- slide.addText(plan.name ?? `Plan ${index + 1}`, {
1235
- x: x + 0.15, y: y + 0.25, w: cardW - 0.3, h: 0.4,
1236
- fontSize: 20, color: isHighlighted ? 'FFFFFF' : COLORS.primary, bold: true, align: 'center', valign: 'top',
1237
- fontFace: FONTS.heading,
1238
- });
1239
- slide.addText(`${plan.price ?? ''} ${plan.period ?? ''}`, {
1240
- x: x + 0.15, y: y + 0.75, w: cardW - 0.3, h: 0.5,
1241
- fontSize: 24, color: isHighlighted ? 'FFFFFF' : COLORS.accent, bold: true, align: 'center', valign: 'top',
1242
- fontFace: FONTS.heading,
1243
- });
1244
- const features = (plan.features ?? []).slice(0, 5);
1245
- let featY = y + 1.4;
1246
- features.forEach((feature) => {
1247
- slide.addText(`• ${feature}`, {
1248
- x: x + 0.2, y: featY, w: cardW - 0.4, h: 0.4,
1249
- fontSize: 13, color: isHighlighted ? 'FFFFFF' : COLORS.secondary, align: 'left', valign: 'top',
1250
- fontFace: FONTS.body,
1251
- });
1252
- featY += 0.4;
1253
- });
1254
- });
1255
- }
1256
- function renderFeatureV2(slide, props) {
1257
- addKicker(slide, props.kicker);
1258
- addTitle(slide, props.title ?? 'Features');
1259
- const features = props.features ?? [];
1260
- const maxFeatures = 3;
1261
- const cardW = 2.8;
1262
- const startX = 0.7;
1263
- const y = 2.6;
1264
- const h = 3.4;
1265
- features.slice(0, maxFeatures).forEach((feature, index) => {
1266
- const x = startX + index * (cardW + 0.25);
1267
- slide.addShape('rect', {
1268
- x, y, w: cardW, h,
1269
- fill: { color: COLORS.surfaceElevated },
1270
- line: { color: COLORS.border, width: 1 },
1271
- rectRadius: 0.08,
1272
- });
1273
- slide.addText(feature.icon ?? '◆', {
1274
- x: x + 0.2, y: y + 0.25, w: cardW - 0.4, h: 0.5,
1275
- fontSize: 28, color: COLORS.accent, align: 'left', valign: 'top',
1276
- fontFace: FONTS.body,
1277
- });
1278
- slide.addText(feature.title ?? `Feature ${index + 1}`, {
1279
- x: x + 0.2, y: y + 0.85, w: cardW - 0.4, h: 0.4,
1280
- fontSize: 18, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
1281
- fontFace: FONTS.heading,
1282
- });
1283
- slide.addText(feature.description ?? '', {
1284
- x: x + 0.2, y: y + 1.35, w: cardW - 0.4, h: 1.8,
1285
- fontSize: 14, color: COLORS.secondary, align: 'left', valign: 'top',
1286
- fontFace: FONTS.body,
1287
- });
1288
- });
1289
- }
1290
- function renderTeamV2(slide, props) {
1291
- addKicker(slide, props.kicker);
1292
- addTitle(slide, props.title ?? 'Team');
1293
- const members = props.members ?? [];
1294
- const maxMembers = 4;
1295
- const cardW = 2.2;
1296
- const startX = 0.6;
1297
- const y = 2.5;
1298
- const h = 3.8;
1299
- members.slice(0, maxMembers).forEach((member, index) => {
1300
- const x = startX + index * (cardW + 0.25);
1301
- slide.addShape('rect', {
1302
- x, y, w: cardW, h,
1303
- fill: { color: COLORS.surfaceElevated },
1304
- line: { color: COLORS.border, width: 1 },
1305
- rectRadius: 0.08,
1306
- });
1307
- slide.addShape('ellipse', {
1308
- x: x + cardW / 2 - 0.35, y: y + 0.25, w: 0.7, h: 0.7,
1309
- fill: { color: COLORS.border },
1310
- });
1311
- slide.addText(member.name ?? `Member ${index + 1}`, {
1312
- x: x + 0.15, y: y + 1.05, w: cardW - 0.3, h: 0.35,
1313
- fontSize: 17, color: COLORS.primary, bold: true, align: 'center', valign: 'top',
1314
- fontFace: FONTS.heading,
1315
- });
1316
- slide.addText(member.role ?? '', {
1317
- x: x + 0.15, y: y + 1.45, w: cardW - 0.3, h: 0.3,
1318
- fontSize: 13, color: COLORS.accent, align: 'center', valign: 'top',
1319
- fontFace: FONTS.body,
1320
- });
1321
- slide.addText(member.bio ?? '', {
1322
- x: x + 0.15, y: y + 1.85, w: cardW - 0.3, h: 1.7,
1323
- fontSize: 12, color: COLORS.secondary, align: 'center', valign: 'top',
1324
- fontFace: FONTS.body,
1325
- });
1326
- });
1327
- }
1328
- function renderMetricV3(slide, props) {
2351
+ export function renderTheme01GalleryV1(slide, props) {
1329
2352
  addKicker(slide, props.kicker);
1330
- addTitle(slide, props.title ?? 'Metrics');
1331
- const metrics = props.metrics ?? [];
1332
- const maxMetrics = 2;
1333
- const cardW = 4.0;
1334
- const startX = 1.0;
1335
- const y = 2.8;
1336
- const h = 3.0;
1337
- metrics.slice(0, maxMetrics).forEach((metric, index) => {
1338
- const x = startX + index * (cardW + 0.5);
2353
+ addTitle(slide, props.title ?? 'Gallery');
2354
+ const images = (props.images || []).slice(0, 4);
2355
+ const positions = [
2356
+ { x: 0.8, y: 2.5 },
2357
+ { x: 5.2, y: 2.5 },
2358
+ { x: 0.8, y: 4.0 },
2359
+ { x: 5.2, y: 4.0 },
2360
+ ];
2361
+ images.forEach((image, index) => {
2362
+ const pos = positions[index];
2363
+ if (!pos)
2364
+ return;
1339
2365
  slide.addShape('rect', {
1340
- x, y, w: cardW, h,
2366
+ x: pos.x, y: pos.y, w: 4.0, h: 1.3,
1341
2367
  fill: { color: COLORS.surfaceElevated },
1342
2368
  line: { color: COLORS.border, width: 1 },
1343
2369
  rectRadius: 0.08,
1344
2370
  });
1345
- slide.addText(metric.label ?? `Metric ${index + 1}`, {
1346
- x: x + 0.25, y: y + 0.3, w: cardW - 0.5, h: 0.4,
1347
- fontSize: 16, color: COLORS.secondary, align: 'left', valign: 'top',
1348
- fontFace: FONTS.body,
1349
- });
1350
- slide.addText(`${metric.value ?? ''}${metric.unit ? ` ${metric.unit}` : ''}`, {
1351
- x: x + 0.25, y: y + 0.85, w: cardW - 0.5, h: 0.8,
1352
- fontSize: 48, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
1353
- fontFace: FONTS.heading,
1354
- });
1355
- if (metric.change) {
1356
- slide.addText(metric.change, {
1357
- x: x + 0.25, y: y + 1.85, w: cardW - 0.5, h: 0.4,
1358
- fontSize: 16, color: COLORS.accent, bold: true, align: 'left', valign: 'top',
2371
+ if (image.url)
2372
+ addImageMaybe(slide, image.url, pos.x + 0.08, pos.y + 0.08, 3.84, 0.9);
2373
+ if (image.caption) {
2374
+ slide.addText(image.caption, {
2375
+ x: pos.x, y: pos.y + 1.05, w: 4.0, h: 0.25,
2376
+ fontSize: 12, color: COLORS.secondary, align: 'center',
1359
2377
  fontFace: FONTS.body,
1360
2378
  });
1361
2379
  }
1362
2380
  });
1363
2381
  }
1364
- function renderCoverV2(slide, props) {
1365
- if (props.image) {
1366
- addImageMaybe(slide, props.image, 5, 0, 5, 5.625);
1367
- }
1368
- else {
1369
- slide.addShape('rect', {
1370
- x: 5, y: 0.8, w: 4.5, h: 4.0,
1371
- fill: { color: COLORS.accent },
1372
- rectRadius: 0.2,
1373
- });
1374
- }
1375
- if (props.kicker) {
1376
- slide.addText(props.kicker, {
1377
- x: 0.8, y: 1.6, w: 4.0, h: 0.4,
1378
- fontSize: 14, color: COLORS.accent, align: 'left',
1379
- fontFace: FONTS.mono,
1380
- });
1381
- }
1382
- slide.addText(props.title, {
1383
- x: 0.8, y: 2.1, w: 4.2, h: 1.2,
1384
- fontSize: 44, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
1385
- fontFace: FONTS.heading,
1386
- });
1387
- if (props.subtitle) {
1388
- slide.addText(props.subtitle, {
1389
- x: 0.8, y: 3.4, w: 4.2, h: 0.8,
1390
- fontSize: 18, color: COLORS.secondary, align: 'left', valign: 'top',
2382
+ export function renderTheme01TableV1(slide, props) {
2383
+ addKicker(slide, props.kicker);
2384
+ addTitle(slide, props.title ?? 'Table');
2385
+ const headers = props.headers || [];
2386
+ const rows = (props.rows || []).slice(0, 6);
2387
+ if (rows.length === 0) {
2388
+ slide.addText('(暂无表格数据)', {
2389
+ x: 2, y: 3.2, w: 6, h: 0.8,
2390
+ fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
1391
2391
  fontFace: FONTS.body,
1392
2392
  });
1393
- }
1394
- if (props.date) {
1395
- slide.addText(props.date, {
1396
- x: 0.8, y: 4.5, w: 4.0, h: 0.3,
1397
- fontSize: 13, color: COLORS.secondary, align: 'left',
1398
- fontFace: FONTS.mono,
1399
- });
1400
- }
1401
- }
1402
- function renderContentV4(slide, props) {
1403
- addKicker(slide, props.kicker);
1404
- addTitle(slide, props.title);
1405
- const cards = (props.cards || []).slice(0, 3);
1406
- if (cards.length === 0)
1407
2393
  return;
1408
- const cardW = 2.6;
1409
- const gap = 0.3;
1410
- const startX = (10 - (cards.length * cardW + (cards.length - 1) * gap)) / 2;
1411
- cards.forEach((card, index) => {
1412
- const x = startX + index * (cardW + gap);
2394
+ }
2395
+ const colCount = Math.max(headers.length, rows[0]?.length || 1);
2396
+ const tableW = 8.4;
2397
+ const startX = 0.8;
2398
+ const startY = 2.5;
2399
+ const rowH = 0.45;
2400
+ const colW = tableW / colCount;
2401
+ // Header
2402
+ headers.forEach((header, index) => {
2403
+ const x = startX + index * colW;
1413
2404
  slide.addShape('rect', {
1414
- x, y: 2.8, w: cardW, h: 2.4,
2405
+ x, y: startY, w: colW, h: rowH,
1415
2406
  fill: { color: COLORS.surfaceElevated },
1416
2407
  line: { color: COLORS.border, width: 1 },
1417
2408
  });
1418
- slide.addText(card.title || '', {
1419
- x: x + 0.15, y: 3.0, w: cardW - 0.3, h: 0.5,
1420
- fontSize: 18, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
2409
+ slide.addText(header, {
2410
+ x, y: startY, w: colW, h: rowH,
2411
+ fontSize: 12, color: COLORS.primary, bold: true, align: 'center', valign: 'middle',
1421
2412
  fontFace: FONTS.heading,
1422
2413
  });
1423
- slide.addText(card.description || '', {
1424
- x: x + 0.15, y: 3.55, w: cardW - 0.3, h: 1.5,
1425
- fontSize: 13, color: COLORS.secondary, align: 'left', valign: 'top',
2414
+ });
2415
+ // Rows
2416
+ rows.forEach((row, rowIndex) => {
2417
+ const y = startY + rowH * (rowIndex + 1);
2418
+ const bg = rowIndex % 2 === 0 ? COLORS.surface : COLORS.surfaceElevated;
2419
+ row.forEach((cell, colIndex) => {
2420
+ const x = startX + colIndex * colW;
2421
+ slide.addShape('rect', {
2422
+ x, y, w: colW, h: rowH,
2423
+ fill: { color: bg },
2424
+ line: { color: COLORS.border, width: 1 },
2425
+ });
2426
+ slide.addText(cell ?? '', {
2427
+ x: x + 0.06, y, w: colW - 0.12, h: rowH,
2428
+ fontSize: 11, color: props.highlightFirstColumn && colIndex === 0 ? COLORS.primary : COLORS.secondary,
2429
+ bold: props.highlightFirstColumn && colIndex === 0,
2430
+ align: colIndex === 0 ? 'left' : 'center', valign: 'middle',
2431
+ fontFace: FONTS.body,
2432
+ });
2433
+ });
2434
+ });
2435
+ }
2436
+ export function renderTheme01TagsV1(slide, props) {
2437
+ addKicker(slide, props.kicker);
2438
+ addTitle(slide, props.title ?? 'Tags');
2439
+ const tags = (props.tags || []).slice(0, 18);
2440
+ if (tags.length === 0) {
2441
+ slide.addText('(暂无标签)', {
2442
+ x: 2, y: 3.2, w: 6, h: 0.8,
2443
+ fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
2444
+ fontFace: FONTS.body,
2445
+ });
2446
+ return;
2447
+ }
2448
+ const toneColors = {
2449
+ positive: { text: '10B981', bg: 'D1FAE5' },
2450
+ negative: { text: 'EF4444', bg: 'FEE2E2' },
2451
+ accent: { text: COLORS.accent, bg: 'DBEAFE' },
2452
+ };
2453
+ let x = 0.8;
2454
+ let y = 2.5;
2455
+ const maxW = 8.4;
2456
+ const gap = 0.15;
2457
+ const tagH = 0.42;
2458
+ tags.forEach((tag) => {
2459
+ const label = tag.label ?? '';
2460
+ const valueText = tag.value !== undefined && tag.value > 0 ? ` ${tag.value}` : '';
2461
+ const text = `${label}${valueText}`;
2462
+ const textW = Math.min(Math.max(text.length * 0.09 + 0.3, 0.7), 2.2);
2463
+ const tone = toneColors[tag.tone ?? ''] ?? { text: COLORS.secondary, bg: COLORS.surfaceElevated };
2464
+ const color = tone.text;
2465
+ const bgColor = tone.bg;
2466
+ if (x + textW > 0.8 + maxW) {
2467
+ x = 0.8;
2468
+ y += tagH + gap;
2469
+ }
2470
+ slide.addShape('roundRect', {
2471
+ x, y, w: textW, h: tagH,
2472
+ fill: { color: bgColor },
2473
+ line: { color: tag.tone ? color : COLORS.border, width: 1 },
2474
+ rectRadius: 0.21,
2475
+ });
2476
+ slide.addText(text, {
2477
+ x, y, w: textW, h: tagH,
2478
+ fontSize: 11, color, bold: true, align: 'center', valign: 'middle',
1426
2479
  fontFace: FONTS.body,
1427
2480
  });
2481
+ x += textW + gap;
1428
2482
  });
1429
2483
  }
1430
- function renderStatsV2(slide, props) {
2484
+ export function renderTheme01FilmstripV1(slide, props) {
1431
2485
  addKicker(slide, props.kicker);
1432
- addTitle(slide, props.title);
1433
- const stats = (props.stats || []).slice(0, 4);
1434
- if (stats.length === 0)
2486
+ addTitle(slide, props.title ?? 'Filmstrip');
2487
+ const images = (props.images || []).slice(0, 5);
2488
+ if (images.length === 0) {
2489
+ slide.addText('(暂无影像)', {
2490
+ x: 2, y: 3.2, w: 6, h: 0.8,
2491
+ fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
2492
+ fontFace: FONTS.body,
2493
+ });
1435
2494
  return;
1436
- const itemW = 8.4 / stats.length;
1437
- stats.forEach((stat, index) => {
1438
- const x = 0.8 + index * itemW;
2495
+ }
2496
+ const gap = 0.2;
2497
+ const frameW = (8.4 - gap * (images.length - 1)) / images.length;
2498
+ const startX = 0.8;
2499
+ const y = 2.5;
2500
+ const imgH = 2.2;
2501
+ images.forEach((image, index) => {
2502
+ const x = startX + index * (frameW + gap);
1439
2503
  slide.addShape('rect', {
1440
- x: x + 0.1, y: 2.8, w: itemW - 0.2, h: 2.4,
2504
+ x, y, w: frameW, h: imgH + 0.5,
1441
2505
  fill: { color: COLORS.surfaceElevated },
1442
2506
  line: { color: COLORS.border, width: 1 },
2507
+ rectRadius: 0.08,
1443
2508
  });
1444
- const valueText = [stat.value, stat.unit].filter(Boolean).join(' ') || '-';
1445
- slide.addText(valueText, {
1446
- x: x + 0.2, y: 3.0, w: itemW - 0.4, h: 0.8,
1447
- fontSize: 36, color: COLORS.primary, bold: true, align: 'center', valign: 'top',
1448
- fontFace: FONTS.heading,
1449
- });
1450
- slide.addText(stat.label || '', {
1451
- x: x + 0.2, y: 3.9, w: itemW - 0.4, h: 0.4,
1452
- fontSize: 14, color: COLORS.secondary, align: 'center', valign: 'top',
1453
- fontFace: FONTS.body,
1454
- });
1455
- if (stat.change) {
1456
- slide.addText(stat.change, {
1457
- x: x + 0.2, y: 4.35, w: itemW - 0.4, h: 0.35,
1458
- fontSize: 13, color: COLORS.accent, align: 'center',
1459
- fontFace: FONTS.mono,
2509
+ if (image.url)
2510
+ addImageMaybe(slide, image.url, x + 0.06, y + 0.06, frameW - 0.12, imgH);
2511
+ if (image.caption) {
2512
+ slide.addText(image.caption, {
2513
+ x, y: y + imgH + 0.08, w: frameW, h: 0.35,
2514
+ fontSize: 11, color: COLORS.secondary, align: 'center', valign: 'middle',
2515
+ fontFace: FONTS.body,
1460
2516
  });
1461
2517
  }
1462
2518
  });
1463
2519
  }
1464
- function renderImageV2(slide, props) {
1465
- if (props.image) {
1466
- addImageMaybe(slide, props.image, 4.2, 0, 5.8, 5.625);
1467
- }
1468
- if (props.caption) {
1469
- slide.addText(props.caption, {
1470
- x: 4.2, y: 5.0, w: 5.8, h: 0.4,
1471
- fontSize: 12, color: COLORS.secondary, align: 'left', valign: 'bottom',
1472
- fontFace: FONTS.mono,
1473
- });
1474
- }
1475
- if (props.kicker) {
1476
- slide.addText(props.kicker, {
1477
- x: 0.8, y: 1.4, w: 3.0, h: 0.4,
1478
- fontSize: 14, color: COLORS.accent, align: 'left',
1479
- fontFace: FONTS.mono,
2520
+ export function renderTheme01RoadmapV1(slide, props) {
2521
+ const phases = (props.phases || []).map((p) => ({
2522
+ title: p.phase || '',
2523
+ description: (p.items || []).join('\n'),
2524
+ }));
2525
+ renderRoadmapV1(slide, { kicker: props.kicker, title: props.title ?? '路线图', phases });
2526
+ }
2527
+ export function renderTheme01MetricBig(slide, props) {
2528
+ const hasInsight = props.showInsight !== false && !!props.insight && (!!props.insight.value || !!props.insight.label || !!props.insight.description);
2529
+ const mainW = hasInsight ? 6.8 : 8.4;
2530
+ const mainX = 0.8;
2531
+ addKicker(slide, props.kicker);
2532
+ addTitle(slide, props.title ?? '核心指标', mainX, 1.3, mainW);
2533
+ if (props.subtitle) {
2534
+ slide.addText(props.subtitle, {
2535
+ x: mainX, y: 2.05, w: mainW, h: 0.35,
2536
+ fontSize: 16, color: COLORS.secondary, align: 'left', valign: 'top',
2537
+ fontFace: FONTS.body,
1480
2538
  });
1481
2539
  }
1482
- slide.addText(props.title, {
1483
- x: 0.8, y: 1.9, w: 3.2, h: 1.2,
1484
- fontSize: 38, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
2540
+ const valueText = [props.value, props.unit].filter(Boolean).join(' ') || '0';
2541
+ slide.addText(valueText, {
2542
+ x: mainX, y: 2.7, w: mainW, h: 1.2,
2543
+ fontSize: hasInsight ? 60 : 72, color: COLORS.primary, bold: true, align: 'center', valign: 'middle',
1485
2544
  fontFace: FONTS.heading,
1486
2545
  });
1487
- if (props.body) {
1488
- slide.addText(props.body, {
1489
- x: 0.8, y: 3.2, w: 3.2, h: 1.8,
1490
- fontSize: 15, color: COLORS.secondary, align: 'left', valign: 'top',
2546
+ if (props.context) {
2547
+ slide.addText(props.context, {
2548
+ x: mainX + (hasInsight ? 0.7 : 0.7), y: 4.0, w: hasInsight ? mainW - 1.4 : 7.0, h: 0.6,
2549
+ fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'top',
1491
2550
  fontFace: FONTS.body,
1492
2551
  });
1493
2552
  }
1494
- }
1495
- function renderQuoteV3(slide, props) {
1496
- slide.addText('"', {
1497
- x: 0.8, y: 1.0, w: 1.5, h: 1.5,
1498
- fontSize: 120, color: COLORS.accent, align: 'left', valign: 'top',
1499
- fontFace: FONTS.heading,
1500
- });
1501
- slide.addText(props.quote, {
1502
- x: 2.0, y: 1.6, w: 7.2, h: 2.6,
1503
- fontSize: 28, color: COLORS.primary, align: 'left', valign: 'top',
1504
- fontFace: FONTS.heading,
1505
- });
1506
- const meta = [props.author, props.role].filter(Boolean).join(' · ');
1507
- if (meta) {
1508
- slide.addText(meta, {
1509
- x: 2.0, y: 4.4, w: 7.2, h: 0.4,
1510
- fontSize: 16, color: COLORS.primary, bold: true, align: 'left',
1511
- fontFace: FONTS.body,
2553
+ const metrics = (props.metrics || []).filter((m) => m != null && (m.value || m.label)).slice(0, 4);
2554
+ if (metrics.length > 0) {
2555
+ const count = metrics.length;
2556
+ const gap = 0.25;
2557
+ const cardW = (mainW - gap * (count - 1)) / count;
2558
+ const startX = mainX;
2559
+ const y = 4.85;
2560
+ const h = 0.65;
2561
+ metrics.forEach((metric, index) => {
2562
+ const x = startX + index * (cardW + gap);
2563
+ const bg = metric.accent ? COLORS.accent : COLORS.surfaceElevated;
2564
+ const textColor = metric.accent ? COLORS.white : COLORS.primary;
2565
+ slide.addShape('rect', {
2566
+ x, y, w: cardW, h,
2567
+ fill: { color: bg },
2568
+ line: { color: COLORS.border, width: 1 },
2569
+ rectRadius: 0.1,
2570
+ });
2571
+ slide.addText(metric.value || '', {
2572
+ x: x + 0.1, y: y + 0.08, w: cardW - 0.2, h: 0.32,
2573
+ fontSize: 18, color: textColor, bold: true, align: 'center', valign: 'top',
2574
+ fontFace: FONTS.heading,
2575
+ });
2576
+ slide.addText(metric.label || '', {
2577
+ x: x + 0.1, y: y + 0.36, w: cardW - 0.2, h: 0.24,
2578
+ fontSize: 11, color: metric.accent ? COLORS.white : COLORS.secondary, align: 'center', valign: 'top',
2579
+ fontFace: FONTS.body,
2580
+ });
1512
2581
  });
1513
2582
  }
1514
- if (props.source) {
1515
- slide.addText(props.source, {
1516
- x: 2.0, y: 4.85, w: 7.2, h: 0.3,
1517
- fontSize: 12, color: COLORS.secondary, align: 'left',
2583
+ if (props.footnote) {
2584
+ slide.addText(props.footnote, {
2585
+ x: mainX, y: 5.7, w: mainW, h: 0.25,
2586
+ fontSize: 11, color: COLORS.secondary, align: 'left',
1518
2587
  fontFace: FONTS.mono,
1519
2588
  });
1520
2589
  }
2590
+ if (hasInsight) {
2591
+ renderInsightPanel(slide, props.insight, 8.0, 2.3, 2.4, 3.2);
2592
+ }
1521
2593
  }
1522
- function renderProcessV3(slide, props) {
2594
+ export function renderTheme01ChartDonut(slide, props) {
1523
2595
  addKicker(slide, props.kicker);
1524
- addTitle(slide, props.title);
1525
- const steps = (props.steps || []).slice(0, 4);
1526
- if (steps.length === 0)
1527
- return;
1528
- const stepH = 0.85;
1529
- const gap = 0.18;
1530
- const startY = 2.6;
1531
- steps.forEach((step, index) => {
1532
- const y = startY + index * (stepH + gap);
1533
- slide.addShape('rect', {
1534
- x: 0.8, y, w: 8.4, h: stepH,
1535
- fill: { color: COLORS.surfaceElevated },
1536
- line: { color: COLORS.border, width: 1 },
1537
- });
1538
- slide.addShape('rect', {
1539
- x: 0.8, y, w: 0.6, h: stepH,
1540
- fill: { color: COLORS.accent },
1541
- });
1542
- slide.addText(String(index + 1).padStart(2, '0'), {
1543
- x: 0.8, y, w: 0.6, h: stepH,
1544
- fontSize: 14, color: 'FFFFFF', bold: true, align: 'center', valign: 'middle',
1545
- fontFace: FONTS.mono,
1546
- });
1547
- slide.addText(step.title || '', {
1548
- x: 1.6, y: y + 0.12, w: 7.4, h: 0.3,
1549
- fontSize: 16, color: COLORS.primary, bold: true, align: 'left',
1550
- fontFace: FONTS.heading,
1551
- });
1552
- slide.addText(step.description || '', {
1553
- x: 1.6, y: y + 0.42, w: 7.4, h: 0.35,
1554
- fontSize: 12, color: COLORS.secondary, align: 'left', valign: 'top',
2596
+ addTitle(slide, props.title ?? '环形图');
2597
+ if (props.subtitle) {
2598
+ slide.addText(props.subtitle, {
2599
+ x: 0.8, y: 2.05, w: 8.4, h: 0.35,
2600
+ fontSize: 16, color: COLORS.secondary, align: 'left', valign: 'top',
1555
2601
  fontFace: FONTS.body,
1556
2602
  });
1557
- });
1558
- }
1559
- function renderFeatureV3(slide, props) {
1560
- if (props.image) {
1561
- addImageMaybe(slide, props.image, 0, 0, 4.5, 5.625);
1562
2603
  }
1563
- if (props.kicker) {
1564
- slide.addText(props.kicker, {
1565
- x: 5.0, y: 0.8, w: 4.2, h: 0.4,
1566
- fontSize: 14, color: COLORS.accent, align: 'left',
1567
- fontFace: FONTS.mono,
2604
+ const rawSegments = (props.segments || [])
2605
+ .filter((s) => s != null && !!(s.label || s.value))
2606
+ .map((s) => ({ ...s }))
2607
+ .sort((a, b) => {
2608
+ const na = parseFloat(String(a.value || '0').replace(/,/g, ''));
2609
+ const nb = parseFloat(String(b.value || '0').replace(/,/g, ''));
2610
+ return nb - na;
2611
+ });
2612
+ if (rawSegments.length === 0) {
2613
+ slide.addText('(暂无环形图数据)', {
2614
+ x: 2, y: 3.2, w: 6, h: 0.8,
2615
+ fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
2616
+ fontFace: FONTS.body,
1568
2617
  });
2618
+ return;
1569
2619
  }
1570
- slide.addText(props.title, {
1571
- x: 5.0, y: 1.3, w: 4.2, h: 0.9,
1572
- fontSize: 36, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
2620
+ const totalValue = rawSegments.reduce((sum, s) => {
2621
+ return sum + parseFloat(String(s.value || '0').replace(/,/g, ''));
2622
+ }, 0);
2623
+ const segments = rawSegments.map((s, i) => {
2624
+ const n = parseFloat(String(s.value || '0').replace(/,/g, ''));
2625
+ const pct = totalValue > 0 ? Math.round((n / totalValue) * 1000) / 10 : 0;
2626
+ return {
2627
+ ...s,
2628
+ percent: s.percent || `${pct}%`,
2629
+ numericValue: n,
2630
+ color: s.color || `var(--lp-${['blue', 'green', 'amber', 'red', 'violet'][i % 5]})`,
2631
+ };
2632
+ });
2633
+ const labels = segments.map((s) => s.label || '');
2634
+ const values = segments.map((s) => s.numericValue);
2635
+ const chartColors = segments.map((_, i) => CHART_COLORS[i % CHART_COLORS.length]);
2636
+ const insight = props.insight;
2637
+ const hasInsight = !!insight && (!!insight.value || !!insight.label || !!insight.description);
2638
+ const chartW = hasInsight ? 4.2 : 4.8;
2639
+ slide.addChart('doughnut', [
2640
+ { name: props.title || '', labels, values },
2641
+ ], {
2642
+ x: 0.6, y: 2.4, w: chartW, h: 3.0,
2643
+ chartColors,
2644
+ showValue: false,
2645
+ holeSize: 60,
2646
+ });
2647
+ const total = props.total || { value: String(totalValue), label: '合计' };
2648
+ const centerX = 0.6 + chartW / 2;
2649
+ slide.addText(total.value || String(totalValue), {
2650
+ x: centerX - 1.8, y: 3.4, w: 3.6, h: 0.6,
2651
+ fontSize: 28, color: COLORS.primary, bold: true, align: 'center', valign: 'middle',
1573
2652
  fontFace: FONTS.heading,
1574
2653
  });
1575
- const features = (props.features || []).slice(0, 3);
1576
- features.forEach((feature, index) => {
1577
- const y = 2.4 + index * 1.0;
2654
+ slide.addText(total.label || '合计', {
2655
+ x: centerX - 1.8, y: 4.0, w: 3.6, h: 0.3,
2656
+ fontSize: 12, color: COLORS.secondary, align: 'center', valign: 'top',
2657
+ fontFace: FONTS.body,
2658
+ });
2659
+ // 右侧图例列表
2660
+ const legendX = hasInsight ? 5.0 : 5.7;
2661
+ const legendW = hasInsight ? 2.6 : 3.3;
2662
+ let legendY = 2.5;
2663
+ segments.forEach((segment, index) => {
2664
+ const color = chartColors[index % chartColors.length];
1578
2665
  slide.addShape('ellipse', {
1579
- x: 5.0, y: y + 0.1, w: 0.2, h: 0.2,
1580
- fill: { color: COLORS.accent },
2666
+ x: legendX, y: legendY + 0.08, w: 0.18, h: 0.18,
2667
+ fill: { color },
1581
2668
  });
1582
- slide.addText(feature.title || '', {
1583
- x: 5.4, y, w: 3.8, h: 0.35,
1584
- fontSize: 16, color: COLORS.primary, bold: true, align: 'left',
2669
+ slide.addText(segment.label || '', {
2670
+ x: legendX + 0.3, y: legendY, w: legendW - 1.4, h: 0.28,
2671
+ fontSize: 13, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
1585
2672
  fontFace: FONTS.heading,
1586
2673
  });
1587
- slide.addText(feature.description || '', {
1588
- x: 5.4, y: y + 0.35, w: 3.8, h: 0.55,
1589
- fontSize: 12, color: COLORS.secondary, align: 'left', valign: 'top',
1590
- fontFace: FONTS.body,
2674
+ if (segment.labelEn) {
2675
+ slide.addText(segment.labelEn, {
2676
+ x: legendX + 0.3, y: legendY + 0.26, w: legendW - 1.4, h: 0.22,
2677
+ fontSize: 10, color: COLORS.secondary, align: 'left', valign: 'top',
2678
+ fontFace: FONTS.body,
2679
+ });
2680
+ }
2681
+ slide.addText(segment.percent || '', {
2682
+ x: legendX + legendW - 1.0, y: legendY, w: 0.9, h: 0.28,
2683
+ fontSize: 13, color: COLORS.accent, bold: true, align: 'right', valign: 'top',
2684
+ fontFace: FONTS.heading,
1591
2685
  });
2686
+ slide.addText(segment.value || '', {
2687
+ x: legendX + legendW - 1.0, y: legendY + 0.26, w: 0.9, h: 0.22,
2688
+ fontSize: 10, color: COLORS.secondary, align: 'right', valign: 'top',
2689
+ fontFace: FONTS.mono,
2690
+ });
2691
+ legendY += segment.labelEn ? 0.65 : 0.5;
1592
2692
  });
1593
- }
1594
- function renderGalleryV3(slide, props) {
1595
- addKicker(slide, props.kicker);
1596
- addTitle(slide, props.title ?? 'GalleryV3');
1597
- const images = (props.images ?? []).slice(0, 3);
1598
- const gap = 0.35;
1599
- const cardW = (8.4 - gap * 2) / 3;
1600
- const startX = 0.8;
1601
- const y = 2.6;
1602
- const imgH = 2.8;
1603
- images.forEach((image, index) => {
1604
- const x = startX + index * (cardW + gap);
1605
- slide.addShape('rect', {
1606
- x,
1607
- y,
1608
- w: cardW,
1609
- h: imgH,
2693
+ if (hasInsight) {
2694
+ slide.addShape('roundRect', {
2695
+ x: 8.0, y: 2.3, w: 2.4, h: 3.2,
1610
2696
  fill: { color: COLORS.surfaceElevated },
1611
2697
  line: { color: COLORS.border, width: 1 },
1612
- rectRadius: 0.08,
2698
+ rectRadius: 0.15,
1613
2699
  });
1614
- if (image.url) {
1615
- try {
1616
- slide.addImage({ path: image.url, x: x + 0.1, y: y + 0.1, w: cardW - 0.2, h: imgH - 0.6 });
1617
- }
1618
- catch {
1619
- slide.addShape('rect', {
1620
- x: x + 0.1, y: y + 0.1, w: cardW - 0.2, h: imgH - 0.6,
1621
- fill: { color: COLORS.accent },
1622
- });
1623
- }
2700
+ let cursorY = 2.6;
2701
+ if (insight.value) {
2702
+ slide.addText(insight.value, {
2703
+ x: 8.2, y: cursorY, w: 2.0, h: 0.6,
2704
+ fontSize: 34, fontFace: FONTS.heading, bold: true, color: COLORS.accent,
2705
+ });
2706
+ cursorY += 0.6;
1624
2707
  }
1625
- slide.addText(image.caption || '', {
1626
- x,
1627
- y: y + imgH - 0.45,
1628
- w: cardW,
1629
- h: 0.4,
1630
- fontSize: 12,
1631
- color: COLORS.secondary,
1632
- align: 'center',
1633
- valign: 'middle',
1634
- fontFace: FONTS.body,
1635
- });
1636
- });
1637
- }
1638
- function renderTestimonialV3(slide, props) {
1639
- const leftX = 0.8;
1640
- const leftW = 5.0;
1641
- const rightX = 6.2;
1642
- const rightW = 3.0;
1643
- slide.addText('"', {
1644
- x: leftX, y: 1.2, w: 1.0, h: 1.0,
1645
- fontSize: 120, color: COLORS.accent, align: 'left', valign: 'top',
1646
- fontFace: FONTS.heading,
1647
- });
1648
- slide.addText(props.quote ?? '', {
1649
- x: leftX, y: 2.2, w: leftW, h: 3.2,
1650
- fontSize: 28, color: COLORS.primary, align: 'left', valign: 'top',
1651
- fontFace: FONTS.heading,
1652
- });
1653
- slide.addShape('rect', {
1654
- x: rightX, y: 1.4, w: rightW, h: 4.4,
1655
- fill: { color: COLORS.surfaceElevated },
1656
- line: { color: COLORS.border, width: 1 },
1657
- rectRadius: 0.1,
1658
- });
1659
- if (props.avatar) {
1660
- try {
1661
- slide.addImage({ path: props.avatar, x: rightX + 0.8, y: 1.8, w: 1.4, h: 1.4 });
2708
+ if (insight.label) {
2709
+ slide.addText(insight.label, {
2710
+ x: 8.2, y: cursorY, w: 2.0, h: 0.3,
2711
+ fontSize: 10, fontFace: FONTS.body, color: COLORS.secondary,
2712
+ });
2713
+ cursorY += 0.45;
1662
2714
  }
1663
- catch {
1664
- slide.addShape('rect', {
1665
- x: rightX + 0.8, y: 1.8, w: 1.4, h: 1.4,
1666
- fill: { color: COLORS.accent },
2715
+ if (insight.description) {
2716
+ cursorY += 0.15;
2717
+ slide.addText(insight.description, {
2718
+ x: 8.2, y: cursorY, w: 2.0, h: 1.6,
2719
+ fontSize: 11, fontFace: FONTS.body, color: COLORS.secondary,
2720
+ valign: 'top',
1667
2721
  });
1668
2722
  }
1669
2723
  }
1670
- slide.addText(props.author ?? '', {
1671
- x: rightX, y: 3.5, w: rightW, h: 0.35,
1672
- fontSize: 18, color: COLORS.primary, bold: true, align: 'center',
1673
- fontFace: FONTS.heading,
1674
- });
1675
- slide.addText(props.role ?? '', {
1676
- x: rightX, y: 3.85, w: rightW, h: 0.3,
1677
- fontSize: 14, color: COLORS.secondary, align: 'center',
1678
- fontFace: FONTS.body,
1679
- });
1680
- slide.addText(props.company ?? '', {
1681
- x: rightX, y: 4.15, w: rightW, h: 0.3,
1682
- fontSize: 13, color: COLORS.secondary, align: 'center',
1683
- fontFace: FONTS.body,
2724
+ if (props.footnote) {
2725
+ slide.addText(props.footnote, {
2726
+ x: 0.6, y: 5.7, w: 8.8, h: 0.25,
2727
+ fontSize: 11, color: COLORS.secondary, align: 'left',
2728
+ fontFace: FONTS.mono,
2729
+ });
2730
+ }
2731
+ }
2732
+ // Theme02 版式复用 theme01 的渲染函数,视觉差异由主题色板控制
2733
+ // ---- Theme02 Batch B/C/D adapters ------------------------------------------
2734
+ export const T2_CHART = ['00E5B0', 'FFD166', '00B4FF', 'FF6B6B', 'A78BFA', 'F472B6'];
2735
+ export function theme03TitleText(title) {
2736
+ const parts = title.split(/(\{\{[^}]+\}\})/g);
2737
+ return parts.map((part) => {
2738
+ const match = part.match(/^\{\{(.+)\}\}$/);
2739
+ if (match) {
2740
+ return { text: match[1], options: { color: COLORS.accent, bold: true } };
2741
+ }
2742
+ return { text: part, options: { color: COLORS.primary, bold: true } };
1684
2743
  });
1685
- if (props.metric) {
1686
- slide.addText(props.metric, {
1687
- x: rightX, y: 4.75, w: rightW, h: 0.45,
1688
- fontSize: 28, color: COLORS.accent, bold: true, align: 'center',
1689
- fontFace: FONTS.heading,
2744
+ }
2745
+ export function addTheme03Topbar(slide, props) {
2746
+ const tagText = [props.tag, props.tagLabel].filter(Boolean).join(' · ');
2747
+ if (tagText) {
2748
+ slide.addText(tagText, {
2749
+ x: 0.84375, y: 0.71875, w: 4, h: 0.25,
2750
+ fontSize: 12, color: COLORS.accent, fontFace: FONTS.mono,
2751
+ valign: 'middle',
1690
2752
  });
1691
- slide.addText(props.metricLabel ?? '', {
1692
- x: rightX, y: 5.2, w: rightW, h: 0.3,
1693
- fontSize: 12, color: COLORS.secondary, align: 'center',
1694
- fontFace: FONTS.body,
2753
+ }
2754
+ if (props.topRightMeta) {
2755
+ slide.addText(props.topRightMeta, {
2756
+ x: 5.15625, y: 0.71875, w: 4, h: 0.25,
2757
+ fontSize: 11, color: COLORS.secondary, fontFace: FONTS.mono,
2758
+ align: 'right', valign: 'middle',
1695
2759
  });
1696
2760
  }
1697
2761
  }
1698
- function renderComparisonV3(slide, props) {
1699
- addKicker(slide, props.kicker);
1700
- addTitle(slide, props.title ?? 'ComparisonV3');
1701
- const rows = (props.rows ?? []).slice(0, 5);
1702
- const startY = 2.6;
1703
- const rowH = 0.8;
1704
- const col1W = 3.2;
1705
- const col2W = 2.8;
1706
- const col3W = 2.8;
1707
- const gap = 0.1;
1708
- const startX = 0.8;
1709
- slide.addShape('rect', {
1710
- x: startX, y: startY, w: col1W, h: 0.5,
1711
- fill: { color: COLORS.surfaceElevated },
1712
- line: { color: COLORS.border, width: 1 },
1713
- });
1714
- slide.addShape('rect', {
1715
- x: startX + col1W + gap, y: startY, w: col2W, h: 0.5,
1716
- fill: { color: COLORS.accent },
1717
- line: { color: COLORS.accent, width: 1 },
1718
- });
1719
- slide.addShape('rect', {
1720
- x: startX + col1W + gap + col2W + gap, y: startY, w: col3W, h: 0.5,
1721
- fill: { color: COLORS.surfaceElevated },
2762
+ export function addTheme03Footer(slide, props) {
2763
+ const y = 5.625 - 0.65625;
2764
+ slide.addShape('line', {
2765
+ x1: 0.84375, y1: y - 0.1, x2: 9.15625, y2: y - 0.1,
1722
2766
  line: { color: COLORS.border, width: 1 },
1723
2767
  });
1724
- slide.addText('维度', {
1725
- x: startX, y: startY, w: col1W, h: 0.5,
1726
- fontSize: 12, color: COLORS.secondary, bold: true, align: 'center', valign: 'middle',
1727
- fontFace: FONTS.body,
1728
- });
1729
- slide.addText(props.leftTitle ?? '方案 A', {
1730
- x: startX + col1W + gap, y: startY, w: col2W, h: 0.5,
1731
- fontSize: 12, color: '#ffffff', bold: true, align: 'center', valign: 'middle',
1732
- fontFace: FONTS.heading,
1733
- });
1734
- slide.addText(props.rightTitle ?? '方案 B', {
1735
- x: startX + col1W + gap + col2W + gap, y: startY, w: col3W, h: 0.5,
1736
- fontSize: 12, color: COLORS.primary, bold: true, align: 'center', valign: 'middle',
1737
- fontFace: FONTS.heading,
1738
- });
1739
- rows.forEach((row, index) => {
1740
- const y = startY + 0.6 + index * rowH;
1741
- const bg = index % 2 === 0 ? COLORS.surface : COLORS.surfaceElevated;
1742
- slide.addShape('rect', {
1743
- x: startX, y, w: col1W, h: rowH,
1744
- fill: { color: bg },
1745
- line: { color: COLORS.border, width: 1 },
1746
- });
1747
- slide.addShape('rect', {
1748
- x: startX + col1W + gap, y, w: col2W, h: rowH,
1749
- fill: { color: bg },
1750
- line: { color: COLORS.border, width: 1 },
1751
- });
1752
- slide.addShape('rect', {
1753
- x: startX + col1W + gap + col2W + gap, y, w: col3W, h: rowH,
1754
- fill: { color: bg },
1755
- line: { color: COLORS.border, width: 1 },
2768
+ if (props.footnoteLeft) {
2769
+ slide.addText(props.footnoteLeft, {
2770
+ x: 0.84375, y: y, w: 4, h: 0.25,
2771
+ fontSize: 10, color: COLORS.secondary, fontFace: FONTS.mono,
2772
+ valign: 'top',
1756
2773
  });
1757
- slide.addText(row.feature ?? '', {
1758
- x: startX + 0.15, y, w: col1W - 0.3, h: rowH,
1759
- fontSize: 13, color: COLORS.primary, align: 'left', valign: 'middle',
1760
- fontFace: FONTS.body,
1761
- });
1762
- slide.addText(row.left ?? '', {
1763
- x: startX + col1W + gap + 0.1, y, w: col2W - 0.2, h: rowH,
1764
- fontSize: 13, color: COLORS.primary, align: 'center', valign: 'middle',
1765
- fontFace: FONTS.body,
1766
- });
1767
- slide.addText(row.right ?? '', {
1768
- x: startX + col1W + gap + col2W + gap + 0.1, y, w: col3W - 0.2, h: rowH,
1769
- fontSize: 13, color: COLORS.primary, align: 'center', valign: 'middle',
1770
- fontFace: FONTS.body,
2774
+ }
2775
+ if (props.footnoteRight) {
2776
+ slide.addText(props.footnoteRight, {
2777
+ x: 5.15625, y: y, w: 4, h: 0.25,
2778
+ fontSize: 10, color: COLORS.secondary, fontFace: FONTS.mono,
2779
+ align: 'right', valign: 'top',
1771
2780
  });
1772
- });
2781
+ }
1773
2782
  }
1774
- function renderTimelineV3(slide, props) {
1775
- addKicker(slide, props.kicker);
1776
- addTitle(slide, props.title ?? 'TimelineV3');
1777
- const events = (props.events ?? []).slice(0, 4);
1778
- const startY = 2.6;
1779
- const rowH = 1.2;
1780
- const dotX = 1.2;
1781
- const cardX = 1.8;
1782
- const cardW = 7.4;
1783
- events.forEach((event, index) => {
1784
- const y = startY + index * rowH;
1785
- slide.addShape('ellipse', {
1786
- x: dotX - 0.12, y: y + 0.08, w: 0.24, h: 0.24,
1787
- fill: { color: COLORS.accent },
1788
- line: { color: COLORS.accent, width: 1 },
1789
- });
1790
- if (index < events.length - 1) {
1791
- slide.addShape('line', {
1792
- x1: dotX, y1: y + 0.36, x2: dotX, y2: y + rowH - 0.08,
1793
- line: { color: COLORS.border, width: 2 },
1794
- });
2783
+ export function getTheme03PricingFeatureValue(feature) {
2784
+ if (feature == null)
2785
+ return undefined;
2786
+ if (typeof feature === 'string')
2787
+ return feature;
2788
+ return feature.value;
2789
+ }
2790
+ export function getTheme03ComparisonValue(item) {
2791
+ if (item == null)
2792
+ return undefined;
2793
+ if (typeof item === 'string')
2794
+ return item;
2795
+ return item.value;
2796
+ }
2797
+ // ---- Theme04 renderers -----------------------------------------------------
2798
+ export function totalForSegments(segments, current) {
2799
+ const total = segments.reduce((sum, s) => sum + (Number(String(s.value || '0').replace(/,/g, '')) || 0), 0);
2800
+ const value = Number(String(current.value || '0').replace(/,/g, '')) || 0;
2801
+ if (total <= 0)
2802
+ return '0';
2803
+ return String(Math.round((value / total) * 100));
2804
+ }
2805
+ export function layoutTreemap(items, x, y, w, h) {
2806
+ const validItems = (items || []).filter((it) => (it.value ?? 0) > 0);
2807
+ if (validItems.length === 0)
2808
+ return [];
2809
+ if (validItems.length === 1) {
2810
+ return [{ x, y, w, h, item: validItems[0] }];
2811
+ }
2812
+ const total = validItems.reduce((sum, it) => sum + (it.value ?? 0), 0);
2813
+ const sorted = [...validItems].sort((a, b) => (b.value ?? 0) - (a.value ?? 0));
2814
+ // Split direction: favor the longer dimension
2815
+ const vertical = w >= h;
2816
+ const results = [];
2817
+ let currentPos = vertical ? x : y;
2818
+ const primary = vertical ? w : h;
2819
+ sorted.forEach((item, idx) => {
2820
+ const ratio = (item.value ?? 0) / total;
2821
+ const size = idx === sorted.length - 1 ? (vertical ? x + w - currentPos : y + h - currentPos) : primary * ratio;
2822
+ if (vertical) {
2823
+ results.push({ x: currentPos, y, w: size, h, item });
2824
+ currentPos += size;
1795
2825
  }
1796
- slide.addShape('rect', {
1797
- x: cardX, y, w: cardW, h: rowH - 0.15,
1798
- fill: { color: index % 2 === 0 ? COLORS.surface : COLORS.surfaceElevated },
1799
- line: { color: COLORS.border, width: 1 },
1800
- rectRadius: 0.08,
1801
- });
1802
- slide.addText(event.date ?? '', {
1803
- x: cardX + 0.2, y: y + 0.15, w: 2.0, h: 0.25,
1804
- fontSize: 12, color: COLORS.accent, bold: true, align: 'left', valign: 'top',
1805
- fontFace: FONTS.body,
1806
- });
1807
- slide.addText(event.title ?? '', {
1808
- x: cardX + 0.2, y: y + 0.42, w: cardW - 0.4, h: 0.3,
1809
- fontSize: 16, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
1810
- fontFace: FONTS.heading,
2826
+ else {
2827
+ results.push({ x, y: currentPos, w, h: size, item });
2828
+ currentPos += size;
2829
+ }
2830
+ });
2831
+ return results;
2832
+ }
2833
+ // ---- Theme06 深色图谱风版式渲染器 -----------------------------------------
2834
+ // ---- Theme06 Phase 3/4 新版式渲染器 ---------------------------------------
2835
+ // ---- Theme06 Phase 1 + Phase 2 新版式渲染器 --------------------------------
2836
+ // ---- Theme05 光谱报告风版式渲染器 -----------------------------------------
2837
+ // ---- Theme05 Phase A / Phase B 新增版式渲染器 -----------------------------
2838
+ // ---- Theme05 Phase B 新版式渲染器 -----------------------------------------
2839
+ // ---- Theme05 Phase C 版式渲染函数 -----------------------------------------
2840
+ // Theme05 光谱报告风版式注册
2841
+ // Phase A 回填
2842
+ // Phase B 新版式
2843
+ // Phase C 新版式
2844
+ // Theme06 深色图谱风版式注册
2845
+ // ==================== Theme07 冷白调研风 PPTX 渲染器 ====================
2846
+ // 坐标系:10" × 5.625",页面内边距 0.65",圆角 0.08"
2847
+ // 设计:冷白底 + 深墨文字 + 深靛蓝主色 + 衬线标题
2848
+ /** theme07 通用装饰线(底部渐变线) */
2849
+ export function addTheme07GlowLine(slide) {
2850
+ slide.addShape('line', {
2851
+ x1: 0.65, y1: 5.05, x2: 9.35, y2: 5.05,
2852
+ line: { color: COLORS.accent, width: 2, transparency: 60 },
2853
+ });
2854
+ }
2855
+ /** theme07 通用卡片(冷白底 + 细边框 + 圆角) */
2856
+ export function addTheme07Card(slide, x, y, w, h, radius = 0.08, fill = COLORS.surface) {
2857
+ slide.addShape('roundRect', {
2858
+ x, y, w, h,
2859
+ fill: { color: fill },
2860
+ line: { color: COLORS.border, width: 1 },
2861
+ rectRadius: radius,
2862
+ });
2863
+ }
2864
+ /** theme07 通用页脚 */
2865
+ export function addTheme07Footer(slide, props) {
2866
+ if (props.footnoteLeft) {
2867
+ slide.addText(props.footnoteLeft, {
2868
+ x: 0.65, y: 5.2, w: 4, h: 0.25,
2869
+ fontSize: 10, color: COLORS.secondary, fontFace: FONTS.mono,
1811
2870
  });
1812
- slide.addText(event.description ?? '', {
1813
- x: cardX + 0.2, y: y + 0.72, w: cardW - 0.4, h: 0.35,
1814
- fontSize: 12, color: COLORS.secondary, align: 'left', valign: 'top',
1815
- fontFace: FONTS.body,
2871
+ }
2872
+ if (props.footnoteRight) {
2873
+ slide.addText(props.footnoteRight, {
2874
+ x: 8.7, y: 5.2, w: 0.65, h: 0.25,
2875
+ fontSize: 10, color: COLORS.secondary, fontFace: FONTS.mono, align: 'right',
1816
2876
  });
1817
- });
2877
+ }
1818
2878
  }
1819
- // ---- PPTX renderer registration -------------------------------------------
1820
- // 按具体 layoutId 注册通用渲染器,保留同一角色下不同变体的差异。
1821
- registerPptxLayoutRenderer('cover_v1', renderCoverV1);
1822
- registerPptxLayoutRenderer('table_of_contents_v1', renderTableOfContentsV1);
1823
- registerPptxLayoutRenderer('metric_v1', renderMetricV1);
1824
- registerPptxLayoutRenderer('metric_v2', renderMetricV2);
1825
- registerPptxLayoutRenderer('metric_v3', renderMetricV3);
1826
- registerPptxLayoutRenderer('stats_v1', renderStatsV1);
1827
- registerPptxLayoutRenderer('chart_v1', renderChartV1);
1828
- registerPptxLayoutRenderer('chart_v2', renderChartV2);
1829
- registerPptxLayoutRenderer('content_v1', renderContentV1);
1830
- registerPptxLayoutRenderer('content_v2', renderContentV2);
1831
- registerPptxLayoutRenderer('content_v3', renderContentV3);
1832
- registerPptxLayoutRenderer('split_v1', renderSplitV1);
1833
- registerPptxLayoutRenderer('comparison_v1', renderComparisonV1);
1834
- registerPptxLayoutRenderer('comparison_v2', renderComparisonV2);
1835
- registerPptxLayoutRenderer('process_v1', renderProcessV1);
1836
- registerPptxLayoutRenderer('process_v2', renderProcessV2);
1837
- registerPptxLayoutRenderer('timeline_v1', renderTimelineV1);
1838
- registerPptxLayoutRenderer('timeline_v2', renderTimelineV2);
1839
- registerPptxLayoutRenderer('roadmap_v1', renderRoadmapV1);
1840
- registerPptxLayoutRenderer('roadmap_v2', renderRoadmapV2);
1841
- registerPptxLayoutRenderer('quote_v1', renderQuoteV1);
1842
- registerPptxLayoutRenderer('quote_v2', renderQuoteV2);
1843
- registerPptxLayoutRenderer('testimonial_v1', renderTestimonialV1);
1844
- registerPptxLayoutRenderer('testimonial_v2', renderTestimonialV2);
1845
- registerPptxLayoutRenderer('faq_v1', renderFaqV1);
1846
- registerPptxLayoutRenderer('feature_v1', renderFeatureV1);
1847
- registerPptxLayoutRenderer('feature_v2', renderFeatureV2);
1848
- registerPptxLayoutRenderer('team_v1', renderTeamV1);
1849
- registerPptxLayoutRenderer('team_v2', renderTeamV2);
1850
- registerPptxLayoutRenderer('partners_v1', renderPartnersV1);
1851
- registerPptxLayoutRenderer('pricing_v1', renderPricingV1);
1852
- registerPptxLayoutRenderer('pricing_v2', renderPricingV2);
1853
- registerPptxLayoutRenderer('gallery_v1', renderGalleryV1);
1854
- registerPptxLayoutRenderer('gallery_v2', renderGalleryV2);
1855
- registerPptxLayoutRenderer('image_v1', renderImageV1);
1856
- registerPptxLayoutRenderer('swot_v1', renderSwotV1);
1857
- registerPptxLayoutRenderer('pest_v1', renderPestV1);
1858
- registerPptxLayoutRenderer('closing_v1', renderClosingV1);
1859
- registerPptxLayoutRenderer('closing_v2', renderClosingV2);
1860
- registerPptxLayoutRenderer('timeline_v3', renderTimelineV3);
1861
- registerPptxLayoutRenderer('comparison_v3', renderComparisonV3);
1862
- registerPptxLayoutRenderer('testimonial_v3', renderTestimonialV3);
1863
- registerPptxLayoutRenderer('gallery_v3', renderGalleryV3);
1864
- registerPptxLayoutRenderer('feature_v3', renderFeatureV3);
1865
- registerPptxLayoutRenderer('process_v3', renderProcessV3);
1866
- registerPptxLayoutRenderer('quote_v3', renderQuoteV3);
1867
- registerPptxLayoutRenderer('image_v2', renderImageV2);
1868
- registerPptxLayoutRenderer('stats_v2', renderStatsV2);
1869
- registerPptxLayoutRenderer('content_v4', renderContentV4);
1870
- registerPptxLayoutRenderer('cover_v2', renderCoverV2);
1871
- // 主题专属变体(阶段 3)
1872
- registerPptxLayoutRenderer('dark_tech_cover_v1', renderDarkTechCoverV1);
1873
- registerPptxLayoutRenderer('dark_tech_feature_v1', renderDarkTechFeatureV1);
1874
- registerPptxLayoutRenderer('dark_tech_metric_v1', renderDarkTechMetricV1);
1875
- registerPptxLayoutRenderer('warm_business_timeline_v1', renderWarmBusinessTimelineV1);
1876
- registerPptxLayoutRenderer('warm_business_testimonial_v1', renderWarmBusinessTestimonialV1);
2879
+ /** theme07 通用顶部标签(kicker) */
2880
+ export function addTheme07Kicker(slide, text, x = 0.65, y = 0.55) {
2881
+ slide.addText(text, {
2882
+ x, y, w: 8, h: 0.3,
2883
+ fontSize: 11, color: COLORS.accent, bold: true, fontFace: FONTS.mono,
2884
+ charSpacing: 2,
2885
+ });
2886
+ }
2887
+ /** 将 theme07 CSS 变量名解析为 PPTX 可用 HEX(去掉 var(--lp-...) 包装) */
2888
+ export function resolveTheme07CssColor(value, fallback = COLORS.accent) {
2889
+ if (!value)
2890
+ return fallback;
2891
+ const inner = value.replace('var(--lp-', '').replace(')', '').trim();
2892
+ const map = {
2893
+ accent: '2E5C8A',
2894
+ 'accent-2': '7B9E7B',
2895
+ 'accent-cool': 'B8623E',
2896
+ blue: '5B9BD5',
2897
+ ink: '1A1D23',
2898
+ ink2: '1A1D23',
2899
+ ink3: '6B7280',
2900
+ red: 'C73E2F',
2901
+ orange: 'C25E00',
2902
+ amber: 'B45309',
2903
+ green: '15803D',
2904
+ teal: '0B7A85',
2905
+ cyan: '0B7A85',
2906
+ violet: '7C2ED3',
2907
+ purple: '7C2ED3',
2908
+ };
2909
+ const hex = map[inner];
2910
+ if (hex)
2911
+ return hex;
2912
+ // 若已经是 6 位 HEX 则直接返回
2913
+ if (/^[0-9A-Fa-f]{6}$/.test(inner))
2914
+ return inner;
2915
+ return fallback;
2916
+ }
2917
+ // ============ theme07 Phase 3/4 通用渲染器 ============
2918
+ /** theme07 通用赛道/垂直页渲染器(sector-layout) */
2919
+ /** theme07 通用表格/结构化数据页渲染器(table-layout) */
2920
+ /** theme07 通用公司案例页渲染器(company-layout) */
2921
+ /** theme07 通用地理分布页渲染器(geo-layout) */
2922
+ /** theme07 通用结尾/金句页渲染器(closing-layout) */
2923
+ // 收尾页(引语变体):居中大引语 + 关键词高亮(支持 [[关键词]] 语法)
2924
+ // ---- Theme07 大数字版式渲染器(stat 系列,新增 4 个) ------------------
2925
+ /** 单数字 Hero:巨号结论数字 + 单位 + 说明 */
2926
+ /** 多数字并列:2–4 组大数字横向并列 */
2927
+ /** 数字 + 图表:左侧巨号结论,右侧支撑柱状序列 */
2928
+ /** 数字对比:左右巨号对峙 + 中间差值徽标 */
2929
+ // Theme07 PPTX 渲染器注册
2930
+ // theme07 Phase 3/4 通用渲染器注册
2931
+ const THEME07_SECTOR_IDS = [
2932
+ 'theme07_knowledge_v1', 'theme07_legal_v1', 'theme07_healthcare_v1', 'theme07_finance_v1',
2933
+ 'theme07_compute_v1', 'theme07_chip_v1', 'theme07_robotics_v1', 'theme07_autonomy_v1',
2934
+ 'theme07_safety_v1', 'theme07_content_gen_v1', 'theme07_education_v1', 'theme07_support_v1',
2935
+ 'theme07_sales_v1', 'theme07_low_code_v1', 'theme07_open_source_v1', 'theme07_alignment_v1',
2936
+ ];
2937
+ THEME07_SECTOR_IDS.forEach((id) => registerPptxLayoutRenderer(id, renderTheme07SectorV1));
2938
+ const THEME07_TABLE_IDS = [
2939
+ 'theme07_early_stage_v1', 'theme07_deal_structure_v1', 'theme07_investor_mix_v1', 'theme07_resource_v1',
2940
+ 'theme07_alliance_v1', 'theme07_ecosystem_v1', 'theme07_revenue_v1', 'theme07_compliance_v1',
2941
+ 'theme07_margin_v1', 'theme07_moat_v1', 'theme07_strategy_infra_v1', 'theme07_strategy_vertical_v1',
2942
+ ];
2943
+ THEME07_TABLE_IDS.forEach((id) => registerPptxLayoutRenderer(id, renderTheme07TableV1));
2944
+ // theme08 黑金实验风:注册全部 39 个版式专属 PPTX 渲染器
2945
+ registerTheme08Renderers(registerPptxLayoutRenderer);
2946
+ // theme09 墨韵专色 · 杂志印刷风:注册 P0 全部 12 个版式专属 PPTX 渲染器
2947
+ registerTheme09Renderers(registerPptxLayoutRenderer);
2948
+ // theme10 金色指数 · 金融编辑风:注册 P0 全部 12 个版式专属 PPTX 渲染器
2949
+ registerTheme10Renderers(registerPptxLayoutRenderer);
2950
+ registerTheme01Renderers(registerPptxLayoutRenderer);
2951
+ registerTheme02Renderers(registerPptxLayoutRenderer);
2952
+ registerTheme03Renderers(registerPptxLayoutRenderer);
2953
+ registerTheme04Renderers(registerPptxLayoutRenderer);
2954
+ registerTheme05Renderers(registerPptxLayoutRenderer);
2955
+ registerTheme06Renderers(registerPptxLayoutRenderer);
2956
+ registerTheme07Renderers(registerPptxLayoutRenderer);
1877
2957
  //# sourceMappingURL=export-pptx.js.map