byteplan-cli 1.0.2 → 1.2.0

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 (65) 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/cli.js +4 -0
  65. package/src/commands/skills.js +279 -0
@@ -0,0 +1,611 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * BytePlan PPT风格 网页报告生成脚本 - 参考案例
4
+ *
5
+ * 本案例展示如何从 BytePlan 分析数据生成 PPT 风格的 网页报告
6
+ *
7
+ * 使用方法:
8
+ * node generate-ppt-style-html.js -o report.html -d data.json
9
+ *
10
+ * 特点:
11
+ * - 11页幻灯片结构
12
+ * - 紫色渐变封面和结尾页
13
+ * - 深色渐变内容页
14
+ * - 卡片式设计
15
+ * - 键盘/触摸导航
16
+ * - Chart.js 交互式图表
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+
22
+ // ============ 颜色配置(与 PPT 一致)============
23
+ const COLORS = {
24
+ purple: '#667eea',
25
+ darkPurple: '#764ba2',
26
+ darkBg: '#1a1a2e',
27
+ darkBg2: '#16213e',
28
+ darkBg3: '#0f3460',
29
+ textWhite: '#FFFFFF',
30
+ textGray: '#CBD5E1',
31
+ textMuted: '#94A3B8',
32
+ green: '#4ade80',
33
+ blue: '#60a5fa',
34
+ pink: '#f472b6',
35
+ orange: '#fbbf24',
36
+ red: '#f87171',
37
+ cyan: '#06B6D4',
38
+ };
39
+
40
+ // ============ PPT风格 HTML 生成函数 ============
41
+
42
+ function generatePPTStyleHTML(data) {
43
+ const {
44
+ title,
45
+ subtitle,
46
+ period,
47
+ sections,
48
+ summary,
49
+ kpis,
50
+ barChart,
51
+ ratioData,
52
+ rankingData,
53
+ tableData,
54
+ insights,
55
+ recommendations,
56
+ source
57
+ } = data;
58
+
59
+ return `<!DOCTYPE html>
60
+ <html lang="zh-CN">
61
+ <head>
62
+ <meta charset="UTF-8">
63
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
64
+ <title>${title}</title>
65
+ <script src="https://cdn.staticfile.org/Chart.js/4.4.1/chart.umd.min.js"></script>
66
+ <style>
67
+ /* ========== 基础样式 ========== */
68
+ * { margin: 0; padding: 0; box-sizing: border-box; }
69
+
70
+ body {
71
+ font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
72
+ background: ${COLORS.darkBg};
73
+ color: ${COLORS.textWhite};
74
+ overflow: hidden;
75
+ }
76
+
77
+ /* ========== 幻灯片容器 ========== */
78
+ .slides-container {
79
+ width: 100vw;
80
+ height: 100vh;
81
+ position: relative;
82
+ }
83
+
84
+ .slide {
85
+ width: 100%;
86
+ height: 100%;
87
+ position: absolute;
88
+ top: 0;
89
+ left: 0;
90
+ opacity: 0;
91
+ visibility: hidden;
92
+ transition: opacity 0.5s ease, visibility 0.5s ease;
93
+ display: flex;
94
+ flex-direction: column;
95
+ justify-content: center;
96
+ align-items: center;
97
+ padding: 30px 50px;
98
+ overflow: hidden;
99
+ }
100
+
101
+ .slide.active {
102
+ opacity: 1;
103
+ visibility: visible;
104
+ }
105
+
106
+ /* ========== 背景样式 ========== */
107
+ .bg-purple {
108
+ background: linear-gradient(135deg, ${COLORS.purple} 0%, ${COLORS.darkPurple} 100%);
109
+ }
110
+ .bg-dark {
111
+ background: linear-gradient(135deg, ${COLORS.darkBg} 0%, ${COLORS.darkBg2} 100%);
112
+ }
113
+ .bg-dark-alt {
114
+ background: linear-gradient(135deg, ${COLORS.darkBg3} 0%, ${COLORS.darkBg2} 100%);
115
+ }
116
+ .bg-dark-reverse {
117
+ background: linear-gradient(135deg, ${COLORS.darkBg2} 0%, ${COLORS.darkBg} 100%);
118
+ }
119
+
120
+ /* ========== 封面样式 ========== */
121
+ .cover-content {
122
+ text-align: center;
123
+ max-width: 90%;
124
+ }
125
+ .cover-content h1 {
126
+ font-size: 3.5rem;
127
+ font-weight: 700;
128
+ margin-bottom: 20px;
129
+ letter-spacing: 3px;
130
+ }
131
+ .cover-content .subtitle {
132
+ font-size: 1.5rem;
133
+ opacity: 0.9;
134
+ margin-bottom: 25px;
135
+ }
136
+ .cover-content .meta {
137
+ font-size: 1.1rem;
138
+ opacity: 0.7;
139
+ }
140
+ .cover-content .tenant {
141
+ margin-top: 40px;
142
+ padding: 15px 35px;
143
+ background: rgba(255,255,255,0.15);
144
+ border-radius: 10px;
145
+ font-size: 1.2rem;
146
+ }
147
+
148
+ /* ========== 目录样式 ========== */
149
+ .toc-content {
150
+ width: 90%;
151
+ max-width: 800px;
152
+ }
153
+ .toc-content h2 {
154
+ font-size: 2.5rem;
155
+ margin-bottom: 35px;
156
+ text-align: center;
157
+ }
158
+ .toc-list {
159
+ display: grid;
160
+ grid-template-columns: 1fr 1fr;
161
+ gap: 20px;
162
+ }
163
+ .toc-item {
164
+ display: flex;
165
+ align-items: center;
166
+ padding: 18px 20px;
167
+ background: rgba(255,255,255,0.1);
168
+ border-radius: 12px;
169
+ transition: transform 0.3s;
170
+ }
171
+ .toc-item:hover { transform: scale(1.03); }
172
+ .toc-item .num {
173
+ font-size: 1.8rem;
174
+ font-weight: bold;
175
+ color: ${COLORS.purple};
176
+ margin-right: 15px;
177
+ }
178
+ .toc-item .text { font-size: 1.2rem; }
179
+
180
+ /* ========== 摘要卡片样式 ========== */
181
+ .summary-content { width: 90%; max-width: 900px; }
182
+ .summary-content h2 { font-size: 2.5rem; margin-bottom: 30px; text-align: center; }
183
+ .summary-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 25px; }
184
+ .summary-card {
185
+ background: rgba(255,255,255,0.1);
186
+ border-radius: 18px;
187
+ padding: 25px;
188
+ text-align: center;
189
+ border: 2px solid rgba(255,255,255,0.2);
190
+ }
191
+ .summary-card .rank { font-size: 1.5rem; color: ${COLORS.orange}; margin-bottom: 12px; }
192
+ .summary-card .name { font-size: 1.8rem; font-weight: bold; margin-bottom: 10px; }
193
+ .summary-card .type { font-size: 1rem; opacity: 0.7; margin-bottom: 15px; }
194
+ .summary-card .value { font-size: 2rem; color: ${COLORS.green}; font-weight: bold; }
195
+ .summary-card .rate { font-size: 1.2rem; color: ${COLORS.blue}; margin-top: 10px; }
196
+
197
+ /* ========== KPI卡片样式 ========== */
198
+ .kpi-content { width: 90%; max-width: 900px; }
199
+ .kpi-content h2 { font-size: 2.5rem; margin-bottom: 30px; text-align: center; }
200
+ .kpi-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; }
201
+ .kpi-card {
202
+ background: rgba(255,255,255,0.1);
203
+ border-radius: 15px;
204
+ padding: 22px;
205
+ text-align: center;
206
+ }
207
+ .kpi-card .icon { font-size: 2.2rem; margin-bottom: 12px; }
208
+ .kpi-card .label { font-size: 0.95rem; opacity: 0.7; margin-bottom: 8px; }
209
+ .kpi-card .value { font-size: 1.8rem; font-weight: bold; color: ${COLORS.green}; }
210
+ .kpi-card .unit { font-size: 0.85rem; opacity: 0.6; }
211
+
212
+ /* ========== 图表区域样式 ========== */
213
+ .chart-content { width: 90%; max-width: 850px; }
214
+ .chart-content h2 { font-size: 2.2rem; margin-bottom: 25px; text-align: center; }
215
+ .chart-box {
216
+ background: rgba(0,0,0,0.3);
217
+ border-radius: 18px;
218
+ padding: 25px;
219
+ }
220
+
221
+ /* ========== 数据表格样式 ========== */
222
+ .table-content { width: 90%; max-width: 900px; }
223
+ .table-content h2 { font-size: 2rem; margin-bottom: 20px; text-align: center; }
224
+ .table-wrapper {
225
+ background: rgba(0,0,0,0.3);
226
+ border-radius: 15px;
227
+ padding: 15px 20px;
228
+ overflow: hidden;
229
+ }
230
+ table { width: 100%; border-collapse: collapse; font-size: 1rem; }
231
+ th, td { padding: 10px 12px; text-align: center; }
232
+ th { background: rgba(102, 126, 234, 0.4); font-weight: 600; }
233
+ tr:nth-child(even) { background: rgba(255,255,255,0.05); }
234
+ .positive { color: ${COLORS.green}; font-weight: bold; }
235
+ .negative { color: ${COLORS.red}; font-weight: bold; }
236
+
237
+ /* ========== 洞察/建议卡片样式 ========== */
238
+ .insight-content, .rec-content { width: 90%; max-width: 850px; }
239
+ .insight-content h2, .rec-content h2 { font-size: 2.5rem; margin-bottom: 30px; text-align: center; }
240
+ .insight-grid, .rec-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 22px; }
241
+
242
+ .insight-card, .rec-card { border-radius: 18px; padding: 25px; }
243
+ .insight-card {
244
+ background: rgba(255,255,255,0.1);
245
+ border-left: 5px solid ${COLORS.purple};
246
+ }
247
+ .insight-card.warning { border-left-color: ${COLORS.orange}; }
248
+ .insight-card h3 { font-size: 1.4rem; color: ${COLORS.purple}; margin-bottom: 12px; }
249
+ .insight-card.warning h3 { color: ${COLORS.orange}; }
250
+ .insight-card p { font-size: 1.1rem; line-height: 1.6; opacity: 0.9; }
251
+
252
+ .rec-card {
253
+ background: rgba(74,222,128,0.2);
254
+ border-left: 5px solid ${COLORS.green};
255
+ }
256
+ .rec-card h3 { font-size: 1.4rem; color: ${COLORS.green}; margin-bottom: 12px; }
257
+ .rec-card p { font-size: 1.1rem; line-height: 1.6; opacity: 0.9; }
258
+
259
+ /* ========== 结尾页样式 ========== */
260
+ .end-content { text-align: center; max-width: 90%; }
261
+ .end-content h1 { font-size: 4rem; font-weight: 700; margin-bottom: 25px; }
262
+ .end-content p { font-size: 1.4rem; opacity: 0.8; margin-bottom: 12px; }
263
+ .end-content .source {
264
+ margin-top: 40px;
265
+ padding: 20px 40px;
266
+ background: rgba(255,255,255,0.15);
267
+ border-radius: 10px;
268
+ font-size: 1.1rem;
269
+ }
270
+
271
+ /* ========== 导航元素样式 ========== */
272
+ .slide-counter {
273
+ position: fixed;
274
+ bottom: 25px;
275
+ right: 25px;
276
+ background: rgba(255,255,255,0.15);
277
+ padding: 10px 18px;
278
+ border-radius: 10px;
279
+ font-size: 1rem;
280
+ opacity: 0.7;
281
+ }
282
+ .progress-bar {
283
+ position: fixed;
284
+ top: 0;
285
+ left: 0;
286
+ height: 4px;
287
+ background: ${COLORS.purple};
288
+ transition: width 0.5s ease;
289
+ z-index: 100;
290
+ }
291
+ </style>
292
+ </head>
293
+ <body>
294
+ <div class="progress-bar" id="progressBar"></div>
295
+
296
+ <div class="slides-container">
297
+ <!-- ==================== 第1页:封面 ==================== -->
298
+ <div class="slide bg-purple active" data-slide="1">
299
+ <div class="cover-content">
300
+ <h1>${title}</h1>
301
+ <p class="subtitle">${subtitle || ''}</p>
302
+ <p class="meta">${period}</p>
303
+ <div class="tenant">${source || 'BytePlan 数据平台'}</div>
304
+ </div>
305
+ </div>
306
+
307
+ <!-- ==================== 第2页:目录 ==================== -->
308
+ <div class="slide bg-dark" data-slide="2">
309
+ <div class="toc-content">
310
+ <h2>目录</h2>
311
+ <div class="toc-list">
312
+ ${(sections || ['核心发现摘要', '关键指标概览', '数据分析', '洞察建议']).map((item, i) =>
313
+ `<div class="toc-item"><span class="num">${(i+1).toString().padStart(2,'0')}</span><span class="text">${item}</span></div>`
314
+ ).join('')}
315
+ </div>
316
+ </div>
317
+ </div>
318
+
319
+ <!-- ==================== 第3页:核心发现摘要 ==================== -->
320
+ <div class="slide bg-dark-alt" data-slide="3">
321
+ <div class="summary-content">
322
+ <h2>🏆 贡献最大的三个要素</h2>
323
+ <div class="summary-grid">
324
+ ${(summary || []).map(item => `
325
+ <div class="summary-card">
326
+ <div class="rank">${item.rank}</div>
327
+ <div class="name">${item.name}</div>
328
+ <div class="type">${item.type}</div>
329
+ <div class="value">${item.value}</div>
330
+ <div class="rate">${item.rate}</div>
331
+ </div>
332
+ `).join('')}
333
+ </div>
334
+ </div>
335
+ </div>
336
+
337
+ <!-- ==================== 第4页:关键指标概览 ==================== -->
338
+ <div class="slide bg-dark" data-slide="4">
339
+ <div class="kpi-content">
340
+ <h2>📊 关键指标概览</h2>
341
+ <div class="kpi-grid">
342
+ ${(kpis || []).map(kpi => `
343
+ <div class="kpi-card">
344
+ <div class="icon">${kpi.icon}</div>
345
+ <div class="label">${kpi.label}</div>
346
+ <div class="value">${kpi.value}</div>
347
+ <div class="unit">${kpi.unit}</div>
348
+ </div>
349
+ `).join('')}
350
+ </div>
351
+ </div>
352
+ </div>
353
+
354
+ <!-- ==================== 第5页:柱状图 ==================== -->
355
+ <div class="slide bg-dark-reverse" data-slide="5">
356
+ <div class="chart-content">
357
+ <h2>📊 ${barChart?.title || '数据对比分析'}</h2>
358
+ <div class="chart-box">
359
+ <canvas id="barChart"></canvas>
360
+ </div>
361
+ </div>
362
+ </div>
363
+
364
+ <!-- ==================== 第6页:贡献率对比 ==================== -->
365
+ <div class="slide bg-dark-alt" data-slide="6">
366
+ <div class="chart-content">
367
+ <h2>📈 ${ratioData?.title || '贡献率对比'}</h2>
368
+ <div class="chart-box">
369
+ <canvas id="ratioChart"></canvas>
370
+ </div>
371
+ </div>
372
+ </div>
373
+
374
+ <!-- ==================== 第7页:排行榜 ==================== -->
375
+ <div class="slide bg-dark" data-slide="7">
376
+ <div class="chart-content">
377
+ <h2>🏆 ${rankingData?.title || '贡献排行榜'}</h2>
378
+ <div class="chart-box">
379
+ <canvas id="rankingChart"></canvas>
380
+ </div>
381
+ </div>
382
+ </div>
383
+
384
+ <!-- ==================== 第8页:数据明细表 ==================== -->
385
+ <div class="slide bg-dark-reverse" data-slide="8">
386
+ <div class="table-content">
387
+ <h2>📋 ${tableData?.title || '数据明细表'}</h2>
388
+ <div class="table-wrapper">
389
+ <table>
390
+ <thead>
391
+ <tr>
392
+ <th>要素名称</th>
393
+ <th>类型</th>
394
+ <th>数值</th>
395
+ <th>贡献</th>
396
+ <th>贡献率</th>
397
+ </tr>
398
+ </thead>
399
+ <tbody>
400
+ ${(tableData?.rows || []).map(row => `
401
+ <tr>
402
+ <td>${row[0]}</td>
403
+ <td>${row[1]}</td>
404
+ <td class="positive">${row[2]}</td>
405
+ <td class="${row[3].startsWith('-') ? 'negative' : 'positive'}">${row[3]}</td>
406
+ <td class="${row[4].startsWith('-') ? 'negative' : 'positive'}">${row[4]}</td>
407
+ </tr>
408
+ `).join('')}
409
+ </tbody>
410
+ </table>
411
+ </div>
412
+ </div>
413
+ </div>
414
+
415
+ <!-- ==================== 第9页:关键洞察 ==================== -->
416
+ <div class="slide bg-dark-alt" data-slide="9">
417
+ <div class="insight-content">
418
+ <h2>💡 关键洞察</h2>
419
+ <div class="insight-grid">
420
+ ${(insights || []).map(item => `
421
+ <div class="insight-card ${item.warning ? 'warning' : ''}">
422
+ <h3>${item.title}</h3>
423
+ <p>${item.content}</p>
424
+ </div>
425
+ `).join('')}
426
+ </div>
427
+ </div>
428
+ </div>
429
+
430
+ <!-- ==================== 第10页:行动建议 ==================== -->
431
+ <div class="slide bg-dark" data-slide="10">
432
+ <div class="rec-content">
433
+ <h2>📝 行动建议</h2>
434
+ <div class="rec-grid">
435
+ ${(recommendations || []).map(item => `
436
+ <div class="rec-card">
437
+ <h3>${item.title}</h3>
438
+ <p>${item.content}</p>
439
+ </div>
440
+ `).join('')}
441
+ </div>
442
+ </div>
443
+ </div>
444
+
445
+ <!-- ==================== 第11页:结尾页 ==================== -->
446
+ <div class="slide bg-purple" data-slide="11">
447
+ <div class="end-content">
448
+ <h1>谢谢观看</h1>
449
+ <p>${title}</p>
450
+ <p>${source || 'BytePlan 数据平台'}</p>
451
+ <div class="source">
452
+ 数据来源:${source || 'BytePlan 数据平台'}<br>
453
+ 分析时间:${period}
454
+ </div>
455
+ </div>
456
+ </div>
457
+ </div>
458
+
459
+ <div class="slide-counter" id="slideCounter">1 / 11</div>
460
+
461
+ <script>
462
+ // ==================== 幻灯片导航逻辑 ====================
463
+ let currentSlide = 1;
464
+ const totalSlides = 11;
465
+
466
+ function showSlide(n) {
467
+ const slides = document.querySelectorAll('.slide');
468
+ slides.forEach(s => s.classList.remove('active'));
469
+ slides[n - 1].classList.add('active');
470
+ document.getElementById('slideCounter').textContent = n + ' / ' + totalSlides;
471
+ document.getElementById('progressBar').style.width = (n / totalSlides * 100) + '%';
472
+ currentSlide = n;
473
+ }
474
+
475
+ function nextSlide() {
476
+ if (currentSlide < totalSlides) showSlide(currentSlide + 1);
477
+ }
478
+
479
+ function prevSlide() {
480
+ if (currentSlide > 1) showSlide(currentSlide - 1);
481
+ }
482
+
483
+ // 键盘导航
484
+ document.addEventListener('keydown', e => {
485
+ if (e.key === 'ArrowDown' || e.key === 'ArrowRight' || e.key === ' ') nextSlide();
486
+ if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') prevSlide();
487
+ });
488
+
489
+ // 触摸导航
490
+ let touchX = 0;
491
+ document.addEventListener('touchstart', e => touchX = e.touches[0].clientX);
492
+ document.addEventListener('touchend', e => {
493
+ const diff = e.changedTouches[0].clientX - touchX;
494
+ if (diff > 50) prevSlide();
495
+ if (diff < -50) nextSlide();
496
+ });
497
+
498
+ // ==================== Chart.js 图表初始化 ====================
499
+ setTimeout(() => {
500
+ // 图表通用配置
501
+ Chart.defaults.color = '${COLORS.textGray}';
502
+ Chart.defaults.borderColor = 'rgba(255,255,255,0.1)';
503
+
504
+ // 柱状图
505
+ new Chart(document.getElementById('barChart'), {
506
+ type: 'bar',
507
+ data: {
508
+ labels: ${JSON.stringify((barChart?.data || []).map(d => d.name))},
509
+ datasets: [{
510
+ label: '数值',
511
+ data: ${JSON.stringify((barChart?.data || []).map(d => d.value))},
512
+ backgroundColor: ['${COLORS.green}', '${COLORS.blue}', '${COLORS.pink}', '${COLORS.purple}', '${COLORS.orange}'],
513
+ borderRadius: 8
514
+ }]
515
+ },
516
+ options: {
517
+ responsive: true,
518
+ plugins: { legend: { labels: { color: '#fff', font: { size: 14 } } } },
519
+ scales: {
520
+ y: { ticks: { color: '${COLORS.textGray}' }, grid: { color: 'rgba(255,255,255,0.1)' } },
521
+ x: { ticks: { color: '#fff' }, grid: { display: false } }
522
+ }
523
+ }
524
+ });
525
+
526
+ // 贡献率图
527
+ new Chart(document.getElementById('ratioChart'), {
528
+ type: 'bar',
529
+ data: {
530
+ labels: ${JSON.stringify((ratioData?.data || []).map(d => d.name))},
531
+ datasets: [{
532
+ label: '贡献率 (%)',
533
+ data: ${JSON.stringify((ratioData?.data || []).map(d => d.value))},
534
+ backgroundColor: '${COLORS.purple}',
535
+ borderRadius: 8
536
+ }]
537
+ },
538
+ options: {
539
+ indexAxis: 'y',
540
+ responsive: true,
541
+ plugins: { legend: { labels: { color: '#fff' } } },
542
+ scales: {
543
+ y: { ticks: { color: '#fff' }, grid: { display: false } },
544
+ x: { ticks: { color: '${COLORS.textGray}' }, grid: { color: 'rgba(255,255,255,0.1)' }, max: 100 }
545
+ }
546
+ }
547
+ });
548
+
549
+ // 排行榜
550
+ new Chart(document.getElementById('rankingChart'), {
551
+ type: 'bar',
552
+ data: {
553
+ labels: ${JSON.stringify((rankingData?.data || []).map(d => d.name))},
554
+ datasets: [{
555
+ label: '数值',
556
+ data: ${JSON.stringify((rankingData?.data || []).map(d => d.value))},
557
+ backgroundColor: '${COLORS.green}',
558
+ borderRadius: 6
559
+ }]
560
+ },
561
+ options: {
562
+ indexAxis: 'y',
563
+ responsive: true,
564
+ plugins: { legend: { display: false } },
565
+ scales: {
566
+ y: { ticks: { color: '#fff' }, grid: { display: false } },
567
+ x: { ticks: { color: '${COLORS.textGray}' }, grid: { color: 'rgba(255,255,255,0.1)' } }
568
+ }
569
+ }
570
+ });
571
+ }, 100);
572
+ </script>
573
+ </body>
574
+ </html>`;
575
+ }
576
+
577
+ // ==================== 主函数 ====================
578
+
579
+ function generateHTMLReport(outputFile, dataFile) {
580
+ // 读取数据文件
581
+ let data = {};
582
+
583
+ if (fs.existsSync(dataFile)) {
584
+ data = JSON.parse(fs.readFileSync(dataFile, 'utf-8'));
585
+ } else {
586
+ console.error('❌ 缺少数据文件:', dataFile);
587
+ process.exit(1);
588
+ }
589
+
590
+ // 生成 HTML
591
+ const html = generatePPTStyleHTML(data);
592
+ fs.writeFileSync(outputFile, html, 'utf-8');
593
+
594
+ console.log(`✅ 网页报告已生成: ${outputFile}`);
595
+ }
596
+
597
+ // ==================== 命令行入口 ====================
598
+
599
+ const args = process.argv.slice(2);
600
+ let outputFile = 'report.html';
601
+ let dataFile = 'data.json';
602
+
603
+ for (let i = 0; i < args.length; i++) {
604
+ if (args[i] === '-o' || args[i] === '--output-file') {
605
+ outputFile = args[++i];
606
+ } else if (args[i] === '-d' || args[i] === '--data-file') {
607
+ dataFile = args[++i];
608
+ }
609
+ }
610
+
611
+ generateHTMLReport(outputFile, dataFile);