@lemonppt/renderer 1.0.7 → 1.0.9

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