一键生成 GitLab 周报汇总

一键生成 GitLab 周报汇总,生成自定义时间段的汇报。

目前為 2022-06-20 提交的版本,檢視 最新版本

// ==UserScript==
// @name         一键生成 GitLab 周报汇总
// @namespace    https://github.com/kiccer
// @version      2.0
// @description  一键生成 GitLab 周报汇总,生成自定义时间段的汇报。
// @author       kiccer<[email protected]>
// @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/clipboard.js/2.0.11/clipboard.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
// ==/UserScript==

/* globals $ GM_addStyle GM_getResourceText GM_xmlhttpRequest moment Pager ClipboardJS toastr */

(function () {
    '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')

    // 一键生成 GitLab 周报汇总
    const copyBtn = $('<a>')
    copyBtn.addClass('btn btn-gray')
    copyBtn.html('一键生成周报')
    copyBtn.appendTo(btnContainer)
    copyBtn.on('click', e => {
        const startTime = moment().subtract(7, 'day').format('YYYY-MM-DD 00:00:00')
        const endTime = moment().format('YYYY-MM-DD 23:59:59')
        const summaryList = 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
        }
    }, async (start, end, label) => {
        await loadPageUntil(start)

        const startTime = start.format('YYYY-MM-DD 00:00:00')
        const endTime = end.format('YYYY-MM-DD 23:59:59')
        const summaryList = getSummary(startTime, endTime)
        const text = getTextBySummary(startTime, endTime, summaryList)

        copy(text)
        toastr.success('复制成功!')
    })

    // 智能生成周报
    const smartBtn = $('<a>')
    smartBtn.addClass('btn btn-gray')
    smartBtn.html('智能生成')
    smartBtn.appendTo(btnContainer)
    smartBtn.on('click', async e => {
        const endTime = moment(await findDate(moment(), [0])).format('YYYY-MM-DD 23:59:59')
        const startTime = moment(await findDate(endTime, [1, 2])).add(1, 'day').format('YYYY-MM-DD 00:00:00')
        const summaryList = getSummary(startTime, endTime)
        const text = getTextBySummary(startTime, endTime, summaryList)

        copy(text)
        toastr.success('复制成功!')
    })

    // 向前寻找日期,工作日0 周末1 法定节假日2,例:findDate(今天, 0) 从今天开始往前找,返回最近的一个工作日日期。
    function findDate (start, type = [0]) {
        return new Promise((resolve, reject) => {
            const loop = (time) => {
                // 节假日万年历API: http://www.free-api.com/doc/562
                GM_xmlhttpRequest({
                    url: `https://www.mxnzp.com/api/holiday/single/${time}?ignoreHoliday=false&app_id=rgihdrm0kslojqvm&app_secret=WnhrK251TWlUUThqaVFWbG5OeGQwdz09`,
                    onload: res => {
                        const { data } = JSON.parse(res.response)
                        output(`${moment(time).format('YYYY-MM-DD')} 是 ${['工作日', '周末', '法定节假日'][data.type]}`)

                        // console.log(data)
                        if (type.includes(data.type)) {
                            resolve(time)
                        } else {
                            loop(moment(time).subtract(1, 'day').format('YYYYMMDD'))
                        }
                    }
                })
            }

            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')
    }

    // 加载页面
    function loadPage () {
        return new Promise((resolve, reject) => {
            const total = $('.event-item').length
            Pager.getOld()
            output('加载页面:' + (Pager.offset / Pager.limit + 1))

            // 轮询,当条数发生改变时视为加载成功
            const loop = () => {
                setTimeout(() => {
                    if ($('.event-item').length > total) {
                        resolve()
                    } else {
                        loop()
                    }
                }, 100)
            }

            loop()
        })
    }

    // 按日期加载足够的页面
    function loadPageUntil (time) {
        return new Promise((resolve, reject) => {
            // 循环加载页面直到满足指定获取完日期范围的数据
            const loop = async () => {
                const lastEventItemTime = moment($('.event-item:last time').attr('datetime'))

                if (lastEventItemTime < time) {
                    resolve()
                } else {
                    await loadPage()
                    loop()
                }
            }

            loop()
        })
    }

    // 多查几页记录
    // setTimeout(() => {
    //     Array(5).fill(loadPage).reduce(async (n, m) => {
    //         await n
    //         return m()
    //     }, Promise.resolve())
    // }, 1000)

    // 获取汇总列表
    function getSummary (startTime, endTime) {
        const eventItem = $('.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 branch/.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) {
        const btn = $('<button>')

        btn.attr('data-text', text)

        // eslint-disable-next-line no-new
        new ClipboardJS(btn[0], {
            text: trigger => trigger.getAttribute('data-text')
        })

        btn.click()
    }
})()

QingJ © 2025

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