wukong-gitlog-cli 1.0.12 → 1.0.14

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,20 @@
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.14](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.13...v1.0.14) (2025-12-02)
6
+
7
+
8
+ ### Features
9
+
10
+ * 🎸 charts show better ([bfceb54](https://github.com/tomatobybike/wukong-gitlog-cli/commit/bfceb54b2bd67cc6bd67d93550799592119daff2))
11
+
12
+ ### [1.0.13](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.12...v1.0.13) (2025-12-01)
13
+
14
+
15
+ ### Features
16
+
17
+ * 🎸 output-wukong ([c310623](https://github.com/tomatobybike/wukong-gitlog-cli/commit/c310623314c44bad792b55967c5aeabda5e61812))
18
+
5
19
  ### [1.0.12](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.11...v1.0.12) (2025-12-01)
6
20
 
7
21
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wukong-gitlog-cli",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
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
@@ -115,11 +115,11 @@ const main = async () => {
115
115
  .option('--out <file>', '输出文件名(不含路径)')
116
116
  .option(
117
117
  '--out-dir <dir>',
118
- '自定义输出目录,支持相对路径或绝对路径,例如 `--out-dir ../output`'
118
+ '自定义输出目录,支持相对路径或绝对路径,例如 `--out-dir ../output-wukong`'
119
119
  )
120
120
  .option(
121
121
  '--out-parent',
122
- '将输出目录放到当前工程的父目录的 `output/`(等同于 `--out-dir ../output`)'
122
+ '将输出目录放到当前工程的父目录的 `output-wukong/`(等同于 `--out-dir ../output-wukong`)'
123
123
  )
124
124
  .option(
125
125
  '--per-period-formats <formats>',
@@ -137,7 +137,7 @@ const main = async () => {
137
137
  )
138
138
  .option(
139
139
  '--serve',
140
- '启动本地 web 服务,查看提交统计(将在 output/data 下生成数据文件)'
140
+ '启动本地 web 服务,查看提交统计(将在 output-wukong/data 下生成数据文件)'
141
141
  )
142
142
  .option(
143
143
  '--port <n>',
@@ -147,7 +147,7 @@ const main = async () => {
147
147
  )
148
148
  .option(
149
149
  '--serve-only',
150
- '仅启动 web 服务,不导出或分析数据(使用 output/data 中已有的数据)'
150
+ '仅启动 web 服务,不导出或分析数据(使用 output-wukong/data 中已有的数据)'
151
151
  )
152
152
  .option('--version', 'show version information')
153
153
  .parse()
@@ -155,7 +155,7 @@ const main = async () => {
155
155
  const opts = program.opts()
156
156
  // compute output directory root early (so serve-only can use it)
157
157
  const outDir = opts.outParent
158
- ? path.resolve(process.cwd(), '..', 'output')
158
+ ? path.resolve(process.cwd(), '..', 'output-wukong')
159
159
  : opts.outDir || undefined
160
160
 
161
161
  if (opts.version) {
package/src/server.mjs CHANGED
@@ -48,7 +48,7 @@ export function startServer(port = 3000, outputDir) {
48
48
  const webRoot = path.resolve(pkgRoot, 'web')
49
49
  const dataRoot = outputDir
50
50
  ? path.resolve(outputDir)
51
- : path.resolve(process.cwd(), 'output')
51
+ : path.resolve(process.cwd(), 'output-wukong')
52
52
 
53
53
  // warn if web directory or data directory doesn't exist
54
54
  if (!fs.existsSync(webRoot)) {
@@ -118,7 +118,7 @@ export function startServer(port = 3000, outputDir) {
118
118
  server.listen(port, () => {
119
119
  const url = `http://localhost:${port}`
120
120
  console.log(chalk.green(`Server started at ${url}`))
121
- console.log(chalk.green(`Serving web/ and output/data/`))
121
+ console.log(chalk.green(`Serving web/ and output-wukong/data/`))
122
122
 
123
123
  // ====== 自动打开浏览器 ======
124
124
  openBrowser(url)
@@ -3,10 +3,10 @@ import path from 'path';
3
3
 
4
4
  export function ensureOutputDir(customDir) {
5
5
  // If a custom absolute/relative path is provided, resolve relative to cwd as-is
6
- // Otherwise default to `output` inside current working directory.
6
+ // Otherwise default to `output-wukong` inside current working directory.
7
7
  const dir = customDir
8
8
  ? path.resolve(process.cwd(), customDir)
9
- : path.resolve(process.cwd(), 'output');
9
+ : path.resolve(process.cwd(), 'output-wukong');
10
10
 
11
11
  if (!fs.existsSync(dir)) {
12
12
  fs.mkdirSync(dir, { recursive: true });
package/web/app.js CHANGED
@@ -100,23 +100,184 @@ function initTableControls() {
100
100
  })
101
101
  }
102
102
 
103
- function drawHourlyOvertime(stats) {
103
+ function drawHourlyOvertime(stats, onHourClick) {
104
104
  const el = document.getElementById('hourlyOvertimeChart')
105
- // eslint-disable-next-line no-undef
106
105
  const chart = echarts.init(el)
107
- const data = stats.hourlyOvertimeCommits || []
106
+
107
+ const commits = stats.hourlyOvertimeCommits || []
108
+ const percent = stats.hourlyOvertimePercent || []
108
109
  const labels = Array.from({ length: 24 }, (_, i) =>
109
110
  String(i).padStart(2, '0')
110
111
  )
112
+
113
+ // 颜色逻辑(与 daily severity 风格一致)
114
+ function getColor(h) {
115
+ if (h >= 21) return '#d32f2f' // 深夜加班 红
116
+ if (h >= 19) return '#fb8c00' // 夜间加班 橙
117
+ if (h >= stats.lunchStart && h < stats.lunchEnd) return '#888888' // 午休灰
118
+ if (h >= stats.startHour && h < stats.endHour) return '#1976d2' // 工作时段 蓝
119
+ return '#b71c1c' // 凌晨 红
120
+ }
121
+
122
+ const data = commits.map((v, h) => ({
123
+ value: v,
124
+ itemStyle: { color: getColor(h) }
125
+ }))
126
+
111
127
  chart.setOption({
112
- tooltip: {},
113
- xAxis: { type: 'category', data: labels },
114
- yAxis: { type: 'value' },
115
- series: [{ type: 'bar', name: 'Overtime commits', data }]
128
+ tooltip: {
129
+ trigger: 'axis',
130
+ formatter(params) {
131
+ const p = params[0]
132
+ const h = parseInt(p.axisValue,10)
133
+ const count = p.value
134
+ const rate = (percent[h] * 100).toFixed(1)
135
+ return `
136
+ 🕒 <b>${h}:00</b><br/>
137
+ 提交次数:<b>${count}</b><br/>
138
+ 占全天比例:<b>${rate}%</b>
139
+ `
140
+ }
141
+ },
142
+
143
+ xAxis: {
144
+ type: 'category',
145
+ data: labels,
146
+ axisLabel: { color: '#555' }
147
+ },
148
+
149
+ yAxis: {
150
+ type: 'value',
151
+ min: 0,
152
+ axisLabel: { color: '#555' }
153
+ },
154
+
155
+ grid: { left: 40, right: 30, top: 20, bottom: 40 },
156
+
157
+ series: [
158
+ {
159
+ type: 'bar',
160
+ name: 'Overtime commits',
161
+ data,
162
+ barWidth: 18,
163
+
164
+ markPoint: {
165
+ symbol: 'pin',
166
+ symbolSize: 45,
167
+ itemStyle: { color: '#d32f2f' },
168
+ data: [
169
+ {
170
+ name: '最晚提交',
171
+ coord: [
172
+ String(stats.latestCommitHour).padStart(2, '0'),
173
+ commits[stats.latestCommitHour]
174
+ ]
175
+ }
176
+ ]
177
+ },
178
+
179
+ markLine: {
180
+ symbol: 'none',
181
+ animation: true,
182
+ label: { color: '#888', formatter: '{b}' },
183
+ lineStyle: { type: 'dashed', color: '#aaa' },
184
+ data: [
185
+ {
186
+ name: '上班开始',
187
+ xAxis: String(stats.startHour).padStart(2, '0')
188
+ },
189
+ { name: '下班时间', xAxis: String(stats.endHour).padStart(2, '0') },
190
+ {
191
+ name: '午休开始',
192
+ xAxis: String(stats.lunchStart).padStart(2, '0')
193
+ },
194
+ { name: '午休结束', xAxis: String(stats.lunchEnd).padStart(2, '0') }
195
+ ]
196
+ }
197
+ }
198
+ ]
116
199
  })
200
+
201
+ // 点击事件(点击某小时 → 打开侧栏)
202
+ if (typeof onHourClick === 'function') {
203
+ chart.on('click', (p) => {
204
+ const hour = Number(p.name)
205
+ onHourClick(hour, commits[hour])
206
+ })
207
+ }
208
+
117
209
  return chart
118
210
  }
119
211
 
212
+ // showSideBarForHour 实现
213
+ function showSideBarForHour(hour, commitsOrCount) {
214
+ // 支持传入 number(仅次数)或 array(详细 commit 列表)
215
+ const sidebar = document.getElementById('hourDetailSidebar')
216
+ const titleEl = document.getElementById('hourSidebarTitle')
217
+ const contentEl = document.getElementById('hourSidebarContent')
218
+
219
+ // 兼容未传入侧栏 DOM 的情况(优雅降级)
220
+ if (!sidebar || !titleEl || !contentEl) {
221
+ console.warn(
222
+ 'hourDetailSidebar DOM not found. Please add the HTML snippet.'
223
+ )
224
+ return
225
+ }
226
+
227
+ titleEl.innerHTML = `🕒 ${String(hour).padStart(2, '0')}:00 - ${String(hour).padStart(2, '0')}:59`
228
+
229
+ // 如果只是 number,显示计数
230
+ if (typeof commitsOrCount === 'number') {
231
+ contentEl.innerHTML = `<div style="font-size:14px;">提交次数:<b>${commitsOrCount}</b></div>`
232
+ } else if (Array.isArray(commitsOrCount) && commitsOrCount.length === 0) {
233
+ contentEl.innerHTML = `<div style="font-size:14px;">当小时无提交记录</div>`
234
+ } else if (Array.isArray(commitsOrCount)) {
235
+ // commits 列表:展示作者/时间/消息(最多前 50 条,避免性能问题)
236
+ const commits = commitsOrCount.slice(0, 50)
237
+ contentEl.innerHTML = commits
238
+ .map((c) => {
239
+ const author = c.author ?? c.name ?? 'unknown'
240
+ const time = c.date ?? c.time ?? ''
241
+ const msg = (c.message ?? c.msg ?? c.body ?? '').replace(/\n/g, ' ')
242
+ 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
+ `
248
+ })
249
+ .join('')
250
+
251
+ if (commitsOrCount.length > 50) {
252
+ const more = commitsOrCount.length - 50
253
+ contentEl.innerHTML += `<div style="color:#888; padding:8px 0">另外 ${more} 条已省略</div>`
254
+ }
255
+ } else {
256
+ contentEl.innerHTML = `<div style="font-size:14px;">无可展示数据</div>`
257
+ }
258
+
259
+ // 打开侧栏
260
+ sidebar.classList.add('show')
261
+ }
262
+
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
+ // 简单的 HTML 转义,防止 XSS 与布局断裂
272
+ function escapeHtml(str = '') {
273
+ return String(str)
274
+ .replaceAll('&', '&amp;')
275
+ .replaceAll('<', '&lt;')
276
+ .replaceAll('>', '&gt;')
277
+ .replaceAll('"', '&quot;')
278
+ .replaceAll("'", '&#39;')
279
+ }
280
+
120
281
  function drawOutsideVsInside(stats) {
121
282
  const el = document.getElementById('outsideVsInsideChart')
122
283
  // eslint-disable-next-line no-undef
@@ -141,57 +302,303 @@ function drawOutsideVsInside(stats) {
141
302
  }
142
303
 
143
304
  function drawDailyTrend(commits) {
305
+ if (!Array.isArray(commits) || commits.length === 0) return null
306
+
307
+ // 聚合每日提交数量
144
308
  const map = new Map()
145
309
  commits.forEach((c) => {
146
310
  const d = new Date(c.date).toISOString().slice(0, 10)
147
311
  map.set(d, (map.get(d) || 0) + 1)
148
312
  })
313
+
149
314
  const labels = Array.from(map.keys()).sort()
150
315
  const data = labels.map((l) => map.get(l))
316
+
151
317
  const el = document.getElementById('dailyTrendChart')
152
318
  // eslint-disable-next-line no-undef
153
319
  const chart = echarts.init(el)
320
+
154
321
  chart.setOption({
155
- tooltip: {},
322
+ tooltip: {
323
+ trigger: 'axis',
324
+ formatter: (params) => {
325
+ const p = params?.[0]
326
+ if (!p) return ''
327
+
328
+ const date = p.axisValue
329
+ const count = p.data
330
+
331
+ // 分级说明
332
+ let level = '🟢 正常(≤5 次)'
333
+ if (count > 5 && count < 10) level = '🟠 较高频(6–10 次)'
334
+ if (count >= 10) level = '🔴 高频(≥10 次)'
335
+
336
+ return `
337
+ <div style="font-size:13px; line-height:1.5;">
338
+ <b>${date}</b><br/>
339
+ 提交次数:<b>${count}</b><br/>
340
+ 等级:${level}
341
+ </div>
342
+ `
343
+ }
344
+ },
345
+
156
346
  xAxis: { type: 'category', data: labels },
157
- yAxis: { type: 'value' },
158
- series: [{ type: 'line', name: '每日提交', data, areaStyle: {} }]
347
+
348
+ yAxis: { type: 'value', min: 0 },
349
+
350
+ series: [
351
+ {
352
+ type: 'line',
353
+ name: '每日提交',
354
+ data,
355
+
356
+ smooth: true,
357
+
358
+ // ⭐ area 渐变背景
359
+ areaStyle: {
360
+ opacity: 0.2
361
+ },
362
+
363
+ // ⭐ 背景区间(低 / 中 / 高频)
364
+ markArea: {
365
+ data: [
366
+ [
367
+ { yAxis: 0 },
368
+ { yAxis: 5, itemStyle: { color: 'rgba(76, 175, 80, 0.12)' } } // 绿
369
+ ],
370
+ [
371
+ { yAxis: 5 },
372
+ { yAxis: 10, itemStyle: { color: 'rgba(251, 140, 0, 0.12)' } } // 橙
373
+ ],
374
+ [
375
+ { yAxis: 10 },
376
+ { yAxis: 50, itemStyle: { color: 'rgba(211, 47, 47, 0.12)' } } // 红
377
+ ]
378
+ ]
379
+ },
380
+
381
+ // ⭐ 阈值线
382
+ markLine: {
383
+ symbol: ['none', 'arrow'],
384
+ data: [
385
+ {
386
+ yAxis: 5,
387
+ lineStyle: { color: '#fb8c00', width: 2, type: 'dashed' },
388
+ label: { formatter: '5 次', color: '#fb8c00' }
389
+ },
390
+ {
391
+ yAxis: 10,
392
+ lineStyle: { color: '#d32f2f', width: 2, type: 'dashed' },
393
+ label: { formatter: '10 次', color: '#d32f2f' }
394
+ }
395
+ ]
396
+ }
397
+ }
398
+ ]
159
399
  })
400
+
160
401
  return chart
161
402
  }
162
403
 
163
404
  function drawWeeklyTrend(weekly) {
405
+ if (!Array.isArray(weekly) || weekly.length === 0) return null
406
+
164
407
  const labels = weekly.map((w) => w.period)
165
- const dataRate = weekly.map((w) => +(w.outsideWorkRate * 100).toFixed(1))
408
+ const dataRate = weekly.map((w) => +(w.outsideWorkRate * 100).toFixed(1)) // %
166
409
  const dataCount = weekly.map((w) => w.outsideWorkCount)
410
+
167
411
  const el = document.getElementById('weeklyTrendChart')
168
412
  // eslint-disable-next-line no-undef
169
413
  const chart = echarts.init(el)
414
+
170
415
  chart.setOption({
171
- tooltip: {},
416
+ tooltip: {
417
+ trigger: 'axis',
418
+ formatter: (params) => {
419
+ const rate = params.find((p) => p.seriesName.includes('%'))?.data
420
+ const count = params.find((p) => p.seriesName.includes('次数'))?.data
421
+
422
+ // 加班等级
423
+ let level = '🟢 健康(<10%)'
424
+ if (rate >= 10 && rate < 20) level = '🟠 中度(10–20%)'
425
+ if (rate >= 20) level = '🔴 严重(≥20%)'
426
+
427
+ 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
+ `
435
+ }
436
+ },
437
+
438
+ legend: {
439
+ top: 10
440
+ },
441
+
172
442
  xAxis: { type: 'category', data: labels },
173
- yAxis: [{ type: 'value', min: 0, max: 100 }, { type: 'value' }],
443
+
444
+ yAxis: [
445
+ { type: 'value', min: 0, max: 100, name: '占比(%)' },
446
+ { type: 'value', name: '次数', min: 0 }
447
+ ],
448
+
174
449
  series: [
175
- { type: 'line', name: '加班占比(%)', data: dataRate, yAxisIndex: 0 },
176
- { type: 'line', name: '加班次数', data: dataCount, yAxisIndex: 1 }
450
+ {
451
+ type: 'line',
452
+ name: '加班占比(%)',
453
+ data: dataRate,
454
+
455
+ // ⭐ 区间背景(与 monthly/daily 对齐)
456
+ markArea: {
457
+ data: [
458
+ [
459
+ { yAxis: 0 },
460
+ { yAxis: 10, itemStyle: { color: 'rgba(76, 175, 80, 0.15)' } } // 绿色
461
+ ],
462
+ [
463
+ { yAxis: 10 },
464
+ { yAxis: 20, itemStyle: { color: 'rgba(251, 140, 0, 0.15)' } } // 橙色
465
+ ],
466
+ [
467
+ { yAxis: 20 },
468
+ { yAxis: 100, itemStyle: { color: 'rgba(211, 47, 47, 0.15)' } } // 红色
469
+ ]
470
+ ]
471
+ },
472
+
473
+ // ⭐ 阈值线
474
+ markLine: {
475
+ symbol: ['none', 'arrow'],
476
+ data: [
477
+ {
478
+ yAxis: 10,
479
+ lineStyle: { color: '#fb8c00', width: 2, type: 'dashed' },
480
+ label: { formatter: '10%', color: '#fb8c00' }
481
+ },
482
+ {
483
+ yAxis: 20,
484
+ lineStyle: { color: '#d32f2f', width: 2, type: 'dashed' },
485
+ label: { formatter: '20%', color: '#d32f2f' }
486
+ }
487
+ ]
488
+ }
489
+ },
490
+
491
+ {
492
+ type: 'line',
493
+ name: '加班次数',
494
+ data: dataCount,
495
+ yAxisIndex: 1,
496
+
497
+ // 次数线使用默认蓝色,避免干扰等级颜色区间
498
+ smooth: true
499
+ }
177
500
  ]
178
501
  })
502
+
179
503
  return chart
180
504
  }
181
505
 
182
506
  function drawMonthlyTrend(monthly) {
183
507
  if (!Array.isArray(monthly) || monthly.length === 0) return null
508
+
184
509
  const labels = monthly.map((m) => m.period)
185
- const dataRate = monthly.map((m) => +(m.outsideWorkRate * 100).toFixed(1))
510
+ const dataRate = monthly.map((m) => +(m.outsideWorkRate * 100).toFixed(1)) // 0–100%
511
+
186
512
  const el = document.getElementById('monthlyTrendChart')
187
513
  // eslint-disable-next-line no-undef
188
514
  const chart = echarts.init(el)
515
+
189
516
  chart.setOption({
190
- tooltip: {},
517
+ tooltip: {
518
+ trigger: 'axis',
519
+ formatter: (params) => {
520
+ const p = params[0]
521
+ if (!p) return ''
522
+
523
+ const rate = p.data
524
+ let level = '🟢 健康(<10%)'
525
+ if (rate >= 10 && rate < 20) level = '🟠 中度(10–20%)'
526
+ if (rate >= 20) level = '🔴 严重(≥20%)'
527
+
528
+ return `
529
+ <div style="font-size:13px; line-height:1.5">
530
+ <b>${p.axisValue}</b><br/>
531
+ 加班占比:<b>${rate}%</b><br/>
532
+ 加班等级:${level}
533
+ </div>
534
+ `
535
+ }
536
+ },
537
+
191
538
  xAxis: { type: 'category', data: labels },
192
539
  yAxis: { type: 'value', min: 0, max: 100 },
193
- series: [{ type: 'line', name: '加班占比(%)', data: dataRate }]
540
+
541
+ series: [
542
+ {
543
+ type: 'line',
544
+ name: '加班占比(%)',
545
+ data: dataRate,
546
+
547
+ // ⭐ 区间背景(可配置)
548
+ markArea: {
549
+ data: [
550
+ // <10% 绿色轻度
551
+ [
552
+ { yAxis: 0 },
553
+ { yAxis: 10, itemStyle: { color: 'rgba(76, 175, 80, 0.15)' } }
554
+ ],
555
+ // 10–20% 橙色中度
556
+ [
557
+ { yAxis: 10 },
558
+ { yAxis: 20, itemStyle: { color: 'rgba(251, 140, 0, 0.15)' } }
559
+ ],
560
+ // ≥20% 红色严重
561
+ [
562
+ { yAxis: 20 },
563
+ { yAxis: 100, itemStyle: { color: 'rgba(211, 47, 47, 0.15)' } }
564
+ ]
565
+ ]
566
+ },
567
+
568
+ // ⭐ 阈值线(同每日图风格)
569
+ markLine: {
570
+ symbol: ['none', 'arrow'],
571
+ data: [
572
+ {
573
+ yAxis: 10,
574
+ lineStyle: {
575
+ color: '#fb8c00',
576
+ width: 2,
577
+ type: 'dashed'
578
+ },
579
+ label: {
580
+ formatter: '10%',
581
+ color: '#fb8c00'
582
+ }
583
+ },
584
+ {
585
+ yAxis: 20,
586
+ lineStyle: {
587
+ color: '#d32f2f',
588
+ width: 2,
589
+ type: 'dashed'
590
+ },
591
+ label: {
592
+ formatter: '20%',
593
+ color: '#d32f2f'
594
+ }
595
+ }
596
+ ]
597
+ }
598
+ }
599
+ ]
194
600
  })
601
+
195
602
  return chart
196
603
  }
197
604
 
@@ -211,6 +618,7 @@ function drawLatestHourDaily(latestByDay) {
211
618
  value: v,
212
619
  itemStyle: {
213
620
  color:
621
+ // eslint-disable-next-line no-nested-ternary
214
622
  v >= 20
215
623
  ? '#d32f2f' // 红
216
624
  : v >= 19
@@ -224,6 +632,7 @@ function drawLatestHourDaily(latestByDay) {
224
632
  const maxV = numericValues.length > 0 ? Math.max(...numericValues) : 0
225
633
 
226
634
  const el = document.getElementById('latestHourDailyChart')
635
+ // eslint-disable-next-line no-undef
227
636
  const chart = echarts.init(el)
228
637
 
229
638
  chart.setOption({
@@ -233,8 +642,31 @@ function drawLatestHourDaily(latestByDay) {
233
642
  const p = Array.isArray(params) ? params[0] : params
234
643
  const v = p?.value != null ? Number(p.value) : null
235
644
  const endH = window.__overtimeEndHour || 18
236
- const sev = v != null ? Math.max(0, v - endH) : 0
237
- return `${p.axisValue}<br/>最晚小时: ${v != null ? v : '-'}<br/>超过下班: ${sev} 小时`
645
+
646
+ if (v == null) {
647
+ return `
648
+ <div style="font-size:13px; line-height:1.5">
649
+ <b>${p.axisValue}</b><br/>
650
+ 无数据
651
+ </div>
652
+ `
653
+ }
654
+
655
+ const overtime = Math.max(0, v - endH)
656
+ const overtimeText = overtime.toFixed(2)
657
+
658
+ let level = '🟢 正常(无明显加班)'
659
+ if (overtime >= 1 && overtime < 2) level = '🟠 中度加班(1–2h)'
660
+ if (overtime >= 2) level = '🔴 严重加班(≥2h)'
661
+
662
+ return `
663
+ <div style="font-size:13px; line-height:1.5">
664
+ <b>${p.axisValue}</b><br/>
665
+ 最晚提交时间:<b>${v.toFixed(2)} 点</b><br/>
666
+ 超出下班:<b>${overtimeText} 小时</b><br/>
667
+ 加班等级:${level}
668
+ </div>
669
+ `
238
670
  }
239
671
  },
240
672
  xAxis: { type: 'category', data: labels },
@@ -287,24 +719,60 @@ function drawLatestHourDaily(latestByDay) {
287
719
  }
288
720
 
289
721
  function drawDailySeverity(latestByDay) {
290
- if (!Array.isArray(latestByDay) || latestByDay.length === 0) return null;
722
+ if (!Array.isArray(latestByDay) || latestByDay.length === 0) return null
291
723
 
292
- const labels = latestByDay.map((d) => d.date);
293
- const endH = window.__overtimeEndHour || 18;
724
+ const labels = latestByDay.map((d) => d.date)
725
+ const endH = window.__overtimeEndHour || 18
294
726
 
295
727
  const raw = latestByDay.map((d) =>
296
728
  typeof d.latestHourNormalized === 'number'
297
729
  ? d.latestHourNormalized
298
730
  : (d.latestHour ?? null)
299
- );
731
+ )
300
732
 
301
- const sev = raw.map((v) => (v == null ? null : Math.max(0, Number(v) - endH)));
733
+ const sev = raw.map((v) => (v == null ? null : Math.max(0, Number(v) - endH)))
302
734
 
303
- const el = document.getElementById('dailySeverityChart');
304
- const chart = echarts.init(el);
735
+ const el = document.getElementById('dailySeverityChart')
736
+ // eslint-disable-next-line no-undef
737
+ const chart = echarts.init(el)
305
738
 
306
739
  chart.setOption({
307
- tooltip: {},
740
+ tooltip: {
741
+ trigger: 'axis',
742
+ formatter: (params) => {
743
+ const p = params[0]
744
+ if (!p) return ''
745
+ const date = p.axisValue
746
+ const overtime = p.data
747
+ const rawHour = raw[p.dataIndex] // 原始 latestHour 或 latestHourNormalized
748
+
749
+ if (overtime == null) {
750
+ return `
751
+ <div style="font-size:13px;">
752
+ <b>${date}</b><br/>
753
+ 无数据
754
+ </div>
755
+ `
756
+ }
757
+
758
+ return `
759
+ <div style="font-size:13px;">
760
+ <b>${date}</b><br/>
761
+ 下班后:<b>${overtime.toFixed(2)} 小时</b><br/>
762
+ 原始最晚提交:${rawHour != null ? `${rawHour.toFixed(2)} 点` : '无'}<br/>
763
+ 加班等级:${
764
+ // eslint-disable-next-line no-nested-ternary
765
+ overtime < 1
766
+ ? '🟢 0–1 小时(轻度)'
767
+ : overtime < 2
768
+ ? '🟠 1–2 小时(中度)'
769
+ : '🔴 ≥2 小时(严重)'
770
+ }
771
+ </div>
772
+ `
773
+ }
774
+ },
775
+
308
776
  xAxis: { type: 'category', data: labels },
309
777
  yAxis: { type: 'value', min: 0 },
310
778
 
@@ -318,10 +786,7 @@ function drawDailySeverity(latestByDay) {
318
786
  markArea: {
319
787
  data: [
320
788
  // 0–1h:透明
321
- [
322
- { yAxis: 0 },
323
- { yAxis: 1, itemStyle: { color: 'rgba(0,0,0,0)' } }
324
- ],
789
+ [{ yAxis: 0 }, { yAxis: 1, itemStyle: { color: 'rgba(0,0,0,0)' } }],
325
790
  // 1–2h:半透明橙色
326
791
  [
327
792
  { yAxis: 1 },
@@ -361,11 +826,197 @@ function drawDailySeverity(latestByDay) {
361
826
  }
362
827
  }
363
828
  ]
364
- });
829
+ })
830
+
831
+ return chart
832
+ }
833
+
834
+ /**
835
+ * 绘制每日趋势(带加班严重度背景区间)并自动分析最累的日期
836
+ * @param {Array} commits - 原始提交记录(包含 c.date)
837
+ * @param {Function} onDayClick - 用户点击某一天时的回调 (date, count) => void
838
+ */
839
+ /**
840
+ * 绘制每日趋势(含严重度背景区间、最累标记、tooltip 明细)
841
+ */
842
+ function drawDailyTrendSeverity(commits, weekly, onDayClick) {
843
+ // ---------- 1. 聚合每日数据 ----------
844
+ const dayMap = new Map()
845
+ const dayCommitsDetail = {}
846
+
847
+ commits.forEach((c) => {
848
+ const d = new Date(c.date).toISOString().slice(0, 10)
849
+
850
+ // 数量统计
851
+ dayMap.set(d, (dayMap.get(d) || 0) + 1)
852
+
853
+ // 详细信息统计(用于 tooltip 显示)
854
+ if (!dayCommitsDetail[d]) dayCommitsDetail[d] = []
855
+ dayCommitsDetail[d].push({
856
+ author: c.author,
857
+ time: c.date,
858
+ msg: c.message
859
+ })
860
+ })
861
+
862
+ const labels = Array.from(dayMap.keys()).sort()
863
+ const data = labels.map((l) => dayMap.get(l))
864
+
865
+ // ---------- 2. 自动分析「最累的一天」 ----------
866
+ const maxDailyCount = Math.max(...data)
867
+ const maxDailyIndex = data.indexOf(maxDailyCount)
868
+ const mostTiredDay = labels[maxDailyIndex]
869
+
870
+ document.getElementById('mostTiredDay').innerHTML =
871
+ `🔥 最累的一天:<b>${mostTiredDay}</b>(${maxDailyCount} 次提交)`
872
+
873
+ // ---------- 3. 自动分析「最累的一周」 ----------
874
+ let maxWeek = null
875
+ if (Array.isArray(weekly)) {
876
+ maxWeek = weekly.reduce((a, b) =>
877
+ a.outsideWorkCount > b.outsideWorkCount ? a : b
878
+ )
879
+ if (maxWeek) {
880
+ document.getElementById('mostTiredWeek').innerHTML =
881
+ `🔥 最累的一周:<b>${maxWeek.period}</b>(${maxWeek.outsideWorkCount} 次加班)`
882
+ }
883
+ }
884
+
885
+ // ---------- 4. 自动分析「最累的月份」 ----------
886
+ const monthMap = new Map()
887
+ commits.forEach((c) => {
888
+ const d = new Date(c.date)
889
+ const ym = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
890
+ monthMap.set(ym, (monthMap.get(ym) || 0) + 1)
891
+ })
892
+
893
+ const mostTiredMonth = Array.from(monthMap.entries()).sort(
894
+ (a, b) => b[1] - a[1]
895
+ )[0]
896
+
897
+ document.getElementById('mostTiredMonth').innerHTML =
898
+ `🔥 最累的月份:<b>${mostTiredMonth[0]}</b>(${mostTiredMonth[1]} 次提交)`
899
+
900
+ // ---------- 5. 背景严重度区块 ----------
901
+ const markArea = {
902
+ silent: true,
903
+ itemStyle: { opacity: 0.15 },
904
+ data: [
905
+ [{ name: '0–1 小时', yAxis: 0 }, { yAxis: 1 }],
906
+ [
907
+ { name: '1–2 小时', yAxis: 1 },
908
+ { yAxis: 2, itemStyle: { color: 'orange', opacity: 0.25 } }
909
+ ],
910
+ [
911
+ { name: '2 小时以上', yAxis: 2 },
912
+ { yAxis: 999, itemStyle: { color: 'red', opacity: 0.25 } }
913
+ ]
914
+ ]
915
+ }
916
+
917
+ // ---------- 6. 构造 tooltip ----------
918
+ const tooltipFormatter = (params) => {
919
+ const date = params?.[0].name
920
+ const count = params?.[0].value
921
+ const details = dayCommitsDetail[date] || []
922
+
923
+ let html = `📅 <b>${date}</b><br/>提交次数:${count}<br/><br/>`
924
+
925
+ details.slice(0, 5).forEach((d) => {
926
+ html += `👤 ${d.author}<br/>🕒 ${d.time}<br/>💬 ${d.msg}<br/><br/>`
927
+ })
928
+
929
+ if (details.length > 5) {
930
+ html += `(其余 ${details.length - 5} 条已省略)`
931
+ }
932
+
933
+ return html
934
+ }
935
+
936
+ // ---------- 7. 绘图 ----------
937
+ const el = document.getElementById('dailyTrendChartDog')
938
+ const chart = echarts.init(el)
939
+
940
+ chart.setOption({
941
+ tooltip: {
942
+ trigger: 'axis',
943
+ formatter: tooltipFormatter,
944
+ axisPointer: { type: 'shadow' }
945
+ },
946
+ xAxis: { type: 'category', data: labels },
947
+ yAxis: { type: 'value', min: 0 },
948
+ series: [
949
+ {
950
+ type: 'line',
951
+ name: '每日提交',
952
+ data,
953
+ areaStyle: {},
954
+ markArea,
955
+ markPoint: {
956
+ data: [
957
+ {
958
+ name: '最累的一天',
959
+ coord: [mostTiredDay, maxDailyCount],
960
+ value: maxDailyCount,
961
+ symbolSize: 70,
962
+ itemStyle: { color: '#ff4d4f' },
963
+ label: { formatter: '🔥 最累' }
964
+ }
965
+ ]
966
+ }
967
+ }
968
+ ]
969
+ })
365
970
 
366
- return chart;
971
+ // ---------- 8. 点击事件 ----------
972
+ if (typeof onDayClick === 'function') {
973
+ chart.on('click', (params) => {
974
+ if (params.componentType === 'series') {
975
+ const date = labels[params.dataIndex]
976
+ const count = data[params.dataIndex]
977
+ onDayClick(date, count, dayCommitsDetail[date])
978
+ }
979
+ })
980
+ }
981
+
982
+ return {
983
+ chart,
984
+ analysis: {
985
+ mostTiredDay,
986
+ mostTiredMonth,
987
+ mostTiredWeek: maxWeek
988
+ }
989
+ }
367
990
  }
368
991
 
992
+ function showDayDetailSidebar(date, count, commits) {
993
+ const sidebar = document.getElementById('dayDetailSidebar')
994
+ const title = document.getElementById('sidebarTitle')
995
+ const content = document.getElementById('sidebarContent')
996
+
997
+ title.innerHTML = `📅 ${date}(${count} 次提交)`
998
+
999
+ // 渲染详情
1000
+ content.innerHTML = commits
1001
+ .map(
1002
+ (c) => `
1003
+ <div style="margin-bottom:12px;">
1004
+ <div>👤 <b>${c.author}</b></div>
1005
+ <div>🕒 ${c.time}</div>
1006
+ <div>💬 ${c.msg}</div>
1007
+ </div>
1008
+ <hr/>
1009
+ `
1010
+ )
1011
+ .join('')
1012
+
1013
+ sidebar.classList.add('show')
1014
+ }
1015
+
1016
+ // 关闭按钮
1017
+ document.getElementById('sidebarClose').onclick = () => {
1018
+ document.getElementById('dayDetailSidebar').classList.remove('show')
1019
+ }
369
1020
 
370
1021
  function renderKpi(stats) {
371
1022
  const el = document.getElementById('kpiContent')
@@ -378,13 +1029,27 @@ function renderKpi(stats) {
378
1029
  (latestOut ? new Date(latestOut.date).getHours() : null)
379
1030
  const cutoff = window.__overnightCutoff ?? 6
380
1031
  const html = [
381
- `<div>最晚一次提交时间:${latest ? formatDate(latest.date) : '-'}${typeof latestHour === 'number' ? `(${String(latestHour).padStart(2, '0')}:00)` : ''}</div>`,
382
- `<div>加班最晚一次提交时间:${latestOut ? formatDate(latestOut.date) : '-'}${typeof latestOutHour === 'number' ? `(${String(latestOutHour).padStart(2, '0')}:00)` : ''}</div>`,
1032
+ `<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
+ `<div class="hr"></div>`,
1034
+ `<div>加班最晚一次提交时间:${latestOut ? formatDate(latestOut.date) : '-'}${typeof latestOutHour === 'number' ? `(${String(latestOutHour).padStart(2, '0')}:00)` : ''} <div class="author">${latestOut.author}</div> <div>${latestOut.message}</div> </div>`,
1035
+ `<div class="hr"></div>`,
383
1036
  `<div>次日归并窗口:凌晨 <b>${cutoff}</b> 点内归前一日</div>`
384
1037
  ].join('')
385
1038
  el.innerHTML = html
386
1039
  }
387
1040
 
1041
+ // 1) 按小时分组(例:commits 为原始提交数组)
1042
+ function groupCommitsByHour(commits) {
1043
+ const byHour = Array.from({ length: 24 }, () => [])
1044
+ commits.forEach((c) => {
1045
+ // 解析 commit 的本地小时(考虑时区已有 '+0800' 等)
1046
+ const d = new Date(c.date)
1047
+ const h = d.getHours() // 若数据已为 UTC,请按需求调整
1048
+ byHour[h].push(c)
1049
+ })
1050
+ return byHour
1051
+ }
1052
+
388
1053
  ;(async function main() {
389
1054
  const { commits, stats, weekly, monthly, latestByDay, config } =
390
1055
  await loadData()
@@ -399,12 +1064,21 @@ function renderKpi(stats) {
399
1064
  initTableControls()
400
1065
  updatePager()
401
1066
  renderCommitsTablePage()
402
- drawHourlyOvertime(stats)
1067
+ // 使用举例
1068
+ const hourCommitsDetail = groupCommitsByHour(commits)
1069
+
1070
+ drawHourlyOvertime(stats, (hour, count) => {
1071
+ // 将 commit 列表传给侧栏(若没有详情,则传空数组)
1072
+ showSideBarForHour(hour, hourCommitsDetail[hour] || [])
1073
+ })
403
1074
  drawOutsideVsInside(stats)
404
1075
  drawDailyTrend(commits)
405
1076
  drawWeeklyTrend(weekly)
406
1077
  drawMonthlyTrend(monthly)
407
1078
  drawLatestHourDaily(latestByDay)
408
1079
  drawDailySeverity(latestByDay)
1080
+ const daily = drawDailyTrendSeverity(commits, weekly, showDayDetailSidebar)
1081
+
1082
+ console.log('最累的一天:', daily.analysis.mostTiredDay)
409
1083
  renderKpi(stats)
410
1084
  })()
package/web/index.html CHANGED
@@ -32,6 +32,7 @@
32
32
  <h2>按日提交趋势</h2>
33
33
  <div id="dailyTrendChart" class="echart"></div>
34
34
  </div>
35
+
35
36
  <div class="chart-card">
36
37
  <h2>每周趋势(加班占比)</h2>
37
38
  <div id="weeklyTrendChart" class="echart"></div>
@@ -48,12 +49,23 @@
48
49
  <h2>每日超过下班的小时数</h2>
49
50
  <div id="dailySeverityChart" class="echart"></div>
50
51
  </div>
52
+ <div class="chart-card">
53
+ <h2>按日提交趋势</h2>
54
+ <div id="dailyTrendChartDog" class="echart"></div>
55
+ </div>
56
+ <div id="mostTiredDay"></div>
57
+ <div id="mostTiredWeek"></div>
58
+ <div id="mostTiredMonth"></div>
51
59
  </section>
52
60
 
53
61
  <section class="table-card">
54
62
  <h2>提交清单</h2>
55
63
  <div id="tableControls">
56
- <input id="searchInput" type="search" placeholder="搜索作者/信息/Hash" />
64
+ <input
65
+ id="searchInput"
66
+ type="search"
67
+ placeholder="搜索作者/信息/Hash"
68
+ />
57
69
  <label for="pageSize">每页显示</label>
58
70
  <select id="pageSize">
59
71
  <option value="10">10</option>
@@ -86,6 +98,26 @@
86
98
  <code>/data/</code>.</small
87
99
  >
88
100
  </footer>
101
+ <!-- 右侧滑出的详情侧栏 -->
102
+ <div id="dayDetailSidebar" class="sidebar">
103
+ <div class="sidebar-header">
104
+ <span id="sidebarTitle"></span>
105
+ <button id="sidebarClose">×</button>
106
+ </div>
107
+
108
+ <div id="sidebarContent" class="sidebar-content"></div>
109
+ </div>
110
+
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
+
89
121
  <script type="module" src="/app.js"></script>
90
122
  </body>
91
123
  </html>
@@ -1,6 +1,11 @@
1
1
  /* 全局 -------------------------------------------------- */
2
2
  body {
3
- font-family: "Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", Arial;
3
+ font-family:
4
+ 'Roboto',
5
+ -apple-system,
6
+ BlinkMacSystemFont,
7
+ 'Segoe UI',
8
+ Arial;
4
9
  margin: 0;
5
10
  padding: 0;
6
11
  background: #f5f5f5;
@@ -14,7 +19,7 @@ header {
14
19
  padding: 16px 24px;
15
20
  display: flex;
16
21
  align-items: center;
17
- box-shadow: 0 2px 8px rgba(0,0,0,0.15);
22
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
18
23
  }
19
24
 
20
25
  header h1 {
@@ -39,7 +44,7 @@ main {
39
44
  background: white;
40
45
  border-radius: 12px;
41
46
  padding: 20px;
42
- box-shadow: 0 3px 8px rgba(0,0,0,0.06);
47
+ box-shadow: 0 3px 8px rgba(0, 0, 0, 0.06);
43
48
  }
44
49
 
45
50
  .chart-card h2,
@@ -51,9 +56,22 @@ main {
51
56
  }
52
57
 
53
58
  /* 图表网格 -------------------------------------------------- */
54
- #charts { display: grid; grid-template-columns: repeat(auto-fit, minmax(640px, 1fr)); gap: 16px; }
55
- #chartsHalf { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; }
56
- .chart-card { background: white; border-radius: 8px; padding: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); }
59
+ #charts {
60
+ display: grid;
61
+ grid-template-columns: repeat(auto-fit, minmax(640px, 1fr));
62
+ gap: 16px;
63
+ }
64
+ #chartsHalf {
65
+ display: grid;
66
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
67
+ gap: 16px;
68
+ }
69
+ .chart-card {
70
+ background: white;
71
+ border-radius: 8px;
72
+ padding: 12px;
73
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
74
+ }
57
75
  .echart {
58
76
  width: 100%;
59
77
  height: 340px;
@@ -89,19 +107,21 @@ td {
89
107
  }
90
108
 
91
109
  /* 输入框 — MUI Filled 风格 */
92
- #tableControls input[type="search"] {
110
+ #tableControls input[type='search'] {
93
111
  flex: 1;
94
112
  min-width: 240px;
95
113
  padding: 10px 12px;
96
114
  border: 1px solid #ccc;
97
115
  background: #fafafa;
98
116
  border-radius: 8px;
99
- transition: border-color 0.2s, box-shadow 0.2s;
117
+ transition:
118
+ border-color 0.2s,
119
+ box-shadow 0.2s;
100
120
  }
101
121
 
102
- #tableControls input[type="search"]:focus {
122
+ #tableControls input[type='search']:focus {
103
123
  border-color: #1976d2;
104
- box-shadow: 0 0 0 3px rgba(25,118,210,0.15);
124
+ box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.15);
105
125
  outline: none;
106
126
  }
107
127
 
@@ -116,24 +136,26 @@ td {
116
136
 
117
137
  #tableControls select:focus {
118
138
  border-color: #1976d2;
119
- box-shadow: 0 0 0 3px rgba(25,118,210,0.15);
139
+ box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.15);
120
140
  outline: none;
121
141
  }
122
142
 
123
143
  /* 按钮 — MUI Button 风格 */
124
144
  .pager button {
125
145
  padding: 8px 14px;
126
- border: 1px solid #00A76F;
127
- background: #00A76F;
146
+ border: 1px solid #00a76f;
147
+ background: #00a76f;
128
148
  color: white;
129
149
  border-radius: 8px;
130
150
  cursor: pointer;
131
- transition: background 0.2s, box-shadow 0.2s;
151
+ transition:
152
+ background 0.2s,
153
+ box-shadow 0.2s;
132
154
  }
133
155
 
134
156
  .pager button:hover {
135
157
  background: #007867;
136
- box-shadow: 0 2px 5px rgba(0,0,0,0.15);
158
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);
137
159
  }
138
160
 
139
161
  .pager button:disabled {
@@ -156,6 +178,74 @@ td {
156
178
  color: #555;
157
179
  }
158
180
 
181
+ .hr {
182
+ border: none;
183
+ border-top: 1px solid #eee;
184
+ margin: 12px 0;
185
+ }
186
+ .author {
187
+ font-weight: bold;
188
+ margin-top: 4px;
189
+ margin-bottom: 8px;
190
+ color: #00a76f;
191
+ }
192
+ .sidebar {
193
+ position: fixed;
194
+ top: 0;
195
+ right: -400px;
196
+ width: 400px;
197
+ height: 100%;
198
+ 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;
203
+ display: flex;
204
+ flex-direction: column;
205
+ box-sizing: border-box;
206
+ }
207
+
208
+ .sidebar.show {
209
+ right: 0;
210
+ }
211
+
212
+ .sidebar-header {
213
+ display: flex;
214
+ justify-content: space-between;
215
+ align-items: center;
216
+ font-size: 18px;
217
+ margin-bottom: 16px;
218
+ }
219
+
220
+ .sidebar-header button {
221
+ background: none;
222
+ border: none;
223
+ font-size: 24px;
224
+ cursor: pointer;
225
+ }
226
+
227
+ .sidebar-content {
228
+ overflow-y: auto;
229
+ font-size: 14px;
230
+ }
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);
238
+ }
239
+ .hour-commit .meta {
240
+ color: #666;
241
+ font-size: 12px;
242
+ margin-bottom: 6px;
243
+ }
244
+ .hour-commit .msg {
245
+ font-weight: 500;
246
+ word-break: break-word;
247
+ }
248
+
159
249
  /* 底部 -------------------------------------------------- */
160
250
  footer {
161
251
  text-align: center;