wukong-gitlog-cli 1.0.11 → 1.0.12
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/CHANGELOG.md +13 -0
- package/package.json +1 -1
- package/src/overtime.mjs +12 -6
- package/web/app.js +308 -177
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [1.0.12](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.11...v1.0.12) (2025-12-01)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* 🎸 make color ([78d234a](https://github.com/tomatobybike/wukong-gitlog-cli/commit/78d234a04db3d925d8bc1d6cb593173026bcf336))
|
|
11
|
+
* 🎸 make line color ([509f7af](https://github.com/tomatobybike/wukong-gitlog-cli/commit/509f7afca7c2c3cca7a59cf3ce1cdae2832b0c06))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* 🐛 next day time ([1d0fea5](https://github.com/tomatobybike/wukong-gitlog-cli/commit/1d0fea548dbd5e8a75a63f80fd22649c19dc3f59))
|
|
17
|
+
|
|
5
18
|
### [1.0.11](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.10...v1.0.11) (2025-12-01)
|
|
6
19
|
|
|
7
20
|
### [1.0.10](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.9...v1.0.10) (2025-12-01)
|
package/package.json
CHANGED
package/src/overtime.mjs
CHANGED
|
@@ -49,6 +49,7 @@ export function analyzeOvertime(records, opts = {}) {
|
|
|
49
49
|
let holidayCount = 0;
|
|
50
50
|
let nightOutsideCount = 0;
|
|
51
51
|
let overtimeSeveritySum = 0;
|
|
52
|
+
let maxSeverity = -1;
|
|
52
53
|
|
|
53
54
|
const byAuthor = new Map();
|
|
54
55
|
|
|
@@ -95,11 +96,11 @@ export function analyzeOvertime(records, opts = {}) {
|
|
|
95
96
|
nightOutsideCount++;
|
|
96
97
|
const sev = hour >= endHour ? (hour - endHour) : (24 - endHour + hour);
|
|
97
98
|
overtimeSeveritySum += sev;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
if (sev > maxSeverity) {
|
|
100
|
+
maxSeverity = sev;
|
|
101
|
+
latestOutsideCommit = r;
|
|
102
|
+
latestOutsideCommitHour = hour;
|
|
103
|
+
}
|
|
103
104
|
}
|
|
104
105
|
}
|
|
105
106
|
if (isNonWork) nonWorkdayCount++;
|
|
@@ -191,6 +192,7 @@ export function analyzeOvertime(records, opts = {}) {
|
|
|
191
192
|
hourlyPercent,
|
|
192
193
|
hourlyOvertimeCommits,
|
|
193
194
|
hourlyOvertimePercent,
|
|
195
|
+
latestOutsideCommitSeverity: (maxSeverity >= 0 ? maxSeverity : null),
|
|
194
196
|
};
|
|
195
197
|
}
|
|
196
198
|
|
|
@@ -239,7 +241,11 @@ export function renderOvertimeText(stats) {
|
|
|
239
241
|
lines.push(` Date : ${formatDateForCountry(latestOutsideCommit.date, country)}`);
|
|
240
242
|
lines.push(` Message: ${latestOutsideCommit.message}`);
|
|
241
243
|
const h = parseCommitDate(latestOutsideCommit.date).hour();
|
|
242
|
-
|
|
244
|
+
let sev = null;
|
|
245
|
+
if (typeof endHour === 'number' && typeof h === 'number') {
|
|
246
|
+
sev = h >= endHour ? (h - endHour) : (24 - endHour + h);
|
|
247
|
+
}
|
|
248
|
+
lines.push(` Hour : ${String(h).padStart(2, '0')}:00${sev != null ? `(超过下班:${sev} 小时)` : ''}`);
|
|
243
249
|
}
|
|
244
250
|
// country: holiday region, lunchStart/lunchEnd define midday break
|
|
245
251
|
lines.push(`下班时间定义:${startHour}:00 - ${endHour}:00 (午休 ${lunchStart}:00 - ${lunchEnd}:00)`);
|
package/web/app.js
CHANGED
|
@@ -1,279 +1,410 @@
|
|
|
1
1
|
/* eslint-disable import/no-absolute-path */
|
|
2
|
-
const formatDate = (d) => new Date(d).toLocaleString()
|
|
2
|
+
const formatDate = (d) => new Date(d).toLocaleString()
|
|
3
3
|
|
|
4
4
|
async function loadData() {
|
|
5
5
|
try {
|
|
6
|
-
const [
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
]
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
6
|
+
const [
|
|
7
|
+
commitsModule,
|
|
8
|
+
statsModule,
|
|
9
|
+
weeklyModule,
|
|
10
|
+
monthlyModule,
|
|
11
|
+
latestByDayModule,
|
|
12
|
+
configModule
|
|
13
|
+
] = await Promise.all([
|
|
14
|
+
import('/data/commits.mjs'),
|
|
15
|
+
import('/data/overtime-stats.mjs'),
|
|
16
|
+
import('/data/overtime-weekly.mjs'),
|
|
17
|
+
import('/data/overtime-monthly.mjs').catch(() => ({ default: [] })),
|
|
18
|
+
import('/data/overtime-latest-by-day.mjs').catch(() => ({ default: [] })),
|
|
19
|
+
import('/data/config.mjs').catch(() => ({ default: {} }))
|
|
20
|
+
])
|
|
21
|
+
const commits = commitsModule.default || []
|
|
22
|
+
const stats = statsModule.default || {}
|
|
23
|
+
const weekly = weeklyModule.default || []
|
|
24
|
+
const monthly = monthlyModule.default || []
|
|
25
|
+
const latestByDay = latestByDayModule.default || []
|
|
26
|
+
const config = configModule.default || {}
|
|
27
|
+
return { commits, stats, weekly, monthly, latestByDay, config }
|
|
21
28
|
} catch (err) {
|
|
22
|
-
console.error('Load data failed', err)
|
|
23
|
-
return { commits: [], stats: {}, weekly: [], monthly: [], latestByDay: [] }
|
|
29
|
+
console.error('Load data failed', err)
|
|
30
|
+
return { commits: [], stats: {}, weekly: [], monthly: [], latestByDay: [] }
|
|
24
31
|
}
|
|
25
32
|
}
|
|
26
33
|
|
|
27
|
-
let commitsAll = []
|
|
28
|
-
let filtered = []
|
|
29
|
-
let page = 1
|
|
30
|
-
let pageSize = 10
|
|
34
|
+
let commitsAll = []
|
|
35
|
+
let filtered = []
|
|
36
|
+
let page = 1
|
|
37
|
+
let pageSize = 10
|
|
31
38
|
|
|
32
39
|
function renderCommitsTablePage() {
|
|
33
|
-
const tbody = document.querySelector('#commitsTable tbody')
|
|
34
|
-
tbody.innerHTML = ''
|
|
35
|
-
const start = (page - 1) * pageSize
|
|
36
|
-
const end = start + pageSize
|
|
40
|
+
const tbody = document.querySelector('#commitsTable tbody')
|
|
41
|
+
tbody.innerHTML = ''
|
|
42
|
+
const start = (page - 1) * pageSize
|
|
43
|
+
const end = start + pageSize
|
|
37
44
|
filtered.slice(start, end).forEach((c) => {
|
|
38
|
-
const tr = document.createElement('tr')
|
|
39
|
-
tr.innerHTML = `<td>${c.hash.slice(0, 8)}</td><td>${c.author}</td><td>${formatDate(c.date)}</td><td>${c.message}</td
|
|
40
|
-
tbody.appendChild(tr)
|
|
41
|
-
})
|
|
45
|
+
const tr = document.createElement('tr')
|
|
46
|
+
tr.innerHTML = `<td>${c.hash.slice(0, 8)}</td><td>${c.author}</td><td>${formatDate(c.date)}</td><td>${c.message}</td>`
|
|
47
|
+
tbody.appendChild(tr)
|
|
48
|
+
})
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
function updatePager() {
|
|
45
|
-
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize))
|
|
46
|
-
if (page > totalPages) page = totalPages
|
|
47
|
-
const pageInfo = document.getElementById('pageInfo')
|
|
48
|
-
pageInfo.textContent = `${page} / ${totalPages}
|
|
49
|
-
document.getElementById('prevPage').disabled = page <= 1
|
|
50
|
-
document.getElementById('nextPage').disabled = page >= totalPages
|
|
52
|
+
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize))
|
|
53
|
+
if (page > totalPages) page = totalPages
|
|
54
|
+
const pageInfo = document.getElementById('pageInfo')
|
|
55
|
+
pageInfo.textContent = `${page} / ${totalPages}`
|
|
56
|
+
document.getElementById('prevPage').disabled = page <= 1
|
|
57
|
+
document.getElementById('nextPage').disabled = page >= totalPages
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
function applySearch() {
|
|
54
|
-
const q = document.getElementById('searchInput').value.trim().toLowerCase()
|
|
61
|
+
const q = document.getElementById('searchInput').value.trim().toLowerCase()
|
|
55
62
|
if (!q) {
|
|
56
|
-
filtered = commitsAll.slice()
|
|
63
|
+
filtered = commitsAll.slice()
|
|
57
64
|
} else {
|
|
58
65
|
filtered = commitsAll.filter((c) => {
|
|
59
|
-
const h = c.hash.toLowerCase()
|
|
60
|
-
const a = String(c.author || '').toLowerCase()
|
|
61
|
-
const m = String(c.message || '').toLowerCase()
|
|
62
|
-
const d = formatDate(c.date).toLowerCase()
|
|
63
|
-
return h.includes(q) || a.includes(q) || m.includes(q) || d.includes(q)
|
|
64
|
-
})
|
|
66
|
+
const h = c.hash.toLowerCase()
|
|
67
|
+
const a = String(c.author || '').toLowerCase()
|
|
68
|
+
const m = String(c.message || '').toLowerCase()
|
|
69
|
+
const d = formatDate(c.date).toLowerCase()
|
|
70
|
+
return h.includes(q) || a.includes(q) || m.includes(q) || d.includes(q)
|
|
71
|
+
})
|
|
65
72
|
}
|
|
66
|
-
page = 1
|
|
67
|
-
updatePager()
|
|
68
|
-
renderCommitsTablePage()
|
|
73
|
+
page = 1
|
|
74
|
+
updatePager()
|
|
75
|
+
renderCommitsTablePage()
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
function initTableControls() {
|
|
72
|
-
document.getElementById('searchInput').addEventListener('input', applySearch)
|
|
79
|
+
document.getElementById('searchInput').addEventListener('input', applySearch)
|
|
73
80
|
document.getElementById('pageSize').addEventListener('change', (e) => {
|
|
74
|
-
pageSize = parseInt(e.target.value, 10) || 10
|
|
75
|
-
page = 1
|
|
76
|
-
updatePager()
|
|
77
|
-
renderCommitsTablePage()
|
|
78
|
-
})
|
|
81
|
+
pageSize = parseInt(e.target.value, 10) || 10
|
|
82
|
+
page = 1
|
|
83
|
+
updatePager()
|
|
84
|
+
renderCommitsTablePage()
|
|
85
|
+
})
|
|
79
86
|
document.getElementById('prevPage').addEventListener('click', () => {
|
|
80
87
|
if (page > 1) {
|
|
81
|
-
page -= 1
|
|
82
|
-
updatePager()
|
|
83
|
-
renderCommitsTablePage()
|
|
88
|
+
page -= 1
|
|
89
|
+
updatePager()
|
|
90
|
+
renderCommitsTablePage()
|
|
84
91
|
}
|
|
85
|
-
})
|
|
92
|
+
})
|
|
86
93
|
document.getElementById('nextPage').addEventListener('click', () => {
|
|
87
|
-
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize))
|
|
94
|
+
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize))
|
|
88
95
|
if (page < totalPages) {
|
|
89
|
-
page += 1
|
|
90
|
-
updatePager()
|
|
91
|
-
renderCommitsTablePage()
|
|
96
|
+
page += 1
|
|
97
|
+
updatePager()
|
|
98
|
+
renderCommitsTablePage()
|
|
92
99
|
}
|
|
93
|
-
})
|
|
100
|
+
})
|
|
94
101
|
}
|
|
95
102
|
|
|
96
103
|
function drawHourlyOvertime(stats) {
|
|
97
|
-
const el = document.getElementById('hourlyOvertimeChart')
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
const
|
|
104
|
+
const el = document.getElementById('hourlyOvertimeChart')
|
|
105
|
+
// eslint-disable-next-line no-undef
|
|
106
|
+
const chart = echarts.init(el)
|
|
107
|
+
const data = stats.hourlyOvertimeCommits || []
|
|
108
|
+
const labels = Array.from({ length: 24 }, (_, i) =>
|
|
109
|
+
String(i).padStart(2, '0')
|
|
110
|
+
)
|
|
101
111
|
chart.setOption({
|
|
102
112
|
tooltip: {},
|
|
103
113
|
xAxis: { type: 'category', data: labels },
|
|
104
114
|
yAxis: { type: 'value' },
|
|
105
115
|
series: [{ type: 'bar', name: 'Overtime commits', data }]
|
|
106
|
-
})
|
|
107
|
-
return chart
|
|
116
|
+
})
|
|
117
|
+
return chart
|
|
108
118
|
}
|
|
109
119
|
|
|
110
120
|
function drawOutsideVsInside(stats) {
|
|
111
|
-
const el = document.getElementById('outsideVsInsideChart')
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
const
|
|
121
|
+
const el = document.getElementById('outsideVsInsideChart')
|
|
122
|
+
// eslint-disable-next-line no-undef
|
|
123
|
+
const chart = echarts.init(el)
|
|
124
|
+
const outside = stats.outsideWorkCount || 0
|
|
125
|
+
const total = stats.total || 0
|
|
126
|
+
const inside = Math.max(0, total - outside)
|
|
116
127
|
chart.setOption({
|
|
117
128
|
tooltip: {},
|
|
118
|
-
series: [
|
|
119
|
-
{
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
129
|
+
series: [
|
|
130
|
+
{
|
|
131
|
+
type: 'pie',
|
|
132
|
+
radius: '55%',
|
|
133
|
+
data: [
|
|
134
|
+
{ value: inside, name: '工作时间内' },
|
|
135
|
+
{ value: outside, name: '下班时间' }
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
})
|
|
140
|
+
return chart
|
|
124
141
|
}
|
|
125
142
|
|
|
126
143
|
function drawDailyTrend(commits) {
|
|
127
|
-
const map = new Map()
|
|
144
|
+
const map = new Map()
|
|
128
145
|
commits.forEach((c) => {
|
|
129
|
-
const d = new Date(c.date).toISOString().slice(0, 10)
|
|
130
|
-
map.set(d, (map.get(d) || 0) + 1)
|
|
131
|
-
})
|
|
132
|
-
const labels = Array.from(map.keys()).sort()
|
|
133
|
-
const data = labels.map(l => map.get(l))
|
|
134
|
-
const el = document.getElementById('dailyTrendChart')
|
|
135
|
-
|
|
146
|
+
const d = new Date(c.date).toISOString().slice(0, 10)
|
|
147
|
+
map.set(d, (map.get(d) || 0) + 1)
|
|
148
|
+
})
|
|
149
|
+
const labels = Array.from(map.keys()).sort()
|
|
150
|
+
const data = labels.map((l) => map.get(l))
|
|
151
|
+
const el = document.getElementById('dailyTrendChart')
|
|
152
|
+
// eslint-disable-next-line no-undef
|
|
153
|
+
const chart = echarts.init(el)
|
|
136
154
|
chart.setOption({
|
|
137
155
|
tooltip: {},
|
|
138
156
|
xAxis: { type: 'category', data: labels },
|
|
139
157
|
yAxis: { type: 'value' },
|
|
140
158
|
series: [{ type: 'line', name: '每日提交', data, areaStyle: {} }]
|
|
141
|
-
})
|
|
142
|
-
return chart
|
|
159
|
+
})
|
|
160
|
+
return chart
|
|
143
161
|
}
|
|
144
162
|
|
|
145
163
|
function drawWeeklyTrend(weekly) {
|
|
146
|
-
const labels = weekly.map(w => w.period)
|
|
147
|
-
const dataRate = weekly.map(w => +(w.outsideWorkRate * 100).toFixed(1))
|
|
148
|
-
const dataCount = weekly.map(w => w.outsideWorkCount)
|
|
149
|
-
const el = document.getElementById('weeklyTrendChart')
|
|
150
|
-
|
|
164
|
+
const labels = weekly.map((w) => w.period)
|
|
165
|
+
const dataRate = weekly.map((w) => +(w.outsideWorkRate * 100).toFixed(1))
|
|
166
|
+
const dataCount = weekly.map((w) => w.outsideWorkCount)
|
|
167
|
+
const el = document.getElementById('weeklyTrendChart')
|
|
168
|
+
// eslint-disable-next-line no-undef
|
|
169
|
+
const chart = echarts.init(el)
|
|
151
170
|
chart.setOption({
|
|
152
171
|
tooltip: {},
|
|
153
172
|
xAxis: { type: 'category', data: labels },
|
|
154
|
-
yAxis: [
|
|
155
|
-
{ type: 'value', min: 0, max: 100 },
|
|
156
|
-
{ type: 'value' }
|
|
157
|
-
],
|
|
173
|
+
yAxis: [{ type: 'value', min: 0, max: 100 }, { type: 'value' }],
|
|
158
174
|
series: [
|
|
159
175
|
{ type: 'line', name: '加班占比(%)', data: dataRate, yAxisIndex: 0 },
|
|
160
176
|
{ type: 'line', name: '加班次数', data: dataCount, yAxisIndex: 1 }
|
|
161
177
|
]
|
|
162
|
-
})
|
|
163
|
-
return chart
|
|
178
|
+
})
|
|
179
|
+
return chart
|
|
164
180
|
}
|
|
165
181
|
|
|
166
182
|
function drawMonthlyTrend(monthly) {
|
|
167
|
-
if (!Array.isArray(monthly) || monthly.length === 0) return null
|
|
168
|
-
const labels = monthly.map(m => m.period)
|
|
169
|
-
const dataRate = monthly.map(m => +(m.outsideWorkRate * 100).toFixed(1))
|
|
170
|
-
const el = document.getElementById('monthlyTrendChart')
|
|
171
|
-
|
|
183
|
+
if (!Array.isArray(monthly) || monthly.length === 0) return null
|
|
184
|
+
const labels = monthly.map((m) => m.period)
|
|
185
|
+
const dataRate = monthly.map((m) => +(m.outsideWorkRate * 100).toFixed(1))
|
|
186
|
+
const el = document.getElementById('monthlyTrendChart')
|
|
187
|
+
// eslint-disable-next-line no-undef
|
|
188
|
+
const chart = echarts.init(el)
|
|
172
189
|
chart.setOption({
|
|
173
190
|
tooltip: {},
|
|
174
191
|
xAxis: { type: 'category', data: labels },
|
|
175
192
|
yAxis: { type: 'value', min: 0, max: 100 },
|
|
176
193
|
series: [{ type: 'line', name: '加班占比(%)', data: dataRate }]
|
|
177
|
-
})
|
|
178
|
-
return chart
|
|
194
|
+
})
|
|
195
|
+
return chart
|
|
179
196
|
}
|
|
180
197
|
|
|
181
198
|
function drawLatestHourDaily(latestByDay) {
|
|
182
|
-
if (!Array.isArray(latestByDay) || latestByDay.length === 0) return null
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
|
|
199
|
+
if (!Array.isArray(latestByDay) || latestByDay.length === 0) return null
|
|
200
|
+
|
|
201
|
+
const labels = latestByDay.map((d) => d.date)
|
|
202
|
+
|
|
203
|
+
const raw = latestByDay.map((d) =>
|
|
204
|
+
typeof d.latestHourNormalized === 'number'
|
|
205
|
+
? d.latestHourNormalized
|
|
206
|
+
: (d.latestHour ?? null)
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
// 数据点颜色
|
|
210
|
+
const data = raw.map((v) => ({
|
|
211
|
+
value: v,
|
|
212
|
+
itemStyle: {
|
|
213
|
+
color:
|
|
214
|
+
v >= 20
|
|
215
|
+
? '#d32f2f' // 红
|
|
216
|
+
: v >= 19
|
|
217
|
+
? '#fb8c00' // 橙
|
|
218
|
+
: '#1976d2' // 蓝
|
|
219
|
+
}
|
|
220
|
+
}))
|
|
221
|
+
|
|
222
|
+
// 获取最大值,用于设置 yAxis 的 max
|
|
223
|
+
const numericValues = raw.filter((v) => typeof v === 'number')
|
|
224
|
+
const maxV = numericValues.length > 0 ? Math.max(...numericValues) : 0
|
|
225
|
+
|
|
226
|
+
const el = document.getElementById('latestHourDailyChart')
|
|
227
|
+
const chart = echarts.init(el)
|
|
228
|
+
|
|
188
229
|
chart.setOption({
|
|
189
230
|
tooltip: {
|
|
190
231
|
trigger: 'axis',
|
|
191
232
|
formatter: (params) => {
|
|
192
|
-
const p = Array.isArray(params) ? params[0] : params
|
|
193
|
-
const v = p
|
|
194
|
-
const endH =
|
|
195
|
-
const sev = v != null ? Math.max(0,
|
|
196
|
-
return `${p.axisValue}<br/>最晚小时: ${v != null ? v : '-'}<br/>超过下班: ${sev}
|
|
233
|
+
const p = Array.isArray(params) ? params[0] : params
|
|
234
|
+
const v = p?.value != null ? Number(p.value) : null
|
|
235
|
+
const endH = window.__overtimeEndHour || 18
|
|
236
|
+
const sev = v != null ? Math.max(0, v - endH) : 0
|
|
237
|
+
return `${p.axisValue}<br/>最晚小时: ${v != null ? v : '-'}<br/>超过下班: ${sev} 小时`
|
|
197
238
|
}
|
|
198
239
|
},
|
|
199
240
|
xAxis: { type: 'category', data: labels },
|
|
200
|
-
yAxis: {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
241
|
+
yAxis: {
|
|
242
|
+
type: 'value',
|
|
243
|
+
min: 0,
|
|
244
|
+
max: Math.max(26, Math.ceil(maxV + 1))
|
|
245
|
+
},
|
|
246
|
+
series: [
|
|
247
|
+
{
|
|
248
|
+
type: 'line',
|
|
249
|
+
name: '每日最晚提交小时',
|
|
250
|
+
data,
|
|
251
|
+
markLine: {
|
|
252
|
+
symbol: ['none', 'arrow'],
|
|
253
|
+
data: [
|
|
254
|
+
// 20 小时线(橙色)
|
|
255
|
+
{
|
|
256
|
+
yAxis: 19,
|
|
257
|
+
lineStyle: {
|
|
258
|
+
color: '#fb8c00',
|
|
259
|
+
width: 2,
|
|
260
|
+
type: 'solid'
|
|
261
|
+
},
|
|
262
|
+
label: {
|
|
263
|
+
formatter: '21h',
|
|
264
|
+
color: '#fb8c00'
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
// 21 小时线(红色)
|
|
268
|
+
{
|
|
269
|
+
yAxis: 20,
|
|
270
|
+
lineStyle: {
|
|
271
|
+
color: '#d32f2f',
|
|
272
|
+
width: 2,
|
|
273
|
+
type: 'solid'
|
|
274
|
+
},
|
|
275
|
+
label: {
|
|
276
|
+
formatter: '24h',
|
|
277
|
+
color: '#d32f2f'
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
]
|
|
281
|
+
}
|
|
211
282
|
}
|
|
212
|
-
|
|
213
|
-
})
|
|
214
|
-
|
|
283
|
+
]
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
return chart
|
|
215
287
|
}
|
|
216
288
|
|
|
217
289
|
function drawDailySeverity(latestByDay) {
|
|
218
290
|
if (!Array.isArray(latestByDay) || latestByDay.length === 0) return null;
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
const
|
|
222
|
-
|
|
291
|
+
|
|
292
|
+
const labels = latestByDay.map((d) => d.date);
|
|
293
|
+
const endH = window.__overtimeEndHour || 18;
|
|
294
|
+
|
|
295
|
+
const raw = latestByDay.map((d) =>
|
|
296
|
+
typeof d.latestHourNormalized === 'number'
|
|
297
|
+
? d.latestHourNormalized
|
|
298
|
+
: (d.latestHour ?? null)
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
const sev = raw.map((v) => (v == null ? null : Math.max(0, Number(v) - endH)));
|
|
302
|
+
|
|
223
303
|
const el = document.getElementById('dailySeverityChart');
|
|
224
304
|
const chart = echarts.init(el);
|
|
305
|
+
|
|
225
306
|
chart.setOption({
|
|
226
307
|
tooltip: {},
|
|
227
308
|
xAxis: { type: 'category', data: labels },
|
|
228
309
|
yAxis: { type: 'value', min: 0 },
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
310
|
+
|
|
311
|
+
series: [
|
|
312
|
+
{
|
|
313
|
+
type: 'line',
|
|
314
|
+
name: '超过下班小时数',
|
|
315
|
+
data: sev,
|
|
316
|
+
|
|
317
|
+
// ⭐ 加班区域背景
|
|
318
|
+
markArea: {
|
|
319
|
+
data: [
|
|
320
|
+
// 0–1h:透明
|
|
321
|
+
[
|
|
322
|
+
{ yAxis: 0 },
|
|
323
|
+
{ yAxis: 1, itemStyle: { color: 'rgba(0,0,0,0)' } }
|
|
324
|
+
],
|
|
325
|
+
// 1–2h:半透明橙色
|
|
326
|
+
[
|
|
327
|
+
{ yAxis: 1 },
|
|
328
|
+
{ yAxis: 2, itemStyle: { color: 'rgba(251, 140, 0, 0.15)' } } // #fb8c00
|
|
329
|
+
],
|
|
330
|
+
// ≥2h:半透明红色
|
|
331
|
+
[
|
|
332
|
+
{ yAxis: 2 },
|
|
333
|
+
{ yAxis: 10, itemStyle: { color: 'rgba(211, 47, 47, 0.15)' } } // #d32f2f
|
|
334
|
+
]
|
|
335
|
+
]
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
// ⭐ 超时阈值标线
|
|
339
|
+
markLine: {
|
|
340
|
+
symbol: ['none', 'arrow'],
|
|
341
|
+
data: [
|
|
342
|
+
{
|
|
343
|
+
yAxis: 1,
|
|
344
|
+
lineStyle: {
|
|
345
|
+
color: '#fb8c00',
|
|
346
|
+
width: 2,
|
|
347
|
+
type: 'dashed'
|
|
348
|
+
},
|
|
349
|
+
label: { formatter: '1h', color: '#fb8c00' }
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
yAxis: 2,
|
|
353
|
+
lineStyle: {
|
|
354
|
+
color: '#d32f2f',
|
|
355
|
+
width: 2,
|
|
356
|
+
type: 'dashed'
|
|
357
|
+
},
|
|
358
|
+
label: { formatter: '2h', color: '#d32f2f' }
|
|
359
|
+
}
|
|
360
|
+
]
|
|
361
|
+
}
|
|
240
362
|
}
|
|
241
|
-
|
|
363
|
+
]
|
|
242
364
|
});
|
|
365
|
+
|
|
243
366
|
return chart;
|
|
244
367
|
}
|
|
245
368
|
|
|
369
|
+
|
|
246
370
|
function renderKpi(stats) {
|
|
247
|
-
const el = document.getElementById('kpiContent')
|
|
248
|
-
if (!el || !stats) return
|
|
249
|
-
const latest = stats.latestCommit
|
|
250
|
-
const latestHour = stats.latestCommitHour
|
|
251
|
-
const latestOut = stats.latestOutsideCommit
|
|
252
|
-
const latestOutHour =
|
|
253
|
-
|
|
371
|
+
const el = document.getElementById('kpiContent')
|
|
372
|
+
if (!el || !stats) return
|
|
373
|
+
const latest = stats.latestCommit
|
|
374
|
+
const latestHour = stats.latestCommitHour
|
|
375
|
+
const latestOut = stats.latestOutsideCommit
|
|
376
|
+
const latestOutHour =
|
|
377
|
+
stats.latestOutsideCommitHour ??
|
|
378
|
+
(latestOut ? new Date(latestOut.date).getHours() : null)
|
|
379
|
+
const cutoff = window.__overnightCutoff ?? 6
|
|
254
380
|
const html = [
|
|
255
|
-
`<div>最晚一次提交时间:${latest ? formatDate(latest.date) : '-'}${typeof latestHour === 'number' ? `(${String(latestHour).padStart(2,'0')}:00)` : ''}</div>`,
|
|
256
|
-
`<div>加班最晚一次提交时间:${latestOut ? formatDate(latestOut.date) : '-'}${typeof latestOutHour === 'number' ? `(${String(latestOutHour).padStart(2,'0')}:00)` : ''}</div>`,
|
|
381
|
+
`<div>最晚一次提交时间:${latest ? formatDate(latest.date) : '-'}${typeof latestHour === 'number' ? `(${String(latestHour).padStart(2, '0')}:00)` : ''}</div>`,
|
|
382
|
+
`<div>加班最晚一次提交时间:${latestOut ? formatDate(latestOut.date) : '-'}${typeof latestOutHour === 'number' ? `(${String(latestOutHour).padStart(2, '0')}:00)` : ''}</div>`,
|
|
257
383
|
`<div>次日归并窗口:凌晨 <b>${cutoff}</b> 点内归前一日</div>`
|
|
258
|
-
].join('')
|
|
259
|
-
el.innerHTML = html
|
|
384
|
+
].join('')
|
|
385
|
+
el.innerHTML = html
|
|
260
386
|
}
|
|
261
387
|
|
|
262
|
-
(async function main() {
|
|
263
|
-
const { commits, stats, weekly, monthly, latestByDay, config } =
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
window.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
388
|
+
;(async function main() {
|
|
389
|
+
const { commits, stats, weekly, monthly, latestByDay, config } =
|
|
390
|
+
await loadData()
|
|
391
|
+
commitsAll = commits
|
|
392
|
+
filtered = commitsAll.slice()
|
|
393
|
+
window.__overtimeEndHour =
|
|
394
|
+
stats && typeof stats.endHour === 'number'
|
|
395
|
+
? stats.endHour
|
|
396
|
+
: (config.endHour ?? 18)
|
|
397
|
+
window.__overnightCutoff =
|
|
398
|
+
typeof config.overnightCutoff === 'number' ? config.overnightCutoff : 6
|
|
399
|
+
initTableControls()
|
|
400
|
+
updatePager()
|
|
401
|
+
renderCommitsTablePage()
|
|
402
|
+
drawHourlyOvertime(stats)
|
|
403
|
+
drawOutsideVsInside(stats)
|
|
404
|
+
drawDailyTrend(commits)
|
|
405
|
+
drawWeeklyTrend(weekly)
|
|
406
|
+
drawMonthlyTrend(monthly)
|
|
407
|
+
drawLatestHourDaily(latestByDay)
|
|
408
|
+
drawDailySeverity(latestByDay)
|
|
409
|
+
renderKpi(stats)
|
|
410
|
+
})()
|