@playcraft/build 0.0.8 → 0.0.11

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 (53) hide show
  1. package/README.md +122 -6
  2. package/dist/analyzers/build-analyzer.d.ts +98 -0
  3. package/dist/analyzers/build-analyzer.js +1160 -0
  4. package/dist/analyzers/enhanced-report-template.d.ts +13 -0
  5. package/dist/analyzers/enhanced-report-template.js +957 -0
  6. package/dist/analyzers/index.d.ts +6 -0
  7. package/dist/analyzers/index.js +9 -0
  8. package/dist/analyzers/optimization-analyzer.d.ts +88 -0
  9. package/dist/analyzers/optimization-analyzer.js +278 -0
  10. package/dist/analyzers/playable-analyzer.d.ts +91 -0
  11. package/dist/analyzers/playable-analyzer.js +976 -0
  12. package/dist/analyzers/report-template.d.ts +50 -0
  13. package/dist/analyzers/report-template.js +591 -0
  14. package/dist/analyzers/scene-asset-collector.js +8 -0
  15. package/dist/base-builder.d.ts +9 -0
  16. package/dist/base-builder.js +149 -1
  17. package/dist/build-state-manager.d.ts +110 -0
  18. package/dist/build-state-manager.js +169 -0
  19. package/dist/generators/config-generator.d.ts +2 -0
  20. package/dist/generators/config-generator.js +179 -10
  21. package/dist/index.d.ts +8 -0
  22. package/dist/index.js +6 -0
  23. package/dist/loaders/playcanvas-loader.d.ts +7 -0
  24. package/dist/loaders/playcanvas-loader.js +17 -0
  25. package/dist/state/build-state-manager.d.ts +174 -0
  26. package/dist/state/build-state-manager.js +235 -0
  27. package/dist/state/index.d.ts +4 -0
  28. package/dist/state/index.js +2 -0
  29. package/dist/state/state-to-report-converter.d.ts +141 -0
  30. package/dist/state/state-to-report-converter.js +177 -0
  31. package/dist/utils.d.ts +4 -0
  32. package/dist/utils.js +11 -0
  33. package/dist/vite/config-builder.js +8 -1
  34. package/dist/vite/plugin-build-state.d.ts +11 -0
  35. package/dist/vite/plugin-build-state.js +145 -0
  36. package/dist/vite/plugin-source-builder.js +1 -0
  37. package/package.json +12 -12
  38. package/dist/templates/__loading__.js +0 -100
  39. package/dist/templates/__modules__.js +0 -47
  40. package/dist/templates/__settings__.template.js +0 -20
  41. package/dist/templates/__start__.js +0 -332
  42. package/dist/templates/index.html +0 -18
  43. package/dist/templates/logo.png +0 -0
  44. package/dist/templates/manifest.json +0 -1
  45. package/dist/templates/patches/cannon.min.js +0 -28
  46. package/dist/templates/patches/lz4.js +0 -10
  47. package/dist/templates/patches/one-page-http-get.js +0 -20
  48. package/dist/templates/patches/one-page-inline-game-scripts.js +0 -52
  49. package/dist/templates/patches/one-page-mraid-resize-canvas.js +0 -46
  50. package/dist/templates/patches/p2.min.js +0 -27
  51. package/dist/templates/patches/playcraft-no-xhr.js +0 -76
  52. package/dist/templates/playcanvas-stable.min.js +0 -16363
  53. package/dist/templates/styles.css +0 -43
@@ -0,0 +1,957 @@
1
+ /**
2
+ * 获取压缩率颜色类名
3
+ */
4
+ const getCompressionColorClass = (ratio) => {
5
+ if (ratio > 0.5)
6
+ return 'compression-good';
7
+ if (ratio > 0.2)
8
+ return 'compression-medium';
9
+ return 'compression-poor';
10
+ };
11
+ /**
12
+ * 获取严重程度颜色类名
13
+ */
14
+ const getSeverityColorClass = (severity) => {
15
+ if (severity === 'high')
16
+ return 'severity-high';
17
+ if (severity === 'medium')
18
+ return 'severity-medium';
19
+ return 'severity-low';
20
+ };
21
+ /**
22
+ * 格式化字节数
23
+ */
24
+ const formatBytes = (bytes) => {
25
+ if (bytes === 0)
26
+ return '0 B';
27
+ const k = 1024;
28
+ const sizes = ['B', 'KB', 'MB', 'GB'];
29
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
30
+ return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
31
+ };
32
+ /**
33
+ * 生成增强的 HTML 报告模板
34
+ */
35
+ export const generateEnhancedReportHTML = (data) => {
36
+ // 生成资产列表表格
37
+ const assetRows = data.assets
38
+ .sort((a, b) => b.finalSize - a.finalSize)
39
+ .slice(0, 50) // Top 50
40
+ .map(asset => {
41
+ const compressionClass = getCompressionColorClass(asset.compressionRatio);
42
+ const compressionPercent = (asset.compressionRatio * 100).toFixed(1);
43
+ const optimizationsText = asset.optimizations.join(', ') || '无';
44
+ return `
45
+ <tr class="asset-row" data-asset-id="${asset.id}">
46
+ <td class="asset-name" title="${asset.originalName}">${asset.originalName}</td>
47
+ <td>${asset.type}</td>
48
+ <td class="size-cell">${formatBytes(asset.originalSize)}</td>
49
+ <td class="size-cell">${formatBytes(asset.finalSize)}</td>
50
+ <td class="compression-cell ${compressionClass}">
51
+ <span class="compression-badge">${compressionPercent}%</span>
52
+ </td>
53
+ <td class="optimizations-cell" title="${optimizationsText}">${optimizationsText}</td>
54
+ <td>${asset.inlined ? '✓ 内联' : '外部'}</td>
55
+ </tr>
56
+ `;
57
+ }).join('');
58
+ // 生成按类型统计表格
59
+ const typeRows = Object.entries(data.byType)
60
+ .sort((a, b) => b[1].finalSize - a[1].finalSize)
61
+ .map(([type, stats]) => {
62
+ const compressionClass = getCompressionColorClass(stats.avgCompressionRatio);
63
+ const compressionPercent = (stats.avgCompressionRatio * 100).toFixed(1);
64
+ return `
65
+ <tr>
66
+ <td>${type}</td>
67
+ <td>${stats.count}</td>
68
+ <td class="size-cell">${formatBytes(stats.originalSize)}</td>
69
+ <td class="size-cell">${formatBytes(stats.finalSize)}</td>
70
+ <td class="compression-cell ${compressionClass}">
71
+ <span class="compression-badge">${compressionPercent}%</span>
72
+ </td>
73
+ <td>
74
+ <div class="bar" style="width: ${(stats.finalSize / data.totalFinalSize * 100).toFixed(1)}%"></div>
75
+ </td>
76
+ </tr>
77
+ `;
78
+ }).join('');
79
+ // 生成按分类统计表格
80
+ const categoryRows = Object.entries(data.byCategory)
81
+ .sort((a, b) => b[1].size - a[1].size)
82
+ .map(([category, stats]) => {
83
+ return `
84
+ <tr>
85
+ <td>${stats.name}</td>
86
+ <td>${stats.count}</td>
87
+ <td class="size-cell">${formatBytes(stats.size)}</td>
88
+ <td>${stats.percentage.toFixed(2)}%</td>
89
+ <td>
90
+ <div class="bar" style="width: ${stats.percentage.toFixed(1)}%"></div>
91
+ </td>
92
+ </tr>
93
+ `;
94
+ }).join('');
95
+ // 生成优化建议部分
96
+ const optimizationSection = data.optimizationAnalysis ? `
97
+ <div class="section optimization-suggestions">
98
+ <h2>💡 优化建议</h2>
99
+
100
+ <div class="optimization-summary">
101
+ <div class="summary-card">
102
+ <div class="summary-label">建议总数</div>
103
+ <div class="summary-value">${data.optimizationAnalysis.totalSuggestions}</div>
104
+ </div>
105
+ <div class="summary-card">
106
+ <div class="summary-label">预估节省</div>
107
+ <div class="summary-value">${data.optimizationAnalysis.totalEstimatedSavingsFormatted}</div>
108
+ </div>
109
+ <div class="summary-card">
110
+ <div class="summary-label">预估优化率</div>
111
+ <div class="summary-value">${(data.optimizationAnalysis.estimatedOptimizationRate * 100).toFixed(1)}%</div>
112
+ </div>
113
+ </div>
114
+
115
+ ${data.optimizationAnalysis.suggestions.length > 0 ? `
116
+ <div class="suggestions-list">
117
+ ${data.optimizationAnalysis.suggestions.map(suggestion => {
118
+ const severityClass = getSeverityColorClass(suggestion.severity);
119
+ return `
120
+ <div class="suggestion-card ${severityClass}">
121
+ <div class="suggestion-header">
122
+ <span class="suggestion-severity">${suggestion.severity === 'high' ? '🔴 高' : suggestion.severity === 'medium' ? '🟡 中' : '🟢 低'}</span>
123
+ <span class="suggestion-type">${suggestion.type}</span>
124
+ <span class="suggestion-savings">可节省: ${suggestion.estimatedSavingsFormatted}</span>
125
+ </div>
126
+ <div class="suggestion-description">${suggestion.description}</div>
127
+ <div class="suggestion-recommendation">
128
+ <strong>建议:</strong>${suggestion.recommendation}
129
+ </div>
130
+ ${suggestion.affectedAssets.length > 0 ? `
131
+ <details class="suggestion-details">
132
+ <summary>受影响的资源 (${suggestion.affectedAssets.length})</summary>
133
+ <ul class="affected-assets">
134
+ ${suggestion.affectedAssets.slice(0, 10).map(asset => `<li>${asset}</li>`).join('')}
135
+ ${suggestion.affectedAssets.length > 10 ? `<li>... 还有 ${suggestion.affectedAssets.length - 10} 个</li>` : ''}
136
+ </ul>
137
+ </details>
138
+ ` : ''}
139
+ ${suggestion.configExample ? `
140
+ <details class="suggestion-details">
141
+ <summary>配置示例</summary>
142
+ <pre class="config-example"><code>${suggestion.configExample}</code></pre>
143
+ </details>
144
+ ` : ''}
145
+ </div>
146
+ `;
147
+ }).join('')}
148
+ </div>
149
+ ` : '<p class="no-suggestions">🎉 太棒了!没有发现明显的优化机会。</p>'}
150
+ </div>
151
+ ` : '';
152
+ return `<!DOCTYPE html>
153
+ <html lang="zh-CN">
154
+ <head>
155
+ <meta charset="UTF-8">
156
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
157
+ <title>构建分析报告 - 增强版</title>
158
+ <style>
159
+ * {
160
+ margin: 0;
161
+ padding: 0;
162
+ box-sizing: border-box;
163
+ }
164
+
165
+ body {
166
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
167
+ background: #1e1e1e;
168
+ color: #e0e0e0;
169
+ padding: 20px;
170
+ line-height: 1.6;
171
+ }
172
+
173
+ .container {
174
+ max-width: 1600px;
175
+ margin: 0 auto;
176
+ }
177
+
178
+ .header {
179
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
180
+ padding: 40px;
181
+ border-radius: 12px;
182
+ margin-bottom: 30px;
183
+ box-shadow: 0 8px 32px rgba(0,0,0,0.3);
184
+ }
185
+
186
+ .header h1 {
187
+ font-size: 32px;
188
+ font-weight: 700;
189
+ color: white;
190
+ margin-bottom: 20px;
191
+ text-shadow: 0 2px 4px rgba(0,0,0,0.2);
192
+ }
193
+
194
+ .header-stats {
195
+ display: grid;
196
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
197
+ gap: 20px;
198
+ }
199
+
200
+ .stat-card {
201
+ background: rgba(255, 255, 255, 0.1);
202
+ backdrop-filter: blur(10px);
203
+ padding: 20px;
204
+ border-radius: 8px;
205
+ border: 1px solid rgba(255, 255, 255, 0.2);
206
+ }
207
+
208
+ .stat-label {
209
+ color: rgba(255, 255, 255, 0.8);
210
+ font-size: 12px;
211
+ text-transform: uppercase;
212
+ letter-spacing: 1px;
213
+ margin-bottom: 8px;
214
+ }
215
+
216
+ .stat-value {
217
+ color: white;
218
+ font-weight: 700;
219
+ font-size: 24px;
220
+ }
221
+
222
+ .section {
223
+ background: #252526;
224
+ padding: 30px;
225
+ border-radius: 12px;
226
+ margin-bottom: 30px;
227
+ border: 1px solid #3e3e42;
228
+ box-shadow: 0 4px 16px rgba(0,0,0,0.2);
229
+ }
230
+
231
+ .section h2 {
232
+ font-size: 22px;
233
+ margin-bottom: 24px;
234
+ color: #e0e0e0;
235
+ font-weight: 600;
236
+ }
237
+
238
+ table {
239
+ width: 100%;
240
+ border-collapse: collapse;
241
+ }
242
+
243
+ th, td {
244
+ padding: 14px 12px;
245
+ text-align: left;
246
+ border-bottom: 1px solid #3e3e42;
247
+ }
248
+
249
+ th {
250
+ background: #1e1e1e;
251
+ color: #888;
252
+ font-size: 11px;
253
+ text-transform: uppercase;
254
+ letter-spacing: 0.8px;
255
+ font-weight: 600;
256
+ position: sticky;
257
+ top: 0;
258
+ z-index: 10;
259
+ }
260
+
261
+ td {
262
+ color: #e0e0e0;
263
+ font-size: 13px;
264
+ }
265
+
266
+ tr:hover {
267
+ background: #2a2a2a;
268
+ }
269
+
270
+ .asset-name {
271
+ font-family: 'Monaco', 'Menlo', monospace;
272
+ font-size: 12px;
273
+ max-width: 300px;
274
+ overflow: hidden;
275
+ text-overflow: ellipsis;
276
+ white-space: nowrap;
277
+ }
278
+
279
+ .size-cell {
280
+ font-family: 'Monaco', 'Menlo', monospace;
281
+ font-size: 12px;
282
+ color: #4ec9b0;
283
+ }
284
+
285
+ .compression-cell {
286
+ text-align: center;
287
+ }
288
+
289
+ .compression-badge {
290
+ display: inline-block;
291
+ padding: 4px 12px;
292
+ border-radius: 12px;
293
+ font-weight: 600;
294
+ font-size: 12px;
295
+ }
296
+
297
+ .compression-good .compression-badge {
298
+ background: rgba(46, 204, 113, 0.2);
299
+ color: #2ecc71;
300
+ border: 1px solid #2ecc71;
301
+ }
302
+
303
+ .compression-medium .compression-badge {
304
+ background: rgba(241, 196, 15, 0.2);
305
+ color: #f1c40f;
306
+ border: 1px solid #f1c40f;
307
+ }
308
+
309
+ .compression-poor .compression-badge {
310
+ background: rgba(231, 76, 60, 0.2);
311
+ color: #e74c3c;
312
+ border: 1px solid #e74c3c;
313
+ }
314
+
315
+ .optimizations-cell {
316
+ font-size: 11px;
317
+ color: #888;
318
+ max-width: 200px;
319
+ overflow: hidden;
320
+ text-overflow: ellipsis;
321
+ white-space: nowrap;
322
+ }
323
+
324
+ .bar {
325
+ height: 24px;
326
+ background: linear-gradient(90deg, #667eea, #764ba2);
327
+ border-radius: 4px;
328
+ min-width: 2px;
329
+ transition: all 0.3s;
330
+ }
331
+
332
+ .bar:hover {
333
+ transform: scaleY(1.1);
334
+ }
335
+
336
+ /* 优化建议样式 */
337
+ .optimization-suggestions {
338
+ background: linear-gradient(135deg, #252526 0%, #2a2a2a 100%);
339
+ }
340
+
341
+ .optimization-summary {
342
+ display: grid;
343
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
344
+ gap: 20px;
345
+ margin-bottom: 30px;
346
+ }
347
+
348
+ .summary-card {
349
+ background: #1e1e1e;
350
+ padding: 20px;
351
+ border-radius: 8px;
352
+ border: 1px solid #3e3e42;
353
+ text-align: center;
354
+ }
355
+
356
+ .summary-label {
357
+ color: #888;
358
+ font-size: 12px;
359
+ text-transform: uppercase;
360
+ letter-spacing: 0.5px;
361
+ margin-bottom: 10px;
362
+ }
363
+
364
+ .summary-value {
365
+ color: #4ec9b0;
366
+ font-weight: 700;
367
+ font-size: 28px;
368
+ }
369
+
370
+ .suggestions-list {
371
+ display: flex;
372
+ flex-direction: column;
373
+ gap: 20px;
374
+ }
375
+
376
+ .suggestion-card {
377
+ background: #1e1e1e;
378
+ padding: 24px;
379
+ border-radius: 8px;
380
+ border-left: 4px solid;
381
+ transition: all 0.3s;
382
+ }
383
+
384
+ .suggestion-card:hover {
385
+ transform: translateX(4px);
386
+ box-shadow: 0 4px 16px rgba(0,0,0,0.3);
387
+ }
388
+
389
+ .suggestion-card.severity-high {
390
+ border-left-color: #e74c3c;
391
+ }
392
+
393
+ .suggestion-card.severity-medium {
394
+ border-left-color: #f1c40f;
395
+ }
396
+
397
+ .suggestion-card.severity-low {
398
+ border-left-color: #2ecc71;
399
+ }
400
+
401
+ .suggestion-header {
402
+ display: flex;
403
+ align-items: center;
404
+ gap: 12px;
405
+ margin-bottom: 12px;
406
+ flex-wrap: wrap;
407
+ }
408
+
409
+ .suggestion-severity {
410
+ font-weight: 600;
411
+ font-size: 13px;
412
+ }
413
+
414
+ .suggestion-type {
415
+ background: #3e3e42;
416
+ padding: 4px 10px;
417
+ border-radius: 4px;
418
+ font-size: 11px;
419
+ color: #888;
420
+ text-transform: uppercase;
421
+ letter-spacing: 0.5px;
422
+ }
423
+
424
+ .suggestion-savings {
425
+ margin-left: auto;
426
+ color: #2ecc71;
427
+ font-weight: 600;
428
+ font-size: 13px;
429
+ }
430
+
431
+ .suggestion-description {
432
+ color: #e0e0e0;
433
+ margin-bottom: 12px;
434
+ font-size: 14px;
435
+ }
436
+
437
+ .suggestion-recommendation {
438
+ color: #888;
439
+ font-size: 13px;
440
+ margin-bottom: 12px;
441
+ line-height: 1.6;
442
+ }
443
+
444
+ .suggestion-recommendation strong {
445
+ color: #4ec9b0;
446
+ }
447
+
448
+ .suggestion-details {
449
+ margin-top: 12px;
450
+ cursor: pointer;
451
+ }
452
+
453
+ .suggestion-details summary {
454
+ color: #4ec9b0;
455
+ font-size: 12px;
456
+ font-weight: 600;
457
+ padding: 8px 0;
458
+ user-select: none;
459
+ }
460
+
461
+ .suggestion-details summary:hover {
462
+ color: #6ee7b7;
463
+ }
464
+
465
+ .affected-assets {
466
+ list-style: none;
467
+ padding: 12px;
468
+ background: #252526;
469
+ border-radius: 4px;
470
+ margin-top: 8px;
471
+ }
472
+
473
+ .affected-assets li {
474
+ padding: 4px 0;
475
+ color: #888;
476
+ font-size: 12px;
477
+ font-family: 'Monaco', 'Menlo', monospace;
478
+ }
479
+
480
+ .config-example {
481
+ background: #252526;
482
+ padding: 16px;
483
+ border-radius: 4px;
484
+ margin-top: 8px;
485
+ overflow-x: auto;
486
+ }
487
+
488
+ .config-example code {
489
+ color: #4ec9b0;
490
+ font-family: 'Monaco', 'Menlo', monospace;
491
+ font-size: 12px;
492
+ line-height: 1.6;
493
+ }
494
+
495
+ .no-suggestions {
496
+ text-align: center;
497
+ padding: 40px;
498
+ color: #2ecc71;
499
+ font-size: 18px;
500
+ }
501
+
502
+ /* 树状图样式 */
503
+ .treemap-container {
504
+ background: #252526;
505
+ padding: 30px;
506
+ border-radius: 12px;
507
+ margin-bottom: 30px;
508
+ border: 1px solid #3e3e42;
509
+ }
510
+
511
+ .treemap {
512
+ width: 100%;
513
+ min-height: 600px;
514
+ position: relative;
515
+ background: #1e1e1e;
516
+ border-radius: 8px;
517
+ overflow: hidden;
518
+ }
519
+
520
+ .treemap-block {
521
+ position: absolute;
522
+ border: 2px solid #1e1e1e;
523
+ cursor: pointer;
524
+ transition: all 0.2s;
525
+ overflow: hidden;
526
+ display: flex;
527
+ flex-direction: column;
528
+ justify-content: center;
529
+ align-items: center;
530
+ padding: 8px;
531
+ }
532
+
533
+ .treemap-block:hover {
534
+ border-color: #fff;
535
+ z-index: 10;
536
+ transform: scale(1.02);
537
+ box-shadow: 0 4px 12px rgba(0,0,0,0.5);
538
+ }
539
+
540
+ .treemap-block-label {
541
+ font-size: 11px;
542
+ font-weight: 600;
543
+ color: white;
544
+ text-align: center;
545
+ text-shadow: 0 1px 2px rgba(0,0,0,0.8);
546
+ word-break: break-word;
547
+ line-height: 1.3;
548
+ }
549
+
550
+ .treemap-block-size {
551
+ font-size: 10px;
552
+ color: rgba(255,255,255,0.8);
553
+ text-shadow: 0 1px 2px rgba(0,0,0,0.8);
554
+ margin-top: 4px;
555
+ }
556
+
557
+ .treemap-block-compression {
558
+ font-size: 9px;
559
+ color: rgba(255,255,255,0.7);
560
+ text-shadow: 0 1px 2px rgba(0,0,0,0.8);
561
+ margin-top: 2px;
562
+ }
563
+
564
+ /* Tooltip */
565
+ .tooltip {
566
+ position: fixed;
567
+ background: rgba(0, 0, 0, 0.95);
568
+ color: white;
569
+ padding: 16px;
570
+ border-radius: 8px;
571
+ font-size: 12px;
572
+ pointer-events: none;
573
+ z-index: 1000;
574
+ display: none;
575
+ border: 1px solid #555;
576
+ box-shadow: 0 8px 24px rgba(0,0,0,0.5);
577
+ max-width: 400px;
578
+ backdrop-filter: blur(10px);
579
+ }
580
+
581
+ .tooltip-title {
582
+ font-weight: 600;
583
+ margin-bottom: 12px;
584
+ color: #4ec9b0;
585
+ font-size: 14px;
586
+ border-bottom: 1px solid #3e3e42;
587
+ padding-bottom: 8px;
588
+ }
589
+
590
+ .tooltip-row {
591
+ display: flex;
592
+ justify-content: space-between;
593
+ margin: 6px 0;
594
+ gap: 20px;
595
+ }
596
+
597
+ .tooltip-row span:first-child {
598
+ color: #888;
599
+ }
600
+
601
+ .tooltip-row span:last-child {
602
+ color: #e0e0e0;
603
+ font-weight: 600;
604
+ }
605
+
606
+ .tooltip-section {
607
+ margin-top: 12px;
608
+ padding-top: 12px;
609
+ border-top: 1px solid #3e3e42;
610
+ }
611
+
612
+ .tooltip-section-title {
613
+ color: #888;
614
+ font-size: 11px;
615
+ text-transform: uppercase;
616
+ letter-spacing: 0.5px;
617
+ margin-bottom: 6px;
618
+ }
619
+
620
+ .tooltip-steps {
621
+ list-style: none;
622
+ padding-left: 0;
623
+ }
624
+
625
+ .tooltip-steps li {
626
+ padding: 4px 0;
627
+ color: #888;
628
+ font-size: 11px;
629
+ }
630
+
631
+ /* 响应式 */
632
+ @media (max-width: 768px) {
633
+ .header-stats {
634
+ grid-template-columns: 1fr;
635
+ }
636
+
637
+ .optimization-summary {
638
+ grid-template-columns: 1fr;
639
+ }
640
+
641
+ table {
642
+ font-size: 11px;
643
+ }
644
+
645
+ th, td {
646
+ padding: 8px 6px;
647
+ }
648
+ }
649
+ </style>
650
+ </head>
651
+ <body>
652
+ <div class="container">
653
+ <div class="header">
654
+ <h1>📊 构建分析报告</h1>
655
+ <div class="header-stats">
656
+ <div class="stat-card">
657
+ <div class="stat-label">资源总数</div>
658
+ <div class="stat-value">${data.totalAssets}</div>
659
+ </div>
660
+ <div class="stat-card">
661
+ <div class="stat-label">原始大小</div>
662
+ <div class="stat-value">${formatBytes(data.totalOriginalSize)}</div>
663
+ </div>
664
+ <div class="stat-card">
665
+ <div class="stat-label">最终大小</div>
666
+ <div class="stat-value">${formatBytes(data.totalFinalSize)}</div>
667
+ </div>
668
+ <div class="stat-card">
669
+ <div class="stat-label">总压缩率</div>
670
+ <div class="stat-value">${(data.totalCompressionRatio * 100).toFixed(1)}%</div>
671
+ </div>
672
+ </div>
673
+ </div>
674
+
675
+ ${optimizationSection}
676
+
677
+ <div class="treemap-container">
678
+ <h2>🗺️ 资源分布图</h2>
679
+ <div class="treemap" id="treemap"></div>
680
+ </div>
681
+
682
+ <div class="section">
683
+ <h2>📦 按类型统计</h2>
684
+ <table>
685
+ <thead>
686
+ <tr>
687
+ <th>类型</th>
688
+ <th>数量</th>
689
+ <th>原始大小</th>
690
+ <th>最终大小</th>
691
+ <th>压缩率</th>
692
+ <th>占比</th>
693
+ </tr>
694
+ </thead>
695
+ <tbody>
696
+ ${typeRows}
697
+ </tbody>
698
+ </table>
699
+ </div>
700
+
701
+ <div class="section">
702
+ <h2>📂 按分类统计</h2>
703
+ <table>
704
+ <thead>
705
+ <tr>
706
+ <th>分类</th>
707
+ <th>数量</th>
708
+ <th>大小</th>
709
+ <th>占比</th>
710
+ <th>可视化</th>
711
+ </tr>
712
+ </thead>
713
+ <tbody>
714
+ ${categoryRows}
715
+ </tbody>
716
+ </table>
717
+ </div>
718
+
719
+ <div class="section">
720
+ <h2>🔝 Top 50 资源</h2>
721
+ <table>
722
+ <thead>
723
+ <tr>
724
+ <th>资源名称</th>
725
+ <th>类型</th>
726
+ <th>原始大小</th>
727
+ <th>最终大小</th>
728
+ <th>压缩率</th>
729
+ <th>优化</th>
730
+ <th>位置</th>
731
+ </tr>
732
+ </thead>
733
+ <tbody>
734
+ ${assetRows}
735
+ </tbody>
736
+ </table>
737
+ </div>
738
+ </div>
739
+
740
+ <div class="tooltip" id="tooltip"></div>
741
+
742
+ <script>
743
+ // 嵌入数据
744
+ const reportData = ${JSON.stringify(data, null, 2)};
745
+
746
+ // 压缩率颜色映射
747
+ const getCompressionColor = (ratio) => {
748
+ if (ratio > 0.5) return '#2ecc71';
749
+ if (ratio > 0.2) return '#f1c40f';
750
+ return '#e74c3c';
751
+ };
752
+
753
+ // 类型颜色映射
754
+ const typeColors = {
755
+ 'script': '#f39c12',
756
+ 'texture': '#9b59b6',
757
+ 'audio': '#3498db',
758
+ 'model': '#e74c3c',
759
+ 'json': '#2ecc71',
760
+ 'html': '#1abc9c',
761
+ 'other': '#95a5a6'
762
+ };
763
+
764
+ // 树状图布局算法
765
+ const layoutTreemap = (items, x, y, width, height) => {
766
+ const sortedItems = [...items].sort((a, b) => b.value - a.value);
767
+ const total = sortedItems.reduce((sum, item) => sum + item.value, 0);
768
+ if (total === 0) return [];
769
+
770
+ const blocks = [];
771
+ let currentY = y;
772
+ let rowItems = [];
773
+ let rowSize = 0;
774
+
775
+ sortedItems.forEach((item, index) => {
776
+ rowItems.push(item);
777
+ rowSize += item.value;
778
+
779
+ const shouldBreak = rowItems.length >= 10 || index === sortedItems.length - 1;
780
+
781
+ if (shouldBreak) {
782
+ const rowRatio = rowSize / total;
783
+ const rowHeight = height * rowRatio;
784
+
785
+ let offsetX = x;
786
+ rowItems.forEach(i => {
787
+ const iWidth = (i.value / rowSize) * width;
788
+
789
+ blocks.push({
790
+ ...i,
791
+ x: offsetX,
792
+ y: currentY,
793
+ width: Math.max(iWidth, 1),
794
+ height: Math.max(rowHeight, 1)
795
+ });
796
+
797
+ offsetX += iWidth;
798
+ });
799
+
800
+ currentY += rowHeight;
801
+ rowItems = [];
802
+ rowSize = 0;
803
+ }
804
+ });
805
+
806
+ return blocks;
807
+ };
808
+
809
+ // 渲染树状图
810
+ const renderTreemap = () => {
811
+ const container = document.getElementById('treemap');
812
+ const width = container.clientWidth;
813
+ const height = 600;
814
+
815
+ container.innerHTML = '';
816
+ container.style.height = height + 'px';
817
+
818
+ const items = reportData.assets.map(asset => ({
819
+ ...asset,
820
+ value: asset.finalSize
821
+ }));
822
+
823
+ const blocks = layoutTreemap(items, 0, 0, width, height);
824
+
825
+ blocks.forEach(block => {
826
+ const div = document.createElement('div');
827
+ div.className = 'treemap-block';
828
+ div.style.left = block.x + 'px';
829
+ div.style.top = block.y + 'px';
830
+ div.style.width = block.width + 'px';
831
+ div.style.height = block.height + 'px';
832
+
833
+ // 根据压缩率设置颜色
834
+ const baseColor = typeColors[block.type] || typeColors.other;
835
+ const compressionAlpha = 0.5 + (block.compressionRatio * 0.5);
836
+ div.style.background = baseColor;
837
+ div.style.opacity = compressionAlpha;
838
+
839
+ const showLabel = block.width > 80 && block.height > 50;
840
+
841
+ if (showLabel) {
842
+ const label = document.createElement('div');
843
+ label.className = 'treemap-block-label';
844
+ label.textContent = block.originalName;
845
+ div.appendChild(label);
846
+
847
+ const size = document.createElement('div');
848
+ size.className = 'treemap-block-size';
849
+ size.textContent = \`\${formatBytes(block.finalSize)}\`;
850
+ div.appendChild(size);
851
+
852
+ const compression = document.createElement('div');
853
+ compression.className = 'treemap-block-compression';
854
+ compression.textContent = \`压缩 \${(block.compressionRatio * 100).toFixed(0)}%\`;
855
+ div.appendChild(compression);
856
+ }
857
+
858
+ // Tooltip
859
+ div.addEventListener('mouseenter', (e) => {
860
+ const tooltip = document.getElementById('tooltip');
861
+
862
+ let stepsHtml = '';
863
+ if (block.processingSteps && block.processingSteps.length > 0) {
864
+ stepsHtml = \`
865
+ <div class="tooltip-section">
866
+ <div class="tooltip-section-title">处理流程</div>
867
+ <ul class="tooltip-steps">
868
+ \${block.processingSteps.map(step => \`
869
+ <li>\${step.stage}: \${formatBytes(step.size)} \${step.compressionRatio ? '(-' + (step.compressionRatio * 100).toFixed(0) + '%)' : ''}</li>
870
+ \`).join('')}
871
+ </ul>
872
+ </div>
873
+ \`;
874
+ }
875
+
876
+ tooltip.innerHTML = \`
877
+ <div class="tooltip-title">\${block.originalName}</div>
878
+ <div class="tooltip-row">
879
+ <span>类型:</span>
880
+ <span>\${block.type}</span>
881
+ </div>
882
+ <div class="tooltip-row">
883
+ <span>原始大小:</span>
884
+ <span>\${formatBytes(block.originalSize)}</span>
885
+ </div>
886
+ <div class="tooltip-row">
887
+ <span>最终大小:</span>
888
+ <span>\${formatBytes(block.finalSize)}</span>
889
+ </div>
890
+ <div class="tooltip-row">
891
+ <span>压缩率:</span>
892
+ <span style="color: \${getCompressionColor(block.compressionRatio)}">\${(block.compressionRatio * 100).toFixed(1)}%</span>
893
+ </div>
894
+ <div class="tooltip-row">
895
+ <span>优化:</span>
896
+ <span>\${block.optimizations.join(', ') || '无'}</span>
897
+ </div>
898
+ <div class="tooltip-row">
899
+ <span>位置:</span>
900
+ <span>\${block.inlined ? '内联' : '外部'}</span>
901
+ </div>
902
+ \${stepsHtml}
903
+ \`;
904
+
905
+ tooltip.style.display = 'block';
906
+ tooltip.style.left = (e.clientX + 15) + 'px';
907
+ tooltip.style.top = (e.clientY + 15) + 'px';
908
+ });
909
+
910
+ div.addEventListener('mousemove', (e) => {
911
+ const tooltip = document.getElementById('tooltip');
912
+ tooltip.style.left = (e.clientX + 15) + 'px';
913
+ tooltip.style.top = (e.clientY + 15) + 'px';
914
+ });
915
+
916
+ div.addEventListener('mouseleave', () => {
917
+ document.getElementById('tooltip').style.display = 'none';
918
+ });
919
+
920
+ container.appendChild(div);
921
+ });
922
+ };
923
+
924
+ // 格式化字节数
925
+ function formatBytes(bytes) {
926
+ if (bytes === 0) return '0 B';
927
+ const k = 1024;
928
+ const sizes = ['B', 'KB', 'MB', 'GB'];
929
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
930
+ return \`\${(bytes / Math.pow(k, i)).toFixed(2)} \${sizes[i]}\`;
931
+ }
932
+
933
+ // 初始化
934
+ renderTreemap();
935
+
936
+ // 响应式调整
937
+ let resizeTimeout;
938
+ window.addEventListener('resize', () => {
939
+ clearTimeout(resizeTimeout);
940
+ resizeTimeout = setTimeout(renderTreemap, 300);
941
+ });
942
+
943
+ // 资产行点击事件
944
+ document.querySelectorAll('.asset-row').forEach(row => {
945
+ row.addEventListener('click', () => {
946
+ const assetId = row.dataset.assetId;
947
+ const asset = reportData.assets.find(a => a.id === assetId);
948
+ if (asset) {
949
+ console.log('Asset details:', asset);
950
+ // 可以在这里添加更多交互逻辑
951
+ }
952
+ });
953
+ });
954
+ </script>
955
+ </body>
956
+ </html>`;
957
+ };