🏠 Home 

一键生成 GitLab 周报汇总

一键生成 GitLab 周报汇总,生成自定义时间段的汇报。(主要为公司内部开发使用。因 gitlab 版本不确定性,不保证完全兼容其他 gitlab 版本,如有需求,请到 github 留 issues。)

// ==UserScript==
// @name         一键生成 GitLab 周报汇总
// @namespace    https://github.com/kiccer
// @version      2.3.4
// @description  一键生成 GitLab 周报汇总,生成自定义时间段的汇报。(主要为公司内部开发使用。因 gitlab 版本不确定性,不保证完全兼容其他 gitlab 版本,如有需求,请到 github 留 issues。)
// @author       kiccer<[email protected]>
// @supportURL   https://github.com/kiccer/TampermonkeyScripts/issues
// @license      MIT
// @match        http://192.168.1.128:8088/*
// @icon         https://gd-hbimg.huaban.com/690fe61ca630eaffd3e052c73d3aa7d66d45d95a6101-gORZdx_fw658/format/webp
// @require      https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js
// @require      https://cdn.bootcdn.net/ajax/libs/bootstrap-daterangepicker/3.1/moment.min.js
// @require      https://cdn.bootcdn.net/ajax/libs/bootstrap-daterangepicker/3.1/daterangepicker.min.js
// @require      https://cdn.bootcdn.net/ajax/libs/toastr.js/latest/toastr.min.js
// @resource toastr_css https://cdn.bootcdn.net/ajax/libs/toastr.js/latest/toastr.min.css
// @resource daterangepicker_css https://cdn.bootcdn.net/ajax/libs/bootstrap-daterangepicker/3.1/daterangepicker.min.css
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @grant        GM_xmlhttpRequest
// @grant        GM_setClipboard
// @noframes
// ==/UserScript==
/* globals $ moment toastr */
$(() => {
'use strict'
const targetPath = $('.header-user-dropdown-toggle').attr('href')
const currentPath = location.pathname
// 判断是否是目标地址
if (targetPath !== currentPath) return
// 加载 CSS
GM_addStyle(GM_getResourceText('toastr_css'))
GM_addStyle(GM_getResourceText('daterangepicker_css'))
GM_addStyle(`
.kiccer-daterange-input {
visibility: hidden;
width: 0;
height: 32px;
border: 0;
padding: 0;
margin: 0;
position: absolute;
}
`)
// 按钮容器
const btnContainer = $('.cover-controls')
// 日报
const copyBtn = $('<a>')
copyBtn.addClass('btn btn-gray')
copyBtn.html('生成日报')
copyBtn.appendTo(btnContainer)
copyBtn.on('click', async e => {
const startTime = moment().format('YYYY-MM-DD 00:00:00')
const endTime = moment().format('YYYY-MM-DD 23:59:59')
const summaryList = await getSummary(startTime, endTime)
const text = getTextBySummary(startTime, endTime, summaryList)
copy(text)
toastr.success('复制成功!')
})
// 智能生成周报
const smartBtn = $('<a>')
smartBtn.addClass('btn btn-gray')
smartBtn.attr('title', '获取过去最近的工作周,自动判断法定节假日。')
smartBtn.html('智能周报')
smartBtn.appendTo(btnContainer)
smartBtn.on('click', async e => {
const end = await findDate(moment(), [0])
const start = await findDate(end.subtract(1, 'day'), [1, 2])
const startTime = start.add(1, 'day').format('YYYY-MM-DD 00:00:00')
const endTime = end.format('YYYY-MM-DD 23:59:59')
const summaryList = await getSummary(startTime, endTime)
const text = getTextBySummary(startTime, endTime, summaryList)
copy(text)
toastr.success('复制成功!')
})
// 自定义汇总时间范围
const customBtn = $('<a>')
customBtn.addClass('btn btn-gray')
customBtn.html('自定义时间')
customBtn.appendTo(btnContainer)
customBtn.on('click', e => {
dateRange.click()
})
// 日期选择期 (https://github.com/dangrossman/daterangepicker)
const dateRange = $('<input type="text" name="daterange" class="kiccer-daterange-input" />')
customBtn.append(dateRange)
dateRange.on('click', e => {
e.stopPropagation()
})
$('input[name="daterange"]').daterangepicker({
opens: 'left',
locale: {
format: 'YYYY-MM-DD',
separator: ' - ',
applyLabel: '确定',
cancelLabel: '取消',
fromLabel: '从',
toLabel: '至',
customRangeLabel: '自定义',
weekLabel: '周',
daysOfWeek: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'],
monthNames: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
firstDay: 1
}
}, (start, end, label) => {
// daterange changed.
}).on('apply.daterangepicker', async (ev, picker) => {
customBtn.attr('disabled', true)
const startTime = picker.startDate.format('YYYY-MM-DD 00:00:00')
const endTime = picker.endDate.format('YYYY-MM-DD 23:59:59')
const summaryList = await getSummary(startTime, endTime)
const text = getTextBySummary(startTime, endTime, summaryList)
copy(text)
toastr.success('复制成功!')
customBtn.attr('disabled', false)
})
// 向前寻找日期,工作日0 周末1 法定节假日2,例:findDate(今天, 0) 从今天开始往前找,返回最近的一个工作日日期。
function findDate (start, type = [0]) {
return new Promise((resolve, reject) => {
const loop = (time) => {
// 节假日万年历API: https://www.mxnzp.com/doc/detail?id=1
GM_xmlhttpRequest({
url: `https://www.mxnzp.com/api/holiday/single/${time}?ignoreHoliday=false&app_id=rkkpflimunbjzeki&app_secret=SHI3cDNEQTRXOHNnYmxiallNeEM3Zz09`,
onload: res => {
const { data, code, msg } = JSON.parse(res.response)
if (code === 1) {
output(`${moment(time).format('YYYY-MM-DD')} 是 ${['工作日', '周末', '法定节假日'][data.type]}`)
// console.log(data)
if (type.includes(data.type)) {
resolve(moment(time))
} else {
loop(moment(time).subtract(1, 'day').format('YYYYMMDD'))
}
} else {
alert(msg)
}
}
})
}
loop(moment(start).format('YYYYMMDD'))
})
}
// 控制台输出
function output (msg) {
console.log(`%c[${moment().format('HH:mm:ss')}'${String(Date.now() % 1000).padStart(3, '0')}] %c${msg}`, 'color: red', 'color: blue')
}
// 加载页面 (v2.2 改为接口请求,然后放到一个隐藏元素内,兼容 gitlab 多版本)
const hideList = $('<div class="content_list_hide" style="display: none;">')
$('#activity').append(hideList)
const pageInfo = {
offset: 0,
limit: 20,
hasNext: true
}
function loadPage () {
return new Promise((resolve, reject) => {
if (!pageInfo.hasNext) {
reject(Error('no more pages.'))
return
}
output('加载页面:' + (pageInfo.offset / pageInfo.limit + 1))
$.ajax({
type: 'GET',
url: $('.content_list').data('href'),
data: `limit=${pageInfo.limit}&offset=${pageInfo.offset}`,
dataType: 'json',
success: (data) => {
// console.log(data)
hideList.html(hideList.html() + data.html)
pageInfo.offset += pageInfo.limit
pageInfo.hasNext = data.count === pageInfo.limit
resolve()
},
error: err => {
reject(err)
}
})
})
}
loadPage() // 加载第一页
// 按日期加载足够的页面
function loadPageUntil (time) {
return new Promise((resolve, reject) => {
// 循环加载页面直到满足指定获取完日期范围的数据
const loop = () => {
const lastEventItemTime = moment($('.content_list_hide .event-item:last time').attr('datetime'))
if (lastEventItemTime < time) {
resolve()
} else {
// 加载完,还有页面就继续加载,没了就退出。
loadPage().then(res => {
loop()
}).catch(() => {
resolve()
})
}
}
loop()
})
}
// 获取汇总列表
async function getSummary (startTime, endTime) {
await loadPageUntil(moment(startTime))
const eventItem = $('.content_list_hide .event-item')
const inScoped = []
// 提取内容
eventItem.each((index, item) => {
const it = $(item)
const time = moment(it.find('time').attr('datetime'))
if (time >= moment(startTime) && time <= moment(endTime)) {
inScoped.push({
time,
project: it.find('.project-name').text(),
branch: it.find('.event-title strong a[title]').text(),
commit: [...it.find('.event_commits .commit')].map(n =>
$(n).text().replace(/^\n\n[0-9a-f]{8}\n·\n\s*(.+)\s*\n\n$/i, '$1')
).filter(n => !/^Merge.*into.*/.test(n))
})
}
})
// 内容归整
const project = []
inScoped.forEach(scope => {
const projectName = scope.project
const itemProject = project.find(n => n.name === projectName)
if (itemProject) {
const itemBranch = itemProject.branch.find(n => n.name === scope.branch)
if (itemBranch) {
itemBranch.commit.push(scope)
} else {
itemProject.branch.push({
name: scope.branch,
commit: [scope]
})
}
} else {
project.push({
name: projectName,
branch: [{
name: scope.branch,
commit: [scope]
}]
})
}
})
// commit 排序后合并成 Array<String> 格式
project.forEach(n => {
n.branch.forEach(m => {
m.commit = m.commit.sort((x, y) => moment(x.time) - moment(y.time))
.map(x => x.commit)
.flat()
})
})
return project
}
// 生成文本汇总信息
function getTextBySummary (startTime, endTime, summaryList) {
const res = [
`周报日期:${startTime.slice(0, 10)} ~ ${endTime.slice(0, 10)}`
]
summaryList.forEach(project => {
res.push(`\n${project.name}`)
project.branch.forEach((branch, index) => {
res.push(`${index ? '\n' : ''}    ${branch.name}`)
branch.commit.forEach(commit => {
res.push(`        ${commit}`)
})
})
})
res.push('\n总结:\n    ')
return res.join('\n')
}
// 拷贝到剪贴板
function copy (text) {
GM_setClipboard(text, 'text')
}
})