wukong-gitlog-cli 1.0.17 → 1.0.18

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 CHANGED
@@ -2,6 +2,15 @@
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.18](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.17...v1.0.18) (2025-12-05)
6
+
7
+
8
+ ### Features
9
+
10
+ * 🎸 buildAuthorLatestDataset ([e607836](https://github.com/tomatobybike/wukong-gitlog-cli/commit/e6078360d29e18d9a9f6f6346547821ef13c1e7d))
11
+ * 🎸 css ([f2c4a71](https://github.com/tomatobybike/wukong-gitlog-cli/commit/f2c4a715b594f37e3ff2fd34f641c57aac33c50a))
12
+ * 🎸 risk ([1f6a55e](https://github.com/tomatobybike/wukong-gitlog-cli/commit/1f6a55e68ad28cc9523019294eaa69bde0036d86))
13
+
5
14
  ### [1.0.17](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.16...v1.0.17) (2025-12-05)
6
15
 
7
16
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wukong-gitlog-cli",
3
- "version": "1.0.17",
3
+ "version": "1.0.18",
4
4
  "description": "Advanced Git commit log exporter with Excel/JSON/TXT output, grouping, stats and CLI.",
5
5
  "keywords": [
6
6
  "git",
package/web/app.js CHANGED
@@ -1,6 +1,21 @@
1
1
  /* eslint-disable import/no-absolute-path */
2
+ /* eslint-disable no-use-before-define */
3
+ /* global echarts */
2
4
  const formatDate = (d) => new Date(d).toLocaleString()
3
5
 
6
+ // ISO 周 key:YYYY-Www
7
+ function getIsoWeekKey(dStr) {
8
+ const d = new Date(dStr)
9
+ if (Number.isNaN(d.valueOf())) return null
10
+ const target = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()))
11
+ const dayNum = target.getUTCDay() || 7 // Sunday=0
12
+ target.setUTCDate(target.getUTCDate() + 4 - dayNum)
13
+ const yearStart = new Date(Date.UTC(target.getUTCFullYear(), 0, 1))
14
+ const weekNo = Math.ceil(((target - yearStart) / 86400000 + 1) / 7)
15
+ const year = target.getUTCFullYear()
16
+ return `${year}-W${String(weekNo).padStart(2, '0')}`
17
+ }
18
+
4
19
  async function loadData() {
5
20
  try {
6
21
  const [
@@ -45,8 +60,10 @@ function renderCommitsTablePage() {
45
60
  const start = (page - 1) * pageSize
46
61
  const end = start + pageSize
47
62
  filtered.slice(start, end).forEach((c) => {
63
+ // FIXME: remove debug log before production
64
+ console.log('❌', 'c', c);
48
65
  const tr = document.createElement('tr')
49
- tr.innerHTML = `<td>${c.hash.slice(0, 8)}</td><td>${c.author}</td><td>${formatDate(c.date)}</td><td>${c.message}</td>`
66
+ 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>`
50
67
  tbody.appendChild(tr)
51
68
  })
52
69
  }
@@ -215,13 +232,11 @@ function drawHourlyOvertime(stats, onHourClick) {
215
232
  if (typeof onHourClick === 'function') {
216
233
  chart.on('click', (p) => {
217
234
  let hour = Number(p.name)
218
- if(p.componentType === 'markLine') {
235
+ if (p.componentType === 'markLine') {
219
236
  hour = Number(p.data.xAxis)
220
237
  }
221
- // FIXME: remove debug log before production
222
- console.log('❌', 'hour', hour, p)
223
238
  document.getElementById('dayDetailSidebar').classList.remove('show')
224
- if (Object.is(hour, NaN)) return
239
+ if (Number.isNaN(hour)) return
225
240
  onHourClick(hour, commits[hour])
226
241
  })
227
242
  }
@@ -1239,12 +1254,12 @@ function computeAndRenderLatestOvertime(latestByDay) {
1239
1254
  // 每天的 latestHourNormalized → 超出下班的小时数
1240
1255
  const dailyOvertime = latestByDay
1241
1256
  .map((d) => {
1242
- const v =
1243
- typeof d.latestHourNormalized === 'number'
1244
- ? d.latestHourNormalized
1245
- : typeof d.latestHour === 'number'
1246
- ? d.latestHour
1247
- : null
1257
+ let v = null
1258
+ if (typeof d.latestHourNormalized === 'number') {
1259
+ v = d.latestHourNormalized
1260
+ } else if (typeof d.latestHour === 'number') {
1261
+ v = d.latestHour
1262
+ }
1248
1263
  if (v == null) return null
1249
1264
  const overtime = Math.max(0, Number(v) - endH)
1250
1265
  return { date: d.date, overtime, raw: v }
@@ -1266,21 +1281,6 @@ function computeAndRenderLatestOvertime(latestByDay) {
1266
1281
  )}</b> 小时,逻辑时间约 ${worstDay.raw.toFixed(2)} 点)`
1267
1282
  }
1268
1283
 
1269
- // 工具:根据日期字符串计算 ISO 周 key:YYYY-Www
1270
- const getIsoWeekKey = (dStr) => {
1271
- const d = new Date(dStr)
1272
- if (Number.isNaN(d.valueOf())) return null
1273
- const target = new Date(
1274
- Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())
1275
- )
1276
- const dayNum = target.getUTCDay() || 7 // Sunday=0
1277
- target.setUTCDate(target.getUTCDate() + 4 - dayNum)
1278
- const yearStart = new Date(Date.UTC(target.getUTCFullYear(), 0, 1))
1279
- const weekNo = Math.ceil(((target - yearStart) / 86400000 + 1) / 7)
1280
- const year = target.getUTCFullYear()
1281
- return `${year}-W${String(weekNo).padStart(2, '0')}`
1282
- }
1283
-
1284
1284
  // 2) 按周聚合:每周取「该周内任意一天的最大加班时长」
1285
1285
  const weekMap = new Map()
1286
1286
  dailyOvertime.forEach((d) => {
@@ -1354,43 +1354,436 @@ function buildDataset(stats, type) {
1354
1354
  return { authors, allPeriods, series };
1355
1355
  }
1356
1356
 
1357
- const drawChangeTrends = (stats, type = 'daily') => {
1358
- const el = document.getElementById("chartAuthorChanges");
1359
- if (!el) return null;
1360
- // FIXME: remove debug log before production
1361
- console.log('❌', 'el', el);
1362
- const chart = echarts.init(el);
1363
-
1364
- function render(type) {
1365
- const { authors, allPeriods, series } = buildDataset(stats, type);
1357
+ const drawChangeTrends = (stats) => {
1358
+ const el = document.getElementById('chartAuthorChanges')
1359
+ if (!el) return null
1360
+ const chart = echarts.init(el)
1366
1361
 
1362
+ function render(t) {
1363
+ const { authors, allPeriods, series } = buildDataset(stats, t)
1367
1364
  chart.setOption({
1368
1365
  tooltip: { trigger: 'axis' },
1369
1366
  legend: { data: authors },
1370
1367
  xAxis: { type: 'category', data: allPeriods },
1371
1368
  yAxis: { type: 'value' },
1372
1369
  series
1373
- });
1370
+ })
1374
1371
  }
1375
1372
 
1376
1373
  // 初次渲染:日
1377
- render('daily');
1374
+ render('daily')
1378
1375
 
1379
1376
  // tabs 切换
1380
- document.querySelectorAll('#tabs button').forEach(btn => {
1381
- btn.onclick = () => {
1382
- document.querySelectorAll('#tabs button').forEach(b => b.classList.remove('active'));
1383
- btn.classList.add('active');
1377
+ const tabs = document.querySelectorAll('#tabs button')
1378
+ tabs.forEach((btnEl) => {
1379
+ btnEl.addEventListener('click', () => {
1380
+ tabs.forEach((b) => b.classList.remove('active'))
1381
+ btnEl.classList.add('active')
1382
+ render(btnEl.dataset.type)
1383
+ })
1384
+ })
1385
+
1386
+ return chart
1387
+ }
1388
+
1389
+ // ========= 开发者加班趋势(基于 commits 现场计算) =========
1390
+ function buildAuthorOvertimeDataset(
1391
+ commits,
1392
+ type,
1393
+ startHour,
1394
+ endHour,
1395
+ cutoff
1396
+ ) {
1397
+ const byAuthor = new Map()
1398
+ const periods = new Set()
1399
+
1400
+ commits.forEach((c) => {
1401
+ const d = new Date(c.date)
1402
+ if (Number.isNaN(d.valueOf())) return
1403
+ const h = d.getHours()
1404
+ const isOvertime =
1405
+ (h >= endHour && h < 24) || (h >= 0 && h < cutoff && h < startHour)
1406
+ if (!isOvertime) return
1407
+
1408
+ let key
1409
+ if (type === 'daily') {
1410
+ key = d.toISOString().slice(0, 10)
1411
+ } else if (type === 'weekly') {
1412
+ key = getIsoWeekKey(d.toISOString().slice(0, 10))
1413
+ } else {
1414
+ key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
1415
+ }
1416
+ if (!key) return
1417
+ periods.add(key)
1418
+
1419
+ const author = c.author || 'unknown'
1420
+ if (!byAuthor.has(author)) byAuthor.set(author, {})
1421
+ const obj = byAuthor.get(author)
1422
+ obj[key] = (obj[key] || 0) + 1
1423
+ })
1424
+
1425
+ const allPeriods = Array.from(periods).sort()
1426
+ const authors = Array.from(byAuthor.keys()).sort()
1427
+ const series = authors.map((a) => ({
1428
+ name: a,
1429
+ type: 'line',
1430
+ smooth: true,
1431
+ data: allPeriods.map((p) => (byAuthor.get(a)[p] || 0))
1432
+ }))
1433
+ return { authors, allPeriods, series }
1434
+ }
1384
1435
 
1385
- render(btn.dataset.type);
1386
- };
1387
- });
1436
+ function drawAuthorOvertimeTrends(commits, stats) {
1437
+ const el = document.getElementById('chartAuthorOvertime')
1438
+ if (!el) return null
1439
+ const chart = echarts.init(el)
1388
1440
 
1389
- return chart;
1441
+ const startHour =
1442
+ typeof stats.startHour === 'number' && stats.startHour >= 0
1443
+ ? stats.startHour
1444
+ : 9
1445
+ const endHour =
1446
+ typeof stats.endHour === 'number' && stats.endHour >= 0
1447
+ ? stats.endHour
1448
+ : window.__overtimeEndHour || 18
1449
+ const cutoff = window.__overnightCutoff ?? 6
1450
+
1451
+ function render(type) {
1452
+ const ds = buildAuthorOvertimeDataset(
1453
+ commits,
1454
+ type,
1455
+ startHour,
1456
+ endHour,
1457
+ cutoff
1458
+ )
1459
+ chart.setOption({
1460
+ tooltip: { trigger: 'axis' },
1461
+ legend: { data: ds.authors },
1462
+ xAxis: { type: 'category', data: ds.allPeriods },
1463
+ yAxis: { type: 'value' },
1464
+ series: ds.series
1465
+ })
1466
+ }
1467
+
1468
+ // 初始按日
1469
+ render('daily')
1470
+
1471
+ // tabs 切换
1472
+ const tabs = document.querySelectorAll('#tabsOvertime button')
1473
+ tabs.forEach((btnEl) => {
1474
+ btnEl.addEventListener('click', () => {
1475
+ tabs.forEach((b) => b.classList.remove('active'))
1476
+ btnEl.classList.add('active')
1477
+ render(btnEl.dataset.type)
1478
+ })
1479
+ })
1480
+
1481
+ // 输出本周风险总结
1482
+ renderWeeklyRiskSummary(commits, { startHour, endHour, cutoff })
1483
+
1484
+ return chart
1485
+ }
1486
+
1487
+ function renderWeeklyRiskSummary(
1488
+ commits,
1489
+ { startHour = 9, endHour = 18, cutoff = 6 } = {}
1490
+ ) {
1491
+ const box = document.getElementById('weeklyRiskSummary')
1492
+ if (!box) return
1493
+
1494
+ // 获取当前周与上一周 key
1495
+ const now = new Date()
1496
+ const curKey = getIsoWeekKey(now.toISOString().slice(0, 10))
1497
+ const prev = new Date(now)
1498
+ prev.setDate(prev.getDate() - 7)
1499
+ const prevKey = getIsoWeekKey(prev.toISOString().slice(0, 10))
1500
+
1501
+ // 统计:每周 -> author -> count;同时统计每周日期集合
1502
+ const weekAuthor = new Map()
1503
+ const weekDatesByAuthor = new Map() // week -> author -> Set(date)
1504
+
1505
+ commits.forEach((c) => {
1506
+ const d = new Date(c.date)
1507
+ if (Number.isNaN(d.valueOf())) return
1508
+ const h = d.getHours()
1509
+ const isOT =
1510
+ (h >= endHour && h < 24) || (h >= 0 && h < cutoff && h < startHour)
1511
+ if (!isOT) return
1512
+
1513
+ const key = getIsoWeekKey(d.toISOString().slice(0, 10))
1514
+ if (!key) return
1515
+ const author = c.author || 'unknown'
1516
+
1517
+ if (!weekAuthor.has(key)) weekAuthor.set(key, new Map())
1518
+ const m = weekAuthor.get(key)
1519
+ m.set(author, (m.get(author) || 0) + 1)
1520
+
1521
+ if (!weekDatesByAuthor.has(key)) weekDatesByAuthor.set(key, new Map())
1522
+ const dMap = weekDatesByAuthor.get(key)
1523
+ if (!dMap.has(author)) dMap.set(author, new Set())
1524
+ dMap.get(author).add(d.toISOString().slice(0, 10))
1525
+ })
1526
+
1527
+ const curMap = weekAuthor.get(curKey) || new Map()
1528
+ const prevMap = weekAuthor.get(prevKey) || new Map()
1529
+ const curTotal = Array.from(curMap.values()).reduce((a, b) => a + b, 0)
1530
+ const prevTotal = Array.from(prevMap.values()).reduce((a, b) => a + b, 0)
1531
+ const delta =
1532
+ prevTotal > 0 ? Math.round(((curTotal - prevTotal) / prevTotal) * 100) : null
1533
+
1534
+ // 找当前周最“活跃”的人(加班提交最多),并统计他加班的自然日数
1535
+ let topAuthor = null
1536
+ let topCount = -1
1537
+ curMap.forEach((v, k) => {
1538
+ if (v > topCount) {
1539
+ topCount = v
1540
+ topAuthor = k
1541
+ }
1542
+ })
1543
+ const curDatesMap = weekDatesByAuthor.get(curKey) || new Map()
1544
+ const topDays =
1545
+ topAuthor && curDatesMap.get(topAuthor)
1546
+ ? curDatesMap.get(topAuthor).size
1547
+ : 0
1548
+
1549
+ // 文案
1550
+ const lines = []
1551
+ lines.push('【本周风险总结】')
1552
+
1553
+ if (curTotal === 0) {
1554
+ lines.push('团队本周暂无加班提交。')
1555
+ } else if (delta === null) {
1556
+ lines.push(`团队本周加班提交 ${curTotal} 次。`)
1557
+ } else {
1558
+ const trend = delta >= 0 ? '上升' : '下降'
1559
+ lines.push(`团队加班${trend} ${Math.abs(delta)}%(vs 上周)。`)
1560
+ }
1561
+
1562
+ if (topAuthor && curTotal > 0) {
1563
+ const pct = Math.round((topCount / curTotal) * 100)
1564
+ lines.push(
1565
+ `${topAuthor} 夜间活跃度 ${pct}%,${topDays} 天出现下班后提交(${endHour}:00 后或次日 ${cutoff}:00 前)。`
1566
+ )
1567
+ }
1568
+
1569
+ box.innerHTML = `
1570
+ <div class="risk-summary">
1571
+ <div class="risk-title">【本周风险总结】</div>
1572
+ <ul>
1573
+ ${lines
1574
+ .slice(1)
1575
+ .map((l) => `<li>${escapeHtml(l)}</li>`)
1576
+ .join('')}
1577
+ </ul>
1578
+ </div>
1579
+ `
1580
+ }
1581
+
1582
+ // ========= 开发者加班“最晚”趋势(每期取最大超时) =========
1583
+ function buildAuthorLatestDataset(
1584
+ commits,
1585
+ type,
1586
+ startHour,
1587
+ endHour,
1588
+ cutoff
1589
+ ) {
1590
+ const byAuthor = new Map()
1591
+ const periods = new Set()
1592
+
1593
+ commits.forEach((c) => {
1594
+ const d = new Date(c.date)
1595
+ if (Number.isNaN(d.valueOf())) return
1596
+ const h = d.getHours()
1597
+
1598
+ let overtime = null
1599
+ if (h >= endHour && h < 24) overtime = h - endHour
1600
+ else if (h >= 0 && h < cutoff && h < startHour)
1601
+ overtime = 24 - endHour + h
1602
+ if (overtime == null) return
1603
+
1604
+ let key
1605
+ if (type === 'daily') {
1606
+ key = d.toISOString().slice(0, 10)
1607
+ } else if (type === 'weekly') {
1608
+ key = getIsoWeekKey(d.toISOString().slice(0, 10))
1609
+ } else {
1610
+ key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
1611
+ }
1612
+ if (!key) return
1613
+ periods.add(key)
1614
+
1615
+ const author = c.author || 'unknown'
1616
+ if (!byAuthor.has(author)) byAuthor.set(author, {})
1617
+ const obj = byAuthor.get(author)
1618
+ obj[key] = Math.max(obj[key] || 0, overtime)
1619
+ })
1620
+
1621
+ const allPeriods = Array.from(periods).sort()
1622
+ const authors = Array.from(byAuthor.keys()).sort()
1623
+ const series = authors.map((a) => ({
1624
+ name: a,
1625
+ type: 'line',
1626
+ smooth: true,
1627
+ data: allPeriods.map((p) => byAuthor.get(a)[p] || 0)
1628
+ }))
1629
+ return { authors, allPeriods, series }
1630
+ }
1631
+
1632
+ function drawAuthorLatestOvertimeTrends(commits, stats) {
1633
+ const el = document.getElementById('chartAuthorLatestOvertime')
1634
+ if (!el) return null
1635
+ const chart = echarts.init(el)
1636
+
1637
+ const startHour =
1638
+ typeof stats.startHour === 'number' && stats.startHour >= 0
1639
+ ? stats.startHour
1640
+ : 9
1641
+ const endHour =
1642
+ typeof stats.endHour === 'number' && stats.endHour >= 0
1643
+ ? stats.endHour
1644
+ : window.__overtimeEndHour || 18
1645
+ const cutoff = window.__overnightCutoff ?? 6
1646
+
1647
+ function render(type) {
1648
+ const ds = buildAuthorLatestDataset(
1649
+ commits,
1650
+ type,
1651
+ startHour,
1652
+ endHour,
1653
+ cutoff
1654
+ )
1655
+ chart.setOption({
1656
+ tooltip: { trigger: 'axis' },
1657
+ legend: { data: ds.authors },
1658
+ xAxis: { type: 'category', data: ds.allPeriods },
1659
+ yAxis: {
1660
+ type: 'value',
1661
+ name: '超出下班(小时)',
1662
+ min: 0
1663
+ },
1664
+ series: ds.series
1665
+ })
1666
+ }
1667
+
1668
+ render('daily')
1669
+
1670
+ const tabs = document.querySelectorAll('#tabsLatestOvertime button')
1671
+ tabs.forEach((btnEl) => {
1672
+ btnEl.addEventListener('click', () => {
1673
+ tabs.forEach((b) => b.classList.remove('active'))
1674
+ btnEl.classList.add('active')
1675
+ render(btnEl.dataset.type)
1676
+ })
1677
+ })
1678
+
1679
+ renderLatestRiskSummary(commits, { startHour, endHour, cutoff })
1680
+
1681
+ return chart
1682
+ }
1683
+
1684
+ // 本周“最晚加班”风险提示
1685
+ function renderLatestRiskSummary(
1686
+ commits,
1687
+ { startHour = 9, endHour = 18, cutoff = 6 } = {}
1688
+ ) {
1689
+ const box = document.getElementById('latestRiskSummary')
1690
+ if (!box) return
1691
+
1692
+ const now = new Date()
1693
+ const curKey = getIsoWeekKey(now.toISOString().slice(0, 10))
1694
+ const prev = new Date(now)
1695
+ prev.setDate(prev.getDate() - 7)
1696
+ const prevKey = getIsoWeekKey(prev.toISOString().slice(0, 10))
1697
+
1698
+ // 统计每周每人最大超时
1699
+ const weekMax = new Map() // week -> Map(author -> {max, date})
1700
+ commits.forEach((c) => {
1701
+ const d = new Date(c.date)
1702
+ if (Number.isNaN(d.valueOf())) return
1703
+ const h = d.getHours()
1704
+ let overtime = null
1705
+ if (h >= endHour && h < 24) overtime = h - endHour
1706
+ else if (h >= 0 && h < cutoff && h < startHour)
1707
+ overtime = 24 - endHour + h
1708
+ if (overtime == null) return
1709
+
1710
+ const wKey = getIsoWeekKey(d.toISOString().slice(0, 10))
1711
+ if (!wKey) return
1712
+ if (!weekMax.has(wKey)) weekMax.set(wKey, new Map())
1713
+ const m = weekMax.get(wKey)
1714
+ const author = c.author || 'unknown'
1715
+ const cur = m.get(author)
1716
+ if (!cur || overtime > cur.max) {
1717
+ m.set(author, { max: overtime, date: d.toISOString().slice(0, 10) })
1718
+ }
1719
+ })
1720
+
1721
+ const curMap = weekMax.get(curKey) || new Map()
1722
+ const prevMap = weekMax.get(prevKey) || new Map()
1723
+
1724
+ // 当前周的全局最晚
1725
+ let topAuthor = null
1726
+ let top = { max: -1, date: null }
1727
+ curMap.forEach((v, k) => {
1728
+ if (v.max > top.max) {
1729
+ top = v
1730
+ topAuthor = k
1731
+ }
1732
+ })
1733
+
1734
+ // 上周全局最晚,用于趋势判断
1735
+ let prevMax = -1
1736
+ prevMap.forEach((v) => {
1737
+ if (v.max > prevMax) prevMax = v.max
1738
+ })
1739
+
1740
+ const lines = []
1741
+ lines.push('【本周最晚加班风险】')
1742
+
1743
+ if (top.max < 0) {
1744
+ lines.push('本周尚无下班后/凌晨提交,未发现明显风险。')
1745
+ } else {
1746
+ let trend = '暂无上周对比'
1747
+ if (prevMax >= 0) {
1748
+ if (top.max > prevMax) trend = '较上周更晚'
1749
+ else if (top.max < prevMax) trend = '较上周提前'
1750
+ else trend = '与上周持平'
1751
+ }
1752
+ lines.push(
1753
+ `${topAuthor} 本周最晚超出下班 ${top.max.toFixed(
1754
+ 2
1755
+ )} 小时(${top.date}),${trend}。`
1756
+ )
1757
+ if (top.max >= 2) {
1758
+ lines.push('已超过 2 小时,存在严重加班风险,请关注工作节奏。')
1759
+ } else if (top.max >= 1) {
1760
+ lines.push('已超过 1 小时,注意控制夜间工作时长。')
1761
+ }
1762
+ }
1763
+
1764
+ box.innerHTML = `
1765
+ <div class="risk-summary">
1766
+ <div class="risk-title">【本周最晚加班风险】</div>
1767
+ <ul>
1768
+ ${lines
1769
+ .slice(1)
1770
+ .map((l) => `<li>${escapeHtml(l)}</li>`)
1771
+ .join('')}
1772
+ </ul>
1773
+ </div>
1774
+ `
1390
1775
  }
1391
1776
 
1392
1777
  async function main() {
1393
- const { commits, stats, weekly, monthly, latestByDay, config,authorChanges } =
1778
+ const {
1779
+ commits,
1780
+ stats,
1781
+ weekly,
1782
+ monthly,
1783
+ latestByDay,
1784
+ config,
1785
+ authorChanges
1786
+ } =
1394
1787
  await loadData()
1395
1788
  commitsAll = commits
1396
1789
  filtered = commitsAll.slice()
@@ -1404,7 +1797,7 @@ async function main() {
1404
1797
  updatePager()
1405
1798
  renderCommitsTablePage()
1406
1799
 
1407
- drawHourlyOvertime(stats, (hour, count) => {
1800
+ drawHourlyOvertime(stats, (hour) => {
1408
1801
  // 使用举例
1409
1802
  const hourCommitsDetail = groupCommitsByHour(commits)
1410
1803
  // 将 commit 列表传给侧栏(若没有详情,则传空数组)
@@ -1431,7 +1824,9 @@ async function main() {
1431
1824
 
1432
1825
  console.log('最累的一天:', daily.analysis.mostTiredDay)
1433
1826
 
1434
- drawChangeTrends(authorChanges, 'daily');
1827
+ drawChangeTrends(authorChanges)
1828
+ drawAuthorOvertimeTrends(commits, stats)
1829
+ drawAuthorLatestOvertimeTrends(commits, stats)
1435
1830
  computeAndRenderLatestOvertime(latestByDay)
1436
1831
  renderKpi(stats)
1437
1832
  }
package/web/index.html CHANGED
@@ -53,18 +53,20 @@
53
53
  <h2>按日提交趋势</h2>
54
54
  <div id="dailyTrendChartDog" class="echart"></div>
55
55
  </div>
56
- <div id="mostTiredDay"></div>
57
- <div id="mostTiredWeek"></div>
58
- <div id="mostTiredMonth"></div>
59
- <div id="latestOvertimeDay"></div>
60
- <div id="latestOvertimeWeek"></div>
61
- <div id="latestOvertimeMonth"></div>
56
+ <div class="chart-card">
57
+ <div id="mostTiredDay"></div>
58
+ <div id="mostTiredWeek"></div>
59
+ <div id="mostTiredMonth"></div>
60
+ <div id="latestOvertimeDay"></div>
61
+ <div id="latestOvertimeWeek"></div>
62
+ <div id="latestOvertimeMonth"></div>
63
+ </div>
62
64
  </section>
63
65
 
64
66
  <div class="chart-card">
65
67
  <h2>开发者 Changed 工作量趋势</h2>
66
68
 
67
- <div id="tabs">
69
+ <div id="tabs" class="tabs">
68
70
  <button data-type="daily" class="active">按日</button>
69
71
  <button data-type="weekly">按周</button>
70
72
  <button data-type="monthly">按月</button>
@@ -72,6 +74,28 @@
72
74
  <div id="chartAuthorChanges" class="echart"></div>
73
75
  </div>
74
76
 
77
+ <div class="chart-card">
78
+ <h2>开发者 加班趋势</h2>
79
+ <div class="tabs" id="tabsOvertime">
80
+ <button data-type="daily" class="active">按日</button>
81
+ <button data-type="weekly">按周</button>
82
+ <button data-type="monthly">按月</button>
83
+ </div>
84
+ <div id="chartAuthorOvertime" class="echart"></div>
85
+ <div id="weeklyRiskSummary" class="risk-summary-box"></div>
86
+ </div>
87
+
88
+ <div class="chart-card">
89
+ <h2>开发者 加班最晚趋势</h2>
90
+ <div class="tabs" id="tabsLatestOvertime">
91
+ <button data-type="daily" class="active">按日</button>
92
+ <button data-type="weekly">按周</button>
93
+ <button data-type="monthly">按月</button>
94
+ </div>
95
+ <div id="chartAuthorLatestOvertime" class="echart"></div>
96
+ <div id="latestRiskSummary" class="risk-summary-box"></div>
97
+ </div>
98
+
75
99
  <section class="table-card">
76
100
  <h2>提交清单</h2>
77
101
  <div id="tableControls">
@@ -100,6 +124,7 @@
100
124
  <th>Author</th>
101
125
  <th>Date</th>
102
126
  <th>Message</th>
127
+ <th>Changed</th>
103
128
  </tr>
104
129
  </thead>
105
130
  <tbody></tbody>
@@ -346,6 +346,82 @@ td {
346
346
  word-break: break-word;
347
347
  }
348
348
 
349
+ /* Tabs(工作量/加班趋势) ----------------------------------- */
350
+ .tabs {
351
+ display: inline-flex;
352
+ gap: 8px;
353
+ margin-bottom: 10px;
354
+ }
355
+
356
+ .tabs button {
357
+ padding: 8px 14px;
358
+ border-radius: 999px;
359
+ border: 1px solid rgba(148, 163, 184, 0.5);
360
+ background: #fff;
361
+ color: #111827;
362
+ cursor: pointer;
363
+ font-size: 13px;
364
+ transition:
365
+ background 0.2s ease,
366
+ color 0.2s ease,
367
+ border-color 0.2s ease,
368
+ box-shadow 0.2s ease;
369
+ }
370
+
371
+ .tabs button:hover {
372
+ background: #f3f4ff;
373
+ border-color: #6366f1;
374
+ color: #111827;
375
+ box-shadow: 0 2px 6px rgba(99, 102, 241, 0.16);
376
+ }
377
+
378
+ .tabs button.active {
379
+ background: #4f46e5;
380
+ color: #fff;
381
+ border-color: #4f46e5;
382
+ box-shadow: 0 3px 10px rgba(79, 70, 229, 0.25);
383
+ }
384
+
385
+ /* 风险总结卡片 ---------------------------------------------- */
386
+ /* 风险总结卡片 — MUI风格 ---------------------------------------------- */
387
+ .risk-summary {
388
+ margin-top: 16px;
389
+ padding: 16px 18px;
390
+ border-radius: 12px;
391
+
392
+ /* 采用 MUI Error Alert 的色板 */
393
+ background: rgba(253, 237, 237, 0.8); /* MUI error.light */
394
+ border: 1px solid rgba(244, 199, 199, 0.9);
395
+
396
+ /* MUI 风格的柔和阴影 */
397
+ box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.08),
398
+ 0px 1px 2px rgba(0, 0, 0, 0.04);
399
+
400
+ color: #b71c1c; /* 比 #991b1b 更接近 MUI error.dark */
401
+ font-size: 14px;
402
+ line-height: 1.65;
403
+ }
404
+
405
+ /* 标题更粗+颜色更深 */
406
+ .risk-summary .risk-title {
407
+ font-weight: 700;
408
+ font-size: 15px;
409
+ margin-bottom: 6px;
410
+ color: #b71c1c;
411
+ }
412
+
413
+ /* 列表 */
414
+ .risk-summary ul {
415
+ margin: 8px 0 0 20px;
416
+ padding: 0;
417
+ color: #7f1d1d;
418
+ }
419
+
420
+ .risk-summary li {
421
+ margin-bottom: 6px;
422
+ }
423
+
424
+
349
425
  /* 底部 -------------------------------------------------- */
350
426
  footer {
351
427
  text-align: center;