wukong-gitlog-cli 1.0.8 → 1.0.11

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.11](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.10...v1.0.11) (2025-12-01)
6
+
7
+ ### [1.0.10](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.9...v1.0.10) (2025-12-01)
8
+
9
+ ### [1.0.9](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.8...v1.0.9) (2025-12-01)
10
+
11
+
12
+ ### Features
13
+
14
+ * 🎸 高亮21点 ([1dbd714](https://github.com/tomatobybike/wukong-gitlog-cli/commit/1dbd714d3690e5cfcc232ab96206c553ad3b39b0))
15
+ * 🎸 each day overtime ([f89b935](https://github.com/tomatobybike/wukong-gitlog-cli/commit/f89b9356bc9088370ca7b1741d1fc17efa61c364))
16
+ * 🎸 everyday latest work ([8332341](https://github.com/tomatobybike/wukong-gitlog-cli/commit/83323414618d1e5d89203846c9feed93b32dd20a))
17
+ * 🎸 kpi ([6120bc1](https://github.com/tomatobybike/wukong-gitlog-cli/commit/6120bc1d0d999b5bd012109378a107d47722bbce))
18
+
5
19
  ### [1.0.8](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.7...v1.0.8) (2025-11-30)
6
20
 
7
21
 
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- ## 📦 `wukong-gitlog-cli`
1
+ ## 📦 `wukong-gitlog-cli`
2
2
 
3
3
  <p align="center">
4
4
  <img src="https://raw.githubusercontent.com/tomatobybike/wukong-gitlog-cli/main/images/logo.svg" width="200" alt="wukong-dev Logo" />
@@ -14,8 +14,7 @@
14
14
 
15
15
  Advanced Git commit log exporter with Excel/JSON/TXT output, grouping, stats and CLI.
16
16
 
17
- English | [简体中文](./README.zh-CN.md)
18
- ---
17
+ ## English | [简体中文](./README.zh-CN.md)
19
18
 
20
19
  ## Features
21
20
 
@@ -38,8 +37,6 @@ English | [简体中文](./README.zh-CN.md)
38
37
 
39
38
  ## Installation
40
39
 
41
-
42
-
43
40
  Install globally to run with a short command (recommended for CLI consumers):
44
41
 
45
42
  ```bash
@@ -62,37 +59,38 @@ wukong-gitlog-cli [options]
62
59
 
63
60
  Command-line options:
64
61
 
65
- - `--author <name>` Filter commits by author name
66
- - `--email <email>` Filter commits by author email
67
- - `--since <date>` Start date (e.g., 2025-01-01)
68
- - `--until <date>` End date
69
- - `--limit <n>` Limit number of commits
70
- - `--no-merges` Exclude merge commits
71
- - `--json` Output JSON
72
- - `--format <type>` Output format: `text` | `excel` | `json` (default: `text`)
73
- - `--group-by <type>` Group commits by date: `day` | `month`
74
- - `--overtime` Analyze overtime culture: output counts/percentages for commits outside work hours and on non-workdays (per-person breakdown)
75
- - `--country <code>` Country/region for holidays (CN|US). Default: `CN`.
76
- - `--work-start <hour>` Workday start hour. Default: `9`.
77
- - `--work-end <hour>` Workday end hour. Default: `18`.
78
- - `--lunch-start <hour>` Lunch break start hour. Default: `12`.
79
- - `--lunch-end <hour>` Lunch break end hour. Default: `14`.
80
- - `--stats` Include a `Stats` sheet in the Excel export
81
- - `--gerrit-api <url>` Optional: Gerrit REST API base URL for resolving `{{changeNumber}}` (e.g. `https://gerrit.example.com/gerrit`)
62
+ - `--author <name>` Filter commits by author name
63
+ - `--email <email>` Filter commits by author email
64
+ - `--since <date>` Start date (e.g., 2025-01-01)
65
+ - `--until <date>` End date
66
+ - `--limit <n>` Limit number of commits
67
+ - `--no-merges` Exclude merge commits
68
+ - `--json` Output JSON
69
+ - `--format <type>` Output format: `text` | `excel` | `json` (default: `text`)
70
+ - `--group-by <type>` Group commits by date: `day` | `month`
71
+ - `--overtime` Analyze overtime culture: output counts/percentages for commits outside work hours and on non-workdays (per-person breakdown)
72
+ - `--country <code>` Country/region for holidays (CN|US). Default: `CN`.
73
+ - `--work-start <hour>` Workday start hour. Default: `9`.
74
+ - `--work-end <hour>` Workday end hour. Default: `18`.
75
+ - `--lunch-start <hour>` Lunch break start hour. Default: `12`.
76
+ - `--lunch-end <hour>` Lunch break end hour. Default: `14`.
77
+ - `--stats` Include a `Stats` sheet in the Excel export
78
+ - `--gerrit-api <url>` Optional: Gerrit REST API base URL for resolving `{{changeNumber}}` (e.g. `https://gerrit.example.com/gerrit`)
82
79
  - `--gerrit-auth <token>` Optional: Authorization for Gerrit REST API (either `user:pass` for Basic or token string for Bearer)
83
- - `--gerrit <prefix>` Show Gerrit URL for each commit (supports templates `{{hash}}`, `{{changeId}}` and `{{changeNumber}}`; `{{changeId}}` falls back to `hash` when absent; `{{changeNumber}}` requires `--gerrit-api` and falls back to `changeId` or `hash`)
84
- - `--out <file>` Output file name (without path). Defaults: `commits.json` / `commits.txt` / `commits.xlsx`
85
- - `--out-dir <dir>` Output directory path — supports relative or absolute path, e.g., `--out-dir ../output`
86
- - `--serve` Start the local web service and view the submission statistics (data files will be generated under output/data)
87
- - `--port <n>` Local web service port (default: 3000)
88
- - `--serve-only` Only start the web service without exporting or analyzing data (using existing data in output/data)
89
- - `--version` show version information
80
+ - `--gerrit <prefix>` Show Gerrit URL for each commit (supports templates `{{hash}}`, `{{changeId}}` and `{{changeNumber}}`; `{{changeId}}` falls back to `hash` when absent; `{{changeNumber}}` requires `--gerrit-api` and falls back to `changeId` or `hash`)
81
+ - `--out <file>` Output file name (without path). Defaults: `commits.json` / `commits.txt` / `commits.xlsx`
82
+ - `--out-dir <dir>` Output directory path — supports relative or absolute path, e.g., `--out-dir ../output`
83
+ - `--serve` Start the local web service and view the submission statistics (data files will be generated under output/data)
84
+ - `--port <n>` Local web service port (default: 3000)
85
+ - `--serve-only` Only start the web service without exporting or analyzing data (using existing data in output/data)
86
+ - `--version` show version information
90
87
 
91
88
  > Output files are written to an `output/` directory in the current working directory.
92
89
  >
93
90
  > Tip: Use `--out-parent` or `--out-dir ../output` to write outputs into the parent folder's `output/` to avoid accidentally committing generated files to your repository.
94
91
 
95
92
  ### Per-period outputs
93
+
96
94
  You can generate per-month and per-week outputs under `output/month/` and `output/week/` using the `--per-period-formats` option. Example:
97
95
 
98
96
  ```bash
@@ -113,6 +111,7 @@ wukong-gitlog-cli --overtime --limit 200 --format text --out commits.txt --per-p
113
111
  ```
114
112
 
115
113
  ### Serve a local dashboard
114
+
116
115
  You can start a small static web dashboard to visualize commit statistics and charts. It will export raw commits and analyzed stats into `output/data/` as `commits.mjs` and `overtime-stats.mjs`, and start a local web server serving `web/` and `output/data/`:
117
116
 
118
117
  ```bash
@@ -128,7 +127,9 @@ wukong-gitlog-cli --serve --port 8080 --overtime --limit 200 --out commits.txt
128
127
 
129
128
  Open `http://localhost:3000` to view the dashboard.
130
129
 
131
-
130
+ <p align="center">
131
+ <img src="https://raw.githubusercontent.com/tomatobybike/wukong-gitlog-cli/main/images/web/overtime.jpg" width="400" alt="wukong-dev Logo" />
132
+ </p>
132
133
 
133
134
  ---
134
135
 
@@ -209,8 +210,6 @@ wukong-gitlog-cli --out-dir ../output --format text --limit 5 --out custom1.txt
209
210
 
210
211
  ---
211
212
 
212
-
213
-
214
213
  ```bash
215
214
  wukong-gitlog-cli --overtime --limit 500
216
215
  ```
@@ -220,7 +219,7 @@ wukong-gitlog-cli --overtime --limit 500
220
219
  - The CLI prints helpful messages after exporting files and writes outputs to the `output/` folder in the repo root.
221
220
  - Internally `src/utils/index.mjs` acts as a barrel that re-exports helper functions located in `src/utils/`.
222
221
  - If you plan to reuse the helpers in other modules, import from `./src/utils/index.mjs` explicitly.
223
- - The Excel export uses `exceljs` and adds an ``autoFilter`` to the sheet header.
222
+ - The Excel export uses `exceljs` and adds an `autoFilter` to the sheet header.
224
223
 
225
224
  Suggested `.gitignore` snippet (to avoid accidentally committing generated files):
226
225
 
@@ -241,4 +240,3 @@ PRs are welcome — add tests and keep changes modular. If you add new CLI flags
241
240
  ## License
242
241
 
243
242
  MIT
244
-
package/README.zh-CN.md CHANGED
@@ -118,7 +118,7 @@ wukong-gitlog-cli --per-period-only
118
118
  启动服务器:
119
119
 
120
120
  ```bash
121
- wukong-gitlog-cli --serve --overtime --limit 200
121
+ wukong-gitlog-cli --overtime --serve --port 5555 --limit 200
122
122
  ```
123
123
 
124
124
  浏览器访问:
@@ -127,6 +127,10 @@ wukong-gitlog-cli --serve --overtime --limit 200
127
127
  http://localhost:3000
128
128
  ```
129
129
 
130
+
131
+ <p align="center">
132
+ <img src="https://raw.githubusercontent.com/tomatobybike/wukong-gitlog-cli/main/images/web/overtime.jpg" width="400" alt="wukong-dev Logo" />
133
+ </p>
130
134
  ---
131
135
 
132
136
  ## 🔗 Gerrit 支持
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wukong-gitlog-cli",
3
- "version": "1.0.8",
3
+ "version": "1.0.11",
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,5 +1,6 @@
1
1
  import chalk from 'chalk'
2
2
  import { Command } from 'commander'
3
+ import dayjs from 'dayjs'
3
4
  import fs from 'fs'
4
5
  import path from 'path'
5
6
  import { fileURLToPath } from 'url'
@@ -105,6 +106,12 @@ const main = async () => {
105
106
  (v) => parseInt(v, 10),
106
107
  14
107
108
  )
109
+ .option(
110
+ '--overnight-cutoff <hour>',
111
+ '次日凌晨归并窗口(小时),默认 6',
112
+ (v) => parseInt(v, 10),
113
+ 6
114
+ )
108
115
  .option('--out <file>', '输出文件名(不含路径)')
109
116
  .option(
110
117
  '--out-dir <dir>',
@@ -351,6 +358,86 @@ const main = async () => {
351
358
  writeTextFile(dataWeeklyFile, weeklyModule)
352
359
  console.log(chalk.green(`Weekly series 已导出: ${dataWeeklyFile}`))
353
360
 
361
+ // 新增:每月趋势数据(用于前端图表)
362
+ const monthGroups2 = groupRecords(records, 'month')
363
+ const monthKeys2 = Object.keys(monthGroups2).sort()
364
+ const monthlySeries = monthKeys2.map((k) => {
365
+ const s = analyzeOvertime(monthGroups2[k], {
366
+ startHour:
367
+ opts.workStart || opts.workStart === 0 ? opts.workStart : 9,
368
+ endHour: opts.workEnd || opts.workEnd === 0 ? opts.workEnd : 18,
369
+ lunchStart:
370
+ opts.lunchStart || opts.lunchStart === 0 ? opts.lunchStart : 12,
371
+ lunchEnd: opts.lunchEnd || opts.lunchEnd === 0 ? opts.lunchEnd : 14,
372
+ country: opts.country || 'CN'
373
+ })
374
+ return {
375
+ period: k,
376
+ total: s.total,
377
+ outsideWorkCount: s.outsideWorkCount,
378
+ outsideWorkRate: s.outsideWorkRate,
379
+ nonWorkdayCount: s.nonWorkdayCount,
380
+ nonWorkdayRate: s.nonWorkdayRate
381
+ }
382
+ })
383
+ const dataMonthlyFile = outputFilePath('data/overtime-monthly.mjs', outDir)
384
+ const monthlyModule = `export default ${JSON.stringify(monthlySeries, null, 2)};\n`
385
+ writeTextFile(dataMonthlyFile, monthlyModule)
386
+ console.log(chalk.green(`Monthly series 已导出: ${dataMonthlyFile}`))
387
+
388
+ // 新增:每日最晚提交小时(用于显著展示加班严重程度)
389
+ const dayGroups2 = groupRecords(records, 'day')
390
+ 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
400
+ const nextKey = dayjs(k).add(1, 'day').format('YYYY-MM-DD')
401
+ const early = dayGroups2[nextKey] || []
402
+ const earlyHours = early
403
+ .map((r) => new Date(r.date))
404
+ .filter((d) => !Number.isNaN(d.valueOf()))
405
+ .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 }
414
+ })
415
+ const dataLatestByDayFile = outputFilePath(
416
+ 'data/overtime-latest-by-day.mjs',
417
+ outDir
418
+ )
419
+ const latestByDayModule = `export default ${JSON.stringify(latestByDay, null, 2)};\n`
420
+ writeTextFile(dataLatestByDayFile, latestByDayModule)
421
+ console.log(
422
+ chalk.green(`Latest-by-day series 已导出: ${dataLatestByDayFile}`)
423
+ )
424
+
425
+ // 导出配置(供前端显示)
426
+ try {
427
+ const configFile = outputFilePath('data/config.mjs', outDir)
428
+ const cfg = {
429
+ startHour: opts.workStart || 9,
430
+ endHour: opts.workEnd || 18,
431
+ lunchStart: opts.lunchStart || 12,
432
+ lunchEnd: opts.lunchEnd || 14,
433
+ overnightCutoff
434
+ }
435
+ writeTextFile(configFile, `export default ${JSON.stringify(cfg, null, 2)};\n`)
436
+ console.log(chalk.green(`Config 已导出: ${configFile}`))
437
+ } catch (e) {
438
+ console.warn('Export config failed:', e && e.message ? e.message : e)
439
+ }
440
+
354
441
  startServer(opts.port || 3000, outDir).catch(() => {})
355
442
  } catch (err) {
356
443
  console.warn(
package/src/overtime.mjs CHANGED
@@ -47,6 +47,8 @@ export function analyzeOvertime(records, opts = {}) {
47
47
  let outsideWorkCount = 0;
48
48
  let nonWorkdayCount = 0;
49
49
  let holidayCount = 0;
50
+ let nightOutsideCount = 0;
51
+ let overtimeSeveritySum = 0;
50
52
 
51
53
  const byAuthor = new Map();
52
54
 
@@ -54,6 +56,8 @@ export function analyzeOvertime(records, opts = {}) {
54
56
  let endCommit = null;
55
57
  let latestCommit = null;
56
58
  let latestOutsideCommit = null;
59
+ let latestCommitHour = null;
60
+ let latestOutsideCommitHour = null;
57
61
 
58
62
  // init holiday checker for country
59
63
  const hd = new DateHolidays();
@@ -87,9 +91,15 @@ export function analyzeOvertime(records, opts = {}) {
87
91
 
88
92
  if (outside) {
89
93
  outsideWorkCount++;
94
+ if (hour >= endHour || hour < startHour) {
95
+ nightOutsideCount++;
96
+ const sev = hour >= endHour ? (hour - endHour) : (24 - endHour + hour);
97
+ overtimeSeveritySum += sev;
98
+ }
90
99
  // 记录最新的加班提交
91
100
  if (!latestOutsideCommit || dt.isAfter(parseCommitDate(latestOutsideCommit.date))) {
92
101
  latestOutsideCommit = r;
102
+ latestOutsideCommitHour = dt.hour();
93
103
  }
94
104
  }
95
105
  if (isNonWork) nonWorkdayCount++;
@@ -137,6 +147,7 @@ export function analyzeOvertime(records, opts = {}) {
137
147
  [startCommit] = validRecords;
138
148
  endCommit = validRecords[validRecords.length - 1];
139
149
  latestCommit = endCommit;
150
+ latestCommitHour = parseCommitDate(latestCommit.date).hour();
140
151
  // cleanup temp _dt
141
152
  for (let i = 0; i < validRecords.length; i++) {
142
153
  // copy the object without _dt for safety
@@ -157,12 +168,18 @@ export function analyzeOvertime(records, opts = {}) {
157
168
  nonWorkdayCount,
158
169
  outsideWorkRate: total ? +(outsideWorkCount / total).toFixed(3) : 0,
159
170
  nonWorkdayRate: total ? +(nonWorkdayCount / total).toFixed(3) : 0,
171
+ nightOutsideCount,
172
+ nightOutsideRate: total ? +(nightOutsideCount / total).toFixed(3) : 0,
173
+ overtimeSeveritySum,
174
+ overtimeSeverityAvg: nightOutsideCount ? +(overtimeSeveritySum / nightOutsideCount).toFixed(2) : 0,
160
175
  perAuthor,
161
176
  /// 提示:计算 min/max 日期 & latest commit
162
177
  startCommit: startCommit || null,
163
178
  endCommit: endCommit || null,
164
179
  latestCommit: latestCommit || null,
165
180
  latestOutsideCommit: latestOutsideCommit || null,
181
+ latestCommitHour,
182
+ latestOutsideCommitHour,
166
183
  startHour,
167
184
  endHour,
168
185
  lunchStart,
@@ -178,8 +195,8 @@ export function analyzeOvertime(records, opts = {}) {
178
195
  }
179
196
 
180
197
  export function renderOvertimeText(stats) {
181
- const { total, outsideWorkCount, nonWorkdayCount, holidayCount, outsideWorkRate, nonWorkdayRate, holidayRate, perAuthor, startHour, endHour, lunchStart, lunchEnd, country, hourlyOvertimeCommits = [], hourlyOvertimePercent = [] } = stats;
182
- const { startCommit, endCommit, latestCommit, latestOutsideCommit } = stats;
198
+ const { total, outsideWorkCount, nonWorkdayCount, holidayCount, outsideWorkRate, nonWorkdayRate, holidayRate, nightOutsideCount = 0, nightOutsideRate = 0, overtimeSeverityAvg = 0, perAuthor, startHour, endHour, lunchStart, lunchEnd, country, hourlyOvertimeCommits = [], hourlyOvertimePercent = [] } = stats;
199
+ const { startCommit, endCommit, latestCommit, latestOutsideCommit, latestCommitHour, latestOutsideCommitHour } = stats;
183
200
  const lines = [];
184
201
 
185
202
  const formatPercent = (v) => `${(v * 100).toFixed(1)}%`;
@@ -213,6 +230,7 @@ export function renderOvertimeText(stats) {
213
230
  lines.push(` Author : ${latestCommit.author}`);
214
231
  lines.push(` Date : ${formatDateForCountry(latestCommit.date, country)}`);
215
232
  lines.push(` Message: ${latestCommit.message}`);
233
+ if (typeof latestCommitHour === 'number') lines.push(` Hour : ${String(latestCommitHour).padStart(2, '0')}:00`);
216
234
  }
217
235
  if (latestOutsideCommit) {
218
236
  lines.push('加班最晚的一次提交:');
@@ -220,12 +238,15 @@ export function renderOvertimeText(stats) {
220
238
  lines.push(` Author : ${latestOutsideCommit.author}`);
221
239
  lines.push(` Date : ${formatDateForCountry(latestOutsideCommit.date, country)}`);
222
240
  lines.push(` Message: ${latestOutsideCommit.message}`);
241
+ const h = parseCommitDate(latestOutsideCommit.date).hour();
242
+ lines.push(` Hour : ${String(h).padStart(2, '0')}:00`);
223
243
  }
224
244
  // country: holiday region, lunchStart/lunchEnd define midday break
225
245
  lines.push(`下班时间定义:${startHour}:00 - ${endHour}:00 (午休 ${lunchStart}:00 - ${lunchEnd}:00)`);
226
246
  lines.push(`国家假期参考:${String(country).toUpperCase()},节假日提交数:${holidayCount},占比:${(holidayRate * 100).toFixed(1)}%`);
227
247
  lines.push(`下班时间(工作时间外)提交数:${outsideWorkCount},占比:${(outsideWorkRate * 100).toFixed(1)}%`);
228
248
  lines.push(`非工作日(周末)提交数:${nonWorkdayCount},占比:${(nonWorkdayRate * 100).toFixed(1)}%`);
249
+ lines.push(`晚间加班(${endHour}:00–次日${startHour}:00)提交数:${nightOutsideCount},占比:${(nightOutsideRate * 100).toFixed(1)}%,平均超过下班:${overtimeSeverityAvg} 小时`);
229
250
  lines.push('');
230
251
  lines.push('每小时加班分布(工作时间外提交数/占比):');
231
252
  const hourLine = ' Hour | OvertimeCount | Percent';
@@ -317,4 +338,3 @@ export function renderOvertimeCsv(stats) {
317
338
  });
318
339
  return rows.join('\n');
319
340
  }
320
-
package/web/app.js CHANGED
@@ -3,18 +3,24 @@ const formatDate = (d) => new Date(d).toLocaleString();
3
3
 
4
4
  async function loadData() {
5
5
  try {
6
- const [commitsModule, statsModule, weeklyModule] = await Promise.all([
6
+ const [commitsModule, statsModule, weeklyModule, monthlyModule, latestByDayModule, configModule] = await Promise.all([
7
7
  import("/data/commits.mjs"),
8
8
  import("/data/overtime-stats.mjs"),
9
9
  import("/data/overtime-weekly.mjs"),
10
+ import("/data/overtime-monthly.mjs").catch(() => ({ default: [] })),
11
+ import("/data/overtime-latest-by-day.mjs").catch(() => ({ default: [] })),
12
+ import("/data/config.mjs").catch(() => ({ default: {} })),
10
13
  ]);
11
14
  const commits = commitsModule.default || [];
12
15
  const stats = statsModule.default || {};
13
16
  const weekly = weeklyModule.default || [];
14
- return { commits, stats, weekly };
17
+ const monthly = monthlyModule.default || [];
18
+ const latestByDay = latestByDayModule.default || [];
19
+ const config = configModule.default || {};
20
+ return { commits, stats, weekly, monthly, latestByDay, config };
15
21
  } catch (err) {
16
22
  console.error('Load data failed', err);
17
- return { commits: [], stats: {}, weekly: [] };
23
+ return { commits: [], stats: {}, weekly: [], monthly: [], latestByDay: [] };
18
24
  }
19
25
  }
20
26
 
@@ -157,10 +163,108 @@ function drawWeeklyTrend(weekly) {
157
163
  return chart;
158
164
  }
159
165
 
166
+ function drawMonthlyTrend(monthly) {
167
+ if (!Array.isArray(monthly) || monthly.length === 0) return null;
168
+ const labels = monthly.map(m => m.period);
169
+ const dataRate = monthly.map(m => +(m.outsideWorkRate * 100).toFixed(1));
170
+ const el = document.getElementById('monthlyTrendChart');
171
+ const chart = echarts.init(el);
172
+ chart.setOption({
173
+ tooltip: {},
174
+ xAxis: { type: 'category', data: labels },
175
+ yAxis: { type: 'value', min: 0, max: 100 },
176
+ series: [{ type: 'line', name: '加班占比(%)', data: dataRate }]
177
+ });
178
+ return chart;
179
+ }
180
+
181
+ function drawLatestHourDaily(latestByDay) {
182
+ if (!Array.isArray(latestByDay) || latestByDay.length === 0) return null;
183
+ const labels = latestByDay.map(d => d.date);
184
+ const raw = latestByDay.map(d => (typeof d.latestHourNormalized === 'number' ? d.latestHourNormalized : d.latestHour ?? null));
185
+ const data = raw.map(v => ({ value: v, itemStyle: { color: (v >= 24) ? '#d32f2f' : (v >= 21 ? '#fb8c00' : '#1976d2') } }));
186
+ const el = document.getElementById('latestHourDailyChart');
187
+ const chart = echarts.init(el);
188
+ chart.setOption({
189
+ tooltip: {
190
+ trigger: 'axis',
191
+ formatter: (params) => {
192
+ const p = Array.isArray(params) ? params[0] : params;
193
+ const v = p && p.value != null ? Number(p.value) : null;
194
+ const endH = (window.__overtimeEndHour || 18);
195
+ const sev = v != null ? Math.max(0, (v >= 24 ? v : v) - endH) : 0;
196
+ return `${p.axisValue}<br/>最晚小时: ${v != null ? v : '-'}<br/>超过下班: ${sev} 小时`;
197
+ }
198
+ },
199
+ xAxis: { type: 'category', data: labels },
200
+ yAxis: { type: 'value', min: 0, max: Math.max(26, Math.ceil(Math.max(...raw.filter(v => typeof v === 'number')) + 1)) },
201
+ series: [{
202
+ type: 'line',
203
+ name: '每日最晚提交小时',
204
+ data,
205
+ markLine: {
206
+ data: [
207
+ { yAxis: 21 },
208
+ { yAxis: 24 }
209
+ ],
210
+ lineStyle: { color: '#fb8c00' }
211
+ }
212
+ }]
213
+ });
214
+ return chart;
215
+ }
216
+
217
+ function drawDailySeverity(latestByDay) {
218
+ if (!Array.isArray(latestByDay) || latestByDay.length === 0) return null;
219
+ const labels = latestByDay.map(d => d.date);
220
+ const endH = (window.__overtimeEndHour || 18);
221
+ const raw = latestByDay.map(d => (typeof d.latestHourNormalized === 'number' ? d.latestHourNormalized : d.latestHour ?? null));
222
+ const sev = raw.map(v => (v == null ? null : Math.max(0, Number(v) - endH)));
223
+ const el = document.getElementById('dailySeverityChart');
224
+ const chart = echarts.init(el);
225
+ chart.setOption({
226
+ tooltip: {},
227
+ xAxis: { type: 'category', data: labels },
228
+ yAxis: { type: 'value', min: 0 },
229
+ series: [{
230
+ type: 'line',
231
+ name: '超过下班小时数',
232
+ data: sev,
233
+ markLine: {
234
+ symbol: 'none',
235
+ data: [
236
+ { yAxis: 1 },
237
+ { yAxis: 2 }
238
+ ],
239
+ lineStyle: { color: '#9e9e9e', type: 'dashed' }
240
+ }
241
+ }]
242
+ });
243
+ return chart;
244
+ }
245
+
246
+ function renderKpi(stats) {
247
+ const el = document.getElementById('kpiContent');
248
+ if (!el || !stats) return;
249
+ const latest = stats.latestCommit;
250
+ const latestHour = stats.latestCommitHour;
251
+ const latestOut = stats.latestOutsideCommit;
252
+ const latestOutHour = stats.latestOutsideCommitHour ?? (latestOut ? new Date(latestOut.date).getHours() : null);
253
+ const cutoff = window.__overnightCutoff ?? 6;
254
+ const html = [
255
+ `<div>最晚一次提交时间:${latest ? formatDate(latest.date) : '-'}${typeof latestHour === 'number' ? `(${String(latestHour).padStart(2,'0')}:00)` : ''}</div>`,
256
+ `<div>加班最晚一次提交时间:${latestOut ? formatDate(latestOut.date) : '-'}${typeof latestOutHour === 'number' ? `(${String(latestOutHour).padStart(2,'0')}:00)` : ''}</div>`,
257
+ `<div>次日归并窗口:凌晨 <b>${cutoff}</b> 点内归前一日</div>`
258
+ ].join('');
259
+ el.innerHTML = html;
260
+ }
261
+
160
262
  (async function main() {
161
- const { commits, stats, weekly } = await loadData();
263
+ const { commits, stats, weekly, monthly, latestByDay, config } = await loadData();
162
264
  commitsAll = commits;
163
265
  filtered = commitsAll.slice();
266
+ window.__overtimeEndHour = stats && typeof stats.endHour === 'number' ? stats.endHour : (config.endHour ?? 18);
267
+ window.__overnightCutoff = typeof config.overnightCutoff === 'number' ? config.overnightCutoff : 6;
164
268
  initTableControls();
165
269
  updatePager();
166
270
  renderCommitsTablePage();
@@ -168,4 +272,8 @@ function drawWeeklyTrend(weekly) {
168
272
  drawOutsideVsInside(stats);
169
273
  drawDailyTrend(commits);
170
274
  drawWeeklyTrend(weekly);
275
+ drawMonthlyTrend(monthly);
276
+ drawLatestHourDaily(latestByDay);
277
+ drawDailySeverity(latestByDay);
278
+ renderKpi(stats);
171
279
  })();
package/web/index.html CHANGED
@@ -17,7 +17,10 @@
17
17
  <h2>下班时间 vs 工作时间提交占比</h2>
18
18
  <div id="outsideVsInsideChart" class="echart"></div>
19
19
  </div>
20
- <div class="chart-card"></div>
20
+ <div class="chart-card" id="kpiCard">
21
+ <h2>关键指标</h2>
22
+ <div id="kpiContent"></div>
23
+ </div>
21
24
  </section>
22
25
  <section id="charts">
23
26
  <div class="chart-card">
@@ -33,6 +36,18 @@
33
36
  <h2>每周趋势(加班占比)</h2>
34
37
  <div id="weeklyTrendChart" class="echart"></div>
35
38
  </div>
39
+ <div class="chart-card">
40
+ <h2>每月趋势(加班占比)</h2>
41
+ <div id="monthlyTrendChart" class="echart"></div>
42
+ </div>
43
+ <div class="chart-card">
44
+ <h2>每日最晚提交时间(小时)</h2>
45
+ <div id="latestHourDailyChart" class="echart"></div>
46
+ </div>
47
+ <div class="chart-card">
48
+ <h2>每日超过下班的小时数</h2>
49
+ <div id="dailySeverityChart" class="echart"></div>
50
+ </div>
36
51
  </section>
37
52
 
38
53
  <section class="table-card">