commit-report 1.0.0 → 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.
- package/README.md +10 -3
- package/dist/index.js +1818 -447
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
- package/templates/report-scripts/00-advanced-derived.html +462 -0
- package/templates/report-scripts/00-filter-state.html +374 -0
- package/templates/report-scripts/00-report-controls.html +272 -0
- package/templates/report-scripts/01-core.html +255 -0
- package/templates/report-scripts/02-commit-details.html +275 -0
- package/templates/report-scripts/03-basic-charts.html +378 -0
- package/templates/report-scripts/04-trend-charts.html +309 -0
- package/templates/report-scripts/05-tables-team-stability.html +372 -0
- package/templates/report-scripts/06-pressure-churn.html +339 -0
- package/templates/report-scripts/07-collab-debt-ai.html +534 -0
- package/templates/report-scripts/08-engineering.html +200 -0
- package/templates/report-scripts/09-extensions.html +313 -0
- package/templates/report-scripts/10-runtime.html +54 -0
- package/templates/report-sections/01-overview.html +342 -0
- package/templates/report-sections/02-advanced.html +406 -0
- package/templates/report.html +40 -1998
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// 热点文件表格
|
|
3
|
+
// ============================================================
|
|
4
|
+
function renderHotFilesTable() {
|
|
5
|
+
const tbody = document.getElementById('hot-files-body');
|
|
6
|
+
tbody.innerHTML = '';
|
|
7
|
+
|
|
8
|
+
if (!stats.quality || !stats.quality.hotFiles || stats.quality.hotFiles.length === 0) {
|
|
9
|
+
tbody.innerHTML = '<tr><td colspan="3" class="py-4 text-center text-slate-400">暂无数据</td></tr>';
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
for (const file of stats.quality.hotFiles) {
|
|
14
|
+
const tr = document.createElement('tr');
|
|
15
|
+
tr.className = 'border-b border-slate-100 dark:border-slate-700/50';
|
|
16
|
+
tr.innerHTML = `
|
|
17
|
+
<td class="py-2 text-slate-700 dark:text-slate-300 truncate max-w-xs" title="${file.path}">${file.path}</td>
|
|
18
|
+
<td class="py-2 text-right text-slate-600 dark:text-slate-400">${file.modifyCount}</td>
|
|
19
|
+
<td class="py-2 text-right text-slate-600 dark:text-slate-400">${file.authors.length}</td>
|
|
20
|
+
`;
|
|
21
|
+
tbody.appendChild(tr);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ============================================================
|
|
26
|
+
// 知识集中度风险表格
|
|
27
|
+
// ============================================================
|
|
28
|
+
function renderSoloFilesTable() {
|
|
29
|
+
const tbody = document.getElementById('solo-files-body');
|
|
30
|
+
tbody.innerHTML = '';
|
|
31
|
+
|
|
32
|
+
if (!stats.collaboration || !stats.collaboration.soloFiles || stats.collaboration.soloFiles.length === 0) {
|
|
33
|
+
tbody.innerHTML = '<tr><td colspan="3" class="py-4 text-center text-slate-400">暂无单人维护文件</td></tr>';
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const file of stats.collaboration.soloFiles) {
|
|
38
|
+
const tr = document.createElement('tr');
|
|
39
|
+
tr.className = 'border-b border-slate-100 dark:border-slate-700/50';
|
|
40
|
+
tr.innerHTML = `
|
|
41
|
+
<td class="py-2 text-slate-700 dark:text-slate-300 truncate max-w-xs" title="${file.path}">${file.path}</td>
|
|
42
|
+
<td class="py-2 text-slate-600 dark:text-slate-400">${file.author}</td>
|
|
43
|
+
<td class="py-2 text-right text-slate-600 dark:text-slate-400">${file.commits}</td>
|
|
44
|
+
`;
|
|
45
|
+
tbody.appendChild(tr);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ============================================================
|
|
50
|
+
// 作者×文件类型贡献表格(支持排序)
|
|
51
|
+
// ============================================================
|
|
52
|
+
let aftSortKey = 'linesAdded';
|
|
53
|
+
let aftSortAsc = false;
|
|
54
|
+
|
|
55
|
+
function renderAuthorFileTypeTable() {
|
|
56
|
+
const tbody = document.getElementById('author-filetype-body');
|
|
57
|
+
tbody.innerHTML = '';
|
|
58
|
+
|
|
59
|
+
if (!stats.authorFileTypeContributions || stats.authorFileTypeContributions.length === 0) {
|
|
60
|
+
tbody.innerHTML = '<tr><td colspan="6" class="py-4 text-center text-slate-400">暂无数据</td></tr>';
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 排序数据
|
|
65
|
+
const sortedData = [...stats.authorFileTypeContributions].sort((a, b) => {
|
|
66
|
+
let valA = a[aftSortKey];
|
|
67
|
+
let valB = b[aftSortKey];
|
|
68
|
+
// 字符串比较
|
|
69
|
+
if (typeof valA === 'string') {
|
|
70
|
+
valA = valA.toLowerCase();
|
|
71
|
+
valB = valB.toLowerCase();
|
|
72
|
+
return aftSortAsc ? valA.localeCompare(valB) : valB.localeCompare(valA);
|
|
73
|
+
}
|
|
74
|
+
// 数字比较
|
|
75
|
+
return aftSortAsc ? valA - valB : valB - valA;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
for (const item of sortedData) {
|
|
79
|
+
const tr = document.createElement('tr');
|
|
80
|
+
tr.className = 'border-b border-slate-100 dark:border-slate-700/50';
|
|
81
|
+
tr.innerHTML = `
|
|
82
|
+
<td class="py-2 text-slate-700 dark:text-slate-300 truncate max-w-[120px]" title="${escapeHtml(item.author)}">${escapeHtml(item.author)}</td>
|
|
83
|
+
<td class="py-2 text-slate-600 dark:text-slate-400">${escapeHtml(item.extension)}</td>
|
|
84
|
+
<td class="py-2 text-right text-slate-600 dark:text-slate-400">${formatNumber(item.fileCount)}</td>
|
|
85
|
+
<td class="py-2 text-right text-emerald-600 dark:text-emerald-400">+${formatNumber(item.linesAdded)}</td>
|
|
86
|
+
<td class="py-2 text-right text-rose-600 dark:text-rose-400">-${formatNumber(item.linesDeleted)}</td>
|
|
87
|
+
<td class="py-2 text-right text-slate-600 dark:text-slate-400">${item.commits}</td>
|
|
88
|
+
`;
|
|
89
|
+
tbody.appendChild(tr);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 更新排序图标
|
|
93
|
+
document.querySelectorAll('#author-filetype-table th[data-sort]').forEach(th => {
|
|
94
|
+
const icon = th.querySelector('.sort-icon');
|
|
95
|
+
if (th.dataset.sort === aftSortKey) {
|
|
96
|
+
icon.textContent = aftSortAsc ? '↑' : '↓';
|
|
97
|
+
} else {
|
|
98
|
+
icon.textContent = '';
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 绑定表头点击排序
|
|
104
|
+
document.querySelectorAll('#author-filetype-table th[data-sort]').forEach(th => {
|
|
105
|
+
th.addEventListener('click', () => {
|
|
106
|
+
const key = th.dataset.sort;
|
|
107
|
+
if (aftSortKey === key) {
|
|
108
|
+
aftSortAsc = !aftSortAsc;
|
|
109
|
+
} else {
|
|
110
|
+
aftSortKey = key;
|
|
111
|
+
aftSortAsc = key === 'author' || key === 'extension'; // 字符串默认升序
|
|
112
|
+
}
|
|
113
|
+
renderAuthorFileTypeTable();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ============================================================
|
|
118
|
+
// 高级分析 - 团队健康度
|
|
119
|
+
// ============================================================
|
|
120
|
+
function renderTeamHealth() {
|
|
121
|
+
const teamHealth = stats.teamHealth;
|
|
122
|
+
|
|
123
|
+
// 检查数据是否存在
|
|
124
|
+
if (!teamHealth) {
|
|
125
|
+
document.getElementById('team-health').innerHTML =
|
|
126
|
+
renderSingleRepoOnlyEmptyState('团队健康度分析');
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const { busFactor, criticalAuthors, knowledgeDistribution, riskLevel } = teamHealth;
|
|
131
|
+
|
|
132
|
+
// 1. 渲染 Bus Factor 仪表盘
|
|
133
|
+
renderBusFactorGauge(busFactor, riskLevel, knowledgeDistribution);
|
|
134
|
+
|
|
135
|
+
// 2. 渲染关键人员列表
|
|
136
|
+
renderCriticalAuthorsList(criticalAuthors);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Bus Factor 仪表盘(半圆仪表)
|
|
140
|
+
function renderBusFactorGauge(busFactor, riskLevel, knowledgeDistribution) {
|
|
141
|
+
const container = d3.select('#bus-factor-gauge');
|
|
142
|
+
container.html(''); // 清空
|
|
143
|
+
|
|
144
|
+
// 风险等级颜色映射
|
|
145
|
+
const riskColors = {
|
|
146
|
+
low: '#10b981', // 绿色
|
|
147
|
+
medium: '#f59e0b', // 橙色
|
|
148
|
+
high: '#ef4444' // 红色
|
|
149
|
+
};
|
|
150
|
+
const color = riskColors[riskLevel] || '#94a3b8';
|
|
151
|
+
|
|
152
|
+
// SVG 尺寸
|
|
153
|
+
const width = 240; // 略微缩小以适应新布局
|
|
154
|
+
const height = 140;
|
|
155
|
+
const radius = 90;
|
|
156
|
+
|
|
157
|
+
const svg = container.append('svg')
|
|
158
|
+
.attr('width', width)
|
|
159
|
+
.attr('height', height)
|
|
160
|
+
.append('g')
|
|
161
|
+
.attr('transform', `translate(${width/2}, ${height - 10})`);
|
|
162
|
+
|
|
163
|
+
// 背景半圆(灰色)
|
|
164
|
+
const bgArc = d3.arc()
|
|
165
|
+
.innerRadius(radius - 15)
|
|
166
|
+
.outerRadius(radius)
|
|
167
|
+
.startAngle(-Math.PI / 2)
|
|
168
|
+
.endAngle(Math.PI / 2);
|
|
169
|
+
|
|
170
|
+
svg.append('path')
|
|
171
|
+
.attr('d', bgArc)
|
|
172
|
+
.attr('fill', '#e2e8f0')
|
|
173
|
+
.attr('class', 'dark:fill-slate-600');
|
|
174
|
+
|
|
175
|
+
// 前景半圆(根据风险等级)
|
|
176
|
+
// 角度:low=0-60度, medium=60-120度, high=120-180度
|
|
177
|
+
const angleMap = { low: Math.PI/6, medium: Math.PI/3, high: Math.PI/2 };
|
|
178
|
+
const targetAngle = angleMap[riskLevel] || 0;
|
|
179
|
+
|
|
180
|
+
const foregroundArc = d3.arc()
|
|
181
|
+
.innerRadius(radius - 15)
|
|
182
|
+
.outerRadius(radius)
|
|
183
|
+
.startAngle(-Math.PI / 2)
|
|
184
|
+
.endAngle(-Math.PI / 2 + targetAngle);
|
|
185
|
+
|
|
186
|
+
svg.append('path')
|
|
187
|
+
.attr('d', foregroundArc)
|
|
188
|
+
.attr('fill', color);
|
|
189
|
+
|
|
190
|
+
// 中心文本 - Bus Factor 数值
|
|
191
|
+
svg.append('text')
|
|
192
|
+
.attr('y', -25)
|
|
193
|
+
.attr('text-anchor', 'middle')
|
|
194
|
+
.attr('class', 'text-5xl font-bold')
|
|
195
|
+
.attr('fill', color)
|
|
196
|
+
.text(busFactor);
|
|
197
|
+
|
|
198
|
+
// 风险等级标签
|
|
199
|
+
const riskLabels = { low: '低风险', medium: '中等风险', high: '高风险' };
|
|
200
|
+
svg.append('text')
|
|
201
|
+
.attr('y', 0)
|
|
202
|
+
.attr('text-anchor', 'middle')
|
|
203
|
+
.attr('class', 'text-sm font-medium fill-slate-500 dark:fill-slate-400')
|
|
204
|
+
.text(riskLabels[riskLevel] || '');
|
|
205
|
+
|
|
206
|
+
// 知识分布均匀度
|
|
207
|
+
d3.select('#bus-factor-gauge')
|
|
208
|
+
.append('div')
|
|
209
|
+
.attr('class', 'text-center mt-2 text-xs text-slate-400')
|
|
210
|
+
.text(`知识分布均匀度: ${(knowledgeDistribution * 100).toFixed(0)}%`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 关键人员列表
|
|
214
|
+
function renderCriticalAuthorsList(criticalAuthors) {
|
|
215
|
+
const container = document.getElementById('critical-authors-list');
|
|
216
|
+
|
|
217
|
+
if (!criticalAuthors || criticalAuthors.length === 0) {
|
|
218
|
+
container.innerHTML = '<div class="flex flex-col items-center justify-center h-40 text-slate-400"><p>未检测到关键人员</p><p class="text-xs mt-1">这意味着知识分布比较均匀</p></div>';
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let html = '<div class="overflow-x-auto max-h-[300px] overflow-y-auto scrollbar-thin">';
|
|
223
|
+
html += '<table class="w-full text-sm">';
|
|
224
|
+
html += '<thead class="sticky top-0 bg-slate-50 dark:bg-slate-800 z-10">';
|
|
225
|
+
html += '<tr class="text-left border-b border-slate-200 dark:border-slate-700">';
|
|
226
|
+
html += '<th class="pb-2 text-slate-500 dark:text-slate-400 font-medium pl-2">姓名</th>';
|
|
227
|
+
html += '<th class="pb-2 text-slate-500 dark:text-slate-400 font-medium text-right">知识评分</th>';
|
|
228
|
+
html += '<th class="pb-2 text-slate-500 dark:text-slate-400 font-medium text-right">独有文件</th>';
|
|
229
|
+
html += '<th class="pb-2 text-slate-500 dark:text-slate-400 font-medium text-right pr-2">主导文件</th>';
|
|
230
|
+
html += '</tr>';
|
|
231
|
+
html += '</thead>';
|
|
232
|
+
html += '<tbody class="divide-y divide-slate-100 dark:divide-slate-700/50">';
|
|
233
|
+
|
|
234
|
+
criticalAuthors.forEach((author) => {
|
|
235
|
+
html += '<tr class="hover:bg-slate-100 dark:hover:bg-slate-700/50 transition-colors">';
|
|
236
|
+
html += `<td class="py-3 font-medium pl-2 text-slate-700 dark:text-slate-300">${escapeHtml(author.name)}</td>`;
|
|
237
|
+
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">${author.knowledgeScore.toFixed(1)}</span></td>`;
|
|
238
|
+
html += `<td class="py-3 text-right text-slate-600 dark:text-slate-400">${author.uniqueFiles.length}</td>`;
|
|
239
|
+
html += `<td class="py-3 text-right text-slate-600 dark:text-slate-400 pr-2">${author.dominantFiles.length}</td>`;
|
|
240
|
+
html += '</tr>';
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
html += '</tbody>';
|
|
244
|
+
html += '</table>';
|
|
245
|
+
html += '</div>';
|
|
246
|
+
|
|
247
|
+
container.innerHTML = html;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ============================================================
|
|
251
|
+
// 高级分析 - 代码稳定性
|
|
252
|
+
// ============================================================
|
|
253
|
+
function renderStability() {
|
|
254
|
+
const stability = stats.stability;
|
|
255
|
+
|
|
256
|
+
// 检查数据是否存在
|
|
257
|
+
if (!stability) {
|
|
258
|
+
document.getElementById('stability').innerHTML =
|
|
259
|
+
renderSingleRepoOnlyEmptyState('代码稳定性分析');
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const { fileChurnRate, stabilityScore, revertRate, fixCommitRate } = stability;
|
|
264
|
+
|
|
265
|
+
// 1. 渲染稳定性指标卡片
|
|
266
|
+
renderStabilityCards(stabilityScore, revertRate, fixCommitRate, fileChurnRate);
|
|
267
|
+
|
|
268
|
+
// 2. 渲染文件流失率柱状图
|
|
269
|
+
renderChurnChart(fileChurnRate);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 稳定性指标卡片
|
|
273
|
+
function renderStabilityCards(stabilityScore, revertRate, fixCommitRate, fileChurnRate) {
|
|
274
|
+
const unstableCount = fileChurnRate.filter(f => f.isUnstable).length;
|
|
275
|
+
const container = document.getElementById('stability-metrics');
|
|
276
|
+
|
|
277
|
+
let html = '<div class="grid grid-cols-2 md:grid-cols-4 gap-4">';
|
|
278
|
+
|
|
279
|
+
// 辅助函数:生成卡片 HTML
|
|
280
|
+
const createCard = (label, value, color, iconPath) => `
|
|
281
|
+
<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">
|
|
282
|
+
<div class="flex items-center justify-between mb-2">
|
|
283
|
+
<div class="text-sm text-slate-500 dark:text-slate-400 font-medium">${label}</div>
|
|
284
|
+
<div class="${color} p-1.5 bg-white dark:bg-slate-800 rounded-lg shadow-sm">
|
|
285
|
+
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
286
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${iconPath}" />
|
|
287
|
+
</svg>
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
<div class="text-2xl font-bold ${color}">${value}</div>
|
|
291
|
+
</div>
|
|
292
|
+
`;
|
|
293
|
+
|
|
294
|
+
// 稳定性评分
|
|
295
|
+
const scoreColor = stabilityScore >= 80 ? 'text-emerald-500' :
|
|
296
|
+
stabilityScore >= 60 ? 'text-amber-500' : 'text-rose-500';
|
|
297
|
+
html += createCard(
|
|
298
|
+
'稳定性评分',
|
|
299
|
+
stabilityScore.toFixed(0),
|
|
300
|
+
scoreColor,
|
|
301
|
+
'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z'
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
// Revert 率
|
|
305
|
+
html += createCard(
|
|
306
|
+
'Revert 率',
|
|
307
|
+
(revertRate * 100).toFixed(1) + '%',
|
|
308
|
+
'text-violet-500',
|
|
309
|
+
'M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6'
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
// Fix 提交率
|
|
313
|
+
html += createCard(
|
|
314
|
+
'Fix 提交率',
|
|
315
|
+
(fixCommitRate * 100).toFixed(1) + '%',
|
|
316
|
+
'text-orange-500',
|
|
317
|
+
'M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z'
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
// 不稳定文件数
|
|
321
|
+
html += createCard(
|
|
322
|
+
'不稳定文件',
|
|
323
|
+
unstableCount,
|
|
324
|
+
'text-rose-500',
|
|
325
|
+
'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z'
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
html += '</div>';
|
|
329
|
+
container.innerHTML = html;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// 文件流失率柱状图
|
|
333
|
+
function renderChurnChart(fileChurnRate) {
|
|
334
|
+
const container = document.getElementById('unstable-files-list');
|
|
335
|
+
|
|
336
|
+
if (!fileChurnRate || fileChurnRate.length === 0) {
|
|
337
|
+
container.innerHTML = '<p class="text-sm text-slate-500 dark:text-slate-400">无流失率数据</p>';
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// 取 TOP 20
|
|
342
|
+
const top20 = fileChurnRate.slice(0, 20);
|
|
343
|
+
|
|
344
|
+
let html = '<div class="space-y-3 max-h-[400px] overflow-y-auto scrollbar-thin pr-2">';
|
|
345
|
+
|
|
346
|
+
top20.forEach(file => {
|
|
347
|
+
const percentage = (file.churnRate * 100).toFixed(1);
|
|
348
|
+
const isUnstable = file.isUnstable;
|
|
349
|
+
const barColor = isUnstable ? 'bg-rose-500' : 'bg-primary-500';
|
|
350
|
+
const textColor = isUnstable ? 'text-rose-600 dark:text-rose-400 font-semibold' : 'text-slate-700 dark:text-slate-300';
|
|
351
|
+
|
|
352
|
+
// 文件路径截断
|
|
353
|
+
const displayPath = file.path.length > 60 ? '...' + file.path.slice(-57) : file.path;
|
|
354
|
+
|
|
355
|
+
html += '<div class="group hover:bg-slate-100 dark:hover:bg-slate-800/50 p-2 rounded-lg transition-colors">';
|
|
356
|
+
html += `<div class="flex items-center justify-between text-sm mb-1.5">`;
|
|
357
|
+
html += `<span class="font-mono text-xs ${textColor}" title="${escapeHtml(file.path)}">${escapeHtml(displayPath)}</span>`;
|
|
358
|
+
html += `<span class="text-xs font-medium ${isUnstable ? 'text-rose-500' : 'text-slate-500'}">${percentage}%</span>`;
|
|
359
|
+
html += '</div>';
|
|
360
|
+
html += '<div class="w-full bg-slate-200 dark:bg-slate-700 rounded-full h-1.5">';
|
|
361
|
+
html += `<div class="${barColor} h-1.5 rounded-full transition-all" style="width: ${Math.min(percentage, 100)}%"></div>`;
|
|
362
|
+
html += '</div>';
|
|
363
|
+
html += `<div class="text-[10px] text-slate-400 mt-1 flex gap-2">`;
|
|
364
|
+
html += `<span>修改 ${file.modifyCount} 次</span>`;
|
|
365
|
+
html += `<span><span class="text-emerald-500">+${formatNumber(file.added)}</span> / <span class="text-rose-500">-${formatNumber(file.deleted)}</span></span>`;
|
|
366
|
+
html += `</div>`;
|
|
367
|
+
html += '</div>';
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
html += '</div>';
|
|
371
|
+
container.innerHTML = html;
|
|
372
|
+
}
|