From 2d455ddb20c190eb7af8b6bff3de9a9ad8bf8cdb Mon Sep 17 00:00:00 2001 From: fengbh <1953356163@qq.com> Date: Fri, 22 May 2026 20:04:00 +0800 Subject: [PATCH] =?UTF-8?q?=E9=9B=86=E6=88=90=20MyTools=EF=BC=9A=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E6=8B=89=E5=8F=96=E9=85=8D=E7=BD=AE=E3=80=81=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E5=8A=A0=E7=8F=AD=E8=B5=B7=E7=AE=97=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E3=80=81=E4=BF=9D=E5=AD=98=E5=8A=A0=E7=8F=AD=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 启动时从 MyTools 拉取加班起算时间配置(1s超时fallback默认17:20) - calculateTimeDifference 改用动态 overtimeStart 替代硬编码 17:20 - 计算完成后显示"保存到 MyTools"按钮,POST 记录到 API - 支持 upsert(同日期覆盖),Toast 提示成功/失败 - MyTools 不可用时所有功能独立正常运行 Co-Authored-By: Claude Opus 4.7 --- holidays_2026.json | 8 +-- main.js | 136 +++++++++++++++++++++++++++++++++++++++++++-- main.template.js | 136 +++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 268 insertions(+), 12 deletions(-) diff --git a/holidays_2026.json b/holidays_2026.json index a5ed819..224b600 100644 --- a/holidays_2026.json +++ b/holidays_2026.json @@ -5419,8 +5419,8 @@ "holiday": 44, "holiday_or": 45, "holiday_overtime": 10, - "holiday_today": 2, - "holiday_legal": 2, + "holiday_today": 1, + "holiday_legal": 1, "holiday_recess": 1 }, { @@ -5439,8 +5439,8 @@ "holiday": 44, "holiday_or": 44, "holiday_overtime": 10, - "holiday_today": 1, - "holiday_legal": 1, + "holiday_today": 2, + "holiday_legal": 2, "holiday_recess": 1 }, { diff --git a/main.js b/main.js index ae44f84..e4b780e 100644 --- a/main.js +++ b/main.js @@ -21984,15 +21984,15 @@ const workDayData = { // 计算最大值和最小值 const maxTime = Math.max(...validTimes); const minTime = Math.min(...validTimes); - // 工作日从下午5点20分(17:20:00)开始计算加班 - const fiveTwentyPM = (17 * 3600 + 20 * 60) * 1000; + // 工作日加班起算时间(从配置获取) + const overtimeStartMs = parseOvertimeStart(overtimeConfig.overtime_start); let timeDifferenceInHours; // 计算加班时间 if (workDay) { // 工作日 - if (maxTime >= fiveTwentyPM) { - timeDifferenceInHours = (maxTime - fiveTwentyPM) / (1000 * 60 * 60); + if (maxTime >= overtimeStartMs) { + timeDifferenceInHours = (maxTime - overtimeStartMs) / (1000 * 60 * 60); } else { timeDifferenceInHours = 0; } @@ -22113,6 +22113,127 @@ const workDayData = { } 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 = {}) { @@ -22198,6 +22319,10 @@ const workDayData = { return; } titleCell.textContent = `加班时间(${totalSum.toFixed(2)}小时)`; + + // 收集记录并显示保存按钮 + lastOvertimeRecords = collectOvertimeRecords(); + showSaveButton(); } const queryButton = document.getElementById("query"); @@ -22226,4 +22351,7 @@ const workDayData = { container.appendChild(button3); console.log("复旦微加班时间统计脚本已加载"); console.log("点击按钮后会统计加班时间"); + + // 启动时拉取配置(非阻塞) + initConfig(); })() \ No newline at end of file diff --git a/main.template.js b/main.template.js index 1d60ec6..1fa63d4 100644 --- a/main.template.js +++ b/main.template.js @@ -61,15 +61,15 @@ // 计算最大值和最小值 const maxTime = Math.max(...validTimes); const minTime = Math.min(...validTimes); - // 工作日从下午5点20分(17:20:00)开始计算加班 - const fiveTwentyPM = (17 * 3600 + 20 * 60) * 1000; + // 工作日加班起算时间(从配置获取) + const overtimeStartMs = parseOvertimeStart(overtimeConfig.overtime_start); let timeDifferenceInHours; // 计算加班时间 if (workDay) { // 工作日 - if (maxTime >= fiveTwentyPM) { - timeDifferenceInHours = (maxTime - fiveTwentyPM) / (1000 * 60 * 60); + if (maxTime >= overtimeStartMs) { + timeDifferenceInHours = (maxTime - overtimeStartMs) / (1000 * 60 * 60); } else { timeDifferenceInHours = 0; } @@ -190,6 +190,127 @@ } 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 = {}) { @@ -275,6 +396,10 @@ return; } titleCell.textContent = `加班时间(${totalSum.toFixed(2)}小时)`; + + // 收集记录并显示保存按钮 + lastOvertimeRecords = collectOvertimeRecords(); + showSaveButton(); } const queryButton = document.getElementById("query"); @@ -303,4 +428,7 @@ container.appendChild(button3); console.log("复旦微加班时间统计脚本已加载"); console.log("点击按钮后会统计加班时间"); + + // 启动时拉取配置(非阻塞) + initConfig(); })() \ No newline at end of file