wukong-gitlog-cli 1.0.19 → 1.0.22
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 +16 -0
- package/package.json +3 -2
- package/web/app.js +225 -48
- package/web/index.html +1 -0
- package/bin/wukong-gitlog-cli +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
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.22](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.21...v1.0.22) (2025-12-08)
|
|
6
|
+
|
|
7
|
+
### [1.0.21](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.20...v1.0.21) (2025-12-08)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* 🎸 weekly rangeMap ([8389e5e](https://github.com/tomatobybike/wukong-gitlog-cli/commit/8389e5e700640273906a4e616333e318a5961d12))
|
|
13
|
+
|
|
14
|
+
### [1.0.20](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.19...v1.0.20) (2025-12-05)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* 🎸 rolling30DurationRiskSummary ([b4a976e](https://github.com/tomatobybike/wukong-gitlog-cli/commit/b4a976e552cfdc1483b0f23f2a4126ecede42af3))
|
|
20
|
+
|
|
5
21
|
### [1.0.19](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.18...v1.0.19) (2025-12-05)
|
|
6
22
|
|
|
7
23
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wukong-gitlog-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.22",
|
|
4
4
|
"description": "Advanced Git commit log exporter with Excel/JSON/TXT output, grouping, stats and CLI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"git",
|
|
@@ -116,5 +116,6 @@
|
|
|
116
116
|
"prettier-plugin-packagejson": "2.5.19",
|
|
117
117
|
"sort-package-json": "3.4.0",
|
|
118
118
|
"standard-version": "9.5.0"
|
|
119
|
-
}
|
|
119
|
+
},
|
|
120
|
+
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
|
120
121
|
}
|
package/web/app.js
CHANGED
|
@@ -16,6 +16,34 @@ function getIsoWeekKey(dStr) {
|
|
|
16
16
|
return `${year}-W${String(weekNo).padStart(2, '0')}`
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function formatDateYMD(d) {
|
|
20
|
+
const yyyy = d.getFullYear()
|
|
21
|
+
const mm = String(d.getMonth() + 1).padStart(2, '0')
|
|
22
|
+
const dd = String(d.getDate()).padStart(2, '0')
|
|
23
|
+
return `${yyyy}-${mm}-${dd}`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getISOWeekRange(isoYear, isoWeek) {
|
|
27
|
+
// 找到 ISO 年的第一个周一
|
|
28
|
+
// ISO 年的第 1 周包含 1 月 4 日
|
|
29
|
+
const simple = new Date(isoYear, 0, 4)
|
|
30
|
+
const dayOfWeek = simple.getDay() || 7 // Sunday=7
|
|
31
|
+
const firstMonday = new Date(simple)
|
|
32
|
+
firstMonday.setDate(simple.getDate() - dayOfWeek + 1)
|
|
33
|
+
|
|
34
|
+
// 计算目标周的周一
|
|
35
|
+
const monday = new Date(firstMonday)
|
|
36
|
+
monday.setDate(firstMonday.getDate() + (isoWeek - 1) * 7)
|
|
37
|
+
|
|
38
|
+
const sunday = new Date(monday)
|
|
39
|
+
sunday.setDate(monday.getDate() + 6)
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
start: formatDateYMD(monday),
|
|
43
|
+
end: formatDateYMD(sunday)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
19
47
|
async function loadData() {
|
|
20
48
|
try {
|
|
21
49
|
const [
|
|
@@ -42,7 +70,15 @@ async function loadData() {
|
|
|
42
70
|
const latestByDay = latestByDayModule.default || []
|
|
43
71
|
const config = configModule.default || {}
|
|
44
72
|
const authorChanges = authorChangesModule.default || {}
|
|
45
|
-
return {
|
|
73
|
+
return {
|
|
74
|
+
commits,
|
|
75
|
+
stats,
|
|
76
|
+
weekly,
|
|
77
|
+
monthly,
|
|
78
|
+
latestByDay,
|
|
79
|
+
config,
|
|
80
|
+
authorChanges
|
|
81
|
+
}
|
|
46
82
|
} catch (err) {
|
|
47
83
|
console.error('Load data failed', err)
|
|
48
84
|
return { commits: [], stats: {}, weekly: [], monthly: [], latestByDay: [] }
|
|
@@ -60,8 +96,6 @@ function renderCommitsTablePage() {
|
|
|
60
96
|
const start = (page - 1) * pageSize
|
|
61
97
|
const end = start + pageSize
|
|
62
98
|
filtered.slice(start, end).forEach((c) => {
|
|
63
|
-
// FIXME: remove debug log before production
|
|
64
|
-
console.log('❌', 'c', c);
|
|
65
99
|
const tr = document.createElement('tr')
|
|
66
100
|
tr.innerHTML = `<td>${c.hash.slice(0, 8)}</td><td>${c.author}</td><td>${formatDate(c.date)}</td><td>${c.message}</td><td>${c.changed}</td>`
|
|
67
101
|
tbody.appendChild(tr)
|
|
@@ -1244,7 +1278,6 @@ function groupCommitsByHour(commits) {
|
|
|
1244
1278
|
return byHour
|
|
1245
1279
|
}
|
|
1246
1280
|
|
|
1247
|
-
|
|
1248
1281
|
// 基于 latestByDay + cutoff/endHour 统计「最晚加班的一天 / 一周 / 一月」
|
|
1249
1282
|
function computeAndRenderLatestOvertime(latestByDay) {
|
|
1250
1283
|
if (!Array.isArray(latestByDay) || latestByDay.length === 0) return
|
|
@@ -1337,21 +1370,21 @@ function computeAndRenderLatestOvertime(latestByDay) {
|
|
|
1337
1370
|
}
|
|
1338
1371
|
|
|
1339
1372
|
function buildDataset(stats, type) {
|
|
1340
|
-
const dataMap = stats[type]
|
|
1373
|
+
const dataMap = stats[type] // { author: { period: changed } }
|
|
1341
1374
|
|
|
1342
|
-
const authors = Object.keys(dataMap)
|
|
1343
|
-
const allPeriods = Array.from(
|
|
1344
|
-
authors.flatMap(a => Object.keys(dataMap[a]))
|
|
1345
|
-
)
|
|
1375
|
+
const authors = Object.keys(dataMap)
|
|
1376
|
+
const allPeriods = Array.from(
|
|
1377
|
+
new Set(authors.flatMap((a) => Object.keys(dataMap[a])))
|
|
1378
|
+
).sort()
|
|
1346
1379
|
|
|
1347
|
-
const series = authors.map(a => ({
|
|
1380
|
+
const series = authors.map((a) => ({
|
|
1348
1381
|
name: a,
|
|
1349
1382
|
type: 'line',
|
|
1350
1383
|
smooth: true,
|
|
1351
|
-
data: allPeriods.map(p => dataMap[a][p] || 0)
|
|
1352
|
-
}))
|
|
1384
|
+
data: allPeriods.map((p) => dataMap[a][p] || 0)
|
|
1385
|
+
}))
|
|
1353
1386
|
|
|
1354
|
-
return { authors, allPeriods, series }
|
|
1387
|
+
return { authors, allPeriods, series }
|
|
1355
1388
|
}
|
|
1356
1389
|
|
|
1357
1390
|
const drawChangeTrends = (stats) => {
|
|
@@ -1361,6 +1394,7 @@ const drawChangeTrends = (stats) => {
|
|
|
1361
1394
|
|
|
1362
1395
|
function render(t) {
|
|
1363
1396
|
const { authors, allPeriods, series } = buildDataset(stats, t)
|
|
1397
|
+
|
|
1364
1398
|
chart.setOption({
|
|
1365
1399
|
tooltip: { trigger: 'axis' },
|
|
1366
1400
|
legend: { data: authors },
|
|
@@ -1387,13 +1421,7 @@ const drawChangeTrends = (stats) => {
|
|
|
1387
1421
|
}
|
|
1388
1422
|
|
|
1389
1423
|
// ========= 开发者加班趋势(基于 commits 现场计算) =========
|
|
1390
|
-
function buildAuthorOvertimeDataset(
|
|
1391
|
-
commits,
|
|
1392
|
-
type,
|
|
1393
|
-
startHour,
|
|
1394
|
-
endHour,
|
|
1395
|
-
cutoff
|
|
1396
|
-
) {
|
|
1424
|
+
function buildAuthorOvertimeDataset(commits, type, startHour, endHour, cutoff) {
|
|
1397
1425
|
const byAuthor = new Map()
|
|
1398
1426
|
const periods = new Set()
|
|
1399
1427
|
|
|
@@ -1428,7 +1456,7 @@ function buildAuthorOvertimeDataset(
|
|
|
1428
1456
|
name: a,
|
|
1429
1457
|
type: 'line',
|
|
1430
1458
|
smooth: true,
|
|
1431
|
-
data: allPeriods.map((p) =>
|
|
1459
|
+
data: allPeriods.map((p) => byAuthor.get(a)[p] || 0)
|
|
1432
1460
|
}))
|
|
1433
1461
|
return { authors, allPeriods, series }
|
|
1434
1462
|
}
|
|
@@ -1456,8 +1484,45 @@ function drawAuthorOvertimeTrends(commits, stats) {
|
|
|
1456
1484
|
endHour,
|
|
1457
1485
|
cutoff
|
|
1458
1486
|
)
|
|
1487
|
+
ds.rangeMap = {}
|
|
1488
|
+
|
|
1489
|
+
for (const period of ds.allPeriods) {
|
|
1490
|
+
if (period.includes('-W')) {
|
|
1491
|
+
const [yy, ww] = period.split('-W')
|
|
1492
|
+
ds.rangeMap[period] = getISOWeekRange(Number(yy), Number(ww))
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1459
1495
|
chart.setOption({
|
|
1460
|
-
tooltip: {
|
|
1496
|
+
tooltip: {
|
|
1497
|
+
trigger: 'axis',
|
|
1498
|
+
formatter(params) {
|
|
1499
|
+
if (!params || !params.length) return ''
|
|
1500
|
+
|
|
1501
|
+
const p = params[0]
|
|
1502
|
+
const label = p.axisValue
|
|
1503
|
+
const isWeekly = type === 'weekly'
|
|
1504
|
+
|
|
1505
|
+
let extra = ''
|
|
1506
|
+
if (isWeekly && ds.rangeMap && ds.rangeMap[label]) {
|
|
1507
|
+
const { start, end } = ds.rangeMap[label]
|
|
1508
|
+
extra = `<div style="margin-top:4px;color:#999;font-size:12px">
|
|
1509
|
+
周区间:${start} ~ ${end}
|
|
1510
|
+
</div>`
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
const lines = params
|
|
1514
|
+
.map(
|
|
1515
|
+
(item) => `${item.marker}${item.seriesName}: ${item.data} 小时`
|
|
1516
|
+
)
|
|
1517
|
+
.join('<br/>')
|
|
1518
|
+
|
|
1519
|
+
return `
|
|
1520
|
+
<div>${label}</div>
|
|
1521
|
+
${extra}
|
|
1522
|
+
${lines}
|
|
1523
|
+
`
|
|
1524
|
+
}
|
|
1525
|
+
},
|
|
1461
1526
|
legend: { data: ds.authors },
|
|
1462
1527
|
xAxis: { type: 'category', data: ds.allPeriods },
|
|
1463
1528
|
yAxis: { type: 'value' },
|
|
@@ -1483,6 +1548,7 @@ function drawAuthorOvertimeTrends(commits, stats) {
|
|
|
1483
1548
|
renderMonthlyRiskSummary(commits, { startHour, endHour, cutoff })
|
|
1484
1549
|
renderWeeklyDurationRiskSummary(commits, { startHour, endHour, cutoff })
|
|
1485
1550
|
renderMonthlyDurationRiskSummary(commits, { startHour, endHour, cutoff })
|
|
1551
|
+
renderRolling30DurationRiskSummary(commits, { startHour, endHour, cutoff })
|
|
1486
1552
|
|
|
1487
1553
|
return chart
|
|
1488
1554
|
}
|
|
@@ -1532,7 +1598,9 @@ function renderWeeklyRiskSummary(
|
|
|
1532
1598
|
const curTotal = Array.from(curMap.values()).reduce((a, b) => a + b, 0)
|
|
1533
1599
|
const prevTotal = Array.from(prevMap.values()).reduce((a, b) => a + b, 0)
|
|
1534
1600
|
const delta =
|
|
1535
|
-
prevTotal > 0
|
|
1601
|
+
prevTotal > 0
|
|
1602
|
+
? Math.round(((curTotal - prevTotal) / prevTotal) * 100)
|
|
1603
|
+
: null
|
|
1536
1604
|
|
|
1537
1605
|
// 找当前周最“活跃”的人(加班提交最多),并统计他加班的自然日数
|
|
1538
1606
|
let topAuthor = null
|
|
@@ -1595,7 +1663,9 @@ function computeAuthorDailyMaxOvertime(commits, startHour, endHour, cutoff) {
|
|
|
1595
1663
|
dayKey = d.toISOString().slice(0, 10)
|
|
1596
1664
|
} else if (h >= 0 && h < cutoff && h < startHour) {
|
|
1597
1665
|
overtime = 24 - endHour + h
|
|
1598
|
-
const cur = new Date(
|
|
1666
|
+
const cur = new Date(
|
|
1667
|
+
Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())
|
|
1668
|
+
)
|
|
1599
1669
|
cur.setUTCDate(cur.getUTCDate() - 1)
|
|
1600
1670
|
dayKey = cur.toISOString().slice(0, 10)
|
|
1601
1671
|
}
|
|
@@ -1617,7 +1687,12 @@ function renderWeeklyDurationRiskSummary(
|
|
|
1617
1687
|
if (!box) return
|
|
1618
1688
|
const now = new Date()
|
|
1619
1689
|
const curWeek = getIsoWeekKey(now.toISOString().slice(0, 10))
|
|
1620
|
-
const byAuthorDay = computeAuthorDailyMaxOvertime(
|
|
1690
|
+
const byAuthorDay = computeAuthorDailyMaxOvertime(
|
|
1691
|
+
commits,
|
|
1692
|
+
startHour,
|
|
1693
|
+
endHour,
|
|
1694
|
+
cutoff
|
|
1695
|
+
)
|
|
1621
1696
|
const sums = []
|
|
1622
1697
|
byAuthorDay.forEach((dayMap, author) => {
|
|
1623
1698
|
let total = 0
|
|
@@ -1638,7 +1713,9 @@ function renderWeeklyDurationRiskSummary(
|
|
|
1638
1713
|
let level = '轻度'
|
|
1639
1714
|
if (total >= 12) level = '严重'
|
|
1640
1715
|
else if (total >= 6) level = '中度'
|
|
1641
|
-
lines.push(
|
|
1716
|
+
lines.push(
|
|
1717
|
+
`${author} 本周累计加班 ${total.toFixed(2)} 小时(${level})。`
|
|
1718
|
+
)
|
|
1642
1719
|
})
|
|
1643
1720
|
}
|
|
1644
1721
|
box.innerHTML = `
|
|
@@ -1662,7 +1739,12 @@ function renderMonthlyDurationRiskSummary(
|
|
|
1662
1739
|
if (!box) return
|
|
1663
1740
|
const now = new Date()
|
|
1664
1741
|
const curMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`
|
|
1665
|
-
const byAuthorDay = computeAuthorDailyMaxOvertime(
|
|
1742
|
+
const byAuthorDay = computeAuthorDailyMaxOvertime(
|
|
1743
|
+
commits,
|
|
1744
|
+
startHour,
|
|
1745
|
+
endHour,
|
|
1746
|
+
cutoff
|
|
1747
|
+
)
|
|
1666
1748
|
const sums = []
|
|
1667
1749
|
byAuthorDay.forEach((dayMap, author) => {
|
|
1668
1750
|
let total = 0
|
|
@@ -1683,7 +1765,9 @@ function renderMonthlyDurationRiskSummary(
|
|
|
1683
1765
|
let level = '轻度'
|
|
1684
1766
|
if (total >= 20) level = '严重'
|
|
1685
1767
|
else if (total >= 10) level = '中度'
|
|
1686
|
-
lines.push(
|
|
1768
|
+
lines.push(
|
|
1769
|
+
`${author} 本月累计加班 ${total.toFixed(2)} 小时(${level})。`
|
|
1770
|
+
)
|
|
1687
1771
|
})
|
|
1688
1772
|
}
|
|
1689
1773
|
box.innerHTML = `
|
|
@@ -1698,6 +1782,62 @@ function renderMonthlyDurationRiskSummary(
|
|
|
1698
1782
|
</div>
|
|
1699
1783
|
`
|
|
1700
1784
|
}
|
|
1785
|
+
|
|
1786
|
+
function renderRolling30DurationRiskSummary(
|
|
1787
|
+
commits,
|
|
1788
|
+
{ startHour = 9, endHour = 18, cutoff = 6 } = {}
|
|
1789
|
+
) {
|
|
1790
|
+
const box = document.getElementById('rolling30DurationRiskSummary')
|
|
1791
|
+
if (!box) return
|
|
1792
|
+
const now = new Date()
|
|
1793
|
+
const utcToday = new Date(
|
|
1794
|
+
Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
|
|
1795
|
+
)
|
|
1796
|
+
utcToday.setUTCDate(utcToday.getUTCDate() - 29)
|
|
1797
|
+
const startKey = utcToday.toISOString().slice(0, 10)
|
|
1798
|
+
|
|
1799
|
+
const byAuthorDay = computeAuthorDailyMaxOvertime(
|
|
1800
|
+
commits,
|
|
1801
|
+
startHour,
|
|
1802
|
+
endHour,
|
|
1803
|
+
cutoff
|
|
1804
|
+
)
|
|
1805
|
+
const sums = []
|
|
1806
|
+
byAuthorDay.forEach((dayMap, author) => {
|
|
1807
|
+
let total = 0
|
|
1808
|
+
dayMap.forEach((v, dayKey) => {
|
|
1809
|
+
if (dayKey >= startKey) total += v
|
|
1810
|
+
})
|
|
1811
|
+
if (total > 0) sums.push({ author, total })
|
|
1812
|
+
})
|
|
1813
|
+
sums.sort((a, b) => b.total - a.total)
|
|
1814
|
+
const top = sums.slice(0, 6)
|
|
1815
|
+
const lines = []
|
|
1816
|
+
lines.push('【最近30天加班时长风险】')
|
|
1817
|
+
if (top.length === 0) {
|
|
1818
|
+
lines.push('最近30天暂无加班时长风险。')
|
|
1819
|
+
} else {
|
|
1820
|
+
top.forEach(({ author, total }) => {
|
|
1821
|
+
let level = '轻度'
|
|
1822
|
+
if (total >= 20) level = '严重'
|
|
1823
|
+
else if (total >= 10) level = '中度'
|
|
1824
|
+
lines.push(
|
|
1825
|
+
`${author} 最近30天累计加班 ${total.toFixed(2)} 小时(${level})。`
|
|
1826
|
+
)
|
|
1827
|
+
})
|
|
1828
|
+
}
|
|
1829
|
+
box.innerHTML = `
|
|
1830
|
+
<div class="risk-summary">
|
|
1831
|
+
<div class="risk-title">【最近30天加班时长风险】</div>
|
|
1832
|
+
<ul>
|
|
1833
|
+
${lines
|
|
1834
|
+
.slice(1)
|
|
1835
|
+
.map((l) => `<li>${escapeHtml(l)}</li>`)
|
|
1836
|
+
.join('')}
|
|
1837
|
+
</ul>
|
|
1838
|
+
</div>
|
|
1839
|
+
`
|
|
1840
|
+
}
|
|
1701
1841
|
function renderMonthlyRiskSummary(
|
|
1702
1842
|
commits,
|
|
1703
1843
|
{ startHour = 9, endHour = 18, cutoff = 6 } = {}
|
|
@@ -1718,7 +1858,8 @@ function renderMonthlyRiskSummary(
|
|
|
1718
1858
|
const d = new Date(c.date)
|
|
1719
1859
|
if (Number.isNaN(d.valueOf())) return
|
|
1720
1860
|
const h = d.getHours()
|
|
1721
|
-
const isOT =
|
|
1861
|
+
const isOT =
|
|
1862
|
+
(h >= endHour && h < 24) || (h >= 0 && h < cutoff && h < startHour)
|
|
1722
1863
|
if (!isOT) return
|
|
1723
1864
|
|
|
1724
1865
|
const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
|
|
@@ -1737,14 +1878,18 @@ function renderMonthlyRiskSummary(
|
|
|
1737
1878
|
const mm = monthMax.get(key)
|
|
1738
1879
|
const cur = mm.get(author)
|
|
1739
1880
|
const dateStr = d.toISOString().slice(0, 10)
|
|
1740
|
-
if (!cur || overtime > cur.max)
|
|
1881
|
+
if (!cur || overtime > cur.max)
|
|
1882
|
+
mm.set(author, { max: overtime, date: dateStr })
|
|
1741
1883
|
})
|
|
1742
1884
|
|
|
1743
1885
|
const curMap = monthAuthor.get(curKey) || new Map()
|
|
1744
1886
|
const prevMap = monthAuthor.get(prevKey) || new Map()
|
|
1745
1887
|
const curTotal = Array.from(curMap.values()).reduce((a, b) => a + b, 0)
|
|
1746
1888
|
const prevTotal = Array.from(prevMap.values()).reduce((a, b) => a + b, 0)
|
|
1747
|
-
const delta =
|
|
1889
|
+
const delta =
|
|
1890
|
+
prevTotal > 0
|
|
1891
|
+
? Math.round(((curTotal - prevTotal) / prevTotal) * 100)
|
|
1892
|
+
: null
|
|
1748
1893
|
|
|
1749
1894
|
let topAuthor = null
|
|
1750
1895
|
let top = { max: -1, date: null }
|
|
@@ -1782,7 +1927,9 @@ function renderMonthlyRiskSummary(
|
|
|
1782
1927
|
else if (top.max < prevMax) trend2 = '较上月提前'
|
|
1783
1928
|
else trend2 = '与上月持平'
|
|
1784
1929
|
}
|
|
1785
|
-
lines.push(
|
|
1930
|
+
lines.push(
|
|
1931
|
+
`${topAuthor} 本月最晚超出下班 ${top.max.toFixed(2)} 小时(${top.date}),${trend2}。`
|
|
1932
|
+
)
|
|
1786
1933
|
if (top.max >= 2) lines.push('已超过 2 小时,存在严重加班风险。')
|
|
1787
1934
|
else if (top.max >= 1) lines.push('已超过 1 小时,存在中度加班风险。')
|
|
1788
1935
|
}
|
|
@@ -1802,13 +1949,7 @@ function renderMonthlyRiskSummary(
|
|
|
1802
1949
|
}
|
|
1803
1950
|
|
|
1804
1951
|
// ========= 开发者加班“最晚”趋势(每期取最大超时) =========
|
|
1805
|
-
function buildAuthorLatestDataset(
|
|
1806
|
-
commits,
|
|
1807
|
-
type,
|
|
1808
|
-
startHour,
|
|
1809
|
-
endHour,
|
|
1810
|
-
cutoff
|
|
1811
|
-
) {
|
|
1952
|
+
function buildAuthorLatestDataset(commits, type, startHour, endHour, cutoff) {
|
|
1812
1953
|
const byAuthor = new Map()
|
|
1813
1954
|
const periods = new Set()
|
|
1814
1955
|
|
|
@@ -1819,8 +1960,7 @@ function buildAuthorLatestDataset(
|
|
|
1819
1960
|
|
|
1820
1961
|
let overtime = null
|
|
1821
1962
|
if (h >= endHour && h < 24) overtime = h - endHour
|
|
1822
|
-
else if (h >= 0 && h < cutoff && h < startHour)
|
|
1823
|
-
overtime = 24 - endHour + h
|
|
1963
|
+
else if (h >= 0 && h < cutoff && h < startHour) overtime = 24 - endHour + h
|
|
1824
1964
|
if (overtime == null) return
|
|
1825
1965
|
|
|
1826
1966
|
let key
|
|
@@ -1841,6 +1981,7 @@ function buildAuthorLatestDataset(
|
|
|
1841
1981
|
})
|
|
1842
1982
|
|
|
1843
1983
|
const allPeriods = Array.from(periods).sort()
|
|
1984
|
+
|
|
1844
1985
|
const authors = Array.from(byAuthor.keys()).sort()
|
|
1845
1986
|
const series = authors.map((a) => ({
|
|
1846
1987
|
name: a,
|
|
@@ -1874,8 +2015,45 @@ function drawAuthorLatestOvertimeTrends(commits, stats) {
|
|
|
1874
2015
|
endHour,
|
|
1875
2016
|
cutoff
|
|
1876
2017
|
)
|
|
2018
|
+
ds.rangeMap = {}
|
|
2019
|
+
|
|
2020
|
+
for (const period of ds.allPeriods) {
|
|
2021
|
+
if (period.includes('-W')) {
|
|
2022
|
+
const [yy, ww] = period.split('-W')
|
|
2023
|
+
ds.rangeMap[period] = getISOWeekRange(Number(yy), Number(ww))
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
1877
2026
|
chart.setOption({
|
|
1878
|
-
tooltip: {
|
|
2027
|
+
tooltip: {
|
|
2028
|
+
trigger: 'axis',
|
|
2029
|
+
formatter(params) {
|
|
2030
|
+
if (!params || !params.length) return ''
|
|
2031
|
+
|
|
2032
|
+
const p = params[0]
|
|
2033
|
+
const label = p.axisValue
|
|
2034
|
+
const isWeekly = type === 'weekly'
|
|
2035
|
+
|
|
2036
|
+
let extra = ''
|
|
2037
|
+
if (isWeekly && ds.rangeMap && ds.rangeMap[label]) {
|
|
2038
|
+
const { start, end } = ds.rangeMap[label]
|
|
2039
|
+
extra = `<div style="margin-top:4px;color:#999;font-size:12px">
|
|
2040
|
+
周区间:${start} ~ ${end}
|
|
2041
|
+
</div>`
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
const lines = params
|
|
2045
|
+
.map(
|
|
2046
|
+
(item) => `${item.marker}${item.seriesName}: ${item.data} 小时`
|
|
2047
|
+
)
|
|
2048
|
+
.join('<br/>')
|
|
2049
|
+
|
|
2050
|
+
return `
|
|
2051
|
+
<div>${label}</div>
|
|
2052
|
+
${extra}
|
|
2053
|
+
${lines}
|
|
2054
|
+
`
|
|
2055
|
+
}
|
|
2056
|
+
},
|
|
1879
2057
|
legend: { data: ds.authors },
|
|
1880
2058
|
xAxis: { type: 'category', data: ds.allPeriods },
|
|
1881
2059
|
yAxis: {
|
|
@@ -1926,8 +2104,7 @@ function renderLatestRiskSummary(
|
|
|
1926
2104
|
const h = d.getHours()
|
|
1927
2105
|
let overtime = null
|
|
1928
2106
|
if (h >= endHour && h < 24) overtime = h - endHour
|
|
1929
|
-
else if (h >= 0 && h < cutoff && h < startHour)
|
|
1930
|
-
overtime = 24 - endHour + h
|
|
2107
|
+
else if (h >= 0 && h < cutoff && h < startHour) overtime = 24 - endHour + h
|
|
1931
2108
|
if (overtime == null) return
|
|
1932
2109
|
|
|
1933
2110
|
const wKey = getIsoWeekKey(d.toISOString().slice(0, 10))
|
|
@@ -2059,7 +2236,9 @@ function renderLatestMonthlyRiskSummary(
|
|
|
2059
2236
|
else if (top.max < prevMax) trend = '较上月提前'
|
|
2060
2237
|
else trend = '与上月持平'
|
|
2061
2238
|
}
|
|
2062
|
-
lines.push(
|
|
2239
|
+
lines.push(
|
|
2240
|
+
`${topAuthor} 本月最晚超出下班 ${top.max.toFixed(2)} 小时(${top.date}),${trend}。`
|
|
2241
|
+
)
|
|
2063
2242
|
if (top.max >= 2) {
|
|
2064
2243
|
lines.push('已超过 2 小时,存在严重加班风险,请关注工作节奏。')
|
|
2065
2244
|
} else if (top.max >= 1) {
|
|
@@ -2089,8 +2268,7 @@ async function main() {
|
|
|
2089
2268
|
latestByDay,
|
|
2090
2269
|
config,
|
|
2091
2270
|
authorChanges
|
|
2092
|
-
} =
|
|
2093
|
-
await loadData()
|
|
2271
|
+
} = await loadData()
|
|
2094
2272
|
commitsAll = commits
|
|
2095
2273
|
filtered = commitsAll.slice()
|
|
2096
2274
|
window.__overtimeEndHour =
|
|
@@ -2137,7 +2315,6 @@ async function main() {
|
|
|
2137
2315
|
renderKpi(stats)
|
|
2138
2316
|
}
|
|
2139
2317
|
|
|
2140
|
-
|
|
2141
2318
|
// 抽屉关闭交互(按钮 + 点击遮罩)
|
|
2142
2319
|
document.getElementById('sidebarClose').onclick = () => {
|
|
2143
2320
|
document.getElementById('dayDetailSidebar').classList.remove('show')
|
package/web/index.html
CHANGED
|
@@ -86,6 +86,7 @@
|
|
|
86
86
|
<div id="monthlyRiskSummary" class="risk-summary-box"></div>
|
|
87
87
|
<div id="weeklyDurationRiskSummary" class="risk-summary-box"></div>
|
|
88
88
|
<div id="monthlyDurationRiskSummary" class="risk-summary-box"></div>
|
|
89
|
+
<div id="rolling30DurationRiskSummary" class="risk-summary-box"></div>
|
|
89
90
|
</div>
|
|
90
91
|
|
|
91
92
|
<div class="chart-card">
|
package/bin/wukong-gitlog-cli
DELETED