@lemonppt/renderer 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/client/client-render.js +65 -5401
  2. package/dist/client/editor-script.js +3355 -0
  3. package/dist/client/theme-echarts.js +65 -25
  4. package/dist/client-render.d.ts +1 -7
  5. package/dist/client-render.d.ts.map +1 -1
  6. package/dist/client-render.js +1 -11
  7. package/dist/client-render.js.map +1 -1
  8. package/dist/editor-script.d.ts +1 -1
  9. package/dist/editor-script.d.ts.map +1 -1
  10. package/dist/editor-script.js +80 -77
  11. package/dist/editor-script.js.map +1 -1
  12. package/dist/export-pdf.js +1 -1
  13. package/dist/export-pdf.js.map +1 -1
  14. package/dist/export-pptx.d.ts +1132 -2
  15. package/dist/export-pptx.d.ts.map +1 -1
  16. package/dist/export-pptx.js +1208 -19525
  17. package/dist/export-pptx.js.map +1 -1
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -0
  21. package/dist/index.js.map +1 -1
  22. package/dist/render-editor-data.d.ts +21 -0
  23. package/dist/render-editor-data.d.ts.map +1 -0
  24. package/dist/render-editor-data.js +1068 -0
  25. package/dist/render-editor-data.js.map +1 -0
  26. package/dist/render.d.ts +0 -2
  27. package/dist/render.d.ts.map +1 -1
  28. package/dist/render.js +7 -323
  29. package/dist/render.js.map +1 -1
  30. package/dist/theme01-pptx.d.ts +12 -0
  31. package/dist/theme01-pptx.d.ts.map +1 -0
  32. package/dist/theme01-pptx.js +2954 -0
  33. package/dist/theme01-pptx.js.map +1 -0
  34. package/dist/theme02-pptx.d.ts +5 -0
  35. package/dist/theme02-pptx.d.ts.map +1 -0
  36. package/dist/theme02-pptx.js +1063 -0
  37. package/dist/theme02-pptx.js.map +1 -0
  38. package/dist/theme03-pptx.d.ts +5 -0
  39. package/dist/theme03-pptx.d.ts.map +1 -0
  40. package/dist/theme03-pptx.js +3279 -0
  41. package/dist/theme03-pptx.js.map +1 -0
  42. package/dist/theme04-pptx.d.ts +6 -0
  43. package/dist/theme04-pptx.d.ts.map +1 -0
  44. package/dist/theme04-pptx.js +5496 -0
  45. package/dist/theme04-pptx.js.map +1 -0
  46. package/dist/theme05-pptx.d.ts +5 -0
  47. package/dist/theme05-pptx.d.ts.map +1 -0
  48. package/dist/theme05-pptx.js +3152 -0
  49. package/dist/theme05-pptx.js.map +1 -0
  50. package/dist/theme06-pptx.d.ts +124 -0
  51. package/dist/theme06-pptx.d.ts.map +1 -0
  52. package/dist/theme06-pptx.js +8154 -0
  53. package/dist/theme06-pptx.js.map +1 -0
  54. package/dist/theme07-pptx.d.ts +49 -0
  55. package/dist/theme07-pptx.d.ts.map +1 -0
  56. package/dist/theme07-pptx.js +2579 -0
  57. package/dist/theme07-pptx.js.map +1 -0
  58. package/dist/theme08-pptx.d.ts +19 -0
  59. package/dist/theme08-pptx.d.ts.map +1 -0
  60. package/dist/theme08-pptx.js +1411 -0
  61. package/dist/theme08-pptx.js.map +1 -0
  62. package/dist/theme09-pptx.d.ts +18 -0
  63. package/dist/theme09-pptx.d.ts.map +1 -0
  64. package/dist/theme09-pptx.js +1513 -0
  65. package/dist/theme09-pptx.js.map +1 -0
  66. package/dist/theme10-pptx.d.ts +18 -0
  67. package/dist/theme10-pptx.d.ts.map +1 -0
  68. package/dist/theme10-pptx.js +2889 -0
  69. package/dist/theme10-pptx.js.map +1 -0
  70. package/package.json +7 -6
  71. package/templates/editor.html +580 -0
  72. package/templates/editor.js +222 -0
@@ -0,0 +1,2954 @@
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 || []).slice(0, 4);
1787
+ if (points.length === 0)
1788
+ return;
1789
+ const count = points.length;
1790
+ const gap = 0.25;
1791
+ const cardW = (8.4 - gap * (count - 1)) / count;
1792
+ const startX = 0.8;
1793
+ const y = 2.6;
1794
+ const h = 2.6;
1795
+ points.forEach((point, index) => {
1796
+ const x = startX + index * (cardW + gap);
1797
+ slide.addShape('rect', {
1798
+ x, y, w: cardW, h,
1799
+ fill: { color: COLORS.surfaceElevated },
1800
+ line: { color: COLORS.border, width: 1 },
1801
+ rectRadius: 0.1,
1802
+ });
1803
+ slide.addText(String(index + 1), {
1804
+ x: x + 0.16, y: y + 0.16, w: 0.6, h: 0.5,
1805
+ fontSize: 28, color: COLORS.accent, bold: true, align: 'left', valign: 'top',
1806
+ fontFace: FONTS.heading,
1807
+ });
1808
+ slide.addText(point, {
1809
+ x: x + 0.16, y: y + 0.85, w: cardW - 0.32, h: 1.55,
1810
+ fontSize: 16, color: COLORS.primary, align: 'left', valign: 'top',
1811
+ fontFace: FONTS.body,
1812
+ });
1813
+ });
1814
+ }
1815
+ function renderTheme01ContentV3(slide, props) {
1816
+ addKicker(slide, props.kicker);
1817
+ addTitle(slide, props.title ?? '内容');
1818
+ const columns = (props.columns || []).slice(0, 3);
1819
+ const count = columns.length || 1;
1820
+ const gap = 0.25;
1821
+ const cardW = (8.4 - gap * (count - 1)) / count;
1822
+ const startX = 0.8;
1823
+ const y = 2.5;
1824
+ const h = 3.0;
1825
+ columns.forEach((column, index) => {
1826
+ const x = startX + index * (cardW + gap);
1827
+ slide.addShape('rect', {
1828
+ x, y, w: cardW, h,
1829
+ fill: { color: COLORS.surfaceElevated },
1830
+ line: { color: COLORS.border, width: 1 },
1831
+ rectRadius: 0.1,
1832
+ });
1833
+ slide.addText(column.title || '', {
1834
+ x: x + 0.16, y: y + 0.2, w: cardW - 0.32, h: 0.45,
1835
+ fontSize: 18, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
1836
+ fontFace: FONTS.heading,
1837
+ });
1838
+ slide.addText(column.text || '', {
1839
+ x: x + 0.16, y: y + 0.75, w: cardW - 0.32, h: 1.9,
1840
+ fontSize: 14, color: COLORS.secondary, align: 'left', valign: 'top',
1841
+ fontFace: FONTS.body,
1842
+ });
1843
+ });
1844
+ }
1845
+ function renderTheme01DiptychContrast(slide, props) {
1846
+ addKicker(slide, props.kicker);
1847
+ addTitle(slide, props.title ?? '双联对比');
1848
+ const left = props.left || {};
1849
+ const right = props.right || {};
1850
+ const centerCard = props.centerCard || {};
1851
+ const comparisons = (centerCard.comparisons || []).slice(0, 3);
1852
+ // 左右背景区
1853
+ slide.addShape('rect', {
1854
+ x: 0.8, y: 2.3, w: 4.0, h: 3.6,
1855
+ fill: { color: COLORS.surfaceElevated },
1856
+ line: { color: COLORS.border, width: 1 },
1857
+ rectRadius: 0.12,
1858
+ });
1859
+ slide.addShape('rect', {
1860
+ x: 5.2, y: 2.3, w: 4.0, h: 3.6,
1861
+ fill: { color: COLORS.surfaceElevated },
1862
+ line: { color: COLORS.border, width: 1 },
1863
+ rectRadius: 0.12,
1864
+ });
1865
+ if (left.imageUrl)
1866
+ addImageMaybe(slide, left.imageUrl, 0.9, 2.4, 3.8, 2.6);
1867
+ if (right.imageUrl)
1868
+ addImageMaybe(slide, right.imageUrl, 5.3, 2.4, 3.8, 2.6);
1869
+ slide.addText(left.labelEn || '', {
1870
+ x: 0.8, y: 2.45, w: 4.0, h: 0.25,
1871
+ fontSize: 11, color: COLORS.secondary, align: 'center', valign: 'top',
1872
+ fontFace: FONTS.mono,
1873
+ });
1874
+ slide.addText(left.label || '', {
1875
+ x: 0.8, y: 5.1, w: 4.0, h: 0.5,
1876
+ fontSize: 22, color: COLORS.primary, bold: true, align: 'center', valign: 'middle',
1877
+ fontFace: FONTS.heading,
1878
+ });
1879
+ slide.addText(right.labelEn || '', {
1880
+ x: 5.2, y: 2.45, w: 4.0, h: 0.25,
1881
+ fontSize: 11, color: COLORS.secondary, align: 'center', valign: 'top',
1882
+ fontFace: FONTS.mono,
1883
+ });
1884
+ slide.addText(right.label || '', {
1885
+ x: 5.2, y: 5.1, w: 4.0, h: 0.5,
1886
+ fontSize: 22, color: COLORS.primary, bold: true, align: 'center', valign: 'middle',
1887
+ fontFace: FONTS.heading,
1888
+ });
1889
+ // 中央结论卡
1890
+ slide.addShape('rect', {
1891
+ x: 3.3, y: 2.7, w: 3.4, h: 2.8,
1892
+ fill: { color: COLORS.surface },
1893
+ line: { color: COLORS.border, width: 1 },
1894
+ rectRadius: 0.12,
1895
+ });
1896
+ if (centerCard.title) {
1897
+ slide.addText(centerCard.title, {
1898
+ x: 3.5, y: 2.85, w: 3.0, h: 0.4,
1899
+ fontSize: 16, color: COLORS.primary, bold: true, align: 'center', valign: 'top',
1900
+ fontFace: FONTS.heading,
1901
+ });
1902
+ }
1903
+ comparisons.forEach((item, index) => {
1904
+ const y = 3.35 + index * 0.55;
1905
+ slide.addText(item.leftValue || '', {
1906
+ x: 3.5, y, w: 1.0, h: 0.25,
1907
+ fontSize: 13, color: COLORS.primary, bold: true, align: 'right', valign: 'middle',
1908
+ fontFace: FONTS.heading,
1909
+ });
1910
+ slide.addText(item.leftLabel || '', {
1911
+ x: 3.5, y: y + 0.22, w: 1.0, h: 0.18,
1912
+ fontSize: 9, color: COLORS.secondary, align: 'right', valign: 'middle',
1913
+ fontFace: FONTS.body,
1914
+ });
1915
+ slide.addText('VS', {
1916
+ x: 4.6, y, w: 0.8, h: 0.25,
1917
+ fontSize: 10, color: COLORS.accent, bold: true, align: 'center', valign: 'middle',
1918
+ fontFace: FONTS.mono,
1919
+ });
1920
+ slide.addText(item.rightValue || '', {
1921
+ x: 5.5, y, w: 1.0, h: 0.25,
1922
+ fontSize: 13, color: COLORS.primary, bold: true, align: 'left', valign: 'middle',
1923
+ fontFace: FONTS.heading,
1924
+ });
1925
+ slide.addText(item.rightLabel || '', {
1926
+ x: 5.5, y: y + 0.22, w: 1.0, h: 0.18,
1927
+ fontSize: 9, color: COLORS.secondary, align: 'left', valign: 'middle',
1928
+ fontFace: FONTS.body,
1929
+ });
1930
+ });
1931
+ if (centerCard.conclusion) {
1932
+ slide.addText(centerCard.conclusion, {
1933
+ x: 3.5, y: 4.95, w: 3.0, h: 0.45,
1934
+ fontSize: 11, color: COLORS.secondary, align: 'center', valign: 'top',
1935
+ fontFace: FONTS.body,
1936
+ });
1937
+ }
1938
+ if (props.footnote) {
1939
+ slide.addText(props.footnote, {
1940
+ x: 0.8, y: 5.85, w: 8.4, h: 0.25,
1941
+ fontSize: 11, color: COLORS.secondary, align: 'left',
1942
+ fontFace: FONTS.mono,
1943
+ });
1944
+ }
1945
+ }
1946
+ function renderTheme01FilmstripV1(slide, props) {
1947
+ addKicker(slide, props.kicker);
1948
+ addTitle(slide, props.title ?? 'Filmstrip');
1949
+ const images = (props.images || []).slice(0, 5);
1950
+ if (images.length === 0) {
1951
+ slide.addText('(暂无影像)', {
1952
+ x: 2, y: 3.2, w: 6, h: 0.8,
1953
+ fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
1954
+ fontFace: FONTS.body,
1955
+ });
1956
+ return;
1957
+ }
1958
+ const gap = 0.2;
1959
+ const frameW = (8.4 - gap * (images.length - 1)) / images.length;
1960
+ const startX = 0.8;
1961
+ const y = 2.5;
1962
+ const imgH = 2.2;
1963
+ images.forEach((image, index) => {
1964
+ const x = startX + index * (frameW + gap);
1965
+ slide.addShape('rect', {
1966
+ x, y, w: frameW, h: imgH + 0.5,
1967
+ fill: { color: COLORS.surfaceElevated },
1968
+ line: { color: COLORS.border, width: 1 },
1969
+ rectRadius: 0.08,
1970
+ });
1971
+ if (image.url)
1972
+ addImageMaybe(slide, image.url, x + 0.06, y + 0.06, frameW - 0.12, imgH);
1973
+ if (image.caption) {
1974
+ slide.addText(image.caption, {
1975
+ x, y: y + imgH + 0.08, w: frameW, h: 0.35,
1976
+ fontSize: 11, color: COLORS.secondary, align: 'center', valign: 'middle',
1977
+ fontFace: FONTS.body,
1978
+ });
1979
+ }
1980
+ });
1981
+ }
1982
+ function renderTheme01GalleryV1(slide, props) {
1983
+ addKicker(slide, props.kicker);
1984
+ addTitle(slide, props.title ?? 'Gallery');
1985
+ const images = (props.images || []).slice(0, 4);
1986
+ const positions = [
1987
+ { x: 0.8, y: 2.5 },
1988
+ { x: 5.2, y: 2.5 },
1989
+ { x: 0.8, y: 4.0 },
1990
+ { x: 5.2, y: 4.0 },
1991
+ ];
1992
+ images.forEach((image, index) => {
1993
+ const pos = positions[index];
1994
+ if (!pos)
1995
+ return;
1996
+ slide.addShape('rect', {
1997
+ x: pos.x, y: pos.y, w: 4.0, h: 1.3,
1998
+ fill: { color: COLORS.surfaceElevated },
1999
+ line: { color: COLORS.border, width: 1 },
2000
+ rectRadius: 0.08,
2001
+ });
2002
+ if (image.url)
2003
+ addImageMaybe(slide, image.url, pos.x + 0.08, pos.y + 0.08, 3.84, 0.9);
2004
+ if (image.caption) {
2005
+ slide.addText(image.caption, {
2006
+ x: pos.x, y: pos.y + 1.05, w: 4.0, h: 0.25,
2007
+ fontSize: 12, color: COLORS.secondary, align: 'center',
2008
+ fontFace: FONTS.body,
2009
+ });
2010
+ }
2011
+ });
2012
+ }
2013
+ function renderTheme01GanttV1(slide, props) {
2014
+ addKicker(slide, props.kicker);
2015
+ addTitle(slide, props.title ?? '甘特排期');
2016
+ const tasks = (props.tasks || []).slice(0, 8);
2017
+ const phases = (props.phases?.length ? props.phases : ['Q1', 'Q2', 'Q3', 'Q4']).slice(0, 6);
2018
+ const chartColors = ['6E9BC4', '8FAE84', 'D9A441', 'C76B5A', 'A07CAE', 'C98BA0', '5FA8A0', 'A7A85F'];
2019
+ if (tasks.length === 0) {
2020
+ slide.addText('(暂无排期数据)', {
2021
+ x: 2, y: 3.2, w: 6, h: 0.8,
2022
+ fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
2023
+ fontFace: FONTS.body,
2024
+ });
2025
+ return;
2026
+ }
2027
+ const startX = 0.8;
2028
+ const endX = 9.2;
2029
+ const totalW = endX - startX;
2030
+ const headerY = 2.3;
2031
+ const bodyY = 2.85;
2032
+ const rowH = 0.42;
2033
+ const phaseW = totalW / phases.length;
2034
+ // phase header
2035
+ phases.forEach((phase, i) => {
2036
+ const x = startX + i * phaseW;
2037
+ slide.addText(phase, {
2038
+ x, y: headerY, w: phaseW, h: 0.35,
2039
+ fontSize: 12, color: COLORS.secondary, align: 'center', valign: 'middle',
2040
+ fontFace: FONTS.mono,
2041
+ });
2042
+ if (i > 0) {
2043
+ slide.addShape('line', {
2044
+ x1: x, y1: headerY, x2: x, y2: bodyY + tasks.length * rowH,
2045
+ line: { color: COLORS.border, width: 1 },
2046
+ });
2047
+ }
2048
+ });
2049
+ // tasks
2050
+ tasks.forEach((task, index) => {
2051
+ const y = bodyY + index * rowH;
2052
+ const start = Math.max(0, Math.min(100, task.start ?? 0));
2053
+ const end = Math.max(start, Math.min(100, task.end ?? 100));
2054
+ const barX = startX + (start / 100) * totalW;
2055
+ const barW = ((end - start) / 100) * totalW;
2056
+ const color = task.color ? task.color.replace('#', '') : chartColors[index % chartColors.length];
2057
+ slide.addText(task.name || '', {
2058
+ x: 0.8, y, w: 2.0, h: rowH,
2059
+ fontSize: 12, color: COLORS.primary, align: 'left', valign: 'middle',
2060
+ fontFace: FONTS.body,
2061
+ });
2062
+ slide.addShape('rect', {
2063
+ x: barX, y: y + 0.08, w: Math.max(0.05, barW), h: 0.26,
2064
+ fill: { color },
2065
+ rectRadius: 0.05,
2066
+ });
2067
+ });
2068
+ }
2069
+ function renderTheme01MetricBig(slide, props) {
2070
+ const hasInsight = props.showInsight !== false && !!props.insight && (!!props.insight.value || !!props.insight.label || !!props.insight.description);
2071
+ const mainW = hasInsight ? 6.8 : 8.4;
2072
+ const mainX = 0.8;
2073
+ addKicker(slide, props.kicker);
2074
+ addTitle(slide, props.title ?? '核心指标', mainX, 1.3, mainW);
2075
+ if (props.subtitle) {
2076
+ slide.addText(props.subtitle, {
2077
+ x: mainX, y: 2.05, w: mainW, h: 0.35,
2078
+ fontSize: 16, color: COLORS.secondary, align: 'left', valign: 'top',
2079
+ fontFace: FONTS.body,
2080
+ });
2081
+ }
2082
+ const valueText = [props.value, props.unit].filter(Boolean).join(' ') || '0';
2083
+ slide.addText(valueText, {
2084
+ x: mainX, y: 2.7, w: mainW, h: 1.2,
2085
+ fontSize: hasInsight ? 60 : 72, color: COLORS.primary, bold: true, align: 'center', valign: 'middle',
2086
+ fontFace: FONTS.heading,
2087
+ });
2088
+ if (props.context) {
2089
+ slide.addText(props.context, {
2090
+ x: mainX + (hasInsight ? 0.7 : 0.7), y: 4.0, w: hasInsight ? mainW - 1.4 : 7.0, h: 0.6,
2091
+ fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'top',
2092
+ fontFace: FONTS.body,
2093
+ });
2094
+ }
2095
+ const metrics = (props.metrics || []).filter((m) => m != null && (m.value || m.label)).slice(0, 4);
2096
+ if (metrics.length > 0) {
2097
+ const count = metrics.length;
2098
+ const gap = 0.25;
2099
+ const cardW = (mainW - gap * (count - 1)) / count;
2100
+ const startX = mainX;
2101
+ const y = 4.85;
2102
+ const h = 0.65;
2103
+ metrics.forEach((metric, index) => {
2104
+ const x = startX + index * (cardW + gap);
2105
+ const bg = metric.accent ? COLORS.accent : COLORS.surfaceElevated;
2106
+ const textColor = metric.accent ? COLORS.white : COLORS.primary;
2107
+ slide.addShape('rect', {
2108
+ x, y, w: cardW, h,
2109
+ fill: { color: bg },
2110
+ line: { color: COLORS.border, width: 1 },
2111
+ rectRadius: 0.1,
2112
+ });
2113
+ slide.addText(metric.value || '', {
2114
+ x: x + 0.1, y: y + 0.08, w: cardW - 0.2, h: 0.32,
2115
+ fontSize: 18, color: textColor, bold: true, align: 'center', valign: 'top',
2116
+ fontFace: FONTS.heading,
2117
+ });
2118
+ slide.addText(metric.label || '', {
2119
+ x: x + 0.1, y: y + 0.36, w: cardW - 0.2, h: 0.24,
2120
+ fontSize: 11, color: metric.accent ? COLORS.white : COLORS.secondary, align: 'center', valign: 'top',
2121
+ fontFace: FONTS.body,
2122
+ });
2123
+ });
2124
+ }
2125
+ if (props.footnote) {
2126
+ slide.addText(props.footnote, {
2127
+ x: mainX, y: 5.7, w: mainW, h: 0.25,
2128
+ fontSize: 11, color: COLORS.secondary, align: 'left',
2129
+ fontFace: FONTS.mono,
2130
+ });
2131
+ }
2132
+ if (hasInsight) {
2133
+ renderInsightPanel(slide, props.insight, 8.0, 2.3, 2.4, 3.2);
2134
+ }
2135
+ }
2136
+ function renderTheme01MetricTriptych(slide, props) {
2137
+ addKicker(slide, props.kicker);
2138
+ addTitle(slide, props.title ?? '指标总览');
2139
+ if (props.subtitle) {
2140
+ slide.addText(props.subtitle, {
2141
+ x: 0.8, y: 2.15, w: 8.4, h: 0.35,
2142
+ fontSize: 14, color: COLORS.secondary, align: 'left', valign: 'top',
2143
+ fontFace: FONTS.body,
2144
+ });
2145
+ }
2146
+ const panels = (props.panels || []).slice(0, 3);
2147
+ if (panels.length === 0) {
2148
+ slide.addText('(暂无指标)', {
2149
+ x: 2, y: 3.2, w: 6, h: 0.8,
2150
+ fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
2151
+ fontFace: FONTS.body,
2152
+ });
2153
+ return;
2154
+ }
2155
+ const panelW = 8.4 / panels.length;
2156
+ const startX = 0.8;
2157
+ const startY = 2.7;
2158
+ const cardH = 3.0;
2159
+ panels.forEach((panel, index) => {
2160
+ const x = startX + index * panelW;
2161
+ slide.addShape('rect', {
2162
+ x, y: startY, w: panelW - 0.15, h: cardH,
2163
+ fill: { color: COLORS.surfaceElevated },
2164
+ line: { color: COLORS.border, width: 1 },
2165
+ rectRadius: 0.1,
2166
+ });
2167
+ slide.addText(panel.index || String(index + 1).padStart(2, '0'), {
2168
+ x: x + 0.15, y: startY + 0.15, w: 0.6, h: 0.35,
2169
+ fontSize: 14, color: COLORS.accent, bold: true, align: 'left', valign: 'top',
2170
+ fontFace: FONTS.mono,
2171
+ });
2172
+ slide.addText(panel.title || '', {
2173
+ x: x + 0.15, y: startY + 0.55, w: panelW - 0.45, h: 0.35,
2174
+ fontSize: 14, color: COLORS.secondary, align: 'left', valign: 'top',
2175
+ fontFace: FONTS.body,
2176
+ });
2177
+ slide.addText(panel.value || '', {
2178
+ x: x + 0.15, y: startY + 0.95, w: panelW - 0.45, h: 0.6,
2179
+ fontSize: 32, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
2180
+ fontFace: FONTS.heading,
2181
+ });
2182
+ // 简化迷你图:用一组小方块表示趋势
2183
+ const values = (panel.chartData || []).filter((v) => typeof v === 'number');
2184
+ if (values.length > 1) {
2185
+ const max = Math.max(...values, 1);
2186
+ const min = Math.min(...values, 0);
2187
+ const range = max - min || 1;
2188
+ const barW = (panelW - 0.6) / values.length * 0.7;
2189
+ const gap = (panelW - 0.6) / values.length * 0.3;
2190
+ values.forEach((v, i) => {
2191
+ const barH = Math.max(((v - min) / range) * 0.6, 0.08);
2192
+ slide.addShape('rect', {
2193
+ x: x + 0.15 + i * (barW + gap),
2194
+ y: startY + 2.4 - barH,
2195
+ w: barW,
2196
+ h: barH,
2197
+ fill: { color: COLORS.accent },
2198
+ });
2199
+ });
2200
+ }
2201
+ slide.addText(panel.subtitle || '', {
2202
+ x: x + 0.15, y: startY + 2.45, w: panelW - 0.45, h: 0.4,
2203
+ fontSize: 12, color: COLORS.secondary, align: 'left', valign: 'top',
2204
+ fontFace: FONTS.body,
2205
+ });
2206
+ });
2207
+ if (props.footnote) {
2208
+ slide.addText(props.footnote, {
2209
+ x: 0.8, y: 5.85, w: 8.4, h: 0.25,
2210
+ fontSize: 11, color: COLORS.secondary, align: 'left',
2211
+ fontFace: FONTS.mono,
2212
+ });
2213
+ }
2214
+ }
2215
+ function renderTheme01MetricV2(slide, props) {
2216
+ const stats = (props.metrics || []).map((m) => ({
2217
+ label: m.label || '',
2218
+ value: m.value || '0',
2219
+ unit: m.unit || '',
2220
+ }));
2221
+ renderStatsV1(slide, { kicker: props.kicker, title: props.title ?? '关键指标', stats });
2222
+ }
2223
+ function renderTheme01OutlookV1(slide, props) {
2224
+ addKicker(slide, props.kicker);
2225
+ addTitle(slide, props.title ?? '投资展望');
2226
+ const items = (props.items || []).slice(0, 3);
2227
+ if (items.length === 0) {
2228
+ slide.addText('(暂无展望数据)', {
2229
+ x: 2, y: 3.2, w: 6, h: 0.8,
2230
+ fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
2231
+ fontFace: FONTS.body,
2232
+ });
2233
+ return;
2234
+ }
2235
+ const startX = 0.8;
2236
+ const y = 2.35;
2237
+ const gap = 0.2;
2238
+ const cardW = (8.4 - gap * (items.length - 1)) / items.length;
2239
+ const cardH = 2.8;
2240
+ items.forEach((item, index) => {
2241
+ const x = startX + index * (cardW + gap);
2242
+ slide.addShape('rect', {
2243
+ x, y, w: cardW, h: cardH,
2244
+ fill: { color: COLORS.surfaceElevated },
2245
+ line: { color: COLORS.border, width: 1 },
2246
+ rectRadius: 0.1,
2247
+ });
2248
+ slide.addShape('ellipse', {
2249
+ x: x + cardW / 2 - 0.2, y: y + 0.18, w: 0.4, h: 0.4,
2250
+ fill: { color: COLORS.accent },
2251
+ });
2252
+ slide.addText(String(index + 1), {
2253
+ x: x + cardW / 2 - 0.2, y: y + 0.18, w: 0.4, h: 0.4,
2254
+ fontSize: 16, color: COLORS.white, bold: true, align: 'center', valign: 'middle',
2255
+ fontFace: FONTS.heading,
2256
+ });
2257
+ slide.addText(item.title || '', {
2258
+ x: x + 0.14, y: y + 0.72, w: cardW - 0.28, h: 0.45,
2259
+ fontSize: 16, color: COLORS.primary, bold: true, align: 'center', valign: 'top',
2260
+ fontFace: FONTS.heading,
2261
+ });
2262
+ slide.addText(`趋势:${item.trend || ''}`, {
2263
+ x: x + 0.14, y: y + 1.25, w: cardW - 0.28, h: 0.55,
2264
+ fontSize: 12, color: COLORS.secondary, align: 'left', valign: 'top',
2265
+ fontFace: FONTS.body,
2266
+ });
2267
+ slide.addText(`行动:${item.action || ''}`, {
2268
+ x: x + 0.14, y: y + 1.85, w: cardW - 0.28, h: cardH - 2.05,
2269
+ fontSize: 12, color: COLORS.primary, align: 'left', valign: 'top',
2270
+ fontFace: FONTS.body,
2271
+ });
2272
+ });
2273
+ }
2274
+ function renderTheme01QuadrantV1(slide, props) {
2275
+ addKicker(slide, props.kicker);
2276
+ addTitle(slide, props.title ?? '四象限分析');
2277
+ const quadrants = (props.quadrants || []).slice(0, 4);
2278
+ const defaultLabels = ['高价值 / 高可行性', '高价值 / 低可行性', '低价值 / 高可行性', '低价值 / 低可行性'];
2279
+ const colors = ['6E9BC4', '8FAE84', 'D9A441', 'C76B5A'];
2280
+ if (quadrants.length === 0) {
2281
+ slide.addText('(暂无四象限数据)', {
2282
+ x: 2, y: 3.2, w: 6, h: 0.8,
2283
+ fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
2284
+ fontFace: FONTS.body,
2285
+ });
2286
+ return;
2287
+ }
2288
+ const startX = 0.8;
2289
+ const startY = 2.4;
2290
+ const gap = 0.2;
2291
+ const cardW = (8.4 - gap) / 2;
2292
+ const cardH = (2.8 - gap) / 2;
2293
+ quadrants.forEach((q, index) => {
2294
+ const col = index % 2;
2295
+ const row = Math.floor(index / 2);
2296
+ const x = startX + col * (cardW + gap);
2297
+ const y = startY + row * (cardH + gap);
2298
+ const color = colors[index % colors.length];
2299
+ slide.addShape('rect', {
2300
+ x, y, w: cardW, h: cardH,
2301
+ fill: { color: COLORS.surfaceElevated },
2302
+ line: { color: COLORS.border, width: 1 },
2303
+ rectRadius: 0.1,
2304
+ });
2305
+ slide.addShape('rect', {
2306
+ x, y, w: cardW, h: 0.08,
2307
+ fill: { color },
2308
+ rectRadius: 0.1,
2309
+ });
2310
+ slide.addText(q.label || defaultLabels[index] || '', {
2311
+ x: x + 0.14, y: y + 0.2, w: cardW - 0.28, h: 0.4,
2312
+ fontSize: 15, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
2313
+ fontFace: FONTS.heading,
2314
+ });
2315
+ const items = (q.items || []).slice(0, 4);
2316
+ if (items.length > 0) {
2317
+ slide.addText(items.join('\n'), {
2318
+ x: x + 0.14, y: y + 0.65, w: cardW - 0.28, h: cardH - 0.8,
2319
+ fontSize: 12, color: COLORS.secondary, align: 'left', valign: 'top',
2320
+ fontFace: FONTS.body,
2321
+ });
2322
+ }
2323
+ });
2324
+ if (props.xAxis) {
2325
+ slide.addText(props.xAxis, {
2326
+ x: startX, y: 5.35, w: 8.4, h: 0.3,
2327
+ fontSize: 12, color: COLORS.secondary, align: 'center', valign: 'top',
2328
+ fontFace: FONTS.mono,
2329
+ });
2330
+ }
2331
+ if (props.yAxis) {
2332
+ slide.addText(props.yAxis, {
2333
+ x: 0.2, y: startY + 1.25, w: 0.4, h: 0.3,
2334
+ fontSize: 12, color: COLORS.secondary, align: 'center', valign: 'middle',
2335
+ fontFace: FONTS.mono,
2336
+ });
2337
+ }
2338
+ }
2339
+ function renderTheme01RankingV1(slide, props) {
2340
+ addKicker(slide, props.kicker);
2341
+ addTitle(slide, props.title ?? '排名');
2342
+ const items = (props.items || [])
2343
+ .filter((item) => typeof item.value === 'number')
2344
+ .sort((a, b) => (b.value ?? 0) - (a.value ?? 0))
2345
+ .slice(0, 8);
2346
+ if (items.length === 0) {
2347
+ slide.addText('(暂无排名数据)', {
2348
+ x: 2, y: 3.2, w: 6, h: 0.8,
2349
+ fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
2350
+ fontFace: FONTS.body,
2351
+ });
2352
+ return;
2353
+ }
2354
+ const labels = items.map((item) => item.label || '');
2355
+ const values = items.map((item) => item.value ?? 0);
2356
+ const chartColors = items.map((_, i) => CHART_COLORS[i % CHART_COLORS.length]);
2357
+ slide.addChart('bar', [{ name: props.title || '数值', labels, values }], {
2358
+ x: 0.8, y: 2.3, w: 8.4, h: 3.2,
2359
+ chartColors,
2360
+ showValue: true,
2361
+ dataLabelColor: COLORS.primary,
2362
+ dataLabelFontSize: 11,
2363
+ });
2364
+ }
2365
+ function renderTheme01RegionV1(slide, props) {
2366
+ addKicker(slide, props.kicker);
2367
+ addTitle(slide, props.title ?? '地区/市场分布');
2368
+ const regions = (props.regions || []).slice(0, 6);
2369
+ if (regions.length === 0) {
2370
+ slide.addText('(暂无地区数据)', {
2371
+ x: 2, y: 3.2, w: 6, h: 0.8,
2372
+ fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
2373
+ fontFace: FONTS.body,
2374
+ });
2375
+ return;
2376
+ }
2377
+ const startX = 0.8;
2378
+ const startY = 2.35;
2379
+ const gap = 0.2;
2380
+ const cols = regions.length <= 4 ? 3 : 3;
2381
+ const rows = Math.ceil(regions.length / cols);
2382
+ const cardW = (8.4 - gap * (cols - 1)) / cols;
2383
+ const cardH = (2.8 - gap * (rows - 1)) / rows;
2384
+ regions.forEach((region, index) => {
2385
+ const col = index % cols;
2386
+ const row = Math.floor(index / cols);
2387
+ const x = startX + col * (cardW + gap);
2388
+ const y = startY + row * (cardH + gap);
2389
+ slide.addShape('rect', {
2390
+ x, y, w: cardW, h: cardH,
2391
+ fill: { color: COLORS.surfaceElevated },
2392
+ line: { color: COLORS.border, width: 1 },
2393
+ rectRadius: 0.1,
2394
+ });
2395
+ slide.addText(region.name || '', {
2396
+ x: x + 0.12, y: y + 0.12, w: cardW - 0.24, h: 0.4,
2397
+ fontSize: 14, color: COLORS.secondary, align: 'left', valign: 'top',
2398
+ fontFace: FONTS.body,
2399
+ });
2400
+ slide.addText(region.value || '', {
2401
+ x: x + 0.12, y: y + 0.5, w: cardW - 0.24, h: 0.55,
2402
+ fontSize: 24, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
2403
+ fontFace: FONTS.heading,
2404
+ });
2405
+ if (region.change) {
2406
+ slide.addText(region.change, {
2407
+ x: x + 0.12, y: y + 1.05, w: cardW - 0.24, h: 0.3,
2408
+ fontSize: 12, color: COLORS.accent, align: 'left', valign: 'top',
2409
+ fontFace: FONTS.mono,
2410
+ });
2411
+ }
2412
+ if (region.note) {
2413
+ slide.addText(region.note, {
2414
+ x: x + 0.12, y: y + 1.4, w: cardW - 0.24, h: cardH - 1.55,
2415
+ fontSize: 11, color: COLORS.secondary, align: 'left', valign: 'top',
2416
+ fontFace: FONTS.body,
2417
+ });
2418
+ }
2419
+ });
2420
+ }
2421
+ function renderTheme01RiskV1(slide, props) {
2422
+ addKicker(slide, props.kicker);
2423
+ addTitle(slide, props.title ?? '风险研判');
2424
+ const items = (props.items || []).slice(0, 4);
2425
+ if (items.length === 0) {
2426
+ slide.addText('(暂无风险数据)', {
2427
+ x: 2, y: 3.2, w: 6, h: 0.8,
2428
+ fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
2429
+ fontFace: FONTS.body,
2430
+ });
2431
+ return;
2432
+ }
2433
+ const startX = 0.8;
2434
+ const startY = 2.35;
2435
+ const gap = 0.22;
2436
+ const cardW = (8.4 - gap) / 2;
2437
+ const cardH = (2.8 - gap) / 2;
2438
+ items.forEach((item, index) => {
2439
+ const col = index % 2;
2440
+ const row = Math.floor(index / 2);
2441
+ const x = startX + col * (cardW + gap);
2442
+ const y = startY + row * (cardH + gap);
2443
+ slide.addShape('rect', {
2444
+ x, y, w: cardW, h: cardH,
2445
+ fill: { color: COLORS.surfaceElevated },
2446
+ line: { color: COLORS.border, width: 1 },
2447
+ rectRadius: 0.1,
2448
+ });
2449
+ slide.addText(String(index + 1).padStart(2, '0'), {
2450
+ x: x + cardW - 0.55, y: y + 0.12, w: 0.45, h: 0.35,
2451
+ fontSize: 14, color: COLORS.accent, align: 'right', valign: 'top',
2452
+ fontFace: FONTS.mono,
2453
+ });
2454
+ slide.addText(item.risk || '', {
2455
+ x: x + 0.14, y: y + 0.12, w: cardW - 0.7, h: 0.45,
2456
+ fontSize: 15, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
2457
+ fontFace: FONTS.heading,
2458
+ });
2459
+ slide.addText(`影响:${item.impact || ''}`, {
2460
+ x: x + 0.14, y: y + 0.62, w: cardW - 0.28, h: 0.35,
2461
+ fontSize: 12, color: COLORS.secondary, align: 'left', valign: 'top',
2462
+ fontFace: FONTS.body,
2463
+ });
2464
+ slide.addText(`应对:${item.response || ''}`, {
2465
+ x: x + 0.14, y: y + 1.0, w: cardW - 0.28, h: cardH - 1.15,
2466
+ fontSize: 12, color: COLORS.primary, align: 'left', valign: 'top',
2467
+ fontFace: FONTS.body,
2468
+ });
2469
+ });
2470
+ }
2471
+ function renderTheme01RoadmapV1(slide, props) {
2472
+ const phases = (props.phases || []).map((p) => ({
2473
+ title: p.phase || '',
2474
+ description: (p.items || []).join('\n'),
2475
+ }));
2476
+ renderRoadmapV1(slide, { kicker: props.kicker, title: props.title ?? '路线图', phases });
2477
+ }
2478
+ function renderTheme01ScorecardV1(slide, props) {
2479
+ addKicker(slide, props.kicker);
2480
+ addTitle(slide, props.title ?? '评分卡');
2481
+ const items = (props.items || []).slice(0, 5);
2482
+ if (items.length === 0)
2483
+ return;
2484
+ const startY = 2.3;
2485
+ const rowH = 0.65;
2486
+ const barH = 0.12;
2487
+ const maxW = 8.4;
2488
+ items.forEach((item, index) => {
2489
+ const y = startY + index * rowH;
2490
+ const score = typeof item.score === 'number' ? item.score : 0;
2491
+ const max = typeof item.max === 'number' && item.max > 0 ? item.max : 100;
2492
+ const pct = Math.min(100, Math.max(0, (score / max) * 100));
2493
+ slide.addText(item.label || '', {
2494
+ x: 0.8, y, w: 4.5, h: 0.35,
2495
+ fontSize: 16, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
2496
+ fontFace: FONTS.heading,
2497
+ });
2498
+ slide.addText(`${score} / ${max}`, {
2499
+ x: 5.4, y, w: 1.8, h: 0.35,
2500
+ fontSize: 16, color: COLORS.accent, bold: true, align: 'right', valign: 'top',
2501
+ fontFace: FONTS.heading,
2502
+ });
2503
+ slide.addShape('rect', {
2504
+ x: 0.8, y: y + 0.4, w: maxW, h: barH,
2505
+ fill: { color: COLORS.light },
2506
+ });
2507
+ slide.addShape('rect', {
2508
+ x: 0.8, y: y + 0.4, w: maxW * (pct / 100), h: barH,
2509
+ fill: { color: COLORS.accent },
2510
+ });
2511
+ });
2512
+ }
2513
+ function renderTheme01SpotlightGrid(slide, props) {
2514
+ addKicker(slide, props.kicker);
2515
+ addTitle(slide, props.title ?? '主题聚焦');
2516
+ if (props.subtitle) {
2517
+ slide.addText(props.subtitle, {
2518
+ x: 0.8, y: 2.15, w: 8.4, h: 0.35,
2519
+ fontSize: 14, color: COLORS.secondary, align: 'left', valign: 'top',
2520
+ fontFace: FONTS.body,
2521
+ });
2522
+ }
2523
+ const columns = (props.columns || []).slice(0, 4);
2524
+ if (columns.length === 0) {
2525
+ slide.addText('(暂无内容)', {
2526
+ x: 2, y: 3.2, w: 6, h: 0.8,
2527
+ fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
2528
+ fontFace: FONTS.body,
2529
+ });
2530
+ return;
2531
+ }
2532
+ const colW = 8.4 / columns.length;
2533
+ const startX = 0.8;
2534
+ const startY = 2.7;
2535
+ const cardH = 3.0;
2536
+ columns.forEach((col, index) => {
2537
+ const x = startX + index * colW;
2538
+ slide.addShape('rect', {
2539
+ x, y: startY, w: colW - 0.15, h: cardH,
2540
+ fill: { color: COLORS.surfaceElevated },
2541
+ line: { color: COLORS.border, width: 1 },
2542
+ rectRadius: 0.1,
2543
+ });
2544
+ if (col.tag) {
2545
+ slide.addText(col.tag, {
2546
+ x: x + 0.15, y: startY + 0.15, w: colW - 0.45, h: 0.3,
2547
+ fontSize: 12, color: COLORS.accent, bold: true, align: 'left', valign: 'top',
2548
+ fontFace: FONTS.mono,
2549
+ });
2550
+ }
2551
+ if (col.imageUrl) {
2552
+ addImageMaybe(slide, col.imageUrl, x + 0.15, startY + 0.55, colW - 0.45, 1.0);
2553
+ }
2554
+ slide.addText(col.title || '', {
2555
+ x: x + 0.15, y: startY + 1.65, w: colW - 0.45, h: 0.45,
2556
+ fontSize: 18, color: COLORS.primary, bold: true, align: 'left', valign: 'top',
2557
+ fontFace: FONTS.heading,
2558
+ });
2559
+ slide.addText(col.description || '', {
2560
+ x: x + 0.15, y: startY + 2.1, w: colW - 0.45, h: 0.75,
2561
+ fontSize: 12, color: COLORS.secondary, align: 'left', valign: 'top',
2562
+ fontFace: FONTS.body,
2563
+ });
2564
+ });
2565
+ if (props.footnote) {
2566
+ slide.addText(props.footnote, {
2567
+ x: 0.8, y: 5.85, w: 8.4, h: 0.25,
2568
+ fontSize: 11, color: COLORS.secondary, align: 'left',
2569
+ fontFace: FONTS.mono,
2570
+ });
2571
+ }
2572
+ }
2573
+ function renderTheme01TableData(slide, props) {
2574
+ addKicker(slide, props.kicker);
2575
+ addTitle(slide, props.title ?? '数据表格');
2576
+ if (props.subtitle) {
2577
+ slide.addText(props.subtitle, {
2578
+ x: 0.8, y: 2.05, w: 8.4, h: 0.35,
2579
+ fontSize: 16, color: COLORS.secondary, align: 'left', valign: 'top',
2580
+ fontFace: FONTS.body,
2581
+ });
2582
+ }
2583
+ const columns = (props.columns || []).filter((c) => c.key && c.label);
2584
+ const rows = (props.rows || []).filter((r) => r != null).slice(0, 12);
2585
+ const effectiveHighlightRow = props.highlightRow ?? (Array.isArray(props.highlightRows) && props.highlightRows.length ? props.highlightRows[0] : undefined);
2586
+ if (columns.length === 0 || rows.length === 0) {
2587
+ slide.addText('(暂无表格数据)', {
2588
+ x: 2, y: 3.2, w: 6, h: 0.8,
2589
+ fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
2590
+ fontFace: FONTS.body,
2591
+ });
2592
+ return;
2593
+ }
2594
+ const tableW = 8.4;
2595
+ const startX = 0.8;
2596
+ const startY = 2.55;
2597
+ const rowH = 0.42;
2598
+ const colW = tableW / columns.length;
2599
+ columns.forEach((column, index) => {
2600
+ const x = startX + index * colW;
2601
+ slide.addShape('rect', {
2602
+ x, y: startY, w: colW, h: rowH,
2603
+ fill: { color: COLORS.surfaceElevated },
2604
+ line: { color: COLORS.border, width: 1 },
2605
+ });
2606
+ slide.addText(column.label || '', {
2607
+ x, y: startY, w: colW, h: rowH,
2608
+ fontSize: 12, color: COLORS.primary, bold: true, align: column.align || 'center', valign: 'middle',
2609
+ fontFace: FONTS.heading,
2610
+ });
2611
+ });
2612
+ rows.forEach((row, rowIndex) => {
2613
+ const y = startY + rowH * (rowIndex + 1);
2614
+ const isHighlight = effectiveHighlightRow === rowIndex;
2615
+ const bg = isHighlight ? '3A2A20' : (rowIndex % 2 === 0 ? COLORS.surface : COLORS.surfaceElevated);
2616
+ columns.forEach((column, colIndex) => {
2617
+ const x = startX + colIndex * colW;
2618
+ slide.addShape('rect', {
2619
+ x, y, w: colW, h: rowH,
2620
+ fill: { color: bg },
2621
+ line: { color: COLORS.border, width: 1 },
2622
+ });
2623
+ slide.addText(row[column.key || ''] ?? '', {
2624
+ x: x + 0.06, y, w: colW - 0.12, h: rowH,
2625
+ fontSize: 11, color: isHighlight ? COLORS.accent : COLORS.primary,
2626
+ bold: isHighlight,
2627
+ align: column.align || 'left', valign: 'middle',
2628
+ fontFace: FONTS.body,
2629
+ });
2630
+ });
2631
+ });
2632
+ if (props.footnote) {
2633
+ slide.addText(props.footnote, {
2634
+ x: 0.8, y: 5.75, w: 8.4, h: 0.25,
2635
+ fontSize: 11, color: COLORS.secondary, align: 'left',
2636
+ fontFace: FONTS.mono,
2637
+ });
2638
+ }
2639
+ }
2640
+ function renderTheme01TableV1(slide, props) {
2641
+ addKicker(slide, props.kicker);
2642
+ addTitle(slide, props.title ?? 'Table');
2643
+ const headers = props.headers || [];
2644
+ const rows = (props.rows || []).slice(0, 6);
2645
+ if (rows.length === 0) {
2646
+ slide.addText('(暂无表格数据)', {
2647
+ x: 2, y: 3.2, w: 6, h: 0.8,
2648
+ fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
2649
+ fontFace: FONTS.body,
2650
+ });
2651
+ return;
2652
+ }
2653
+ const colCount = Math.max(headers.length, rows[0]?.length || 1);
2654
+ const tableW = 8.4;
2655
+ const startX = 0.8;
2656
+ const startY = 2.5;
2657
+ const rowH = 0.45;
2658
+ const colW = tableW / colCount;
2659
+ // Header
2660
+ headers.forEach((header, index) => {
2661
+ const x = startX + index * colW;
2662
+ slide.addShape('rect', {
2663
+ x, y: startY, w: colW, h: rowH,
2664
+ fill: { color: COLORS.surfaceElevated },
2665
+ line: { color: COLORS.border, width: 1 },
2666
+ });
2667
+ slide.addText(header, {
2668
+ x, y: startY, w: colW, h: rowH,
2669
+ fontSize: 12, color: COLORS.primary, bold: true, align: 'center', valign: 'middle',
2670
+ fontFace: FONTS.heading,
2671
+ });
2672
+ });
2673
+ // Rows
2674
+ rows.forEach((row, rowIndex) => {
2675
+ const y = startY + rowH * (rowIndex + 1);
2676
+ const bg = rowIndex % 2 === 0 ? COLORS.surface : COLORS.surfaceElevated;
2677
+ row.forEach((cell, colIndex) => {
2678
+ const x = startX + colIndex * colW;
2679
+ slide.addShape('rect', {
2680
+ x, y, w: colW, h: rowH,
2681
+ fill: { color: bg },
2682
+ line: { color: COLORS.border, width: 1 },
2683
+ });
2684
+ slide.addText(cell ?? '', {
2685
+ x: x + 0.06, y, w: colW - 0.12, h: rowH,
2686
+ fontSize: 11, color: props.highlightFirstColumn && colIndex === 0 ? COLORS.primary : COLORS.secondary,
2687
+ bold: props.highlightFirstColumn && colIndex === 0,
2688
+ align: colIndex === 0 ? 'left' : 'center', valign: 'middle',
2689
+ fontFace: FONTS.body,
2690
+ });
2691
+ });
2692
+ });
2693
+ }
2694
+ function renderTheme01TagsV1(slide, props) {
2695
+ addKicker(slide, props.kicker);
2696
+ addTitle(slide, props.title ?? 'Tags');
2697
+ const tags = (props.tags || []).slice(0, 18);
2698
+ if (tags.length === 0) {
2699
+ slide.addText('(暂无标签)', {
2700
+ x: 2, y: 3.2, w: 6, h: 0.8,
2701
+ fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
2702
+ fontFace: FONTS.body,
2703
+ });
2704
+ return;
2705
+ }
2706
+ const toneColors = {
2707
+ positive: { text: '8FAE84', bg: '1E2A1E' },
2708
+ negative: { text: 'C76B5A', bg: '2A1C19' },
2709
+ accent: { text: COLORS.accent, bg: '2A1E18' },
2710
+ };
2711
+ let x = 0.8;
2712
+ let y = 2.5;
2713
+ const maxW = 8.4;
2714
+ const gap = 0.15;
2715
+ const tagH = 0.42;
2716
+ tags.forEach((tag) => {
2717
+ const label = tag.label ?? '';
2718
+ const valueText = tag.value !== undefined && tag.value > 0 ? ` ${tag.value}` : '';
2719
+ const text = `${label}${valueText}`;
2720
+ const textW = Math.min(Math.max(text.length * 0.09 + 0.3, 0.7), 2.2);
2721
+ const tone = toneColors[tag.tone ?? ''] ?? { text: COLORS.secondary, bg: COLORS.surfaceElevated };
2722
+ const color = tone.text;
2723
+ const bgColor = tone.bg;
2724
+ if (x + textW > 0.8 + maxW) {
2725
+ x = 0.8;
2726
+ y += tagH + gap;
2727
+ }
2728
+ slide.addShape('roundRect', {
2729
+ x, y, w: textW, h: tagH,
2730
+ fill: { color: bgColor },
2731
+ line: { color: tag.tone ? color : COLORS.border, width: 1 },
2732
+ rectRadius: 0.21,
2733
+ });
2734
+ slide.addText(text, {
2735
+ x, y, w: textW, h: tagH,
2736
+ fontSize: 11, color, bold: true, align: 'center', valign: 'middle',
2737
+ fontFace: FONTS.body,
2738
+ });
2739
+ x += textW + gap;
2740
+ });
2741
+ }
2742
+ function renderTheme01TeamV2(slide, props) {
2743
+ const members = (props.members || []).map((m) => ({ ...m, avatar: m.imageUrl }));
2744
+ renderTeamV2(slide, { kicker: props.kicker, title: props.title ?? '团队', members });
2745
+ }
2746
+ function renderTheme01TimelineV1(slide, props) {
2747
+ const milestones = props.events || [];
2748
+ renderTimelineV1(slide, { kicker: props.kicker, title: props.title ?? '时间轴', milestones });
2749
+ }
2750
+ function renderTheme01TrendV1(slide, props) {
2751
+ addKicker(slide, props.kicker);
2752
+ addTitle(slide, props.title ?? '趋势');
2753
+ const series = (props.series || []).slice(0, 4);
2754
+ const allLabels = Array.from(new Set(series.flatMap((s) => (s.data || []).map((p) => p.label || '')))).filter(Boolean);
2755
+ if (allLabels.length === 0 || series.length === 0) {
2756
+ slide.addText('(暂无趋势数据)', {
2757
+ x: 2, y: 3.2, w: 6, h: 0.8,
2758
+ fontSize: 18, color: COLORS.secondary, align: 'center', valign: 'middle',
2759
+ fontFace: FONTS.body,
2760
+ });
2761
+ return;
2762
+ }
2763
+ const chartData = series.map((s) => ({
2764
+ name: s.name || '系列',
2765
+ labels: allLabels,
2766
+ values: allLabels.map((label) => {
2767
+ const point = (s.data || []).find((p) => p.label === label);
2768
+ return typeof point?.value === 'number' ? point.value : 0;
2769
+ }),
2770
+ }));
2771
+ slide.addChart('line', chartData, {
2772
+ x: 0.8, y: 2.3, w: 8.4, h: 3.2,
2773
+ chartColors: CHART_COLORS,
2774
+ showValue: true,
2775
+ dataLabelColor: COLORS.primary,
2776
+ dataLabelFontSize: 10,
2777
+ });
2778
+ }
2779
+ function renderTimelineV1(slide, props) {
2780
+ addKicker(slide, props.kicker);
2781
+ addTitle(slide, props.title);
2782
+ const milestones = (props.milestones || []).slice(0, 4);
2783
+ const count = milestones.length || 1;
2784
+ const stepWidth = 8.4 / count;
2785
+ milestones.forEach((m, index) => {
2786
+ const cx = 0.8 + stepWidth * index + stepWidth / 2;
2787
+ slide.addShape('ellipse', {
2788
+ x: cx - 0.2, y: 3.0, w: 0.4, h: 0.4,
2789
+ fill: { color: COLORS.accent },
2790
+ });
2791
+ slide.addShape('line', {
2792
+ x1: 0.8 + stepWidth * index, y1: 3.2, x2: 0.8 + stepWidth * (index + 1), y2: 3.2,
2793
+ line: { color: COLORS.border, width: 2 },
2794
+ });
2795
+ slide.addText(m.date || '', {
2796
+ x: cx - stepWidth / 2 + 0.1, y: 2.4, w: stepWidth - 0.2, h: 0.4,
2797
+ fontSize: 12, color: COLORS.accent, align: 'center', valign: 'top',
2798
+ fontFace: FONTS.mono,
2799
+ });
2800
+ slide.addText(m.title || '', {
2801
+ x: cx - stepWidth / 2 + 0.1, y: 3.6, w: stepWidth - 0.2, h: 0.4,
2802
+ fontSize: 14, color: COLORS.primary, bold: true, align: 'center', valign: 'top',
2803
+ fontFace: FONTS.heading,
2804
+ });
2805
+ slide.addText(m.description || '', {
2806
+ x: cx - stepWidth / 2 + 0.1, y: 4.05, w: stepWidth - 0.2, h: 0.8,
2807
+ fontSize: 12, color: COLORS.secondary, align: 'center', valign: 'top',
2808
+ fontFace: FONTS.body,
2809
+ });
2810
+ });
2811
+ }
2812
+ function renderTheme01ComponentsV1(slide, props) {
2813
+ addKicker(slide, props.kicker ?? 'DESIGN SYSTEM');
2814
+ addTitle(slide, props.title ?? '暖炭暗色编辑风');
2815
+ const swatches = [
2816
+ [COLORS.accent, '陶土'],
2817
+ [CHART_COLORS[0], '岩蓝'],
2818
+ [CHART_COLORS[1], '鼠尾草'],
2819
+ [CHART_COLORS[2], '黄铜'],
2820
+ [CHART_COLORS[3], '赤陶红'],
2821
+ [CHART_COLORS[4], '梅紫'],
2822
+ [CHART_COLORS[5], '陶玫瑰'],
2823
+ [CHART_COLORS[6], '青'],
2824
+ [CHART_COLORS[7], '焦橙'],
2825
+ [CHART_COLORS[8], '橄榄'],
2826
+ ];
2827
+ const chipW = 0.92;
2828
+ const chipH = 0.5;
2829
+ const gap = 0.12;
2830
+ const startX = 0.8;
2831
+ const startY = 2.5;
2832
+ swatches.forEach(([color, name], i) => {
2833
+ const col = i % 3;
2834
+ const row = Math.floor(i / 3);
2835
+ const x = startX + col * (chipW + gap);
2836
+ const y = startY + row * (chipH + 0.42);
2837
+ slide.addShape('roundRect', {
2838
+ x, y, w: chipW, h: chipH,
2839
+ fill: { color },
2840
+ line: { color: COLORS.border, width: 1 },
2841
+ rectRadius: 0.1,
2842
+ });
2843
+ slide.addText(name, {
2844
+ x, y: y + chipH + 0.04, w: chipW, h: 0.32,
2845
+ fontSize: 10, color: COLORS.secondary, align: 'center', valign: 'middle',
2846
+ fontFace: FONTS.body,
2847
+ });
2848
+ });
2849
+ // 标签三型 + 样本柱图
2850
+ const labelY = 4.5;
2851
+ slide.addShape('roundRect', { x: 4.4, y: labelY, w: 0.8, h: 0.36, fill: { color: COLORS.accent }, line: { type: 'none' }, rectRadius: 0.18 });
2852
+ 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 });
2853
+ 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 });
2854
+ 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 });
2855
+ const barData = [38, 64, 52, 80, 46, 70];
2856
+ const barW = 0.46;
2857
+ const bGap = 0.18;
2858
+ const baseY = 5.0;
2859
+ const bx0 = 4.4;
2860
+ barData.forEach((h, i) => {
2861
+ const x = bx0 + i * (barW + bGap);
2862
+ const hgt = (h / 100) * 0.9;
2863
+ slide.addShape('rect', {
2864
+ x, y: baseY - hgt, w: barW, h: hgt,
2865
+ fill: { color: i % 2 === 0 ? COLORS.accent : CHART_COLORS[0] },
2866
+ line: { type: 'none' },
2867
+ rectRadius: 0.06,
2868
+ });
2869
+ });
2870
+ }
2871
+ // ────────────────────────────────────────────────────────────────
2872
+ // 注册表(供 export-pptx 通过 registerTheme01Renderers 注入)
2873
+ // ────────────────────────────────────────────────────────────────
2874
+ const RENDERERS = {
2875
+ 'theme01_appendix_v1': renderTheme01AppendixV1,
2876
+ 'theme01_bento_v1': renderTheme01BentoV1,
2877
+ 'theme01_case_study': renderTheme01CaseStudy,
2878
+ 'theme01_chapter_v1': renderCoverV1,
2879
+ 'theme01_chapter_v2': renderCoverV1,
2880
+ 'theme01_chapter_v3': renderTheme01ChapterV3,
2881
+ 'theme01_chart_bar3d': renderChartBar3d,
2882
+ 'theme01_chart_donut': renderTheme01ChartDonut,
2883
+ 'theme01_chart_funnel': renderChartFunnel,
2884
+ 'theme01_chart_gauge': renderChartGauge,
2885
+ 'theme01_chart_graph': renderChartGraph,
2886
+ 'theme01_chart_heatmap': renderChartHeatmap,
2887
+ 'theme01_chart_radar': renderChartRadar,
2888
+ 'theme01_chart_sankey': renderChartSankey,
2889
+ 'theme01_chart_sunburst': renderChartSunburst,
2890
+ 'theme01_chart_treemap': renderChartTreemap,
2891
+ 'theme01_chart_v1': renderChartV1,
2892
+ 'theme01_chart_wordcloud': renderChartWordcloud,
2893
+ 'theme01_closing_v2': renderClosing,
2894
+ 'theme01_comparison_v1': renderComparisonV1,
2895
+ 'theme01_comparison_v2': renderTheme01ComparisonV2,
2896
+ 'theme01_comparison_v3': renderComparisonV3,
2897
+ 'theme01_conclusion_v1': renderTheme01ConclusionV1,
2898
+ 'theme01_content_v1': renderContentV1,
2899
+ 'theme01_content_v2': renderContentV2,
2900
+ 'theme01_content_v3': renderTheme01ContentV3,
2901
+ 'theme01_content_v4': renderContentV1,
2902
+ 'theme01_cover_v1': renderCoverV1,
2903
+ 'theme01_cover_v2': renderCoverV1,
2904
+ 'theme01_cover_v3': renderCoverV1,
2905
+ 'theme01_cover_v4': renderCoverV1,
2906
+ 'theme01_diptych_contrast': renderTheme01DiptychContrast,
2907
+ 'theme01_faq_v1': renderFaqV1,
2908
+ 'theme01_feature_v1': renderFeatureV1,
2909
+ 'theme01_feature_v2': renderFeatureV2,
2910
+ 'theme01_filmstrip_v1': renderTheme01FilmstripV1,
2911
+ 'theme01_gallery_v1': renderTheme01GalleryV1,
2912
+ 'theme01_gantt_v1': renderTheme01GanttV1,
2913
+ 'theme01_image_v1': renderImageV1,
2914
+ 'theme01_metric_big': renderTheme01MetricBig,
2915
+ 'theme01_metric_triptych': renderTheme01MetricTriptych,
2916
+ 'theme01_metric_v1': renderMetricV1,
2917
+ 'theme01_metric_v2': renderTheme01MetricV2,
2918
+ 'theme01_metric_v3': renderMetricV1,
2919
+ 'theme01_outlook_v1': renderTheme01OutlookV1,
2920
+ 'theme01_partners_v1': renderPartnersV1,
2921
+ 'theme01_pest_v1': renderPestV1,
2922
+ 'theme01_pricing_v1': renderPricingV1,
2923
+ 'theme01_process_v1': renderProcessV1,
2924
+ 'theme01_quadrant_v1': renderTheme01QuadrantV1,
2925
+ 'theme01_quote_v1': renderQuoteV1,
2926
+ 'theme01_quote_v2': renderQuoteV2,
2927
+ 'theme01_quote_v3': renderQuoteV3,
2928
+ 'theme01_ranking_v1': renderTheme01RankingV1,
2929
+ 'theme01_region_v1': renderTheme01RegionV1,
2930
+ 'theme01_risk_v1': renderTheme01RiskV1,
2931
+ 'theme01_roadmap_v1': renderTheme01RoadmapV1,
2932
+ 'theme01_scorecard_v1': renderTheme01ScorecardV1,
2933
+ 'theme01_spotlight_grid': renderTheme01SpotlightGrid,
2934
+ 'theme01_stats_v1': renderStatsV1,
2935
+ 'theme01_swot_v1': renderSwotV1,
2936
+ 'theme01_table_data': renderTheme01TableData,
2937
+ 'theme01_table_of_contents_v1': renderTableOfContentsV1,
2938
+ 'theme01_table_of_contents_v2': renderTableOfContentsV1,
2939
+ 'theme01_table_v1': renderTheme01TableV1,
2940
+ 'theme01_tags_v1': renderTheme01TagsV1,
2941
+ 'theme01_team_v1': renderTeamV1,
2942
+ 'theme01_team_v2': renderTheme01TeamV2,
2943
+ 'theme01_testimonial_v1': renderTestimonialV1,
2944
+ 'theme01_timeline_v1': renderTheme01TimelineV1,
2945
+ 'theme01_trend_v1': renderTheme01TrendV1,
2946
+ 'theme01_components_v1': renderTheme01ComponentsV1,
2947
+ };
2948
+ /** 将全部 theme01 渲染器注册到导出管线的按 layoutId 索引注册表(按版式注入双基底 COLORS)。 */
2949
+ export function registerTheme01Renderers(register) {
2950
+ for (const [id, fn] of Object.entries(RENDERERS)) {
2951
+ register(id, (s, p) => { COLORS = P(id); fn(s, p); });
2952
+ }
2953
+ }
2954
+ //# sourceMappingURL=theme01-pptx.js.map