// ==UserScript== // @name 复旦微加班时间统计 // @namespace http://start.fengbohan.com/ // @version 2026-01-28 // @description 更新2026年的节假日数据 // @author bhfeng // @match http://192.168.36.67:7888/shr/dynamic.do?uipk=com.kingdee.eas.hr.ats.app.WorkCalendarItem* // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAAdVBMVEX///////7+/v4kVZ729/lafbO2xd3x8/f5+vz8/Pzd4+4tW6GSp8xnh7nP2OgxXqLK1OZ1kb/s7/UgUZytvdhKcKw+aKgaTZlhgrbU3eqar9Dm6fFVd7CEnsZwjb4VSZfAy+BCbKq3xd58l8OXrM6nudaMo8kyjkGlAAABfklEQVQokT1Si5arIBCLDIqCiogP8FHtWvv/n3iH7vZyDjDMQAIJwLdlpVIlz0L8T0FAxDaTy5JBrWn52ZhxoO6tQ+ZyjT7fJSqRpYJAJte8qbDPmxKPY4vgXOqdhramwzp6B1GTKRJahjjVVdlQI3FauiTahkOmFfHyJ9qJduC2lHf4o69weiZYyLyAYvLWFe3O1apHa80AsZOpGfdhZrLHDfSDwpumFuVO5HqI8xot3xvrM5Qy+DGiqo3fTgbvteRRT36AHmlk2J/R26vlbHmuiMVrWtOlpoGPX2a2wV3BMjQiNSXakezVA50zfvbHyGTMQlcFnRNtRQXEl3u4lklWVRi6FNRuyTRD/5E7sXehvIlCBM6c3zBed3E/dNKqCdlg/bRIlENj/XHMxztphdUGxNxTKNhAPbz3/edjlEC0uRTLc6b81syu5K8fPOhxKtAvm5/NMw/vNRNfy1U9hUKpbnFNsyh8v0Pa0A/O1SyE+LrxV6mSdjH2gsPf/D//7BjTQny+YwAAAABJRU5ErkJggg== // @grant none // @run-at document-idle // @license MI // ==/UserScript== (function() { 'use strict'; // 硬编码工作日数据 /* WORKDAY_DATA_PLACEHOLDER */ const workDayList = workDayData.list; function isWorkDay(weekday, date) { // 将日期格式化为 YYYYMMDD date = date.replace(/-/g, ''); // 查找工作日列表中是否存在该日期 const foundItem = workDayList.find(item => String(item.date) === date); if (!foundItem) { if (weekday === "周六" || weekday === "周日") { return false; // 周六和周日默认不是工作日 } else{ return true; // 默认工作日是周一到周五 } } else if (foundItem.workday === 1) { return true; // 情况1:工作日 } else { return false; // 情况2:非工作日 } } function calculateTimeDifference(timeString, workDay) { // 判断字符串是否为 "--" if (timeString === '--') { return 0; } // 按逗号分割字符串 const times = timeString.split(','); // 解析时间并转换为毫秒 const parsedTimes = times.map(timeStr => { const [hours, minutes, seconds] = timeStr.trim().split(':').map(Number); return isNaN(hours) || isNaN(minutes) || isNaN(seconds) ? null : (hours * 3600 + minutes * 60 + seconds) * 1000; }); // 过滤掉无效的时间 const validTimes = parsedTimes.filter(time => time !== null); if (validTimes.length === 0) { console.warn(`calculateTimeDifference: 没有有效的时间数据, timeString="${timeString}"`); return 0; } // 计算最大值和最小值 const maxTime = Math.max(...validTimes); const minTime = Math.min(...validTimes); // 工作日加班起算时间(从配置获取) const overtimeStartMs = parseOvertimeStart(overtimeConfig.overtime_start); let timeDifferenceInHours; // 计算加班时间 if (workDay) { // 工作日 if (maxTime >= overtimeStartMs) { timeDifferenceInHours = (maxTime - overtimeStartMs) / (1000 * 60 * 60); } else { timeDifferenceInHours = 0; } } else { // 非工作日 timeDifferenceInHours = (maxTime - minTime) / (1000 * 60 * 60); } return timeDifferenceInHours; } function sumOfSecondColumn(tableId) { const table = document.getElementById(tableId); if (!table) { console.error(`sumOfSecondColumn: 未找到ID为 "${tableId}" 的表格`); return 0; } let totalSum = 0; const rows = table.getElementsByTagName('tr'); // 遍历每行 从1开始跳过表头 for (let i = 1; i < rows.length; i++) { const cells = rows[i].getElementsByTagName('td'); if (cells.length > 1) { // 确保该行至少有两列 const cellValue = cells[3].textContent; const weekday = cells[2].textContent; const date = cells[1].textContent; const workDay = isWorkDay(weekday, date); const difference = calculateTimeDifference(cellValue, workDay); totalSum += difference; // 修改单元格的值 cells[5].title = ""; cells[5].textContent = `${difference.toFixed(2)}H`; if (!workDay){ cells[5].style.backgroundColor = "#90EE90"; } } } return totalSum; } // 方便快速查询前一月、前二月、前三月 function simulateDateInput(elementId, dateValue) { const inputElement = document.getElementById(elementId); if (!inputElement) { console.error(`未找到ID为 ${elementId} 的输入框`); return; } inputElement.focus(); inputElement.value = dateValue; inputElement.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); inputElement.dispatchEvent(new Event('change', { bubbles: true, cancelable: true })); inputElement.blur(); } function getCustomMonthRange(monthsAgo) { const formatLocal = (date) => { return date.toLocaleDateString('en-CA', { year: 'numeric', month: '2-digit', day: '2-digit' }).replace(/\//g, '-'); }; const now = new Date(); let year = now.getFullYear(); let month = now.getMonth(); const day = now.getDate(); // 如果今天是21号及以后,当前周期是本月21到下月20 // 否则,周期是上月21到本月20 if (day >= 21) { month = month - monthsAgo; let startYear = year, startMonth = month; while (startMonth < 0) { startMonth += 12; startYear--; } let endYear = startYear, endMonth = startMonth + 1; if (endMonth > 11) { endMonth -= 12; endYear++; } const firstDay = new Date(startYear, startMonth, 21); const lastDay = new Date(endYear, endMonth, 20); return { firstDay: formatLocal(firstDay), lastDay: formatLocal(lastDay) }; } else { month = month - monthsAgo - 1; let startYear = year, startMonth = month; while (startMonth < 0) { startMonth += 12; startYear--; } let endYear = startYear, endMonth = startMonth + 1; if (endMonth > 11) { endMonth -= 12; endYear++; } const firstDay = new Date(startYear, startMonth, 21); const lastDay = new Date(endYear, endMonth, 20); return { firstDay: formatLocal(firstDay), lastDay: formatLocal(lastDay) }; } } const QUERY_DELAY_MS = 500; const MYTOOLS_BASE = 'https://mytools.fengbohan.com'; // 加班配置(默认值,启动时尝试从 MyTools 拉取) let overtimeConfig = { overtime_start: '17:20', weekly_target: 18 }; // 上次计算的结果缓存(用于保存到 MyTools) let lastOvertimeRecords = []; async function initConfig() { try { const resp = await fetch(`${MYTOOLS_BASE}/api/overtime-config`, { signal: AbortSignal.timeout(2000) }); if (resp.ok) { const data = await resp.json(); overtimeConfig = data; console.log('FM-OHS: 已从 MyTools 拉取配置', overtimeConfig); } } catch (e) { console.log('FM-OHS: MyTools 不可用,使用默认配置'); } } // 解析加班起算时间为秒数 function parseOvertimeStart(timeStr) { const [h, m] = timeStr.split(':').map(Number); return (h * 3600 + m * 60) * 1000; } // 收集表格数据(不修改 DOM,返回记录数组) function collectOvertimeRecords() { const table = document.getElementById('grid'); if (!table) return []; const rows = table.getElementsByTagName('tr'); const records = []; for (let i = 1; i < rows.length; i++) { const cells = rows[i].getElementsByTagName('td'); if (cells.length > 1) { const timeStr = cells[3].textContent; const weekday = cells[2].textContent; const date = cells[1].textContent; const workDay = isWorkDay(weekday, date); const hours = calculateTimeDifference(timeStr, workDay); records.push({ date, weekday, hours: parseFloat(hours.toFixed(2)), workday: workDay }); } } return records; } // Toast 提示 function showToast(message, type) { const toast = document.createElement('div'); toast.textContent = message; Object.assign(toast.style, { position: 'fixed', top: '20px', right: '20px', zIndex: '99999', padding: '12px 20px', borderRadius: '6px', fontSize: '14px', fontFamily: '"Microsoft Yahei", sans-serif', color: '#fff', backgroundColor: type === 'success' ? '#28a745' : '#dc3545', boxShadow: '0 4px 12px rgba(0,0,0,0.2)', transition: 'opacity 0.3s', }); document.body.appendChild(toast); setTimeout(() => { toast.style.opacity = '0'; }, 2500); setTimeout(() => { toast.remove(); }, 3000); } // 保存加班记录到 MyTools async function saveToMyTools() { if (lastOvertimeRecords.length === 0) { showToast('没有可保存的加班记录', 'error'); return; } const total = lastOvertimeRecords.reduce((s, r) => s + r.hours, 0); const weekLabel = (() => { const d = new Date(); const iso = (d2 => { const start = new Date(d2.getFullYear(), 0, 1); const days = Math.floor((d2 - start) / 86400000); return `${d2.getFullYear()}-W${String(Math.ceil((days + start.getDay() + 1) / 7)).padStart(2, '0')}`; })(d); return iso; })(); try { const resp = await fetch(`${MYTOOLS_BASE}/api/overtime-records`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ week: weekLabel, records: lastOvertimeRecords, total: parseFloat(total.toFixed(2)) }), signal: AbortSignal.timeout(3000), }); if (resp.ok) { showToast(`已保存 ${lastOvertimeRecords.length} 条记录到 MyTools`, 'success'); } else { showToast('保存失败: ' + resp.status, 'error'); } } catch (e) { showToast('MyTools 不可用,请确认服务已启动', 'error'); } } // 创建保存按钮(浮动在表格上方) function showSaveButton() { // 移除旧按钮 const old = document.getElementById('fmohs-save-btn'); if (old) old.remove(); const btn = document.createElement('button'); btn.id = 'fmohs-save-btn'; btn.textContent = '保存到 MyTools'; Object.assign(btn.style, { margin: '8px 4px', padding: '6px 16px', fontSize: '13px', backgroundColor: '#1f6feb', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer', fontFamily: '"Microsoft Yahei", sans-serif', }); btn.addEventListener('click', saveToMyTools); // 插入到 grid 表格上方 const gridView = document.getElementById('gview_grid'); if (gridView) { gridView.parentNode.insertBefore(btn, gridView); } } // 创建菜单按钮 function createMenuButton(text, options = {}) { const button = document.createElement('button'); button.textContent = text; const copiedStyles = { webkitTapHighlightColor: "rgba(0, 0, 0, 0)", fontFamily: '"Helvetica Neue", Helvetica, Arial, "Microsoft Yahei", "Hiragino Sans GB", "Heiti SC", "WenQuanYi Micro Hei", sans-serif', fontSize: "14px", listStyle: "none", textAlign: "center", boxSizing: "border-box", webkitTextSizeAdjust: "100%", backgroundColor: "transparent", display: "block", lineHeight: "60px", height: "60px", padding: "0px 10px", textDecoration: "none", color: "#e5e5e5", cursor: "pointer", }; Object.assign(button.style, copiedStyles); // 处理点击事件回调 if (typeof options.onClick === 'function') { button.addEventListener('click', options.onClick); } return button; } // 创建查询按钮(设置日期范围并查询) function createQueryButton(text, rangeGetter) { return createMenuButton(text, { onClick: () => { const { firstDay, lastDay } = rangeGetter(); console.log(`firstDay: ${firstDay}, lastDay: ${lastDay}`); simulateDateInput('beginDate', firstDay); simulateDateInput('endDate', lastDay); queryButton.click(); setTimeout(() => { get_work_time(); }, QUERY_DELAY_MS); } }); } // 计算当周(周一至周日)的日期范围 function getCurrentWeekRange() { const now = new Date(); const dayOfWeek = now.getDay(); // 0=周日, 1=周一, ..., 6=周六 const mondayOffset = dayOfWeek === 0 ? -6 : 1 - dayOfWeek; const monday = new Date(now); monday.setDate(now.getDate() + mondayOffset); const sunday = new Date(monday); sunday.setDate(monday.getDate() + 6); const formatLocal = (date) => { return date.toLocaleDateString('en-CA', { year: 'numeric', month: '2-digit', day: '2-digit' }).replace(/\//g, '-'); }; return { firstDay: formatLocal(monday), lastDay: formatLocal(sunday) }; } // 查询工作时间 function get_work_time() { const totalSum = sumOfSecondColumn('grid'); const headerTable = document.querySelector("#gview_grid > div.ui-state-default.ui-jqgrid-hdiv > div > table > thead > tr"); if (!headerTable) { console.error('get_work_time: 未找到表头元素'); return; } const rows = headerTable.getElementsByTagName('th'); const titleCell = rows[5].querySelector('#jqgh_grid_fillCardTimeStr'); if (!titleCell) { console.error('get_work_time: 未找到表头加班列元素'); return; } titleCell.textContent = `加班时间(${totalSum.toFixed(2)}小时)`; // 收集记录并显示保存按钮 lastOvertimeRecords = collectOvertimeRecords(); showSaveButton(); } const queryButton = document.getElementById("query"); const button = createMenuButton("统计加班时间", { onClick: () => { get_work_time(); } }); const button4 = createQueryButton('查当周', getCurrentWeekRange); const button0 = createQueryButton('查当月', () => getCustomMonthRange(0)); const button1 = createQueryButton('查上月', () => getCustomMonthRange(1)); const button2 = createQueryButton('查上上月', () => getCustomMonthRange(2)); const button3 = createQueryButton('查上上上月', () => getCustomMonthRange(3)); // 获取要将按钮添加到其中的容器 let container = document.getElementById('seclevelmenu'); if (!container) { container = document.body; } container.appendChild(button); container.appendChild(button4); container.appendChild(button0); container.appendChild(button1); container.appendChild(button2); container.appendChild(button3); console.log("复旦微加班时间统计脚本已加载"); console.log("点击按钮后会统计加班时间"); // 启动时拉取配置(非阻塞) initConfig(); })()