dash-devtools 1.0.0__py3-none-any.whl
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.
- dash_devtools/__init__.py +8 -0
- dash_devtools/__main__.py +11 -0
- dash_devtools/ai_engine.py +441 -0
- dash_devtools/browser.py +541 -0
- dash_devtools/cli.py +1452 -0
- dash_devtools/database.py +338 -0
- dash_devtools/dbdiagram.py +183 -0
- dash_devtools/e2e.py +329 -0
- dash_devtools/fixers/__init__.py +57 -0
- dash_devtools/fixers/migration_fixer.py +115 -0
- dash_devtools/fixers/ux_fixer.py +106 -0
- dash_devtools/fixers/version_bumper.py +115 -0
- dash_devtools/gas_mes_test.py +1241 -0
- dash_devtools/generators/__init__.py +84 -0
- dash_devtools/health.py +476 -0
- dash_devtools/hooks/__init__.py +250 -0
- dash_devtools/hooks/pre_commit.py +161 -0
- dash_devtools/hooks/pre_push.py +275 -0
- dash_devtools/init_test.py +352 -0
- dash_devtools/markdown_report.py +309 -0
- dash_devtools/migrators/__init__.py +21 -0
- dash_devtools/perf.py +321 -0
- dash_devtools/report.py +667 -0
- dash_devtools/reporters/__init__.py +11 -0
- dash_devtools/spec.py +230 -0
- dash_devtools/stats.py +355 -0
- dash_devtools/test_suite.py +690 -0
- dash_devtools/testing.py +416 -0
- dash_devtools/validators/__init__.py +157 -0
- dash_devtools/validators/backend/__init__.py +12 -0
- dash_devtools/validators/backend/nodejs.py +245 -0
- dash_devtools/validators/backend/python.py +439 -0
- dash_devtools/validators/code_quality.py +243 -0
- dash_devtools/validators/common/__init__.py +11 -0
- dash_devtools/validators/common/quality.py +319 -0
- dash_devtools/validators/common/security.py +270 -0
- dash_devtools/validators/common/spec.py +273 -0
- dash_devtools/validators/detector.py +394 -0
- dash_devtools/validators/frontend/__init__.py +14 -0
- dash_devtools/validators/frontend/angular.py +245 -0
- dash_devtools/validators/frontend/gas.py +310 -0
- dash_devtools/validators/frontend/vite.py +539 -0
- dash_devtools/validators/migration.py +292 -0
- dash_devtools/validators/performance.py +167 -0
- dash_devtools/validators/security.py +205 -0
- dash_devtools/vision/__init__.py +368 -0
- dash_devtools/watch.py +266 -0
- dash_devtools/word_report.py +690 -0
- dash_devtools-1.0.0.dist-info/METADATA +834 -0
- dash_devtools-1.0.0.dist-info/RECORD +53 -0
- dash_devtools-1.0.0.dist-info/WHEEL +5 -0
- dash_devtools-1.0.0.dist-info/entry_points.txt +2 -0
- dash_devtools-1.0.0.dist-info/top_level.txt +1 -0
dash_devtools/report.py
ADDED
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
"""
|
|
2
|
+
整合報告產生器
|
|
3
|
+
|
|
4
|
+
產生完整的專案報告,包含:
|
|
5
|
+
- 健康評分
|
|
6
|
+
- 程式碼統計
|
|
7
|
+
- UI 截圖
|
|
8
|
+
- 測試結果
|
|
9
|
+
- HTML 報告輸出
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import subprocess
|
|
14
|
+
import base64
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from typing import Dict, List, Optional
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
from rich.panel import Panel
|
|
21
|
+
from rich.text import Text
|
|
22
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
23
|
+
|
|
24
|
+
console = Console()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class TestResult:
|
|
29
|
+
"""測試結果"""
|
|
30
|
+
framework: str # pytest, jest, vitest, etc.
|
|
31
|
+
passed: int = 0
|
|
32
|
+
failed: int = 0
|
|
33
|
+
skipped: int = 0
|
|
34
|
+
duration: float = 0.0
|
|
35
|
+
output: str = ""
|
|
36
|
+
success: bool = True
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class ScreenshotResult:
|
|
41
|
+
"""截圖結果"""
|
|
42
|
+
url: str
|
|
43
|
+
path: str
|
|
44
|
+
success: bool = True
|
|
45
|
+
error: str = ""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class ReportData:
|
|
50
|
+
"""報告資料"""
|
|
51
|
+
project_name: str
|
|
52
|
+
generated_at: str
|
|
53
|
+
health_scores: Dict = field(default_factory=dict)
|
|
54
|
+
stats: Dict = field(default_factory=dict)
|
|
55
|
+
test_result: Optional[TestResult] = None
|
|
56
|
+
screenshots: List[ScreenshotResult] = field(default_factory=list)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ReportGenerator:
|
|
60
|
+
"""報告產生器"""
|
|
61
|
+
|
|
62
|
+
def __init__(self, project_path: str):
|
|
63
|
+
self.project_path = Path(project_path)
|
|
64
|
+
self.project_name = self.project_path.name
|
|
65
|
+
self.report_dir = self.project_path / 'reports'
|
|
66
|
+
self.report_data = ReportData(
|
|
67
|
+
project_name=self.project_name,
|
|
68
|
+
generated_at=datetime.now().isoformat()
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def collect_health(self) -> Dict:
|
|
72
|
+
"""收集健康評分"""
|
|
73
|
+
from .health import HealthChecker
|
|
74
|
+
|
|
75
|
+
checker = HealthChecker(str(self.project_path))
|
|
76
|
+
scores = checker.check_all()
|
|
77
|
+
|
|
78
|
+
health_data = {
|
|
79
|
+
'total_score': sum(s.score for s in scores.values()) // len(scores),
|
|
80
|
+
'scores': {}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for key, score in scores.items():
|
|
84
|
+
health_data['scores'][key] = {
|
|
85
|
+
'score': score.score,
|
|
86
|
+
'category': score.category,
|
|
87
|
+
'issues': score.issues,
|
|
88
|
+
'recommendations': score.recommendations
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
self.report_data.health_scores = health_data
|
|
92
|
+
return health_data
|
|
93
|
+
|
|
94
|
+
def collect_stats(self) -> Dict:
|
|
95
|
+
"""收集程式碼統計"""
|
|
96
|
+
from .stats import StatsCollector
|
|
97
|
+
|
|
98
|
+
collector = StatsCollector(str(self.project_path))
|
|
99
|
+
stats = collector.collect()
|
|
100
|
+
|
|
101
|
+
stats_data = {
|
|
102
|
+
'total_files': stats.total_files,
|
|
103
|
+
'total_lines': stats.total_lines,
|
|
104
|
+
'total_code_lines': stats.total_code_lines,
|
|
105
|
+
'size_bytes': stats.total_size_bytes,
|
|
106
|
+
'languages': {},
|
|
107
|
+
'largest_files': stats.largest_files[:5],
|
|
108
|
+
'complexity_issues': stats.complexity_issues
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for name, lang in stats.languages.items():
|
|
112
|
+
stats_data['languages'][name] = {
|
|
113
|
+
'files': lang.files,
|
|
114
|
+
'lines': lang.lines
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
self.report_data.stats = stats_data
|
|
118
|
+
return stats_data
|
|
119
|
+
|
|
120
|
+
def run_tests(self) -> TestResult:
|
|
121
|
+
"""執行測試"""
|
|
122
|
+
result = TestResult(framework='unknown')
|
|
123
|
+
|
|
124
|
+
# 偵測測試框架
|
|
125
|
+
package_json = self.project_path / 'package.json'
|
|
126
|
+
if package_json.exists():
|
|
127
|
+
try:
|
|
128
|
+
pkg = json.loads(package_json.read_text())
|
|
129
|
+
deps = {**pkg.get('dependencies', {}), **pkg.get('devDependencies', {})}
|
|
130
|
+
scripts = pkg.get('scripts', {})
|
|
131
|
+
|
|
132
|
+
# 判斷測試框架
|
|
133
|
+
if 'vitest' in deps:
|
|
134
|
+
result.framework = 'vitest'
|
|
135
|
+
elif 'jest' in deps:
|
|
136
|
+
result.framework = 'jest'
|
|
137
|
+
elif '@angular-devkit/build-angular' in deps:
|
|
138
|
+
result.framework = 'karma'
|
|
139
|
+
|
|
140
|
+
# 執行測試
|
|
141
|
+
if 'test' in scripts:
|
|
142
|
+
try:
|
|
143
|
+
proc = subprocess.run(
|
|
144
|
+
['npm', 'test', '--', '--passWithNoTests', '--run'],
|
|
145
|
+
cwd=self.project_path,
|
|
146
|
+
capture_output=True,
|
|
147
|
+
text=True,
|
|
148
|
+
timeout=120
|
|
149
|
+
)
|
|
150
|
+
result.output = proc.stdout + proc.stderr
|
|
151
|
+
result.success = proc.returncode == 0
|
|
152
|
+
|
|
153
|
+
# 簡單解析結果
|
|
154
|
+
if 'passed' in result.output.lower():
|
|
155
|
+
result.passed = result.output.lower().count('passed')
|
|
156
|
+
if 'failed' in result.output.lower():
|
|
157
|
+
result.failed = result.output.lower().count('failed')
|
|
158
|
+
|
|
159
|
+
except subprocess.TimeoutExpired:
|
|
160
|
+
result.success = False
|
|
161
|
+
result.output = "測試超時 (120秒)"
|
|
162
|
+
except Exception as e:
|
|
163
|
+
result.success = False
|
|
164
|
+
result.output = str(e)
|
|
165
|
+
|
|
166
|
+
except Exception:
|
|
167
|
+
pass
|
|
168
|
+
|
|
169
|
+
# Python 專案
|
|
170
|
+
requirements = self.project_path / 'requirements.txt'
|
|
171
|
+
pytest_ini = self.project_path / 'pytest.ini'
|
|
172
|
+
if requirements.exists() or pytest_ini.exists():
|
|
173
|
+
result.framework = 'pytest'
|
|
174
|
+
try:
|
|
175
|
+
proc = subprocess.run(
|
|
176
|
+
['python', '-m', 'pytest', '--tb=short', '-q'],
|
|
177
|
+
cwd=self.project_path,
|
|
178
|
+
capture_output=True,
|
|
179
|
+
text=True,
|
|
180
|
+
timeout=120
|
|
181
|
+
)
|
|
182
|
+
result.output = proc.stdout + proc.stderr
|
|
183
|
+
result.success = proc.returncode == 0
|
|
184
|
+
|
|
185
|
+
# 解析 pytest 結果
|
|
186
|
+
import re
|
|
187
|
+
match = re.search(r'(\d+) passed', result.output)
|
|
188
|
+
if match:
|
|
189
|
+
result.passed = int(match.group(1))
|
|
190
|
+
match = re.search(r'(\d+) failed', result.output)
|
|
191
|
+
if match:
|
|
192
|
+
result.failed = int(match.group(1))
|
|
193
|
+
|
|
194
|
+
except Exception as e:
|
|
195
|
+
result.output = str(e)
|
|
196
|
+
result.success = False
|
|
197
|
+
|
|
198
|
+
self.report_data.test_result = result
|
|
199
|
+
return result
|
|
200
|
+
|
|
201
|
+
def take_screenshots(self, urls: List[str] = None) -> List[ScreenshotResult]:
|
|
202
|
+
"""使用 agent-browser 截圖"""
|
|
203
|
+
import shutil
|
|
204
|
+
results = []
|
|
205
|
+
|
|
206
|
+
# 檢查 agent-browser 是否安裝
|
|
207
|
+
if not shutil.which('agent-browser'):
|
|
208
|
+
console.print("[yellow]agent-browser 未安裝,跳過截圖[/yellow]")
|
|
209
|
+
console.print("[dim]安裝方式: npm install -g agent-browser && agent-browser install[/dim]")
|
|
210
|
+
return results
|
|
211
|
+
|
|
212
|
+
# 如果沒指定 URL,嘗試偵測本地開發伺服器
|
|
213
|
+
if not urls:
|
|
214
|
+
package_json = self.project_path / 'package.json'
|
|
215
|
+
if package_json.exists():
|
|
216
|
+
try:
|
|
217
|
+
pkg = json.loads(package_json.read_text())
|
|
218
|
+
scripts = pkg.get('scripts', {})
|
|
219
|
+
if 'dev' in scripts or 'start' in scripts:
|
|
220
|
+
urls = ['http://localhost:5173', 'http://localhost:3000', 'http://localhost:4200']
|
|
221
|
+
except Exception:
|
|
222
|
+
pass
|
|
223
|
+
|
|
224
|
+
if not urls:
|
|
225
|
+
return results
|
|
226
|
+
|
|
227
|
+
# 確保報告目錄存在
|
|
228
|
+
screenshots_dir = self.report_dir / 'screenshots'
|
|
229
|
+
screenshots_dir.mkdir(parents=True, exist_ok=True)
|
|
230
|
+
|
|
231
|
+
# 使用 agent-browser 截圖
|
|
232
|
+
for url in urls:
|
|
233
|
+
screenshot_path = screenshots_dir / f"screenshot_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
# 開啟頁面
|
|
237
|
+
open_result = subprocess.run(
|
|
238
|
+
['agent-browser', 'open', url],
|
|
239
|
+
capture_output=True,
|
|
240
|
+
text=True,
|
|
241
|
+
timeout=30
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
if open_result.returncode != 0:
|
|
245
|
+
results.append(ScreenshotResult(
|
|
246
|
+
url=url,
|
|
247
|
+
path="",
|
|
248
|
+
success=False,
|
|
249
|
+
error=f"無法開啟頁面: {open_result.stderr}"
|
|
250
|
+
))
|
|
251
|
+
subprocess.run(['agent-browser', 'close'], capture_output=True)
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
# 等待頁面載入
|
|
255
|
+
subprocess.run(
|
|
256
|
+
['agent-browser', 'wait', '--load', 'networkidle'],
|
|
257
|
+
capture_output=True,
|
|
258
|
+
timeout=15
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# 額外等待 JS 渲染
|
|
262
|
+
subprocess.run(
|
|
263
|
+
['agent-browser', 'wait', '2000'],
|
|
264
|
+
capture_output=True,
|
|
265
|
+
timeout=5
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# 截圖
|
|
269
|
+
screenshot_result = subprocess.run(
|
|
270
|
+
['agent-browser', 'screenshot', str(screenshot_path), '--full'],
|
|
271
|
+
capture_output=True,
|
|
272
|
+
text=True,
|
|
273
|
+
timeout=30
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# 關閉瀏覽器
|
|
277
|
+
subprocess.run(['agent-browser', 'close'], capture_output=True)
|
|
278
|
+
|
|
279
|
+
if screenshot_result.returncode == 0 and screenshot_path.exists():
|
|
280
|
+
results.append(ScreenshotResult(
|
|
281
|
+
url=url,
|
|
282
|
+
path=str(screenshot_path),
|
|
283
|
+
success=True
|
|
284
|
+
))
|
|
285
|
+
else:
|
|
286
|
+
results.append(ScreenshotResult(
|
|
287
|
+
url=url,
|
|
288
|
+
path="",
|
|
289
|
+
success=False,
|
|
290
|
+
error=screenshot_result.stderr or "截圖失敗"
|
|
291
|
+
))
|
|
292
|
+
|
|
293
|
+
except subprocess.TimeoutExpired:
|
|
294
|
+
subprocess.run(['agent-browser', 'close'], capture_output=True)
|
|
295
|
+
results.append(ScreenshotResult(
|
|
296
|
+
url=url,
|
|
297
|
+
path="",
|
|
298
|
+
success=False,
|
|
299
|
+
error="操作超時"
|
|
300
|
+
))
|
|
301
|
+
except Exception as e:
|
|
302
|
+
subprocess.run(['agent-browser', 'close'], capture_output=True)
|
|
303
|
+
results.append(ScreenshotResult(
|
|
304
|
+
url=url,
|
|
305
|
+
path="",
|
|
306
|
+
success=False,
|
|
307
|
+
error=str(e)
|
|
308
|
+
))
|
|
309
|
+
|
|
310
|
+
self.report_data.screenshots = results
|
|
311
|
+
return results
|
|
312
|
+
|
|
313
|
+
def generate_html_report(self) -> str:
|
|
314
|
+
"""產生 HTML 報告"""
|
|
315
|
+
self.report_dir.mkdir(parents=True, exist_ok=True)
|
|
316
|
+
report_path = self.report_dir / f"report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html"
|
|
317
|
+
|
|
318
|
+
# 取得評分顏色
|
|
319
|
+
def get_color(score):
|
|
320
|
+
if score >= 90:
|
|
321
|
+
return '#22c55e' # green
|
|
322
|
+
elif score >= 70:
|
|
323
|
+
return '#eab308' # yellow
|
|
324
|
+
elif score >= 50:
|
|
325
|
+
return '#f97316' # orange
|
|
326
|
+
else:
|
|
327
|
+
return '#ef4444' # red
|
|
328
|
+
|
|
329
|
+
# 語言分佈圖資料
|
|
330
|
+
languages_data = self.report_data.stats.get('languages', {})
|
|
331
|
+
total_lines = self.report_data.stats.get('total_lines', 1)
|
|
332
|
+
|
|
333
|
+
lang_bars = ""
|
|
334
|
+
colors = ['#3b82f6', '#22c55e', '#eab308', '#ef4444', '#8b5cf6', '#06b6d4', '#f97316', '#ec4899']
|
|
335
|
+
for i, (name, data) in enumerate(sorted(languages_data.items(), key=lambda x: x[1]['lines'], reverse=True)[:6]):
|
|
336
|
+
pct = (data['lines'] / total_lines) * 100
|
|
337
|
+
color = colors[i % len(colors)]
|
|
338
|
+
lang_bars += f'''
|
|
339
|
+
<div class="lang-bar">
|
|
340
|
+
<span class="lang-name">{name}</span>
|
|
341
|
+
<div class="bar-container">
|
|
342
|
+
<div class="bar-fill" style="width: {pct}%; background: {color};"></div>
|
|
343
|
+
</div>
|
|
344
|
+
<span class="lang-pct">{pct:.1f}%</span>
|
|
345
|
+
</div>
|
|
346
|
+
'''
|
|
347
|
+
|
|
348
|
+
# 健康評分卡片
|
|
349
|
+
health = self.report_data.health_scores
|
|
350
|
+
total_score = health.get('total_score', 0)
|
|
351
|
+
score_cards = ""
|
|
352
|
+
for key, data in health.get('scores', {}).items():
|
|
353
|
+
score = data['score']
|
|
354
|
+
category = data['category']
|
|
355
|
+
score_cards += f'''
|
|
356
|
+
<div class="score-card">
|
|
357
|
+
<div class="score-value" style="color: {get_color(score)}">{score}</div>
|
|
358
|
+
<div class="score-label">{category}</div>
|
|
359
|
+
</div>
|
|
360
|
+
'''
|
|
361
|
+
|
|
362
|
+
# 測試結果
|
|
363
|
+
test_html = ""
|
|
364
|
+
if self.report_data.test_result:
|
|
365
|
+
tr = self.report_data.test_result
|
|
366
|
+
test_status = "PASS" if tr.success else "FAIL"
|
|
367
|
+
test_color = "#22c55e" if tr.success else "#ef4444"
|
|
368
|
+
test_html = f'''
|
|
369
|
+
<div class="section">
|
|
370
|
+
<h2>測試結果</h2>
|
|
371
|
+
<div class="test-result">
|
|
372
|
+
<div class="test-status" style="background: {test_color}">{test_status}</div>
|
|
373
|
+
<div class="test-stats">
|
|
374
|
+
<span class="passed">{tr.passed} passed</span>
|
|
375
|
+
<span class="failed">{tr.failed} failed</span>
|
|
376
|
+
</div>
|
|
377
|
+
<div class="test-framework">Framework: {tr.framework}</div>
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
'''
|
|
381
|
+
|
|
382
|
+
# 截圖
|
|
383
|
+
screenshots_html = ""
|
|
384
|
+
for ss in self.report_data.screenshots:
|
|
385
|
+
if ss.success and Path(ss.path).exists():
|
|
386
|
+
# 轉換為 base64
|
|
387
|
+
img_data = base64.b64encode(Path(ss.path).read_bytes()).decode()
|
|
388
|
+
screenshots_html += f'''
|
|
389
|
+
<div class="screenshot">
|
|
390
|
+
<div class="screenshot-url">{ss.url}</div>
|
|
391
|
+
<img src="data:image/png;base64,{img_data}" alt="Screenshot" />
|
|
392
|
+
</div>
|
|
393
|
+
'''
|
|
394
|
+
|
|
395
|
+
if screenshots_html:
|
|
396
|
+
screenshots_html = f'''
|
|
397
|
+
<div class="section">
|
|
398
|
+
<h2>UI 截圖</h2>
|
|
399
|
+
<div class="screenshots-grid">
|
|
400
|
+
{screenshots_html}
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
'''
|
|
404
|
+
|
|
405
|
+
# 問題與建議
|
|
406
|
+
issues_html = ""
|
|
407
|
+
recommendations_html = ""
|
|
408
|
+
for data in health.get('scores', {}).values():
|
|
409
|
+
for issue in data.get('issues', []):
|
|
410
|
+
issues_html += f'<li class="issue">{issue}</li>'
|
|
411
|
+
for rec in data.get('recommendations', []):
|
|
412
|
+
recommendations_html += f'<li class="recommendation">{rec}</li>'
|
|
413
|
+
|
|
414
|
+
html = f'''<!DOCTYPE html>
|
|
415
|
+
<html lang="zh-TW">
|
|
416
|
+
<head>
|
|
417
|
+
<meta charset="UTF-8">
|
|
418
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
419
|
+
<title>{self.project_name} - 專案報告</title>
|
|
420
|
+
<style>
|
|
421
|
+
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
|
422
|
+
body {{
|
|
423
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
424
|
+
background: #0f172a;
|
|
425
|
+
color: #e2e8f0;
|
|
426
|
+
line-height: 1.6;
|
|
427
|
+
padding: 2rem;
|
|
428
|
+
}}
|
|
429
|
+
.container {{ max-width: 1200px; margin: 0 auto; }}
|
|
430
|
+
.header {{
|
|
431
|
+
text-align: center;
|
|
432
|
+
padding: 2rem;
|
|
433
|
+
background: linear-gradient(135deg, #1e293b, #334155);
|
|
434
|
+
border-radius: 1rem;
|
|
435
|
+
margin-bottom: 2rem;
|
|
436
|
+
}}
|
|
437
|
+
.header h1 {{ font-size: 2rem; margin-bottom: 0.5rem; }}
|
|
438
|
+
.header .date {{ color: #94a3b8; font-size: 0.875rem; }}
|
|
439
|
+
.total-score {{
|
|
440
|
+
font-size: 4rem;
|
|
441
|
+
font-weight: bold;
|
|
442
|
+
color: {get_color(total_score)};
|
|
443
|
+
margin: 1rem 0;
|
|
444
|
+
}}
|
|
445
|
+
.section {{
|
|
446
|
+
background: #1e293b;
|
|
447
|
+
border-radius: 1rem;
|
|
448
|
+
padding: 1.5rem;
|
|
449
|
+
margin-bottom: 1.5rem;
|
|
450
|
+
}}
|
|
451
|
+
.section h2 {{
|
|
452
|
+
font-size: 1.25rem;
|
|
453
|
+
margin-bottom: 1rem;
|
|
454
|
+
color: #38bdf8;
|
|
455
|
+
}}
|
|
456
|
+
.scores-grid {{
|
|
457
|
+
display: grid;
|
|
458
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
459
|
+
gap: 1rem;
|
|
460
|
+
}}
|
|
461
|
+
.score-card {{
|
|
462
|
+
background: #334155;
|
|
463
|
+
padding: 1rem;
|
|
464
|
+
border-radius: 0.5rem;
|
|
465
|
+
text-align: center;
|
|
466
|
+
}}
|
|
467
|
+
.score-value {{ font-size: 2rem; font-weight: bold; }}
|
|
468
|
+
.score-label {{ color: #94a3b8; font-size: 0.875rem; }}
|
|
469
|
+
.stats-grid {{
|
|
470
|
+
display: grid;
|
|
471
|
+
grid-template-columns: repeat(4, 1fr);
|
|
472
|
+
gap: 1rem;
|
|
473
|
+
}}
|
|
474
|
+
.stat-item {{
|
|
475
|
+
background: #334155;
|
|
476
|
+
padding: 1rem;
|
|
477
|
+
border-radius: 0.5rem;
|
|
478
|
+
text-align: center;
|
|
479
|
+
}}
|
|
480
|
+
.stat-value {{ font-size: 1.5rem; font-weight: bold; color: #38bdf8; }}
|
|
481
|
+
.stat-label {{ color: #94a3b8; font-size: 0.75rem; }}
|
|
482
|
+
.lang-bar {{
|
|
483
|
+
display: flex;
|
|
484
|
+
align-items: center;
|
|
485
|
+
margin-bottom: 0.5rem;
|
|
486
|
+
}}
|
|
487
|
+
.lang-name {{ width: 100px; font-size: 0.875rem; }}
|
|
488
|
+
.bar-container {{
|
|
489
|
+
flex: 1;
|
|
490
|
+
height: 8px;
|
|
491
|
+
background: #334155;
|
|
492
|
+
border-radius: 4px;
|
|
493
|
+
overflow: hidden;
|
|
494
|
+
margin: 0 1rem;
|
|
495
|
+
}}
|
|
496
|
+
.bar-fill {{ height: 100%; border-radius: 4px; }}
|
|
497
|
+
.lang-pct {{ width: 60px; text-align: right; font-size: 0.875rem; color: #94a3b8; }}
|
|
498
|
+
.test-result {{
|
|
499
|
+
display: flex;
|
|
500
|
+
align-items: center;
|
|
501
|
+
gap: 1rem;
|
|
502
|
+
}}
|
|
503
|
+
.test-status {{
|
|
504
|
+
padding: 0.5rem 1rem;
|
|
505
|
+
border-radius: 0.25rem;
|
|
506
|
+
font-weight: bold;
|
|
507
|
+
}}
|
|
508
|
+
.test-stats .passed {{ color: #22c55e; }}
|
|
509
|
+
.test-stats .failed {{ color: #ef4444; margin-left: 1rem; }}
|
|
510
|
+
.test-framework {{ color: #94a3b8; margin-left: auto; }}
|
|
511
|
+
.issues-list, .recommendations-list {{
|
|
512
|
+
list-style: none;
|
|
513
|
+
padding-left: 0;
|
|
514
|
+
}}
|
|
515
|
+
.issue, .recommendation {{
|
|
516
|
+
padding: 0.5rem 1rem;
|
|
517
|
+
margin-bottom: 0.5rem;
|
|
518
|
+
border-radius: 0.25rem;
|
|
519
|
+
font-size: 0.875rem;
|
|
520
|
+
}}
|
|
521
|
+
.issue {{ background: rgba(239, 68, 68, 0.1); border-left: 3px solid #ef4444; }}
|
|
522
|
+
.recommendation {{ background: rgba(234, 179, 8, 0.1); border-left: 3px solid #eab308; }}
|
|
523
|
+
.screenshots-grid {{ display: grid; gap: 1rem; }}
|
|
524
|
+
.screenshot img {{
|
|
525
|
+
max-width: 100%;
|
|
526
|
+
border-radius: 0.5rem;
|
|
527
|
+
border: 1px solid #334155;
|
|
528
|
+
}}
|
|
529
|
+
.screenshot-url {{
|
|
530
|
+
font-size: 0.75rem;
|
|
531
|
+
color: #94a3b8;
|
|
532
|
+
margin-bottom: 0.5rem;
|
|
533
|
+
}}
|
|
534
|
+
.footer {{
|
|
535
|
+
text-align: center;
|
|
536
|
+
color: #64748b;
|
|
537
|
+
font-size: 0.75rem;
|
|
538
|
+
margin-top: 2rem;
|
|
539
|
+
}}
|
|
540
|
+
</style>
|
|
541
|
+
</head>
|
|
542
|
+
<body>
|
|
543
|
+
<div class="container">
|
|
544
|
+
<div class="header">
|
|
545
|
+
<h1>{self.project_name}</h1>
|
|
546
|
+
<div class="date">報告產生時間: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</div>
|
|
547
|
+
<div class="total-score">{total_score}/100</div>
|
|
548
|
+
</div>
|
|
549
|
+
|
|
550
|
+
<div class="section">
|
|
551
|
+
<h2>健康評分</h2>
|
|
552
|
+
<div class="scores-grid">
|
|
553
|
+
{score_cards}
|
|
554
|
+
</div>
|
|
555
|
+
</div>
|
|
556
|
+
|
|
557
|
+
<div class="section">
|
|
558
|
+
<h2>程式碼統計</h2>
|
|
559
|
+
<div class="stats-grid">
|
|
560
|
+
<div class="stat-item">
|
|
561
|
+
<div class="stat-value">{self.report_data.stats.get('total_files', 0):,}</div>
|
|
562
|
+
<div class="stat-label">檔案數</div>
|
|
563
|
+
</div>
|
|
564
|
+
<div class="stat-item">
|
|
565
|
+
<div class="stat-value">{self.report_data.stats.get('total_lines', 0):,}</div>
|
|
566
|
+
<div class="stat-label">總行數</div>
|
|
567
|
+
</div>
|
|
568
|
+
<div class="stat-item">
|
|
569
|
+
<div class="stat-value">{self.report_data.stats.get('total_code_lines', 0):,}</div>
|
|
570
|
+
<div class="stat-label">程式碼行數</div>
|
|
571
|
+
</div>
|
|
572
|
+
<div class="stat-item">
|
|
573
|
+
<div class="stat-value">{self.report_data.stats.get('size_bytes', 0) / 1024:.0f} KB</div>
|
|
574
|
+
<div class="stat-label">大小</div>
|
|
575
|
+
</div>
|
|
576
|
+
</div>
|
|
577
|
+
</div>
|
|
578
|
+
|
|
579
|
+
<div class="section">
|
|
580
|
+
<h2>語言分佈</h2>
|
|
581
|
+
{lang_bars}
|
|
582
|
+
</div>
|
|
583
|
+
|
|
584
|
+
{test_html}
|
|
585
|
+
|
|
586
|
+
{screenshots_html}
|
|
587
|
+
|
|
588
|
+
<div class="section">
|
|
589
|
+
<h2>問題與建議</h2>
|
|
590
|
+
<h3 style="color: #ef4444; font-size: 0.875rem; margin-bottom: 0.5rem;">問題</h3>
|
|
591
|
+
<ul class="issues-list">{issues_html if issues_html else '<li style="color: #94a3b8;">無問題</li>'}</ul>
|
|
592
|
+
<h3 style="color: #eab308; font-size: 0.875rem; margin: 1rem 0 0.5rem;">建議</h3>
|
|
593
|
+
<ul class="recommendations-list">{recommendations_html if recommendations_html else '<li style="color: #94a3b8;">無建議</li>'}</ul>
|
|
594
|
+
</div>
|
|
595
|
+
|
|
596
|
+
<div class="footer">
|
|
597
|
+
Generated by DashAI DevTools v2.0
|
|
598
|
+
</div>
|
|
599
|
+
</div>
|
|
600
|
+
</body>
|
|
601
|
+
</html>'''
|
|
602
|
+
|
|
603
|
+
report_path.write_text(html, encoding='utf-8')
|
|
604
|
+
return str(report_path)
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def run_report(project_path: str, include_tests: bool = True,
|
|
608
|
+
include_screenshots: bool = False, urls: List[str] = None,
|
|
609
|
+
open_browser: bool = True) -> dict:
|
|
610
|
+
"""產生完整報告"""
|
|
611
|
+
|
|
612
|
+
generator = ReportGenerator(project_path)
|
|
613
|
+
|
|
614
|
+
with Progress(
|
|
615
|
+
SpinnerColumn(),
|
|
616
|
+
TextColumn("[progress.description]{task.description}"),
|
|
617
|
+
console=console
|
|
618
|
+
) as progress:
|
|
619
|
+
|
|
620
|
+
# 收集健康評分
|
|
621
|
+
task = progress.add_task("收集健康評分...", total=None)
|
|
622
|
+
generator.collect_health()
|
|
623
|
+
progress.update(task, description="[green]健康評分 ✓")
|
|
624
|
+
|
|
625
|
+
# 收集統計
|
|
626
|
+
task = progress.add_task("收集程式碼統計...", total=None)
|
|
627
|
+
generator.collect_stats()
|
|
628
|
+
progress.update(task, description="[green]程式碼統計 ✓")
|
|
629
|
+
|
|
630
|
+
# 執行測試
|
|
631
|
+
if include_tests:
|
|
632
|
+
task = progress.add_task("執行測試...", total=None)
|
|
633
|
+
test_result = generator.run_tests()
|
|
634
|
+
if test_result.framework != 'unknown':
|
|
635
|
+
status = "[green]✓" if test_result.success else "[red]✗"
|
|
636
|
+
progress.update(task, description=f"{status} 測試完成 ({test_result.framework})")
|
|
637
|
+
else:
|
|
638
|
+
progress.update(task, description="[yellow]跳過測試 (未偵測到測試框架)")
|
|
639
|
+
|
|
640
|
+
# 截圖
|
|
641
|
+
if include_screenshots:
|
|
642
|
+
task = progress.add_task("擷取截圖...", total=None)
|
|
643
|
+
screenshots = generator.take_screenshots(urls)
|
|
644
|
+
success_count = sum(1 for s in screenshots if s.success)
|
|
645
|
+
progress.update(task, description=f"[green]截圖完成 ({success_count}/{len(screenshots)})")
|
|
646
|
+
|
|
647
|
+
# 產生報告
|
|
648
|
+
task = progress.add_task("產生 HTML 報告...", total=None)
|
|
649
|
+
report_path = generator.generate_html_report()
|
|
650
|
+
progress.update(task, description="[green]報告已產生 ✓")
|
|
651
|
+
|
|
652
|
+
console.print()
|
|
653
|
+
console.print(f"[green]報告已產生:[/green] {report_path}")
|
|
654
|
+
|
|
655
|
+
# 開啟瀏覽器
|
|
656
|
+
if open_browser:
|
|
657
|
+
try:
|
|
658
|
+
import webbrowser
|
|
659
|
+
webbrowser.open(f'file://{report_path}')
|
|
660
|
+
except Exception:
|
|
661
|
+
pass
|
|
662
|
+
|
|
663
|
+
return {
|
|
664
|
+
'success': True,
|
|
665
|
+
'report_path': report_path,
|
|
666
|
+
'health_score': generator.report_data.health_scores.get('total_score', 0)
|
|
667
|
+
}
|