Files
FM-OHS/main.template.js

547 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ==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';
// 当前显示月份的偏移量0=当月周期,正数=往前翻,负数=往后翻)
let currentMonthOffset = 0;
// 加班配置(默认值,启动时尝试从 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 navigateMonth(delta) {
currentMonthOffset += delta;
const range = getCustomMonthRange(currentMonthOffset);
console.log(`navigateMonth offset=${currentMonthOffset}: ${range.firstDay} ~ ${range.lastDay}`);
simulateDateInput('beginDate', range.firstDay);
simulateDateInput('endDate', range.lastDay);
queryButton.click();
setTimeout(() => get_work_time(), QUERY_DELAY_MS);
}
// 根据指定年月查询考勤周期上月21 ~ 本月20
function queryByYearMonth(year, month) {
const formatLocal = (date) => {
return date.toLocaleDateString('en-CA', {
year: 'numeric', month: '2-digit', day: '2-digit'
}).replace(/\//g, '-');
};
// 考勤周期当月21日 至 次月20日
let startMonth = month - 1; // JS month: 0-indexed
let startYear = year;
const firstDay = new Date(startYear, startMonth, 21);
let endYear = year, endMonth = month;
if (endMonth > 11) { endMonth -= 12; endYear++; }
const lastDay = new Date(endYear, endMonth, 20);
// 推算偏移量,让后续 navigateMonth 能正确工作
const now = new Date();
const currentCycleStart = getCustomMonthRange(0).firstDay;
const targetFirst = formatLocal(firstDay);
// 近似:计算目标月与当前月的月份差
const monthDiff = (now.getFullYear() - year) * 12 + (now.getMonth() + 1) - month;
const day = now.getDate();
if (day >= 21) {
currentMonthOffset = monthDiff;
} else {
currentMonthOffset = monthDiff - 1;
}
console.log(`queryByYearMonth ${year}-${month}: ${targetFirst} ~ ${formatLocal(lastDay)}`);
simulateDateInput('beginDate', targetFirst);
simulateDateInput('endDate', formatLocal(lastDay));
queryButton.click();
setTimeout(() => get_work_time(), QUERY_DELAY_MS);
}
// 查询工作时间
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('查当月', () => {
currentMonthOffset = 0;
navigateMonth(0);
});
const buttonNext = createQueryButton('查下月', () => navigateMonth(-1));
const buttonPrev = createQueryButton('查上月', () => navigateMonth(1));
const button2 = createQueryButton('查上上月', () => navigateMonth(2));
const button3 = createQueryButton('查上上上月', () => navigateMonth(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(buttonPrev);
container.appendChild(button2);
container.appendChild(button3);
// 年月选择器
(function createYearMonthSelector() {
const wrapper = document.createElement('span');
wrapper.style.cssText = 'display:inline-flex;align-items:center;gap:4px;margin:0 8px;';
const yearSel = document.createElement('select');
yearSel.id = 'fmohs-year';
const thisYear = new Date().getFullYear();
for (let y = 2024; y <= thisYear + 1; y++) {
const opt = document.createElement('option');
opt.value = y; opt.textContent = y + '年';
if (y === thisYear) opt.selected = true;
yearSel.appendChild(opt);
}
yearSel.style.cssText = 'padding:2px 6px;font-size:12px;border:1px solid #666;border-radius:3px;background:#333;color:#e5e5e5;font-family:inherit;cursor:pointer;';
const monthSel = document.createElement('select');
monthSel.id = 'fmohs-month';
for (let m = 1; m <= 12; m++) {
const opt = document.createElement('option');
opt.value = m; opt.textContent = m + '月';
if (m === new Date().getMonth() + 1) opt.selected = true;
monthSel.appendChild(opt);
}
monthSel.style.cssText = yearSel.style.cssText;
const queryBtn = document.createElement('button');
queryBtn.textContent = '查询';
queryBtn.style.cssText = 'padding:2px 12px;font-size:12px;color:#e5e5e5;background:transparent;border:1px solid #666;border-radius:3px;cursor:pointer;font-family:inherit;transition:color 0.15s,border-color 0.15s;';
queryBtn.addEventListener('mouseenter', () => { queryBtn.style.color = '#fff'; queryBtn.style.borderColor = '#999'; });
queryBtn.addEventListener('mouseleave', () => { queryBtn.style.color = '#e5e5e5'; queryBtn.style.borderColor = '#666'; });
queryBtn.addEventListener('click', () => {
const y = parseInt(yearSel.value);
const m = parseInt(monthSel.value);
queryByYearMonth(y, m);
});
wrapper.appendChild(yearSel);
wrapper.appendChild(monthSel);
wrapper.appendChild(queryBtn);
container.appendChild(wrapper);
})();
console.log("复旦微加班时间统计脚本已加载");
console.log("点击按钮后会统计加班时间");
// 启动时拉取配置(非阻塞)
initConfig();
})()