byteplan-cli 1.0.2 → 1.2.2

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 (66) hide show
  1. package/package.json +7 -3
  2. package/skills/byteplan-analysis/SKILL.md +1078 -0
  3. package/skills/byteplan-api/API_REFERENCE.md +249 -0
  4. package/skills/byteplan-api/SKILL.md +96 -0
  5. package/skills/byteplan-api/package.json +16 -0
  6. package/skills/byteplan-api/scripts/api.js +973 -0
  7. package/skills/byteplan-excel/SKILL.md +212 -0
  8. package/skills/byteplan-excel/examples/margin-analysis.json +40 -0
  9. package/skills/byteplan-excel/package.json +12 -0
  10. package/skills/byteplan-excel/pnpm-lock.yaml +68 -0
  11. package/skills/byteplan-excel/scripts/generate_excel.js +156 -0
  12. package/skills/byteplan-html/SKILL.md +490 -0
  13. package/skills/byteplan-html/examples/example-output.html +184 -0
  14. package/skills/byteplan-html/examples/generate-ppt-style-html.js +611 -0
  15. package/skills/byteplan-html/examples/margin-contribution-analysis.json +152 -0
  16. package/skills/byteplan-html/package.json +18 -0
  17. package/skills/byteplan-html/scripts/generate_html.js +517 -0
  18. package/skills/byteplan-ppt/SKILL.md +394 -0
  19. package/skills/byteplan-ppt/examples/margin-contribution-analysis.json +152 -0
  20. package/skills/byteplan-ppt/package.json +16 -0
  21. package/skills/byteplan-ppt/pnpm-lock.yaml +138 -0
  22. package/skills/byteplan-ppt/scripts/check_ppt_overlap.js +318 -0
  23. package/skills/byteplan-ppt/scripts/generate_ppt.js +680 -0
  24. package/skills/byteplan-video/SKILL.md +606 -0
  25. package/skills/byteplan-video/examples/sample-video-data.json +82 -0
  26. package/skills/byteplan-video/remotion-project/package.json +22 -0
  27. package/skills/byteplan-video/remotion-project/pnpm-lock.yaml +1646 -0
  28. package/skills/byteplan-video/remotion-project/remotion.config.ts +6 -0
  29. package/skills/byteplan-video/remotion-project/scene_durations.json +32 -0
  30. package/skills/byteplan-video/remotion-project/scripts/generate_audio.js +279 -0
  31. package/skills/byteplan-video/remotion-project/src/DynamicReport.tsx +172 -0
  32. package/skills/byteplan-video/remotion-project/src/Root.tsx +51 -0
  33. package/skills/byteplan-video/remotion-project/src/SalesReport.tsx +107 -0
  34. package/skills/byteplan-video/remotion-project/src/index.tsx +4 -0
  35. package/skills/byteplan-video/remotion-project/src/scenes/ChartSlide.tsx +201 -0
  36. package/skills/byteplan-video/remotion-project/src/scenes/CoverSlide.tsx +61 -0
  37. package/skills/byteplan-video/remotion-project/src/scenes/EndSlide.tsx +60 -0
  38. package/skills/byteplan-video/remotion-project/src/scenes/InsightSlide.tsx +101 -0
  39. package/skills/byteplan-video/remotion-project/src/scenes/KpiSlide.tsx +84 -0
  40. package/skills/byteplan-video/remotion-project/src/scenes/RecommendationSlide.tsx +100 -0
  41. package/skills/byteplan-video/remotion-project/tsconfig.json +13 -0
  42. package/skills/byteplan-video/remotion-project/video_data.json +76 -0
  43. package/skills/byteplan-video/scripts/generate_video.js +270 -0
  44. package/skills/byteplan-video/templates/package.json +31 -0
  45. package/skills/byteplan-video/templates/pnpm-lock.yaml +2200 -0
  46. package/skills/byteplan-video/templates/remotion.config.ts +9 -0
  47. package/skills/byteplan-video/templates/scripts/generate-audio.ts +55 -0
  48. package/skills/byteplan-video/templates/src/components/BarChartScene.tsx +153 -0
  49. package/skills/byteplan-video/templates/src/components/InsightScene.tsx +135 -0
  50. package/skills/byteplan-video/templates/src/components/LineChartScene.tsx +214 -0
  51. package/skills/byteplan-video/templates/src/components/SceneFactory.tsx +34 -0
  52. package/skills/byteplan-video/templates/src/components/SummaryScene.tsx +155 -0
  53. package/skills/byteplan-video/templates/src/components/TitleScene.tsx +130 -0
  54. package/skills/byteplan-video/templates/src/compositions/AnalysisVideo.tsx +39 -0
  55. package/skills/byteplan-video/templates/src/index.tsx +28 -0
  56. package/skills/byteplan-video/templates/src/register-root.tsx +4 -0
  57. package/skills/byteplan-video/templates/src/storyboard/types.ts +46 -0
  58. package/skills/byteplan-video/templates/tsconfig.json +17 -0
  59. package/skills/byteplan-video/templates/tsconfig.scripts.json +13 -0
  60. package/skills/byteplan-word/SKILL.md +233 -0
  61. package/skills/byteplan-word/package.json +12 -0
  62. package/skills/byteplan-word/pnpm-lock.yaml +120 -0
  63. package/skills/byteplan-word/scripts/generate_word.js +548 -0
  64. package/src/api.js +78 -22
  65. package/src/cli.js +11 -0
  66. package/src/commands/skills.js +279 -0
@@ -0,0 +1,680 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * BytePlan PPT 报告生成脚本
4
+ * 设计风格与 byteplan-html 完全一致
5
+ *
6
+ * 特点:
7
+ * - 紫色渐变封面和结尾页
8
+ * - 深色渐变内容页
9
+ * - 卡片式设计
10
+ * - 微软雅黑字体
11
+ * - 与 HTML 风格统一
12
+ */
13
+
14
+ import PptxGenJS from 'pptxgenjs';
15
+ import fs from 'fs';
16
+ import path from 'path';
17
+
18
+ // ============ 颜色配置(与 HTML 一致)============
19
+ const COLORS = {
20
+ // 主题色
21
+ purple: '667eea',
22
+ darkPurple: '764ba2',
23
+
24
+ // 背景色
25
+ darkBg: '1a1a2e',
26
+ darkBg2: '16213e',
27
+ darkBg3: '0f3460',
28
+
29
+ // 文字色
30
+ textWhite: 'FFFFFF',
31
+ textGray: 'CBD5E1',
32
+ textMuted: '94A3B8',
33
+
34
+ // 数据色
35
+ green: '4ade80',
36
+ blue: '60a5fa',
37
+ pink: 'f472b6',
38
+ orange: 'fbbf24',
39
+ red: 'f87171',
40
+ cyan: '06B6D4',
41
+ };
42
+
43
+ // 字体配置
44
+ const FONT = '微软雅黑';
45
+
46
+ // 图表配色
47
+ const CHART_COLORS = [COLORS.green, COLORS.blue, COLORS.pink, COLORS.purple, COLORS.orange];
48
+
49
+ // ============ 第1页:封面(紫色渐变)============
50
+ function createCoverSlide(pptx, data) {
51
+ const slide = pptx.addSlide();
52
+ slide.background = { color: COLORS.purple };
53
+
54
+ // 主标题
55
+ slide.addText(data.title || '数据分析报告', {
56
+ x: 0, y: 1.8, w: 10, h: 0.8,
57
+ fontSize: 44, bold: true, color: COLORS.textWhite, align: 'center', fontFace: FONT
58
+ });
59
+
60
+ // 副标题
61
+ if (data.subtitle) {
62
+ slide.addText(data.subtitle, {
63
+ x: 0, y: 2.7, w: 10, h: 0.5,
64
+ fontSize: 22, color: COLORS.textGray, align: 'center', fontFace: FONT
65
+ });
66
+ }
67
+
68
+ // 数据周期
69
+ if (data.period) {
70
+ slide.addText(data.period, {
71
+ x: 0, y: 3.3, w: 10, h: 0.4,
72
+ fontSize: 16, color: COLORS.textMuted, align: 'center', fontFace: FONT
73
+ });
74
+ }
75
+
76
+ // 租户信息(圆角卡片)
77
+ slide.addShape('roundRect', {
78
+ x: 3.5, y: 4.0, w: 3, h: 0.5,
79
+ fill: { color: COLORS.textWhite, transparency: 85 },
80
+ line: { width: 0 }
81
+ });
82
+ slide.addText(data.source || 'BytePlan 数据平台', {
83
+ x: 3.5, y: 4.0, w: 3, h: 0.5,
84
+ fontSize: 11, color: COLORS.textWhite, align: 'center', fontFace: FONT
85
+ });
86
+ }
87
+
88
+ // ============ 第2页:目录 ============
89
+ function createTocSlide(pptx, data) {
90
+ const slide = pptx.addSlide();
91
+ slide.background = { color: COLORS.darkBg };
92
+
93
+ slide.addText('目录', {
94
+ x: 0.5, y: 0.3, w: 9, h: 0.7,
95
+ fontSize: 36, bold: true, color: COLORS.textWhite, fontFace: FONT
96
+ });
97
+
98
+ const sections = data.sections || ['核心发现摘要', '关键指标概览', '数据分析', '洞察建议'];
99
+
100
+ sections.forEach((section, i) => {
101
+ const col = i % 2;
102
+ const row = Math.floor(i / 2);
103
+ // 两列布局,中间留出间距
104
+ const x = col === 0 ? 0.5 : 5.3;
105
+ const y = 1.2 + row * 0.85;
106
+
107
+ // 卡片背景(宽度调整为4.2,两列之间有0.6的间距)
108
+ slide.addShape('roundRect', {
109
+ x, y, w: 4.2, h: 0.6,
110
+ fill: { color: COLORS.textWhite, transparency: 90 },
111
+ line: { width: 0 }
112
+ });
113
+
114
+ // 编号和文字合并为一个文本,避免换行问题
115
+ const numStr = (i + 1).toString().padStart(2, '0');
116
+ slide.addText([
117
+ { text: numStr, options: { bold: true, color: COLORS.purple, fontSize: 16 } },
118
+ { text: ' ' + section, options: { color: COLORS.textWhite, fontSize: 13 } }
119
+ ], {
120
+ x: x + 0.2, y: y + 0.12, w: 3.8, h: 0.4,
121
+ fontFace: FONT, align: 'left', valign: 'middle'
122
+ });
123
+ });
124
+ }
125
+
126
+ // ============ 第3页:核心发现摘要(三卡片)============
127
+ function createSummarySlide(pptx, data) {
128
+ const slide = pptx.addSlide();
129
+ slide.background = { color: COLORS.darkBg3 };
130
+
131
+ slide.addText('🏆 贡献最大的三个要素', {
132
+ x: 0, y: 0.4, w: 10, h: 0.6,
133
+ fontSize: 28, bold: true, color: COLORS.textWhite, align: 'center', fontFace: FONT
134
+ });
135
+
136
+ const summary = data.summary || [
137
+ { rank: '🥇 第一名', name: '项目A', type: '类型', value: '¥1,000,000', rate: '贡献率 68%' },
138
+ { rank: '🥈 第二名', name: '项目B', type: '类型', value: '¥500,000', rate: '贡献率 50%' },
139
+ { rank: '🥉 第三名', name: '项目C', type: '类型', value: '¥300,000', rate: '贡献率 40%' }
140
+ ];
141
+
142
+ summary.forEach((item, i) => {
143
+ const x = 0.8 + i * 3.1;
144
+
145
+ // 卡片背景
146
+ slide.addShape('roundRect', {
147
+ x, y: 1.3, w: 2.8, h: 3.2,
148
+ fill: { color: COLORS.textWhite, transparency: 90 },
149
+ line: { color: COLORS.textWhite, transparency: 80, width: 1 }
150
+ });
151
+
152
+ // 排名
153
+ slide.addText(item.rank || '', {
154
+ x, y: 1.5, w: 2.8, h: 0.5,
155
+ fontSize: 16, color: COLORS.orange, align: 'center', fontFace: FONT
156
+ });
157
+
158
+ // 名称
159
+ slide.addText(item.name || '', {
160
+ x, y: 2.0, w: 2.8, h: 0.5,
161
+ fontSize: 26, bold: true, color: COLORS.textWhite, align: 'center', fontFace: FONT
162
+ });
163
+
164
+ // 类型
165
+ slide.addText(item.type || '', {
166
+ x, y: 2.5, w: 2.8, h: 0.4,
167
+ fontSize: 12, color: COLORS.textMuted, align: 'center', fontFace: FONT
168
+ });
169
+
170
+ // 数值(绿色)
171
+ slide.addText(item.value || '', {
172
+ x, y: 3.1, w: 2.8, h: 0.6,
173
+ fontSize: 22, bold: true, color: COLORS.green, align: 'center', fontFace: FONT
174
+ });
175
+
176
+ // 贡献率(蓝色)
177
+ slide.addText(item.rate || '', {
178
+ x, y: 3.7, w: 2.8, h: 0.4,
179
+ fontSize: 14, color: COLORS.blue, align: 'center', fontFace: FONT
180
+ });
181
+ });
182
+ }
183
+
184
+ // ============ 第4页:关键指标概览(四卡片)============
185
+ function createKpiSlide(pptx, data) {
186
+ const slide = pptx.addSlide();
187
+ slide.background = { color: COLORS.darkBg };
188
+
189
+ slide.addText('📊 关键指标概览', {
190
+ x: 0, y: 0.4, w: 10, h: 0.6,
191
+ fontSize: 28, bold: true, color: COLORS.textWhite, align: 'center', fontFace: FONT
192
+ });
193
+
194
+ const kpis = data.kpis || [
195
+ { icon: '💰', label: '指标1', value: '1,000', unit: '元' },
196
+ { icon: '📈', label: '指标2', value: '74%', unit: '' },
197
+ { icon: '🌍', label: '指标3', value: '500', unit: '元' },
198
+ { icon: '⚠️', label: '指标4', value: '2', unit: '个' }
199
+ ];
200
+
201
+ kpis.forEach((kpi, i) => {
202
+ const x = 0.6 + i * 2.35;
203
+
204
+ // 卡片背景
205
+ slide.addShape('roundRect', {
206
+ x, y: 1.3, w: 2.1, h: 3.0,
207
+ fill: { color: COLORS.textWhite, transparency: 90 },
208
+ line: { width: 0 }
209
+ });
210
+
211
+ // 图标
212
+ slide.addText(kpi.icon || '📊', {
213
+ x, y: 1.5, w: 2.1, h: 0.6,
214
+ fontSize: 32, align: 'center'
215
+ });
216
+
217
+ // 标签
218
+ slide.addText(kpi.label || '', {
219
+ x, y: 2.2, w: 2.1, h: 0.4,
220
+ fontSize: 11, color: COLORS.textMuted, align: 'center', fontFace: FONT
221
+ });
222
+
223
+ // 数值(绿色)
224
+ slide.addText(kpi.value || '', {
225
+ x, y: 2.6, w: 2.1, h: 0.6,
226
+ fontSize: 24, bold: true, color: COLORS.green, align: 'center', fontFace: FONT
227
+ });
228
+
229
+ // 单位
230
+ slide.addText(kpi.unit || '', {
231
+ x, y: 3.2, w: 2.1, h: 0.3,
232
+ fontSize: 10, color: COLORS.textMuted, align: 'center', fontFace: FONT
233
+ });
234
+ });
235
+ }
236
+
237
+ // ============ 第5-7页:图表幻灯片(使用形状绘制,兼容性最好)============
238
+ function createChartSlides(pptx, data) {
239
+ const chartSlides = [];
240
+
241
+ // 支持两种格式:新的 charts 数组 或 旧的 barChart/ratioData/rankingData
242
+ let processedCharts = [];
243
+
244
+ if (data.charts && Array.isArray(data.charts)) {
245
+ processedCharts = data.charts.map((chart, index) => ({
246
+ title: chart.title || `图表 ${index + 1}`,
247
+ type: chart.type || 'bar',
248
+ data: chart.data || [],
249
+ canvasId: `chart_${index}`
250
+ }));
251
+ } else {
252
+ if (data.barChart && data.barChart.data) {
253
+ processedCharts.push({
254
+ title: data.barChart.title || '数据对比',
255
+ type: 'bar',
256
+ data: data.barChart.data,
257
+ canvasId: 'barChart'
258
+ });
259
+ }
260
+ if (data.ratioData && data.ratioData.data) {
261
+ processedCharts.push({
262
+ title: data.ratioData.title || '贡献率对比',
263
+ type: 'horizontalBar',
264
+ data: data.ratioData.data,
265
+ canvasId: 'ratioData'
266
+ });
267
+ }
268
+ if (data.rankingData && data.rankingData.data) {
269
+ processedCharts.push({
270
+ title: data.rankingData.title || '排行榜',
271
+ type: 'ranking',
272
+ data: data.rankingData.data,
273
+ canvasId: 'rankingData'
274
+ });
275
+ }
276
+ }
277
+
278
+ // 为每个图表创建幻灯片
279
+ processedCharts.forEach((chart, index) => {
280
+ const slide = pptx.addSlide();
281
+ const bgColors = [COLORS.darkBg2, COLORS.darkBg3, COLORS.darkBg];
282
+ slide.background = { color: bgColors[index % 3] };
283
+
284
+ slide.addText('📊 ' + chart.title, {
285
+ x: 0, y: 0.3, w: 10, h: 0.6,
286
+ fontSize: 26, bold: true, color: COLORS.textWhite, align: 'center', fontFace: FONT
287
+ });
288
+
289
+ // 图表区域背景
290
+ slide.addShape('roundRect', {
291
+ x: 0.8, y: 1.0, w: 8.4, h: 4.0,
292
+ fill: { color: '1E293B' },
293
+ line: { width: 0 }
294
+ });
295
+
296
+ if (chart.data && chart.data.length > 0) {
297
+ const isHorizontal = chart.type === 'horizontalBar' || chart.type === 'ranking';
298
+
299
+ // 动态生成颜色
300
+ let chartColors;
301
+ if (chart.type === 'ranking') {
302
+ chartColors = chart.data.map((_, i) => {
303
+ if (i === 0) return COLORS.green;
304
+ if (i < 3) return COLORS.blue;
305
+ return COLORS.purple;
306
+ });
307
+ } else if (isHorizontal && chart.data.some(d => d.value < 0)) {
308
+ chartColors = chart.data.map(d => d.value < 0 ? COLORS.red : COLORS.green);
309
+ } else {
310
+ chartColors = CHART_COLORS.slice(0, chart.data.length);
311
+ }
312
+
313
+ // 使用形状绘制柱状图(兼容性最好)
314
+ const chartData = chart.data;
315
+ const maxValue = Math.max(...chartData.map(d => Math.abs(d.value || 0)));
316
+
317
+ // 背景区域边界(与上面 addShape 保持一致)
318
+ const bgX = 0.8;
319
+ const bgY = 1.0;
320
+ const bgWidth = 8.4;
321
+ const bgHeight = 4.0;
322
+ const padding = 0.3; // 内边距
323
+
324
+ if (isHorizontal) {
325
+ // 横向条形图 - 根据背景高度动态计算
326
+ const availableHeight = bgHeight - padding * 2;
327
+ const dataCount = Math.min(chartData.length, 10); // 最多10条
328
+ const barGap = Math.max(0.08, availableHeight / dataCount * 0.2); // 间距至少0.08
329
+ const barHeight = Math.max(0.2, (availableHeight - barGap * (dataCount - 1)) / dataCount);
330
+
331
+ // 标签和条形的区域分配
332
+ const labelWidth = 1.3;
333
+ const maxBarWidth = bgWidth - labelWidth - padding * 2 - 0.8; // 留出数值空间
334
+ const startX = bgX + labelWidth + padding;
335
+ const startY = bgY + padding;
336
+
337
+ chartData.slice(0, 10).forEach((item, i) => {
338
+ const y = startY + i * (barHeight + barGap);
339
+ const barWidth = Math.min(maxBarWidth, Math.max(0.3, Math.abs(item.value || 0) / maxValue * maxBarWidth));
340
+ const barColor = chartColors[i] || COLORS.purple;
341
+
342
+ // 标签
343
+ slide.addText(item.name || '', {
344
+ x: bgX + padding, y, w: labelWidth - 0.1, h: barHeight,
345
+ fontSize: Math.min(12, Math.max(9, 12 - dataCount * 0.3)), color: COLORS.textWhite, align: 'right', fontFace: FONT, valign: 'middle'
346
+ });
347
+
348
+ // 条形
349
+ slide.addShape('roundRect', {
350
+ x: startX, y, w: barWidth, h: barHeight,
351
+ fill: { color: barColor },
352
+ line: { width: 0 }
353
+ });
354
+
355
+ // 数值
356
+ const valueText = maxValue <= 100 ? `${item.value}%` : item.value >= 1000 ? `${(item.value / 1000).toFixed(0)}k` : String(item.value);
357
+ slide.addText(valueText, {
358
+ x: startX + barWidth + 0.08, y, w: 0.8, h: barHeight,
359
+ fontSize: Math.min(11, Math.max(8, 11 - dataCount * 0.2)), color: COLORS.green, align: 'left', fontFace: FONT, valign: 'middle'
360
+ });
361
+ });
362
+ } else {
363
+ // 纵向柱状图 - 根据背景宽度和数据量动态计算
364
+ const availableWidth = bgWidth - padding * 2;
365
+ const availableHeight = bgHeight - padding * 2 - 0.5; // 底部留空间给标签
366
+ const dataCount = Math.min(chartData.length, 8); // 最多8条
367
+
368
+ // 动态计算柱子宽度和间距
369
+ const totalBarRatio = 0.65; // 柱子占总宽度的比例
370
+ const totalGapRatio = 0.35; // 间距占总宽度的比例
371
+ const barWidth = Math.max(0.4, (availableWidth * totalBarRatio) / dataCount);
372
+ const barGap = Math.max(0.15, (availableWidth * totalGapRatio) / (dataCount + 1));
373
+
374
+ // 起始位置(居中)
375
+ const totalBarsWidth = dataCount * barWidth + (dataCount - 1) * barGap;
376
+ const startX = bgX + padding + (availableWidth - totalBarsWidth) / 2;
377
+ const chartAreaY = bgY + padding;
378
+
379
+ chartData.slice(0, 8).forEach((item, i) => {
380
+ const x = startX + i * (barWidth + barGap);
381
+ const barHeight = Math.max(0.1, (Math.abs(item.value || 0) / maxValue) * availableHeight);
382
+ const y = chartAreaY + availableHeight - barHeight;
383
+ const barColor = chartColors[i] || COLORS.purple;
384
+
385
+ // 柱状条
386
+ slide.addShape('roundRect', {
387
+ x, y, w: barWidth, h: barHeight,
388
+ fill: { color: barColor },
389
+ line: { width: 0 }
390
+ });
391
+
392
+ // 数值(在柱状条上方)
393
+ const valueText = maxValue <= 100 ? `${item.value}%` : item.value >= 10000 ? `${(item.value / 10000).toFixed(1)}万` : item.value >= 1000 ? `${(item.value / 1000).toFixed(0)}k` : String(item.value);
394
+ slide.addText(valueText, {
395
+ x, y: y - 0.28, w: barWidth, h: 0.25,
396
+ fontSize: Math.min(10, Math.max(8, 10 - dataCount * 0.5)), bold: true, color: COLORS.green, align: 'center', fontFace: FONT
397
+ });
398
+
399
+ // 标签(在柱状条下方)
400
+ slide.addText(item.name || '', {
401
+ x, y: chartAreaY + availableHeight + 0.05, w: barWidth, h: 0.35,
402
+ fontSize: Math.min(11, Math.max(8, 11 - dataCount * 0.4)), color: COLORS.textWhite, align: 'center', fontFace: FONT
403
+ });
404
+ });
405
+ }
406
+ }
407
+
408
+ chartSlides.push(slide);
409
+ });
410
+
411
+ return chartSlides.length;
412
+ }
413
+
414
+ // ============ 第8页:数据明细表 ============
415
+ function createTableSlide(pptx, data) {
416
+ const slide = pptx.addSlide();
417
+ slide.background = { color: COLORS.darkBg2 };
418
+
419
+ const tableData = data.tableData || { title: '数据明细', rows: [] };
420
+
421
+ slide.addText('📋 ' + (tableData.title || '数据明细表'), {
422
+ x: 0, y: 0.3, w: 10, h: 0.5,
423
+ fontSize: 26, bold: true, color: COLORS.textWhite, align: 'center', fontFace: FONT
424
+ });
425
+
426
+ // 表格区域背景
427
+ slide.addShape('roundRect', {
428
+ x: 0.6, y: 0.9, w: 8.8, h: 4.2,
429
+ fill: { color: '1E293B' },
430
+ line: { width: 0 }
431
+ });
432
+
433
+ const rows = tableData.rows || [];
434
+ const columns = tableData.columns || ['要素名称', '类型', '数值', '贡献', '贡献率'];
435
+
436
+ if (rows.length === 0) return;
437
+
438
+ // 构建表格数据
439
+ const tableRows = [
440
+ // 表头
441
+ columns.map(col => ({
442
+ text: typeof col === 'string' ? col : col.label,
443
+ options: { bold: true, color: COLORS.textWhite, fontFace: FONT, fill: { color: COLORS.purple, transparency: 40 } }
444
+ })),
445
+ // 数据行
446
+ ...rows.slice(0, 8).map(row => {
447
+ if (Array.isArray(row)) {
448
+ return row.map((cell, i) => {
449
+ const isNegative = typeof cell === 'string' && cell.startsWith('-');
450
+ return {
451
+ text: String(cell),
452
+ options: {
453
+ color: isNegative ? COLORS.red : (i >= 3 ? COLORS.green : COLORS.textGray),
454
+ fontFace: FONT
455
+ }
456
+ };
457
+ });
458
+ }
459
+ return columns.map(col => {
460
+ const key = typeof col === 'string' ? col : col.key;
461
+ const value = row[key] || '';
462
+ const isNegative = typeof value === 'string' && value.startsWith('-');
463
+ return {
464
+ text: String(value),
465
+ options: {
466
+ color: isNegative ? COLORS.red : COLORS.textGray,
467
+ fontFace: FONT
468
+ }
469
+ };
470
+ });
471
+ })
472
+ ];
473
+
474
+ slide.addTable(tableRows, {
475
+ x: 0.8, y: 1.1, w: 8.4, h: 3.8,
476
+ fontFace: FONT,
477
+ fontSize: 11,
478
+ color: COLORS.textGray,
479
+ align: 'center',
480
+ valign: 'middle',
481
+ border: { type: 'solid', pt: 0.5, color: '334155' },
482
+ fill: { color: '1E293B' },
483
+ colW: [1.8, 1.0, 1.8, 1.8, 1.0]
484
+ });
485
+ }
486
+
487
+ // ============ 第9页:关键洞察(四卡片)============
488
+ function createInsightSlide(pptx, data) {
489
+ const slide = pptx.addSlide();
490
+ slide.background = { color: COLORS.darkBg3 };
491
+
492
+ slide.addText('💡 关键洞察', {
493
+ x: 0, y: 0.3, w: 10, h: 0.6,
494
+ fontSize: 28, bold: true, color: COLORS.textWhite, align: 'center', fontFace: FONT
495
+ });
496
+
497
+ const insights = data.insights || [
498
+ { title: '发现一', content: '这是关键发现内容。', warning: false },
499
+ { title: '发现二', content: '这是另一个发现。', warning: false },
500
+ { title: '发现三', content: '这是第三个发现。', warning: false },
501
+ { title: '⚠️ 风险提示', content: '这是风险提示内容。', warning: true }
502
+ ];
503
+
504
+ insights.forEach((item, i) => {
505
+ const col = i % 2;
506
+ const row = Math.floor(i / 2);
507
+ const x = 0.6 + col * 4.6;
508
+ const y = 1.1 + row * 1.9;
509
+ // 恢复最初的颜色搭配:普通紫色,警告橙色
510
+ const borderColor = item.warning ? COLORS.orange : COLORS.purple;
511
+ const titleColor = item.warning ? COLORS.orange : COLORS.purple;
512
+
513
+ // 卡片背景(直角)- 参考行动建议的风格,使用紫色/橙色透明背景
514
+ slide.addShape('rect', {
515
+ x, y, w: 4.2, h: 1.6,
516
+ fill: { color: borderColor, transparency: 80 },
517
+ line: { width: 0 }
518
+ });
519
+
520
+ // 左边框(更亮的同色边框)
521
+ slide.addShape('rect', {
522
+ x, y, w: 0.1, h: 1.6,
523
+ fill: { color: borderColor },
524
+ line: { width: 0 }
525
+ });
526
+
527
+ // 标题
528
+ slide.addText(item.title || '', {
529
+ x: x + 0.2, y: y + 0.15, w: 3.8, h: 0.4,
530
+ fontSize: 16, bold: true, color: titleColor, fontFace: FONT
531
+ });
532
+
533
+ // 内容
534
+ slide.addText(item.content || '', {
535
+ x: x + 0.2, y: y + 0.6, w: 3.8, h: 0.8,
536
+ fontSize: 12, color: COLORS.textGray, fontFace: FONT, valign: 'top'
537
+ });
538
+ });
539
+ }
540
+
541
+ // ============ 第10页:行动建议(四卡片)============
542
+ function createRecommendationSlide(pptx, data) {
543
+ const slide = pptx.addSlide();
544
+ slide.background = { color: COLORS.darkBg };
545
+
546
+ slide.addText('📝 行动建议', {
547
+ x: 0, y: 0.3, w: 10, h: 0.6,
548
+ fontSize: 28, bold: true, color: COLORS.textWhite, align: 'center', fontFace: FONT
549
+ });
550
+
551
+ const recommendations = data.recommendations || [
552
+ { title: '建议一', content: '这是第一条建议。' },
553
+ { title: '建议二', content: '这是第二条建议。' },
554
+ { title: '建议三', content: '这是第三条建议。' },
555
+ { title: '建议四', content: '这是第四条建议。' }
556
+ ];
557
+
558
+ recommendations.forEach((item, i) => {
559
+ const col = i % 2;
560
+ const row = Math.floor(i / 2);
561
+ const x = 0.6 + col * 4.6;
562
+ const y = 1.1 + row * 1.9;
563
+
564
+ // 卡片背景(直角,绿色调)
565
+ slide.addShape('rect', {
566
+ x, y, w: 4.2, h: 1.6,
567
+ fill: { color: COLORS.green, transparency: 80 },
568
+ line: { width: 0 }
569
+ });
570
+
571
+ // 左边框(绿色)
572
+ slide.addShape('rect', {
573
+ x, y, w: 0.08, h: 1.6,
574
+ fill: { color: COLORS.green },
575
+ line: { width: 0 }
576
+ });
577
+
578
+ // 标题
579
+ slide.addText(item.title || '', {
580
+ x: x + 0.2, y: y + 0.15, w: 3.8, h: 0.4,
581
+ fontSize: 16, bold: true, color: COLORS.green, fontFace: FONT
582
+ });
583
+
584
+ // 内容
585
+ slide.addText(item.content || '', {
586
+ x: x + 0.2, y: y + 0.6, w: 3.8, h: 0.8,
587
+ fontSize: 12, color: COLORS.textGray, fontFace: FONT, valign: 'top'
588
+ });
589
+ });
590
+ }
591
+
592
+ // ============ 第11页:结尾页(紫色渐变)============
593
+ function createEndingSlide(pptx, data) {
594
+ const slide = pptx.addSlide();
595
+ slide.background = { color: COLORS.purple };
596
+
597
+ slide.addText('谢谢观看', {
598
+ x: 0, y: 1.6, w: 10, h: 0.9,
599
+ fontSize: 56, bold: true, color: COLORS.textWhite, align: 'center', fontFace: FONT
600
+ });
601
+
602
+ slide.addText(data.title || '数据分析报告', {
603
+ x: 0, y: 2.5, w: 10, h: 0.5,
604
+ fontSize: 20, color: COLORS.textGray, align: 'center', fontFace: FONT
605
+ });
606
+
607
+ slide.addText(data.source || 'BytePlan 数据平台', {
608
+ x: 0, y: 3.0, w: 10, h: 0.4,
609
+ fontSize: 16, color: COLORS.textMuted, align: 'center', fontFace: FONT
610
+ });
611
+
612
+ // 来源信息卡片
613
+ slide.addShape('roundRect', {
614
+ x: 3, y: 3.8, w: 4, h: 0.8,
615
+ fill: { color: COLORS.textWhite, transparency: 85 },
616
+ line: { width: 0 }
617
+ });
618
+
619
+ slide.addText(`数据来源:${data.source || 'BytePlan 数据平台'}\n分析时间:${data.period || new Date().toLocaleDateString('zh-CN')}`, {
620
+ x: 3, y: 3.9, w: 4, h: 0.6,
621
+ fontSize: 11, color: COLORS.textWhite, align: 'center', fontFace: FONT
622
+ });
623
+ }
624
+
625
+ // ============ 主函数 ============
626
+ function generatePpt(outputFile, dataFile = null) {
627
+ // 加载数据
628
+ let data = {};
629
+
630
+ if (dataFile) {
631
+ const filePath = path.resolve(dataFile);
632
+ if (fs.existsSync(filePath)) {
633
+ data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
634
+ }
635
+ } else {
636
+ const defaultPath = path.resolve('ppt_data.json');
637
+ if (fs.existsSync(defaultPath)) {
638
+ data = JSON.parse(fs.readFileSync(defaultPath, 'utf-8'));
639
+ }
640
+ }
641
+
642
+ // 创建演示文稿 (16:9)
643
+ const pptx = new PptxGenJS();
644
+ pptx.defineLayout({ name: 'WIDESCREEN', width: 10, height: 5.625 });
645
+ pptx.layout = 'WIDESCREEN';
646
+ pptx.author = 'BytePlan';
647
+ pptx.company = 'BytePlan 数据平台';
648
+
649
+ // 生成幻灯片
650
+ createCoverSlide(pptx, data); // 1. 封面
651
+ createTocSlide(pptx, data); // 2. 目录
652
+ createSummarySlide(pptx, data); // 3. 核心发现
653
+ createKpiSlide(pptx, data); // 4. KPI
654
+ const chartCount = createChartSlides(pptx, data); // 5-N. 图表(动态数量)
655
+ createTableSlide(pptx, data); // N+1. 数据表
656
+ createInsightSlide(pptx, data); // N+2. 关键洞察
657
+ createRecommendationSlide(pptx, data); // N+3. 行动建议
658
+ createEndingSlide(pptx, data); // N+4. 结尾页
659
+
660
+ // 保存
661
+ pptx.writeFile({ fileName: outputFile });
662
+
663
+ const totalSlides = 11 + (chartCount > 3 ? chartCount - 3 : 0);
664
+ return `Successfully generated PPT with ${totalSlides} slides -> ${outputFile}`;
665
+ }
666
+
667
+ // CLI 入口
668
+ const args = process.argv.slice(2);
669
+ let outputFile = 'analysis-report.pptx';
670
+ let dataFile = null;
671
+
672
+ for (let i = 0; i < args.length; i++) {
673
+ if (args[i] === '-o' || args[i] === '--output-file') {
674
+ outputFile = args[++i];
675
+ } else if (args[i] === '-d' || args[i] === '--data-file') {
676
+ dataFile = args[++i];
677
+ }
678
+ }
679
+
680
+ console.log(generatePpt(outputFile, dataFile));