wukong-gitlog-cli 1.0.14 → 1.0.16

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,33 @@
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.16](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.15...v1.0.16) (2025-12-03)
6
+
7
+
8
+ ### Features
9
+
10
+ * 🎸 sider bar UI ([e3a48ab](https://github.com/tomatobybike/wukong-gitlog-cli/commit/e3a48abe5b0148fe0d9dcd42b7b028cad53d4c94))
11
+ * 🎸 ui show ([e9eba58](https://github.com/tomatobybike/wukong-gitlog-cli/commit/e9eba5823333545d598be363b58eca1f6ecfa6ed))
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * 🐛 makeline clickEvent ([4ad6378](https://github.com/tomatobybike/wukong-gitlog-cli/commit/4ad6378d88aca2dde6083efde844730443228e48))
17
+
18
+ ### [1.0.15](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.14...v1.0.15) (2025-12-03)
19
+
20
+
21
+ ### Features
22
+
23
+ * 🎸 dayDetailSidebar ([21b99fd](https://github.com/tomatobybike/wukong-gitlog-cli/commit/21b99fd859af45641987dfd5f50a8c5604a380c5))
24
+ * 🎸 each day ([ffa3d5e](https://github.com/tomatobybike/wukong-gitlog-cli/commit/ffa3d5e6192a69a727c747ffa325b56ae5bf78d4))
25
+ * 🎸 hourlyOvertimeChart ([93f62b7](https://github.com/tomatobybike/wukong-gitlog-cli/commit/93f62b7b9906154a16bdcf38bd7b0979d44bebfe))
26
+ * 🎸 latestOvertimeDay ([40cb157](https://github.com/tomatobybike/wukong-gitlog-cli/commit/40cb15788411441306e0c838108817fa36268624))
27
+ * 🎸 next day cutof ([efe6a5f](https://github.com/tomatobybike/wukong-gitlog-cli/commit/efe6a5f41e54ca0fdadb3c0071f1fcf7a286e6c2))
28
+ * 🎸 onDayClick ([9132b54](https://github.com/tomatobybike/wukong-gitlog-cli/commit/9132b5423449ceb8a964d813dfd4931834993913))
29
+ * 🎸 renderKpi ([e26cc0d](https://github.com/tomatobybike/wukong-gitlog-cli/commit/e26cc0deac83a7e9bccaae81ba572da0e39e82b3))
30
+ * 🎸 week range ([cd565da](https://github.com/tomatobybike/wukong-gitlog-cli/commit/cd565dac98b6c38442eeacdd6dd423b93d657ab0))
31
+
5
32
  ### [1.0.14](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.13...v1.0.14) (2025-12-02)
6
33
 
7
34
 
package/README.md CHANGED
@@ -53,6 +53,10 @@ wukong-gitlog-cli --help
53
53
 
54
54
  ## Usage
55
55
 
56
+ ```bash
57
+ wukong-gitlog-cli --overtime --serve --port 5555
58
+ ```
59
+
56
60
  ```bash
57
61
  wukong-gitlog-cli [options]
58
62
  ```
package/README.zh-CN.md CHANGED
@@ -55,6 +55,10 @@ wukong-gitlog-cli --help
55
55
 
56
56
  ## 🚀 使用方法
57
57
 
58
+ ```bash
59
+ wukong-gitlog-cli --overtime --serve --port 5555
60
+ ```
61
+
58
62
  ```bash
59
63
  wukong-gitlog-cli [options]
60
64
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wukong-gitlog-cli",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "description": "Advanced Git commit log exporter with Excel/JSON/TXT output, grouping, stats and CLI.",
5
5
  "keywords": [
6
6
  "git",
package/src/cli.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import chalk from 'chalk'
2
2
  import { Command } from 'commander'
3
3
  import dayjs from 'dayjs'
4
+ import isoWeek from 'dayjs/plugin/isoWeek.js'
4
5
  import fs from 'fs'
5
6
  import path from 'path'
6
7
  import { fileURLToPath } from 'url'
@@ -31,6 +32,8 @@ const pkg = JSON.parse(
31
32
  fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf8')
32
33
  )
33
34
 
35
+ dayjs.extend(isoWeek)
36
+
34
37
  const PKG_NAME = pkg.name
35
38
  const VERSION = pkg.version
36
39
 
@@ -51,6 +54,21 @@ const version = async () => {
51
54
  process.exit(0)
52
55
  }
53
56
 
57
+ /** 将 "2025-W48" → { start: '2025-11-24', end: '2025-11-30' } */
58
+ export function getWeekRange(periodStr) {
59
+ // periodStr = "2025-W48"
60
+ const [year, w] = periodStr.split('-W')
61
+ const week = parseInt(w, 10)
62
+
63
+ const start = dayjs().year(year).isoWeek(week).startOf('week') // Monday
64
+ const end = dayjs().year(year).isoWeek(week).endOf('week') // Sunday
65
+
66
+ return {
67
+ start: start.format('YYYY-MM-DD'),
68
+ end: end.format('YYYY-MM-DD')
69
+ }
70
+ }
71
+
54
72
  const main = async () => {
55
73
  const program = new Command()
56
74
 
@@ -343,6 +361,7 @@ const main = async () => {
343
361
  })
344
362
  return {
345
363
  period: k,
364
+ range: getWeekRange(k),
346
365
  total: s.total,
347
366
  outsideWorkCount: s.outsideWorkCount,
348
367
  outsideWorkRate: s.outsideWorkRate,
@@ -380,7 +399,10 @@ const main = async () => {
380
399
  nonWorkdayRate: s.nonWorkdayRate
381
400
  }
382
401
  })
383
- const dataMonthlyFile = outputFilePath('data/overtime-monthly.mjs', outDir)
402
+ const dataMonthlyFile = outputFilePath(
403
+ 'data/overtime-monthly.mjs',
404
+ outDir
405
+ )
384
406
  const monthlyModule = `export default ${JSON.stringify(monthlySeries, null, 2)};\n`
385
407
  writeTextFile(dataMonthlyFile, monthlyModule)
386
408
  console.log(chalk.green(`Monthly series 已导出: ${dataMonthlyFile}`))
@@ -388,29 +410,92 @@ const main = async () => {
388
410
  // 新增:每日最晚提交小时(用于显著展示加班严重程度)
389
411
  const dayGroups2 = groupRecords(records, 'day')
390
412
  const dayKeys2 = Object.keys(dayGroups2).sort()
391
- const overnightCutoff = Number.isFinite(opts.overnightCutoff) ? opts.overnightCutoff : 6
392
- const latestByDay = dayKeys2.map((k) => {
393
- const list = dayGroups2[k]
394
- const vals = list
395
- .map((r) => ({ r, _dt: new Date(r.date) }))
396
- .filter((x) => x._dt && !Number.isNaN(x._dt.valueOf()))
397
- .sort((a, b) => a._dt.valueOf() - b._dt.valueOf())
398
- const last = vals.length > 0 ? vals[vals.length - 1] : null
399
- const hour = last ? new Date(last.r.date).getHours() : null
413
+
414
+ // 次日凌晨归并窗口(默认 6 点前仍算前一天的加班)
415
+ const overnightCutoff = Number.isFinite(opts.overnightCutoff)
416
+ ? opts.overnightCutoff
417
+ : 6
418
+ // 次日上班时间(默认按 workStart,若未指定则 9 点)
419
+ const workStartHour =
420
+ opts.workStart || opts.workStart === 0 ? opts.workStart : 9
421
+ const workEndHour =
422
+ opts.workEnd || opts.workEnd === 0 ? opts.workEnd : 18
423
+
424
+ // 有些日期「本身没有 commit」,但第二天凌晨有提交要归并到这一天,
425
+ // 需要补出这些“虚拟日期”,否则 latestByDay 会漏掉这天。
426
+ const virtualPrevDays = new Set()
427
+ records.forEach((r) => {
428
+ const d = new Date(r.date)
429
+ if (Number.isNaN(d.valueOf())) return
430
+ const h = d.getHours()
431
+ if (h < 0 || h >= overnightCutoff || h >= workStartHour) return
432
+ const curDay = dayjs(d).format('YYYY-MM-DD')
433
+ const prevDay = dayjs(curDay).subtract(1, 'day').format('YYYY-MM-DD')
434
+ if (!dayGroups2[prevDay]) {
435
+ virtualPrevDays.add(prevDay)
436
+ }
437
+ })
438
+
439
+ const allDayKeys = Array.from(
440
+ new Set([...dayKeys2, ...virtualPrevDays])
441
+ ).sort()
442
+
443
+ const latestByDay = allDayKeys.map((k) => {
444
+ const list = dayGroups2[k] || []
445
+
446
+ // 1) 当天「下班后」的提交:只统计 >= workEndHour 的小时
447
+ const sameDayHours = list
448
+ .map((r) => new Date(r.date))
449
+ .filter((d) => !Number.isNaN(d.valueOf()))
450
+ .map((d) => d.getHours())
451
+ .filter((h) => h >= workEndHour && h < 24)
452
+
453
+ // 2) 次日凌晨、但仍算前一日加班的提交
400
454
  const nextKey = dayjs(k).add(1, 'day').format('YYYY-MM-DD')
401
455
  const early = dayGroups2[nextKey] || []
402
456
  const earlyHours = early
403
457
  .map((r) => new Date(r.date))
404
458
  .filter((d) => !Number.isNaN(d.valueOf()))
405
459
  .map((d) => d.getHours())
406
- .filter((h) => h >= 0 && h < overnightCutoff)
407
- const earlyMax = earlyHours.length > 0 ? Math.max(...earlyHours) : null
408
- const normalized = typeof earlyMax === 'number' ? 24 + earlyMax : null
409
- const latestHourNormalized = Math.max(
410
- typeof hour === 'number' ? hour : -1,
411
- typeof normalized === 'number' ? normalized : -1
412
- )
413
- return { date: k, latestHour: hour, latestHourNormalized: latestHourNormalized >= 0 ? latestHourNormalized : null }
460
+ // 只看 [0, overnightCutoff) 之间的小时,
461
+ // 并且默认认为 < workStartHour 属于「次日上班前」
462
+ .filter(
463
+ (h) =>
464
+ h >= 0 &&
465
+ h < overnightCutoff &&
466
+ // 保护性判断:若有人把 overnightCutoff 设得大于上班时间,
467
+ // 我们仍然只统计到上班时间为止
468
+ h < workStartHour
469
+ )
470
+
471
+ // 3) 计算「逻辑上的最晚加班时间」
472
+ // - 当天晚上的用原始小时(如 22 点)
473
+ // - 次日凌晨的用 24 + 小时(如 1 点 → 25)
474
+ const overtimeValues = [
475
+ ...sameDayHours.map((h) => h),
476
+ ...earlyHours.map((h) => 24 + h)
477
+ ]
478
+
479
+ if (overtimeValues.length === 0) {
480
+ // 这一天没有任何「下班后到次日上班前」的提交
481
+ return {
482
+ date: k,
483
+ latestHour: null,
484
+ latestHourNormalized: null
485
+ }
486
+ }
487
+
488
+ const latestHourNormalized = Math.max(...overtimeValues)
489
+
490
+ // latestHour 保留「当天自然日内」的最晚提交通常小时数,供前端需要时参考
491
+ const sameDayMax =
492
+ sameDayHours.length > 0 ? Math.max(...sameDayHours) : null
493
+
494
+ return {
495
+ date: k,
496
+ latestHour: sameDayMax,
497
+ latestHourNormalized
498
+ }
414
499
  })
415
500
  const dataLatestByDayFile = outputFilePath(
416
501
  'data/overtime-latest-by-day.mjs',
@@ -432,7 +517,10 @@ const main = async () => {
432
517
  lunchEnd: opts.lunchEnd || 14,
433
518
  overnightCutoff
434
519
  }
435
- writeTextFile(configFile, `export default ${JSON.stringify(cfg, null, 2)};\n`)
520
+ writeTextFile(
521
+ configFile,
522
+ `export default ${JSON.stringify(cfg, null, 2)};\n`
523
+ )
436
524
  console.log(chalk.green(`Config 已导出: ${configFile}`))
437
525
  } catch (e) {
438
526
  console.warn('Export config failed:', e && e.message ? e.message : e)
package/web/app.js CHANGED
@@ -129,7 +129,7 @@ function drawHourlyOvertime(stats, onHourClick) {
129
129
  trigger: 'axis',
130
130
  formatter(params) {
131
131
  const p = params[0]
132
- const h = parseInt(p.axisValue,10)
132
+ const h = parseInt(p.axisValue, 10)
133
133
  const count = p.value
134
134
  const rate = (percent[h] * 100).toFixed(1)
135
135
  return `
@@ -184,14 +184,24 @@ function drawHourlyOvertime(stats, onHourClick) {
184
184
  data: [
185
185
  {
186
186
  name: '上班开始',
187
+ nameValue: String(stats.startHour).padStart(2, '0'),
187
188
  xAxis: String(stats.startHour).padStart(2, '0')
188
189
  },
189
- { name: '下班时间', xAxis: String(stats.endHour).padStart(2, '0') },
190
+ {
191
+ name: '下班时间',
192
+ nameValue: String(stats.endHour).padStart(2, '0'),
193
+ xAxis: String(stats.endHour).padStart(2, '0')
194
+ },
190
195
  {
191
196
  name: '午休开始',
197
+ nameValue: String(stats.lunchStart).padStart(2, '0'),
192
198
  xAxis: String(stats.lunchStart).padStart(2, '0')
193
199
  },
194
- { name: '午休结束', xAxis: String(stats.lunchEnd).padStart(2, '0') }
200
+ {
201
+ name: '午休结束',
202
+ nameValue: String(stats.lunchEnd).padStart(2, '0'),
203
+ xAxis: String(stats.lunchEnd).padStart(2, '0')
204
+ }
195
205
  ]
196
206
  }
197
207
  }
@@ -201,7 +211,14 @@ function drawHourlyOvertime(stats, onHourClick) {
201
211
  // 点击事件(点击某小时 → 打开侧栏)
202
212
  if (typeof onHourClick === 'function') {
203
213
  chart.on('click', (p) => {
204
- const hour = Number(p.name)
214
+ let hour = Number(p.name)
215
+ if(p.componentType === 'markLine') {
216
+ hour = Number(p.data.xAxis)
217
+ }
218
+ // FIXME: remove debug log before production
219
+ console.log('❌', 'hour', hour, p)
220
+ document.getElementById('dayDetailSidebar').classList.remove('show')
221
+ if (Object.is(hour, NaN)) return
205
222
  onHourClick(hour, commits[hour])
206
223
  })
207
224
  }
@@ -212,9 +229,11 @@ function drawHourlyOvertime(stats, onHourClick) {
212
229
  // showSideBarForHour 实现
213
230
  function showSideBarForHour(hour, commitsOrCount) {
214
231
  // 支持传入 number(仅次数)或 array(详细 commit 列表)
215
- const sidebar = document.getElementById('hourDetailSidebar')
216
- const titleEl = document.getElementById('hourSidebarTitle')
217
- const contentEl = document.getElementById('hourSidebarContent')
232
+ // 统一复用通用详情侧栏 DOM
233
+ const sidebar = document.getElementById('dayDetailSidebar')
234
+ const backdrop = document.getElementById('sidebarBackdrop')
235
+ const titleEl = document.getElementById('sidebarTitle')
236
+ const contentEl = document.getElementById('sidebarContent')
218
237
 
219
238
  // 兼容未传入侧栏 DOM 的情况(优雅降级)
220
239
  if (!sidebar || !titleEl || !contentEl) {
@@ -234,19 +253,22 @@ function showSideBarForHour(hour, commitsOrCount) {
234
253
  } else if (Array.isArray(commitsOrCount)) {
235
254
  // commits 列表:展示作者/时间/消息(最多前 50 条,避免性能问题)
236
255
  const commits = commitsOrCount.slice(0, 50)
237
- contentEl.innerHTML = commits
256
+ contentEl.innerHTML = `<div class="sidebar-list">${commits
238
257
  .map((c) => {
239
258
  const author = c.author ?? c.name ?? 'unknown'
240
259
  const time = c.date ?? c.time ?? ''
241
260
  const msg = (c.message ?? c.msg ?? c.body ?? '').replace(/\n/g, ' ')
242
261
  return `
243
- <div class="hour-commit">
244
- <div class="meta">👤 <b>${escapeHtml(author)}</b> · 🕒 ${escapeHtml(time)}</div>
245
- <div class="msg">${escapeHtml(msg)}</div>
246
- </div>
247
- `
262
+ <div class="sidebar-item">
263
+ <div class="sidebar-item-header">
264
+ <span class="author">👤 ${escapeHtml(author)}</span>
265
+ <span class="time">🕒 ${escapeHtml(time)}</span>
266
+ </div>
267
+ <div class="sidebar-item-message">${escapeHtml(msg)}</div>
268
+ </div>
269
+ `
248
270
  })
249
- .join('')
271
+ .join('')}</div>`
250
272
 
251
273
  if (commitsOrCount.length > 50) {
252
274
  const more = commitsOrCount.length - 50
@@ -256,18 +278,11 @@ function showSideBarForHour(hour, commitsOrCount) {
256
278
  contentEl.innerHTML = `<div style="font-size:14px;">无可展示数据</div>`
257
279
  }
258
280
 
259
- // 打开侧栏
281
+ // 打开侧栏 + 遮罩
260
282
  sidebar.classList.add('show')
283
+ if (backdrop) backdrop.classList.add('show')
261
284
  }
262
285
 
263
- // 关闭按钮绑定(只需运行一次)
264
- ;(function bindHourSidebarClose() {
265
- const btn = document.getElementById('hourSidebarClose')
266
- const sidebar = document.getElementById('hourDetailSidebar')
267
- if (!btn || !sidebar) return
268
- btn.addEventListener('click', () => sidebar.classList.remove('show'))
269
- })()
270
-
271
286
  // 简单的 HTML 转义,防止 XSS 与布局断裂
272
287
  function escapeHtml(str = '') {
273
288
  return String(str)
@@ -301,7 +316,7 @@ function drawOutsideVsInside(stats) {
301
316
  return chart
302
317
  }
303
318
 
304
- function drawDailyTrend(commits) {
319
+ function drawDailyTrend(commits, onDayClick) {
305
320
  if (!Array.isArray(commits) || commits.length === 0) return null
306
321
 
307
322
  // 聚合每日提交数量
@@ -398,10 +413,66 @@ function drawDailyTrend(commits) {
398
413
  ]
399
414
  })
400
415
 
416
+ // 点击某一天,打开抽屉显示当日 commits
417
+ if (typeof onDayClick === 'function') {
418
+ chart.on('click', (params) => {
419
+ const idx = params.dataIndex
420
+ const date = labels[idx]
421
+ const count = data[idx]
422
+ const dayCommits = commits.filter(
423
+ (c) => new Date(c.date).toISOString().slice(0, 10) === date
424
+ )
425
+ onDayClick(date, count, dayCommits)
426
+ })
427
+ }
428
+
401
429
  return chart
402
430
  }
403
431
 
404
- function drawWeeklyTrend(weekly) {
432
+ function showSideBarForWeek(period, weeklyItem, commits = []) {
433
+ // 统一复用通用详情侧栏 DOM
434
+ const sidebar = document.getElementById('dayDetailSidebar')
435
+ const backdrop = document.getElementById('sidebarBackdrop')
436
+ const titleEl = document.getElementById('sidebarTitle')
437
+ const contentEl = document.getElementById('sidebarContent')
438
+
439
+ titleEl.innerHTML = `📅 周期:<b>${period}</b>`
440
+
441
+ let html = `
442
+ <div style="padding:6px 0;">
443
+ 加班次数:<b>${weeklyItem.outsideWorkCount}</b><br/>
444
+ 占比:<b>${(weeklyItem.outsideWorkRate * 100).toFixed(1)}%</b>
445
+ </div>
446
+ <hr/>
447
+ `
448
+
449
+ if (!commits.length) {
450
+ html += `<div style="padding:10px;color:#777;">该周无提交记录</div>`
451
+ } else {
452
+ html += `<div class="sidebar-list">${commits
453
+ .map((c) => {
454
+ const author = escapeHtml(c.author || 'unknown')
455
+ const time = escapeHtml(c.date || '')
456
+ const msg = escapeHtml((c.message || '').replace(/\n/g, ' '))
457
+ return `
458
+ <div class="sidebar-item">
459
+ <div class="sidebar-item-header">
460
+ <span class="author">👤 ${author}</span>
461
+ <span class="time">🕒 ${time}</span>
462
+ </div>
463
+ <div class="sidebar-item-message">${msg}</div>
464
+ </div>
465
+ `
466
+ })
467
+ .join('')}</div>`
468
+ }
469
+
470
+ contentEl.innerHTML = html
471
+ sidebar.classList.add('show')
472
+ if (backdrop) backdrop.classList.add('show')
473
+ }
474
+
475
+ function drawWeeklyTrend(weekly, commits, onWeekClick) {
405
476
  if (!Array.isArray(weekly) || weekly.length === 0) return null
406
477
 
407
478
  const labels = weekly.map((w) => w.period)
@@ -409,13 +480,16 @@ function drawWeeklyTrend(weekly) {
409
480
  const dataCount = weekly.map((w) => w.outsideWorkCount)
410
481
 
411
482
  const el = document.getElementById('weeklyTrendChart')
412
- // eslint-disable-next-line no-undef
413
483
  const chart = echarts.init(el)
414
484
 
415
485
  chart.setOption({
416
486
  tooltip: {
417
487
  trigger: 'axis',
418
488
  formatter: (params) => {
489
+ const pp = params[0]
490
+ const weekItem = weekly[pp.dataIndex]
491
+ const { start, end } = weekItem.range
492
+
419
493
  const rate = params.find((p) => p.seriesName.includes('%'))?.data
420
494
  const count = params.find((p) => p.seriesName.includes('次数'))?.data
421
495
 
@@ -425,22 +499,20 @@ function drawWeeklyTrend(weekly) {
425
499
  if (rate >= 20) level = '🔴 严重(≥20%)'
426
500
 
427
501
  return `
428
- <div style="font-size:13px; line-height:1.5;">
429
- <b>${params[0].axisValue}</b><br/>
430
- 加班占比:<b>${rate}%</b><br/>
431
- 加班次数:${count} 次<br/>
432
- 等级:${level}
433
- </div>
434
- `
502
+ <div style="font-size:13px; line-height:1.5;">
503
+ <b>${params[0].axisValue}</b><br/>
504
+ 📅 周区间:<b>${start} ~ ${end}</b><br/>
505
+ 加班占比:<b>${rate}%</b><br/>
506
+ 加班次数:${count} 次<br/>
507
+ 等级:${level}
508
+ </div>
509
+ `
435
510
  }
436
511
  },
437
512
 
438
- legend: {
439
- top: 10
440
- },
513
+ legend: { top: 10 },
441
514
 
442
515
  xAxis: { type: 'category', data: labels },
443
-
444
516
  yAxis: [
445
517
  { type: 'value', min: 0, max: 100, name: '占比(%)' },
446
518
  { type: 'value', name: '次数', min: 0 }
@@ -451,26 +523,22 @@ function drawWeeklyTrend(weekly) {
451
523
  type: 'line',
452
524
  name: '加班占比(%)',
453
525
  data: dataRate,
454
-
455
- // ⭐ 区间背景(与 monthly/daily 对齐)
456
526
  markArea: {
457
527
  data: [
458
528
  [
459
529
  { yAxis: 0 },
460
- { yAxis: 10, itemStyle: { color: 'rgba(76, 175, 80, 0.15)' } } // 绿色
530
+ { yAxis: 10, itemStyle: { color: 'rgba(76, 175, 80, 0.15)' } }
461
531
  ],
462
532
  [
463
533
  { yAxis: 10 },
464
- { yAxis: 20, itemStyle: { color: 'rgba(251, 140, 0, 0.15)' } } // 橙色
534
+ { yAxis: 20, itemStyle: { color: 'rgba(251, 140, 0, 0.15)' } }
465
535
  ],
466
536
  [
467
537
  { yAxis: 20 },
468
- { yAxis: 100, itemStyle: { color: 'rgba(211, 47, 47, 0.15)' } } // 红色
538
+ { yAxis: 100, itemStyle: { color: 'rgba(211, 47, 47, 0.15)' } }
469
539
  ]
470
540
  ]
471
541
  },
472
-
473
- // ⭐ 阈值线
474
542
  markLine: {
475
543
  symbol: ['none', 'arrow'],
476
544
  data: [
@@ -493,17 +561,35 @@ function drawWeeklyTrend(weekly) {
493
561
  name: '加班次数',
494
562
  data: dataCount,
495
563
  yAxisIndex: 1,
496
-
497
- // 次数线使用默认蓝色,避免干扰等级颜色区间
498
564
  smooth: true
499
565
  }
500
566
  ]
501
567
  })
502
568
 
569
+ // ⭐ 点击事件:从 commits 过滤该周提交
570
+ chart.on('click', (p) => {
571
+ const idx = p.dataIndex
572
+ const w = weekly[idx]
573
+
574
+ const start = new Date(w.range.start)
575
+ const end = new Date(w.range.end)
576
+ end.setHours(23, 59, 59, 999) // 包含当天
577
+
578
+ const weeklyCommits = commits.filter((c) => {
579
+ const d = new Date(c.date)
580
+ return d >= start && d <= end
581
+ })
582
+
583
+ // 回调交给外面决定如何打开侧栏
584
+ if (typeof onWeekClick === 'function') {
585
+ onWeekClick(w.period, w, weeklyCommits)
586
+ }
587
+ })
588
+
503
589
  return chart
504
590
  }
505
591
 
506
- function drawMonthlyTrend(monthly) {
592
+ function drawMonthlyTrend(monthly, commits, onMonthClick) {
507
593
  if (!Array.isArray(monthly) || monthly.length === 0) return null
508
594
 
509
595
  const labels = monthly.map((m) => m.period)
@@ -599,10 +685,27 @@ function drawMonthlyTrend(monthly) {
599
685
  ]
600
686
  })
601
687
 
688
+ // 点击某个月份,打开抽屉显示该月的所有 commits
689
+ if (typeof onMonthClick === 'function' && Array.isArray(commits)) {
690
+ chart.on('click', (params) => {
691
+ const idx = params.dataIndex
692
+ const ym = labels[idx] // 'YYYY-MM'
693
+ const monthCommits = commits.filter((c) => {
694
+ const d = new Date(c.date)
695
+ const m = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(
696
+ 2,
697
+ '0'
698
+ )}`
699
+ return m === ym
700
+ })
701
+ onMonthClick(ym, monthCommits.length, monthCommits)
702
+ })
703
+ }
704
+
602
705
  return chart
603
706
  }
604
707
 
605
- function drawLatestHourDaily(latestByDay) {
708
+ function drawLatestHourDaily(latestByDay, commits, onDayClick) {
606
709
  if (!Array.isArray(latestByDay) || latestByDay.length === 0) return null
607
710
 
608
711
  const labels = latestByDay.map((d) => d.date)
@@ -680,6 +783,8 @@ function drawLatestHourDaily(latestByDay) {
680
783
  type: 'line',
681
784
  name: '每日最晚提交小时',
682
785
  data,
786
+ // 让折线在 null 点之间连起来,避免视觉上“断裂”
787
+ connectNulls: true,
683
788
  markLine: {
684
789
  symbol: ['none', 'arrow'],
685
790
  data: [
@@ -715,10 +820,28 @@ function drawLatestHourDaily(latestByDay) {
715
820
  ]
716
821
  })
717
822
 
823
+ // 点击某一天的最晚提交时间点,打开抽屉显示该日 commits
824
+ if (typeof onDayClick === 'function' && Array.isArray(commits)) {
825
+ // 预聚合:按天收集 commits
826
+ const dayCommitsMap = {}
827
+ commits.forEach((c) => {
828
+ const d = new Date(c.date).toISOString().slice(0, 10)
829
+ if (!dayCommitsMap[d]) dayCommitsMap[d] = []
830
+ dayCommitsMap[d].push(c)
831
+ })
832
+
833
+ chart.on('click', (params) => {
834
+ const idx = params.dataIndex
835
+ const date = labels[idx]
836
+ const list = dayCommitsMap[date] || []
837
+ onDayClick(date, list.length, list)
838
+ })
839
+ }
840
+
718
841
  return chart
719
842
  }
720
843
 
721
- function drawDailySeverity(latestByDay) {
844
+ function drawDailySeverity(latestByDay, commits, onDayClick) {
722
845
  if (!Array.isArray(latestByDay) || latestByDay.length === 0) return null
723
846
 
724
847
  const labels = latestByDay.map((d) => d.date)
@@ -730,7 +853,9 @@ function drawDailySeverity(latestByDay) {
730
853
  : (d.latestHour ?? null)
731
854
  )
732
855
 
733
- const sev = raw.map((v) => (v == null ? null : Math.max(0, Number(v) - endH)))
856
+ // 若某天 latestHourNormalized 为空,表示「没有下班后到次日上班前的提交」,
857
+ // 这里按 0 小时加班处理,保证折线连续。
858
+ const sev = raw.map((v) => (v == null ? 0 : Math.max(0, Number(v) - endH)))
734
859
 
735
860
  const el = document.getElementById('dailySeverityChart')
736
861
  // eslint-disable-next-line no-undef
@@ -746,15 +871,6 @@ function drawDailySeverity(latestByDay) {
746
871
  const overtime = p.data
747
872
  const rawHour = raw[p.dataIndex] // 原始 latestHour 或 latestHourNormalized
748
873
 
749
- if (overtime == null) {
750
- return `
751
- <div style="font-size:13px;">
752
- <b>${date}</b><br/>
753
- 无数据
754
- </div>
755
- `
756
- }
757
-
758
874
  return `
759
875
  <div style="font-size:13px;">
760
876
  <b>${date}</b><br/>
@@ -781,6 +897,8 @@ function drawDailySeverity(latestByDay) {
781
897
  type: 'line',
782
898
  name: '超过下班小时数',
783
899
  data: sev,
900
+ // 连续显示 0 小时加班的日期,避免折线断开
901
+ connectNulls: true,
784
902
 
785
903
  // ⭐ 加班区域背景
786
904
  markArea: {
@@ -828,6 +946,23 @@ function drawDailySeverity(latestByDay) {
828
946
  ]
829
947
  })
830
948
 
949
+ // 点击某一天的「超过下班小时数」点,打开抽屉显示该日 commits
950
+ if (typeof onDayClick === 'function' && Array.isArray(commits)) {
951
+ const dayCommitsMap = {}
952
+ commits.forEach((c) => {
953
+ const d = new Date(c.date).toISOString().slice(0, 10)
954
+ if (!dayCommitsMap[d]) dayCommitsMap[d] = []
955
+ dayCommitsMap[d].push(c)
956
+ })
957
+
958
+ chart.on('click', (params) => {
959
+ const idx = params.dataIndex
960
+ const date = labels[idx]
961
+ const list = dayCommitsMap[date] || []
962
+ onDayClick(date, list.length, list)
963
+ })
964
+ }
965
+
831
966
  return chart
832
967
  }
833
968
 
@@ -991,6 +1126,7 @@ function drawDailyTrendSeverity(commits, weekly, onDayClick) {
991
1126
 
992
1127
  function showDayDetailSidebar(date, count, commits) {
993
1128
  const sidebar = document.getElementById('dayDetailSidebar')
1129
+ const backdrop = document.getElementById('sidebarBackdrop')
994
1130
  const title = document.getElementById('sidebarTitle')
995
1131
  const content = document.getElementById('sidebarContent')
996
1132
 
@@ -1000,22 +1136,19 @@ function showDayDetailSidebar(date, count, commits) {
1000
1136
  content.innerHTML = commits
1001
1137
  .map(
1002
1138
  (c) => `
1003
- <div style="margin-bottom:12px;">
1004
- <div>👤 <b>${c.author}</b></div>
1005
- <div>🕒 ${c.time}</div>
1006
- <div>💬 ${c.msg}</div>
1139
+ <div class="sidebar-item">
1140
+ <div class="sidebar-item-header">
1141
+ <span class="author">👤 ${escapeHtml(c.author || 'unknown')}</span>
1142
+ <span class="time">🕒 ${escapeHtml(c.time || c.date || '')}</span>
1143
+ </div>
1144
+ <div class="sidebar-item-message">${escapeHtml(c.msg || c.message || '')}</div>
1007
1145
  </div>
1008
- <hr/>
1009
1146
  `
1010
1147
  )
1011
1148
  .join('')
1012
1149
 
1013
1150
  sidebar.classList.add('show')
1014
- }
1015
-
1016
- // 关闭按钮
1017
- document.getElementById('sidebarClose').onclick = () => {
1018
- document.getElementById('dayDetailSidebar').classList.remove('show')
1151
+ if (backdrop) backdrop.classList.add('show')
1019
1152
  }
1020
1153
 
1021
1154
  function renderKpi(stats) {
@@ -1023,11 +1156,54 @@ function renderKpi(stats) {
1023
1156
  if (!el || !stats) return
1024
1157
  const latest = stats.latestCommit
1025
1158
  const latestHour = stats.latestCommitHour
1026
- const latestOut = stats.latestOutsideCommit
1027
- const latestOutHour =
1028
- stats.latestOutsideCommitHour ??
1029
- (latestOut ? new Date(latestOut.date).getHours() : null)
1159
+
1160
+ // 使用 cutoff + 上下班时间,重新在全部 commits 中计算「加班最晚一次提交」
1030
1161
  const cutoff = window.__overnightCutoff ?? 6
1162
+ const startHour =
1163
+ typeof stats.startHour === 'number' && stats.startHour >= 0
1164
+ ? stats.startHour
1165
+ : 9
1166
+ const endHour =
1167
+ typeof stats.endHour === 'number' && stats.endHour >= 0
1168
+ ? stats.endHour
1169
+ : (window.__overtimeEndHour ?? 18)
1170
+
1171
+ let latestOut = null
1172
+ let latestOutHour = null
1173
+ let maxSeverity = -1
1174
+
1175
+ if (Array.isArray(commitsAll) && commitsAll.length > 0) {
1176
+ commitsAll.forEach((c) => {
1177
+ const d = new Date(c.date)
1178
+ if (!d || Number.isNaN(d.valueOf())) return
1179
+ const h = d.getHours()
1180
+
1181
+ // 只看「当日下班后」以及「次日凌晨 cutoff 之前,且仍在上班前」的提交
1182
+ let sev = null
1183
+ if (h >= endHour && h < 24) {
1184
+ // 当晚:直接按 h - endHour 计算
1185
+ sev = h - endHour
1186
+ } else if (h >= 0 && h < cutoff && h < startHour) {
1187
+ // 次日凌晨:视作跨天,加上 24
1188
+ sev = 24 - endHour + h
1189
+ }
1190
+
1191
+ if (sev != null && sev >= 0 && sev > maxSeverity) {
1192
+ maxSeverity = sev
1193
+ latestOut = c
1194
+ latestOutHour = h
1195
+ }
1196
+ })
1197
+ }
1198
+
1199
+ // 若按 cutoff 没算出结果,则退回到原来的 stats.latestOutsideCommit
1200
+ if (!latestOut && stats.latestOutsideCommit) {
1201
+ latestOut = stats.latestOutsideCommit
1202
+ latestOutHour =
1203
+ stats.latestOutsideCommitHour ??
1204
+ (latestOut ? new Date(latestOut.date).getHours() : null)
1205
+ }
1206
+
1031
1207
  const html = [
1032
1208
  `<div>最晚一次提交时间:${latest ? formatDate(latest.date) : '-'}${typeof latestHour === 'number' ? `(${String(latestHour).padStart(2, '0')}:00)` : ''} <div class="author">${latest.author}</div> <div> ${latest.message} <div></div>`,
1033
1209
  `<div class="hr"></div>`,
@@ -1050,7 +1226,7 @@ function groupCommitsByHour(commits) {
1050
1226
  return byHour
1051
1227
  }
1052
1228
 
1053
- ;(async function main() {
1229
+ async function main() {
1054
1230
  const { commits, stats, weekly, monthly, latestByDay, config } =
1055
1231
  await loadData()
1056
1232
  commitsAll = commits
@@ -1064,21 +1240,155 @@ function groupCommitsByHour(commits) {
1064
1240
  initTableControls()
1065
1241
  updatePager()
1066
1242
  renderCommitsTablePage()
1067
- // 使用举例
1068
- const hourCommitsDetail = groupCommitsByHour(commits)
1069
1243
 
1070
1244
  drawHourlyOvertime(stats, (hour, count) => {
1245
+ // 使用举例
1246
+ const hourCommitsDetail = groupCommitsByHour(commits)
1071
1247
  // 将 commit 列表传给侧栏(若没有详情,则传空数组)
1072
1248
  showSideBarForHour(hour, hourCommitsDetail[hour] || [])
1073
1249
  })
1074
1250
  drawOutsideVsInside(stats)
1075
- drawDailyTrend(commits)
1076
- drawWeeklyTrend(weekly)
1077
- drawMonthlyTrend(monthly)
1078
- drawLatestHourDaily(latestByDay)
1079
- drawDailySeverity(latestByDay)
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
+
1080
1267
  const daily = drawDailyTrendSeverity(commits, weekly, showDayDetailSidebar)
1081
1268
 
1082
1269
  console.log('最累的一天:', daily.analysis.mostTiredDay)
1270
+ computeAndRenderLatestOvertime(latestByDay)
1083
1271
  renderKpi(stats)
1084
- })()
1272
+ }
1273
+
1274
+ // 基于 latestByDay + cutoff/endHour 统计「最晚加班的一天 / 一周 / 一月」
1275
+ function computeAndRenderLatestOvertime(latestByDay) {
1276
+ if (!Array.isArray(latestByDay) || latestByDay.length === 0) return
1277
+
1278
+ const endH = window.__overtimeEndHour || 18
1279
+
1280
+ // 每天的 latestHourNormalized → 超出下班的小时数
1281
+ const dailyOvertime = latestByDay
1282
+ .map((d) => {
1283
+ const v =
1284
+ typeof d.latestHourNormalized === 'number'
1285
+ ? d.latestHourNormalized
1286
+ : typeof d.latestHour === 'number'
1287
+ ? d.latestHour
1288
+ : null
1289
+ if (v == null) return null
1290
+ const overtime = Math.max(0, Number(v) - endH)
1291
+ return { date: d.date, overtime, raw: v }
1292
+ })
1293
+ .filter(Boolean)
1294
+
1295
+ if (!dailyOvertime.length) return
1296
+
1297
+ // 1) 最晚加班的一天(超出下班小时数最大,若相同取日期更晚)
1298
+ const dailySorted = [...dailyOvertime].sort((a, b) => {
1299
+ if (b.overtime !== a.overtime) return b.overtime - a.overtime
1300
+ return new Date(b.date) - new Date(a.date)
1301
+ })
1302
+ const worstDay = dailySorted[0]
1303
+ const dayEl = document.getElementById('latestOvertimeDay')
1304
+ if (dayEl) {
1305
+ dayEl.innerHTML = `⏰ 最晚加班的一天:<b>${worstDay.date}</b>(超过下班 <b>${worstDay.overtime.toFixed(
1306
+ 2
1307
+ )}</b> 小时,逻辑时间约 ${worstDay.raw.toFixed(2)} 点)`
1308
+ }
1309
+
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
+ // 2) 按周聚合:每周取「该周内任意一天的最大加班时长」
1326
+ const weekMap = new Map()
1327
+ dailyOvertime.forEach((d) => {
1328
+ const key = getIsoWeekKey(d.date)
1329
+ if (!key) return
1330
+ const cur = weekMap.get(key)
1331
+ if (!cur || d.overtime > cur.overtime) {
1332
+ weekMap.set(key, d)
1333
+ }
1334
+ })
1335
+
1336
+ if (weekMap.size) {
1337
+ const weeks = Array.from(weekMap.entries()).sort((a, b) => {
1338
+ if (b[1].overtime !== a[1].overtime) return b[1].overtime - a[1].overtime
1339
+ return new Date(b[1].date) - new Date(a[1].date)
1340
+ })
1341
+ const [weekKey, weekInfo] = weeks[0]
1342
+ const weekEl = document.getElementById('latestOvertimeWeek')
1343
+ if (weekEl) {
1344
+ weekEl.innerHTML = `⏰ 最晚加班的一周:<b>${weekKey}</b>(代表日期 ${weekInfo.date},超过下班 <b>${weekInfo.overtime.toFixed(
1345
+ 2
1346
+ )}</b> 小时)`
1347
+ }
1348
+ }
1349
+
1350
+ // 3) 按月聚合:每月取「该月任意一天的最大加班时长」
1351
+ const monthMap = new Map()
1352
+ dailyOvertime.forEach((d) => {
1353
+ const dt = new Date(d.date)
1354
+ if (Number.isNaN(dt.valueOf())) return
1355
+ const key = `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(
1356
+ 2,
1357
+ '0'
1358
+ )}`
1359
+ const cur = monthMap.get(key)
1360
+ if (!cur || d.overtime > cur.overtime) {
1361
+ monthMap.set(key, d)
1362
+ }
1363
+ })
1364
+
1365
+ if (monthMap.size) {
1366
+ const months = Array.from(monthMap.entries()).sort((a, b) => {
1367
+ if (b[1].overtime !== a[1].overtime) return b[1].overtime - a[1].overtime
1368
+ return new Date(b[1].date) - new Date(a[1].date)
1369
+ })
1370
+ const [monthKey, monthInfo] = months[0]
1371
+ const monthEl = document.getElementById('latestOvertimeMonth')
1372
+ if (monthEl) {
1373
+ monthEl.innerHTML = `⏰ 最晚加班的月份:<b>${monthKey}</b>(代表日期 ${monthInfo.date},超过下班 <b>${monthInfo.overtime.toFixed(
1374
+ 2
1375
+ )}</b> 小时)`
1376
+ }
1377
+ }
1378
+ }
1379
+
1380
+ // 抽屉关闭交互(按钮 + 点击遮罩)
1381
+ document.getElementById('sidebarClose').onclick = () => {
1382
+ document.getElementById('dayDetailSidebar').classList.remove('show')
1383
+ const backdrop = document.getElementById('sidebarBackdrop')
1384
+ if (backdrop) backdrop.classList.remove('show')
1385
+ }
1386
+
1387
+ const sidebarBackdropEl = document.getElementById('sidebarBackdrop')
1388
+ if (sidebarBackdropEl) {
1389
+ sidebarBackdropEl.addEventListener('click', () => {
1390
+ document.getElementById('dayDetailSidebar').classList.remove('show')
1391
+ sidebarBackdropEl.classList.remove('show')
1392
+ })
1393
+ }
1394
+ main()
package/web/index.html CHANGED
@@ -56,6 +56,9 @@
56
56
  <div id="mostTiredDay"></div>
57
57
  <div id="mostTiredWeek"></div>
58
58
  <div id="mostTiredMonth"></div>
59
+ <div id="latestOvertimeDay"></div>
60
+ <div id="latestOvertimeWeek"></div>
61
+ <div id="latestOvertimeMonth"></div>
59
62
  </section>
60
63
 
61
64
  <section class="table-card">
@@ -98,26 +101,17 @@
98
101
  <code>/data/</code>.</small
99
102
  >
100
103
  </footer>
101
- <!-- 右侧滑出的详情侧栏 -->
104
+ <!-- 通用:右侧滑出的详情侧栏(小时 / 天 / 周 复用同一个 DOM) -->
105
+ <!-- 背景遮罩,点击可关闭侧栏 -->
106
+ <div id="sidebarBackdrop" class="sidebar-backdrop"></div>
102
107
  <div id="dayDetailSidebar" class="sidebar">
103
108
  <div class="sidebar-header">
104
109
  <span id="sidebarTitle"></span>
105
110
  <button id="sidebarClose">×</button>
106
111
  </div>
107
-
108
112
  <div id="sidebarContent" class="sidebar-content"></div>
109
113
  </div>
110
114
 
111
- <!-- 小时详情侧栏(独立) -->
112
- <div id="hourDetailSidebar" class="sidebar">
113
- <div class="sidebar-header">
114
- <span id="hourSidebarTitle"> </span>
115
- <button id="hourSidebarClose">×</button>
116
- </div>
117
-
118
- <div id="hourSidebarContent" class="sidebar-content"></div>
119
- </div>
120
-
121
115
  <script type="module" src="/app.js"></script>
122
116
  </body>
123
117
  </html>
@@ -189,20 +189,40 @@ td {
189
189
  margin-bottom: 8px;
190
190
  color: #00a76f;
191
191
  }
192
+ /* 抽屉 & 遮罩(参考 Material UI Drawer) -------------------- */
193
+ .sidebar-backdrop {
194
+ position: fixed;
195
+ inset: 0;
196
+ background: rgba(15, 23, 42, 0.38); /* 深色半透明遮罩 */
197
+ opacity: 0;
198
+ pointer-events: none;
199
+ transition: opacity 0.25s ease;
200
+ z-index: 1200;
201
+ }
202
+
203
+ .sidebar-backdrop.show {
204
+ opacity: 1;
205
+ pointer-events: auto;
206
+ }
207
+
192
208
  .sidebar {
193
209
  position: fixed;
194
210
  top: 0;
195
- right: -400px;
196
- width: 400px;
211
+ right: -460px;
212
+ width: min(460px, 100%);
197
213
  height: 100%;
198
214
  background: #fff;
199
- box-shadow: -2px 0 6px rgba(0, 0, 0, 0.15);
200
- transition: right 0.35s ease;
201
- z-index: 9999;
202
- padding: 16px;
215
+ box-shadow:
216
+ 0 10px 15px rgba(0, 0, 0, 0.1),
217
+ 0 4px 6px rgba(0, 0, 0, 0.08);
218
+ transition: right 0.3s ease;
219
+ z-index: 1300;
220
+ padding: 20px 20px 16px;
203
221
  display: flex;
204
222
  flex-direction: column;
205
223
  box-sizing: border-box;
224
+ border-radius: 12px 0 0 12px;
225
+ border-left: 1px solid rgba(145, 158, 171, 0.24);
206
226
  }
207
227
 
208
228
  .sidebar.show {
@@ -213,35 +233,115 @@ td {
213
233
  display: flex;
214
234
  justify-content: space-between;
215
235
  align-items: center;
216
- font-size: 18px;
217
- margin-bottom: 16px;
236
+ font-size: 16px;
237
+ font-weight: 500;
238
+ margin-bottom: 12px;
239
+ color: #111827;
240
+ }
241
+
242
+ .sidebar-header span {
243
+ display: inline-flex;
244
+ align-items: center;
245
+ gap: 6px;
218
246
  }
219
247
 
220
248
  .sidebar-header button {
221
- background: none;
249
+ width: 32px;
250
+ height: 32px;
251
+ display: inline-flex;
252
+ align-items: center;
253
+ justify-content: center;
254
+ background: rgba(148, 163, 184, 0.12);
222
255
  border: none;
223
- font-size: 24px;
256
+ border-radius: 999px;
257
+ font-size: 18px;
224
258
  cursor: pointer;
259
+ color: #4b5563;
260
+ transition:
261
+ background 0.2s ease,
262
+ color 0.2s ease,
263
+ box-shadow 0.2s ease;
264
+ }
265
+
266
+ .sidebar-header button:hover {
267
+ background: rgba(148, 163, 184, 0.22);
268
+ color: #111827;
269
+ box-shadow: 0 2px 6px rgba(15, 23, 42, 0.18);
225
270
  }
226
271
 
227
272
  .sidebar-content {
228
273
  overflow-y: auto;
229
- font-size: 14px;
274
+ font-size: 13px;
275
+ line-height: 1.6;
276
+ padding-right: 4px;
230
277
  }
231
- /* 小块提交记录样式 */
232
- .hour-commit {
233
- padding: 10px 8px;
234
- border-radius: 6px;
235
- background: #fafafa;
236
- margin-bottom: 10px;
237
- box-shadow: 0 1px 0 rgba(0, 0, 0, 0.02);
278
+
279
+ .sidebar-content::-webkit-scrollbar {
280
+ width: 6px;
281
+ }
282
+
283
+ .sidebar-content::-webkit-scrollbar-thumb {
284
+ background: rgba(148, 163, 184, 0.6);
285
+ border-radius: 999px;
238
286
  }
239
- .hour-commit .meta {
240
- color: #666;
287
+
288
+ .sidebar-content::-webkit-scrollbar-track {
289
+ background: transparent;
290
+ }
291
+
292
+ /* 抽屉中的提交列表(参考 MUI List / ListItem) ------------- */
293
+ .sidebar-list {
294
+ display: flex;
295
+ flex-direction: column;
296
+ gap: 8px;
297
+ padding: 4px 0;
298
+ }
299
+
300
+ .sidebar-item,
301
+ .hour-commit,
302
+ .week-commit {
303
+ padding: 10px 12px;
304
+ border-radius: 8px;
305
+ background: #f9fafb;
306
+ border: 1px solid rgba(148, 163, 184, 0.25);
307
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
308
+ transition:
309
+ background 0.18s ease,
310
+ box-shadow 0.18s ease,
311
+ transform 0.1s ease;
312
+ margin: 8px 0;
313
+ }
314
+
315
+ .sidebar-item:hover,
316
+ .hour-commit:hover,
317
+ .week-commit:hover {
318
+ background: #f3f4ff;
319
+ box-shadow: 0 3px 8px rgba(15, 23, 42, 0.12);
320
+ transform: translateY(-1px);
321
+ }
322
+
323
+ .sidebar-item-header {
324
+ display: flex;
325
+ justify-content: space-between;
326
+ align-items: center;
241
327
  font-size: 12px;
242
- margin-bottom: 6px;
328
+ color: #64748b;
329
+ margin-bottom: 4px;
243
330
  }
244
- .hour-commit .msg {
331
+
332
+ .sidebar-item-header .author {
333
+ font-weight: 600;
334
+ color: #111827;
335
+ }
336
+
337
+ .sidebar-item-header .time {
338
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
339
+ 'Liberation Mono', 'Courier New', monospace;
340
+ }
341
+
342
+ .sidebar-item-message {
343
+ font-size: 13px;
344
+ color: #111827;
245
345
  font-weight: 500;
246
346
  word-break: break-word;
247
347
  }