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 +20 -0
- package/package.json +1 -1
- package/src/cli.mjs +5 -5
- package/src/overtime.mjs +12 -6
- package/src/server.mjs +2 -2
- package/src/utils/output.mjs +2 -2
- package/web/app.js +308 -177
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
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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)
|
package/src/utils/output.mjs
CHANGED
|
@@ -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 [
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
]
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
99
|
-
const
|
|
100
|
-
const
|
|
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
|
-
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
const
|
|
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: [
|
|
119
|
-
{
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
|
|
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
|
|
194
|
-
const endH =
|
|
195
|
-
const sev = v != null ? Math.max(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: {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
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
|
-
|
|
220
|
-
const
|
|
221
|
-
const
|
|
222
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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 =
|
|
253
|
-
|
|
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 } =
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
window.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
+
})()
|