- 新增"查下月"按钮(getCustomMonthRange(-1)) - 按钮按逻辑排列:查当下 → 查下月 → 查上月 → 查上上月 → 查上上上月 - 按钮 hover 时颜色变亮 + 底部淡白底线,增加 transition 过渡 - 增大 padding 到 14px,按钮间距更舒适 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
451 lines
17 KiB
JavaScript
451 lines
17 KiB
JavaScript
// ==UserScript==
|
||
// @name 复旦微加班时间统计
|
||
// @namespace http://start.fengbohan.com/
|
||
// @version 2026-05-22
|
||
// @description 更新2026年的节假日数据,新增查当周、MyTools联动
|
||
// @author bhfeng
|
||
// @match http://192.168.36.67:7888/shr/dynamic.do?uipk=com.kingdee.eas.hr.ats.app.WorkCalendarItem*
|
||
// @updateURL https://gitea.fengbohan.com/fengbh/FM-OHS/raw/branch/main/main.js
|
||
// @downloadURL https://gitea.fengbohan.com/fengbh/FM-OHS/raw/branch/main/main.js
|
||
// @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 14px",
|
||
textDecoration: "none",
|
||
color: "#e5e5e5",
|
||
cursor: "pointer",
|
||
border: "none",
|
||
borderBottom: "2px solid transparent",
|
||
transition: "color 0.15s, border-bottom-color 0.15s",
|
||
outline: "none",
|
||
};
|
||
Object.assign(button.style, copiedStyles);
|
||
|
||
button.addEventListener('mouseenter', () => {
|
||
button.style.color = "#fff";
|
||
button.style.borderBottomColor = "rgba(255,255,255,0.4)";
|
||
});
|
||
button.addEventListener('mouseleave', () => {
|
||
button.style.color = "#e5e5e5";
|
||
button.style.borderBottomColor = "transparent";
|
||
});
|
||
|
||
// 处理点击事件回调
|
||
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 buttonNext = createQueryButton('查下月', () => getCustomMonthRange(-1));
|
||
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(buttonNext);
|
||
container.appendChild(button1);
|
||
container.appendChild(button2);
|
||
container.appendChild(button3);
|
||
console.log("复旦微加班时间统计脚本已加载");
|
||
console.log("点击按钮后会统计加班时间");
|
||
|
||
// 启动时拉取配置(非阻塞)
|
||
initConfig();
|
||
})() |