一键生成 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')
    }
})

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址