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,517 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * BytePlan 网页报告生成脚本
4
+ * 支持两种风格:传统网页(滚动式)和 PPT风格(幻灯片切换)
5
+ *
6
+ * 图表类型智能选择:
7
+ * - line_chart: 适合趋势数据(时间序列、季度变化、增长趋势)
8
+ * - pie_chart: 适合占比数据(市场份额、贡献比例、构成分析)
9
+ * - bar_chart: 适合对比数据(不同类别数值对比)
10
+ * - horizontal_bar: 适合排行榜或比例数据
11
+ *
12
+ * 设计原则:
13
+ * - 单文件输出,无需服务器
14
+ * - 响应式布局,适配各种屏幕
15
+ * - 深色主题,专业数据分析风格
16
+ * - PPT风格支持键盘/触摸导航
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+
22
+ // ============ 图表类型智能选择 ============
23
+
24
+ /**
25
+ * 根据数据特征自动选择最合适的图表类型
26
+ * @param {Object} chartData - 图表数据
27
+ * @param {string} analysisGoal - 分析目标关键词
28
+ * @returns {string} - 图表类型: 'line' | 'pie' | 'bar' | 'horizontalBar'
29
+ */
30
+ function selectChartType(chartData, analysisGoal = '') {
31
+ const data = chartData?.data || [];
32
+ const title = (chartData?.title || '').toLowerCase();
33
+ const goal = (analysisGoal || '').toLowerCase();
34
+
35
+ // 1. 趋势关键词 -> 折线图(必须是时间序列数据)
36
+ const trendKeywords = ['趋势', '增长', '变化', '走势', 'timeline', 'trend', 'growth', 'change'];
37
+ const hasTrendKeyword = trendKeywords.some(kw => title.includes(kw));
38
+ if (hasTrendKeyword) {
39
+ const labels = data.map(d => d.name || d.label || '');
40
+ const hasTimeLabels = labels.some(l =>
41
+ /^Q[1-4]/.test(l) || /^\d{4}年/.test(l) || /^\d{1,2}月/.test(l) || /^[上下]半年/.test(l) || /^FY\d{2}/.test(l)
42
+ );
43
+ if (hasTimeLabels) {
44
+ return 'line';
45
+ }
46
+ }
47
+
48
+ // 2. 对比关键词 -> 柱状图
49
+ const compareKeywords = ['对比', '比较', 'vs', 'versus', 'compare', 'comparison'];
50
+ if (compareKeywords.some(kw => title.includes(kw))) {
51
+ return 'bar';
52
+ }
53
+
54
+ // 3. 排行榜关键词 -> 横向条形图
55
+ const rankingKeywords = ['排行', '排名', 'top', '榜单', 'ranking', 'leaderboard'];
56
+ if (rankingKeywords.some(kw => title.includes(kw))) {
57
+ return 'horizontalBar';
58
+ }
59
+
60
+ // 4. 贡献率关键词 -> 横向条形图(百分比展示)
61
+ const rateKeywords = ['贡献率', '比率', '百分比', 'rate', 'percentage', 'ratio'];
62
+ if (rateKeywords.some(kw => title.includes(kw))) {
63
+ return 'horizontalBar';
64
+ }
65
+
66
+ // 5. 占比/构成关键词 -> 饼图
67
+ const pieKeywords = ['占比', '构成', '比例', '份额', '分布', 'composition', 'proportion', 'share', 'distribution', '饼图'];
68
+ if (pieKeywords.some(kw => title.includes(kw))) {
69
+ if (data.length <= 8 && data.every(d => (d.value || 0) >= 0)) {
70
+ return 'pie';
71
+ }
72
+ }
73
+
74
+ // 默认:柱状图
75
+ return 'bar';
76
+ }
77
+
78
+ /**
79
+ * 生成 Chart.js 配置
80
+ * @param {string} type - 图表类型
81
+ * @param {Object} chartData - 图表数据
82
+ * @param {string} canvasId - Canvas ID
83
+ * @returns {string} - Chart.js 初始化代码
84
+ */
85
+ function generateChartConfig(type, chartData, canvasId) {
86
+ const data = chartData?.data || [];
87
+ const labels = JSON.stringify(data.map(d => d.name || d.label || ''));
88
+ const valuesArray = data.map(d => d.value || 0);
89
+ const values = JSON.stringify(valuesArray);
90
+
91
+ const colors = ['#4ade80', '#60a5fa', '#f472b6', '#667eea', '#fbbf24', '#06B6D4', '#f87171', '#a78bfa'];
92
+
93
+ switch (type) {
94
+ case 'line':
95
+ return `new Chart(document.getElementById('${canvasId}'), {
96
+ type: 'line',
97
+ data: {
98
+ labels: ${labels},
99
+ datasets: [{
100
+ label: '${chartData?.title || '数值'}',
101
+ data: ${values},
102
+ borderColor: '#4ade80',
103
+ backgroundColor: 'rgba(74, 222, 128, 0.1)',
104
+ borderWidth: 3,
105
+ fill: true,
106
+ tension: 0.4,
107
+ pointBackgroundColor: '#4ade80',
108
+ pointBorderColor: '#fff',
109
+ pointRadius: 6,
110
+ pointHoverRadius: 8
111
+ }]
112
+ },
113
+ options: {
114
+ responsive: true,
115
+ plugins: {
116
+ legend: { labels: { color: '#fff', font: { size: 14 } } },
117
+ tooltip: { backgroundColor: 'rgba(0,0,0,0.8)', titleColor: '#fff', bodyColor: '#fff' }
118
+ },
119
+ scales: {
120
+ y: {
121
+ ticks: { color: '#CBD5E1', font: { size: 12 } },
122
+ grid: { color: 'rgba(255,255,255,0.1)' },
123
+ beginAtZero: true
124
+ },
125
+ x: {
126
+ ticks: { color: '#fff', font: { size: 13 } },
127
+ grid: { display: false }
128
+ }
129
+ }
130
+ }
131
+ })`;
132
+
133
+ case 'pie':
134
+ return `new Chart(document.getElementById('${canvasId}'), {
135
+ type: 'pie',
136
+ data: {
137
+ labels: ${labels},
138
+ datasets: [{
139
+ data: ${values},
140
+ backgroundColor: ${JSON.stringify(colors.slice(0, data.length))},
141
+ borderColor: 'rgba(255,255,255,0.2)',
142
+ borderWidth: 2
143
+ }]
144
+ },
145
+ options: {
146
+ responsive: true,
147
+ maintainAspectRatio: true,
148
+ plugins: {
149
+ legend: {
150
+ position: 'right',
151
+ labels: { color: '#fff', font: { size: 14 }, padding: 15 }
152
+ },
153
+ tooltip: {
154
+ backgroundColor: 'rgba(0,0,0,0.8)',
155
+ callbacks: {
156
+ label: function(context) {
157
+ const total = context.dataset.data.reduce((a, b) => a + b, 0);
158
+ const percentage = ((context.raw / total) * 100).toFixed(1);
159
+ return context.label + ': ' + context.raw + ' (' + percentage + '%)';
160
+ }
161
+ }
162
+ }
163
+ }
164
+ }
165
+ })`;
166
+
167
+ case 'horizontalBar':
168
+ return `new Chart(document.getElementById('${canvasId}'), {
169
+ type: 'bar',
170
+ data: {
171
+ labels: ${labels},
172
+ datasets: [{
173
+ label: '${chartData?.title || '数值'}',
174
+ data: ${values},
175
+ backgroundColor: ${valuesArray.some(v => v < 0) ? JSON.stringify(valuesArray.map(v => v < 0 ? '#f87171' : '#4ade80')) : "'#667eea'"},
176
+ borderRadius: 6
177
+ }]
178
+ },
179
+ options: {
180
+ indexAxis: 'y',
181
+ responsive: true,
182
+ plugins: {
183
+ legend: { labels: { color: '#fff' } }
184
+ },
185
+ scales: {
186
+ y: { ticks: { color: '#fff', font: { size: 13 } }, grid: { display: false } },
187
+ x: { ticks: { color: '#CBD5E1' }, grid: { color: 'rgba(255,255,255,0.1)' }, ${valuesArray.every(v => v >= 0 && v <= 100) ? 'max: 100,' : ''} beginAtZero: true }
188
+ }
189
+ }
190
+ })`;
191
+
192
+ case 'bar':
193
+ default:
194
+ return `new Chart(document.getElementById('${canvasId}'), {
195
+ type: 'bar',
196
+ data: {
197
+ labels: ${labels},
198
+ datasets: [{
199
+ label: '${chartData?.title || '数值'}',
200
+ data: ${values},
201
+ backgroundColor: ${JSON.stringify(colors.slice(0, data.length))},
202
+ borderRadius: 8
203
+ }]
204
+ },
205
+ options: {
206
+ responsive: true,
207
+ plugins: {
208
+ legend: { labels: { color: '#fff' } }
209
+ },
210
+ scales: {
211
+ y: { ticks: { color: '#CBD5E1' }, grid: { color: 'rgba(255,255,255,0.1)' }, beginAtZero: true },
212
+ x: { ticks: { color: '#fff' }, grid: { display: false } }
213
+ }
214
+ }
215
+ })`;
216
+ }
217
+ }
218
+
219
+ // ============ PPT风格 HTML 生成 ============
220
+
221
+ /**
222
+ * 获取图表图标(根据类型)
223
+ */
224
+ function getChartIcon(type) {
225
+ switch (type) {
226
+ case 'line': return '📈';
227
+ case 'pie': return '🥧';
228
+ case 'horizontalBar': return '📊';
229
+ default: return '📊';
230
+ }
231
+ }
232
+
233
+ function generatePPTStyleHTML(data) {
234
+ const { title, subtitle, period, sections, summary, kpis, charts, tableData, insights, recommendations, source, analysisGoal } = data;
235
+
236
+ // 处理图表数据
237
+ // 支持两种格式:新的 charts 数组 或 旧的 barChart/ratioData/rankingData
238
+ let processedCharts = [];
239
+
240
+ if (charts && Array.isArray(charts)) {
241
+ // 新格式:charts 数组
242
+ processedCharts = charts.map((chart, index) => {
243
+ const chartType = chart.type || selectChartType(chart, analysisGoal || title);
244
+ return {
245
+ ...chart,
246
+ type: chartType,
247
+ canvasId: `chart_${index}`,
248
+ icon: getChartIcon(chartType)
249
+ };
250
+ });
251
+ } else {
252
+ // 旧格式兼容:barChart, ratioData, rankingData
253
+ const legacyCharts = [
254
+ { data: data.barChart, defaultType: 'bar', defaultTitle: '数据对比分析', canvasId: 'barChart' },
255
+ { data: data.ratioData, defaultType: 'horizontalBar', defaultTitle: '贡献率对比', canvasId: 'ratioChart' },
256
+ { data: data.rankingData, defaultType: 'horizontalBar', defaultTitle: '贡献排行榜', canvasId: 'rankingChart' }
257
+ ];
258
+
259
+ processedCharts = legacyCharts
260
+ .filter(c => c.data && c.data.data && c.data.data.length > 0)
261
+ .map(c => {
262
+ const chartType = c.data.type || selectChartType(c.data, analysisGoal || title);
263
+ return {
264
+ ...c.data,
265
+ type: chartType,
266
+ canvasId: c.canvasId,
267
+ title: c.data.title || c.defaultTitle,
268
+ icon: getChartIcon(chartType)
269
+ };
270
+ });
271
+ }
272
+
273
+ // 计算总幻灯片数
274
+ const baseSlides = 4; // 封面、目录、摘要、KPI
275
+ const chartSlides = processedCharts.length;
276
+ const endingSlides = 4; // 表格、洞察、建议、结尾
277
+ const totalSlides = baseSlides + chartSlides + endingSlides;
278
+
279
+ // 生成图表幻灯片 HTML
280
+ const chartSlidesHTML = processedCharts.map((chart, index) => {
281
+ const slideNum = baseSlides + index + 1;
282
+ const bgClass = ['bg-dark-reverse', 'bg-dark-alt', 'bg-dark'][index % 3];
283
+ return `
284
+ <div class="slide ${bgClass}" data-slide="${slideNum}">
285
+ <div class="chart-content">
286
+ <h2>${chart.icon} ${chart.title}</h2>
287
+ <div class="chart-box"><canvas id="${chart.canvasId}"></div>
288
+ </div>
289
+ </div>`;
290
+ }).join('');
291
+
292
+ // 生成图表初始化脚本
293
+ const chartInitScript = processedCharts.map(chart =>
294
+ generateChartConfig(chart.type, chart, chart.canvasId)
295
+ ).join('\n ');
296
+
297
+ // 动态计算幻灯片编号
298
+ const tableSlideNum = baseSlides + chartSlides + 1;
299
+ const insightSlideNum = tableSlideNum + 1;
300
+ const recSlideNum = insightSlideNum + 1;
301
+ const endSlideNum = recSlideNum + 1;
302
+
303
+ return `<!DOCTYPE html>
304
+ <html lang="zh-CN">
305
+ <head>
306
+ <meta charset="UTF-8">
307
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
308
+ <title>${title}</title>
309
+ <script src="https://cdn.staticfile.org/Chart.js/4.4.1/chart.umd.min.js"></script>
310
+ <style>
311
+ * { margin: 0; padding: 0; box-sizing: border-box; }
312
+ body { font-family: 'Microsoft YaHei', Arial, sans-serif; background: #1a1a2e; color: #fff; overflow: hidden; }
313
+ .slides-container { width: 100vw; height: 100vh; position: relative; }
314
+ .slide { width: 100%; height: 100%; position: absolute; top: 0; left: 0; opacity: 0; visibility: hidden; transition: opacity 0.5s ease, visibility 0.5s ease; display: flex; flex-direction: column; justify-content: center; align-items: center; padding: 30px 50px; overflow: hidden; }
315
+ .slide.active { opacity: 1; visibility: visible; }
316
+ .bg-purple { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
317
+ .bg-dark { background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); }
318
+ .bg-dark-alt { background: linear-gradient(135deg, #0f3460 0%, #16213e 100%); }
319
+ .bg-dark-reverse { background: linear-gradient(135deg, #16213e 0%, #1a1a2e 100%); }
320
+ .cover-content { text-align: center; max-width: 90%; }
321
+ .cover-content h1 { font-size: 3.5rem; font-weight: 700; margin-bottom: 20px; letter-spacing: 3px; }
322
+ .cover-content .subtitle { font-size: 1.5rem; opacity: 0.9; margin-bottom: 25px; }
323
+ .cover-content .meta { font-size: 1.1rem; opacity: 0.7; }
324
+ .cover-content .tenant { margin-top: 40px; padding: 15px 35px; background: rgba(255,255,255,0.15); border-radius: 10px; font-size: 1.2rem; }
325
+ .toc-content { width: 90%; max-width: 800px; }
326
+ .toc-content h2 { font-size: 2.5rem; margin-bottom: 35px; text-align: center; }
327
+ .toc-list { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
328
+ .toc-item { display: flex; align-items: center; padding: 18px 20px; background: rgba(255,255,255,0.1); border-radius: 12px; }
329
+ .toc-item .num { font-size: 1.8rem; font-weight: bold; color: #667eea; margin-right: 15px; }
330
+ .toc-item .text { font-size: 1.2rem; }
331
+ .summary-content { width: 90%; max-width: 900px; }
332
+ .summary-content h2 { font-size: 2.5rem; margin-bottom: 30px; text-align: center; }
333
+ .summary-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 25px; }
334
+ .summary-card { background: rgba(255,255,255,0.1); border-radius: 18px; padding: 25px; text-align: center; border: 2px solid rgba(255,255,255,0.2); }
335
+ .summary-card .rank { font-size: 1.5rem; color: #fbbf24; margin-bottom: 12px; }
336
+ .summary-card .name { font-size: 1.8rem; font-weight: bold; margin-bottom: 10px; }
337
+ .summary-card .type { font-size: 1rem; opacity: 0.7; margin-bottom: 15px; }
338
+ .summary-card .value { font-size: 2rem; color: #4ade80; font-weight: bold; }
339
+ .summary-card .rate { font-size: 1.2rem; color: #60a5fa; margin-top: 10px; }
340
+ .kpi-content { width: 90%; max-width: 900px; }
341
+ .kpi-content h2 { font-size: 2.5rem; margin-bottom: 30px; text-align: center; }
342
+ .kpi-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; }
343
+ .kpi-card { background: rgba(255,255,255,0.1); border-radius: 15px; padding: 22px; text-align: center; }
344
+ .kpi-card .icon { font-size: 2.2rem; margin-bottom: 12px; }
345
+ .kpi-card .label { font-size: 0.95rem; opacity: 0.7; margin-bottom: 8px; }
346
+ .kpi-card .value { font-size: 1.8rem; font-weight: bold; color: #4ade80; }
347
+ .kpi-card .unit { font-size: 0.85rem; opacity: 0.6; }
348
+ .chart-content { width: 90%; max-width: 850px; }
349
+ .chart-content h2 { font-size: 2.2rem; margin-bottom: 25px; text-align: center; }
350
+ .chart-box { background: rgba(0,0,0,0.3); border-radius: 18px; padding: 25px; }
351
+ .table-content { width: 90%; max-width: 900px; }
352
+ .table-content h2 { font-size: 2rem; margin-bottom: 20px; text-align: center; }
353
+ .table-wrapper { background: rgba(0,0,0,0.3); border-radius: 15px; padding: 15px 20px; overflow: hidden; }
354
+ table { width: 100%; border-collapse: collapse; font-size: 1rem; }
355
+ th, td { padding: 10px 12px; text-align: center; }
356
+ th { background: rgba(102, 126, 234, 0.4); font-weight: 600; }
357
+ tr:nth-child(even) { background: rgba(255,255,255,0.05); }
358
+ .positive { color: #4ade80; font-weight: bold; }
359
+ .negative { color: #f87171; font-weight: bold; }
360
+ .insight-content, .rec-content { width: 90%; max-width: 850px; }
361
+ .insight-content h2, .rec-content h2 { font-size: 2.5rem; margin-bottom: 30px; text-align: center; }
362
+ .insight-grid, .rec-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 22px; }
363
+ .insight-card, .rec-card { border-radius: 18px; padding: 25px; }
364
+ .insight-card { background: rgba(255,255,255,0.1); border-left: 5px solid #667eea; }
365
+ .insight-card.warning { border-left-color: #fbbf24; }
366
+ .insight-card h3 { font-size: 1.4rem; color: #667eea; margin-bottom: 12px; }
367
+ .insight-card.warning h3 { color: #fbbf24; }
368
+ .insight-card p { font-size: 1.1rem; line-height: 1.6; opacity: 0.9; }
369
+ .rec-card { background: rgba(74,222,128,0.2); border-left: 5px solid #4ade80; }
370
+ .rec-card h3 { font-size: 1.4rem; color: #4ade80; margin-bottom: 12px; }
371
+ .rec-card p { font-size: 1.1rem; line-height: 1.6; opacity: 0.9; }
372
+ .end-content { text-align: center; max-width: 90%; }
373
+ .end-content h1 { font-size: 4rem; font-weight: 700; margin-bottom: 25px; }
374
+ .end-content p { font-size: 1.4rem; opacity: 0.8; margin-bottom: 12px; }
375
+ .end-content .source { margin-top: 40px; padding: 20px 40px; background: rgba(255,255,255,0.15); border-radius: 10px; font-size: 1.1rem; }
376
+ .slide-counter { position: fixed; bottom: 25px; right: 25px; background: rgba(255,255,255,0.15); padding: 10px 18px; border-radius: 10px; font-size: 1rem; opacity: 0.7; }
377
+ .progress-bar { position: fixed; top: 0; left: 0; height: 4px; background: #667eea; transition: width 0.5s ease; z-index: 100; }
378
+ .nav-hint { position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.75); padding: 12px 24px; border-radius: 12px; font-size: 0.95rem; color: #fff; opacity: 1; transition: opacity 0.5s ease; z-index: 99; display: flex; align-items: center; gap: 12px; backdrop-filter: blur(8px); border: 1px solid rgba(255,255,255,0.1); }
379
+ .nav-hint.hidden { opacity: 0; pointer-events: none; }
380
+ .nav-hint kbd { background: rgba(255,255,255,0.15); padding: 4px 8px; border-radius: 4px; font-family: inherit; margin: 0 2px; }
381
+ </style>
382
+ </head>
383
+ <body>
384
+ <div class="progress-bar" id="progressBar"></div>
385
+ <div class="nav-hint" id="navHint">⌨️ 使用 <kbd>↑</kbd><kbd>↓</kbd><kbd>←</kbd><kbd>→</kbd> 或滚轮切换幻灯片</div>
386
+ <div class="slides-container">
387
+ <div class="slide bg-purple active" data-slide="1">
388
+ <div class="cover-content">
389
+ <h1>${title}</h1>
390
+ <p class="subtitle">${subtitle || ''}</p>
391
+ <p class="meta">${period}</p>
392
+ <div class="tenant">${source || 'BytePlan 数据平台'}</div>
393
+ </div>
394
+ </div>
395
+ <div class="slide bg-dark" data-slide="2">
396
+ <div class="toc-content">
397
+ <h2>目录</h2>
398
+ <div class="toc-list">
399
+ ${(sections || ['核心发现摘要', '关键指标概览', '数据分析', '洞察建议']).map((item, i) => `<div class="toc-item"><span class="num">${(i+1).toString().padStart(2,'0')}</span><span class="text">${item}</span></div>`).join('')}
400
+ </div>
401
+ </div>
402
+ </div>
403
+ <div class="slide bg-dark-alt" data-slide="3">
404
+ <div class="summary-content">
405
+ <h2>🏆 贡献最大的三个要素</h2>
406
+ <div class="summary-grid">
407
+ ${(summary || []).map(item => `<div class="summary-card"><div class="rank">${item.rank}</div><div class="name">${item.name}</div><div class="type">${item.type}</div><div class="value">${item.value}</div><div class="rate">${item.rate}</div></div>`).join('')}
408
+ </div>
409
+ </div>
410
+ </div>
411
+ <div class="slide bg-dark" data-slide="4">
412
+ <div class="kpi-content">
413
+ <h2>📊 关键指标概览</h2>
414
+ <div class="kpi-grid">
415
+ ${(kpis || []).map(kpi => `<div class="kpi-card"><div class="icon">${kpi.icon}</div><div class="label">${kpi.label}</div><div class="value">${kpi.value}</div><div class="unit">${kpi.unit}</div></div>`).join('')}
416
+ </div>
417
+ </div>
418
+ </div>
419
+ ${chartSlidesHTML}
420
+ <div class="slide bg-dark-reverse" data-slide="${tableSlideNum}">
421
+ <div class="table-content">
422
+ <h2>📋 ${tableData?.title || '数据明细表'}</h2>
423
+ <div class="table-wrapper">
424
+ <table>
425
+ <thead><tr>${(tableData?.columns || ['要素名称', '类型', '数值', '贡献', '贡献率']).map(col => `<th>${col}</th>`).join('')}</tr></thead>
426
+ <tbody>
427
+ ${(tableData?.rows || []).map(row => `<tr>${row.map((cell, i) => `<td class="${cell.toString().startsWith('-') ? 'negative' : 'positive'}">${cell}</td>`).join('')}</tr>`).join('')}
428
+ </tbody>
429
+ </table>
430
+ </div>
431
+ </div>
432
+ </div>
433
+ <div class="slide bg-dark-alt" data-slide="${insightSlideNum}">
434
+ <div class="insight-content">
435
+ <h2>💡 关键洞察</h2>
436
+ <div class="insight-grid">
437
+ ${(insights || []).map(item => `<div class="insight-card ${item.warning ? 'warning' : ''}"><h3>${item.title}</h3><p>${item.content}</p></div>`).join('')}
438
+ </div>
439
+ </div>
440
+ </div>
441
+ <div class="slide bg-dark" data-slide="${recSlideNum}">
442
+ <div class="rec-content">
443
+ <h2>📝 行动建议</h2>
444
+ <div class="rec-grid">
445
+ ${(recommendations || []).map(item => `<div class="rec-card"><h3>${item.title}</h3><p>${item.content}</p></div>`).join('')}
446
+ </div>
447
+ </div>
448
+ </div>
449
+ <div class="slide bg-purple" data-slide="${endSlideNum}">
450
+ <div class="end-content">
451
+ <h1>谢谢观看</h1>
452
+ <p>${title}</p>
453
+ <p>${source || 'BytePlan 数据平台'}</p>
454
+ <div class="source">数据来源:${source || 'BytePlan 数据平台'}<br>分析时间:${period}</div>
455
+ </div>
456
+ </div>
457
+ </div>
458
+ <div class="slide-counter" id="slideCounter">1 / ${totalSlides}</div>
459
+ <script>
460
+ let currentSlide = 1, totalSlides = ${totalSlides};
461
+ function showSlide(n) { const slides = document.querySelectorAll('.slide'); slides.forEach(s => s.classList.remove('active')); slides[n - 1].classList.add('active'); document.getElementById('slideCounter').textContent = n + ' / ' + totalSlides; document.getElementById('progressBar').style.width = (n / totalSlides * 100) + '%'; currentSlide = n; }
462
+ function nextSlide() { if (currentSlide < totalSlides) showSlide(currentSlide + 1); }
463
+ function prevSlide() { if (currentSlide > 1) showSlide(currentSlide - 1); }
464
+ function hideNavHint() { document.getElementById('navHint').classList.add('hidden'); }
465
+ document.addEventListener('keydown', e => { hideNavHint(); if (e.key === 'ArrowDown' || e.key === 'ArrowRight' || e.key === ' ') nextSlide(); if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') prevSlide(); });
466
+ let touchX = 0; document.addEventListener('touchstart', e => touchX = e.touches[0].clientX); document.addEventListener('touchend', e => { const diff = e.changedTouches[0].clientX - touchX; if (diff > 50) prevSlide(); if (diff < -50) nextSlide(); });
467
+ let wheelTimeout; document.addEventListener('wheel', e => { hideNavHint(); if (wheelTimeout) return; wheelTimeout = setTimeout(() => wheelTimeout = null, 500); if (e.deltaY > 0) nextSlide(); else if (e.deltaY < 0) prevSlide(); }, { passive: true });
468
+ setTimeout(() => hideNavHint(), 6000);
469
+ setTimeout(() => {
470
+ ${chartInitScript}
471
+ }, 100);
472
+ </script>
473
+ </body>
474
+ </html>`;
475
+ }
476
+
477
+ // ============ 主函数 ============
478
+
479
+ function generateHTMLReport(outputFile, dataFile, style = 'ppt') {
480
+ let data = {};
481
+
482
+ if (fs.existsSync(dataFile || 'ppt_data.json')) {
483
+ data = JSON.parse(fs.readFileSync(dataFile || 'ppt_data.json', 'utf-8'));
484
+ } else if (fs.existsSync('analysisPlan.json')) {
485
+ const plan = JSON.parse(fs.readFileSync('analysisPlan.json', 'utf-8'));
486
+ data = { title: plan.planName, subtitle: plan.planDescription, period: new Date().toLocaleDateString('zh-CN'), source: 'BytePlan 数据平台' };
487
+ } else {
488
+ console.error('❌ 缺少数据文件');
489
+ process.exit(1);
490
+ }
491
+
492
+ const html = generatePPTStyleHTML(data);
493
+ fs.writeFileSync(outputFile, html, 'utf-8');
494
+
495
+ console.log(`✅ 网页报告已生成: ${outputFile}`);
496
+ }
497
+
498
+ // 命令行入口
499
+ const args = process.argv.slice(2);
500
+ let outputFile = 'analysis_report.html';
501
+ let dataFile = null;
502
+
503
+ for (let i = 0; i < args.length; i++) {
504
+ if (args[i] === '-o' || args[i] === '--output-file') outputFile = args[++i];
505
+ else if (args[i] === '-d' || args[i] === '--data-file') dataFile = args[++i];
506
+ }
507
+
508
+ // 自动发现数据文件:未指定 -d 时按优先级查找
509
+ if (!dataFile) {
510
+ const candidates = ['report_data.json', 'ppt_data.json', 'analysis_results.json'];
511
+ for (const f of candidates) {
512
+ if (fs.existsSync(f)) { dataFile = f; break; }
513
+ }
514
+ if (!dataFile) dataFile = 'ppt_data.json'; // 兜底,让后续逻辑报错
515
+ }
516
+
517
+ generateHTMLReport(outputFile, dataFile);