新增查当周按钮,代码重构与数据分离
- 新增"查当周"按钮,计算当周周一至周日的加班总工时 - 新增 createQueryButton 工厂函数,消除月按钮重复代码 - 修复 calculateTimeDifference 返回字符串导致 NaN 的 bug - 修复 sumOfSecondColumn/get_work_time 缺少空值保护的 bug - 修复 isWorkDay 中 == 改为 === - var 统一为 const/let,提取 QUERY_DELAY_MS 常量 - 提取 getCustomMonthRange 中重复的 formatLocal - 按钮增加 cursor:pointer 样式 - 数据与代码分离:main.template.js(~300行)+ build.py 自动组装 main.js - update.py/comp_json.py 支持命令行年份参数,默认当年 - comp_json.py 增加去重逻辑 - 更新 README 为 GitHub 开源项目风格,含快速上手和每年更新章节 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
109
README.md
109
README.md
@@ -1,23 +1,104 @@
|
|||||||
# FM-OHS
|
# FM-OHS
|
||||||
|
|
||||||
统计复旦微加班工时
|
复旦微加班时间统计 —— 一个运行在金蝶 EAS HR 考勤页面上的 [Tampermonkey][tm] 用户脚本,自动计算并显示加班工时。
|
||||||
|
|
||||||
## 主要功能:
|
[tm]: https://www.tampermonkey.net/
|
||||||
|
|
||||||
- 查询加班工时
|
## 功能
|
||||||
- 下班时间按下午5:20后计算,月计算周期:上月21号~本月20号。
|
|
||||||
- 根据内置的工作日数据库判断是否工作日
|
|
||||||
- 查询结果显示在表格上,简单直观
|
|
||||||
|
|
||||||
### 下载安装
|
- **加班自动计算**:工作日按 17:20 后计算,非工作日(周末/节假日)按全天计算
|
||||||
|
- **内置节假日数据库**:根据工作日数据自动识别调休日、法定节假日
|
||||||
|
- **多维度查询**:支持当月、上月、当周等一键查询
|
||||||
|
- **结果直显**:加班时数直接写入考勤表格,非工作日绿色高亮
|
||||||
|
|
||||||
1. 安装浏览器脚本管理扩展(任选其一)
|
## 快速上手
|
||||||
- [脚本猫](https://docs.scriptcat.org/)
|
|
||||||
- [tampermonkey](https://www.tampermonkey.net/index.php?locale=zh)
|
|
||||||
|
|
||||||
2. 脚本已经上传到[Greasy Fork](https://greasyfork.org/zh-CN) 点击安装即可
|
### 1. 安装脚本管理器
|
||||||
- 脚本链接:[Greasy Fork - 复旦微加班时间统计](https://greasyfork.org/zh-CN/scripts/542984-%E5%A4%8D%E6%97%A6%E5%BE%AE%E5%8A%A0%E7%8F%AD%E6%97%B6%E9%97%B4%E7%BB%9F%E8%AE%A1)
|
|
||||||
|
|
||||||
### 使用方法:
|
任选其一:
|
||||||
|
|
||||||
脚本会在菜单栏新增五个按钮,点击对应的按钮即可实现不同的查询功能。
|
- [Tampermonkey](https://www.tampermonkey.net/index.php?locale=zh)(推荐)
|
||||||
|
- [脚本猫](https://docs.scriptcat.org/)
|
||||||
|
|
||||||
|
### 2. 安装脚本
|
||||||
|
|
||||||
|
前往 [Greasy Fork - 复旦微加班时间统计][gf] 点击安装。
|
||||||
|
|
||||||
|
[gf]: https://greasyfork.org/zh-CN/scripts/542984-%E5%A4%8D%E6%97%A6%E5%BE%AE%E5%8A%A0%E7%8F%AD%E6%97%B6%E9%97%B4%E7%BB%9F%E8%AE%A1
|
||||||
|
|
||||||
|
### 3. 使用
|
||||||
|
|
||||||
|
打开金蝶 EAS HR 考勤页面,菜单栏会自动出现 6 个按钮:
|
||||||
|
|
||||||
|
| 按钮 | 功能 |
|
||||||
|
|------|------|
|
||||||
|
| 统计加班时间 | 直接计算当前表格中的加班工时 |
|
||||||
|
| 查当月 | 查询当月考勤周期(21 日至次月 20 日) |
|
||||||
|
| 查上月 | 查询上月考勤周期 |
|
||||||
|
| 查上上月 | 查询两个月前的考勤周期 |
|
||||||
|
| 查上上上月 | 查询三个月前的考勤周期 |
|
||||||
|
| 查当周 | 查询当周(周一至周日) |
|
||||||
|
|
||||||
|
## 每年更新
|
||||||
|
|
||||||
|
节假日数据每年需要更新一次。API 每天限 5 次调用,因此设计为一年调用一次后持久化。
|
||||||
|
|
||||||
|
### 前置条件
|
||||||
|
|
||||||
|
- Python 3.x
|
||||||
|
- `requests` 库:`pip install requests`
|
||||||
|
|
||||||
|
### 更新步骤
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 抓取新年节假日数据(修改 update.py 中的 year 参数为目标年份)
|
||||||
|
python update.py
|
||||||
|
|
||||||
|
# 2. 合并到数据库并自动构建 main.js
|
||||||
|
python comp_json.py
|
||||||
|
|
||||||
|
# 3. 将生成的 main.js 内容更新到 Greasy Fork 或手动安装到 Tampermonkey
|
||||||
|
```
|
||||||
|
|
||||||
|
`comp_json.py` 会自动将新数据合并到 `data.json`,然后调用 `build.py` 生成包含完整数据的 `main.js`。
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
### 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
FM-OHS/
|
||||||
|
├── main.template.js # 脚本代码模板(~300 行,可读可维护)
|
||||||
|
├── main.js # 构建产物,由 build.py 生成(提交到 Greasy Fork)
|
||||||
|
├── data.json # 节假日数据(唯一数据源)
|
||||||
|
├── build.py # 构建脚本:将 data.json 注入模板 → main.js
|
||||||
|
├── update.py # 从 API 抓取年度节假日数据
|
||||||
|
├── comp_json.py # 合并新数据到 data.json 并自动构建
|
||||||
|
└── holidays_2026.json # 2026 年原始 API 响应数据
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修改代码
|
||||||
|
|
||||||
|
1. 编辑 `main.template.js`(不需要碰 `main.js`)
|
||||||
|
2. 运行 `python build.py` 生成 `main.js`
|
||||||
|
3. 在浏览器中加载 `main.js` 验证
|
||||||
|
|
||||||
|
### 数据格式
|
||||||
|
|
||||||
|
`data.json` 中的每条记录:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"year": 2026,
|
||||||
|
"date": 20260101,
|
||||||
|
"week": 4,
|
||||||
|
"workday": 1,
|
||||||
|
"holiday": 88
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
关键字段:`workday`(1 = 工作日,2 = 非工作日),`date`(YYYYMMDD 格式)。
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|||||||
21
build.py
Normal file
21
build.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
# 读取 data.json
|
||||||
|
with open("data.json", "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
# 读取模板
|
||||||
|
with open("main.template.js", "r", encoding="utf-8") as f:
|
||||||
|
template = f.read()
|
||||||
|
|
||||||
|
# 构造数据块
|
||||||
|
data_block = "const workDayData = " + json.dumps(data, ensure_ascii=False, indent=2) + ";"
|
||||||
|
|
||||||
|
# 替换占位符
|
||||||
|
output = template.replace("/* WORKDAY_DATA_PLACEHOLDER */", data_block)
|
||||||
|
|
||||||
|
# 写出 main.js
|
||||||
|
with open("main.js", "w", encoding="utf-8") as f:
|
||||||
|
f.write(output)
|
||||||
|
|
||||||
|
print(f"main.js 构建完成 ({len(output)} 字符)")
|
||||||
30
comp_json.py
30
comp_json.py
@@ -1,17 +1,33 @@
|
|||||||
import json
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
# 读取 A 和 B 文件
|
# 年份:命令行参数 > 当前年份
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
year = int(sys.argv[1])
|
||||||
|
else:
|
||||||
|
year = datetime.now().year
|
||||||
|
|
||||||
|
# 读取现有数据
|
||||||
with open("data.json", "r", encoding="utf-8") as f:
|
with open("data.json", "r", encoding="utf-8") as f:
|
||||||
a = json.load(f)
|
a = json.load(f)
|
||||||
|
|
||||||
with open("holidays_2026.json", "r", encoding="utf-8") as f:
|
# 读取新年份数据
|
||||||
|
filename = f"holidays_{year}.json"
|
||||||
|
with open(filename, "r", encoding="utf-8") as f:
|
||||||
b = json.load(f)
|
b = json.load(f)
|
||||||
|
|
||||||
# 假设 list 在 data.list 下(根据你之前的接口结构)
|
# 去重:跳过 data.json 中已存在的日期
|
||||||
a["list"].extend(b["data"]["list"])
|
existing_dates = {item["date"] for item in a["list"]}
|
||||||
|
new_items = [item for item in b["data"]["list"] if item["date"] not in existing_dates]
|
||||||
|
a["list"].extend(new_items)
|
||||||
|
|
||||||
# 保存合并结果
|
print(f"合并完成: 新增 {len(new_items)} 条记录,跳过 {len(b['data']['list']) - len(new_items)} 条重复")
|
||||||
with open("merged.json", "w", encoding="utf-8") as f:
|
|
||||||
|
# 保存 data.json
|
||||||
|
with open("data.json", "w", encoding="utf-8") as f:
|
||||||
json.dump(a, f, ensure_ascii=False, indent=2)
|
json.dump(a, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
print("✅ 合并完成!结果已保存到 merged.json")
|
# 自动构建 main.js
|
||||||
|
subprocess.run(["python", "build.py"])
|
||||||
|
|||||||
190
main.js
190
main.js
@@ -14,7 +14,7 @@
|
|||||||
(function() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
// 硬编码工作日数据
|
// 硬编码工作日数据
|
||||||
const workDayData = {
|
const workDayData = {
|
||||||
"list": [
|
"list": [
|
||||||
{
|
{
|
||||||
"year": 2024,
|
"year": 2024,
|
||||||
@@ -21937,7 +21937,7 @@
|
|||||||
"holiday_recess": 1
|
"holiday_recess": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
};
|
||||||
const workDayList = workDayData.list;
|
const workDayList = workDayData.list;
|
||||||
function isWorkDay(weekday, date) {
|
function isWorkDay(weekday, date) {
|
||||||
// 将日期格式化为 YYYYMMDD
|
// 将日期格式化为 YYYYMMDD
|
||||||
@@ -21945,7 +21945,7 @@
|
|||||||
// 查找工作日列表中是否存在该日期
|
// 查找工作日列表中是否存在该日期
|
||||||
const foundItem = workDayList.find(item => String(item.date) === date);
|
const foundItem = workDayList.find(item => String(item.date) === date);
|
||||||
if (!foundItem) {
|
if (!foundItem) {
|
||||||
if (weekday === "周六" || weekday == "周日") {
|
if (weekday === "周六" || weekday === "周日") {
|
||||||
return false; // 周六和周日默认不是工作日
|
return false; // 周六和周日默认不是工作日
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
@@ -21977,7 +21977,8 @@
|
|||||||
const validTimes = parsedTimes.filter(time => time !== null);
|
const validTimes = parsedTimes.filter(time => time !== null);
|
||||||
|
|
||||||
if (validTimes.length === 0) {
|
if (validTimes.length === 0) {
|
||||||
return 'No valid times provided';
|
console.warn(`calculateTimeDifference: 没有有效的时间数据, timeString="${timeString}"`);
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算最大值和最小值
|
// 计算最大值和最小值
|
||||||
@@ -22004,19 +22005,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sumOfSecondColumn(tableId) {
|
function sumOfSecondColumn(tableId) {
|
||||||
var table = document.getElementById(tableId);
|
const table = document.getElementById(tableId);
|
||||||
var totalSum = 0;
|
if (!table) {
|
||||||
var rows = table.getElementsByTagName('tr');
|
console.error(`sumOfSecondColumn: 未找到ID为 "${tableId}" 的表格`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let totalSum = 0;
|
||||||
|
const rows = table.getElementsByTagName('tr');
|
||||||
|
|
||||||
// 遍历每行 从1开始跳过表头
|
// 遍历每行 从1开始跳过表头
|
||||||
for (var i = 1; i < rows.length; i++) {
|
for (let i = 1; i < rows.length; i++) {
|
||||||
var cells = rows[i].getElementsByTagName('td');
|
const cells = rows[i].getElementsByTagName('td');
|
||||||
if (cells.length > 1) { // 确保该行至少有两列
|
if (cells.length > 1) { // 确保该行至少有两列
|
||||||
var cellValue = cells[3].textContent;
|
const cellValue = cells[3].textContent;
|
||||||
var weekday = cells[2].textContent;
|
const weekday = cells[2].textContent;
|
||||||
var date = cells[1].textContent;
|
const date = cells[1].textContent;
|
||||||
var workDay = isWorkDay(weekday, date);
|
const workDay = isWorkDay(weekday, date);
|
||||||
var difference = calculateTimeDifference(cellValue, workDay);
|
const difference = calculateTimeDifference(cellValue, workDay);
|
||||||
totalSum += difference;
|
totalSum += difference;
|
||||||
|
|
||||||
// 修改单元格的值
|
// 修改单元格的值
|
||||||
@@ -22053,10 +22058,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getCustomMonthRange(monthsAgo) {
|
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();
|
const now = new Date();
|
||||||
let year = now.getFullYear();
|
let year = now.getFullYear();
|
||||||
let month = now.getMonth();
|
let month = now.getMonth();
|
||||||
let day = now.getDate();
|
const day = now.getDate();
|
||||||
|
|
||||||
// 如果今天是21号及以后,当前周期是本月21到下月20
|
// 如果今天是21号及以后,当前周期是本月21到下月20
|
||||||
// 否则,周期是上月21到本月20
|
// 否则,周期是上月21到本月20
|
||||||
@@ -22074,13 +22087,6 @@
|
|||||||
}
|
}
|
||||||
const firstDay = new Date(startYear, startMonth, 21);
|
const firstDay = new Date(startYear, startMonth, 21);
|
||||||
const lastDay = new Date(endYear, endMonth, 20);
|
const lastDay = new Date(endYear, endMonth, 20);
|
||||||
const formatLocal = (date) => {
|
|
||||||
return date.toLocaleDateString('en-CA', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit'
|
|
||||||
}).replace(/\//g, '-');
|
|
||||||
};
|
|
||||||
return {
|
return {
|
||||||
firstDay: formatLocal(firstDay),
|
firstDay: formatLocal(firstDay),
|
||||||
lastDay: formatLocal(lastDay)
|
lastDay: formatLocal(lastDay)
|
||||||
@@ -22099,13 +22105,6 @@
|
|||||||
}
|
}
|
||||||
const firstDay = new Date(startYear, startMonth, 21);
|
const firstDay = new Date(startYear, startMonth, 21);
|
||||||
const lastDay = new Date(endYear, endMonth, 20);
|
const lastDay = new Date(endYear, endMonth, 20);
|
||||||
const formatLocal = (date) => {
|
|
||||||
return date.toLocaleDateString('en-CA', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit'
|
|
||||||
}).replace(/\//g, '-');
|
|
||||||
};
|
|
||||||
return {
|
return {
|
||||||
firstDay: formatLocal(firstDay),
|
firstDay: formatLocal(firstDay),
|
||||||
lastDay: formatLocal(lastDay)
|
lastDay: formatLocal(lastDay)
|
||||||
@@ -22113,6 +22112,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QUERY_DELAY_MS = 500;
|
||||||
|
|
||||||
// 创建菜单按钮
|
// 创建菜单按钮
|
||||||
function createMenuButton(text, options = {}) {
|
function createMenuButton(text, options = {}) {
|
||||||
const button = document.createElement('button');
|
const button = document.createElement('button');
|
||||||
@@ -22132,6 +22133,7 @@
|
|||||||
padding: "0px 10px",
|
padding: "0px 10px",
|
||||||
textDecoration: "none",
|
textDecoration: "none",
|
||||||
color: "#e5e5e5",
|
color: "#e5e5e5",
|
||||||
|
cursor: "pointer",
|
||||||
};
|
};
|
||||||
Object.assign(button.style, copiedStyles);
|
Object.assign(button.style, copiedStyles);
|
||||||
|
|
||||||
@@ -22141,81 +22143,78 @@
|
|||||||
}
|
}
|
||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
// 查询工作时间
|
|
||||||
function get_work_time() {
|
// 创建查询按钮(设置日期范围并查询)
|
||||||
const message = sumOfSecondColumn('grid').toFixed(2);
|
function createQueryButton(text, rangeGetter) {
|
||||||
const table = document.querySelector("#gview_grid > div.ui-state-default.ui-jqgrid-hdiv > div > table > thead > tr")
|
return createMenuButton(text, {
|
||||||
const rows = table.getElementsByTagName('th');
|
onClick: () => {
|
||||||
// 修改表头
|
const { firstDay, lastDay } = rangeGetter();
|
||||||
rows[5].querySelector('#jqgh_grid_fillCardTimeStr').textContent = `加班时间(${message}小时)`;
|
console.log(`firstDay: ${firstDay}, lastDay: ${lastDay}`);
|
||||||
|
simulateDateInput('beginDate', firstDay);
|
||||||
|
simulateDateInput('endDate', lastDay);
|
||||||
|
queryButton.click();
|
||||||
|
setTimeout(() => {
|
||||||
|
get_work_time();
|
||||||
|
}, QUERY_DELAY_MS);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var query_button = document.getElementById("query");
|
// 计算当周(周一至周日)的日期范围
|
||||||
// 创建菜单按钮
|
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)}小时)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryButton = document.getElementById("query");
|
||||||
|
|
||||||
const button = createMenuButton("统计加班时间", {
|
const button = createMenuButton("统计加班时间", {
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
get_work_time();
|
get_work_time();
|
||||||
}
|
}
|
||||||
})
|
|
||||||
const button0 = createMenuButton('查当月', {
|
|
||||||
onClick: () => {
|
|
||||||
const month = getCustomMonthRange(0);
|
|
||||||
const firstDay = month.firstDay;
|
|
||||||
const lastDay = month.lastDay;
|
|
||||||
console.log(`firstDay: ${firstDay}, lastDay ${lastDay}`)
|
|
||||||
simulateDateInput('beginDate', firstDay);
|
|
||||||
simulateDateInput('endDate', lastDay);
|
|
||||||
query_button.click();
|
|
||||||
setTimeout(function() {
|
|
||||||
get_work_time();
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const button1 = createMenuButton('查上月', {
|
|
||||||
onClick: () => {
|
|
||||||
const month = getCustomMonthRange(1);
|
|
||||||
const firstDay = month.firstDay;
|
|
||||||
const lastDay = month.lastDay;
|
|
||||||
console.log(`firstDay: ${firstDay}, lastDay ${lastDay}`)
|
|
||||||
simulateDateInput('beginDate', firstDay);
|
|
||||||
simulateDateInput('endDate', lastDay);
|
|
||||||
query_button.click();
|
|
||||||
setTimeout(function() {
|
|
||||||
get_work_time();
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const button2 = createMenuButton('查上上月', {
|
|
||||||
onClick: () => {
|
|
||||||
const month = getCustomMonthRange(2);
|
|
||||||
const firstDay = month.firstDay;
|
|
||||||
const lastDay = month.lastDay;
|
|
||||||
console.log(`firstDay: ${firstDay}, lastDay ${lastDay}`)
|
|
||||||
simulateDateInput('beginDate', firstDay);
|
|
||||||
simulateDateInput('endDate', lastDay);
|
|
||||||
query_button.click();
|
|
||||||
setTimeout(function() {
|
|
||||||
get_work_time();
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const button3 = createMenuButton('查上上上月', {
|
|
||||||
onClick: () => {
|
|
||||||
const month = getCustomMonthRange(3);
|
|
||||||
const firstDay = month.firstDay;
|
|
||||||
const lastDay = month.lastDay;
|
|
||||||
console.log(`firstDay: ${firstDay}, lastDay ${lastDay}`)
|
|
||||||
simulateDateInput('beginDate', firstDay);
|
|
||||||
simulateDateInput('endDate', lastDay);
|
|
||||||
query_button.click();
|
|
||||||
setTimeout(function() {
|
|
||||||
get_work_time();
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
const button0 = createQueryButton('查当月', () => getCustomMonthRange(0));
|
||||||
|
const button1 = createQueryButton('查上月', () => getCustomMonthRange(1));
|
||||||
|
const button2 = createQueryButton('查上上月', () => getCustomMonthRange(2));
|
||||||
|
const button3 = createQueryButton('查上上上月', () => getCustomMonthRange(3));
|
||||||
|
const button4 = createQueryButton('查当周', getCurrentWeekRange);
|
||||||
|
|
||||||
// 获取要将按钮添加到其中的容器
|
// 获取要将按钮添加到其中的容器
|
||||||
var container = document.getElementById('seclevelmenu');
|
let container = document.getElementById('seclevelmenu');
|
||||||
if (!container) {
|
if (!container) {
|
||||||
container = document.body;
|
container = document.body;
|
||||||
}
|
}
|
||||||
@@ -22224,6 +22223,7 @@
|
|||||||
container.appendChild(button1);
|
container.appendChild(button1);
|
||||||
container.appendChild(button2);
|
container.appendChild(button2);
|
||||||
container.appendChild(button3);
|
container.appendChild(button3);
|
||||||
|
container.appendChild(button4);
|
||||||
console.log("复旦微加班时间统计脚本已加载");
|
console.log("复旦微加班时间统计脚本已加载");
|
||||||
console.log("点击按钮后会统计加班时间");
|
console.log("点击按钮后会统计加班时间");
|
||||||
})()
|
})()
|
||||||
306
main.template.js
Normal file
306
main.template.js
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
// ==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);
|
||||||
|
// 工作日从下午5点20分(17:20:00)开始计算加班
|
||||||
|
const fiveTwentyPM = (17 * 3600 + 20 * 60) * 1000;
|
||||||
|
let timeDifferenceInHours;
|
||||||
|
|
||||||
|
// 计算加班时间
|
||||||
|
if (workDay) {
|
||||||
|
// 工作日
|
||||||
|
if (maxTime >= fiveTwentyPM) {
|
||||||
|
timeDifferenceInHours = (maxTime - fiveTwentyPM) / (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;
|
||||||
|
|
||||||
|
// 创建菜单按钮
|
||||||
|
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)}小时)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryButton = document.getElementById("query");
|
||||||
|
|
||||||
|
const button = createMenuButton("统计加班时间", {
|
||||||
|
onClick: () => {
|
||||||
|
get_work_time();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const button0 = createQueryButton('查当月', () => getCustomMonthRange(0));
|
||||||
|
const button1 = createQueryButton('查上月', () => getCustomMonthRange(1));
|
||||||
|
const button2 = createQueryButton('查上上月', () => getCustomMonthRange(2));
|
||||||
|
const button3 = createQueryButton('查上上上月', () => getCustomMonthRange(3));
|
||||||
|
const button4 = createQueryButton('查当周', getCurrentWeekRange);
|
||||||
|
|
||||||
|
// 获取要将按钮添加到其中的容器
|
||||||
|
let container = document.getElementById('seclevelmenu');
|
||||||
|
if (!container) {
|
||||||
|
container = document.body;
|
||||||
|
}
|
||||||
|
container.appendChild(button);
|
||||||
|
container.appendChild(button0);
|
||||||
|
container.appendChild(button1);
|
||||||
|
container.appendChild(button2);
|
||||||
|
container.appendChild(button3);
|
||||||
|
container.appendChild(button4);
|
||||||
|
console.log("复旦微加班时间统计脚本已加载");
|
||||||
|
console.log("点击按钮后会统计加班时间");
|
||||||
|
})()
|
||||||
28
update.py
28
update.py
@@ -1,32 +1,38 @@
|
|||||||
import requests
|
import requests
|
||||||
import json
|
import json
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# 年份:命令行参数 > 当前年份
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
year = int(sys.argv[1])
|
||||||
|
else:
|
||||||
|
year = datetime.now().year
|
||||||
|
|
||||||
# 接口地址(建议加上你的 api_key,否则可能无法访问)
|
|
||||||
url = "https://api.apihubs.cn/holiday/get"
|
url = "https://api.apihubs.cn/holiday/get"
|
||||||
params = {
|
params = {
|
||||||
"year": 2026,
|
"year": year,
|
||||||
"page": 1,
|
"page": 1,
|
||||||
"size": 370
|
"size": 370
|
||||||
# "api_key": "your_api_key_here" # ⚠️ 如果接口坞要求认证,请取消注释并填入你的 key
|
# "api_key": "your_api_key_here" # 如果接口要求认证,请取消注释并填入你的 key
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.get(url, params=params)
|
response = requests.get(url, params=params)
|
||||||
response.raise_for_status() # 检查 HTTP 错误
|
response.raise_for_status()
|
||||||
|
|
||||||
# 解析 JSON 数据
|
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
# 保存到文件
|
filename = f"holidays_{year}.json"
|
||||||
with open("holidays_2026.json", "w", encoding="utf-8") as f:
|
with open(filename, "w", encoding="utf-8") as f:
|
||||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
print("✅ 数据已成功保存到 holidays_2026.json")
|
print(f"数据已成功保存到 {filename}")
|
||||||
|
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
print(f"❌ 网络请求失败: {e}")
|
print(f"网络请求失败: {e}")
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
print("❌ 返回内容不是有效的 JSON 格式")
|
print("返回内容不是有效的 JSON 格式")
|
||||||
print("原始响应:", response.text)
|
print("原始响应:", response.text)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ 发生未知错误: {e}")
|
print(f"发生未知错误: {e}")
|
||||||
|
|||||||
Reference in New Issue
Block a user