wukong-gitlog-cli 1.0.15 → 1.0.17

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,27 @@
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.17](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.16...v1.0.17) (2025-12-05)
6
+
7
+
8
+ ### Features
9
+
10
+ * 🎸 change line total ([bf01655](https://github.com/tomatobybike/wukong-gitlog-cli/commit/bf01655f3721b87d4bb36e74a7ce6dadf5e7ee94))
11
+ * 🎸 develop Changes totals ([fb64986](https://github.com/tomatobybike/wukong-gitlog-cli/commit/fb64986cfde215fc58ce7b1b66cbc8bdbf280627))
12
+
13
+ ### [1.0.16](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.15...v1.0.16) (2025-12-03)
14
+
15
+
16
+ ### Features
17
+
18
+ * 🎸 sider bar UI ([e3a48ab](https://github.com/tomatobybike/wukong-gitlog-cli/commit/e3a48abe5b0148fe0d9dcd42b7b028cad53d4c94))
19
+ * 🎸 ui show ([e9eba58](https://github.com/tomatobybike/wukong-gitlog-cli/commit/e9eba5823333545d598be363b58eca1f6ecfa6ed))
20
+
21
+
22
+ ### Bug Fixes
23
+
24
+ * 🐛 makeline clickEvent ([4ad6378](https://github.com/tomatobybike/wukong-gitlog-cli/commit/4ad6378d88aca2dde6083efde844730443228e48))
25
+
5
26
  ### [1.0.15](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.14...v1.0.15) (2025-12-03)
6
27
 
7
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wukong-gitlog-cli",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "description": "Advanced Git commit log exporter with Excel/JSON/TXT output, grouping, stats and CLI.",
5
5
  "keywords": [
6
6
  "git",
@@ -90,20 +90,19 @@
90
90
  ]
91
91
  },
92
92
  "dependencies": {
93
+ "boxen": "8.0.1",
94
+ "chalk": "5.6.2",
93
95
  "commander": "12.1.0",
94
- "dayjs": "1.11.19",
95
96
  "date-holidays": "2.1.1",
96
- "string-width": "5.1.2",
97
+ "dayjs": "1.11.19",
97
98
  "exceljs": "4.4.0",
98
- "zx": "7.2.4",
99
99
  "is-online": "12.0.2",
100
- "boxen": "8.0.1",
101
- "chalk": "5.6.2"
100
+ "ora": "9.0.0",
101
+ "string-width": "5.1.2",
102
+ "zx": "7.2.4"
102
103
  },
103
104
  "devDependencies": {
104
105
  "@trivago/prettier-plugin-sort-imports": "5.2.2",
105
- "husky": "^9.1.7",
106
- "lint-staged": "^16.2.7",
107
106
  "eslint": "8.57.1",
108
107
  "eslint-config-airbnb-base": "15.0.0",
109
108
  "eslint-config-prettier": "10.1.8",
@@ -111,6 +110,8 @@
111
110
  "eslint-plugin-import": "2.32.0",
112
111
  "eslint-plugin-prettier": "5.5.4",
113
112
  "eslint-plugin-simple-import-sort": "12.1.1",
113
+ "husky": "^9.1.7",
114
+ "lint-staged": "^16.2.7",
114
115
  "prettier": "3.6.2",
115
116
  "prettier-plugin-packagejson": "2.5.19",
116
117
  "sort-package-json": "3.4.0",
package/src/cli.mjs CHANGED
@@ -3,12 +3,18 @@ import { Command } from 'commander'
3
3
  import dayjs from 'dayjs'
4
4
  import isoWeek from 'dayjs/plugin/isoWeek.js'
5
5
  import fs from 'fs'
6
+ import ora from 'ora'
6
7
  import path from 'path'
7
8
  import { fileURLToPath } from 'url'
8
9
 
9
10
  import { CLI_NAME } from './constants/index.mjs'
10
- import { exportExcel, exportExcelPerPeriodSheets } from './excel.mjs'
11
+ import {
12
+ exportExcel,
13
+ exportExcelAuthorChangeStats,
14
+ exportExcelPerPeriodSheets
15
+ } from './excel.mjs'
11
16
  import { getGitLogs } from './git.mjs'
17
+ import { renderAuthorChangesJson } from './json.mjs'
12
18
  import {
13
19
  analyzeOvertime,
14
20
  renderOvertimeCsv,
@@ -16,7 +22,7 @@ import {
16
22
  renderOvertimeText
17
23
  } from './overtime.mjs'
18
24
  import { startServer } from './server.mjs'
19
- import { renderText } from './text.mjs'
25
+ import { renderChangedLinesText, renderText } from './text.mjs'
20
26
  import { checkUpdateWithPatch } from './utils/checkUpdate.mjs'
21
27
  import {
22
28
  groupRecords,
@@ -194,6 +200,8 @@ const main = async () => {
194
200
  return
195
201
  }
196
202
 
203
+ const spinner = ora('Loading...').start()
204
+
197
205
  let records = await getGitLogs(opts)
198
206
 
199
207
  // compute output directory root if user provided one or wants parent
@@ -342,6 +350,13 @@ const main = async () => {
342
350
  const dataCommitsFile = outputFilePath('data/commits.mjs', outDir)
343
351
  const commitsModule = `export default ${JSON.stringify(records, null, 2)};\n`
344
352
  writeTextFile(dataCommitsFile, commitsModule)
353
+
354
+ const dataCommitsChangedFile = outputFilePath('data/author-changes.mjs', outDir)
355
+ const jsonText = renderAuthorChangesJson(records)
356
+
357
+ const commitsChangedModule = `export default ${JSON.stringify(jsonText, null, 2)};\n`
358
+ writeTextFile(dataCommitsChangedFile, commitsChangedModule)
359
+
345
360
  const dataStatsFile = outputFilePath('data/overtime-stats.mjs', outDir)
346
361
  const statsModule = `export default ${JSON.stringify(stats, null, 2)};\n`
347
362
  writeTextFile(dataStatsFile, statsModule)
@@ -770,7 +785,10 @@ const main = async () => {
770
785
  const file = opts.out || 'commits.json'
771
786
  const filepath = outputFilePath(file, outDir)
772
787
  writeJSON(filepath, groups || records)
788
+ const jsonText = renderAuthorChangesJson(records)
789
+ writeJSON(outputFilePath('author-changes.json', outDir), jsonText)
773
790
  console.log(chalk.green(`JSON 已导出: ${filepath}`))
791
+ spinner.succeed('Done')
774
792
  return
775
793
  }
776
794
 
@@ -779,8 +797,13 @@ const main = async () => {
779
797
  const filepath = outputFilePath(file, outDir)
780
798
  const text = renderText(records, groups, { showGerrit: !!opts.gerrit })
781
799
  writeTextFile(filepath, text)
800
+ writeTextFile(
801
+ outputFilePath('author-changes.text', outDir),
802
+ renderChangedLinesText(records)
803
+ )
782
804
  console.log(text)
783
805
  console.log(chalk.green(`文本已导出: ${filepath}`))
806
+ spinner.succeed('Done')
784
807
  return
785
808
  }
786
809
 
@@ -794,11 +817,17 @@ const main = async () => {
794
817
  stats: opts.stats,
795
818
  gerrit: opts.gerrit
796
819
  })
820
+ await exportExcelAuthorChangeStats(
821
+ records,
822
+ outputFilePath('author_stats.xlsx', outDir)
823
+ )
797
824
  const text = renderText(records, groups)
798
825
  writeTextFile(txtPath, text)
799
826
  console.log(chalk.green(`Excel 已导出: ${excelPath}`))
800
827
  console.log(chalk.green(`文本已自动导出: ${txtPath}`))
828
+ spinner.succeed('Done')
801
829
  }
830
+
802
831
  await autoCheckUpdate()
803
832
  }
804
833
 
package/src/excel.mjs CHANGED
@@ -1,97 +1,167 @@
1
- import ExcelJS from 'exceljs';
2
- import dayjs from 'dayjs';
1
+ import dayjs from 'dayjs'
2
+ import ExcelJS from 'exceljs'
3
+ import { buildAuthorChangeStats } from './stats.mjs'
4
+
3
5
 
4
6
  export async function exportExcel(records, groups, options = {}) {
5
- const { file, stats, gerrit } = options;
7
+ const { file, stats, gerrit } = options
6
8
 
7
- const wb = new ExcelJS.Workbook();
8
- const ws = wb.addWorksheet('Commits');
9
+ const wb = new ExcelJS.Workbook()
10
+ const ws = wb.addWorksheet('Commits')
9
11
 
10
12
  const cols = [
11
13
  { header: 'Hash', key: 'hash', width: 12 },
12
14
  { header: 'Author', key: 'author', width: 20 },
13
15
  { header: 'Email', key: 'email', width: 30 },
14
16
  { header: 'Date', key: 'date', width: 20 },
15
- { header: 'Message', key: 'message', width: 80 }
16
- ];
17
+ { header: 'Message', key: 'message', width: 80 },
18
+ { header: 'Changed', key: 'changed', width: 12 } // ★ 新增 changed 列
19
+ ]
17
20
 
18
21
  if (gerrit) {
19
- cols.push({ header: 'Gerrit', key: 'gerrit', width: 50 });
22
+ cols.push({ header: 'Gerrit', key: 'gerrit', width: 50 })
20
23
  }
21
24
 
22
- ws.columns = cols;
25
+ ws.columns = cols
23
26
 
24
- (groups ? Object.values(groups).flat() : records).forEach(r =>
27
+ ;(groups ? Object.values(groups).flat() : records).forEach((r) =>
25
28
  ws.addRow(r)
26
- );
29
+ )
27
30
 
28
- ws.autoFilter = { from: { row: 1, column: 1 }, to: { row: 1, column: cols.length } };
31
+ ws.autoFilter = {
32
+ from: { row: 1, column: 1 },
33
+ to: { row: 1, column: cols.length }
34
+ }
29
35
 
30
36
  // --- stats sheet ---
31
37
  if (stats) {
32
- const statWs = wb.addWorksheet('Stats');
38
+ const statWs = wb.addWorksheet('Stats')
33
39
 
34
- const map = {};
40
+ const map = {}
35
41
 
36
- records.forEach(r => {
37
- const d = dayjs(r.date).format('YYYY-MM-DD');
38
- map[d] = (map[d] || 0) + 1;
39
- });
42
+ records.forEach((r) => {
43
+ const d = dayjs(r.date).format('YYYY-MM-DD')
44
+ map[d] = (map[d] || 0) + 1
45
+ })
40
46
 
41
47
  statWs.columns = [
42
48
  { header: 'Date', key: 'date', width: 15 },
43
49
  { header: 'Commits', key: 'count', width: 15 }
44
- ];
50
+ ]
45
51
 
46
52
  Object.entries(map).forEach(([d, cnt]) =>
47
53
  statWs.addRow({ date: d, count: cnt })
48
- );
54
+ )
49
55
  }
50
56
 
51
- await wb.xlsx.writeFile(file);
57
+ await wb.xlsx.writeFile(file)
52
58
  }
53
59
 
54
60
  export async function exportExcelPerPeriodSheets(groups, file, options = {}) {
55
- // groups: { periodKey: [records] }
56
- const { stats, gerrit } = options;
61
+ const { stats, gerrit } = options
57
62
 
58
- const wb = new ExcelJS.Workbook();
63
+ const wb = new ExcelJS.Workbook()
59
64
 
60
65
  const cols = [
61
66
  { header: 'Hash', key: 'hash', width: 12 },
62
67
  { header: 'Author', key: 'author', width: 20 },
63
68
  { header: 'Email', key: 'email', width: 30 },
64
69
  { header: 'Date', key: 'date', width: 20 },
65
- { header: 'Message', key: 'message', width: 80 }
66
- ];
67
- if (gerrit) cols.push({ header: 'Gerrit', key: 'gerrit', width: 50 });
70
+ { header: 'Message', key: 'message', width: 80 },
71
+ { header: 'Changed', key: 'changed', width: 12 } // ★ 新增 changed 列
72
+ ]
73
+
74
+ if (gerrit) cols.push({ header: 'Gerrit', key: 'gerrit', width: 50 })
75
+
76
+ const keys = Object.keys(groups).sort()
68
77
 
69
- const keys = Object.keys(groups).sort();
70
78
  keys.forEach((k) => {
71
- const ws = wb.addWorksheet(String(k).slice(0, 31)); // Excel sheet name limit 31
72
- ws.columns = cols;
73
- groups[k].forEach(r => ws.addRow(r));
74
- ws.autoFilter = { from: { row: 1, column: 1 }, to: { row: 1, column: cols.length } };
75
- });
76
-
77
- // summary sheet with counts per period
78
- const summary = wb.addWorksheet('Summary');
79
+ const ws = wb.addWorksheet(String(k).slice(0, 31))
80
+ ws.columns = cols
81
+ groups[k].forEach((r) => ws.addRow(r))
82
+
83
+ ws.autoFilter = {
84
+ from: { row: 1, column: 1 },
85
+ to: { row: 1, column: cols.length }
86
+ }
87
+ })
88
+
89
+ // summary sheet
90
+ const summary = wb.addWorksheet('Summary')
79
91
  summary.columns = [
80
92
  { header: 'Period', key: 'period', width: 20 },
81
93
  { header: 'Commits', key: 'count', width: 12 }
82
- ];
83
- keys.forEach(k => summary.addRow({ period: k, count: groups[k].length }));
94
+ ]
95
+ keys.forEach((k) => summary.addRow({ period: k, count: groups[k].length }))
84
96
 
85
- await wb.xlsx.writeFile(file);
97
+ await wb.xlsx.writeFile(file)
86
98
  }
87
99
 
88
- export async function exportExcelPerPeriodFiles(groups, dir, filePrefix, options = {}) {
89
- // groups: { periodKey: [records] }
90
- // dir: output directory path, filePrefix: base name without extension
91
- // For each key, call exportExcel with that key's records
92
- const keys = Object.keys(groups).sort();
100
+ export async function exportExcelPerPeriodFiles(
101
+ groups,
102
+ dir,
103
+ filePrefix,
104
+ options = {}
105
+ ) {
106
+ const keys = Object.keys(groups).sort()
93
107
  for (const k of keys) {
94
- const perFile = `${dir}/overtime_${filePrefix}_${k}.xlsx`;
95
- await exportExcel(groups[k], null, { file: perFile, stats: options.stats, gerrit: options.gerrit });
108
+ const perFile = `${dir}/overtime_${filePrefix}_${k}.xlsx`
109
+ await exportExcel(groups[k], null, {
110
+ file: perFile,
111
+ stats: options.stats,
112
+ gerrit: options.gerrit
113
+ })
96
114
  }
97
115
  }
116
+
117
+ /**
118
+ * 导出作者的日/周/月 changed 统计
119
+ * @param {*} records 原始 commits(包含 author/date/changed)
120
+ * @param {*} file 输出文件
121
+ */
122
+ export async function exportExcelAuthorChangeStats(records, file) {
123
+ const stats = buildAuthorChangeStats(records)
124
+
125
+ const wb = new ExcelJS.Workbook()
126
+
127
+ //-------------------------------------------
128
+ // 工具方法:创建一个统计 sheet
129
+ //-------------------------------------------
130
+ function addStatSheet(title, dataMap) {
131
+ const ws = wb.addWorksheet(title)
132
+
133
+ ws.columns = [
134
+ { header: 'Author', key: 'author', width: 20 },
135
+ { header: 'Period', key: 'period', width: 20 },
136
+ { header: 'Changed', key: 'changed', width: 12 }
137
+ ]
138
+
139
+ for (const [author, periodMap] of Object.entries(dataMap)) {
140
+ const periods = Object.keys(periodMap).sort()
141
+ for (const p of periods) {
142
+ ws.addRow({
143
+ author,
144
+ period: p,
145
+ changed: periodMap[p]
146
+ })
147
+ }
148
+ }
149
+
150
+ ws.autoFilter = {
151
+ from: { row: 1, column: 1 },
152
+ to: { row: 1, column: 3 }
153
+ }
154
+ }
155
+
156
+ //-------------------------------------------
157
+ // 创建 3 个 Sheet
158
+ //-------------------------------------------
159
+ addStatSheet('Author Daily', stats.daily)
160
+ addStatSheet('Author Weekly', stats.weekly)
161
+ addStatSheet('Author Monthly', stats.monthly)
162
+
163
+ //-------------------------------------------
164
+ // 保存文件
165
+ //-------------------------------------------
166
+ await wb.xlsx.writeFile(file)
167
+ }
package/src/git.mjs CHANGED
@@ -1,12 +1,9 @@
1
1
  import { $ } from 'zx'
2
2
 
3
-
4
3
  export async function getGitLogs(opts) {
5
4
  const { author, email, since, until, limit, merges } = opts
6
5
 
7
- // include subject and full body so we can extract Change-Id from commit message
8
6
  const pretty = '%H%x1f%an%x1f%ae%x1f%ad%x1f%s%x1f%B%x1e'
9
-
10
7
  const args = ['log', `--pretty=format:${pretty}`, '--date=iso']
11
8
 
12
9
  if (author) args.push(`--author=${author}`)
@@ -16,15 +13,13 @@ export async function getGitLogs(opts) {
16
13
  if (merges === false) args.push(`--no-merges`)
17
14
  if (limit) args.push(`-n`, `${limit}`)
18
15
 
19
- // 使用 spread 形式传参,ZX 才会正确处理
20
16
  const { stdout } = await $`git ${args}`.quiet()
21
17
 
22
- return stdout
18
+ const commits = stdout
23
19
  .split('\x1e')
24
20
  .filter(Boolean)
25
21
  .map((r) => {
26
22
  const f = r.split('\x1f').map((s) => (s || '').trim())
27
-
28
23
  const hash = f[0]
29
24
  const authorName = f[1]
30
25
  const emailAddr = f[2]
@@ -32,7 +27,6 @@ export async function getGitLogs(opts) {
32
27
  const subject = f[4]
33
28
  const body = f[5] || ''
34
29
 
35
- // extract Change-Id from commit body (line like "Change-Id: Iabc123...")
36
30
  const [, changeId] = body.match(/Change-Id:\s*(I[0-9a-fA-F]+)/) || []
37
31
 
38
32
  return {
@@ -45,4 +39,36 @@ export async function getGitLogs(opts) {
45
39
  changeId
46
40
  }
47
41
  })
42
+
43
+ // === 新增:为每个 commit 计算代码增量 ===
44
+ for (const c of commits) {
45
+ try {
46
+ const { stdout: diffOut } = await $`git show --numstat --format= ${c.hash}`.quiet()
47
+ // numstat 格式: "12 5 path/file.js"
48
+ const lines = diffOut
49
+ .split('\n')
50
+ .map((l) => l.trim())
51
+ .filter((l) => l && /^\d+\s+\d+/.test(l))
52
+
53
+ let added = 0
54
+ let deleted = 0
55
+
56
+ for (const line of lines) {
57
+ const [a, d] = line.split(/\s+/)
58
+ added += parseInt(a, 10) || 0
59
+ deleted += parseInt(d, 10) || 0
60
+ }
61
+
62
+ c.added = added
63
+ c.deleted = deleted
64
+ c.changed = added + deleted
65
+ } catch (err) {
66
+ // 避免阻塞,异常时改动量设置为0
67
+ c.added = 0
68
+ c.deleted = 0
69
+ c.changed = 0
70
+ }
71
+ }
72
+
73
+ return commits
48
74
  }
package/src/json.mjs ADDED
@@ -0,0 +1,5 @@
1
+ import { buildAuthorChangeStats } from './stats.mjs'
2
+
3
+ export function renderAuthorChangesJson(records) {
4
+ return buildAuthorChangeStats(records)
5
+ }
@@ -0,0 +1,51 @@
1
+ export function renderAuthorChangeStatsText(stats, opts = {}) {
2
+ const {
3
+ section = 'all', // 'daily' | 'weekly' | 'monthly' | 'all'
4
+ } = opts;
5
+
6
+ const pad = (s, n) =>
7
+ s.length >= n ? `${s.slice(0, n - 1)}…` : s + ' '.repeat(n - s.length);
8
+
9
+ const buildSection = (title, obj, unitLabel) => {
10
+ if (!obj || !Object.keys(obj).length) return '';
11
+
12
+ const lines = [];
13
+
14
+ const header =
15
+ `${pad('Author', 18)} | ${pad(unitLabel, 12)} | ${pad('Changed', 10)}`;
16
+ const line = '-'.repeat(header.length);
17
+
18
+ lines.push(`\n=== ${title} ===`);
19
+ lines.push(header);
20
+ lines.push(line);
21
+
22
+ for (const [author, periodMap] of Object.entries(obj)) {
23
+ const keys = Object.keys(periodMap).sort();
24
+ for (const key of keys) {
25
+ lines.push(
26
+ [
27
+ pad(author, 18),
28
+ pad(key, 12),
29
+ pad(String(periodMap[key]), 10),
30
+ ].join(' | ')
31
+ );
32
+ }
33
+ }
34
+
35
+ return lines.join('\n');
36
+ };
37
+
38
+ let output = '';
39
+
40
+ if (section === 'all' || section === 'daily') {
41
+ output += buildSection('Daily Changed', stats.daily, 'Date');
42
+ }
43
+ if (section === 'all' || section === 'weekly') {
44
+ output += buildSection('Weekly Changed', stats.weekly, 'Week');
45
+ }
46
+ if (section === 'all' || section === 'monthly') {
47
+ output += buildSection('Monthly Changed', stats.monthly, 'Month');
48
+ }
49
+
50
+ return `${output.trim() }\n`;
51
+ }
package/src/stats.mjs ADDED
@@ -0,0 +1,44 @@
1
+ import dayjs from 'dayjs'
2
+ import isoWeek from 'dayjs/plugin/isoWeek.js'
3
+
4
+ dayjs.extend(isoWeek)
5
+
6
+ /**
7
+ * 自动统计每个作者的日/周/月 changed
8
+ *
9
+ * records: [{
10
+ * author: 'Tom',
11
+ * date: '2025-01-01T12:00:00Z',
12
+ * changed: 123
13
+ * }]
14
+ */
15
+ export function buildAuthorChangeStats(records) {
16
+ const result = {
17
+ daily: {}, // { author: { 'YYYY-MM-DD': x } }
18
+ weekly: {}, // { author: { 'YYYY-WW': x } }
19
+ monthly: {} // { author: { 'YYYY-MM': x } }
20
+ }
21
+
22
+ for (const r of records) {
23
+ const author = r.author || 'Unknown'
24
+ const changed = Number(r.changed || 0)
25
+
26
+ const d = dayjs(r.date)
27
+
28
+ const dayKey = d.format('YYYY-MM-DD')
29
+ const weekKey = `${d.format('GGGG')}-W${d.isoWeek().toString().padStart(2, '0')}`
30
+ const monthKey = d.format('YYYY-MM')
31
+
32
+ if (!result.daily[author]) result.daily[author] = {}
33
+ if (!result.weekly[author]) result.weekly[author] = {}
34
+ if (!result.monthly[author]) result.monthly[author] = {}
35
+
36
+ result.daily[author][dayKey] = (result.daily[author][dayKey] || 0) + changed
37
+ result.weekly[author][weekKey] =
38
+ (result.weekly[author][weekKey] || 0) + changed
39
+ result.monthly[author][monthKey] =
40
+ (result.monthly[author][monthKey] || 0) + changed
41
+ }
42
+
43
+ return result
44
+ }
package/src/text.mjs CHANGED
@@ -1,56 +1,51 @@
1
+ import { renderAuthorChangeStatsText } from './stats-text.mjs'
2
+ import { buildAuthorChangeStats } from './stats.mjs'
3
+
1
4
  export function renderText(records, groups = null, opts = {}) {
2
- const { showGerrit = false } = opts;
5
+ const { showGerrit = false } = opts
3
6
  const pad = (s, n) =>
4
- s.length >= n ? `${s.slice(0, n - 1) }…` : s + ' '.repeat(n - s.length);
7
+ s.length >= n ? `${s.slice(0, n - 1)}…` : s + ' '.repeat(n - s.length)
8
+
9
+ const baseHeader = `${pad('Hash', 10)} | ${pad('Author', 18)} | ${pad(
10
+ 'Date',
11
+ 20
12
+ )} | ${pad('Message', 40)} | ${pad('Changed', 8)}` // ★ 新增 changed 列
5
13
 
6
- const baseHeader =
7
- `${pad('Hash', 10)
8
- } | ${
9
- pad('Author', 18)
10
- } | ${
11
- pad('Date', 20)
12
- } | ${
13
- pad('Message', 60)}`;
14
+ const gerritHeader = showGerrit ? ` | ${pad('Gerrit', 50)}` : ''
15
+ const header = baseHeader + gerritHeader
14
16
 
15
- const gerritHeader = showGerrit ? ` | ${ pad('Gerrit', 50)}` : '';
16
- const header = baseHeader + gerritHeader;
17
+ const line = '-'.repeat(header.length)
17
18
 
18
- const line = '-'.repeat(header.length);
19
+ const rows = []
19
20
 
20
- const rows = [];
21
+ const buildRow = (r) => {
22
+ return (
23
+ [
24
+ pad(r.hash.slice(0, 8), 10),
25
+ pad(r.author, 18),
26
+ pad(r.date.replace(/ .+/, ''), 20),
27
+ pad(r.message, 40),
28
+ pad(String(r.changed ?? 0), 8) // ★ 输出 changed 数量
29
+ ].join(' | ') + (showGerrit ? ` | ${pad(r.gerrit || '', 50)}` : '')
30
+ )
31
+ }
21
32
 
22
33
  if (groups) {
23
34
  for (const [g, list] of Object.entries(groups)) {
24
- rows.push(`\n=== ${g} ===\n`);
25
- list.forEach(r => {
26
- rows.push(
27
- (
28
- [
29
- pad(r.hash.slice(0, 8), 10),
30
- pad(r.author, 18),
31
- pad(r.date.replace(/ .+/, ''), 20),
32
- pad(r.message, 60)
33
- ].join(' | ') +
34
- (showGerrit ? ` | ${ pad(r.gerrit || '', 50)}` : '')
35
- )
36
- );
37
- });
35
+ rows.push(`\n=== ${g} ===\n`)
36
+ list.forEach((r) => rows.push(buildRow(r)))
38
37
  }
39
38
  } else {
40
- records.forEach(r => {
41
- rows.push(
42
- (
43
- [
44
- pad(r.hash.slice(0, 8), 10),
45
- pad(r.author, 18),
46
- pad(r.date.replace(/ .+/, ''), 20),
47
- pad(r.message, 60)
48
- ].join(' | ') +
49
- (showGerrit ? ` | ${ pad(r.gerrit || '', 50)}` : '')
50
- )
51
- );
52
- });
39
+ records.forEach((r) => rows.push(buildRow(r)))
53
40
  }
54
41
 
55
- return [header, line, ...rows].join('\n');
42
+ return [header, line, ...rows].join('\n')
43
+ }
44
+
45
+ export function renderChangedLinesText(records, opts = {}) {
46
+ const stats = buildAuthorChangeStats(records)
47
+
48
+ const result = renderAuthorChangeStatsText(stats, opts)
49
+
50
+ return result
56
51
  }
package/web/app.js CHANGED
@@ -9,14 +9,16 @@ async function loadData() {
9
9
  weeklyModule,
10
10
  monthlyModule,
11
11
  latestByDayModule,
12
- configModule
12
+ configModule,
13
+ authorChangesModule
13
14
  ] = await Promise.all([
14
15
  import('/data/commits.mjs'),
15
16
  import('/data/overtime-stats.mjs'),
16
17
  import('/data/overtime-weekly.mjs'),
17
18
  import('/data/overtime-monthly.mjs').catch(() => ({ default: [] })),
18
19
  import('/data/overtime-latest-by-day.mjs').catch(() => ({ default: [] })),
19
- import('/data/config.mjs').catch(() => ({ default: {} }))
20
+ import('/data/config.mjs').catch(() => ({ default: {} })),
21
+ import('/data/author-changes.mjs').catch(() => ({ default: {} }))
20
22
  ])
21
23
  const commits = commitsModule.default || []
22
24
  const stats = statsModule.default || {}
@@ -24,7 +26,8 @@ async function loadData() {
24
26
  const monthly = monthlyModule.default || []
25
27
  const latestByDay = latestByDayModule.default || []
26
28
  const config = configModule.default || {}
27
- return { commits, stats, weekly, monthly, latestByDay, config }
29
+ const authorChanges = authorChangesModule.default || {}
30
+ return { commits, stats, weekly, monthly, latestByDay, config, authorChanges }
28
31
  } catch (err) {
29
32
  console.error('Load data failed', err)
30
33
  return { commits: [], stats: {}, weekly: [], monthly: [], latestByDay: [] }
@@ -184,14 +187,24 @@ function drawHourlyOvertime(stats, onHourClick) {
184
187
  data: [
185
188
  {
186
189
  name: '上班开始',
190
+ nameValue: String(stats.startHour).padStart(2, '0'),
187
191
  xAxis: String(stats.startHour).padStart(2, '0')
188
192
  },
189
- { name: '下班时间', xAxis: String(stats.endHour).padStart(2, '0') },
193
+ {
194
+ name: '下班时间',
195
+ nameValue: String(stats.endHour).padStart(2, '0'),
196
+ xAxis: String(stats.endHour).padStart(2, '0')
197
+ },
190
198
  {
191
199
  name: '午休开始',
200
+ nameValue: String(stats.lunchStart).padStart(2, '0'),
192
201
  xAxis: String(stats.lunchStart).padStart(2, '0')
193
202
  },
194
- { name: '午休结束', xAxis: String(stats.lunchEnd).padStart(2, '0') }
203
+ {
204
+ name: '午休结束',
205
+ nameValue: String(stats.lunchEnd).padStart(2, '0'),
206
+ xAxis: String(stats.lunchEnd).padStart(2, '0')
207
+ }
195
208
  ]
196
209
  }
197
210
  }
@@ -201,7 +214,14 @@ function drawHourlyOvertime(stats, onHourClick) {
201
214
  // 点击事件(点击某小时 → 打开侧栏)
202
215
  if (typeof onHourClick === 'function') {
203
216
  chart.on('click', (p) => {
204
- const hour = Number(p.name)
217
+ let hour = Number(p.name)
218
+ if(p.componentType === 'markLine') {
219
+ hour = Number(p.data.xAxis)
220
+ }
221
+ // FIXME: remove debug log before production
222
+ console.log('❌', 'hour', hour, p)
223
+ document.getElementById('dayDetailSidebar').classList.remove('show')
224
+ if (Object.is(hour, NaN)) return
205
225
  onHourClick(hour, commits[hour])
206
226
  })
207
227
  }
@@ -214,6 +234,7 @@ function showSideBarForHour(hour, commitsOrCount) {
214
234
  // 支持传入 number(仅次数)或 array(详细 commit 列表)
215
235
  // 统一复用通用详情侧栏 DOM
216
236
  const sidebar = document.getElementById('dayDetailSidebar')
237
+ const backdrop = document.getElementById('sidebarBackdrop')
217
238
  const titleEl = document.getElementById('sidebarTitle')
218
239
  const contentEl = document.getElementById('sidebarContent')
219
240
 
@@ -235,19 +256,22 @@ function showSideBarForHour(hour, commitsOrCount) {
235
256
  } else if (Array.isArray(commitsOrCount)) {
236
257
  // commits 列表:展示作者/时间/消息(最多前 50 条,避免性能问题)
237
258
  const commits = commitsOrCount.slice(0, 50)
238
- contentEl.innerHTML = commits
259
+ contentEl.innerHTML = `<div class="sidebar-list">${commits
239
260
  .map((c) => {
240
261
  const author = c.author ?? c.name ?? 'unknown'
241
262
  const time = c.date ?? c.time ?? ''
242
263
  const msg = (c.message ?? c.msg ?? c.body ?? '').replace(/\n/g, ' ')
243
264
  return `
244
- <div class="hour-commit">
245
- <div class="meta">👤 <b>${escapeHtml(author)}</b> · 🕒 ${escapeHtml(time)}</div>
246
- <div class="msg">${escapeHtml(msg)}</div>
247
- </div>
248
- `
265
+ <div class="sidebar-item">
266
+ <div class="sidebar-item-header">
267
+ <span class="author">👤 ${escapeHtml(author)}</span>
268
+ <span class="time">🕒 ${escapeHtml(time)}</span>
269
+ </div>
270
+ <div class="sidebar-item-message">${escapeHtml(msg)}</div>
271
+ </div>
272
+ `
249
273
  })
250
- .join('')
274
+ .join('')}</div>`
251
275
 
252
276
  if (commitsOrCount.length > 50) {
253
277
  const more = commitsOrCount.length - 50
@@ -257,8 +281,9 @@ function showSideBarForHour(hour, commitsOrCount) {
257
281
  contentEl.innerHTML = `<div style="font-size:14px;">无可展示数据</div>`
258
282
  }
259
283
 
260
- // 打开侧栏
284
+ // 打开侧栏 + 遮罩
261
285
  sidebar.classList.add('show')
286
+ if (backdrop) backdrop.classList.add('show')
262
287
  }
263
288
 
264
289
  // 简单的 HTML 转义,防止 XSS 与布局断裂
@@ -410,6 +435,7 @@ function drawDailyTrend(commits, onDayClick) {
410
435
  function showSideBarForWeek(period, weeklyItem, commits = []) {
411
436
  // 统一复用通用详情侧栏 DOM
412
437
  const sidebar = document.getElementById('dayDetailSidebar')
438
+ const backdrop = document.getElementById('sidebarBackdrop')
413
439
  const titleEl = document.getElementById('sidebarTitle')
414
440
  const contentEl = document.getElementById('sidebarContent')
415
441
 
@@ -426,22 +452,27 @@ function showSideBarForWeek(period, weeklyItem, commits = []) {
426
452
  if (!commits.length) {
427
453
  html += `<div style="padding:10px;color:#777;">该周无提交记录</div>`
428
454
  } else {
429
- html += commits
455
+ html += `<div class="sidebar-list">${commits
430
456
  .map((c) => {
457
+ const author = escapeHtml(c.author || 'unknown')
458
+ const time = escapeHtml(c.date || '')
459
+ const msg = escapeHtml((c.message || '').replace(/\n/g, ' '))
431
460
  return `
432
- <div class="week-commit">
433
- <div class="meta">👤 <b>${escapeHtml(c.author || 'unknown')}</b> · 🕒 ${
434
- c.date
435
- }</div>
436
- <div class="msg">${escapeHtml((c.message || '').replace(/\n/g, ' '))}</div>
461
+ <div class="sidebar-item">
462
+ <div class="sidebar-item-header">
463
+ <span class="author">👤 ${author}</span>
464
+ <span class="time">🕒 ${time}</span>
465
+ </div>
466
+ <div class="sidebar-item-message">${msg}</div>
437
467
  </div>
438
468
  `
439
469
  })
440
- .join('')
470
+ .join('')}</div>`
441
471
  }
442
472
 
443
473
  contentEl.innerHTML = html
444
474
  sidebar.classList.add('show')
475
+ if (backdrop) backdrop.classList.add('show')
445
476
  }
446
477
 
447
478
  function drawWeeklyTrend(weekly, commits, onWeekClick) {
@@ -543,7 +574,6 @@ function drawWeeklyTrend(weekly, commits, onWeekClick) {
543
574
  const idx = p.dataIndex
544
575
  const w = weekly[idx]
545
576
 
546
-
547
577
  const start = new Date(w.range.start)
548
578
  const end = new Date(w.range.end)
549
579
  end.setHours(23, 59, 59, 999) // 包含当天
@@ -828,9 +858,7 @@ function drawDailySeverity(latestByDay, commits, onDayClick) {
828
858
 
829
859
  // 若某天 latestHourNormalized 为空,表示「没有下班后到次日上班前的提交」,
830
860
  // 这里按 0 小时加班处理,保证折线连续。
831
- const sev = raw.map((v) =>
832
- v == null ? 0 : Math.max(0, Number(v) - endH)
833
- )
861
+ const sev = raw.map((v) => (v == null ? 0 : Math.max(0, Number(v) - endH)))
834
862
 
835
863
  const el = document.getElementById('dailySeverityChart')
836
864
  // eslint-disable-next-line no-undef
@@ -1101,6 +1129,7 @@ function drawDailyTrendSeverity(commits, weekly, onDayClick) {
1101
1129
 
1102
1130
  function showDayDetailSidebar(date, count, commits) {
1103
1131
  const sidebar = document.getElementById('dayDetailSidebar')
1132
+ const backdrop = document.getElementById('sidebarBackdrop')
1104
1133
  const title = document.getElementById('sidebarTitle')
1105
1134
  const content = document.getElementById('sidebarContent')
1106
1135
 
@@ -1110,17 +1139,19 @@ function showDayDetailSidebar(date, count, commits) {
1110
1139
  content.innerHTML = commits
1111
1140
  .map(
1112
1141
  (c) => `
1113
- <div style="margin-bottom:12px;">
1114
- <div>👤 <b>${c.author}</b></div>
1115
- <div>🕒 ${c.time || c.date}</div>
1116
- <div>💬 ${c.msg ||c.message}</div>
1142
+ <div class="sidebar-item">
1143
+ <div class="sidebar-item-header">
1144
+ <span class="author">👤 ${escapeHtml(c.author || 'unknown')}</span>
1145
+ <span class="time">🕒 ${escapeHtml(c.time || c.date || '')}</span>
1146
+ </div>
1147
+ <div class="sidebar-item-message">${escapeHtml(c.msg || c.message || '')}</div>
1117
1148
  </div>
1118
- <hr/>
1119
1149
  `
1120
1150
  )
1121
1151
  .join('')
1122
1152
 
1123
1153
  sidebar.classList.add('show')
1154
+ if (backdrop) backdrop.classList.add('show')
1124
1155
  }
1125
1156
 
1126
1157
  function renderKpi(stats) {
@@ -1132,13 +1163,13 @@ function renderKpi(stats) {
1132
1163
  // 使用 cutoff + 上下班时间,重新在全部 commits 中计算「加班最晚一次提交」
1133
1164
  const cutoff = window.__overnightCutoff ?? 6
1134
1165
  const startHour =
1135
- (typeof stats.startHour === 'number' && stats.startHour >= 0
1166
+ typeof stats.startHour === 'number' && stats.startHour >= 0
1136
1167
  ? stats.startHour
1137
- : 9)
1168
+ : 9
1138
1169
  const endHour =
1139
- (typeof stats.endHour === 'number' && stats.endHour >= 0
1170
+ typeof stats.endHour === 'number' && stats.endHour >= 0
1140
1171
  ? stats.endHour
1141
- : window.__overtimeEndHour ?? 18)
1172
+ : (window.__overtimeEndHour ?? 18)
1142
1173
 
1143
1174
  let latestOut = null
1144
1175
  let latestOutHour = null
@@ -1198,50 +1229,6 @@ function groupCommitsByHour(commits) {
1198
1229
  return byHour
1199
1230
  }
1200
1231
 
1201
- async function main() {
1202
- const { commits, stats, weekly, monthly, latestByDay, config } =
1203
- await loadData()
1204
- commitsAll = commits
1205
- filtered = commitsAll.slice()
1206
- window.__overtimeEndHour =
1207
- stats && typeof stats.endHour === 'number'
1208
- ? stats.endHour
1209
- : (config.endHour ?? 18)
1210
- window.__overnightCutoff =
1211
- typeof config.overnightCutoff === 'number' ? config.overnightCutoff : 6
1212
- initTableControls()
1213
- updatePager()
1214
- renderCommitsTablePage()
1215
-
1216
- drawHourlyOvertime(stats, (hour, count) => {
1217
- // 使用举例
1218
- const hourCommitsDetail = groupCommitsByHour(commits)
1219
- // 将 commit 列表传给侧栏(若没有详情,则传空数组)
1220
- showSideBarForHour(hour, hourCommitsDetail[hour] || [])
1221
- })
1222
- drawOutsideVsInside(stats)
1223
-
1224
- // 按日提交趋势:点击某天打开抽屉,显示当日所有 commits
1225
- drawDailyTrend(commits, showDayDetailSidebar)
1226
-
1227
- // 周趋势:保持原有点击行为(显示该周详情)
1228
- drawWeeklyTrend(weekly, commits, showSideBarForWeek)
1229
-
1230
- // 月趋势(加班占比):点击某个月打开抽屉,显示该月所有 commits
1231
- drawMonthlyTrend(monthly, commits, showDayDetailSidebar)
1232
-
1233
- // 每日最晚提交时间(小时):点击某天打开抽屉,显示当日所有 commits
1234
- drawLatestHourDaily(latestByDay, commits, showDayDetailSidebar)
1235
-
1236
- // 每日超过下班的小时数:点击某天打开抽屉,显示当日所有 commits
1237
- drawDailySeverity(latestByDay, commits, showDayDetailSidebar)
1238
-
1239
- const daily = drawDailyTrendSeverity(commits, weekly, showDayDetailSidebar)
1240
-
1241
- console.log('最累的一天:', daily.analysis.mostTiredDay)
1242
- computeAndRenderLatestOvertime(latestByDay)
1243
- renderKpi(stats)
1244
- }
1245
1232
 
1246
1233
  // 基于 latestByDay + cutoff/endHour 统计「最晚加班的一天 / 一周 / 一月」
1247
1234
  function computeAndRenderLatestOvertime(latestByDay) {
@@ -1349,8 +1336,119 @@ function computeAndRenderLatestOvertime(latestByDay) {
1349
1336
  }
1350
1337
  }
1351
1338
 
1352
- // 关闭按钮
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, type = 'daily') => {
1358
+ const el = document.getElementById("chartAuthorChanges");
1359
+ if (!el) return null;
1360
+ // FIXME: remove debug log before production
1361
+ console.log('❌', 'el', el);
1362
+ const chart = echarts.init(el);
1363
+
1364
+ function render(type) {
1365
+ const { authors, allPeriods, series } = buildDataset(stats, type);
1366
+
1367
+ chart.setOption({
1368
+ tooltip: { trigger: 'axis' },
1369
+ legend: { data: authors },
1370
+ xAxis: { type: 'category', data: allPeriods },
1371
+ yAxis: { type: 'value' },
1372
+ series
1373
+ });
1374
+ }
1375
+
1376
+ // 初次渲染:日
1377
+ render('daily');
1378
+
1379
+ // tabs 切换
1380
+ document.querySelectorAll('#tabs button').forEach(btn => {
1381
+ btn.onclick = () => {
1382
+ document.querySelectorAll('#tabs button').forEach(b => b.classList.remove('active'));
1383
+ btn.classList.add('active');
1384
+
1385
+ render(btn.dataset.type);
1386
+ };
1387
+ });
1388
+
1389
+ return chart;
1390
+ }
1391
+
1392
+ async function main() {
1393
+ const { commits, stats, weekly, monthly, latestByDay, config,authorChanges } =
1394
+ await loadData()
1395
+ commitsAll = commits
1396
+ filtered = commitsAll.slice()
1397
+ window.__overtimeEndHour =
1398
+ stats && typeof stats.endHour === 'number'
1399
+ ? stats.endHour
1400
+ : (config.endHour ?? 18)
1401
+ window.__overnightCutoff =
1402
+ typeof config.overnightCutoff === 'number' ? config.overnightCutoff : 6
1403
+ initTableControls()
1404
+ updatePager()
1405
+ renderCommitsTablePage()
1406
+
1407
+ drawHourlyOvertime(stats, (hour, count) => {
1408
+ // 使用举例
1409
+ const hourCommitsDetail = groupCommitsByHour(commits)
1410
+ // 将 commit 列表传给侧栏(若没有详情,则传空数组)
1411
+ showSideBarForHour(hour, hourCommitsDetail[hour] || [])
1412
+ })
1413
+ drawOutsideVsInside(stats)
1414
+
1415
+ // 按日提交趋势:点击某天打开抽屉,显示当日所有 commits
1416
+ drawDailyTrend(commits, showDayDetailSidebar)
1417
+
1418
+ // 周趋势:保持原有点击行为(显示该周详情)
1419
+ drawWeeklyTrend(weekly, commits, showSideBarForWeek)
1420
+
1421
+ // 月趋势(加班占比):点击某个月打开抽屉,显示该月所有 commits
1422
+ drawMonthlyTrend(monthly, commits, showDayDetailSidebar)
1423
+
1424
+ // 每日最晚提交时间(小时):点击某天打开抽屉,显示当日所有 commits
1425
+ drawLatestHourDaily(latestByDay, commits, showDayDetailSidebar)
1426
+
1427
+ // 每日超过下班的小时数:点击某天打开抽屉,显示当日所有 commits
1428
+ drawDailySeverity(latestByDay, commits, showDayDetailSidebar)
1429
+
1430
+ const daily = drawDailyTrendSeverity(commits, weekly, showDayDetailSidebar)
1431
+
1432
+ console.log('最累的一天:', daily.analysis.mostTiredDay)
1433
+
1434
+ drawChangeTrends(authorChanges, 'daily');
1435
+ computeAndRenderLatestOvertime(latestByDay)
1436
+ renderKpi(stats)
1437
+ }
1438
+
1439
+
1440
+ // 抽屉关闭交互(按钮 + 点击遮罩)
1353
1441
  document.getElementById('sidebarClose').onclick = () => {
1354
1442
  document.getElementById('dayDetailSidebar').classList.remove('show')
1443
+ const backdrop = document.getElementById('sidebarBackdrop')
1444
+ if (backdrop) backdrop.classList.remove('show')
1445
+ }
1446
+
1447
+ const sidebarBackdropEl = document.getElementById('sidebarBackdrop')
1448
+ if (sidebarBackdropEl) {
1449
+ sidebarBackdropEl.addEventListener('click', () => {
1450
+ document.getElementById('dayDetailSidebar').classList.remove('show')
1451
+ sidebarBackdropEl.classList.remove('show')
1452
+ })
1355
1453
  }
1356
1454
  main()
package/web/index.html CHANGED
@@ -61,6 +61,17 @@
61
61
  <div id="latestOvertimeMonth"></div>
62
62
  </section>
63
63
 
64
+ <div class="chart-card">
65
+ <h2>开发者 Changed 工作量趋势</h2>
66
+
67
+ <div id="tabs">
68
+ <button data-type="daily" class="active">按日</button>
69
+ <button data-type="weekly">按周</button>
70
+ <button data-type="monthly">按月</button>
71
+ </div>
72
+ <div id="chartAuthorChanges" class="echart"></div>
73
+ </div>
74
+
64
75
  <section class="table-card">
65
76
  <h2>提交清单</h2>
66
77
  <div id="tableControls">
@@ -102,6 +113,8 @@
102
113
  >
103
114
  </footer>
104
115
  <!-- 通用:右侧滑出的详情侧栏(小时 / 天 / 周 复用同一个 DOM) -->
116
+ <!-- 背景遮罩,点击可关闭侧栏 -->
117
+ <div id="sidebarBackdrop" class="sidebar-backdrop"></div>
105
118
  <div id="dayDetailSidebar" class="sidebar">
106
119
  <div class="sidebar-header">
107
120
  <span id="sidebarTitle"></span>
@@ -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
  }