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.
- package/README.md +2 -2
- package/dist/index.js +1794 -463
- 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,255 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
// ============================================================
|
|
3
|
+
// 初始化
|
|
4
|
+
// ============================================================
|
|
5
|
+
const baseStats = DATA.stats;
|
|
6
|
+
let stats = baseStats;
|
|
7
|
+
const tooltip = document.getElementById('tooltip');
|
|
8
|
+
const isDark = () => document.documentElement.classList.contains('dark');
|
|
9
|
+
const DETAIL_PAGE_SIZE = 100;
|
|
10
|
+
let detailFiltersReady = false;
|
|
11
|
+
let detailCurrentPage = 1;
|
|
12
|
+
const detailExpandedCommits = new Set();
|
|
13
|
+
const reportState = {
|
|
14
|
+
author: '',
|
|
15
|
+
directory: '',
|
|
16
|
+
advancedGroup: 'health',
|
|
17
|
+
advancedTab: 'team-health',
|
|
18
|
+
hashReady: false,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// 主题切换
|
|
22
|
+
document.getElementById('theme-toggle').addEventListener('click', () => {
|
|
23
|
+
document.documentElement.classList.toggle('dark');
|
|
24
|
+
// 重新绘制所有图表以更新颜色
|
|
25
|
+
renderAll();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// 检测系统主题
|
|
29
|
+
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
30
|
+
document.documentElement.classList.add('dark');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function formatNumber(n) {
|
|
34
|
+
return n.toLocaleString('zh-CN');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function formatDateKeyLocal(date) {
|
|
38
|
+
const d = new Date(date);
|
|
39
|
+
const y = d.getFullYear();
|
|
40
|
+
const m = String(d.getMonth() + 1).padStart(2, '0');
|
|
41
|
+
const day = String(d.getDate()).padStart(2, '0');
|
|
42
|
+
return `${y}-${m}-${day}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function formatDateTimeLocal(date) {
|
|
46
|
+
return new Date(date).toLocaleString('zh-CN', {
|
|
47
|
+
month: '2-digit',
|
|
48
|
+
day: '2-digit',
|
|
49
|
+
hour: '2-digit',
|
|
50
|
+
minute: '2-digit',
|
|
51
|
+
hour12: false,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function escapeHtml(str) {
|
|
56
|
+
return String(str ?? '').replace(/[&<>"']/g, char => ({
|
|
57
|
+
'&': '&',
|
|
58
|
+
'<': '<',
|
|
59
|
+
'>': '>',
|
|
60
|
+
'"': '"',
|
|
61
|
+
"'": ''',
|
|
62
|
+
}[char]));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
function showTooltip(event, text) {
|
|
67
|
+
tooltip.style.opacity = 1;
|
|
68
|
+
tooltip.innerHTML = text;
|
|
69
|
+
tooltip.style.left = event.pageX + 12 + 'px';
|
|
70
|
+
tooltip.style.top = event.pageY - 28 + 'px';
|
|
71
|
+
}
|
|
72
|
+
function hideTooltip() { tooltip.style.opacity = 0; }
|
|
73
|
+
|
|
74
|
+
// ============================================================
|
|
75
|
+
function renderAll() {
|
|
76
|
+
renderSummary();
|
|
77
|
+
renderGlobalFilterMeta();
|
|
78
|
+
renderHealthSummary();
|
|
79
|
+
renderQualityCards();
|
|
80
|
+
renderAICard();
|
|
81
|
+
renderHeatmap();
|
|
82
|
+
renderPersonalCommitDetails();
|
|
83
|
+
renderAITrendChart();
|
|
84
|
+
renderWeeklyTrendChart();
|
|
85
|
+
renderCumulativeChart();
|
|
86
|
+
renderHourlyChart();
|
|
87
|
+
renderWeekdayChart();
|
|
88
|
+
renderFileTypeChart();
|
|
89
|
+
renderCommitTypeChart();
|
|
90
|
+
renderAuthorChart();
|
|
91
|
+
renderDirectoryChart();
|
|
92
|
+
renderHotFilesTable();
|
|
93
|
+
renderSoloFilesTable();
|
|
94
|
+
renderAuthorFileTypeTable();
|
|
95
|
+
renderExtraStats();
|
|
96
|
+
|
|
97
|
+
// 渲染当前可见的高级分析模块,避免隐藏图表宽度为 0。
|
|
98
|
+
syncAIUsageTabVisibility();
|
|
99
|
+
renderAdvancedTab(reportState.advancedTab);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ============================================================
|
|
103
|
+
// 概要卡片
|
|
104
|
+
// ============================================================
|
|
105
|
+
function renderSummary() {
|
|
106
|
+
document.getElementById('card-commits').textContent = formatNumber(stats.totalCommits);
|
|
107
|
+
document.getElementById('card-added').textContent = '+' + formatNumber(stats.linesAdded);
|
|
108
|
+
document.getElementById('card-deleted').textContent = '-' + formatNumber(stats.linesDeleted);
|
|
109
|
+
document.getElementById('card-files').textContent = formatNumber(stats.filesChanged);
|
|
110
|
+
document.getElementById('meta-info').textContent = DATA.repos.join(', ');
|
|
111
|
+
document.getElementById('time-range').textContent = DATA.timeRange
|
|
112
|
+
? DATA.timeRange.from + ' ~ ' + DATA.timeRange.to
|
|
113
|
+
: '所有提交';
|
|
114
|
+
document.getElementById('generated-at').textContent = DATA.generatedAt;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ============================================================
|
|
118
|
+
// 质量卡片
|
|
119
|
+
// ============================================================
|
|
120
|
+
function renderQualityCards() {
|
|
121
|
+
if (!stats.quality) return;
|
|
122
|
+
document.getElementById('card-avg-files').textContent = stats.quality.avgFilesPerCommit.toFixed(1);
|
|
123
|
+
document.getElementById('card-avg-lines').textContent = stats.quality.avgLinesPerCommit.toFixed(0);
|
|
124
|
+
document.getElementById('card-churn').textContent = (stats.quality.churnRate * 100).toFixed(1) + '%';
|
|
125
|
+
document.getElementById('card-streak').textContent = stats.timePatterns ? stats.timePatterns.longestStreak : 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ============================================================
|
|
129
|
+
// AI 使用统计卡片
|
|
130
|
+
// ============================================================
|
|
131
|
+
function renderAICard() {
|
|
132
|
+
const card = document.getElementById('ai-usage-card');
|
|
133
|
+
if (!stats.aiMetrics) {
|
|
134
|
+
card.style.display = 'none';
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
card.style.display = 'grid';
|
|
139
|
+
|
|
140
|
+
document.getElementById('card-ai-percentage').textContent = stats.aiMetrics.aiPercentage.toFixed(1) + '%';
|
|
141
|
+
document.getElementById('card-ai-lines').textContent = formatNumber(stats.aiMetrics.totalAILines);
|
|
142
|
+
document.getElementById('card-total-lines').textContent = formatNumber(stats.aiMetrics.totalLines);
|
|
143
|
+
document.getElementById('card-suspicious-commits').textContent = formatNumber(stats.aiMetrics.suspiciousCommits);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ============================================================
|
|
147
|
+
// 提交热力图 (GitHub 风格)
|
|
148
|
+
// ============================================================
|
|
149
|
+
function renderHeatmap() {
|
|
150
|
+
const container = document.getElementById('heatmap');
|
|
151
|
+
container.innerHTML = '';
|
|
152
|
+
|
|
153
|
+
const heatmapData = stats.dailyHeatmap;
|
|
154
|
+
const cellSize = 13;
|
|
155
|
+
const cellGap = 3;
|
|
156
|
+
const totalSize = cellSize + cellGap;
|
|
157
|
+
const weekDayLabels = ['', '一', '', '三', '', '五', ''];
|
|
158
|
+
|
|
159
|
+
// 计算日期范围
|
|
160
|
+
const dates = Object.keys(heatmapData).sort();
|
|
161
|
+
if (dates.length === 0) {
|
|
162
|
+
container.innerHTML = '<p class="text-slate-400 text-sm">暂无数据</p>';
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 从数据或 timeRange 推断日期范围
|
|
167
|
+
const startDate = DATA.timeRange ? new Date(DATA.timeRange.from) : new Date(dates[0]);
|
|
168
|
+
const endDate = DATA.timeRange ? new Date(DATA.timeRange.to) : new Date(dates[dates.length - 1]);
|
|
169
|
+
|
|
170
|
+
// 调整到该周的周一
|
|
171
|
+
const adjustedStart = new Date(startDate);
|
|
172
|
+
adjustedStart.setDate(adjustedStart.getDate() - ((adjustedStart.getDay() + 6) % 7));
|
|
173
|
+
|
|
174
|
+
// 生成所有日期
|
|
175
|
+
const allDays = [];
|
|
176
|
+
const current = new Date(adjustedStart);
|
|
177
|
+
while (current <= endDate) {
|
|
178
|
+
const key = current.toISOString().split('T')[0];
|
|
179
|
+
allDays.push({ date: key, count: heatmapData[key] || 0 });
|
|
180
|
+
current.setDate(current.getDate() + 1);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const weeks = Math.ceil(allDays.length / 7);
|
|
184
|
+
const width = weeks * totalSize + 40;
|
|
185
|
+
const height = 7 * totalSize + 30;
|
|
186
|
+
|
|
187
|
+
const maxCount = Math.max(...allDays.map(d => d.count), 1);
|
|
188
|
+
|
|
189
|
+
const lightColors = ['#ebedf0', '#9be9a8', '#40c463', '#30a14e', '#216e39'];
|
|
190
|
+
const darkColors = ['#161b22', '#0e4429', '#006d32', '#26a641', '#39d353'];
|
|
191
|
+
const colors = isDark() ? darkColors : lightColors;
|
|
192
|
+
|
|
193
|
+
const colorScale = (count) => {
|
|
194
|
+
if (count === 0) return colors[0];
|
|
195
|
+
const ratio = count / maxCount;
|
|
196
|
+
if (ratio <= 0.25) return colors[1];
|
|
197
|
+
if (ratio <= 0.5) return colors[2];
|
|
198
|
+
if (ratio <= 0.75) return colors[3];
|
|
199
|
+
return colors[4];
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const svg = d3.select(container)
|
|
203
|
+
.append('svg')
|
|
204
|
+
.attr('width', width)
|
|
205
|
+
.attr('height', height);
|
|
206
|
+
|
|
207
|
+
// 星期标签
|
|
208
|
+
weekDayLabels.forEach((label, i) => {
|
|
209
|
+
svg.append('text')
|
|
210
|
+
.attr('x', 20)
|
|
211
|
+
.attr('y', 28 + i * totalSize)
|
|
212
|
+
.attr('text-anchor', 'middle')
|
|
213
|
+
.attr('font-size', '10px')
|
|
214
|
+
.attr('fill', isDark() ? '#8b949e' : '#959da5')
|
|
215
|
+
.text(label);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// 月份标签
|
|
219
|
+
const monthPositions = {};
|
|
220
|
+
allDays.forEach((d, i) => {
|
|
221
|
+
const date = new Date(d.date);
|
|
222
|
+
if (date.getDate() <= 7) {
|
|
223
|
+
const weekIdx = Math.floor(i / 7);
|
|
224
|
+
const monthName = date.toLocaleDateString('zh-CN', { month: 'short' });
|
|
225
|
+
if (!monthPositions[monthName]) {
|
|
226
|
+
monthPositions[monthName] = 36 + weekIdx * totalSize;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
Object.entries(monthPositions).forEach(([month, x]) => {
|
|
232
|
+
svg.append('text')
|
|
233
|
+
.attr('x', x)
|
|
234
|
+
.attr('y', 10)
|
|
235
|
+
.attr('font-size', '10px')
|
|
236
|
+
.attr('fill', isDark() ? '#8b949e' : '#959da5')
|
|
237
|
+
.text(month);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// 热力图格子
|
|
241
|
+
allDays.forEach((d, i) => {
|
|
242
|
+
const weekIdx = Math.floor(i / 7);
|
|
243
|
+
const dayIdx = i % 7;
|
|
244
|
+
|
|
245
|
+
svg.append('rect')
|
|
246
|
+
.attr('class', 'heatmap-cell')
|
|
247
|
+
.attr('x', 36 + weekIdx * totalSize)
|
|
248
|
+
.attr('y', 18 + dayIdx * totalSize)
|
|
249
|
+
.attr('width', cellSize)
|
|
250
|
+
.attr('height', cellSize)
|
|
251
|
+
.attr('fill', colorScale(d.count))
|
|
252
|
+
.on('mouseover', (event) => showTooltip(event, `${d.date}: ${d.count} 次提交`))
|
|
253
|
+
.on('mouseout', hideTooltip);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// 个人提交明细
|
|
3
|
+
// ============================================================
|
|
4
|
+
function setupCommitDetailFilters() {
|
|
5
|
+
['detail-author', 'detail-from', 'detail-to'].forEach(id => {
|
|
6
|
+
const el = document.getElementById(id);
|
|
7
|
+
if (el) {
|
|
8
|
+
el.addEventListener('change', () => {
|
|
9
|
+
detailCurrentPage = 1;
|
|
10
|
+
detailExpandedCommits.clear();
|
|
11
|
+
renderPersonalCommitDetails();
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function bindDetailListControls(container, pagination) {
|
|
19
|
+
pagination.querySelectorAll('[data-detail-action="page"]').forEach(button => {
|
|
20
|
+
button.addEventListener('click', () => {
|
|
21
|
+
if (button.disabled) return;
|
|
22
|
+
detailCurrentPage = Number(button.getAttribute('data-detail-page')) || 1;
|
|
23
|
+
renderPersonalCommitDetails();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
container.querySelectorAll('[data-detail-action="toggle-files"]').forEach(button => {
|
|
28
|
+
button.addEventListener('click', () => {
|
|
29
|
+
const key = button.getAttribute('data-detail-key');
|
|
30
|
+
if (!key) return;
|
|
31
|
+
if (detailExpandedCommits.has(key)) {
|
|
32
|
+
detailExpandedCommits.delete(key);
|
|
33
|
+
} else {
|
|
34
|
+
detailExpandedCommits.add(key);
|
|
35
|
+
}
|
|
36
|
+
renderPersonalCommitDetails();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function initCommitDetailFilters(commits) {
|
|
42
|
+
if (detailFiltersReady) return;
|
|
43
|
+
|
|
44
|
+
const authorSelect = document.getElementById('detail-author');
|
|
45
|
+
const fromInput = document.getElementById('detail-from');
|
|
46
|
+
const toInput = document.getElementById('detail-to');
|
|
47
|
+
if (!authorSelect || !fromInput || !toInput) return;
|
|
48
|
+
|
|
49
|
+
const authors = new Map();
|
|
50
|
+
commits.forEach(commit => {
|
|
51
|
+
const key = commit.email.toLowerCase();
|
|
52
|
+
if (!authors.has(key)) {
|
|
53
|
+
authors.set(key, `${commit.author} <${commit.email}>`);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
authorSelect.innerHTML = '<option value="">全部作者</option>' +
|
|
58
|
+
Array.from(authors.entries())
|
|
59
|
+
.sort((a, b) => a[1].localeCompare(b[1]))
|
|
60
|
+
.map(([email, label]) => `<option value="${escapeHtml(email)}">${escapeHtml(label)}</option>`)
|
|
61
|
+
.join('');
|
|
62
|
+
|
|
63
|
+
const dates = commits.map(commit => formatDateKeyLocal(commit.date)).sort();
|
|
64
|
+
const minDate = DATA.timeRange ? DATA.timeRange.from : dates[0];
|
|
65
|
+
const maxDate = DATA.timeRange ? DATA.timeRange.to : dates[dates.length - 1];
|
|
66
|
+
|
|
67
|
+
if (minDate) {
|
|
68
|
+
fromInput.value = minDate;
|
|
69
|
+
fromInput.min = minDate;
|
|
70
|
+
toInput.min = minDate;
|
|
71
|
+
}
|
|
72
|
+
if (maxDate) {
|
|
73
|
+
toInput.value = maxDate;
|
|
74
|
+
fromInput.max = maxDate;
|
|
75
|
+
toInput.max = maxDate;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
detailFiltersReady = true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getFilteredCommitDetails() {
|
|
82
|
+
const commits = stats.commitDetails || [];
|
|
83
|
+
initCommitDetailFilters(commits);
|
|
84
|
+
|
|
85
|
+
const author = document.getElementById('detail-author')?.value || '';
|
|
86
|
+
const from = document.getElementById('detail-from')?.value || '';
|
|
87
|
+
const to = document.getElementById('detail-to')?.value || '';
|
|
88
|
+
|
|
89
|
+
return commits
|
|
90
|
+
.filter(commit => {
|
|
91
|
+
const dateKey = formatDateKeyLocal(commit.date);
|
|
92
|
+
const email = commit.email.toLowerCase();
|
|
93
|
+
return (!author || email === author) &&
|
|
94
|
+
(!from || dateKey >= from) &&
|
|
95
|
+
(!to || dateKey <= to);
|
|
96
|
+
})
|
|
97
|
+
.sort((a, b) => new Date(b.date) - new Date(a.date));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function renderPersonalCommitDetails() {
|
|
101
|
+
const commits = stats.commitDetails || [];
|
|
102
|
+
const root = document.getElementById('detail-commits');
|
|
103
|
+
if (!root) return;
|
|
104
|
+
|
|
105
|
+
if (commits.length === 0) {
|
|
106
|
+
document.getElementById('detail-total').textContent = '0';
|
|
107
|
+
document.getElementById('detail-days').textContent = '0';
|
|
108
|
+
document.getElementById('detail-added').textContent = '+0';
|
|
109
|
+
document.getElementById('detail-deleted').textContent = '-0';
|
|
110
|
+
document.getElementById('detail-hourly').innerHTML = '<p class="text-sm text-slate-400">暂无数据</p>';
|
|
111
|
+
document.getElementById('detail-daily').innerHTML = '<p class="text-sm text-slate-400">暂无数据</p>';
|
|
112
|
+
document.getElementById('detail-list-meta').textContent = '';
|
|
113
|
+
document.getElementById('detail-pagination').innerHTML = '';
|
|
114
|
+
root.innerHTML = '<p class="text-sm text-slate-400">暂无数据</p>';
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const filtered = getFilteredCommitDetails();
|
|
119
|
+
const totalAdded = filtered.reduce((sum, commit) => sum + commit.linesAdded, 0);
|
|
120
|
+
const totalDeleted = filtered.reduce((sum, commit) => sum + commit.linesDeleted, 0);
|
|
121
|
+
const daySet = new Set(filtered.map(commit => formatDateKeyLocal(commit.date)));
|
|
122
|
+
|
|
123
|
+
document.getElementById('detail-total').textContent = formatNumber(filtered.length);
|
|
124
|
+
document.getElementById('detail-days').textContent = formatNumber(daySet.size);
|
|
125
|
+
document.getElementById('detail-added').textContent = '+' + formatNumber(totalAdded);
|
|
126
|
+
document.getElementById('detail-deleted').textContent = '-' + formatNumber(totalDeleted);
|
|
127
|
+
|
|
128
|
+
renderDetailHourly(filtered);
|
|
129
|
+
renderDetailDaily(filtered);
|
|
130
|
+
renderDetailCommitList(filtered);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function renderDetailHourly(commits) {
|
|
134
|
+
const container = document.getElementById('detail-hourly');
|
|
135
|
+
const hours = new Array(24).fill(0);
|
|
136
|
+
commits.forEach(commit => {
|
|
137
|
+
hours[new Date(commit.date).getHours()]++;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const max = Math.max(...hours, 1);
|
|
141
|
+
const rows = hours
|
|
142
|
+
.map((count, hour) => ({ hour, count }))
|
|
143
|
+
.filter(item => item.count > 0);
|
|
144
|
+
|
|
145
|
+
if (rows.length === 0) {
|
|
146
|
+
container.innerHTML = '<p class="text-sm text-slate-400">当前筛选无提交</p>';
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
container.innerHTML = rows.map(({ hour, count }) => `
|
|
151
|
+
<div class="flex items-center gap-3">
|
|
152
|
+
<div class="w-12 text-xs text-slate-500 dark:text-slate-400">${String(hour).padStart(2, '0')}:00</div>
|
|
153
|
+
<div class="flex-1 h-2 rounded bg-slate-100 dark:bg-slate-700 overflow-hidden">
|
|
154
|
+
<div class="h-2 rounded bg-primary-500" style="width: ${(count / max) * 100}%"></div>
|
|
155
|
+
</div>
|
|
156
|
+
<div class="w-10 text-right text-xs font-medium">${count}</div>
|
|
157
|
+
</div>
|
|
158
|
+
`).join('');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function renderDetailDaily(commits) {
|
|
162
|
+
const container = document.getElementById('detail-daily');
|
|
163
|
+
const daily = new Map();
|
|
164
|
+
|
|
165
|
+
commits.forEach(commit => {
|
|
166
|
+
const key = formatDateKeyLocal(commit.date);
|
|
167
|
+
if (!daily.has(key)) daily.set(key, []);
|
|
168
|
+
daily.get(key).push(commit);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
if (daily.size === 0) {
|
|
172
|
+
container.innerHTML = '<p class="text-sm text-slate-400">当前筛选无提交</p>';
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
container.innerHTML = Array.from(daily.entries())
|
|
177
|
+
.sort((a, b) => b[0].localeCompare(a[0]))
|
|
178
|
+
.map(([date, items]) => {
|
|
179
|
+
const added = items.reduce((sum, commit) => sum + commit.linesAdded, 0);
|
|
180
|
+
const deleted = items.reduce((sum, commit) => sum + commit.linesDeleted, 0);
|
|
181
|
+
return `
|
|
182
|
+
<div class="rounded-lg border border-slate-200 dark:border-slate-700 p-3">
|
|
183
|
+
<div class="flex items-center justify-between gap-3">
|
|
184
|
+
<div class="font-medium text-slate-700 dark:text-slate-200">${date}</div>
|
|
185
|
+
<div class="text-xs text-slate-500 dark:text-slate-400">${items.length} 次,+${formatNumber(added)} / -${formatNumber(deleted)}</div>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
`;
|
|
189
|
+
})
|
|
190
|
+
.join('');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function getDetailCommitKey(commit) {
|
|
194
|
+
return `${commit.repoName || ''}::${commit.hash}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function renderDetailCommitList(commits) {
|
|
198
|
+
const container = document.getElementById('detail-commits');
|
|
199
|
+
const meta = document.getElementById('detail-list-meta');
|
|
200
|
+
const pagination = document.getElementById('detail-pagination');
|
|
201
|
+
|
|
202
|
+
if (commits.length === 0) {
|
|
203
|
+
container.innerHTML = '<p class="text-sm text-slate-400">当前筛选无提交</p>';
|
|
204
|
+
meta.textContent = '';
|
|
205
|
+
pagination.innerHTML = '';
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const totalPages = Math.max(1, Math.ceil(commits.length / DETAIL_PAGE_SIZE));
|
|
210
|
+
detailCurrentPage = Math.min(Math.max(1, detailCurrentPage), totalPages);
|
|
211
|
+
const start = (detailCurrentPage - 1) * DETAIL_PAGE_SIZE;
|
|
212
|
+
const pageCommits = commits.slice(start, start + DETAIL_PAGE_SIZE);
|
|
213
|
+
const end = start + pageCommits.length;
|
|
214
|
+
|
|
215
|
+
meta.textContent = commits.length > 1000
|
|
216
|
+
? `筛选结果 ${formatNumber(commits.length)} 条,当前分页渲染 ${formatNumber(start + 1)}-${formatNumber(end)} 条`
|
|
217
|
+
: `显示 ${formatNumber(start + 1)}-${formatNumber(end)} / ${formatNumber(commits.length)} 条`;
|
|
218
|
+
|
|
219
|
+
pagination.innerHTML = totalPages <= 1 ? '' : `
|
|
220
|
+
<button type="button" data-detail-action="page" data-detail-page="${detailCurrentPage - 1}"
|
|
221
|
+
class="px-3 py-1.5 rounded border border-slate-200 dark:border-slate-700 text-xs disabled:opacity-40 disabled:cursor-not-allowed"
|
|
222
|
+
${detailCurrentPage === 1 ? 'disabled' : ''}>上一页</button>
|
|
223
|
+
<span class="text-xs text-slate-500 dark:text-slate-400">${detailCurrentPage} / ${totalPages}</span>
|
|
224
|
+
<button type="button" data-detail-action="page" data-detail-page="${detailCurrentPage + 1}"
|
|
225
|
+
class="px-3 py-1.5 rounded border border-slate-200 dark:border-slate-700 text-xs disabled:opacity-40 disabled:cursor-not-allowed"
|
|
226
|
+
${detailCurrentPage === totalPages ? 'disabled' : ''}>下一页</button>
|
|
227
|
+
`;
|
|
228
|
+
|
|
229
|
+
container.innerHTML = pageCommits.map(commit => {
|
|
230
|
+
const key = getDetailCommitKey(commit);
|
|
231
|
+
const isExpanded = detailExpandedCommits.has(key);
|
|
232
|
+
const files = !isExpanded
|
|
233
|
+
? ''
|
|
234
|
+
: commit.files.length === 0
|
|
235
|
+
? '<div class="text-xs text-slate-400 py-2">无文件变更记录</div>'
|
|
236
|
+
: commit.files.map(file => `
|
|
237
|
+
<div class="grid grid-cols-[1fr_auto_auto] gap-3 py-1.5 border-t border-slate-100 dark:border-slate-700/50 first:border-t-0">
|
|
238
|
+
<div class="truncate font-mono text-xs text-slate-600 dark:text-slate-300" title="${escapeHtml(file.path)}">${escapeHtml(file.path)}</div>
|
|
239
|
+
<div class="text-xs text-emerald-600 dark:text-emerald-400 text-right">+${formatNumber(file.added)}</div>
|
|
240
|
+
<div class="text-xs text-rose-600 dark:text-rose-400 text-right">-${formatNumber(file.deleted)}</div>
|
|
241
|
+
</div>
|
|
242
|
+
`).join('');
|
|
243
|
+
|
|
244
|
+
return `
|
|
245
|
+
<div class="rounded-lg border border-slate-200 dark:border-slate-700 p-4">
|
|
246
|
+
<div class="flex flex-col md:flex-row md:items-start md:justify-between gap-2 mb-3">
|
|
247
|
+
<div class="min-w-0">
|
|
248
|
+
<div class="font-medium text-slate-800 dark:text-slate-100 truncate" title="${escapeHtml(commit.message)}">${escapeHtml(commit.message)}</div>
|
|
249
|
+
<div class="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
|
250
|
+
<span class="font-mono">${escapeHtml(commit.hash.slice(0, 10))}</span>
|
|
251
|
+
<span class="mx-1">·</span>
|
|
252
|
+
<span>${escapeHtml(commit.author)}</span>
|
|
253
|
+
${commit.repoName ? `<span class="mx-1">·</span><span>${escapeHtml(commit.repoName)}</span>` : ''}
|
|
254
|
+
<span class="mx-1">·</span>
|
|
255
|
+
<span>${formatDateTimeLocal(commit.date)}</span>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
<div class="shrink-0 text-sm flex items-center gap-3">
|
|
259
|
+
<div>
|
|
260
|
+
<span class="text-emerald-600 dark:text-emerald-400">+${formatNumber(commit.linesAdded)}</span>
|
|
261
|
+
<span class="text-slate-400 mx-1">/</span>
|
|
262
|
+
<span class="text-rose-600 dark:text-rose-400">-${formatNumber(commit.linesDeleted)}</span>
|
|
263
|
+
</div>
|
|
264
|
+
<button type="button" data-detail-action="toggle-files" data-detail-key="${escapeHtml(key)}"
|
|
265
|
+
class="px-2 py-1 rounded border border-slate-200 dark:border-slate-700 text-xs text-slate-600 dark:text-slate-300 hover:text-primary-500">
|
|
266
|
+
${isExpanded ? '收起文件' : `展开文件 (${formatNumber(commit.files.length)})`}
|
|
267
|
+
</button>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
<div>${files}</div>
|
|
271
|
+
</div>
|
|
272
|
+
`;
|
|
273
|
+
}).join('');
|
|
274
|
+
bindDetailListControls(container, pagination);
|
|
275
|
+
}
|