@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,2957 +0,0 @@
1
- // lemonPPT - AI-powered presentation generation
2
- // Copyright (c) 2026 lemonforme
3
- // SPDX-License-Identifier: AGPL-3.0-or-later
4
- //
5
- // theme01 暖炭暗色编辑风 · espresso+terracotta — PPTX 版式专属渲染器(全量)。
6
- // 方法论对齐 theme09:双基底(light / tint)按版式预分配、共享原语层、callback 注入避免循环依赖。
7
- // 外观翻成 theme01 自有暖炭暗色(accent C56A43 陶土),不复制 theme09 纸/墨/朱砂或 #46e3c6/#4a86ff。
8
- //
9
- // 本文件完全自包含(仅依赖 pptxgenjs 与 node 内置模块),由 export-pptx.ts 通过
10
- // `registerTheme01Renderers(registerPptxLayoutRenderer)` 注入注册表,不反向 import export-pptx。
11
- import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
12
- import path from 'node:path';
13
- import crypto from 'node:crypto';
14
- import { tmpdir } from 'node:os';
15
- // ────────────────────────────────────────────────────────────────
16
- // 暖炭暗色编辑风调色板(与 templates 端 theme01Tokens 同源,PPTX 不支持 color-mix 故固化)
17
- // ────────────────────────────────────────────────────────────────
18
- const FONTS = { heading: 'Inter', body: 'Inter', mono: 'JetBrains Mono' };
19
- // 与 templates 端 theme01ColorSequence 同源(9 色大地序列,PPTX 不支持 color-mix 故固化)
20
- const CHART_COLORS = ['6E9BC4', '8FAE84', 'D9A441', 'C76B5A', 'A07CAE', 'C98BA0', '5FA8A0', 'C97E4E', 'A7A85F'];
21
- const LIGHT = {
22
- bg: '1E1B16',
23
- primary: 'F2E9DC',
24
- secondary: 'C9BCA8',
25
- accent: 'C56A43',
26
- white: 'F2E9DC',
27
- light: '2C2820',
28
- border: '3A342A',
29
- surface: '28231C',
30
- surfaceElevated: '322C23',
31
- fonts: FONTS,
32
- chartColors: CHART_COLORS,
33
- };
34
- // tint 基底:深墨 ink(仅背景节奏变化,强调色与明暗不翻转)
35
- const TINT = {
36
- ...LIGHT,
37
- bg: '16130F',
38
- surfaceElevated: '2C271F',
39
- };
40
- // 双基底预分配:每个版式固定基底,翻页形成呼吸节奏
41
- const SUBSTRATE = {
42
- 'theme01_appendix_v1': 'light',
43
- 'theme01_bento_v1': 'light',
44
- 'theme01_case_study': 'light',
45
- 'theme01_chapter_v1': 'light',
46
- 'theme01_chapter_v2': 'light',
47
- 'theme01_chapter_v3': 'light',
48
- 'theme01_chart_bar3d': 'light',
49
- 'theme01_chart_donut': 'light',
50
- 'theme01_chart_funnel': 'light',
51
- 'theme01_chart_gauge': 'light',
52
- 'theme01_chart_graph': 'light',
53
- 'theme01_chart_heatmap': 'light',
54
- 'theme01_chart_radar': 'light',
55
- 'theme01_chart_sankey': 'light',
56
- 'theme01_chart_sunburst': 'light',
57
- 'theme01_chart_treemap': 'light',
58
- 'theme01_chart_v1': 'light',
59
- 'theme01_chart_wordcloud': 'light',
60
- 'theme01_closing_v2': 'tint',
61
- 'theme01_comparison_v1': 'light',
62
- 'theme01_comparison_v2': 'light',
63
- 'theme01_comparison_v3': 'light',
64
- 'theme01_conclusion_v1': 'light',
65
- 'theme01_content_v1': 'light',
66
- 'theme01_content_v2': 'light',
67
- 'theme01_content_v3': 'light',
68
- 'theme01_content_v4': 'light',
69
- 'theme01_cover_v1': 'light',
70
- 'theme01_cover_v2': 'light',
71
- 'theme01_cover_v3': 'light',
72
- 'theme01_cover_v4': 'light',
73
- 'theme01_diptych_contrast': 'light',
74
- 'theme01_faq_v1': 'light',
75
- 'theme01_feature_v1': 'light',
76
- 'theme01_feature_v2': 'light',
77
- 'theme01_filmstrip_v1': 'light',
78
- 'theme01_gallery_v1': 'light',
79
- 'theme01_gantt_v1': 'light',
80
- 'theme01_image_v1': 'light',
81
- 'theme01_metric_big': 'light',
82
- 'theme01_metric_triptych': 'light',
83
- 'theme01_metric_v1': 'light',
84
- 'theme01_metric_v2': 'light',
85
- 'theme01_metric_v3': 'light',
86
- 'theme01_outlook_v1': 'light',
87
- 'theme01_partners_v1': 'light',
88
- 'theme01_pest_v1': 'light',
89
- 'theme01_pricing_v1': 'light',
90
- 'theme01_process_v1': 'light',
91
- 'theme01_quadrant_v1': 'light',
92
- 'theme01_quote_v1': 'light',
93
- 'theme01_quote_v2': 'light',
94
- 'theme01_quote_v3': 'light',
95
- 'theme01_ranking_v1': 'light',
96
- 'theme01_region_v1': 'light',
97
- 'theme01_risk_v1': 'light',
98
- 'theme01_roadmap_v1': 'light',
99
- 'theme01_scorecard_v1': 'light',
100
- 'theme01_spotlight_grid': 'light',
101
- 'theme01_stats_v1': 'light',
102
- 'theme01_swot_v1': 'light',
103
- 'theme01_table_data': 'light',
104
- 'theme01_table_of_contents_v1': 'light',
105
- 'theme01_table_of_contents_v2': 'light',
106
- 'theme01_table_v1': 'light',
107
- 'theme01_tags_v1': 'light',
108
- 'theme01_team_v1': 'light',
109
- 'theme01_team_v2': 'light',
110
- 'theme01_testimonial_v1': 'light',
111
- 'theme01_timeline_v1': 'light',
112
- 'theme01_trend_v1': 'light',
113
- 'theme01_components_v1': 'light',
114
- };
115
- function P(layoutId) {
116
- const sub = SUBSTRATE[layoutId] ?? 'light';
117
- const base = sub === 'tint' ? TINT : LIGHT;
118
- return { sub, isTint: sub === 'tint', ...base };
119
- }
120
- // 模块级当前调色板(按版式在 registerTheme01Renderers 的包装器里注入 COLORS = P(id))
121
- let COLORS = { sub: 'light', isTint: false, ...LIGHT };
122
- // ────────────────────────────────────────────────────────────────
123
- // 画布常量(16:9,10 × 5.625 英寸)
124
- // ────────────────────────────────────────────────────────────────
125
- const MX = 0.8;
126
- function applyBackground(slide) {
127
- slide.background = { color: COLORS.bg };
128
- if (COLORS.isTint) {
129
- slide.addShape('rect', {
130
- x: 0, y: 0, w: 10, h: 5.625,
131
- fill: {
132
- color: '241C14',
133
- gradient: { type: 'linear', angle: 90, stops: [{ position: 0, color: '241C14' }, { position: 100, color: '16130F' }] },
134
- },
135
- line: { type: 'none' },
136
- });
137
- }
138
- }
139
- // ────────────────────────────────────────────────────────────────
140
- // 临时图片(data: URI → 临时文件,pptxgenjs 在 Node 环境只能读取本地路径)
141
- // ────────────────────────────────────────────────────────────────
142
- let exportTmpDir = null;
143
- const dataUriToTempPath = new Map();
144
- function getExportTmpDir() {
145
- if (!exportTmpDir) {
146
- try {
147
- exportTmpDir = mkdtempSync(path.join(tmpdir(), 'lemonppt-t01-'));
148
- }
149
- catch {
150
- return null;
151
- }
152
- }
153
- return exportTmpDir;
154
- }
155
- function dataUriToTempFile(url) {
156
- const cached = dataUriToTempPath.get(url);
157
- if (cached)
158
- return cached;
159
- const match = url.match(/^data:([^;]+);base64,(.+)$/);
160
- if (!match)
161
- return undefined;
162
- const mime = match[1];
163
- const base64 = match[2];
164
- const ext = mime === 'image/svg+xml' ? 'svg' : mime === 'image/png' ? 'png' : mime === 'image/jpeg' ? 'jpg' : mime === 'image/webp' ? 'webp' : 'bin';
165
- try {
166
- const buffer = Buffer.from(base64, 'base64');
167
- const tmpDir = getExportTmpDir();
168
- if (!tmpDir)
169
- return undefined;
170
- const fileName = `img-${crypto.createHash('sha256').update(buffer).digest('hex').slice(0, 16)}.${ext}`;
171
- const filePath = path.join(tmpDir, fileName);
172
- writeFileSync(filePath, buffer);
173
- dataUriToTempPath.set(url, filePath);
174
- return filePath;
175
- }
176
- catch {
177
- return undefined;
178
- }
179
- }
180
- function addImageMaybe(slide, url, x, y, w, h) {
181
- if (!url)
182
- return;
183
- let pathOrUrl = url;
184
- if (url.startsWith('data:')) {
185
- const tempPath = dataUriToTempFile(url);
186
- if (!tempPath) {
187
- slide.addText('[图片]', { x, y, w, h, fontSize: 14, color: '6E6E73', align: 'center', valign: 'middle' });
188
- return;
189
- }
190
- pathOrUrl = tempPath;
191
- }
192
- try {
193
- slide.addImage({ path: pathOrUrl, x, y, w, h, sizing: { type: 'crop', w, h } });
194
- }
195
- catch {
196
- slide.addText('[图片]', { x, y, w, h, fontSize: 14, color: '6E6E73', align: 'center', valign: 'middle' });
197
- }
198
- }
199
- export function cleanupTheme01TempImages() {
200
- dataUriToTempPath.clear();
201
- if (exportTmpDir) {
202
- try {
203
- rmSync(exportTmpDir, { recursive: true, force: true });
204
- }
205
- catch { /* ignore */ }
206
- exportTmpDir = null;
207
- }
208
- }
209
- // ────────────────────────────────────────────────────────────────
210
- // 共享原语助手(COLORS 风格,对齐 theme09 的 kicker/title/footer 写法)
211
- // ────────────────────────────────────────────────────────────────
212
- function addKicker(slide, kicker, x = MX, y = 0.8) {
213
- if (!kicker)
214
- return;
215
- slide.addText(kicker, { x, y, w: 8.4, h: 0.4, fontSize: 14, color: COLORS.accent, align: 'left', fontFace: FONTS.mono });
216
- }
217
- function addTitle(slide, title, x = MX, y = 1.3, w = 8.4, h = 0.9, fontSize = 44) {
218
- slide.addText(title, { x, y, w, h, fontSize, color: COLORS.primary, bold: true, align: 'left', valign: 'top', fontFace: FONTS.heading });
219
- }
220
- function addBulletList(slide, items, x, y, w, maxItems = 8) {
221
- const list = (items || []).slice(0, maxItems);
222
- let cy = y;
223
- for (const item of list) {
224
- slide.addShape('ellipse', { x: x + 0.05, y: cy + 0.16, w: 0.1, h: 0.1, fill: { color: COLORS.accent } });
225
- slide.addText(item, { x: x + 0.3, y: cy, w: w - 0.3, h: 0.55, fontSize: 20, color: COLORS.primary, align: 'left', valign: 'top', fontFace: FONTS.body });
226
- cy += 0.7;
227
- }
228
- return cy;
229
- }
230
- function renderInsightPanel(slide, insight, x, y, w, h) {
231
- if (!insight || (!insight.value && !insight.label && !insight.description))
232
- return;
233
- slide.addShape('roundRect', { x, y, w, h, fill: { color: COLORS.surfaceElevated }, line: { color: COLORS.border, width: 1 }, rectRadius: 0.15 });
234
- let cursorY = y + 0.25;
235
- if (insight.value) {
236
- slide.addText(insight.value, { x: x + 0.2, y: cursorY, w: w - 0.4, h: 0.65, fontSize: 36, color: COLORS.accent, bold: true, align: 'left', valign: 'top', fontFace: FONTS.heading });
237
- cursorY += 0.62;
238
- }
239
- if (insight.label) {
240
- slide.addText(insight.label, { x: x + 0.2, y: cursorY, w: w - 0.4, h: 0.3, fontSize: 11, color: COLORS.secondary, align: 'left', valign: 'top', fontFace: FONTS.body });
241
- cursorY += 0.38;
242
- }
243
- if (insight.description) {
244
- slide.addText(insight.description, { x: x + 0.2, y: cursorY, w: w - 0.4, h: h - (cursorY - y) - 0.25, fontSize: 12, color: COLORS.secondary, align: 'left', valign: 'top', fontFace: FONTS.body });
245
- }
246
- }
247
- // ────────────────────────────────────────────────────────────────
248
- // 试点版式渲染器(COLORS 风格,逻辑与 export-pptx 原实现一致)
249
- // ────────────────────────────────────────────────────────────────
250
- export function renderCoverV1(slide, props) {
251
- const p = props;
252
- COLORS = P('theme01_cover_v1');
253
- applyBackground(slide);
254
- if (p.image)
255
- addImageMaybe(slide, p.image, 0, 0, 10, 5.625);
256
- if (p.kicker)
257
- addKicker(slide, p.kicker, 1, 2.0);
258
- addTitle(slide, p.title, 1, 2.5, 8, 1.2, 54);
259
- if (p.subtitle) {
260
- slide.addText(p.subtitle, { x: 1.5, y: 3.8, w: 7, h: 0.8, fontSize: 22, color: COLORS.secondary, align: 'center', fontFace: FONTS.body });
261
- }
262
- if (p.date) {
263
- slide.addText(p.date, { x: 1, y: 5.0, w: 8, h: 0.3, fontSize: 14, color: COLORS.secondary, align: 'center', fontFace: FONTS.mono });
264
- }
265
- }
266
- export function renderChartV1(slide, props) {
267
- const p = props;
268
- COLORS = P('theme01_chart_v1');
269
- applyBackground(slide);
270
- if (p.kicker)
271
- addKicker(slide, p.kicker);
272
- if (p.title)
273
- addTitle(slide, p.title);
274
- const labels = p.labels || [];
275
- const data = p.data || [];
276
- const insight = p.insight || {};
277
- const hasInsight = !!insight.headline || !!insight.subheadline || (insight.items && insight.items.length > 0) || !!insight.badge?.text;
278
- if (labels.length === 0 || data.length === 0) {
279
- slide.addText('(暂无图表数据)', { x: 2, y: 3.2, w: 6, h: 0.8, fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle', fontFace: FONTS.body });
280
- return;
281
- }
282
- const chartType = p.type || 'bar';
283
- const chartColors = chartType === 'pie'
284
- ? data.map((_, i) => COLORS.chartColors[i % COLORS.chartColors.length])
285
- : [COLORS.accent];
286
- const chartData = [{ name: p.title || '', labels, values: data }];
287
- const chartW = hasInsight ? 5.6 : 8.4;
288
- slide.addChart(chartType, chartData, {
289
- x: 0.6, y: 2.3, w: chartW, h: 3.2, chartColors, showValue: true, dataLabelColor: COLORS.primary, dataLabelFontSize: 10,
290
- });
291
- if (hasInsight) {
292
- slide.addShape('roundRect', { x: 6.5, y: 2.3, w: 3.0, h: 3.2, fill: { color: 'F2E9DC', transparency: 90 }, line: { color: '3A342A', width: 1 }, rectRadius: 0.15 });
293
- let cursorY = 2.5;
294
- if (insight.headline) {
295
- slide.addText(insight.headline, { x: 6.7, y: cursorY, w: 2.6, h: 0.6, fontSize: 34, fontFace: FONTS.heading, bold: true, color: COLORS.primary });
296
- cursorY += 0.6;
297
- }
298
- if (insight.subheadline) {
299
- slide.addText(insight.subheadline, { x: 6.7, y: cursorY, w: 2.6, h: 0.3, fontSize: 10, fontFace: FONTS.body, color: COLORS.secondary });
300
- cursorY += 0.45;
301
- }
302
- if (insight.items && insight.items.length > 0) {
303
- cursorY += 0.1;
304
- insight.items.forEach((item) => {
305
- slide.addText(item.label || '', { x: 6.7, y: cursorY, w: 1.3, h: 0.28, fontSize: 11, fontFace: FONTS.body, color: COLORS.secondary });
306
- slide.addText(item.value || '', { x: 8.0, y: cursorY, w: 1.3, h: 0.28, fontSize: 12, fontFace: FONTS.body, bold: true, color: COLORS.primary, align: 'right' });
307
- cursorY += 0.34;
308
- });
309
- }
310
- if (insight.badge?.text) {
311
- const badgeColor = insight.badge.tone === 'success' ? '8FAE84' : insight.badge.tone === 'warning' ? 'D9A441' : COLORS.accent;
312
- cursorY += 0.1;
313
- slide.addShape('roundRect', { x: 6.7, y: cursorY, w: 2.0, h: 0.32, fill: { color: badgeColor }, rectRadius: 0.16 });
314
- slide.addText(insight.badge.text, { x: 6.7, y: cursorY, w: 2.0, h: 0.32, fontSize: 10, fontFace: FONTS.body, bold: true, color: 'FFFFFF', align: 'center', valign: 'middle' });
315
- }
316
- }
317
- if (p.footnote || p.unit) {
318
- const footerText = [p.unit ? `单位:${p.unit}` : '', p.footnote || ''].filter(Boolean).join(' · ');
319
- slide.addText(footerText, { x: 0.6, y: 5.75, w: 8.9, h: 0.3, fontSize: 11, fontFace: FONTS.body, color: COLORS.secondary });
320
- }
321
- }
322
- export function renderContentV1(slide, props) {
323
- const p = props;
324
- COLORS = P('theme01_content_v1');
325
- applyBackground(slide);
326
- if (p.kicker)
327
- addKicker(slide, p.kicker);
328
- if (p.title)
329
- addTitle(slide, p.title);
330
- addBulletList(slide, p.points || [], MX, 2.8, 8.4);
331
- }
332
- export function renderComparisonV1(slide, props) {
333
- const p = props;
334
- COLORS = P('theme01_comparison_v1');
335
- applyBackground(slide);
336
- if (p.kicker)
337
- addKicker(slide, p.kicker);
338
- if (p.title)
339
- addTitle(slide, p.title);
340
- renderComparisonColumns(slide, p.leftTitle || '方案 A', p.leftPoints || [], p.rightTitle || '方案 B', p.rightPoints || []);
341
- }
342
- function renderComparisonColumns(slide, leftTitle, leftPoints, rightTitle, rightPoints) {
343
- slide.addText(leftTitle, { x: MX, y: 2.4, w: 4.0, h: 0.5, fontSize: 24, color: COLORS.primary, bold: true, align: 'left', fontFace: FONTS.heading });
344
- slide.addText(rightTitle, { x: 5.2, y: 2.4, w: 4.0, h: 0.5, fontSize: 24, color: COLORS.primary, bold: true, align: 'left', fontFace: FONTS.heading });
345
- addBulletList(slide, leftPoints, MX, 3.0, 4.0);
346
- addBulletList(slide, rightPoints, 5.2, 3.0, 4.0);
347
- }
348
- export function renderClosing(slide, props) {
349
- const p = props;
350
- COLORS = P('theme01_closing_v2');
351
- applyBackground(slide);
352
- let y = 2.2;
353
- if (p.kicker) {
354
- slide.addText(p.kicker, { x: 1, y, w: 8, h: 0.4, fontSize: 14, color: COLORS.accent, align: 'center', fontFace: FONTS.mono });
355
- y += 0.5;
356
- }
357
- slide.addText(p.title, { x: 1, y, w: 8, h: 1.0, fontSize: 48, color: COLORS.primary, bold: true, align: 'center', fontFace: FONTS.heading });
358
- y += 1.1;
359
- if (p.subtitle) {
360
- slide.addText(p.subtitle, { x: 1.5, y, w: 7, h: 0.6, fontSize: 22, color: COLORS.secondary, align: 'center', fontFace: FONTS.body });
361
- y += 0.9;
362
- }
363
- if (p.cta) {
364
- slide.addShape('roundRect', { x: 3.5, y, w: 3, h: 0.6, fill: { color: COLORS.accent }, rectRadius: 0.3 });
365
- slide.addText(p.cta, { x: 3.5, y, w: 3, h: 0.6, fontSize: 16, color: COLORS.white, bold: true, align: 'center', valign: 'middle', fontFace: FONTS.body });
366
- y += 0.9;
367
- }
368
- const lines = [p.contact, p.email, p.link].filter(Boolean);
369
- lines.forEach((line) => {
370
- slide.addText(line, { x: 1, y, w: 8, h: 0.35, fontSize: 16, color: COLORS.accent, align: 'center', fontFace: FONTS.mono });
371
- y += 0.45;
372
- });
373
- }
374
- // ────────────────────────────────────────────────────────────────
375
- // 全量版式渲染器(从 export-pptx.ts 搬移,逻辑逐字一致,props 改为 any)
376
- // ────────────────────────────────────────────────────────────────
377
- function render2x2Grid(slide, cells) {
378
- const positions = [
379
- { x: 0.8, y: 2.5 },
380
- { x: 5.2, y: 2.5 },
381
- { x: 0.8, y: 4.1 },
382
- { x: 5.2, y: 4.1 },
383
- ];
384
- cells.forEach((cell, index) => {
385
- const pos = positions[index];
386
- if (!pos)
387
- return;
388
- slide.addShape('rect', {
389
- x: pos.x, y: pos.y, w: 4.0, h: 1.4,
390
- fill: { color: COLORS.light },
391
- line: { color: COLORS.border, width: 1 },
392
- });
393
- slide.addText(cell.title, {
394
- x: pos.x + 0.15, y: pos.y + 0.1, w: 3.7, h: 0.35,
395
- fontSize: 16, color: COLORS.accent, bold: true, align: 'left',
396
- fontFace: FONTS.heading,
397
- });
398
- slide.addText(cell.text, {
399
- x: pos.x + 0.15, y: pos.y + 0.5, w: 3.7, h: 0.8,
400
- fontSize: 13, color: COLORS.primary, align: 'left', valign: 'top',
401
- fontFace: FONTS.body,
402
- });
403
- });
404
- }
405
- function renderChartBar3d(slide, props) {
406
- addKicker(slide, props.kicker);
407
- if (props.title)
408
- addTitle(slide, props.title);
409
- const labels = props.labels || [];
410
- const data = props.data || [];
411
- if (labels.length === 0 || data.length === 0) {
412
- slide.addText('(暂无图表数据)', {
413
- x: 2, y: 3.2, w: 6, h: 0.8,
414
- fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
415
- fontFace: FONTS.body,
416
- });
417
- return;
418
- }
419
- const chartColors = data.map((_, i) => CHART_COLORS[i % CHART_COLORS.length]);
420
- slide.addChart('bar', [{ name: props.title || '数值', labels, values: data }], {
421
- x: 0.8, y: 2.3, w: 8.4, h: 3.2,
422
- chartColors,
423
- showValue: true,
424
- dataLabelColor: COLORS.primary,
425
- dataLabelFontSize: 10,
426
- });
427
- }
428
- function renderChartFunnel(slide, props) {
429
- if (props.kicker)
430
- addKicker(slide, props.kicker);
431
- if (props.title)
432
- addTitle(slide, props.title);
433
- const raw = props.data || [];
434
- const insight = props.insight || {};
435
- const hasInsight = !!insight.value || !!insight.label || !!insight.description;
436
- if (raw.length === 0) {
437
- slide.addText('(暂无图表数据)', {
438
- x: 2, y: 3.2, w: 6, h: 0.8,
439
- fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
440
- fontFace: FONTS.body,
441
- });
442
- return;
443
- }
444
- const labels = raw.map((d) => d.name);
445
- const values = raw.map((d) => d.value);
446
- const chartColors = raw.map((_, i) => CHART_COLORS[i % CHART_COLORS.length]);
447
- const chartW = hasInsight ? 5.6 : 8.4;
448
- // PptxGenJS 没有真正的漏斗图,使用横向柱状图模拟漏斗层级
449
- slide.addChart('bar', [{ name: props.title || '漏斗', labels, values }], {
450
- x: 0.6, y: 2.3, w: chartW, h: 3.2,
451
- chartColors,
452
- showValue: true,
453
- dataLabelColor: COLORS.primary,
454
- dataLabelFontSize: 10,
455
- });
456
- if (hasInsight) {
457
- slide.addShape('roundRect', {
458
- x: 6.5, y: 2.3, w: 3.0, h: 3.2,
459
- fill: { color: 'F2E9DC', transparency: 90 },
460
- line: { color: '3A342A', width: 1 },
461
- rectRadius: 0.15,
462
- });
463
- let cursorY = 2.6;
464
- if (insight.value) {
465
- slide.addText(insight.value, {
466
- x: 6.7, y: cursorY, w: 2.6, h: 0.6,
467
- fontSize: 34, fontFace: FONTS.heading, bold: true, color: COLORS.primary,
468
- });
469
- cursorY += 0.6;
470
- }
471
- if (insight.label) {
472
- slide.addText(insight.label, {
473
- x: 6.7, y: cursorY, w: 2.6, h: 0.3,
474
- fontSize: 10, fontFace: FONTS.body, color: COLORS.secondary,
475
- });
476
- cursorY += 0.45;
477
- }
478
- if (insight.description) {
479
- cursorY += 0.15;
480
- slide.addText(insight.description, {
481
- x: 6.7, y: cursorY, w: 2.6, h: 1.6,
482
- fontSize: 11, fontFace: FONTS.body, color: COLORS.secondary,
483
- valign: 'top',
484
- });
485
- }
486
- }
487
- if (props.footnote) {
488
- slide.addText(props.footnote, {
489
- x: 0.6, y: 5.75, w: 8.9, h: 0.3,
490
- fontSize: 11, fontFace: FONTS.body, color: COLORS.secondary,
491
- });
492
- }
493
- }
494
- function renderChartGauge(slide, props) {
495
- addKicker(slide, props.kicker);
496
- if (props.title)
497
- addTitle(slide, props.title);
498
- const value = props.value ?? 0;
499
- const min = props.min ?? 0;
500
- const max = Math.max(props.max ?? 100, min + 1);
501
- const unit = props.unit || '';
502
- const percentage = Math.min(100, Math.max(0, ((value - min) / (max - min)) * 100));
503
- slide.addText(`${value}${unit}`, {
504
- x: 0, y: 2.4, w: 10, h: 1.0,
505
- fontSize: 48, bold: true, color: COLORS.accent,
506
- align: 'center', valign: 'middle', fontFace: FONTS.heading,
507
- });
508
- slide.addText(`${percentage.toFixed(1)}%`, {
509
- x: 0, y: 3.4, w: 10, h: 0.5,
510
- fontSize: 18, color: COLORS.secondary,
511
- align: 'center', valign: 'middle', fontFace: FONTS.body,
512
- });
513
- // pptxgenjs 3.12 运行时支持 doughnut,但当前类型定义较旧,使用 as any 绕过。
514
- slide.addChart('doughnut', [
515
- { name: '已完成', labels: ['已完成', '未完成'], values: [percentage, 100 - percentage] },
516
- ], {
517
- x: 3, y: 4.0, w: 4, h: 1.5,
518
- chartColors: [COLORS.accent, COLORS.border],
519
- showValue: false,
520
- holeSize: 60,
521
- });
522
- const insight = props.insight;
523
- const hasInsight = !!insight && (!!insight.value || !!insight.label || !!insight.description);
524
- if (hasInsight) {
525
- slide.addShape('roundRect', {
526
- x: 6.5, y: 2.3, w: 3.0, h: 3.2,
527
- fill: { color: COLORS.surfaceElevated },
528
- line: { color: COLORS.border, width: 1 },
529
- rectRadius: 0.15,
530
- });
531
- let cursorY = 2.6;
532
- if (insight.value) {
533
- slide.addText(insight.value, {
534
- x: 6.7, y: cursorY, w: 2.6, h: 0.6,
535
- fontSize: 34, fontFace: FONTS.heading, bold: true, color: COLORS.accent,
536
- });
537
- cursorY += 0.6;
538
- }
539
- if (insight.label) {
540
- slide.addText(insight.label, {
541
- x: 6.7, y: cursorY, w: 2.6, h: 0.3,
542
- fontSize: 10, fontFace: FONTS.body, color: COLORS.secondary,
543
- });
544
- cursorY += 0.45;
545
- }
546
- if (insight.description) {
547
- cursorY += 0.15;
548
- slide.addText(insight.description, {
549
- x: 6.7, y: cursorY, w: 2.6, h: 1.6,
550
- fontSize: 11, fontFace: FONTS.body, color: COLORS.secondary,
551
- valign: 'top',
552
- });
553
- }
554
- }
555
- }
556
- function renderChartGraph(slide, props) {
557
- addKicker(slide, props.kicker);
558
- if (props.title)
559
- addTitle(slide, props.title);
560
- const nodes = props.nodes || [];
561
- if (nodes.length === 0) {
562
- slide.addText('(暂无图表数据)', {
563
- x: 2, y: 3.2, w: 6, h: 0.8,
564
- fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
565
- fontFace: FONTS.body,
566
- });
567
- return;
568
- }
569
- const sorted = [...nodes]
570
- .sort((a, b) => (b.value ?? 0) - (a.value ?? 0))
571
- .slice(0, 12);
572
- const labels = sorted.map((d) => d.name);
573
- const values = sorted.map((d) => d.value ?? 0);
574
- const chartColors = sorted.map((_, i) => CHART_COLORS[i % CHART_COLORS.length]);
575
- slide.addChart('bar', [{ name: props.title || '节点权重', labels, values }], {
576
- x: 0.8, y: 2.3, w: 8.4, h: 3.2,
577
- chartColors,
578
- showValue: true,
579
- dataLabelColor: COLORS.primary,
580
- dataLabelFontSize: 10,
581
- });
582
- }
583
- function renderChartHeatmap(slide, props) {
584
- addKicker(slide, props.kicker);
585
- if (props.title)
586
- addTitle(slide, props.title);
587
- const raw = props.data || [];
588
- const flat = raw.map((cell) => ({ name: `${cell[0]} / ${cell[1]}`, value: cell[2] }));
589
- const sorted = flat.sort((a, b) => b.value - a.value).slice(0, 12);
590
- const insight = props.insight;
591
- const hasInsight = !!insight && (!!insight.value || !!insight.label || !!insight.description);
592
- if (sorted.length === 0) {
593
- slide.addText('(暂无图表数据)', {
594
- x: 2, y: 3.2, w: 6, h: 0.8,
595
- fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
596
- fontFace: FONTS.body,
597
- });
598
- return;
599
- }
600
- const labels = sorted.map((d) => d.name);
601
- const values = sorted.map((d) => d.value);
602
- const chartColors = sorted.map((_, i) => CHART_COLORS[i % CHART_COLORS.length]);
603
- const chartW = hasInsight ? 5.6 : 8.4;
604
- slide.addChart('bar', [{ name: props.title || '热度', labels, values }], {
605
- x: 0.8, y: 2.3, w: chartW, h: 3.2,
606
- chartColors,
607
- showValue: true,
608
- dataLabelColor: COLORS.primary,
609
- dataLabelFontSize: 10,
610
- });
611
- if (hasInsight) {
612
- slide.addShape('roundRect', {
613
- x: 6.5, y: 2.3, w: 3.0, h: 3.2,
614
- fill: { color: COLORS.surfaceElevated },
615
- line: { color: COLORS.border, width: 1 },
616
- rectRadius: 0.15,
617
- });
618
- let cursorY = 2.6;
619
- if (insight.value) {
620
- slide.addText(insight.value, {
621
- x: 6.7, y: cursorY, w: 2.6, h: 0.6,
622
- fontSize: 34, fontFace: FONTS.heading, bold: true, color: COLORS.accent,
623
- });
624
- cursorY += 0.6;
625
- }
626
- if (insight.label) {
627
- slide.addText(insight.label, {
628
- x: 6.7, y: cursorY, w: 2.6, h: 0.3,
629
- fontSize: 10, fontFace: FONTS.body, color: COLORS.secondary,
630
- });
631
- cursorY += 0.45;
632
- }
633
- if (insight.description) {
634
- cursorY += 0.15;
635
- slide.addText(insight.description, {
636
- x: 6.7, y: cursorY, w: 2.6, h: 1.6,
637
- fontSize: 11, fontFace: FONTS.body, color: COLORS.secondary,
638
- valign: 'top',
639
- });
640
- }
641
- }
642
- }
643
- function renderChartRadar(slide, props) {
644
- addKicker(slide, props.kicker);
645
- if (props.title)
646
- addTitle(slide, props.title);
647
- const indicators = props.indicators || [];
648
- const datasets = props.data || [];
649
- const insight = props.insight;
650
- const hasInsight = !!insight && (!!insight.value || !!insight.label || !!insight.description);
651
- if (indicators.length === 0 || datasets.length === 0) {
652
- slide.addText('(暂无图表数据)', {
653
- x: 2, y: 3.2, w: 6, h: 0.8,
654
- fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
655
- fontFace: FONTS.body,
656
- });
657
- return;
658
- }
659
- const labels = indicators.map((i) => i.name);
660
- const chartData = datasets.map((d) => ({
661
- name: d.name,
662
- labels,
663
- values: d.value,
664
- }));
665
- const chartColors = datasets.map((_, i) => CHART_COLORS[i % CHART_COLORS.length]);
666
- const chartW = hasInsight ? 5.6 : 8.4;
667
- // pptxgenjs 3.12 运行时支持 radar,但当前类型定义较旧,使用 as any 绕过。
668
- slide.addChart('radar', chartData, {
669
- x: 0.8, y: 2.3, w: chartW, h: 3.2,
670
- chartColors,
671
- showValue: true,
672
- dataLabelColor: COLORS.primary,
673
- dataLabelFontSize: 10,
674
- });
675
- if (hasInsight) {
676
- slide.addShape('roundRect', {
677
- x: 6.5, y: 2.3, w: 3.0, h: 3.2,
678
- fill: { color: COLORS.surfaceElevated },
679
- line: { color: COLORS.border, width: 1 },
680
- rectRadius: 0.15,
681
- });
682
- let cursorY = 2.6;
683
- if (insight.value) {
684
- slide.addText(insight.value, {
685
- x: 6.7, y: cursorY, w: 2.6, h: 0.6,
686
- fontSize: 34, fontFace: FONTS.heading, bold: true, color: COLORS.accent,
687
- });
688
- cursorY += 0.6;
689
- }
690
- if (insight.label) {
691
- slide.addText(insight.label, {
692
- x: 6.7, y: cursorY, w: 2.6, h: 0.3,
693
- fontSize: 10, fontFace: FONTS.body, color: COLORS.secondary,
694
- });
695
- cursorY += 0.45;
696
- }
697
- if (insight.description) {
698
- cursorY += 0.15;
699
- slide.addText(insight.description, {
700
- x: 6.7, y: cursorY, w: 2.6, h: 1.6,
701
- fontSize: 11, fontFace: FONTS.body, color: COLORS.secondary,
702
- valign: 'top',
703
- });
704
- }
705
- }
706
- }
707
- function renderChartSankey(slide, props) {
708
- addKicker(slide, props.kicker);
709
- if (props.title)
710
- addTitle(slide, props.title);
711
- const raw = props.data || [];
712
- const aggregated = new Map();
713
- raw.forEach((link) => {
714
- const key = `${link.source} → ${link.target}`;
715
- aggregated.set(key, (aggregated.get(key) ?? 0) + link.value);
716
- });
717
- const sorted = Array.from(aggregated.entries())
718
- .map(([name, value]) => ({ name, value }))
719
- .sort((a, b) => b.value - a.value)
720
- .slice(0, 10);
721
- if (sorted.length === 0) {
722
- slide.addText('(暂无图表数据)', {
723
- x: 2, y: 3.2, w: 6, h: 0.8,
724
- fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
725
- fontFace: FONTS.body,
726
- });
727
- return;
728
- }
729
- const labels = sorted.map((d) => d.name);
730
- const values = sorted.map((d) => d.value);
731
- const chartColors = sorted.map((_, i) => CHART_COLORS[i % CHART_COLORS.length]);
732
- slide.addChart('bar', [{ name: props.title || '流量', labels, values }], {
733
- x: 0.8, y: 2.3, w: 8.4, h: 3.2,
734
- chartColors,
735
- showValue: true,
736
- dataLabelColor: COLORS.primary,
737
- dataLabelFontSize: 10,
738
- });
739
- }
740
- function renderChartSunburst(slide, props) {
741
- addKicker(slide, props.kicker);
742
- if (props.title)
743
- addTitle(slide, props.title);
744
- const raw = props.data || [];
745
- const flat = raw.flatMap((item) => {
746
- if (item.children && item.children.length > 0) {
747
- return item.children.map((child) => ({ name: `${item.name} / ${child.name}`, value: child.value }));
748
- }
749
- return [{ name: item.name, value: item.value }];
750
- });
751
- const sorted = flat.sort((a, b) => b.value - a.value).slice(0, 10);
752
- if (sorted.length === 0) {
753
- slide.addText('(暂无图表数据)', {
754
- x: 2, y: 3.2, w: 6, h: 0.8,
755
- fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
756
- fontFace: FONTS.body,
757
- });
758
- return;
759
- }
760
- const labels = sorted.map((d) => d.name);
761
- const values = sorted.map((d) => d.value);
762
- const chartColors = sorted.map((_, i) => CHART_COLORS[i % CHART_COLORS.length]);
763
- slide.addChart('bar', [{ name: props.title || '数据', labels, values }], {
764
- x: 0.8, y: 2.3, w: 8.4, h: 3.2,
765
- chartColors,
766
- showValue: true,
767
- dataLabelColor: COLORS.primary,
768
- dataLabelFontSize: 10,
769
- });
770
- }
771
- function renderChartTreemap(slide, props) {
772
- addKicker(slide, props.kicker);
773
- if (props.title)
774
- addTitle(slide, props.title);
775
- const raw = props.data || [];
776
- // 把树状数据拍平为 (name, value) 列表,按 value 降序取前 10
777
- const flat = raw.flatMap((item) => {
778
- if (item.children && item.children.length > 0) {
779
- return item.children.map((child) => ({ name: `${item.name} / ${child.name}`, value: child.value }));
780
- }
781
- return [{ name: item.name, value: item.value }];
782
- });
783
- const sorted = flat.sort((a, b) => b.value - a.value).slice(0, 10);
784
- if (sorted.length === 0) {
785
- slide.addText('(暂无图表数据)', {
786
- x: 2, y: 3.2, w: 6, h: 0.8,
787
- fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
788
- fontFace: FONTS.body,
789
- });
790
- return;
791
- }
792
- const labels = sorted.map((d) => d.name);
793
- const values = sorted.map((d) => d.value);
794
- const chartColors = sorted.map((_, i) => CHART_COLORS[i % CHART_COLORS.length]);
795
- slide.addChart('bar', [{ name: props.title || '数据', labels, values }], {
796
- x: 0.8, y: 2.3, w: 8.4, h: 3.2,
797
- chartColors,
798
- showValue: true,
799
- dataLabelColor: COLORS.primary,
800
- dataLabelFontSize: 10,
801
- });
802
- }
803
- function renderChartWordcloud(slide, props) {
804
- addKicker(slide, props.kicker);
805
- if (props.title)
806
- addTitle(slide, props.title);
807
- const words = props.words || [];
808
- if (words.length === 0) {
809
- slide.addText('(暂无图表数据)', {
810
- x: 2, y: 3.2, w: 6, h: 0.8,
811
- fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
812
- fontFace: FONTS.body,
813
- });
814
- return;
815
- }
816
- const sorted = [...words].sort((a, b) => b.value - a.value).slice(0, 12);
817
- const labels = sorted.map((d) => d.name);
818
- const values = sorted.map((d) => d.value);
819
- const chartColors = sorted.map((_, i) => CHART_COLORS[i % CHART_COLORS.length]);
820
- slide.addChart('bar', [{ name: props.title || '词频', labels, values }], {
821
- x: 0.8, y: 2.3, w: 8.4, h: 3.2,
822
- chartColors,
823
- showValue: true,
824
- dataLabelColor: COLORS.primary,
825
- dataLabelFontSize: 10,
826
- });
827
- }
828
- function renderComparisonV3(slide, props) {
829
- addKicker(slide, props.kicker);
830
- addTitle(slide, props.title ?? 'ComparisonV3');
831
- const rows = (props.rows ?? []).slice(0, 3);
832
- const startY = 2.5;
833
- const rowH = 0.75;
834
- const col1W = 3.2;
835
- const col2W = 2.8;
836
- const col3W = 2.8;
837
- const gap = 0.1;
838
- const startX = 0.8;
839
- slide.addShape('rect', {
840
- x: startX, y: startY, w: col1W, h: 0.5,
841
- fill: { color: COLORS.surfaceElevated },
842
- line: { color: COLORS.border, width: 1 },
843
- });
844
- slide.addShape('rect', {
845
- x: startX + col1W + gap, y: startY, w: col2W, h: 0.5,
846
- fill: { color: COLORS.accent },
847
- line: { color: COLORS.accent, width: 1 },
848
- });
849
- slide.addShape('rect', {
850
- x: startX + col1W + gap + col2W + gap, y: startY, w: col3W, h: 0.5,
851
- fill: { color: COLORS.surfaceElevated },
852
- line: { color: COLORS.border, width: 1 },
853
- });
854
- slide.addText('维度', {
855
- x: startX, y: startY, w: col1W, h: 0.5,
856
- fontSize: 12, color: COLORS.secondary, bold: true, align: 'center', valign: 'middle',
857
- fontFace: FONTS.body,
858
- });
859
- slide.addText(props.leftTitle ?? '方案 A', {
860
- x: startX + col1W + gap, y: startY, w: col2W, h: 0.5,
861
- fontSize: 12, color: COLORS.white, bold: true, align: 'center', valign: 'middle',
862
- fontFace: FONTS.heading,
863
- });
864
- slide.addText(props.rightTitle ?? '方案 B', {
865
- x: startX + col1W + gap + col2W + gap, y: startY, w: col3W, h: 0.5,
866
- fontSize: 12, color: COLORS.primary, bold: true, align: 'center', valign: 'middle',
867
- fontFace: FONTS.heading,
868
- });
869
- rows.forEach((row, index) => {
870
- const y = startY + 0.6 + index * rowH;
871
- const bg = index % 2 === 0 ? COLORS.surface : COLORS.surfaceElevated;
872
- slide.addShape('rect', {
873
- x: startX, y, w: col1W, h: rowH,
874
- fill: { color: bg },
875
- line: { color: COLORS.border, width: 1 },
876
- });
877
- slide.addShape('rect', {
878
- x: startX + col1W + gap, y, w: col2W, h: rowH,
879
- fill: { color: bg },
880
- line: { color: COLORS.border, width: 1 },
881
- });
882
- slide.addShape('rect', {
883
- x: startX + col1W + gap + col2W + gap, y, w: col3W, h: rowH,
884
- fill: { color: bg },
885
- line: { color: COLORS.border, width: 1 },
886
- });
887
- slide.addText(row.feature ?? '', {
888
- x: startX + 0.15, y, w: col1W - 0.3, h: rowH,
889
- fontSize: 13, color: COLORS.primary, align: 'left', valign: 'middle',
890
- fontFace: FONTS.body,
891
- });
892
- slide.addText(row.left ?? '', {
893
- x: startX + col1W + gap + 0.1, y, w: col2W - 0.2, h: rowH,
894
- fontSize: 13, color: COLORS.primary, align: 'center', valign: 'middle',
895
- fontFace: FONTS.body,
896
- });
897
- slide.addText(row.right ?? '', {
898
- x: startX + col1W + gap + col2W + gap + 0.1, y, w: col3W - 0.2, h: rowH,
899
- fontSize: 13, color: COLORS.primary, align: 'center', valign: 'middle',
900
- fontFace: FONTS.body,
901
- });
902
- });
903
- }
904
- function renderContentV2(slide, props) {
905
- addKicker(slide, props.kicker);
906
- addTitle(slide, props.title);
907
- slide.addText('左栏', {
908
- x: 0.8, y: 2.4, w: 4.0, h: 0.4,
909
- fontSize: 18, color: COLORS.accent, bold: true, align: 'left',
910
- fontFace: FONTS.heading,
911
- });
912
- slide.addText('右栏', {
913
- x: 5.2, y: 2.4, w: 4.0, h: 0.4,
914
- fontSize: 18, color: COLORS.accent, bold: true, align: 'left',
915
- fontFace: FONTS.heading,
916
- });
917
- addBulletList(slide, props.leftPoints || [], 0.8, 2.9, 4.0);
918
- addBulletList(slide, props.rightPoints || [], 5.2, 2.9, 4.0);
919
- }
920
- function renderFaqV1(slide, props) {
921
- addKicker(slide, props.kicker);
922
- addTitle(slide, props.title);
923
- const items = (props.items || []).slice(0, 4);
924
- let y = 2.0;
925
- items.forEach((item) => {
926
- slide.addText(`Q: ${item.q || ''}`, {
927
- x: 0.8, y, w: 8.4, h: 0.32,
928
- fontSize: 18, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
929
- fontFace: FONTS.heading,
930
- });
931
- slide.addText(`A: ${item.a || ''}`, {
932
- x: 0.8, y: y + 0.37, w: 8.4, h: 0.43,
933
- fontSize: 16, color: COLORS.secondary, align: 'left', valign: 'top',
934
- fontFace: FONTS.body,
935
- });
936
- y += 0.85;
937
- });
938
- }
939
- function renderFeatureV1(slide, props) {
940
- addKicker(slide, props.kicker);
941
- addTitle(slide, props.title);
942
- const features = (props.features || []).slice(0, 3);
943
- const cardWidth = 2.6;
944
- const gap = 0.3;
945
- const startX = (10 - (features.length * cardWidth + (features.length - 1) * gap)) / 2;
946
- features.forEach((feature, index) => {
947
- const x = startX + index * (cardWidth + gap);
948
- slide.addShape('rect', {
949
- x, y: 2.8, w: cardWidth, h: 2.2,
950
- fill: { color: COLORS.light },
951
- line: { color: COLORS.border, width: 1 },
952
- });
953
- slide.addText(feature.title || '', {
954
- x: x + 0.15, y: 3.0, w: cardWidth - 0.3, h: 0.5,
955
- fontSize: 20, color: COLORS.primary, bold: true, align: 'center',
956
- fontFace: FONTS.heading,
957
- });
958
- slide.addText(feature.description || '', {
959
- x: x + 0.15, y: 3.6, w: cardWidth - 0.3, h: 1.2,
960
- fontSize: 14, color: COLORS.secondary, align: 'center', valign: 'top',
961
- fontFace: FONTS.body,
962
- });
963
- });
964
- }
965
- function renderFeatureV2(slide, props) {
966
- addKicker(slide, props.kicker);
967
- addTitle(slide, props.title);
968
- if (props.subtitle) {
969
- slide.addText(props.subtitle, {
970
- x: 0.6, y: 1.5, w: 8.8, h: 0.4,
971
- fontSize: 16, color: COLORS.secondary, valign: 'top',
972
- fontFace: FONTS.body,
973
- });
974
- }
975
- if (props.imageUrl)
976
- addImageMaybe(slide, props.imageUrl, 0.6, 2.2, 4.2, 3.0);
977
- const items = (props.items || []).slice(0, 5);
978
- items.forEach((item, index) => {
979
- const y = 2.2 + index * 0.95;
980
- slide.addShape('rect', {
981
- x: 5.2, y, w: 4.2, h: 0.85,
982
- fill: { color: COLORS.light },
983
- line: { color: COLORS.border, width: 1 },
984
- });
985
- slide.addText(item.title || '', {
986
- x: 5.35, y: y + 0.08, w: 3.9, h: 0.3,
987
- fontSize: 15, color: COLORS.primary, bold: true,
988
- fontFace: FONTS.heading,
989
- });
990
- slide.addText(item.description || '', {
991
- x: 5.35, y: y + 0.36, w: 3.9, h: 0.4,
992
- fontSize: 11, color: COLORS.secondary, valign: 'top',
993
- fontFace: FONTS.body,
994
- });
995
- });
996
- if (props.footer) {
997
- slide.addText(props.footer, {
998
- x: 0.6, y: 5.4, w: 8.8, h: 0.3,
999
- fontSize: 11, color: COLORS.secondary,
1000
- fontFace: FONTS.mono,
1001
- });
1002
- }
1003
- }
1004
- function renderImageV1(slide, props) {
1005
- if (props.imageUrl) {
1006
- addImageMaybe(slide, props.imageUrl, 0, 0, 10, 5.625);
1007
- slide.addShape('rect', {
1008
- x: 0, y: 0, w: 10, h: 5.625,
1009
- fill: { color: '000000', transparency: 40 },
1010
- });
1011
- }
1012
- slide.addText(props.title, {
1013
- x: 1, y: 2.4, w: 8, h: 1.0,
1014
- fontSize: 48, color: COLORS.white, bold: true, align: 'center',
1015
- fontFace: FONTS.heading,
1016
- });
1017
- if (props.subtitle) {
1018
- slide.addText(props.subtitle, {
1019
- x: 1.5, y: 3.5, w: 7, h: 0.7,
1020
- fontSize: 22, color: COLORS.white, align: 'center',
1021
- fontFace: FONTS.body,
1022
- });
1023
- }
1024
- }
1025
- function renderMetricV1(slide, props) {
1026
- if (props.label) {
1027
- slide.addText(props.label, {
1028
- x: 1, y: 1.8, w: 8, h: 0.4,
1029
- fontSize: 16, color: COLORS.secondary, align: 'center',
1030
- fontFace: FONTS.body,
1031
- });
1032
- }
1033
- const metricText = props.unit ? `${props.value} ${props.unit}` : props.value;
1034
- slide.addText(metricText, {
1035
- x: 1, y: 2.4, w: 8, h: 1.4,
1036
- fontSize: 90, color: COLORS.primary, bold: true, align: 'center',
1037
- fontFace: FONTS.heading,
1038
- });
1039
- if (props.description) {
1040
- slide.addText(props.description, {
1041
- x: 1.5, y: 4.1, w: 7, h: 0.8,
1042
- fontSize: 20, color: COLORS.secondary, align: 'center',
1043
- fontFace: FONTS.body,
1044
- });
1045
- }
1046
- }
1047
- function renderPartnersV1(slide, props) {
1048
- addKicker(slide, props.kicker);
1049
- addTitle(slide, props.title);
1050
- const partners = (props.partners || []).slice(0, 8);
1051
- const cols = 4;
1052
- const cellW = 2.0;
1053
- const cellH = 1.1;
1054
- const gapX = 0.4;
1055
- const gapY = 0.3;
1056
- const startX = (10 - (cols * cellW + (cols - 1) * gapX)) / 2;
1057
- partners.forEach((partner, index) => {
1058
- const row = Math.floor(index / cols);
1059
- const col = index % cols;
1060
- const x = startX + col * (cellW + gapX);
1061
- const y = 2.5 + row * (cellH + gapY);
1062
- slide.addShape('rect', {
1063
- x, y, w: cellW, h: cellH,
1064
- fill: { color: COLORS.light },
1065
- line: { color: COLORS.border, width: 1 },
1066
- });
1067
- if (partner.logoUrl)
1068
- addImageMaybe(slide, partner.logoUrl, x + 0.25, y + 0.15, cellW - 0.5, cellH - 0.5);
1069
- slide.addText(partner.name || '', {
1070
- x, y: y + cellH - 0.25, w: cellW, h: 0.25,
1071
- fontSize: 12, color: COLORS.secondary, align: 'center',
1072
- fontFace: FONTS.body,
1073
- });
1074
- });
1075
- }
1076
- function renderPestV1(slide, props) {
1077
- addKicker(slide, props.kicker);
1078
- addTitle(slide, props.title);
1079
- render2x2Grid(slide, [
1080
- { title: '政治 Political', text: props.political || '' },
1081
- { title: '经济 Economic', text: props.economic || '' },
1082
- { title: '社会 Social', text: props.social || '' },
1083
- { title: '技术 Technological', text: props.technological || '' },
1084
- ]);
1085
- }
1086
- function renderPricingV1(slide, props) {
1087
- addKicker(slide, props.kicker);
1088
- addTitle(slide, props.title);
1089
- const tiers = (props.tiers || []).slice(0, 3);
1090
- const cardWidth = 2.5;
1091
- const gap = 0.35;
1092
- const startX = (10 - (tiers.length * cardWidth + (tiers.length - 1) * gap)) / 2;
1093
- tiers.forEach((tier, index) => {
1094
- const x = startX + index * (cardWidth + gap);
1095
- const isHighlight = index === 1;
1096
- slide.addShape('rect', {
1097
- x, y: 2.6, w: cardWidth, h: 2.6,
1098
- fill: { color: isHighlight ? COLORS.accent : COLORS.light },
1099
- line: { color: COLORS.border, width: 1 },
1100
- });
1101
- slide.addText(tier.name || '', {
1102
- x, y: 2.8, w: cardWidth, h: 0.4,
1103
- fontSize: 18, color: isHighlight ? COLORS.white : COLORS.primary, bold: true, align: 'center',
1104
- fontFace: FONTS.heading,
1105
- });
1106
- slide.addText([tier.price, tier.period].filter(Boolean).join(' '), {
1107
- x, y: 3.25, w: cardWidth, h: 0.5,
1108
- fontSize: 24, color: isHighlight ? COLORS.white : COLORS.primary, bold: true, align: 'center',
1109
- fontFace: FONTS.heading,
1110
- });
1111
- const features = (tier.features || []).slice(0, 3);
1112
- let fy = 3.85;
1113
- features.forEach((feature) => {
1114
- slide.addText(feature, {
1115
- x: x + 0.15, y: fy, w: cardWidth - 0.3, h: 0.3,
1116
- fontSize: 12, color: isHighlight ? COLORS.white : COLORS.secondary, align: 'center',
1117
- fontFace: FONTS.body,
1118
- });
1119
- fy += 0.35;
1120
- });
1121
- if (tier.cta) {
1122
- slide.addText(tier.cta, {
1123
- x: x + 0.3, y: 4.8, w: cardWidth - 0.6, h: 0.3,
1124
- fontSize: 12, color: isHighlight ? COLORS.accent : COLORS.white,
1125
- fill: isHighlight ? { color: COLORS.white } : { color: COLORS.accent },
1126
- align: 'center', valign: 'middle',
1127
- fontFace: FONTS.body,
1128
- });
1129
- }
1130
- });
1131
- }
1132
- function renderProcessV1(slide, props) {
1133
- addKicker(slide, props.kicker);
1134
- addTitle(slide, props.title);
1135
- const steps = props.steps || [];
1136
- const count = steps.length;
1137
- if (count === 0) {
1138
- slide.addText('(暂无流程步骤)', {
1139
- x: 2, y: 3.2, w: 6, h: 0.8,
1140
- fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
1141
- fontFace: FONTS.body,
1142
- });
1143
- return;
1144
- }
1145
- const stepWidth = 8.4 / count;
1146
- steps.forEach((step, index) => {
1147
- const x = 0.8 + index * stepWidth;
1148
- slide.addShape('ellipse', {
1149
- x: x + stepWidth / 2 - 0.3, y: 2.6, w: 0.6, h: 0.6,
1150
- fill: { color: COLORS.accent },
1151
- });
1152
- slide.addText(String(index + 1), {
1153
- x: x + stepWidth / 2 - 0.3, y: 2.6, w: 0.6, h: 0.6,
1154
- fontSize: 22, color: COLORS.white, bold: true, align: 'center', valign: 'middle',
1155
- fontFace: FONTS.heading,
1156
- });
1157
- slide.addText(step, {
1158
- x: x + 0.1, y: 3.5, w: stepWidth - 0.2, h: 1.2,
1159
- fontSize: 18, color: COLORS.primary, align: 'center', valign: 'top',
1160
- fontFace: FONTS.body,
1161
- });
1162
- });
1163
- }
1164
- function renderQuoteV1(slide, props) {
1165
- slide.addText(`“${props.quote}”`, {
1166
- x: 1, y: 2.0, w: 8, h: 2.2,
1167
- fontSize: 36, color: COLORS.primary, bold: true, align: 'center', valign: 'middle',
1168
- fontFace: FONTS.heading,
1169
- });
1170
- const attribution = [props.author, props.source].filter(Boolean).join(' · ');
1171
- if (attribution) {
1172
- slide.addText(attribution, {
1173
- x: 1, y: 4.4, w: 8, h: 0.4,
1174
- fontSize: 18, color: COLORS.secondary, align: 'center',
1175
- fontFace: FONTS.mono,
1176
- });
1177
- }
1178
- }
1179
- function renderQuoteV2(slide, props) {
1180
- slide.addShape('rect', {
1181
- x: 0.6, y: 1.5, w: 0.15, h: 2.8,
1182
- fill: { color: COLORS.accent },
1183
- });
1184
- slide.addText(`“${props.quote}”`, {
1185
- x: 1.0, y: 1.8, w: 8.0, h: 2.0,
1186
- fontSize: 34, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
1187
- fontFace: FONTS.heading,
1188
- });
1189
- const attribution = [props.author, props.role].filter(Boolean).join(' · ');
1190
- if (attribution) {
1191
- slide.addText(attribution, {
1192
- x: 1.0, y: 4.0, w: 8.0, h: 0.4,
1193
- fontSize: 18, color: COLORS.secondary, align: 'left',
1194
- fontFace: FONTS.mono,
1195
- });
1196
- }
1197
- }
1198
- function renderQuoteV3(slide, props) {
1199
- slide.addText('"', {
1200
- x: 0.8, y: 1.0, w: 1.5, h: 1.5,
1201
- fontSize: 120, color: COLORS.accent, align: 'left', valign: 'top',
1202
- fontFace: FONTS.heading,
1203
- });
1204
- slide.addText(props.quote, {
1205
- x: 2.0, y: 1.6, w: 7.2, h: 2.6,
1206
- fontSize: 28, color: COLORS.primary, align: 'left', valign: 'top',
1207
- fontFace: FONTS.heading,
1208
- });
1209
- const meta = [props.author, props.role].filter(Boolean).join(' · ');
1210
- if (meta) {
1211
- slide.addText(meta, {
1212
- x: 2.0, y: 4.4, w: 7.2, h: 0.4,
1213
- fontSize: 16, color: COLORS.primary, bold: true, align: 'left',
1214
- fontFace: FONTS.body,
1215
- });
1216
- }
1217
- if (props.source) {
1218
- slide.addText(props.source, {
1219
- x: 2.0, y: 4.85, w: 7.2, h: 0.3,
1220
- fontSize: 12, color: COLORS.secondary, align: 'left',
1221
- fontFace: FONTS.mono,
1222
- });
1223
- }
1224
- }
1225
- function renderRoadmapV1(slide, props) {
1226
- addKicker(slide, props.kicker);
1227
- addTitle(slide, props.title);
1228
- const phases = (props.phases || []).slice(0, 4);
1229
- let y = 2.1;
1230
- phases.forEach((phase, index) => {
1231
- const statusColor = phase.status === '已完成' ? COLORS.accent : COLORS.secondary;
1232
- slide.addShape('rect', {
1233
- x: 0.8, y, w: 0.2, h: 0.7,
1234
- fill: { color: statusColor },
1235
- });
1236
- slide.addText(`${index + 1}. ${phase.title || ''}`, {
1237
- x: 1.1, y, w: 7.8, h: 0.35,
1238
- fontSize: 18, color: COLORS.primary, bold: true, align: 'left',
1239
- fontFace: FONTS.heading,
1240
- });
1241
- slide.addText(phase.description || '', {
1242
- x: 1.1, y: y + 0.38, w: 7.8, h: 0.45,
1243
- fontSize: 14, color: COLORS.secondary, align: 'left', valign: 'top',
1244
- fontFace: FONTS.body,
1245
- });
1246
- y += 0.8;
1247
- });
1248
- }
1249
- function renderStatsV1(slide, props) {
1250
- const hasInsight = props.showInsight !== false && !!props.insight && (!!props.insight.value || !!props.insight.label || !!props.insight.description);
1251
- const mainW = hasInsight ? 6.8 : 8.4;
1252
- const cardW = (mainW - 0.4) / 2;
1253
- const startX = 0.8;
1254
- addKicker(slide, props.kicker);
1255
- addTitle(slide, props.title, startX, 1.3, mainW);
1256
- const stats = (props.stats || []).slice(0, 4);
1257
- const positions = [
1258
- { x: startX, y: 2.5 },
1259
- { x: startX + cardW + 0.2, y: 2.5 },
1260
- { x: startX, y: 4.0 },
1261
- { x: startX + cardW + 0.2, y: 4.0 },
1262
- ];
1263
- stats.forEach((stat, index) => {
1264
- const pos = positions[index];
1265
- if (!pos)
1266
- return;
1267
- slide.addShape('rect', {
1268
- x: pos.x, y: pos.y, w: cardW, h: 1.3,
1269
- fill: { color: COLORS.light },
1270
- line: { color: COLORS.border, width: 1 },
1271
- });
1272
- const valueText = [stat.value, stat.unit].filter(Boolean).join(' ');
1273
- slide.addText(valueText || '-', {
1274
- x: pos.x + 0.2, y: pos.y + 0.15, w: cardW - 0.4, h: 0.55,
1275
- fontSize: 30, color: COLORS.primary, bold: true, align: 'left',
1276
- fontFace: FONTS.heading,
1277
- });
1278
- slide.addText(stat.label || '', {
1279
- x: pos.x + 0.2, y: pos.y + 0.75, w: cardW - 1.0, h: 0.35,
1280
- fontSize: 14, color: COLORS.secondary, align: 'left',
1281
- fontFace: FONTS.body,
1282
- });
1283
- if (stat.change) {
1284
- slide.addText(stat.change, {
1285
- x: pos.x + cardW - 1.0, y: pos.y + 0.75, w: 0.8, h: 0.35,
1286
- fontSize: 14, color: COLORS.accent, align: 'right',
1287
- fontFace: FONTS.mono,
1288
- });
1289
- }
1290
- });
1291
- if (hasInsight) {
1292
- renderInsightPanel(slide, props.insight, 8.0, 2.3, 2.4, 3.2);
1293
- }
1294
- }
1295
- function renderSwotV1(slide, props) {
1296
- addKicker(slide, props.kicker);
1297
- addTitle(slide, props.title);
1298
- render2x2Grid(slide, [
1299
- { title: '优势 Strengths', text: props.strength || '' },
1300
- { title: '劣势 Weaknesses', text: props.weakness || '' },
1301
- { title: '机会 Opportunities', text: props.opportunity || '' },
1302
- { title: '威胁 Threats', text: props.threat || '' },
1303
- ]);
1304
- }
1305
- function renderTableOfContentsV1(slide, props) {
1306
- addKicker(slide, props.kicker);
1307
- addTitle(slide, props.title);
1308
- const items = (props.items || []).slice(0, 4);
1309
- let y = 2.4;
1310
- items.forEach((item, index) => {
1311
- const number = String(index + 1).padStart(2, '0');
1312
- slide.addText(number, {
1313
- x: 0.8, y, w: 0.8, h: 0.45,
1314
- fontSize: 18, color: COLORS.accent, align: 'left', valign: 'top',
1315
- fontFace: FONTS.mono,
1316
- });
1317
- slide.addText(item, {
1318
- x: 1.7, y, w: 7.8, h: 0.45,
1319
- fontSize: 26, color: COLORS.primary, align: 'left', valign: 'top',
1320
- fontFace: FONTS.body,
1321
- });
1322
- y += 0.75;
1323
- });
1324
- }
1325
- function renderTeamV1(slide, props) {
1326
- addKicker(slide, props.kicker);
1327
- addTitle(slide, props.title);
1328
- const members = (props.members || []).slice(0, 4);
1329
- const cardWidth = 2.0;
1330
- const gap = 0.4;
1331
- const startX = (10 - (members.length * cardWidth + (members.length - 1) * gap)) / 2;
1332
- members.forEach((member, index) => {
1333
- const x = startX + index * (cardWidth + gap);
1334
- if (member.imageUrl)
1335
- addImageMaybe(slide, member.imageUrl, x + 0.5, 2.7, 1, 1);
1336
- slide.addText(member.name || '', {
1337
- x, y: 3.85, w: cardWidth, h: 0.35,
1338
- fontSize: 16, color: COLORS.primary, bold: true, align: 'center',
1339
- fontFace: FONTS.heading,
1340
- });
1341
- slide.addText(member.role || '', {
1342
- x, y: 4.2, w: cardWidth, h: 0.3,
1343
- fontSize: 12, color: COLORS.accent, align: 'center',
1344
- fontFace: FONTS.mono,
1345
- });
1346
- slide.addText(member.bio || '', {
1347
- x, y: 4.55, w: cardWidth, h: 0.55,
1348
- fontSize: 11, color: COLORS.secondary, align: 'center', valign: 'top',
1349
- fontFace: FONTS.body,
1350
- });
1351
- });
1352
- }
1353
- function renderTeamV2(slide, props) {
1354
- addKicker(slide, props.kicker);
1355
- addTitle(slide, props.title ?? 'Team');
1356
- const members = props.members ?? [];
1357
- const maxMembers = 4;
1358
- const cardW = 2.05;
1359
- const gap = 0.3;
1360
- const startX = 0.6;
1361
- const y = 2.4;
1362
- const h = 3.2;
1363
- members.slice(0, maxMembers).forEach((member, index) => {
1364
- const x = startX + index * (cardW + gap);
1365
- slide.addShape('rect', {
1366
- x, y, w: cardW, h,
1367
- fill: { color: COLORS.surfaceElevated },
1368
- line: { color: COLORS.border, width: 1 },
1369
- rectRadius: 0.08,
1370
- });
1371
- slide.addShape('ellipse', {
1372
- x: x + cardW / 2 - 0.35, y: y + 0.25, w: 0.7, h: 0.7,
1373
- fill: { color: COLORS.border },
1374
- });
1375
- slide.addText(member.name ?? `Member ${index + 1}`, {
1376
- x: x + 0.15, y: y + 1.05, w: cardW - 0.3, h: 0.35,
1377
- fontSize: 17, color: COLORS.primary, bold: true, align: 'center', valign: 'top',
1378
- fontFace: FONTS.heading,
1379
- });
1380
- slide.addText(member.role ?? '', {
1381
- x: x + 0.15, y: y + 1.45, w: cardW - 0.3, h: 0.3,
1382
- fontSize: 13, color: COLORS.accent, align: 'center', valign: 'top',
1383
- fontFace: FONTS.body,
1384
- });
1385
- slide.addText(member.bio ?? '', {
1386
- x: x + 0.15, y: y + 1.85, w: cardW - 0.3, h: 1.25,
1387
- fontSize: 12, color: COLORS.secondary, align: 'center', valign: 'top',
1388
- fontFace: FONTS.body,
1389
- });
1390
- });
1391
- }
1392
- function renderTestimonialV1(slide, props) {
1393
- slide.addText(`“${props.quote}”`, {
1394
- x: 1, y: 1.8, w: 8, h: 2.2,
1395
- fontSize: 34, color: COLORS.primary, bold: true, align: 'center', valign: 'middle',
1396
- fontFace: FONTS.heading,
1397
- });
1398
- if (props.avatarUrl)
1399
- addImageMaybe(slide, props.avatarUrl, 4.5, 4.1, 1, 1);
1400
- const attribution = [props.author, props.role, props.company].filter(Boolean).join(' · ');
1401
- if (attribution) {
1402
- slide.addText(attribution, {
1403
- x: 1, y: 5.0, w: 8, h: 0.4,
1404
- fontSize: 16, color: COLORS.secondary, align: 'center',
1405
- fontFace: FONTS.mono,
1406
- });
1407
- }
1408
- }
1409
- function renderTheme01AppendixV1(slide, props) {
1410
- if (props.subtitle) {
1411
- slide.addText(props.subtitle, {
1412
- x: 0.8, y: 0.8, w: 8.4, h: 0.4,
1413
- fontSize: 14, color: COLORS.accent, align: 'left', fontFace: FONTS.mono,
1414
- });
1415
- }
1416
- addTitle(slide, props.title ?? '附录 / 数据来源', 0.8, 1.25);
1417
- const sources = (props.sources || []).slice(0, 8);
1418
- if (sources.length === 0)
1419
- return;
1420
- let cy = 2.4;
1421
- for (const source of sources) {
1422
- const label = source.label ? `[${source.label}] ` : '';
1423
- slide.addText(`${label}${source.value || ''}`, {
1424
- x: 0.8, y: cy, w: 8.4, h: 0.55,
1425
- fontSize: 15, color: COLORS.primary, align: 'left', valign: 'top',
1426
- fontFace: FONTS.body,
1427
- });
1428
- cy += 0.65;
1429
- }
1430
- }
1431
- function renderTheme01BentoV1(slide, props) {
1432
- addKicker(slide, props.kicker);
1433
- addTitle(slide, props.title ?? 'Bento');
1434
- const items = (props.items || []).slice(0, 4);
1435
- if (items.length === 0) {
1436
- slide.addText('(暂无 Bento 内容)', {
1437
- x: 2, y: 3.2, w: 6, h: 0.8,
1438
- fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
1439
- fontFace: FONTS.body,
1440
- });
1441
- return;
1442
- }
1443
- const positions = [
1444
- { x: 0.8, y: 2.4, w: 4.0, h: 2.8 },
1445
- { x: 5.2, y: 2.4, w: 4.0, h: 1.3 },
1446
- { x: 5.2, y: 3.9, w: 1.9, h: 1.3 },
1447
- { x: 7.3, y: 3.9, w: 1.9, h: 1.3 },
1448
- ];
1449
- items.forEach((item, index) => {
1450
- const pos = positions[index];
1451
- if (!pos)
1452
- return;
1453
- slide.addShape('rect', {
1454
- x: pos.x, y: pos.y, w: pos.w, h: pos.h,
1455
- fill: { color: COLORS.surfaceElevated },
1456
- line: { color: COLORS.border, width: 1 },
1457
- rectRadius: 0.12,
1458
- });
1459
- if (item.imageUrl)
1460
- addImageMaybe(slide, item.imageUrl, pos.x + 0.08, pos.y + 0.08, pos.w - 0.16, pos.h * 0.45);
1461
- slide.addText(item.title || '', {
1462
- x: pos.x + 0.15, y: pos.y + pos.h * 0.55, w: pos.w - 0.3, h: 0.45,
1463
- fontSize: 16, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
1464
- fontFace: FONTS.heading,
1465
- });
1466
- slide.addText(item.description || '', {
1467
- x: pos.x + 0.15, y: pos.y + pos.h * 0.55 + 0.45, w: pos.w - 0.3, h: 0.6,
1468
- fontSize: 12, color: COLORS.secondary, align: 'left', valign: 'top',
1469
- fontFace: FONTS.body,
1470
- });
1471
- });
1472
- }
1473
- function renderTheme01CaseStudy(slide, props) {
1474
- addKicker(slide, props.kicker);
1475
- addTitle(slide, props.title ?? '案例');
1476
- if (props.subtitle) {
1477
- slide.addText(props.subtitle, {
1478
- x: 0.8, y: 2.05, w: 8.4, h: 0.35,
1479
- fontSize: 16, color: COLORS.secondary, align: 'left', valign: 'top',
1480
- fontFace: FONTS.body,
1481
- });
1482
- }
1483
- if (props.intro) {
1484
- slide.addText(props.intro, {
1485
- x: 0.8, y: 2.45, w: 8.4, h: 0.6,
1486
- fontSize: 14, color: COLORS.primary, align: 'left', valign: 'top',
1487
- fontFace: FONTS.body,
1488
- });
1489
- }
1490
- const rounds = (props.rounds || [])
1491
- .filter((r) => r != null && (r.date || r.round || r.amount))
1492
- .slice(0, 6);
1493
- if (rounds.length > 0) {
1494
- const startY = props.intro ? 3.2 : 2.55;
1495
- const rowH = 0.55;
1496
- const colW = 8.4 / 4;
1497
- const startX = 0.8;
1498
- const headers = ['日期', '轮次', '估值', '金额'];
1499
- headers.forEach((header, index) => {
1500
- const x = startX + index * colW;
1501
- slide.addShape('rect', {
1502
- x, y: startY, w: colW, h: 0.35,
1503
- fill: { color: COLORS.surfaceElevated },
1504
- line: { color: COLORS.border, width: 1 },
1505
- });
1506
- slide.addText(header, {
1507
- x, y: startY, w: colW, h: 0.35,
1508
- fontSize: 11, color: COLORS.secondary, bold: true, align: 'center', valign: 'middle',
1509
- fontFace: FONTS.heading,
1510
- });
1511
- });
1512
- rounds.forEach((round, rowIndex) => {
1513
- const y = startY + 0.35 + rowIndex * rowH;
1514
- const cells = [round.date || '', round.round || '', round.valuation || '', round.amount || ''];
1515
- cells.forEach((cell, colIndex) => {
1516
- const x = startX + colIndex * colW;
1517
- const bg = rowIndex % 2 === 0 ? COLORS.surface : COLORS.surfaceElevated;
1518
- slide.addShape('rect', {
1519
- x, y, w: colW, h: rowH,
1520
- fill: { color: bg },
1521
- line: { color: COLORS.border, width: 1 },
1522
- });
1523
- slide.addText(cell, {
1524
- x: x + 0.06, y, w: colW - 0.12, h: rowH,
1525
- fontSize: 11, color: COLORS.primary, align: 'center', valign: 'middle',
1526
- fontFace: FONTS.body,
1527
- });
1528
- });
1529
- });
1530
- }
1531
- if (props.quote) {
1532
- slide.addText(`“${props.quote}”`, {
1533
- x: 0.8, y: 4.85, w: 8.4, h: 0.55,
1534
- fontSize: 16, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
1535
- fontFace: FONTS.heading,
1536
- });
1537
- if (props.quoteAuthor) {
1538
- slide.addText(`—— ${props.quoteAuthor}`, {
1539
- x: 0.8, y: 5.35, w: 8.4, h: 0.25,
1540
- fontSize: 12, color: COLORS.secondary, align: 'left', valign: 'top',
1541
- fontFace: FONTS.mono,
1542
- });
1543
- }
1544
- }
1545
- if (props.footnote) {
1546
- slide.addText(props.footnote, {
1547
- x: 0.8, y: 5.7, w: 8.4, h: 0.25,
1548
- fontSize: 11, color: COLORS.secondary, align: 'left',
1549
- fontFace: FONTS.mono,
1550
- });
1551
- }
1552
- }
1553
- function renderTheme01ChapterV3(slide, props) {
1554
- addImageMaybe(slide, props.imageUrl, 0, 0, 10, 5.625);
1555
- slide.addShape('rect', {
1556
- x: 0, y: 0, w: 10, h: 5.625,
1557
- fill: { color: '000000', transparency: 40 },
1558
- });
1559
- const titleH = 0.9;
1560
- const subtitleH = props.subtitle ? 0.45 : 0;
1561
- const cardH = 0.9 + titleH + subtitleH + 0.7;
1562
- const cardY = (5.625 - cardH) / 2;
1563
- slide.addShape('rect', {
1564
- x: 1.5, y: cardY, w: 7, h: cardH,
1565
- fill: { color: 'F2E9DC', transparency: 92 },
1566
- line: { color: '3A342A', width: 1 },
1567
- rectRadius: 0.15,
1568
- });
1569
- let cy = cardY + 0.35;
1570
- if (props.kicker) {
1571
- slide.addText(props.kicker, {
1572
- x: 1.75, y: cy, w: 6.5, h: 0.35,
1573
- fontSize: 13, color: COLORS.accent, align: 'center', valign: 'top',
1574
- fontFace: FONTS.mono,
1575
- });
1576
- cy += 0.45;
1577
- }
1578
- slide.addText(props.title, {
1579
- x: 1.75, y: cy, w: 6.5, h: titleH,
1580
- fontSize: 40, color: COLORS.white, bold: true, align: 'center', valign: 'top',
1581
- fontFace: FONTS.heading,
1582
- });
1583
- cy += titleH + 0.1;
1584
- if (props.subtitle) {
1585
- slide.addText(props.subtitle, {
1586
- x: 1.75, y: cy, w: 6.5, h: subtitleH,
1587
- fontSize: 18, color: COLORS.white, align: 'center', valign: 'top',
1588
- fontFace: FONTS.body,
1589
- });
1590
- }
1591
- }
1592
- function renderTheme01ChartDonut(slide, props) {
1593
- addKicker(slide, props.kicker);
1594
- addTitle(slide, props.title ?? '环形图');
1595
- if (props.subtitle) {
1596
- slide.addText(props.subtitle, {
1597
- x: 0.8, y: 2.05, w: 8.4, h: 0.35,
1598
- fontSize: 16, color: COLORS.secondary, align: 'left', valign: 'top',
1599
- fontFace: FONTS.body,
1600
- });
1601
- }
1602
- const rawSegments = (props.segments || [])
1603
- .filter((s) => s != null && !!(s.label || s.value))
1604
- .map((s) => ({ ...s }))
1605
- .sort((a, b) => {
1606
- const na = parseFloat(String(a.value || '0').replace(/,/g, ''));
1607
- const nb = parseFloat(String(b.value || '0').replace(/,/g, ''));
1608
- return nb - na;
1609
- });
1610
- if (rawSegments.length === 0) {
1611
- slide.addText('(暂无环形图数据)', {
1612
- x: 2, y: 3.2, w: 6, h: 0.8,
1613
- fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
1614
- fontFace: FONTS.body,
1615
- });
1616
- return;
1617
- }
1618
- const totalValue = rawSegments.reduce((sum, s) => {
1619
- return sum + parseFloat(String(s.value || '0').replace(/,/g, ''));
1620
- }, 0);
1621
- const segments = rawSegments.map((s, i) => {
1622
- const n = parseFloat(String(s.value || '0').replace(/,/g, ''));
1623
- const pct = totalValue > 0 ? Math.round((n / totalValue) * 1000) / 10 : 0;
1624
- return {
1625
- ...s,
1626
- percent: s.percent || `${pct}%`,
1627
- numericValue: n,
1628
- color: s.color || `var(--lp-${['blue', 'green', 'amber', 'red', 'violet'][i % 5]})`,
1629
- };
1630
- });
1631
- const labels = segments.map((s) => s.label || '');
1632
- const values = segments.map((s) => s.numericValue);
1633
- const chartColors = segments.map((_, i) => CHART_COLORS[i % CHART_COLORS.length]);
1634
- const insight = props.insight;
1635
- const hasInsight = !!insight && (!!insight.value || !!insight.label || !!insight.description);
1636
- const chartW = hasInsight ? 4.2 : 4.8;
1637
- slide.addChart('doughnut', [
1638
- { name: props.title || '', labels, values },
1639
- ], {
1640
- x: 0.6, y: 2.4, w: chartW, h: 3.0,
1641
- chartColors,
1642
- showValue: false,
1643
- holeSize: 60,
1644
- });
1645
- const total = props.total || { value: String(totalValue), label: '合计' };
1646
- const centerX = 0.6 + chartW / 2;
1647
- slide.addText(total.value || String(totalValue), {
1648
- x: centerX - 1.8, y: 3.4, w: 3.6, h: 0.6,
1649
- fontSize: 28, color: COLORS.primary, bold: true, align: 'center', valign: 'middle',
1650
- fontFace: FONTS.heading,
1651
- });
1652
- slide.addText(total.label || '合计', {
1653
- x: centerX - 1.8, y: 4.0, w: 3.6, h: 0.3,
1654
- fontSize: 12, color: COLORS.secondary, align: 'center', valign: 'top',
1655
- fontFace: FONTS.body,
1656
- });
1657
- // 右侧图例列表
1658
- const legendX = hasInsight ? 5.0 : 5.7;
1659
- const legendW = hasInsight ? 2.6 : 3.3;
1660
- let legendY = 2.5;
1661
- segments.forEach((segment, index) => {
1662
- const color = chartColors[index % chartColors.length];
1663
- slide.addShape('ellipse', {
1664
- x: legendX, y: legendY + 0.08, w: 0.18, h: 0.18,
1665
- fill: { color },
1666
- });
1667
- slide.addText(segment.label || '', {
1668
- x: legendX + 0.3, y: legendY, w: legendW - 1.4, h: 0.28,
1669
- fontSize: 13, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
1670
- fontFace: FONTS.heading,
1671
- });
1672
- if (segment.labelEn) {
1673
- slide.addText(segment.labelEn, {
1674
- x: legendX + 0.3, y: legendY + 0.26, w: legendW - 1.4, h: 0.22,
1675
- fontSize: 10, color: COLORS.secondary, align: 'left', valign: 'top',
1676
- fontFace: FONTS.body,
1677
- });
1678
- }
1679
- slide.addText(segment.percent || '', {
1680
- x: legendX + legendW - 1.0, y: legendY, w: 0.9, h: 0.28,
1681
- fontSize: 13, color: COLORS.accent, bold: true, align: 'right', valign: 'top',
1682
- fontFace: FONTS.heading,
1683
- });
1684
- slide.addText(segment.value || '', {
1685
- x: legendX + legendW - 1.0, y: legendY + 0.26, w: 0.9, h: 0.22,
1686
- fontSize: 10, color: COLORS.secondary, align: 'right', valign: 'top',
1687
- fontFace: FONTS.mono,
1688
- });
1689
- legendY += segment.labelEn ? 0.65 : 0.5;
1690
- });
1691
- if (hasInsight) {
1692
- slide.addShape('roundRect', {
1693
- x: 8.0, y: 2.3, w: 2.4, h: 3.2,
1694
- fill: { color: COLORS.surfaceElevated },
1695
- line: { color: COLORS.border, width: 1 },
1696
- rectRadius: 0.15,
1697
- });
1698
- let cursorY = 2.6;
1699
- if (insight.value) {
1700
- slide.addText(insight.value, {
1701
- x: 8.2, y: cursorY, w: 2.0, h: 0.6,
1702
- fontSize: 34, fontFace: FONTS.heading, bold: true, color: COLORS.accent,
1703
- });
1704
- cursorY += 0.6;
1705
- }
1706
- if (insight.label) {
1707
- slide.addText(insight.label, {
1708
- x: 8.2, y: cursorY, w: 2.0, h: 0.3,
1709
- fontSize: 10, fontFace: FONTS.body, color: COLORS.secondary,
1710
- });
1711
- cursorY += 0.45;
1712
- }
1713
- if (insight.description) {
1714
- cursorY += 0.15;
1715
- slide.addText(insight.description, {
1716
- x: 8.2, y: cursorY, w: 2.0, h: 1.6,
1717
- fontSize: 11, fontFace: FONTS.body, color: COLORS.secondary,
1718
- valign: 'top',
1719
- });
1720
- }
1721
- }
1722
- if (props.footnote) {
1723
- slide.addText(props.footnote, {
1724
- x: 0.6, y: 5.7, w: 8.8, h: 0.25,
1725
- fontSize: 11, color: COLORS.secondary, align: 'left',
1726
- fontFace: FONTS.mono,
1727
- });
1728
- }
1729
- }
1730
- function renderTheme01ComparisonV2(slide, props) {
1731
- addKicker(slide, props.kicker);
1732
- addTitle(slide, props.title ?? '评分对比');
1733
- const cards = (props.cards || []).slice(0, 4);
1734
- const count = cards.length || 1;
1735
- const gap = 0.25;
1736
- const cardW = (8.4 - gap * (count - 1)) / count;
1737
- const startX = 0.8;
1738
- const y = 2.5;
1739
- const h = 3.0;
1740
- cards.forEach((card, index) => {
1741
- const score = typeof card.score === 'number' ? card.score : 0;
1742
- const max = typeof card.max === 'number' && card.max > 0 ? card.max : 100;
1743
- const pct = Math.min(100, Math.max(0, (score / max) * 100));
1744
- const x = startX + index * (cardW + gap);
1745
- slide.addShape('rect', {
1746
- x, y, w: cardW, h,
1747
- fill: { color: COLORS.surfaceElevated },
1748
- line: { color: COLORS.border, width: 1 },
1749
- rectRadius: 0.1,
1750
- });
1751
- slide.addText(card.label || '', {
1752
- x: x + 0.16, y: y + 0.2, w: cardW - 0.32, h: 0.45,
1753
- fontSize: 17, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
1754
- fontFace: FONTS.heading,
1755
- });
1756
- slide.addText(`${score} / ${max}`, {
1757
- x: x + 0.16, y: y + 0.75, w: cardW - 0.32, h: 0.4,
1758
- fontSize: 22, color: COLORS.accent, bold: true, align: 'left', valign: 'top',
1759
- fontFace: FONTS.heading,
1760
- });
1761
- slide.addShape('rect', {
1762
- x: x + 0.16, y: y + 1.25, w: cardW - 0.32, h: 0.12,
1763
- fill: { color: COLORS.light },
1764
- });
1765
- slide.addShape('rect', {
1766
- x: x + 0.16, y: y + 1.25, w: (cardW - 0.32) * (pct / 100), h: 0.12,
1767
- fill: { color: COLORS.accent },
1768
- });
1769
- if (card.note) {
1770
- slide.addText(card.note, {
1771
- x: x + 0.16, y: y + 1.55, w: cardW - 0.32, h: 1.15,
1772
- fontSize: 12, color: COLORS.secondary, align: 'left', valign: 'top',
1773
- fontFace: FONTS.body,
1774
- });
1775
- }
1776
- });
1777
- }
1778
- function renderTheme01ConclusionV1(slide, props) {
1779
- if (props.subtitle) {
1780
- slide.addText(props.subtitle, {
1781
- x: 0.8, y: 0.8, w: 8.4, h: 0.4,
1782
- fontSize: 14, color: COLORS.accent, align: 'left', fontFace: FONTS.mono,
1783
- });
1784
- }
1785
- addTitle(slide, props.title ?? '结论', 0.8, 1.25);
1786
- const points = (props.points || [])
1787
- .map((p) => (typeof p === 'string' ? p : String(p?.item ?? p ?? '')))
1788
- .filter((p) => p.length > 0)
1789
- .slice(0, 4);
1790
- if (points.length === 0)
1791
- return;
1792
- const count = points.length;
1793
- const gap = 0.25;
1794
- const cardW = (8.4 - gap * (count - 1)) / count;
1795
- const startX = 0.8;
1796
- const y = 2.6;
1797
- const h = 2.6;
1798
- points.forEach((point, index) => {
1799
- const x = startX + index * (cardW + gap);
1800
- slide.addShape('rect', {
1801
- x, y, w: cardW, h,
1802
- fill: { color: COLORS.surfaceElevated },
1803
- line: { color: COLORS.border, width: 1 },
1804
- rectRadius: 0.1,
1805
- });
1806
- slide.addText(String(index + 1), {
1807
- x: x + 0.16, y: y + 0.16, w: 0.6, h: 0.5,
1808
- fontSize: 28, color: COLORS.accent, bold: true, align: 'left', valign: 'top',
1809
- fontFace: FONTS.heading,
1810
- });
1811
- slide.addText(String(point), {
1812
- x: x + 0.16, y: y + 0.85, w: cardW - 0.32, h: 1.55,
1813
- fontSize: 16, color: COLORS.primary, align: 'left', valign: 'top',
1814
- fontFace: FONTS.body,
1815
- });
1816
- });
1817
- }
1818
- function renderTheme01ContentV3(slide, props) {
1819
- addKicker(slide, props.kicker);
1820
- addTitle(slide, props.title ?? '内容');
1821
- const columns = (props.columns || []).slice(0, 3);
1822
- const count = columns.length || 1;
1823
- const gap = 0.25;
1824
- const cardW = (8.4 - gap * (count - 1)) / count;
1825
- const startX = 0.8;
1826
- const y = 2.5;
1827
- const h = 3.0;
1828
- columns.forEach((column, index) => {
1829
- const x = startX + index * (cardW + gap);
1830
- slide.addShape('rect', {
1831
- x, y, w: cardW, h,
1832
- fill: { color: COLORS.surfaceElevated },
1833
- line: { color: COLORS.border, width: 1 },
1834
- rectRadius: 0.1,
1835
- });
1836
- slide.addText(column.title || '', {
1837
- x: x + 0.16, y: y + 0.2, w: cardW - 0.32, h: 0.45,
1838
- fontSize: 18, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
1839
- fontFace: FONTS.heading,
1840
- });
1841
- slide.addText(column.text || '', {
1842
- x: x + 0.16, y: y + 0.75, w: cardW - 0.32, h: 1.9,
1843
- fontSize: 14, color: COLORS.secondary, align: 'left', valign: 'top',
1844
- fontFace: FONTS.body,
1845
- });
1846
- });
1847
- }
1848
- function renderTheme01DiptychContrast(slide, props) {
1849
- addKicker(slide, props.kicker);
1850
- addTitle(slide, props.title ?? '双联对比');
1851
- const left = props.left || {};
1852
- const right = props.right || {};
1853
- const centerCard = props.centerCard || {};
1854
- const comparisons = (centerCard.comparisons || []).slice(0, 3);
1855
- // 左右背景区
1856
- slide.addShape('rect', {
1857
- x: 0.8, y: 2.3, w: 4.0, h: 3.6,
1858
- fill: { color: COLORS.surfaceElevated },
1859
- line: { color: COLORS.border, width: 1 },
1860
- rectRadius: 0.12,
1861
- });
1862
- slide.addShape('rect', {
1863
- x: 5.2, y: 2.3, w: 4.0, h: 3.6,
1864
- fill: { color: COLORS.surfaceElevated },
1865
- line: { color: COLORS.border, width: 1 },
1866
- rectRadius: 0.12,
1867
- });
1868
- if (left.imageUrl)
1869
- addImageMaybe(slide, left.imageUrl, 0.9, 2.4, 3.8, 2.6);
1870
- if (right.imageUrl)
1871
- addImageMaybe(slide, right.imageUrl, 5.3, 2.4, 3.8, 2.6);
1872
- slide.addText(left.labelEn || '', {
1873
- x: 0.8, y: 2.45, w: 4.0, h: 0.25,
1874
- fontSize: 11, color: COLORS.secondary, align: 'center', valign: 'top',
1875
- fontFace: FONTS.mono,
1876
- });
1877
- slide.addText(left.label || '', {
1878
- x: 0.8, y: 5.1, w: 4.0, h: 0.5,
1879
- fontSize: 22, color: COLORS.primary, bold: true, align: 'center', valign: 'middle',
1880
- fontFace: FONTS.heading,
1881
- });
1882
- slide.addText(right.labelEn || '', {
1883
- x: 5.2, y: 2.45, w: 4.0, h: 0.25,
1884
- fontSize: 11, color: COLORS.secondary, align: 'center', valign: 'top',
1885
- fontFace: FONTS.mono,
1886
- });
1887
- slide.addText(right.label || '', {
1888
- x: 5.2, y: 5.1, w: 4.0, h: 0.5,
1889
- fontSize: 22, color: COLORS.primary, bold: true, align: 'center', valign: 'middle',
1890
- fontFace: FONTS.heading,
1891
- });
1892
- // 中央结论卡
1893
- slide.addShape('rect', {
1894
- x: 3.3, y: 2.7, w: 3.4, h: 2.8,
1895
- fill: { color: COLORS.surface },
1896
- line: { color: COLORS.border, width: 1 },
1897
- rectRadius: 0.12,
1898
- });
1899
- if (centerCard.title) {
1900
- slide.addText(centerCard.title, {
1901
- x: 3.5, y: 2.85, w: 3.0, h: 0.4,
1902
- fontSize: 16, color: COLORS.primary, bold: true, align: 'center', valign: 'top',
1903
- fontFace: FONTS.heading,
1904
- });
1905
- }
1906
- comparisons.forEach((item, index) => {
1907
- const y = 3.35 + index * 0.55;
1908
- slide.addText(item.leftValue || '', {
1909
- x: 3.5, y, w: 1.0, h: 0.25,
1910
- fontSize: 13, color: COLORS.primary, bold: true, align: 'right', valign: 'middle',
1911
- fontFace: FONTS.heading,
1912
- });
1913
- slide.addText(item.leftLabel || '', {
1914
- x: 3.5, y: y + 0.22, w: 1.0, h: 0.18,
1915
- fontSize: 9, color: COLORS.secondary, align: 'right', valign: 'middle',
1916
- fontFace: FONTS.body,
1917
- });
1918
- slide.addText('VS', {
1919
- x: 4.6, y, w: 0.8, h: 0.25,
1920
- fontSize: 10, color: COLORS.accent, bold: true, align: 'center', valign: 'middle',
1921
- fontFace: FONTS.mono,
1922
- });
1923
- slide.addText(item.rightValue || '', {
1924
- x: 5.5, y, w: 1.0, h: 0.25,
1925
- fontSize: 13, color: COLORS.primary, bold: true, align: 'left', valign: 'middle',
1926
- fontFace: FONTS.heading,
1927
- });
1928
- slide.addText(item.rightLabel || '', {
1929
- x: 5.5, y: y + 0.22, w: 1.0, h: 0.18,
1930
- fontSize: 9, color: COLORS.secondary, align: 'left', valign: 'middle',
1931
- fontFace: FONTS.body,
1932
- });
1933
- });
1934
- if (centerCard.conclusion) {
1935
- slide.addText(centerCard.conclusion, {
1936
- x: 3.5, y: 4.95, w: 3.0, h: 0.45,
1937
- fontSize: 11, color: COLORS.secondary, align: 'center', valign: 'top',
1938
- fontFace: FONTS.body,
1939
- });
1940
- }
1941
- if (props.footnote) {
1942
- slide.addText(props.footnote, {
1943
- x: 0.8, y: 5.85, w: 8.4, h: 0.25,
1944
- fontSize: 11, color: COLORS.secondary, align: 'left',
1945
- fontFace: FONTS.mono,
1946
- });
1947
- }
1948
- }
1949
- function renderTheme01FilmstripV1(slide, props) {
1950
- addKicker(slide, props.kicker);
1951
- addTitle(slide, props.title ?? 'Filmstrip');
1952
- const images = (props.images || []).slice(0, 5);
1953
- if (images.length === 0) {
1954
- slide.addText('(暂无影像)', {
1955
- x: 2, y: 3.2, w: 6, h: 0.8,
1956
- fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
1957
- fontFace: FONTS.body,
1958
- });
1959
- return;
1960
- }
1961
- const gap = 0.2;
1962
- const frameW = (8.4 - gap * (images.length - 1)) / images.length;
1963
- const startX = 0.8;
1964
- const y = 2.5;
1965
- const imgH = 2.2;
1966
- images.forEach((image, index) => {
1967
- const x = startX + index * (frameW + gap);
1968
- slide.addShape('rect', {
1969
- x, y, w: frameW, h: imgH + 0.5,
1970
- fill: { color: COLORS.surfaceElevated },
1971
- line: { color: COLORS.border, width: 1 },
1972
- rectRadius: 0.08,
1973
- });
1974
- if (image.url)
1975
- addImageMaybe(slide, image.url, x + 0.06, y + 0.06, frameW - 0.12, imgH);
1976
- if (image.caption) {
1977
- slide.addText(image.caption, {
1978
- x, y: y + imgH + 0.08, w: frameW, h: 0.35,
1979
- fontSize: 11, color: COLORS.secondary, align: 'center', valign: 'middle',
1980
- fontFace: FONTS.body,
1981
- });
1982
- }
1983
- });
1984
- }
1985
- function renderTheme01GalleryV1(slide, props) {
1986
- addKicker(slide, props.kicker);
1987
- addTitle(slide, props.title ?? 'Gallery');
1988
- const images = (props.images || []).slice(0, 4);
1989
- const positions = [
1990
- { x: 0.8, y: 2.5 },
1991
- { x: 5.2, y: 2.5 },
1992
- { x: 0.8, y: 4.0 },
1993
- { x: 5.2, y: 4.0 },
1994
- ];
1995
- images.forEach((image, index) => {
1996
- const pos = positions[index];
1997
- if (!pos)
1998
- return;
1999
- slide.addShape('rect', {
2000
- x: pos.x, y: pos.y, w: 4.0, h: 1.3,
2001
- fill: { color: COLORS.surfaceElevated },
2002
- line: { color: COLORS.border, width: 1 },
2003
- rectRadius: 0.08,
2004
- });
2005
- if (image.url)
2006
- addImageMaybe(slide, image.url, pos.x + 0.08, pos.y + 0.08, 3.84, 0.9);
2007
- if (image.caption) {
2008
- slide.addText(image.caption, {
2009
- x: pos.x, y: pos.y + 1.05, w: 4.0, h: 0.25,
2010
- fontSize: 12, color: COLORS.secondary, align: 'center',
2011
- fontFace: FONTS.body,
2012
- });
2013
- }
2014
- });
2015
- }
2016
- function renderTheme01GanttV1(slide, props) {
2017
- addKicker(slide, props.kicker);
2018
- addTitle(slide, props.title ?? '甘特排期');
2019
- const tasks = (props.tasks || []).slice(0, 8);
2020
- const phases = (props.phases?.length ? props.phases : ['Q1', 'Q2', 'Q3', 'Q4']).slice(0, 6);
2021
- const chartColors = ['6E9BC4', '8FAE84', 'D9A441', 'C76B5A', 'A07CAE', 'C98BA0', '5FA8A0', 'A7A85F'];
2022
- if (tasks.length === 0) {
2023
- slide.addText('(暂无排期数据)', {
2024
- x: 2, y: 3.2, w: 6, h: 0.8,
2025
- fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
2026
- fontFace: FONTS.body,
2027
- });
2028
- return;
2029
- }
2030
- const startX = 0.8;
2031
- const endX = 9.2;
2032
- const totalW = endX - startX;
2033
- const headerY = 2.3;
2034
- const bodyY = 2.85;
2035
- const rowH = 0.42;
2036
- const phaseW = totalW / phases.length;
2037
- // phase header
2038
- phases.forEach((phase, i) => {
2039
- const x = startX + i * phaseW;
2040
- slide.addText(phase, {
2041
- x, y: headerY, w: phaseW, h: 0.35,
2042
- fontSize: 12, color: COLORS.secondary, align: 'center', valign: 'middle',
2043
- fontFace: FONTS.mono,
2044
- });
2045
- if (i > 0) {
2046
- slide.addShape('line', {
2047
- x1: x, y1: headerY, x2: x, y2: bodyY + tasks.length * rowH,
2048
- line: { color: COLORS.border, width: 1 },
2049
- });
2050
- }
2051
- });
2052
- // tasks
2053
- tasks.forEach((task, index) => {
2054
- const y = bodyY + index * rowH;
2055
- const start = Math.max(0, Math.min(100, task.start ?? 0));
2056
- const end = Math.max(start, Math.min(100, task.end ?? 100));
2057
- const barX = startX + (start / 100) * totalW;
2058
- const barW = ((end - start) / 100) * totalW;
2059
- const color = task.color ? task.color.replace('#', '') : chartColors[index % chartColors.length];
2060
- slide.addText(task.name || '', {
2061
- x: 0.8, y, w: 2.0, h: rowH,
2062
- fontSize: 12, color: COLORS.primary, align: 'left', valign: 'middle',
2063
- fontFace: FONTS.body,
2064
- });
2065
- slide.addShape('rect', {
2066
- x: barX, y: y + 0.08, w: Math.max(0.05, barW), h: 0.26,
2067
- fill: { color },
2068
- rectRadius: 0.05,
2069
- });
2070
- });
2071
- }
2072
- function renderTheme01MetricBig(slide, props) {
2073
- const hasInsight = props.showInsight !== false && !!props.insight && (!!props.insight.value || !!props.insight.label || !!props.insight.description);
2074
- const mainW = hasInsight ? 6.8 : 8.4;
2075
- const mainX = 0.8;
2076
- addKicker(slide, props.kicker);
2077
- addTitle(slide, props.title ?? '核心指标', mainX, 1.3, mainW);
2078
- if (props.subtitle) {
2079
- slide.addText(props.subtitle, {
2080
- x: mainX, y: 2.05, w: mainW, h: 0.35,
2081
- fontSize: 16, color: COLORS.secondary, align: 'left', valign: 'top',
2082
- fontFace: FONTS.body,
2083
- });
2084
- }
2085
- const valueText = [props.value, props.unit].filter(Boolean).join(' ') || '0';
2086
- slide.addText(valueText, {
2087
- x: mainX, y: 2.7, w: mainW, h: 1.2,
2088
- fontSize: hasInsight ? 60 : 72, color: COLORS.primary, bold: true, align: 'center', valign: 'middle',
2089
- fontFace: FONTS.heading,
2090
- });
2091
- if (props.context) {
2092
- slide.addText(props.context, {
2093
- x: mainX + (hasInsight ? 0.7 : 0.7), y: 4.0, w: hasInsight ? mainW - 1.4 : 7.0, h: 0.6,
2094
- fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'top',
2095
- fontFace: FONTS.body,
2096
- });
2097
- }
2098
- const metrics = (props.metrics || []).filter((m) => m != null && (m.value || m.label)).slice(0, 4);
2099
- if (metrics.length > 0) {
2100
- const count = metrics.length;
2101
- const gap = 0.25;
2102
- const cardW = (mainW - gap * (count - 1)) / count;
2103
- const startX = mainX;
2104
- const y = 4.85;
2105
- const h = 0.65;
2106
- metrics.forEach((metric, index) => {
2107
- const x = startX + index * (cardW + gap);
2108
- const bg = metric.accent ? COLORS.accent : COLORS.surfaceElevated;
2109
- const textColor = metric.accent ? COLORS.white : COLORS.primary;
2110
- slide.addShape('rect', {
2111
- x, y, w: cardW, h,
2112
- fill: { color: bg },
2113
- line: { color: COLORS.border, width: 1 },
2114
- rectRadius: 0.1,
2115
- });
2116
- slide.addText(metric.value || '', {
2117
- x: x + 0.1, y: y + 0.08, w: cardW - 0.2, h: 0.32,
2118
- fontSize: 18, color: textColor, bold: true, align: 'center', valign: 'top',
2119
- fontFace: FONTS.heading,
2120
- });
2121
- slide.addText(metric.label || '', {
2122
- x: x + 0.1, y: y + 0.36, w: cardW - 0.2, h: 0.24,
2123
- fontSize: 11, color: metric.accent ? COLORS.white : COLORS.secondary, align: 'center', valign: 'top',
2124
- fontFace: FONTS.body,
2125
- });
2126
- });
2127
- }
2128
- if (props.footnote) {
2129
- slide.addText(props.footnote, {
2130
- x: mainX, y: 5.7, w: mainW, h: 0.25,
2131
- fontSize: 11, color: COLORS.secondary, align: 'left',
2132
- fontFace: FONTS.mono,
2133
- });
2134
- }
2135
- if (hasInsight) {
2136
- renderInsightPanel(slide, props.insight, 8.0, 2.3, 2.4, 3.2);
2137
- }
2138
- }
2139
- function renderTheme01MetricTriptych(slide, props) {
2140
- addKicker(slide, props.kicker);
2141
- addTitle(slide, props.title ?? '指标总览');
2142
- if (props.subtitle) {
2143
- slide.addText(props.subtitle, {
2144
- x: 0.8, y: 2.15, w: 8.4, h: 0.35,
2145
- fontSize: 14, color: COLORS.secondary, align: 'left', valign: 'top',
2146
- fontFace: FONTS.body,
2147
- });
2148
- }
2149
- const panels = (props.panels || []).slice(0, 3);
2150
- if (panels.length === 0) {
2151
- slide.addText('(暂无指标)', {
2152
- x: 2, y: 3.2, w: 6, h: 0.8,
2153
- fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
2154
- fontFace: FONTS.body,
2155
- });
2156
- return;
2157
- }
2158
- const panelW = 8.4 / panels.length;
2159
- const startX = 0.8;
2160
- const startY = 2.7;
2161
- const cardH = 3.0;
2162
- panels.forEach((panel, index) => {
2163
- const x = startX + index * panelW;
2164
- slide.addShape('rect', {
2165
- x, y: startY, w: panelW - 0.15, h: cardH,
2166
- fill: { color: COLORS.surfaceElevated },
2167
- line: { color: COLORS.border, width: 1 },
2168
- rectRadius: 0.1,
2169
- });
2170
- slide.addText(panel.index || String(index + 1).padStart(2, '0'), {
2171
- x: x + 0.15, y: startY + 0.15, w: 0.6, h: 0.35,
2172
- fontSize: 14, color: COLORS.accent, bold: true, align: 'left', valign: 'top',
2173
- fontFace: FONTS.mono,
2174
- });
2175
- slide.addText(panel.title || '', {
2176
- x: x + 0.15, y: startY + 0.55, w: panelW - 0.45, h: 0.35,
2177
- fontSize: 14, color: COLORS.secondary, align: 'left', valign: 'top',
2178
- fontFace: FONTS.body,
2179
- });
2180
- slide.addText(panel.value || '', {
2181
- x: x + 0.15, y: startY + 0.95, w: panelW - 0.45, h: 0.6,
2182
- fontSize: 32, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
2183
- fontFace: FONTS.heading,
2184
- });
2185
- // 简化迷你图:用一组小方块表示趋势
2186
- const values = (panel.chartData || []).filter((v) => typeof v === 'number');
2187
- if (values.length > 1) {
2188
- const max = Math.max(...values, 1);
2189
- const min = Math.min(...values, 0);
2190
- const range = max - min || 1;
2191
- const barW = (panelW - 0.6) / values.length * 0.7;
2192
- const gap = (panelW - 0.6) / values.length * 0.3;
2193
- values.forEach((v, i) => {
2194
- const barH = Math.max(((v - min) / range) * 0.6, 0.08);
2195
- slide.addShape('rect', {
2196
- x: x + 0.15 + i * (barW + gap),
2197
- y: startY + 2.4 - barH,
2198
- w: barW,
2199
- h: barH,
2200
- fill: { color: COLORS.accent },
2201
- });
2202
- });
2203
- }
2204
- slide.addText(panel.subtitle || '', {
2205
- x: x + 0.15, y: startY + 2.45, w: panelW - 0.45, h: 0.4,
2206
- fontSize: 12, color: COLORS.secondary, align: 'left', valign: 'top',
2207
- fontFace: FONTS.body,
2208
- });
2209
- });
2210
- if (props.footnote) {
2211
- slide.addText(props.footnote, {
2212
- x: 0.8, y: 5.85, w: 8.4, h: 0.25,
2213
- fontSize: 11, color: COLORS.secondary, align: 'left',
2214
- fontFace: FONTS.mono,
2215
- });
2216
- }
2217
- }
2218
- function renderTheme01MetricV2(slide, props) {
2219
- const stats = (props.metrics || []).map((m) => ({
2220
- label: m.label || '',
2221
- value: m.value || '0',
2222
- unit: m.unit || '',
2223
- }));
2224
- renderStatsV1(slide, { kicker: props.kicker, title: props.title ?? '关键指标', stats });
2225
- }
2226
- function renderTheme01OutlookV1(slide, props) {
2227
- addKicker(slide, props.kicker);
2228
- addTitle(slide, props.title ?? '投资展望');
2229
- const items = (props.items || []).slice(0, 3);
2230
- if (items.length === 0) {
2231
- slide.addText('(暂无展望数据)', {
2232
- x: 2, y: 3.2, w: 6, h: 0.8,
2233
- fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
2234
- fontFace: FONTS.body,
2235
- });
2236
- return;
2237
- }
2238
- const startX = 0.8;
2239
- const y = 2.35;
2240
- const gap = 0.2;
2241
- const cardW = (8.4 - gap * (items.length - 1)) / items.length;
2242
- const cardH = 2.8;
2243
- items.forEach((item, index) => {
2244
- const x = startX + index * (cardW + gap);
2245
- slide.addShape('rect', {
2246
- x, y, w: cardW, h: cardH,
2247
- fill: { color: COLORS.surfaceElevated },
2248
- line: { color: COLORS.border, width: 1 },
2249
- rectRadius: 0.1,
2250
- });
2251
- slide.addShape('ellipse', {
2252
- x: x + cardW / 2 - 0.2, y: y + 0.18, w: 0.4, h: 0.4,
2253
- fill: { color: COLORS.accent },
2254
- });
2255
- slide.addText(String(index + 1), {
2256
- x: x + cardW / 2 - 0.2, y: y + 0.18, w: 0.4, h: 0.4,
2257
- fontSize: 16, color: COLORS.white, bold: true, align: 'center', valign: 'middle',
2258
- fontFace: FONTS.heading,
2259
- });
2260
- slide.addText(item.title || '', {
2261
- x: x + 0.14, y: y + 0.72, w: cardW - 0.28, h: 0.45,
2262
- fontSize: 16, color: COLORS.primary, bold: true, align: 'center', valign: 'top',
2263
- fontFace: FONTS.heading,
2264
- });
2265
- slide.addText(`趋势:${item.trend || ''}`, {
2266
- x: x + 0.14, y: y + 1.25, w: cardW - 0.28, h: 0.55,
2267
- fontSize: 12, color: COLORS.secondary, align: 'left', valign: 'top',
2268
- fontFace: FONTS.body,
2269
- });
2270
- slide.addText(`行动:${item.action || ''}`, {
2271
- x: x + 0.14, y: y + 1.85, w: cardW - 0.28, h: cardH - 2.05,
2272
- fontSize: 12, color: COLORS.primary, align: 'left', valign: 'top',
2273
- fontFace: FONTS.body,
2274
- });
2275
- });
2276
- }
2277
- function renderTheme01QuadrantV1(slide, props) {
2278
- addKicker(slide, props.kicker);
2279
- addTitle(slide, props.title ?? '四象限分析');
2280
- const quadrants = (props.quadrants || []).slice(0, 4);
2281
- const defaultLabels = ['高价值 / 高可行性', '高价值 / 低可行性', '低价值 / 高可行性', '低价值 / 低可行性'];
2282
- const colors = ['6E9BC4', '8FAE84', 'D9A441', 'C76B5A'];
2283
- if (quadrants.length === 0) {
2284
- slide.addText('(暂无四象限数据)', {
2285
- x: 2, y: 3.2, w: 6, h: 0.8,
2286
- fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
2287
- fontFace: FONTS.body,
2288
- });
2289
- return;
2290
- }
2291
- const startX = 0.8;
2292
- const startY = 2.4;
2293
- const gap = 0.2;
2294
- const cardW = (8.4 - gap) / 2;
2295
- const cardH = (2.8 - gap) / 2;
2296
- quadrants.forEach((q, index) => {
2297
- const col = index % 2;
2298
- const row = Math.floor(index / 2);
2299
- const x = startX + col * (cardW + gap);
2300
- const y = startY + row * (cardH + gap);
2301
- const color = colors[index % colors.length];
2302
- slide.addShape('rect', {
2303
- x, y, w: cardW, h: cardH,
2304
- fill: { color: COLORS.surfaceElevated },
2305
- line: { color: COLORS.border, width: 1 },
2306
- rectRadius: 0.1,
2307
- });
2308
- slide.addShape('rect', {
2309
- x, y, w: cardW, h: 0.08,
2310
- fill: { color },
2311
- rectRadius: 0.1,
2312
- });
2313
- slide.addText(q.label || defaultLabels[index] || '', {
2314
- x: x + 0.14, y: y + 0.2, w: cardW - 0.28, h: 0.4,
2315
- fontSize: 15, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
2316
- fontFace: FONTS.heading,
2317
- });
2318
- const items = (q.items || []).slice(0, 4);
2319
- if (items.length > 0) {
2320
- slide.addText(items.join('\n'), {
2321
- x: x + 0.14, y: y + 0.65, w: cardW - 0.28, h: cardH - 0.8,
2322
- fontSize: 12, color: COLORS.secondary, align: 'left', valign: 'top',
2323
- fontFace: FONTS.body,
2324
- });
2325
- }
2326
- });
2327
- if (props.xAxis) {
2328
- slide.addText(props.xAxis, {
2329
- x: startX, y: 5.35, w: 8.4, h: 0.3,
2330
- fontSize: 12, color: COLORS.secondary, align: 'center', valign: 'top',
2331
- fontFace: FONTS.mono,
2332
- });
2333
- }
2334
- if (props.yAxis) {
2335
- slide.addText(props.yAxis, {
2336
- x: 0.2, y: startY + 1.25, w: 0.4, h: 0.3,
2337
- fontSize: 12, color: COLORS.secondary, align: 'center', valign: 'middle',
2338
- fontFace: FONTS.mono,
2339
- });
2340
- }
2341
- }
2342
- function renderTheme01RankingV1(slide, props) {
2343
- addKicker(slide, props.kicker);
2344
- addTitle(slide, props.title ?? '排名');
2345
- const items = (props.items || [])
2346
- .filter((item) => typeof item.value === 'number')
2347
- .sort((a, b) => (b.value ?? 0) - (a.value ?? 0))
2348
- .slice(0, 8);
2349
- if (items.length === 0) {
2350
- slide.addText('(暂无排名数据)', {
2351
- x: 2, y: 3.2, w: 6, h: 0.8,
2352
- fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
2353
- fontFace: FONTS.body,
2354
- });
2355
- return;
2356
- }
2357
- const labels = items.map((item) => item.label || '');
2358
- const values = items.map((item) => item.value ?? 0);
2359
- const chartColors = items.map((_, i) => CHART_COLORS[i % CHART_COLORS.length]);
2360
- slide.addChart('bar', [{ name: props.title || '数值', labels, values }], {
2361
- x: 0.8, y: 2.3, w: 8.4, h: 3.2,
2362
- chartColors,
2363
- showValue: true,
2364
- dataLabelColor: COLORS.primary,
2365
- dataLabelFontSize: 11,
2366
- });
2367
- }
2368
- function renderTheme01RegionV1(slide, props) {
2369
- addKicker(slide, props.kicker);
2370
- addTitle(slide, props.title ?? '地区/市场分布');
2371
- const regions = (props.regions || []).slice(0, 6);
2372
- if (regions.length === 0) {
2373
- slide.addText('(暂无地区数据)', {
2374
- x: 2, y: 3.2, w: 6, h: 0.8,
2375
- fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
2376
- fontFace: FONTS.body,
2377
- });
2378
- return;
2379
- }
2380
- const startX = 0.8;
2381
- const startY = 2.35;
2382
- const gap = 0.2;
2383
- const cols = regions.length <= 4 ? 3 : 3;
2384
- const rows = Math.ceil(regions.length / cols);
2385
- const cardW = (8.4 - gap * (cols - 1)) / cols;
2386
- const cardH = (2.8 - gap * (rows - 1)) / rows;
2387
- regions.forEach((region, index) => {
2388
- const col = index % cols;
2389
- const row = Math.floor(index / cols);
2390
- const x = startX + col * (cardW + gap);
2391
- const y = startY + row * (cardH + gap);
2392
- slide.addShape('rect', {
2393
- x, y, w: cardW, h: cardH,
2394
- fill: { color: COLORS.surfaceElevated },
2395
- line: { color: COLORS.border, width: 1 },
2396
- rectRadius: 0.1,
2397
- });
2398
- slide.addText(region.name || '', {
2399
- x: x + 0.12, y: y + 0.12, w: cardW - 0.24, h: 0.4,
2400
- fontSize: 14, color: COLORS.secondary, align: 'left', valign: 'top',
2401
- fontFace: FONTS.body,
2402
- });
2403
- slide.addText(region.value || '', {
2404
- x: x + 0.12, y: y + 0.5, w: cardW - 0.24, h: 0.55,
2405
- fontSize: 24, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
2406
- fontFace: FONTS.heading,
2407
- });
2408
- if (region.change) {
2409
- slide.addText(region.change, {
2410
- x: x + 0.12, y: y + 1.05, w: cardW - 0.24, h: 0.3,
2411
- fontSize: 12, color: COLORS.accent, align: 'left', valign: 'top',
2412
- fontFace: FONTS.mono,
2413
- });
2414
- }
2415
- if (region.note) {
2416
- slide.addText(region.note, {
2417
- x: x + 0.12, y: y + 1.4, w: cardW - 0.24, h: cardH - 1.55,
2418
- fontSize: 11, color: COLORS.secondary, align: 'left', valign: 'top',
2419
- fontFace: FONTS.body,
2420
- });
2421
- }
2422
- });
2423
- }
2424
- function renderTheme01RiskV1(slide, props) {
2425
- addKicker(slide, props.kicker);
2426
- addTitle(slide, props.title ?? '风险研判');
2427
- const items = (props.items || []).slice(0, 4);
2428
- if (items.length === 0) {
2429
- slide.addText('(暂无风险数据)', {
2430
- x: 2, y: 3.2, w: 6, h: 0.8,
2431
- fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
2432
- fontFace: FONTS.body,
2433
- });
2434
- return;
2435
- }
2436
- const startX = 0.8;
2437
- const startY = 2.35;
2438
- const gap = 0.22;
2439
- const cardW = (8.4 - gap) / 2;
2440
- const cardH = (2.8 - gap) / 2;
2441
- items.forEach((item, index) => {
2442
- const col = index % 2;
2443
- const row = Math.floor(index / 2);
2444
- const x = startX + col * (cardW + gap);
2445
- const y = startY + row * (cardH + gap);
2446
- slide.addShape('rect', {
2447
- x, y, w: cardW, h: cardH,
2448
- fill: { color: COLORS.surfaceElevated },
2449
- line: { color: COLORS.border, width: 1 },
2450
- rectRadius: 0.1,
2451
- });
2452
- slide.addText(String(index + 1).padStart(2, '0'), {
2453
- x: x + cardW - 0.55, y: y + 0.12, w: 0.45, h: 0.35,
2454
- fontSize: 14, color: COLORS.accent, align: 'right', valign: 'top',
2455
- fontFace: FONTS.mono,
2456
- });
2457
- slide.addText(item.risk || '', {
2458
- x: x + 0.14, y: y + 0.12, w: cardW - 0.7, h: 0.45,
2459
- fontSize: 15, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
2460
- fontFace: FONTS.heading,
2461
- });
2462
- slide.addText(`影响:${item.impact || ''}`, {
2463
- x: x + 0.14, y: y + 0.62, w: cardW - 0.28, h: 0.35,
2464
- fontSize: 12, color: COLORS.secondary, align: 'left', valign: 'top',
2465
- fontFace: FONTS.body,
2466
- });
2467
- slide.addText(`应对:${item.response || ''}`, {
2468
- x: x + 0.14, y: y + 1.0, w: cardW - 0.28, h: cardH - 1.15,
2469
- fontSize: 12, color: COLORS.primary, align: 'left', valign: 'top',
2470
- fontFace: FONTS.body,
2471
- });
2472
- });
2473
- }
2474
- function renderTheme01RoadmapV1(slide, props) {
2475
- const phases = (props.phases || []).map((p) => ({
2476
- title: p.phase || '',
2477
- description: (p.items || []).join('\n'),
2478
- }));
2479
- renderRoadmapV1(slide, { kicker: props.kicker, title: props.title ?? '路线图', phases });
2480
- }
2481
- function renderTheme01ScorecardV1(slide, props) {
2482
- addKicker(slide, props.kicker);
2483
- addTitle(slide, props.title ?? '评分卡');
2484
- const items = (props.items || []).slice(0, 5);
2485
- if (items.length === 0)
2486
- return;
2487
- const startY = 2.3;
2488
- const rowH = 0.65;
2489
- const barH = 0.12;
2490
- const maxW = 8.4;
2491
- items.forEach((item, index) => {
2492
- const y = startY + index * rowH;
2493
- const score = typeof item.score === 'number' ? item.score : 0;
2494
- const max = typeof item.max === 'number' && item.max > 0 ? item.max : 100;
2495
- const pct = Math.min(100, Math.max(0, (score / max) * 100));
2496
- slide.addText(item.label || '', {
2497
- x: 0.8, y, w: 4.5, h: 0.35,
2498
- fontSize: 16, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
2499
- fontFace: FONTS.heading,
2500
- });
2501
- slide.addText(`${score} / ${max}`, {
2502
- x: 5.4, y, w: 1.8, h: 0.35,
2503
- fontSize: 16, color: COLORS.accent, bold: true, align: 'right', valign: 'top',
2504
- fontFace: FONTS.heading,
2505
- });
2506
- slide.addShape('rect', {
2507
- x: 0.8, y: y + 0.4, w: maxW, h: barH,
2508
- fill: { color: COLORS.light },
2509
- });
2510
- slide.addShape('rect', {
2511
- x: 0.8, y: y + 0.4, w: maxW * (pct / 100), h: barH,
2512
- fill: { color: COLORS.accent },
2513
- });
2514
- });
2515
- }
2516
- function renderTheme01SpotlightGrid(slide, props) {
2517
- addKicker(slide, props.kicker);
2518
- addTitle(slide, props.title ?? '主题聚焦');
2519
- if (props.subtitle) {
2520
- slide.addText(props.subtitle, {
2521
- x: 0.8, y: 2.15, w: 8.4, h: 0.35,
2522
- fontSize: 14, color: COLORS.secondary, align: 'left', valign: 'top',
2523
- fontFace: FONTS.body,
2524
- });
2525
- }
2526
- const columns = (props.columns || []).slice(0, 4);
2527
- if (columns.length === 0) {
2528
- slide.addText('(暂无内容)', {
2529
- x: 2, y: 3.2, w: 6, h: 0.8,
2530
- fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
2531
- fontFace: FONTS.body,
2532
- });
2533
- return;
2534
- }
2535
- const colW = 8.4 / columns.length;
2536
- const startX = 0.8;
2537
- const startY = 2.7;
2538
- const cardH = 3.0;
2539
- columns.forEach((col, index) => {
2540
- const x = startX + index * colW;
2541
- slide.addShape('rect', {
2542
- x, y: startY, w: colW - 0.15, h: cardH,
2543
- fill: { color: COLORS.surfaceElevated },
2544
- line: { color: COLORS.border, width: 1 },
2545
- rectRadius: 0.1,
2546
- });
2547
- if (col.tag) {
2548
- slide.addText(col.tag, {
2549
- x: x + 0.15, y: startY + 0.15, w: colW - 0.45, h: 0.3,
2550
- fontSize: 12, color: COLORS.accent, bold: true, align: 'left', valign: 'top',
2551
- fontFace: FONTS.mono,
2552
- });
2553
- }
2554
- if (col.imageUrl) {
2555
- addImageMaybe(slide, col.imageUrl, x + 0.15, startY + 0.55, colW - 0.45, 1.0);
2556
- }
2557
- slide.addText(col.title || '', {
2558
- x: x + 0.15, y: startY + 1.65, w: colW - 0.45, h: 0.45,
2559
- fontSize: 18, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
2560
- fontFace: FONTS.heading,
2561
- });
2562
- slide.addText(col.description || '', {
2563
- x: x + 0.15, y: startY + 2.1, w: colW - 0.45, h: 0.75,
2564
- fontSize: 12, color: COLORS.secondary, align: 'left', valign: 'top',
2565
- fontFace: FONTS.body,
2566
- });
2567
- });
2568
- if (props.footnote) {
2569
- slide.addText(props.footnote, {
2570
- x: 0.8, y: 5.85, w: 8.4, h: 0.25,
2571
- fontSize: 11, color: COLORS.secondary, align: 'left',
2572
- fontFace: FONTS.mono,
2573
- });
2574
- }
2575
- }
2576
- function renderTheme01TableData(slide, props) {
2577
- addKicker(slide, props.kicker);
2578
- addTitle(slide, props.title ?? '数据表格');
2579
- if (props.subtitle) {
2580
- slide.addText(props.subtitle, {
2581
- x: 0.8, y: 2.05, w: 8.4, h: 0.35,
2582
- fontSize: 16, color: COLORS.secondary, align: 'left', valign: 'top',
2583
- fontFace: FONTS.body,
2584
- });
2585
- }
2586
- const columns = (props.columns || []).filter((c) => c.key && c.label);
2587
- const rows = (props.rows || []).filter((r) => r != null).slice(0, 12);
2588
- const effectiveHighlightRow = props.highlightRow ?? (Array.isArray(props.highlightRows) && props.highlightRows.length ? props.highlightRows[0] : undefined);
2589
- if (columns.length === 0 || rows.length === 0) {
2590
- slide.addText('(暂无表格数据)', {
2591
- x: 2, y: 3.2, w: 6, h: 0.8,
2592
- fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
2593
- fontFace: FONTS.body,
2594
- });
2595
- return;
2596
- }
2597
- const tableW = 8.4;
2598
- const startX = 0.8;
2599
- const startY = 2.55;
2600
- const rowH = 0.42;
2601
- const colW = tableW / columns.length;
2602
- columns.forEach((column, index) => {
2603
- const x = startX + index * colW;
2604
- slide.addShape('rect', {
2605
- x, y: startY, w: colW, h: rowH,
2606
- fill: { color: COLORS.surfaceElevated },
2607
- line: { color: COLORS.border, width: 1 },
2608
- });
2609
- slide.addText(column.label || '', {
2610
- x, y: startY, w: colW, h: rowH,
2611
- fontSize: 12, color: COLORS.primary, bold: true, align: column.align || 'center', valign: 'middle',
2612
- fontFace: FONTS.heading,
2613
- });
2614
- });
2615
- rows.forEach((row, rowIndex) => {
2616
- const y = startY + rowH * (rowIndex + 1);
2617
- const isHighlight = effectiveHighlightRow === rowIndex;
2618
- const bg = isHighlight ? '3A2A20' : (rowIndex % 2 === 0 ? COLORS.surface : COLORS.surfaceElevated);
2619
- columns.forEach((column, colIndex) => {
2620
- const x = startX + colIndex * colW;
2621
- slide.addShape('rect', {
2622
- x, y, w: colW, h: rowH,
2623
- fill: { color: bg },
2624
- line: { color: COLORS.border, width: 1 },
2625
- });
2626
- slide.addText(row[column.key || ''] ?? '', {
2627
- x: x + 0.06, y, w: colW - 0.12, h: rowH,
2628
- fontSize: 11, color: isHighlight ? COLORS.accent : COLORS.primary,
2629
- bold: isHighlight,
2630
- align: column.align || 'left', valign: 'middle',
2631
- fontFace: FONTS.body,
2632
- });
2633
- });
2634
- });
2635
- if (props.footnote) {
2636
- slide.addText(props.footnote, {
2637
- x: 0.8, y: 5.75, w: 8.4, h: 0.25,
2638
- fontSize: 11, color: COLORS.secondary, align: 'left',
2639
- fontFace: FONTS.mono,
2640
- });
2641
- }
2642
- }
2643
- function renderTheme01TableV1(slide, props) {
2644
- addKicker(slide, props.kicker);
2645
- addTitle(slide, props.title ?? 'Table');
2646
- const headers = props.headers || [];
2647
- const rows = (props.rows || []).slice(0, 6);
2648
- if (rows.length === 0) {
2649
- slide.addText('(暂无表格数据)', {
2650
- x: 2, y: 3.2, w: 6, h: 0.8,
2651
- fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
2652
- fontFace: FONTS.body,
2653
- });
2654
- return;
2655
- }
2656
- const colCount = Math.max(headers.length, rows[0]?.length || 1);
2657
- const tableW = 8.4;
2658
- const startX = 0.8;
2659
- const startY = 2.5;
2660
- const rowH = 0.45;
2661
- const colW = tableW / colCount;
2662
- // Header
2663
- headers.forEach((header, index) => {
2664
- const x = startX + index * colW;
2665
- slide.addShape('rect', {
2666
- x, y: startY, w: colW, h: rowH,
2667
- fill: { color: COLORS.surfaceElevated },
2668
- line: { color: COLORS.border, width: 1 },
2669
- });
2670
- slide.addText(header, {
2671
- x, y: startY, w: colW, h: rowH,
2672
- fontSize: 12, color: COLORS.primary, bold: true, align: 'center', valign: 'middle',
2673
- fontFace: FONTS.heading,
2674
- });
2675
- });
2676
- // Rows
2677
- rows.forEach((row, rowIndex) => {
2678
- const y = startY + rowH * (rowIndex + 1);
2679
- const bg = rowIndex % 2 === 0 ? COLORS.surface : COLORS.surfaceElevated;
2680
- row.forEach((cell, colIndex) => {
2681
- const x = startX + colIndex * colW;
2682
- slide.addShape('rect', {
2683
- x, y, w: colW, h: rowH,
2684
- fill: { color: bg },
2685
- line: { color: COLORS.border, width: 1 },
2686
- });
2687
- slide.addText(cell ?? '', {
2688
- x: x + 0.06, y, w: colW - 0.12, h: rowH,
2689
- fontSize: 11, color: props.highlightFirstColumn && colIndex === 0 ? COLORS.primary : COLORS.secondary,
2690
- bold: props.highlightFirstColumn && colIndex === 0,
2691
- align: colIndex === 0 ? 'left' : 'center', valign: 'middle',
2692
- fontFace: FONTS.body,
2693
- });
2694
- });
2695
- });
2696
- }
2697
- function renderTheme01TagsV1(slide, props) {
2698
- addKicker(slide, props.kicker);
2699
- addTitle(slide, props.title ?? 'Tags');
2700
- const tags = (props.tags || []).slice(0, 18);
2701
- if (tags.length === 0) {
2702
- slide.addText('(暂无标签)', {
2703
- x: 2, y: 3.2, w: 6, h: 0.8,
2704
- fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
2705
- fontFace: FONTS.body,
2706
- });
2707
- return;
2708
- }
2709
- const toneColors = {
2710
- positive: { text: '8FAE84', bg: '1E2A1E' },
2711
- negative: { text: 'C76B5A', bg: '2A1C19' },
2712
- accent: { text: COLORS.accent, bg: '2A1E18' },
2713
- };
2714
- let x = 0.8;
2715
- let y = 2.5;
2716
- const maxW = 8.4;
2717
- const gap = 0.15;
2718
- const tagH = 0.42;
2719
- tags.forEach((tag) => {
2720
- const label = tag.label ?? '';
2721
- const valueText = tag.value !== undefined && tag.value > 0 ? ` ${tag.value}` : '';
2722
- const text = `${label}${valueText}`;
2723
- const textW = Math.min(Math.max(text.length * 0.09 + 0.3, 0.7), 2.2);
2724
- const tone = toneColors[tag.tone ?? ''] ?? { text: COLORS.secondary, bg: COLORS.surfaceElevated };
2725
- const color = tone.text;
2726
- const bgColor = tone.bg;
2727
- if (x + textW > 0.8 + maxW) {
2728
- x = 0.8;
2729
- y += tagH + gap;
2730
- }
2731
- slide.addShape('roundRect', {
2732
- x, y, w: textW, h: tagH,
2733
- fill: { color: bgColor },
2734
- line: { color: tag.tone ? color : COLORS.border, width: 1 },
2735
- rectRadius: 0.21,
2736
- });
2737
- slide.addText(text, {
2738
- x, y, w: textW, h: tagH,
2739
- fontSize: 11, color, bold: true, align: 'center', valign: 'middle',
2740
- fontFace: FONTS.body,
2741
- });
2742
- x += textW + gap;
2743
- });
2744
- }
2745
- function renderTheme01TeamV2(slide, props) {
2746
- const members = (props.members || []).map((m) => ({ ...m, avatar: m.imageUrl }));
2747
- renderTeamV2(slide, { kicker: props.kicker, title: props.title ?? '团队', members });
2748
- }
2749
- function renderTheme01TimelineV1(slide, props) {
2750
- const milestones = props.events || [];
2751
- renderTimelineV1(slide, { kicker: props.kicker, title: props.title ?? '时间轴', milestones });
2752
- }
2753
- function renderTheme01TrendV1(slide, props) {
2754
- addKicker(slide, props.kicker);
2755
- addTitle(slide, props.title ?? '趋势');
2756
- const series = (props.series || []).slice(0, 4);
2757
- const allLabels = Array.from(new Set(series.flatMap((s) => (s.data || []).map((p) => p.label || '')))).filter(Boolean);
2758
- if (allLabels.length === 0 || series.length === 0) {
2759
- slide.addText('(暂无趋势数据)', {
2760
- x: 2, y: 3.2, w: 6, h: 0.8,
2761
- fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
2762
- fontFace: FONTS.body,
2763
- });
2764
- return;
2765
- }
2766
- const chartData = series.map((s) => ({
2767
- name: s.name || '系列',
2768
- labels: allLabels,
2769
- values: allLabels.map((label) => {
2770
- const point = (s.data || []).find((p) => p.label === label);
2771
- return typeof point?.value === 'number' ? point.value : 0;
2772
- }),
2773
- }));
2774
- slide.addChart('line', chartData, {
2775
- x: 0.8, y: 2.3, w: 8.4, h: 3.2,
2776
- chartColors: CHART_COLORS,
2777
- showValue: true,
2778
- dataLabelColor: COLORS.primary,
2779
- dataLabelFontSize: 10,
2780
- });
2781
- }
2782
- function renderTimelineV1(slide, props) {
2783
- addKicker(slide, props.kicker);
2784
- addTitle(slide, props.title);
2785
- const milestones = (props.milestones || []).slice(0, 4);
2786
- const count = milestones.length || 1;
2787
- const stepWidth = 8.4 / count;
2788
- milestones.forEach((m, index) => {
2789
- const cx = 0.8 + stepWidth * index + stepWidth / 2;
2790
- slide.addShape('ellipse', {
2791
- x: cx - 0.2, y: 3.0, w: 0.4, h: 0.4,
2792
- fill: { color: COLORS.accent },
2793
- });
2794
- slide.addShape('line', {
2795
- x1: 0.8 + stepWidth * index, y1: 3.2, x2: 0.8 + stepWidth * (index + 1), y2: 3.2,
2796
- line: { color: COLORS.border, width: 2 },
2797
- });
2798
- slide.addText(m.date || '', {
2799
- x: cx - stepWidth / 2 + 0.1, y: 2.4, w: stepWidth - 0.2, h: 0.4,
2800
- fontSize: 12, color: COLORS.accent, align: 'center', valign: 'top',
2801
- fontFace: FONTS.mono,
2802
- });
2803
- slide.addText(m.title || '', {
2804
- x: cx - stepWidth / 2 + 0.1, y: 3.6, w: stepWidth - 0.2, h: 0.4,
2805
- fontSize: 14, color: COLORS.primary, bold: true, align: 'center', valign: 'top',
2806
- fontFace: FONTS.heading,
2807
- });
2808
- slide.addText(m.description || '', {
2809
- x: cx - stepWidth / 2 + 0.1, y: 4.05, w: stepWidth - 0.2, h: 0.8,
2810
- fontSize: 12, color: COLORS.secondary, align: 'center', valign: 'top',
2811
- fontFace: FONTS.body,
2812
- });
2813
- });
2814
- }
2815
- function renderTheme01ComponentsV1(slide, props) {
2816
- addKicker(slide, props.kicker ?? 'DESIGN SYSTEM');
2817
- addTitle(slide, props.title ?? '暖炭暗色编辑风');
2818
- const swatches = [
2819
- [COLORS.accent, '陶土'],
2820
- [CHART_COLORS[0], '岩蓝'],
2821
- [CHART_COLORS[1], '鼠尾草'],
2822
- [CHART_COLORS[2], '黄铜'],
2823
- [CHART_COLORS[3], '赤陶红'],
2824
- [CHART_COLORS[4], '梅紫'],
2825
- [CHART_COLORS[5], '陶玫瑰'],
2826
- [CHART_COLORS[6], '青'],
2827
- [CHART_COLORS[7], '焦橙'],
2828
- [CHART_COLORS[8], '橄榄'],
2829
- ];
2830
- const chipW = 0.92;
2831
- const chipH = 0.5;
2832
- const gap = 0.12;
2833
- const startX = 0.8;
2834
- const startY = 2.5;
2835
- swatches.forEach(([color, name], i) => {
2836
- const col = i % 3;
2837
- const row = Math.floor(i / 3);
2838
- const x = startX + col * (chipW + gap);
2839
- const y = startY + row * (chipH + 0.42);
2840
- slide.addShape('roundRect', {
2841
- x, y, w: chipW, h: chipH,
2842
- fill: { color },
2843
- line: { color: COLORS.border, width: 1 },
2844
- rectRadius: 0.1,
2845
- });
2846
- slide.addText(name, {
2847
- x, y: y + chipH + 0.04, w: chipW, h: 0.32,
2848
- fontSize: 10, color: COLORS.secondary, align: 'center', valign: 'middle',
2849
- fontFace: FONTS.body,
2850
- });
2851
- });
2852
- // 标签三型 + 样本柱图
2853
- const labelY = 4.5;
2854
- slide.addShape('roundRect', { x: 4.4, y: labelY, w: 0.8, h: 0.36, fill: { color: COLORS.accent }, line: { type: 'none' }, rectRadius: 0.18 });
2855
- slide.addText('01', { x: 4.4, y: labelY, w: 0.8, h: 0.36, fontSize: 11, color: COLORS.white, bold: true, align: 'center', valign: 'middle', fontFace: FONTS.mono });
2856
- slide.addShape('roundRect', { x: 5.35, y: labelY, w: 0.9, h: 0.36, fill: { color: COLORS.surfaceElevated }, line: { color: COLORS.border, width: 1 }, rectRadius: 0.18 });
2857
- slide.addText('KEYWORD', { x: 5.35, y: labelY, w: 0.9, h: 0.36, fontSize: 10, color: COLORS.secondary, bold: true, align: 'center', valign: 'middle', fontFace: FONTS.mono });
2858
- const barData = [38, 64, 52, 80, 46, 70];
2859
- const barW = 0.46;
2860
- const bGap = 0.18;
2861
- const baseY = 5.0;
2862
- const bx0 = 4.4;
2863
- barData.forEach((h, i) => {
2864
- const x = bx0 + i * (barW + bGap);
2865
- const hgt = (h / 100) * 0.9;
2866
- slide.addShape('rect', {
2867
- x, y: baseY - hgt, w: barW, h: hgt,
2868
- fill: { color: i % 2 === 0 ? COLORS.accent : CHART_COLORS[0] },
2869
- line: { type: 'none' },
2870
- rectRadius: 0.06,
2871
- });
2872
- });
2873
- }
2874
- // ────────────────────────────────────────────────────────────────
2875
- // 注册表(供 export-pptx 通过 registerTheme01Renderers 注入)
2876
- // ────────────────────────────────────────────────────────────────
2877
- const RENDERERS = {
2878
- 'theme01_appendix_v1': renderTheme01AppendixV1,
2879
- 'theme01_bento_v1': renderTheme01BentoV1,
2880
- 'theme01_case_study': renderTheme01CaseStudy,
2881
- 'theme01_chapter_v1': renderCoverV1,
2882
- 'theme01_chapter_v2': renderCoverV1,
2883
- 'theme01_chapter_v3': renderTheme01ChapterV3,
2884
- 'theme01_chart_bar3d': renderChartBar3d,
2885
- 'theme01_chart_donut': renderTheme01ChartDonut,
2886
- 'theme01_chart_funnel': renderChartFunnel,
2887
- 'theme01_chart_gauge': renderChartGauge,
2888
- 'theme01_chart_graph': renderChartGraph,
2889
- 'theme01_chart_heatmap': renderChartHeatmap,
2890
- 'theme01_chart_radar': renderChartRadar,
2891
- 'theme01_chart_sankey': renderChartSankey,
2892
- 'theme01_chart_sunburst': renderChartSunburst,
2893
- 'theme01_chart_treemap': renderChartTreemap,
2894
- 'theme01_chart_v1': renderChartV1,
2895
- 'theme01_chart_wordcloud': renderChartWordcloud,
2896
- 'theme01_closing_v2': renderClosing,
2897
- 'theme01_comparison_v1': renderComparisonV1,
2898
- 'theme01_comparison_v2': renderTheme01ComparisonV2,
2899
- 'theme01_comparison_v3': renderComparisonV3,
2900
- 'theme01_conclusion_v1': renderTheme01ConclusionV1,
2901
- 'theme01_content_v1': renderContentV1,
2902
- 'theme01_content_v2': renderContentV2,
2903
- 'theme01_content_v3': renderTheme01ContentV3,
2904
- 'theme01_content_v4': renderContentV1,
2905
- 'theme01_cover_v1': renderCoverV1,
2906
- 'theme01_cover_v2': renderCoverV1,
2907
- 'theme01_cover_v3': renderCoverV1,
2908
- 'theme01_cover_v4': renderCoverV1,
2909
- 'theme01_diptych_contrast': renderTheme01DiptychContrast,
2910
- 'theme01_faq_v1': renderFaqV1,
2911
- 'theme01_feature_v1': renderFeatureV1,
2912
- 'theme01_feature_v2': renderFeatureV2,
2913
- 'theme01_filmstrip_v1': renderTheme01FilmstripV1,
2914
- 'theme01_gallery_v1': renderTheme01GalleryV1,
2915
- 'theme01_gantt_v1': renderTheme01GanttV1,
2916
- 'theme01_image_v1': renderImageV1,
2917
- 'theme01_metric_big': renderTheme01MetricBig,
2918
- 'theme01_metric_triptych': renderTheme01MetricTriptych,
2919
- 'theme01_metric_v1': renderMetricV1,
2920
- 'theme01_metric_v2': renderTheme01MetricV2,
2921
- 'theme01_metric_v3': renderMetricV1,
2922
- 'theme01_outlook_v1': renderTheme01OutlookV1,
2923
- 'theme01_partners_v1': renderPartnersV1,
2924
- 'theme01_pest_v1': renderPestV1,
2925
- 'theme01_pricing_v1': renderPricingV1,
2926
- 'theme01_process_v1': renderProcessV1,
2927
- 'theme01_quadrant_v1': renderTheme01QuadrantV1,
2928
- 'theme01_quote_v1': renderQuoteV1,
2929
- 'theme01_quote_v2': renderQuoteV2,
2930
- 'theme01_quote_v3': renderQuoteV3,
2931
- 'theme01_ranking_v1': renderTheme01RankingV1,
2932
- 'theme01_region_v1': renderTheme01RegionV1,
2933
- 'theme01_risk_v1': renderTheme01RiskV1,
2934
- 'theme01_roadmap_v1': renderTheme01RoadmapV1,
2935
- 'theme01_scorecard_v1': renderTheme01ScorecardV1,
2936
- 'theme01_spotlight_grid': renderTheme01SpotlightGrid,
2937
- 'theme01_stats_v1': renderStatsV1,
2938
- 'theme01_swot_v1': renderSwotV1,
2939
- 'theme01_table_data': renderTheme01TableData,
2940
- 'theme01_table_of_contents_v1': renderTableOfContentsV1,
2941
- 'theme01_table_of_contents_v2': renderTableOfContentsV1,
2942
- 'theme01_table_v1': renderTheme01TableV1,
2943
- 'theme01_tags_v1': renderTheme01TagsV1,
2944
- 'theme01_team_v1': renderTeamV1,
2945
- 'theme01_team_v2': renderTheme01TeamV2,
2946
- 'theme01_testimonial_v1': renderTestimonialV1,
2947
- 'theme01_timeline_v1': renderTheme01TimelineV1,
2948
- 'theme01_trend_v1': renderTheme01TrendV1,
2949
- 'theme01_components_v1': renderTheme01ComponentsV1,
2950
- };
2951
- /** 将全部 theme01 渲染器注册到导出管线的按 layoutId 索引注册表(按版式注入双基底 COLORS)。 */
2952
- export function registerTheme01Renderers(register) {
2953
- for (const [id, fn] of Object.entries(RENDERERS)) {
2954
- register(id, (s, p) => { COLORS = P(id); fn(s, p); });
2955
- }
2956
- }
2957
- //# sourceMappingURL=theme01-pptx.js.map