H3C剩余工时计算

收集H3C内部考勤日历页面的打卡数据并计算剩余工时

// ==UserScript==
// @name         H3C剩余工时计算
// @namespace    H3C剩余工时计算
// @version      2025-01-02
// @description  收集H3C内部考勤日历页面的打卡数据并计算剩余工时
// @author       H3Cer
// @match        https://eip.h3c.com/myCenter/kaoqin
// @grant        none
// ==/UserScript==
// 说明:
// 1、 使用前请将上下班时间、午休时间、是否是弹性工作制等等内容修改为自己的班次内容(下方两行/****/中间的内容)
// 2、 为了适应2025年开始实施的新考勤制度,特意将非工作日的打卡也统计在内(但是当天有加班记录的不统计)
// 3、 工作日只统计早晚剩余工时(工作日包括正常工作日和调休后上班的周末),非工作日全天统计(但是不包括午休时间)
// 4、 脚本考虑到的各种异常情况可能不太全面,如果发生统计错误或者无法统计的情况,请谅解(尤其是存在考勤异常时,暂时忘了考勤异常时是如何显示的了)

(function() {
    'use strict';
    let isInsertDiv = false;
    // 累计总分钟数
    let totalMinutesWorkOvertime = 0;

    // 辅助函数:将时间字符串转换为分钟数
    const timeToMinutes = (timeStr) => {
        const [hours, minutes] = timeStr.split(':').map(Number);
        return isNaN(hours) || isNaN(minutes) ? 0 : hours * 60 + minutes; // 防止出现 NaN
    };

    /************************************************************************************************/
    const delayInterval = 5000; // 延时运行时间,打开考勤日历页面后延时一段时间再进行统计(毫秒),避免页面加载未完成导致显示块插入位置不正确
    const scanInterval = 1000; // 扫描更新间隔,开始统计之后,以此时间为间隔,持续进行扫描、统计(毫秒)
    const morningLimit = timeToMinutes('09:30'); //上班时间(有弹性工作制的,此处请填写弹性上班时间)
    const eveningLimit = timeToMinutes('18:00'); //下班时间
    const flexibleTime = 60; //弹性工作制时间(分钟),非弹性工作制的写0
    const siestaLimit1 = timeToMinutes('12:00'); //午休开始时间
    const siestaLimit2 = timeToMinutes('13:30'); //午休结束时间
    /************************************************************************************************/

    // 在页面顶部创建一个显示总分钟数和总小时数的 div// 在页面顶部插入显示总分钟数和总小时数的 div,插入在 .ant-fullcalendar-fullscreen 上方
    const createDisplayElement = () => {
        // 查找 .ant-fullcalendar-fullscreen 元素
        const fullscreenElement = document.querySelector('.ant-fullcalendar-fullscreen');

        // 如果找到了该元素,则创建并插入新的显示 div
        if (fullscreenElement) {
            const displayDiv = document.createElement('div');
            displayDiv.id = 'total-time-display';
            displayDiv.style.backgroundColor = '#f0f0f0';
            displayDiv.style.padding = '16px';
            displayDiv.style.textAlign = 'left';
            displayDiv.style.zIndex = '1000';
            displayDiv.style.fontSize = '16px';
            displayDiv.style.fontWeight = 'bold';
            displayDiv.style.color = '#333';

            // 初始化文字为 "剩余工时: "
            displayDiv.textContent = "剩余工时: ";

            // 将新元素插入到 .ant-fullcalendar-fullscreen 上方
            fullscreenElement.parentNode.insertBefore(displayDiv, fullscreenElement);
        }
    };

    // 更新显示的总分钟数和总小时数
    const updateDisplay = (totalMinutesWorkOvertime, totalHours) => {
        const displayDiv = document.getElementById('total-time-display');
        console.log(`displayDiv: ${displayDiv}`);
        if (displayDiv) {
            displayDiv.textContent = `剩余工时: ${totalHours} 小时( ${totalMinutesWorkOvertime} 分钟 )`;
        } else {
            console.log("无法找到指定displayDiv");
        }
    };

    const scanElements = () => {
        try {
            console.clear(); // 清空控制台

            totalMinutesWorkOvertime = 0;

            // 查找 .ant-calendar-picker 元素(用于获取当前月份,排除非本月的日期)
            const element1 = document.querySelector('.ant-calendar-picker');
            if (element1) {
                const textContent = element1.textContent.trim();
                // console.log('找到的文字内容:', textContent);

                // 查找所有 .ant-fullcalendar-cell 元素(此元素就是每一天的信息)
                const elements2 = document.querySelectorAll('.ant-fullcalendar-cell');
                console.log(`共找到 ${elements2.length} 个元素:`);
                elements2.forEach((element2, index) => {
                    if (element2.title.includes(textContent)) {

                        let currentDate = 0;

                        // 查找 element2 内部的 class="ant-fullcalendar-value" 元素(确定当天是哪一天(1~31的天数序号))
                        const currentDateElements = element2.querySelector('.ant-fullcalendar-value');
                        if (currentDateElements) {
                            const tmpDate = currentDateElements.textContent.trim()
                            if (!isNaN(currentDate)) {
                                currentDate = tmpDate;
                            }
                        }


                        // 检查 element2 的子元素是否包含 "加班"
                        const childElements = element2.querySelectorAll('*');
                        const containsOvertime = Array.from(childElements).some(child =>
                            child.textContent.trim().includes('加班')
                        );

                        if (containsOvertime) {
                            console.log(`本月 ${currentDate} 日内容包含 "加班",不统计`);
                            return; // 跳过当前元素的处理
                        }

                        // 查找 element2 内部的 class="absolute" 的子元素(打卡时间,格式是:上班打开时间(时:分) - 下班打卡时间(时:分))
                        const absoluteElement = element2.querySelector('.absolute');
                        if (absoluteElement) {
                            const timeRange = absoluteElement.textContent.trim();

                            // 如果 .absolute 元素为空,则跳过
                            if (!timeRange) {
                                //console.log(`本月 ${currentDate} 日内容为空,不统计`);
                                return;
                            }

                            //console.log(`第 ${index + 1} 个元素中的 .absolute 值: ${timeRange}`);

                            let minutesWorkOvertimeBefore = 0;
                            let minutesWorkOvertimeAfter = 0;

                            // 检查是否是节假日
                            const isHoliday = element2.querySelector('.ant-fullcalendar-holiday');
                            // const isHoliday = false //debug使用
                            if (isHoliday) {
                                //如果是节假日,则全天统计,并且不考虑弹性工作制(但是要减去午休时间);

                                const [startTimeTmp, endTimeTmp] = timeRange.split(' - ').map(timeToMinutes);
                                const startTime = isNaN(startTimeTmp) ? siestaLimit1 : startTimeTmp; // 防止出现 NaN
                                const endTime = isNaN(endTimeTmp) ? siestaLimit2 : endTimeTmp; // 防止出现 NaN

                                if (siestaLimit1 > endTime) { // 如果整个工作时间段都在上午
                                    // 计算上午的剩余工时
                                    minutesWorkOvertimeBefore = endTime - startTime;

                                    // 计算下午的剩余工时
                                    minutesWorkOvertimeAfter = 0;
                                } else if (startTime > siestaLimit2) { // 如果整个工作时间段都在下午
                                    // 计算上午的剩余工时
                                    minutesWorkOvertimeBefore = 0;

                                    // 计算下午的剩余工时
                                    minutesWorkOvertimeAfter = endTime - startTime;
                                } else {
                                    // 计算上午的剩余工时
                                    minutesWorkOvertimeBefore = Math.max(0, siestaLimit1 - startTime);

                                    // 计算下午的剩余工时
                                    minutesWorkOvertimeAfter = Math.max(0, endTime - siestaLimit2);
                                }
                            } else {
                                const [startTimeTmp, endTimeTmp] = timeRange.split(' - ').map(timeToMinutes);
                                const startTime = isNaN(startTimeTmp) ? morningLimit : startTimeTmp; // 防止出现 NaN
                                const endTime = isNaN(endTimeTmp) ? eveningLimit : endTimeTmp; // 防止出现 NaN

                                // 计算上午的剩余工时
                                minutesWorkOvertimeBefore = Math.max(0, morningLimit - startTime) - flexibleTime;

                                // 计算下午的剩余工时
                                minutesWorkOvertimeAfter = Math.max(0, endTime - eveningLimit);

                            }

                            //console.log(`本月 ${currentDate} 日早于 09:30 的分钟数: ${minutesWorkOvertimeBefore}`);
                            //console.log(`本月 ${currentDate} 日晚于 18:00 的分钟数: ${minutesWorkOvertimeAfter}`);
                            const minutesCurrentDate = minutesWorkOvertimeBefore + minutesWorkOvertimeAfter;
                            console.log(`本月 ${currentDate} 日剩余工时: ${minutesCurrentDate}`);

                            // 求和
                            totalMinutesWorkOvertime += minutesWorkOvertimeBefore;
                            totalMinutesWorkOvertime += minutesWorkOvertimeAfter;
                        }
                    }
                });

                // 输出累计结果
                console.log(`总分钟数: ${totalMinutesWorkOvertime}`);

                // 计算小时数
                const totalHours = ((totalMinutesWorkOvertime) / 60).toFixed(2);
                console.log(`总小时数: ${totalHours}`);

                // 确认显示元素是否已经增加
                console.log(`isInsertDiv: ${isInsertDiv}`);
                if (!isInsertDiv) {
                    // 初始化创建显示元素
                    createDisplayElement();
                    isInsertDiv = true;
                }

                // 更新显示的总分钟数和总小时数
                updateDisplay(totalMinutesWorkOvertime, totalHours);

            } else {
                // console.log('未找到 .ant-calendar-picker 元素');
            }
        } catch (error) {
            // 什么都不做,直接忽略异常
        }
    };

    //延时运行
    setTimeout(() => {
        scanElements(); //最开始运行一次
        setInterval(scanElements, scanInterval); // 设置定时器,循环运行
    }, delayInterval);

})();