wukong-gitlog-cli 1.0.16 → 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 +17 -0
- package/package.json +9 -8
- package/src/cli.mjs +31 -2
- package/src/excel.mjs +116 -46
- package/src/git.mjs +33 -7
- package/src/json.mjs +5 -0
- package/src/stats-text.mjs +51 -0
- package/src/stats.mjs +44 -0
- package/src/text.mjs +37 -42
- package/web/app.js +528 -73
- package/web/index.html +42 -6
- package/web/static/style.css +76 -0
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 [
|
|
@@ -9,14 +24,16 @@ async function loadData() {
|
|
|
9
24
|
weeklyModule,
|
|
10
25
|
monthlyModule,
|
|
11
26
|
latestByDayModule,
|
|
12
|
-
configModule
|
|
27
|
+
configModule,
|
|
28
|
+
authorChangesModule
|
|
13
29
|
] = await Promise.all([
|
|
14
30
|
import('/data/commits.mjs'),
|
|
15
31
|
import('/data/overtime-stats.mjs'),
|
|
16
32
|
import('/data/overtime-weekly.mjs'),
|
|
17
33
|
import('/data/overtime-monthly.mjs').catch(() => ({ default: [] })),
|
|
18
34
|
import('/data/overtime-latest-by-day.mjs').catch(() => ({ default: [] })),
|
|
19
|
-
import('/data/config.mjs').catch(() => ({ default: {} }))
|
|
35
|
+
import('/data/config.mjs').catch(() => ({ default: {} })),
|
|
36
|
+
import('/data/author-changes.mjs').catch(() => ({ default: {} }))
|
|
20
37
|
])
|
|
21
38
|
const commits = commitsModule.default || []
|
|
22
39
|
const stats = statsModule.default || {}
|
|
@@ -24,7 +41,8 @@ async function loadData() {
|
|
|
24
41
|
const monthly = monthlyModule.default || []
|
|
25
42
|
const latestByDay = latestByDayModule.default || []
|
|
26
43
|
const config = configModule.default || {}
|
|
27
|
-
|
|
44
|
+
const authorChanges = authorChangesModule.default || {}
|
|
45
|
+
return { commits, stats, weekly, monthly, latestByDay, config, authorChanges }
|
|
28
46
|
} catch (err) {
|
|
29
47
|
console.error('Load data failed', err)
|
|
30
48
|
return { commits: [], stats: {}, weekly: [], monthly: [], latestByDay: [] }
|
|
@@ -42,8 +60,10 @@ function renderCommitsTablePage() {
|
|
|
42
60
|
const start = (page - 1) * pageSize
|
|
43
61
|
const end = start + pageSize
|
|
44
62
|
filtered.slice(start, end).forEach((c) => {
|
|
63
|
+
// FIXME: remove debug log before production
|
|
64
|
+
console.log('❌', 'c', c);
|
|
45
65
|
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>`
|
|
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>`
|
|
47
67
|
tbody.appendChild(tr)
|
|
48
68
|
})
|
|
49
69
|
}
|
|
@@ -212,13 +232,11 @@ function drawHourlyOvertime(stats, onHourClick) {
|
|
|
212
232
|
if (typeof onHourClick === 'function') {
|
|
213
233
|
chart.on('click', (p) => {
|
|
214
234
|
let hour = Number(p.name)
|
|
215
|
-
if(p.componentType === 'markLine') {
|
|
235
|
+
if (p.componentType === 'markLine') {
|
|
216
236
|
hour = Number(p.data.xAxis)
|
|
217
237
|
}
|
|
218
|
-
// FIXME: remove debug log before production
|
|
219
|
-
console.log('❌', 'hour', hour, p)
|
|
220
238
|
document.getElementById('dayDetailSidebar').classList.remove('show')
|
|
221
|
-
if (
|
|
239
|
+
if (Number.isNaN(hour)) return
|
|
222
240
|
onHourClick(hour, commits[hour])
|
|
223
241
|
})
|
|
224
242
|
}
|
|
@@ -1226,50 +1244,6 @@ function groupCommitsByHour(commits) {
|
|
|
1226
1244
|
return byHour
|
|
1227
1245
|
}
|
|
1228
1246
|
|
|
1229
|
-
async function main() {
|
|
1230
|
-
const { commits, stats, weekly, monthly, latestByDay, config } =
|
|
1231
|
-
await loadData()
|
|
1232
|
-
commitsAll = commits
|
|
1233
|
-
filtered = commitsAll.slice()
|
|
1234
|
-
window.__overtimeEndHour =
|
|
1235
|
-
stats && typeof stats.endHour === 'number'
|
|
1236
|
-
? stats.endHour
|
|
1237
|
-
: (config.endHour ?? 18)
|
|
1238
|
-
window.__overnightCutoff =
|
|
1239
|
-
typeof config.overnightCutoff === 'number' ? config.overnightCutoff : 6
|
|
1240
|
-
initTableControls()
|
|
1241
|
-
updatePager()
|
|
1242
|
-
renderCommitsTablePage()
|
|
1243
|
-
|
|
1244
|
-
drawHourlyOvertime(stats, (hour, count) => {
|
|
1245
|
-
// 使用举例
|
|
1246
|
-
const hourCommitsDetail = groupCommitsByHour(commits)
|
|
1247
|
-
// 将 commit 列表传给侧栏(若没有详情,则传空数组)
|
|
1248
|
-
showSideBarForHour(hour, hourCommitsDetail[hour] || [])
|
|
1249
|
-
})
|
|
1250
|
-
drawOutsideVsInside(stats)
|
|
1251
|
-
|
|
1252
|
-
// 按日提交趋势:点击某天打开抽屉,显示当日所有 commits
|
|
1253
|
-
drawDailyTrend(commits, showDayDetailSidebar)
|
|
1254
|
-
|
|
1255
|
-
// 周趋势:保持原有点击行为(显示该周详情)
|
|
1256
|
-
drawWeeklyTrend(weekly, commits, showSideBarForWeek)
|
|
1257
|
-
|
|
1258
|
-
// 月趋势(加班占比):点击某个月打开抽屉,显示该月所有 commits
|
|
1259
|
-
drawMonthlyTrend(monthly, commits, showDayDetailSidebar)
|
|
1260
|
-
|
|
1261
|
-
// 每日最晚提交时间(小时):点击某天打开抽屉,显示当日所有 commits
|
|
1262
|
-
drawLatestHourDaily(latestByDay, commits, showDayDetailSidebar)
|
|
1263
|
-
|
|
1264
|
-
// 每日超过下班的小时数:点击某天打开抽屉,显示当日所有 commits
|
|
1265
|
-
drawDailySeverity(latestByDay, commits, showDayDetailSidebar)
|
|
1266
|
-
|
|
1267
|
-
const daily = drawDailyTrendSeverity(commits, weekly, showDayDetailSidebar)
|
|
1268
|
-
|
|
1269
|
-
console.log('最累的一天:', daily.analysis.mostTiredDay)
|
|
1270
|
-
computeAndRenderLatestOvertime(latestByDay)
|
|
1271
|
-
renderKpi(stats)
|
|
1272
|
-
}
|
|
1273
1247
|
|
|
1274
1248
|
// 基于 latestByDay + cutoff/endHour 统计「最晚加班的一天 / 一周 / 一月」
|
|
1275
1249
|
function computeAndRenderLatestOvertime(latestByDay) {
|
|
@@ -1280,12 +1254,12 @@ function computeAndRenderLatestOvertime(latestByDay) {
|
|
|
1280
1254
|
// 每天的 latestHourNormalized → 超出下班的小时数
|
|
1281
1255
|
const dailyOvertime = latestByDay
|
|
1282
1256
|
.map((d) => {
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
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
|
+
}
|
|
1289
1263
|
if (v == null) return null
|
|
1290
1264
|
const overtime = Math.max(0, Number(v) - endH)
|
|
1291
1265
|
return { date: d.date, overtime, raw: v }
|
|
@@ -1307,21 +1281,6 @@ function computeAndRenderLatestOvertime(latestByDay) {
|
|
|
1307
1281
|
)}</b> 小时,逻辑时间约 ${worstDay.raw.toFixed(2)} 点)`
|
|
1308
1282
|
}
|
|
1309
1283
|
|
|
1310
|
-
// 工具:根据日期字符串计算 ISO 周 key:YYYY-Www
|
|
1311
|
-
const getIsoWeekKey = (dStr) => {
|
|
1312
|
-
const d = new Date(dStr)
|
|
1313
|
-
if (Number.isNaN(d.valueOf())) return null
|
|
1314
|
-
const target = new Date(
|
|
1315
|
-
Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())
|
|
1316
|
-
)
|
|
1317
|
-
const dayNum = target.getUTCDay() || 7 // Sunday=0
|
|
1318
|
-
target.setUTCDate(target.getUTCDate() + 4 - dayNum)
|
|
1319
|
-
const yearStart = new Date(Date.UTC(target.getUTCFullYear(), 0, 1))
|
|
1320
|
-
const weekNo = Math.ceil(((target - yearStart) / 86400000 + 1) / 7)
|
|
1321
|
-
const year = target.getUTCFullYear()
|
|
1322
|
-
return `${year}-W${String(weekNo).padStart(2, '0')}`
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
1284
|
// 2) 按周聚合:每周取「该周内任意一天的最大加班时长」
|
|
1326
1285
|
const weekMap = new Map()
|
|
1327
1286
|
dailyOvertime.forEach((d) => {
|
|
@@ -1377,6 +1336,502 @@ function computeAndRenderLatestOvertime(latestByDay) {
|
|
|
1377
1336
|
}
|
|
1378
1337
|
}
|
|
1379
1338
|
|
|
1339
|
+
function buildDataset(stats, type) {
|
|
1340
|
+
const dataMap = stats[type]; // { author: { period: changed } }
|
|
1341
|
+
|
|
1342
|
+
const authors = Object.keys(dataMap);
|
|
1343
|
+
const allPeriods = Array.from(new Set(
|
|
1344
|
+
authors.flatMap(a => Object.keys(dataMap[a]))
|
|
1345
|
+
)).sort();
|
|
1346
|
+
|
|
1347
|
+
const series = authors.map(a => ({
|
|
1348
|
+
name: a,
|
|
1349
|
+
type: 'line',
|
|
1350
|
+
smooth: true,
|
|
1351
|
+
data: allPeriods.map(p => dataMap[a][p] || 0)
|
|
1352
|
+
}));
|
|
1353
|
+
|
|
1354
|
+
return { authors, allPeriods, series };
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
const drawChangeTrends = (stats) => {
|
|
1358
|
+
const el = document.getElementById('chartAuthorChanges')
|
|
1359
|
+
if (!el) return null
|
|
1360
|
+
const chart = echarts.init(el)
|
|
1361
|
+
|
|
1362
|
+
function render(t) {
|
|
1363
|
+
const { authors, allPeriods, series } = buildDataset(stats, t)
|
|
1364
|
+
chart.setOption({
|
|
1365
|
+
tooltip: { trigger: 'axis' },
|
|
1366
|
+
legend: { data: authors },
|
|
1367
|
+
xAxis: { type: 'category', data: allPeriods },
|
|
1368
|
+
yAxis: { type: 'value' },
|
|
1369
|
+
series
|
|
1370
|
+
})
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// 初次渲染:日
|
|
1374
|
+
render('daily')
|
|
1375
|
+
|
|
1376
|
+
// tabs 切换
|
|
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
|
+
}
|
|
1435
|
+
|
|
1436
|
+
function drawAuthorOvertimeTrends(commits, stats) {
|
|
1437
|
+
const el = document.getElementById('chartAuthorOvertime')
|
|
1438
|
+
if (!el) return null
|
|
1439
|
+
const chart = echarts.init(el)
|
|
1440
|
+
|
|
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
|
+
`
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
async function main() {
|
|
1778
|
+
const {
|
|
1779
|
+
commits,
|
|
1780
|
+
stats,
|
|
1781
|
+
weekly,
|
|
1782
|
+
monthly,
|
|
1783
|
+
latestByDay,
|
|
1784
|
+
config,
|
|
1785
|
+
authorChanges
|
|
1786
|
+
} =
|
|
1787
|
+
await loadData()
|
|
1788
|
+
commitsAll = commits
|
|
1789
|
+
filtered = commitsAll.slice()
|
|
1790
|
+
window.__overtimeEndHour =
|
|
1791
|
+
stats && typeof stats.endHour === 'number'
|
|
1792
|
+
? stats.endHour
|
|
1793
|
+
: (config.endHour ?? 18)
|
|
1794
|
+
window.__overnightCutoff =
|
|
1795
|
+
typeof config.overnightCutoff === 'number' ? config.overnightCutoff : 6
|
|
1796
|
+
initTableControls()
|
|
1797
|
+
updatePager()
|
|
1798
|
+
renderCommitsTablePage()
|
|
1799
|
+
|
|
1800
|
+
drawHourlyOvertime(stats, (hour) => {
|
|
1801
|
+
// 使用举例
|
|
1802
|
+
const hourCommitsDetail = groupCommitsByHour(commits)
|
|
1803
|
+
// 将 commit 列表传给侧栏(若没有详情,则传空数组)
|
|
1804
|
+
showSideBarForHour(hour, hourCommitsDetail[hour] || [])
|
|
1805
|
+
})
|
|
1806
|
+
drawOutsideVsInside(stats)
|
|
1807
|
+
|
|
1808
|
+
// 按日提交趋势:点击某天打开抽屉,显示当日所有 commits
|
|
1809
|
+
drawDailyTrend(commits, showDayDetailSidebar)
|
|
1810
|
+
|
|
1811
|
+
// 周趋势:保持原有点击行为(显示该周详情)
|
|
1812
|
+
drawWeeklyTrend(weekly, commits, showSideBarForWeek)
|
|
1813
|
+
|
|
1814
|
+
// 月趋势(加班占比):点击某个月打开抽屉,显示该月所有 commits
|
|
1815
|
+
drawMonthlyTrend(monthly, commits, showDayDetailSidebar)
|
|
1816
|
+
|
|
1817
|
+
// 每日最晚提交时间(小时):点击某天打开抽屉,显示当日所有 commits
|
|
1818
|
+
drawLatestHourDaily(latestByDay, commits, showDayDetailSidebar)
|
|
1819
|
+
|
|
1820
|
+
// 每日超过下班的小时数:点击某天打开抽屉,显示当日所有 commits
|
|
1821
|
+
drawDailySeverity(latestByDay, commits, showDayDetailSidebar)
|
|
1822
|
+
|
|
1823
|
+
const daily = drawDailyTrendSeverity(commits, weekly, showDayDetailSidebar)
|
|
1824
|
+
|
|
1825
|
+
console.log('最累的一天:', daily.analysis.mostTiredDay)
|
|
1826
|
+
|
|
1827
|
+
drawChangeTrends(authorChanges)
|
|
1828
|
+
drawAuthorOvertimeTrends(commits, stats)
|
|
1829
|
+
drawAuthorLatestOvertimeTrends(commits, stats)
|
|
1830
|
+
computeAndRenderLatestOvertime(latestByDay)
|
|
1831
|
+
renderKpi(stats)
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
|
|
1380
1835
|
// 抽屉关闭交互(按钮 + 点击遮罩)
|
|
1381
1836
|
document.getElementById('sidebarClose').onclick = () => {
|
|
1382
1837
|
document.getElementById('dayDetailSidebar').classList.remove('show')
|