commit-report 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,534 @@
1
+ // ============================================================
2
+ // 高级分析 - 协作热度
3
+ // ============================================================
4
+ function renderCollaboration() {
5
+ const collaboration = stats.advancedCollaboration;
6
+
7
+ // 检查数据是否存在
8
+ if (!collaboration) {
9
+ document.getElementById('collaboration').innerHTML =
10
+ renderSingleRepoOnlyEmptyState('协作热度分析');
11
+ return;
12
+ }
13
+
14
+ const { tightCoupling, pairProgramming, couplingScore } = collaboration;
15
+
16
+ // 1. 渲染耦合评分卡片
17
+ renderCollaborationMetrics(couplingScore, tightCoupling);
18
+
19
+ // 2. 渲染文件耦合列表
20
+ renderFileCouplingList(tightCoupling);
21
+
22
+ // 3. 渲染结对编程检测
23
+ renderPairProgrammingList(pairProgramming);
24
+ }
25
+
26
+ // 耦合评分卡片
27
+ function renderCollaborationMetrics(couplingScore, tightCoupling) {
28
+ const container = document.getElementById('collaboration-metrics');
29
+
30
+ const highCouplingCount = tightCoupling.filter(f => f.coupling > 0.7).length;
31
+
32
+ const scoreColor = couplingScore < 30 ? 'text-emerald-500' :
33
+ couplingScore < 60 ? 'text-amber-500' : 'text-rose-500';
34
+
35
+ let html = '<div class="grid grid-cols-2 md:grid-cols-4 gap-4">';
36
+
37
+ // 辅助函数:生成卡片 HTML
38
+ const createCard = (label, value, color, desc, iconPath) => `
39
+ <div class="bg-slate-50 dark:bg-slate-700/50 rounded-xl p-5 flex flex-col justify-between h-full border border-slate-100 dark:border-slate-700/50">
40
+ <div class="flex items-center justify-between mb-2">
41
+ <div class="text-sm text-slate-500 dark:text-slate-400 font-medium">${label}</div>
42
+ <div class="${color} p-1.5 bg-white dark:bg-slate-800 rounded-lg shadow-sm">
43
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
44
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${iconPath}" />
45
+ </svg>
46
+ </div>
47
+ </div>
48
+ <div class="text-2xl font-bold ${color}">${value}</div>
49
+ <div class="text-xs text-slate-400 mt-1">${desc}</div>
50
+ </div>
51
+ `;
52
+
53
+ // 耦合评分
54
+ html += createCard(
55
+ '耦合评分',
56
+ couplingScore.toFixed(0),
57
+ scoreColor,
58
+ '分数越低越好',
59
+ 'M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1'
60
+ );
61
+
62
+ // 高耦合文件对数量
63
+ html += createCard(
64
+ '高耦合文件对',
65
+ highCouplingCount,
66
+ 'text-rose-500',
67
+ '耦合度 >70%',
68
+ 'M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z'
69
+ );
70
+
71
+ html += '</div>';
72
+
73
+ container.innerHTML = html;
74
+ }
75
+
76
+ // 文件耦合列表(TOP 20)
77
+ function renderFileCouplingList(tightCoupling) {
78
+ const container = document.getElementById('file-pairs-list');
79
+
80
+ if (!tightCoupling || tightCoupling.length === 0) {
81
+ container.innerHTML = '<p class="text-sm text-slate-500 dark:text-slate-400">无文件耦合数据</p>';
82
+ return;
83
+ }
84
+
85
+ // 取 TOP 20
86
+ const top20 = tightCoupling.slice(0, 20);
87
+
88
+ let html = '<div class="overflow-x-auto max-h-[350px] overflow-y-auto scrollbar-thin">';
89
+ html += '<table class="w-full text-sm">';
90
+ html += '<thead class="sticky top-0 bg-slate-50 dark:bg-slate-700/50 backdrop-blur-sm z-10">';
91
+ html += '<tr class="text-left border-b border-slate-200 dark:border-slate-600">';
92
+ html += '<th class="py-2 pl-2 text-slate-500 dark:text-slate-400 font-medium">文件对</th>';
93
+ html += '<th class="py-2 text-slate-500 dark:text-slate-400 font-medium text-right">共同提交</th>';
94
+ html += '<th class="py-2 text-slate-500 dark:text-slate-400 font-medium text-right pr-2">耦合度</th>';
95
+ html += '</tr>';
96
+ html += '</thead>';
97
+ html += '<tbody class="divide-y divide-slate-100 dark:divide-slate-700/50">';
98
+
99
+ top20.forEach((pair) => {
100
+ const couplingPercentage = (pair.coupling * 100).toFixed(1);
101
+ const isHighCoupling = pair.coupling > 0.7;
102
+ const couplingColor = isHighCoupling ? 'text-rose-600 dark:text-rose-400 font-semibold' : 'text-slate-600 dark:text-slate-400';
103
+ const badgeBg = isHighCoupling ? 'bg-rose-100 dark:bg-rose-900/30' : 'bg-slate-100 dark:bg-slate-700';
104
+
105
+ // 文件路径截断
106
+ const displayPath1 = pair.file1.length > 30 ? '...' + pair.file1.slice(-27) : pair.file1;
107
+ const displayPath2 = pair.file2.length > 30 ? '...' + pair.file2.slice(-27) : pair.file2;
108
+
109
+ html += `<tr class="hover:bg-slate-100 dark:hover:bg-slate-800/50 transition-colors">`;
110
+ html += `<td class="py-3 pl-2">
111
+ <div class="flex flex-col gap-1">
112
+ <span class="font-mono text-xs text-slate-600 dark:text-slate-300" title="${escapeHtml(pair.file1)}">${escapeHtml(displayPath1)}</span>
113
+ <span class="font-mono text-xs text-slate-400" title="${escapeHtml(pair.file2)}">↳ ${escapeHtml(displayPath2)}</span>
114
+ </div>
115
+ </td>`;
116
+ html += `<td class="py-3 text-right text-slate-600 dark:text-slate-400 text-xs">${pair.coOccurrence}</td>`;
117
+ html += `<td class="py-3 text-right pr-2"><span class="px-2 py-0.5 ${badgeBg} ${couplingColor} rounded text-xs">${couplingPercentage}%</span></td>`;
118
+ html += '</tr>';
119
+ });
120
+
121
+ html += '</tbody>';
122
+ html += '</table>';
123
+ html += '</div>';
124
+
125
+ container.innerHTML = html;
126
+ }
127
+
128
+ // 结对编程检测列表
129
+ function renderPairProgrammingList(pairProgramming) {
130
+ const container = document.getElementById('pair-programming-list');
131
+
132
+ if (!pairProgramming || pairProgramming.length === 0) {
133
+ container.innerHTML = '<div class="flex flex-col items-center justify-center h-40 text-slate-400"><p>未检测到明显的结对编程模式</p><p class="text-xs mt-1">需共同修改 ≥3 个文件</p></div>';
134
+ return;
135
+ }
136
+
137
+ let html = '<div class="overflow-x-auto max-h-[350px] overflow-y-auto scrollbar-thin">';
138
+ html += '<table class="w-full text-sm">';
139
+ html += '<thead class="sticky top-0 bg-slate-50 dark:bg-slate-700/50 backdrop-blur-sm z-10">';
140
+ html += '<tr class="text-left border-b border-slate-200 dark:border-slate-600">';
141
+ html += '<th class="py-2 pl-2 text-slate-500 dark:text-slate-400 font-medium">协作成员</th>';
142
+ html += '<th class="py-2 text-slate-500 dark:text-slate-400 font-medium text-right">共同文件</th>';
143
+ html += '<th class="py-2 text-slate-500 dark:text-slate-400 font-medium text-right pr-2">协作次数</th>';
144
+ html += '</tr>';
145
+ html += '</thead>';
146
+ html += '<tbody class="divide-y divide-slate-100 dark:divide-slate-700/50">';
147
+
148
+ pairProgramming.forEach((pair) => {
149
+ html += `<tr class="hover:bg-slate-100 dark:hover:bg-slate-800/50 transition-colors">`;
150
+ html += `<td class="py-3 pl-2">
151
+ <div class="flex items-center gap-2">
152
+ <span class="font-medium text-slate-700 dark:text-slate-200">${escapeHtml(pair.author1)}</span>
153
+ <span class="text-slate-400 text-xs">+</span>
154
+ <span class="font-medium text-slate-700 dark:text-slate-200">${escapeHtml(pair.author2)}</span>
155
+ </div>
156
+ </td>`;
157
+ html += `<td class="py-3 text-right"><span class="px-2 py-0.5 bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 rounded text-xs font-medium">${pair.sharedFiles.length}</span></td>`;
158
+ html += `<td class="py-3 text-right text-slate-600 dark:text-slate-400 pr-2">${pair.collaborationCount}</td>`;
159
+ html += '</tr>';
160
+ });
161
+
162
+ html += '</tbody>';
163
+ html += '</table>';
164
+ html += '</div>';
165
+
166
+ container.innerHTML = html;
167
+ }
168
+
169
+ // ============================================================
170
+ // 技术债分析
171
+ // ============================================================
172
+ function renderTechDebt() {
173
+ const debt = stats.techDebt;
174
+ if (!debt) {
175
+ document.getElementById('tech-debt').innerHTML =
176
+ renderSingleRepoOnlyEmptyState('技术债分析');
177
+ return;
178
+ }
179
+
180
+ renderTechDebtRadar('#tech-debt-radar', debt.radar);
181
+ renderDuplicationHeatmap('#duplication-heatmap', debt.duplication);
182
+ renderRiskTrendChart('#risk-trend-chart', debt.trends);
183
+ renderRiskFilesTable(debt.highRiskFiles);
184
+ }
185
+
186
+ // ============================================================
187
+ // AI 使用分析
188
+ // ============================================================
189
+ function renderAIUsageAnalysis() {
190
+ renderTopAIDirs();
191
+ renderAuthorAIStats();
192
+ renderHighAICommits();
193
+ }
194
+
195
+ function syncAIUsageTabVisibility() {
196
+ const aiTab = document.getElementById('ai-usage-tab');
197
+ if (!aiTab) return;
198
+
199
+ aiTab.style.display = stats.aiMetrics ? 'flex' : 'none';
200
+ if (!stats.aiMetrics && reportState.advancedTab === 'ai-usage') {
201
+ reportState.advancedTab = 'ai-quality';
202
+ reportState.advancedGroup = 'ai';
203
+ }
204
+ }
205
+
206
+ function renderAdvancedTab(tabId) {
207
+ switch (tabId) {
208
+ case 'team-health':
209
+ renderTeamHealth();
210
+ break;
211
+ case 'stability':
212
+ renderStability();
213
+ break;
214
+ case 'work-pressure':
215
+ renderWorkPressure();
216
+ break;
217
+ case 'contributor-churn':
218
+ renderContributorChurn();
219
+ break;
220
+ case 'collaboration':
221
+ renderCollaboration();
222
+ break;
223
+ case 'tech-debt':
224
+ renderTechDebt();
225
+ break;
226
+ case 'engineering-quality':
227
+ renderEngineeringMetrics();
228
+ break;
229
+ case 'change-size':
230
+ renderChangeSize();
231
+ break;
232
+ case 'dir-coupling':
233
+ renderDirCoupling();
234
+ break;
235
+ case 'ai-quality':
236
+ renderAIQuality();
237
+ break;
238
+ case 'ai-usage':
239
+ renderAIUsageAnalysis();
240
+ break;
241
+ default:
242
+ renderTeamHealth();
243
+ }
244
+ }
245
+
246
+ function renderTopAIDirs() {
247
+ const tbody = document.getElementById('top-ai-dirs-body');
248
+ if (!tbody || !stats.directoryAIStats) return;
249
+
250
+ const topDirs = stats.directoryAIStats
251
+ .filter(dir => dir.totalLines > 0)
252
+ .sort((a, b) => b.aiPercentage - a.aiPercentage)
253
+ .slice(0, 10);
254
+
255
+ if (topDirs.length === 0) {
256
+ tbody.innerHTML = '<tr><td colspan="4" class="py-4 text-center text-slate-400">暂无可展示的目录 AI 数据</td></tr>';
257
+ return;
258
+ }
259
+
260
+ let html = '';
261
+ topDirs.forEach(dir => {
262
+ const riskLevel = dir.isHighRisk ? '高' : dir.aiPercentage > 50 ? '中' : '低';
263
+ const riskColor = dir.isHighRisk ? 'bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400' :
264
+ dir.aiPercentage > 50 ? 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-600 dark:text-yellow-400' :
265
+ 'bg-emerald-100 dark:bg-emerald-900/30 text-emerald-600 dark:text-emerald-400';
266
+
267
+ html += '<tr class="border-b border-slate-200 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-600/30">';
268
+ html += `<td class="py-3 font-medium">${escapeHtml(dir.displayPath || dir.path)}</td>`;
269
+ html += `<td class="py-3 text-right">${formatNumber(dir.commits)}</td>`;
270
+ html += `<td class="py-3 text-right font-semibold text-purple-600 dark:text-purple-400">${dir.aiPercentage.toFixed(1)}%</td>`;
271
+ html += `<td class="py-3 text-center"><span class="px-2 py-1 rounded text-xs font-medium ${riskColor}">${riskLevel}</span></td>`;
272
+ html += '</tr>';
273
+ });
274
+
275
+ tbody.innerHTML = html;
276
+ }
277
+
278
+ function renderAuthorAIStats() {
279
+ const tbody = document.getElementById('author-ai-stats-body');
280
+ if (!tbody || !stats.authorAIStats) return;
281
+
282
+ const authors = stats.authorAIStats
283
+ .filter(author => author.totalLines > 0)
284
+ .sort((a, b) => b.aiPercentage - a.aiPercentage)
285
+ .slice(0, 10);
286
+
287
+ if (authors.length === 0) {
288
+ tbody.innerHTML = '<tr><td colspan="4" class="py-4 text-center text-slate-400">暂无作者 AI 数据</td></tr>';
289
+ return;
290
+ }
291
+
292
+ tbody.innerHTML = authors.map(author => `
293
+ <tr class="border-b border-slate-200 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-600/30">
294
+ <td class="py-3">
295
+ <div class="font-medium">${escapeHtml(author.author)}</div>
296
+ <div class="text-xs text-slate-400">${escapeHtml(author.email)}</div>
297
+ </td>
298
+ <td class="py-3 text-right text-purple-600 dark:text-purple-400">${formatNumber(author.aiLines)}</td>
299
+ <td class="py-3 text-right">${formatNumber(author.totalLines)}</td>
300
+ <td class="py-3 text-right font-semibold">${author.aiPercentage.toFixed(1)}%</td>
301
+ </tr>
302
+ `).join('');
303
+ }
304
+
305
+ function renderHighAICommits() {
306
+ const tbody = document.getElementById('high-ai-commits-body');
307
+ if (!tbody || !stats.aiMetrics || !stats.aiMetrics.highAICommits) return;
308
+
309
+ const commits = stats.aiMetrics.highAICommits;
310
+
311
+ if (commits.length === 0) {
312
+ tbody.innerHTML = '<tr><td colspan="8" class="py-4 text-center text-slate-400">暂无高 AI 提交</td></tr>';
313
+ return;
314
+ }
315
+
316
+ let html = '';
317
+ commits.forEach(commit => {
318
+ const scoreColor = commit.aiScore > 80 ? 'text-red-500' :
319
+ commit.aiScore > 70 ? 'text-orange-500' :
320
+ 'text-yellow-500';
321
+
322
+ html += '<tr class="border-b border-slate-200 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-600/30">';
323
+ html += `<td class="py-3 font-mono text-xs">${commit.hash.substring(0, 8)}</td>`;
324
+ html += `<td class="py-3 text-slate-600 dark:text-slate-400">${escapeHtml(commit.repoName || '-')}</td>`;
325
+ html += `<td class="py-3">${escapeHtml(commit.author)}</td>`;
326
+ html += `<td class="py-3 text-right font-semibold ${scoreColor}">${commit.aiScore.toFixed(0)}</td>`;
327
+ html += `<td class="py-3">${formatAIReasons(commit.reasons)}</td>`;
328
+ html += `<td class="py-3 text-right text-emerald-600 dark:text-emerald-400">+${formatNumber(commit.linesAdded)}</td>`;
329
+ html += `<td class="py-3 text-right">${commit.filesCount}</td>`;
330
+ html += `<td class="py-3 text-slate-600 dark:text-slate-400">${new Date(commit.date).toLocaleDateString('zh-CN')}</td>`;
331
+ html += '</tr>';
332
+ });
333
+
334
+ tbody.innerHTML = html;
335
+ }
336
+
337
+ function formatAIReasons(reasons) {
338
+ if (!reasons || reasons.length === 0) {
339
+ return '<span class="text-slate-400">-</span>';
340
+ }
341
+
342
+ return reasons.slice(0, 3).map(reason =>
343
+ `<span class="inline-flex mb-1 mr-1 px-2 py-0.5 rounded bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 text-xs">${escapeHtml(reason)}</span>`
344
+ ).join('');
345
+ }
346
+
347
+ function renderTechDebtRadar(selector, data) {
348
+ if (!data || data.length === 0) return;
349
+
350
+ const container = d3.select(selector);
351
+ container.html('');
352
+ const width = 350, height = 350, margin = 50;
353
+ const radius = Math.min(width, height) / 2 - margin;
354
+ const dimensions = data.map(d => d.dimension);
355
+ const angleSlice = (Math.PI * 2) / dimensions.length;
356
+
357
+ const svg = container.append('svg')
358
+ .attr('viewBox', `0 0 ${width} ${height}`)
359
+ .append('g')
360
+ .attr('transform', `translate(${width/2},${height/2})`);
361
+
362
+ const colorScale = d3.scaleLinear()
363
+ .domain([0, 50, 100])
364
+ .range(['#10b981', '#f59e0b', '#ef4444']);
365
+
366
+ for (let i = 0; i < 5; i++) {
367
+ svg.append('circle')
368
+ .attr('r', (radius / 5) * (i + 1))
369
+ .attr('fill', 'none')
370
+ .attr('stroke', isDark() ? '#334155' : '#e2e8f0')
371
+ .attr('stroke-dasharray', '4,4');
372
+ }
373
+
374
+ const axis = svg.selectAll('.axis').data(dimensions).enter().append('g');
375
+ axis.append('line')
376
+ .attr('x2', (d, i) => radius * Math.cos(angleSlice * i - Math.PI/2))
377
+ .attr('y2', (d, i) => radius * Math.sin(angleSlice * i - Math.PI/2))
378
+ .attr('stroke', isDark() ? '#475569' : '#cbd5e1');
379
+
380
+ axis.append('text')
381
+ .attr('class', 'text-[10px] fill-slate-500 dark:fill-slate-400')
382
+ .attr('text-anchor', 'middle')
383
+ .attr('x', (d, i) => (radius + 20) * Math.cos(angleSlice * i - Math.PI/2))
384
+ .attr('y', (d, i) => (radius + 20) * Math.sin(angleSlice * i - Math.PI/2))
385
+ .text(d => d);
386
+
387
+ const radarLine = d3.lineRadial()
388
+ .radius(d => (d.score / 100) * radius)
389
+ .angle((d, i) => i * angleSlice)
390
+ .curve(d3.curveLinearClosed);
391
+
392
+ const values = data.map(d => ({ axis: d.dimension, score: d.score }));
393
+ const avgScore = d3.mean(values, d => d.score);
394
+
395
+ svg.append('path')
396
+ .datum(values)
397
+ .attr('d', radarLine)
398
+ .attr('fill', colorScale(avgScore))
399
+ .attr('fill-opacity', 0.4)
400
+ .attr('stroke', colorScale(avgScore))
401
+ .attr('stroke-width', 2);
402
+ }
403
+
404
+ function renderDuplicationHeatmap(selector, data) {
405
+ if (!data || !data.fileScores || data.fileScores.length === 0) {
406
+ d3.select(selector).html('<div class="flex items-center justify-center h-full text-slate-400">暂无重复代码检测数据</div>');
407
+ return;
408
+ }
409
+
410
+ const container = d3.select(selector);
411
+ container.html('');
412
+ const margin = { top: 20, right: 20, bottom: 40, left: 80 };
413
+ const width = container.node().clientWidth - margin.left - margin.right;
414
+ const height = 240 - margin.top - margin.bottom;
415
+
416
+ const svg = container.append('svg')
417
+ .attr('width', width + margin.left + margin.right)
418
+ .attr('height', height + margin.top + margin.bottom)
419
+ .append('g')
420
+ .attr('transform', `translate(${margin.left},${margin.top})`);
421
+
422
+ const topFiles = data.fileScores.slice(0, 10);
423
+ const y = d3.scaleBand()
424
+ .range([0, height])
425
+ .domain(topFiles.map(d => d.file.split('/').pop()))
426
+ .padding(0.1);
427
+
428
+ const x = d3.scaleLinear()
429
+ .range([0, width])
430
+ .domain([0, d3.max(topFiles, d => d.score) || 100]);
431
+
432
+ const colorScale = d3.scaleSequential(isDark() ? d3.interpolateYlOrRd : d3.interpolateReds)
433
+ .domain([0, d3.max(topFiles, d => d.score) || 100]);
434
+
435
+ svg.selectAll('.bar')
436
+ .data(topFiles)
437
+ .enter()
438
+ .append('rect')
439
+ .attr('y', d => y(d.file.split('/').pop()))
440
+ .attr('height', y.bandwidth())
441
+ .attr('x', 0)
442
+ .attr('width', d => x(d.score))
443
+ .style('fill', d => colorScale(d.score))
444
+ .on('mouseover', (e, d) => showTooltip(e, `${d.file}: ${d.score} 行重复`))
445
+ .on('mouseout', hideTooltip);
446
+
447
+ svg.append('g')
448
+ .call(d3.axisLeft(y))
449
+ .attr('color', isDark() ? '#475569' : '#cbd5e1');
450
+ }
451
+
452
+ function renderRiskTrendChart(selector, data) {
453
+ if (!data || data.length === 0) {
454
+ d3.select(selector).html('<div class="flex items-center justify-center h-full text-slate-400">暂无趋势数据</div>');
455
+ return;
456
+ }
457
+
458
+ const container = d3.select(selector);
459
+ container.html('');
460
+ const margin = { top: 20, right: 30, bottom: 30, left: 40 };
461
+ const width = container.node().clientWidth - margin.left - margin.right;
462
+ const height = 200 - margin.top - margin.bottom;
463
+
464
+ const svg = container.append('svg')
465
+ .attr('width', width + margin.left + margin.right)
466
+ .attr('height', height + margin.top + margin.bottom)
467
+ .append('g')
468
+ .attr('transform', `translate(${margin.left},${margin.top})`);
469
+
470
+ const x = d3.scaleTime()
471
+ .domain(d3.extent(data, d => new Date(d.date)))
472
+ .range([0, width]);
473
+
474
+ const y = d3.scaleLinear()
475
+ .domain([0, d3.max(data, d => d.debt) || 100])
476
+ .nice()
477
+ .range([height, 0]);
478
+
479
+ svg.append('g')
480
+ .attr('transform', `translate(0,${height})`)
481
+ .call(d3.axisBottom(x).ticks(5))
482
+ .attr('color', isDark() ? '#475569' : '#cbd5e1');
483
+
484
+ svg.append('g')
485
+ .call(d3.axisLeft(y).ticks(5))
486
+ .attr('color', isDark() ? '#475569' : '#cbd5e1');
487
+
488
+ const line = d3.line()
489
+ .x(d => x(new Date(d.date)))
490
+ .y(d => y(d.debt))
491
+ .curve(d3.curveMonotoneX);
492
+
493
+ svg.append('path')
494
+ .datum(data)
495
+ .attr('fill', 'none')
496
+ .attr('stroke', '#ef4444')
497
+ .attr('stroke-width', 2)
498
+ .attr('d', line);
499
+
500
+ const area = d3.area()
501
+ .x(d => x(new Date(d.date)))
502
+ .y0(height)
503
+ .y1(d => y(d.debt))
504
+ .curve(d3.curveMonotoneX);
505
+
506
+ svg.append('path')
507
+ .datum(data)
508
+ .attr('fill', 'rgba(239, 68, 68, 0.1)')
509
+ .attr('d', area);
510
+ }
511
+
512
+ function renderRiskFilesTable(files) {
513
+ const tbody = document.getElementById('risk-files-body');
514
+ if (!tbody) return;
515
+
516
+ tbody.innerHTML = '';
517
+ if (!files || files.length === 0) {
518
+ tbody.innerHTML = '<tr><td colspan="3" class="py-4 text-center text-slate-400">未发现显著高风险文件</td></tr>';
519
+ return;
520
+ }
521
+
522
+ files.forEach(file => {
523
+ const tr = document.createElement('tr');
524
+ tr.className = 'border-b border-slate-100 dark:border-slate-700/50 hover:bg-slate-50 dark:hover:bg-slate-800/50';
525
+ tr.innerHTML = `
526
+ <td class="py-3 text-slate-700 dark:text-slate-300 font-mono text-xs truncate max-w-md" title="${escapeHtml(file.path)}">${escapeHtml(file.path)}</td>
527
+ <td class="py-3 text-right">
528
+ <span class="px-2 py-1 rounded text-xs font-bold ${file.riskScore > 80 ? 'bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-300' : 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300'}">${file.riskScore.toFixed(0)}</span>
529
+ </td>
530
+ <td class="py-3 text-right text-slate-600 dark:text-slate-400">${file.complexity.toFixed(0)}</td>
531
+ `;
532
+ tbody.appendChild(tr);
533
+ });
534
+ }