wukong-gitlog-cli 1.0.11 → 1.0.13

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,26 @@
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.13](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.12...v1.0.13) (2025-12-01)
6
+
7
+
8
+ ### Features
9
+
10
+ * 🎸 output-wukong ([c310623](https://github.com/tomatobybike/wukong-gitlog-cli/commit/c310623314c44bad792b55967c5aeabda5e61812))
11
+
12
+ ### [1.0.12](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.11...v1.0.12) (2025-12-01)
13
+
14
+
15
+ ### Features
16
+
17
+ * 🎸 make color ([78d234a](https://github.com/tomatobybike/wukong-gitlog-cli/commit/78d234a04db3d925d8bc1d6cb593173026bcf336))
18
+ * 🎸 make line color ([509f7af](https://github.com/tomatobybike/wukong-gitlog-cli/commit/509f7afca7c2c3cca7a59cf3ce1cdae2832b0c06))
19
+
20
+
21
+ ### Bug Fixes
22
+
23
+ * 🐛 next day time ([1d0fea5](https://github.com/tomatobybike/wukong-gitlog-cli/commit/1d0fea548dbd5e8a75a63f80fd22649c19dc3f59))
24
+
5
25
  ### [1.0.11](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.10...v1.0.11) (2025-12-01)
6
26
 
7
27
  ### [1.0.10](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.9...v1.0.10) (2025-12-01)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wukong-gitlog-cli",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
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/overtime.mjs CHANGED
@@ -49,6 +49,7 @@ export function analyzeOvertime(records, opts = {}) {
49
49
  let holidayCount = 0;
50
50
  let nightOutsideCount = 0;
51
51
  let overtimeSeveritySum = 0;
52
+ let maxSeverity = -1;
52
53
 
53
54
  const byAuthor = new Map();
54
55
 
@@ -95,11 +96,11 @@ export function analyzeOvertime(records, opts = {}) {
95
96
  nightOutsideCount++;
96
97
  const sev = hour >= endHour ? (hour - endHour) : (24 - endHour + hour);
97
98
  overtimeSeveritySum += sev;
98
- }
99
- // 记录最新的加班提交
100
- if (!latestOutsideCommit || dt.isAfter(parseCommitDate(latestOutsideCommit.date))) {
101
- latestOutsideCommit = r;
102
- latestOutsideCommitHour = dt.hour();
99
+ if (sev > maxSeverity) {
100
+ maxSeverity = sev;
101
+ latestOutsideCommit = r;
102
+ latestOutsideCommitHour = hour;
103
+ }
103
104
  }
104
105
  }
105
106
  if (isNonWork) nonWorkdayCount++;
@@ -191,6 +192,7 @@ export function analyzeOvertime(records, opts = {}) {
191
192
  hourlyPercent,
192
193
  hourlyOvertimeCommits,
193
194
  hourlyOvertimePercent,
195
+ latestOutsideCommitSeverity: (maxSeverity >= 0 ? maxSeverity : null),
194
196
  };
195
197
  }
196
198
 
@@ -239,7 +241,11 @@ export function renderOvertimeText(stats) {
239
241
  lines.push(` Date : ${formatDateForCountry(latestOutsideCommit.date, country)}`);
240
242
  lines.push(` Message: ${latestOutsideCommit.message}`);
241
243
  const h = parseCommitDate(latestOutsideCommit.date).hour();
242
- lines.push(` Hour : ${String(h).padStart(2, '0')}:00`);
244
+ let sev = null;
245
+ if (typeof endHour === 'number' && typeof h === 'number') {
246
+ sev = h >= endHour ? (h - endHour) : (24 - endHour + h);
247
+ }
248
+ lines.push(` Hour : ${String(h).padStart(2, '0')}:00${sev != null ? `(超过下班:${sev} 小时)` : ''}`);
243
249
  }
244
250
  // country: holiday region, lunchStart/lunchEnd define midday break
245
251
  lines.push(`下班时间定义:${startHour}:00 - ${endHour}:00 (午休 ${lunchStart}:00 - ${lunchEnd}:00)`);
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
@@ -1,279 +1,410 @@
1
1
  /* eslint-disable import/no-absolute-path */
2
- const formatDate = (d) => new Date(d).toLocaleString();
2
+ const formatDate = (d) => new Date(d).toLocaleString()
3
3
 
4
4
  async function loadData() {
5
5
  try {
6
- const [commitsModule, statsModule, weeklyModule, monthlyModule, latestByDayModule, configModule] = await Promise.all([
7
- import("/data/commits.mjs"),
8
- import("/data/overtime-stats.mjs"),
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: {} })),
13
- ]);
14
- const commits = commitsModule.default || [];
15
- const stats = statsModule.default || {};
16
- const weekly = weeklyModule.default || [];
17
- const monthly = monthlyModule.default || [];
18
- const latestByDay = latestByDayModule.default || [];
19
- const config = configModule.default || {};
20
- return { commits, stats, weekly, monthly, latestByDay, config };
6
+ const [
7
+ commitsModule,
8
+ statsModule,
9
+ weeklyModule,
10
+ monthlyModule,
11
+ latestByDayModule,
12
+ configModule
13
+ ] = await Promise.all([
14
+ import('/data/commits.mjs'),
15
+ import('/data/overtime-stats.mjs'),
16
+ import('/data/overtime-weekly.mjs'),
17
+ import('/data/overtime-monthly.mjs').catch(() => ({ default: [] })),
18
+ import('/data/overtime-latest-by-day.mjs').catch(() => ({ default: [] })),
19
+ import('/data/config.mjs').catch(() => ({ default: {} }))
20
+ ])
21
+ const commits = commitsModule.default || []
22
+ const stats = statsModule.default || {}
23
+ const weekly = weeklyModule.default || []
24
+ const monthly = monthlyModule.default || []
25
+ const latestByDay = latestByDayModule.default || []
26
+ const config = configModule.default || {}
27
+ return { commits, stats, weekly, monthly, latestByDay, config }
21
28
  } catch (err) {
22
- console.error('Load data failed', err);
23
- return { commits: [], stats: {}, weekly: [], monthly: [], latestByDay: [] };
29
+ console.error('Load data failed', err)
30
+ return { commits: [], stats: {}, weekly: [], monthly: [], latestByDay: [] }
24
31
  }
25
32
  }
26
33
 
27
- let commitsAll = [];
28
- let filtered = [];
29
- let page = 1;
30
- let pageSize = 10;
34
+ let commitsAll = []
35
+ let filtered = []
36
+ let page = 1
37
+ let pageSize = 10
31
38
 
32
39
  function renderCommitsTablePage() {
33
- const tbody = document.querySelector('#commitsTable tbody');
34
- tbody.innerHTML = '';
35
- const start = (page - 1) * pageSize;
36
- const end = start + pageSize;
40
+ const tbody = document.querySelector('#commitsTable tbody')
41
+ tbody.innerHTML = ''
42
+ const start = (page - 1) * pageSize
43
+ const end = start + pageSize
37
44
  filtered.slice(start, end).forEach((c) => {
38
- const tr = document.createElement('tr');
39
- tr.innerHTML = `<td>${c.hash.slice(0, 8)}</td><td>${c.author}</td><td>${formatDate(c.date)}</td><td>${c.message}</td>`;
40
- tbody.appendChild(tr);
41
- });
45
+ const tr = document.createElement('tr')
46
+ tr.innerHTML = `<td>${c.hash.slice(0, 8)}</td><td>${c.author}</td><td>${formatDate(c.date)}</td><td>${c.message}</td>`
47
+ tbody.appendChild(tr)
48
+ })
42
49
  }
43
50
 
44
51
  function updatePager() {
45
- const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
46
- if (page > totalPages) page = totalPages;
47
- const pageInfo = document.getElementById('pageInfo');
48
- pageInfo.textContent = `${page} / ${totalPages}`;
49
- document.getElementById('prevPage').disabled = page <= 1;
50
- document.getElementById('nextPage').disabled = page >= totalPages;
52
+ const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize))
53
+ if (page > totalPages) page = totalPages
54
+ const pageInfo = document.getElementById('pageInfo')
55
+ pageInfo.textContent = `${page} / ${totalPages}`
56
+ document.getElementById('prevPage').disabled = page <= 1
57
+ document.getElementById('nextPage').disabled = page >= totalPages
51
58
  }
52
59
 
53
60
  function applySearch() {
54
- const q = document.getElementById('searchInput').value.trim().toLowerCase();
61
+ const q = document.getElementById('searchInput').value.trim().toLowerCase()
55
62
  if (!q) {
56
- filtered = commitsAll.slice();
63
+ filtered = commitsAll.slice()
57
64
  } else {
58
65
  filtered = commitsAll.filter((c) => {
59
- const h = c.hash.toLowerCase();
60
- const a = String(c.author || '').toLowerCase();
61
- const m = String(c.message || '').toLowerCase();
62
- const d = formatDate(c.date).toLowerCase();
63
- return h.includes(q) || a.includes(q) || m.includes(q) || d.includes(q);
64
- });
66
+ const h = c.hash.toLowerCase()
67
+ const a = String(c.author || '').toLowerCase()
68
+ const m = String(c.message || '').toLowerCase()
69
+ const d = formatDate(c.date).toLowerCase()
70
+ return h.includes(q) || a.includes(q) || m.includes(q) || d.includes(q)
71
+ })
65
72
  }
66
- page = 1;
67
- updatePager();
68
- renderCommitsTablePage();
73
+ page = 1
74
+ updatePager()
75
+ renderCommitsTablePage()
69
76
  }
70
77
 
71
78
  function initTableControls() {
72
- document.getElementById('searchInput').addEventListener('input', applySearch);
79
+ document.getElementById('searchInput').addEventListener('input', applySearch)
73
80
  document.getElementById('pageSize').addEventListener('change', (e) => {
74
- pageSize = parseInt(e.target.value, 10) || 10;
75
- page = 1;
76
- updatePager();
77
- renderCommitsTablePage();
78
- });
81
+ pageSize = parseInt(e.target.value, 10) || 10
82
+ page = 1
83
+ updatePager()
84
+ renderCommitsTablePage()
85
+ })
79
86
  document.getElementById('prevPage').addEventListener('click', () => {
80
87
  if (page > 1) {
81
- page -= 1;
82
- updatePager();
83
- renderCommitsTablePage();
88
+ page -= 1
89
+ updatePager()
90
+ renderCommitsTablePage()
84
91
  }
85
- });
92
+ })
86
93
  document.getElementById('nextPage').addEventListener('click', () => {
87
- const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
94
+ const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize))
88
95
  if (page < totalPages) {
89
- page += 1;
90
- updatePager();
91
- renderCommitsTablePage();
96
+ page += 1
97
+ updatePager()
98
+ renderCommitsTablePage()
92
99
  }
93
- });
100
+ })
94
101
  }
95
102
 
96
103
  function drawHourlyOvertime(stats) {
97
- const el = document.getElementById('hourlyOvertimeChart');
98
- const chart = echarts.init(el);
99
- const data = stats.hourlyOvertimeCommits || [];
100
- const labels = Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0'));
104
+ const el = document.getElementById('hourlyOvertimeChart')
105
+ // eslint-disable-next-line no-undef
106
+ const chart = echarts.init(el)
107
+ const data = stats.hourlyOvertimeCommits || []
108
+ const labels = Array.from({ length: 24 }, (_, i) =>
109
+ String(i).padStart(2, '0')
110
+ )
101
111
  chart.setOption({
102
112
  tooltip: {},
103
113
  xAxis: { type: 'category', data: labels },
104
114
  yAxis: { type: 'value' },
105
115
  series: [{ type: 'bar', name: 'Overtime commits', data }]
106
- });
107
- return chart;
116
+ })
117
+ return chart
108
118
  }
109
119
 
110
120
  function drawOutsideVsInside(stats) {
111
- const el = document.getElementById('outsideVsInsideChart');
112
- const chart = echarts.init(el);
113
- const outside = stats.outsideWorkCount || 0;
114
- const total = stats.total || 0;
115
- const inside = Math.max(0, total - outside);
121
+ const el = document.getElementById('outsideVsInsideChart')
122
+ // eslint-disable-next-line no-undef
123
+ const chart = echarts.init(el)
124
+ const outside = stats.outsideWorkCount || 0
125
+ const total = stats.total || 0
126
+ const inside = Math.max(0, total - outside)
116
127
  chart.setOption({
117
128
  tooltip: {},
118
- series: [{ type: 'pie', radius: '55%', data: [
119
- { value: inside, name: '工作时间内' },
120
- { value: outside, name: '下班时间' }
121
- ] }]
122
- });
123
- return chart;
129
+ series: [
130
+ {
131
+ type: 'pie',
132
+ radius: '55%',
133
+ data: [
134
+ { value: inside, name: '工作时间内' },
135
+ { value: outside, name: '下班时间' }
136
+ ]
137
+ }
138
+ ]
139
+ })
140
+ return chart
124
141
  }
125
142
 
126
143
  function drawDailyTrend(commits) {
127
- const map = new Map();
144
+ const map = new Map()
128
145
  commits.forEach((c) => {
129
- const d = new Date(c.date).toISOString().slice(0, 10);
130
- map.set(d, (map.get(d) || 0) + 1);
131
- });
132
- const labels = Array.from(map.keys()).sort();
133
- const data = labels.map(l => map.get(l));
134
- const el = document.getElementById('dailyTrendChart');
135
- const chart = echarts.init(el);
146
+ const d = new Date(c.date).toISOString().slice(0, 10)
147
+ map.set(d, (map.get(d) || 0) + 1)
148
+ })
149
+ const labels = Array.from(map.keys()).sort()
150
+ const data = labels.map((l) => map.get(l))
151
+ const el = document.getElementById('dailyTrendChart')
152
+ // eslint-disable-next-line no-undef
153
+ const chart = echarts.init(el)
136
154
  chart.setOption({
137
155
  tooltip: {},
138
156
  xAxis: { type: 'category', data: labels },
139
157
  yAxis: { type: 'value' },
140
158
  series: [{ type: 'line', name: '每日提交', data, areaStyle: {} }]
141
- });
142
- return chart;
159
+ })
160
+ return chart
143
161
  }
144
162
 
145
163
  function drawWeeklyTrend(weekly) {
146
- const labels = weekly.map(w => w.period);
147
- const dataRate = weekly.map(w => +(w.outsideWorkRate * 100).toFixed(1));
148
- const dataCount = weekly.map(w => w.outsideWorkCount);
149
- const el = document.getElementById('weeklyTrendChart');
150
- const chart = echarts.init(el);
164
+ const labels = weekly.map((w) => w.period)
165
+ const dataRate = weekly.map((w) => +(w.outsideWorkRate * 100).toFixed(1))
166
+ const dataCount = weekly.map((w) => w.outsideWorkCount)
167
+ const el = document.getElementById('weeklyTrendChart')
168
+ // eslint-disable-next-line no-undef
169
+ const chart = echarts.init(el)
151
170
  chart.setOption({
152
171
  tooltip: {},
153
172
  xAxis: { type: 'category', data: labels },
154
- yAxis: [
155
- { type: 'value', min: 0, max: 100 },
156
- { type: 'value' }
157
- ],
173
+ yAxis: [{ type: 'value', min: 0, max: 100 }, { type: 'value' }],
158
174
  series: [
159
175
  { type: 'line', name: '加班占比(%)', data: dataRate, yAxisIndex: 0 },
160
176
  { type: 'line', name: '加班次数', data: dataCount, yAxisIndex: 1 }
161
177
  ]
162
- });
163
- return chart;
178
+ })
179
+ return chart
164
180
  }
165
181
 
166
182
  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);
183
+ if (!Array.isArray(monthly) || monthly.length === 0) return null
184
+ const labels = monthly.map((m) => m.period)
185
+ const dataRate = monthly.map((m) => +(m.outsideWorkRate * 100).toFixed(1))
186
+ const el = document.getElementById('monthlyTrendChart')
187
+ // eslint-disable-next-line no-undef
188
+ const chart = echarts.init(el)
172
189
  chart.setOption({
173
190
  tooltip: {},
174
191
  xAxis: { type: 'category', data: labels },
175
192
  yAxis: { type: 'value', min: 0, max: 100 },
176
193
  series: [{ type: 'line', name: '加班占比(%)', data: dataRate }]
177
- });
178
- return chart;
194
+ })
195
+ return chart
179
196
  }
180
197
 
181
198
  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);
199
+ if (!Array.isArray(latestByDay) || latestByDay.length === 0) return null
200
+
201
+ const labels = latestByDay.map((d) => d.date)
202
+
203
+ const raw = latestByDay.map((d) =>
204
+ typeof d.latestHourNormalized === 'number'
205
+ ? d.latestHourNormalized
206
+ : (d.latestHour ?? null)
207
+ )
208
+
209
+ // 数据点颜色
210
+ const data = raw.map((v) => ({
211
+ value: v,
212
+ itemStyle: {
213
+ color:
214
+ v >= 20
215
+ ? '#d32f2f' // 红
216
+ : v >= 19
217
+ ? '#fb8c00' // 橙
218
+ : '#1976d2' // 蓝
219
+ }
220
+ }))
221
+
222
+ // 获取最大值,用于设置 yAxis 的 max
223
+ const numericValues = raw.filter((v) => typeof v === 'number')
224
+ const maxV = numericValues.length > 0 ? Math.max(...numericValues) : 0
225
+
226
+ const el = document.getElementById('latestHourDailyChart')
227
+ const chart = echarts.init(el)
228
+
188
229
  chart.setOption({
189
230
  tooltip: {
190
231
  trigger: 'axis',
191
232
  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} 小时`;
233
+ const p = Array.isArray(params) ? params[0] : params
234
+ const v = p?.value != null ? Number(p.value) : null
235
+ 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} 小时`
197
238
  }
198
239
  },
199
240
  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' }
241
+ yAxis: {
242
+ type: 'value',
243
+ min: 0,
244
+ max: Math.max(26, Math.ceil(maxV + 1))
245
+ },
246
+ series: [
247
+ {
248
+ type: 'line',
249
+ name: '每日最晚提交小时',
250
+ data,
251
+ markLine: {
252
+ symbol: ['none', 'arrow'],
253
+ data: [
254
+ // 20 小时线(橙色)
255
+ {
256
+ yAxis: 19,
257
+ lineStyle: {
258
+ color: '#fb8c00',
259
+ width: 2,
260
+ type: 'solid'
261
+ },
262
+ label: {
263
+ formatter: '21h',
264
+ color: '#fb8c00'
265
+ }
266
+ },
267
+ // 21 小时线(红色)
268
+ {
269
+ yAxis: 20,
270
+ lineStyle: {
271
+ color: '#d32f2f',
272
+ width: 2,
273
+ type: 'solid'
274
+ },
275
+ label: {
276
+ formatter: '24h',
277
+ color: '#d32f2f'
278
+ }
279
+ }
280
+ ]
281
+ }
211
282
  }
212
- }]
213
- });
214
- return chart;
283
+ ]
284
+ })
285
+
286
+ return chart
215
287
  }
216
288
 
217
289
  function drawDailySeverity(latestByDay) {
218
290
  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)));
291
+
292
+ const labels = latestByDay.map((d) => d.date);
293
+ const endH = window.__overtimeEndHour || 18;
294
+
295
+ const raw = latestByDay.map((d) =>
296
+ typeof d.latestHourNormalized === 'number'
297
+ ? d.latestHourNormalized
298
+ : (d.latestHour ?? null)
299
+ );
300
+
301
+ const sev = raw.map((v) => (v == null ? null : Math.max(0, Number(v) - endH)));
302
+
223
303
  const el = document.getElementById('dailySeverityChart');
224
304
  const chart = echarts.init(el);
305
+
225
306
  chart.setOption({
226
307
  tooltip: {},
227
308
  xAxis: { type: 'category', data: labels },
228
309
  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' }
310
+
311
+ series: [
312
+ {
313
+ type: 'line',
314
+ name: '超过下班小时数',
315
+ data: sev,
316
+
317
+ // 加班区域背景
318
+ markArea: {
319
+ data: [
320
+ // 0–1h:透明
321
+ [
322
+ { yAxis: 0 },
323
+ { yAxis: 1, itemStyle: { color: 'rgba(0,0,0,0)' } }
324
+ ],
325
+ // 1–2h:半透明橙色
326
+ [
327
+ { yAxis: 1 },
328
+ { yAxis: 2, itemStyle: { color: 'rgba(251, 140, 0, 0.15)' } } // #fb8c00
329
+ ],
330
+ // ≥2h:半透明红色
331
+ [
332
+ { yAxis: 2 },
333
+ { yAxis: 10, itemStyle: { color: 'rgba(211, 47, 47, 0.15)' } } // #d32f2f
334
+ ]
335
+ ]
336
+ },
337
+
338
+ // ⭐ 超时阈值标线
339
+ markLine: {
340
+ symbol: ['none', 'arrow'],
341
+ data: [
342
+ {
343
+ yAxis: 1,
344
+ lineStyle: {
345
+ color: '#fb8c00',
346
+ width: 2,
347
+ type: 'dashed'
348
+ },
349
+ label: { formatter: '1h', color: '#fb8c00' }
350
+ },
351
+ {
352
+ yAxis: 2,
353
+ lineStyle: {
354
+ color: '#d32f2f',
355
+ width: 2,
356
+ type: 'dashed'
357
+ },
358
+ label: { formatter: '2h', color: '#d32f2f' }
359
+ }
360
+ ]
361
+ }
240
362
  }
241
- }]
363
+ ]
242
364
  });
365
+
243
366
  return chart;
244
367
  }
245
368
 
369
+
246
370
  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;
371
+ const el = document.getElementById('kpiContent')
372
+ if (!el || !stats) return
373
+ const latest = stats.latestCommit
374
+ const latestHour = stats.latestCommitHour
375
+ const latestOut = stats.latestOutsideCommit
376
+ const latestOutHour =
377
+ stats.latestOutsideCommitHour ??
378
+ (latestOut ? new Date(latestOut.date).getHours() : null)
379
+ const cutoff = window.__overnightCutoff ?? 6
254
380
  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>`,
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>`,
257
383
  `<div>次日归并窗口:凌晨 <b>${cutoff}</b> 点内归前一日</div>`
258
- ].join('');
259
- el.innerHTML = html;
384
+ ].join('')
385
+ el.innerHTML = html
260
386
  }
261
387
 
262
- (async function main() {
263
- const { commits, stats, weekly, monthly, latestByDay, config } = await loadData();
264
- commitsAll = commits;
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;
268
- initTableControls();
269
- updatePager();
270
- renderCommitsTablePage();
271
- drawHourlyOvertime(stats);
272
- drawOutsideVsInside(stats);
273
- drawDailyTrend(commits);
274
- drawWeeklyTrend(weekly);
275
- drawMonthlyTrend(monthly);
276
- drawLatestHourDaily(latestByDay);
277
- drawDailySeverity(latestByDay);
278
- renderKpi(stats);
279
- })();
388
+ ;(async function main() {
389
+ const { commits, stats, weekly, monthly, latestByDay, config } =
390
+ await loadData()
391
+ commitsAll = commits
392
+ filtered = commitsAll.slice()
393
+ window.__overtimeEndHour =
394
+ stats && typeof stats.endHour === 'number'
395
+ ? stats.endHour
396
+ : (config.endHour ?? 18)
397
+ window.__overnightCutoff =
398
+ typeof config.overnightCutoff === 'number' ? config.overnightCutoff : 6
399
+ initTableControls()
400
+ updatePager()
401
+ renderCommitsTablePage()
402
+ drawHourlyOvertime(stats)
403
+ drawOutsideVsInside(stats)
404
+ drawDailyTrend(commits)
405
+ drawWeeklyTrend(weekly)
406
+ drawMonthlyTrend(monthly)
407
+ drawLatestHourDaily(latestByDay)
408
+ drawDailySeverity(latestByDay)
409
+ renderKpi(stats)
410
+ })()