// ==UserScript== // @name 再不摸鱼就下班了 // @namespace https://github.com/Yangshengzhou03 // @version 0.0.1 // @description 实时追踪工作时间,精准倒计时下班;动态计算今日薪资收入,让每一秒的摸鱼都心中有数。支持自定义时薪、弹性工作制,你的专属职场回血插件。 // @author Yangshengzhou // @license MIT // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @icon https://acgnhome.com/wp-content/themes/nav/images/favicon.png // @run-at document-idle // @compatible chrome 80+ // @compatible firefox 75+ // ==/UserScript== (function () { 'use strict'; const C = { HOUR: 3600000, MIN: 60000, DAY: 86400000, SEC: 1000 }; const DEF = { workStartTime: '09:00', workEndTime: '18:00', dailyWage: 350, payday: 10, workdays: [1, 2, 3, 4, 5], flexibleWork: false, dailyWorkHours: 8, shortcut: 'alt+ctrl', lang: 'zh', visible: true }; const I18N = { zh: { title: '再不摸鱼就下班了 © 2026 Yang Shengzhou.', earnings: '今日赚了', payday: '发薪', weekend: '周末', offWork: '恭喜上岸', restDay: '休息日', days: '天', settings: '设置', workStart: '上班时间', workEnd: '下班时间', dailyWage: '每天工资', paydayDate: '发薪日', flexible: '弹性工作制', workHours: '工作时长', workdays: '工作日', shortcut: '快捷键', save: '保存设置', lang: '语言', saved: '设置已保存', saveFailed: '保存失败', yuan: '¥', day: '日', hour: '小时', weekDays: ['一', '二', '三', '四', '五', '六', '日'], tooltip: { countdown: '距离下班还有', earnings: '今日已赚', payday: '距离发薪', weekend: '距离周末', drag: '拖动移动,点击设置' } }, en: { title: 'FishTime © 2026 Yang Shengzhou.', earnings: "Today's Earnings", payday: 'Payday', weekend: 'Weekend', offWork: 'Off Work', restDay: 'Rest Day', days: 'days', settings: 'Settings', workStart: 'Start Time', workEnd: 'End Time', dailyWage: 'Daily Wage', paydayDate: 'Payday', flexible: 'Flexible Hours', workHours: 'Work Hours', workdays: 'Workdays', shortcut: 'Shortcut', save: 'Save', lang: 'Language', saved: 'Settings Saved', saveFailed: 'Save Failed', yuan: 'CNY', day: 'day', hour: 'hours', weekDays: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], tooltip: { countdown: 'Time until off work', earnings: 'Earned today', payday: 'Days until payday', weekend: 'Days until weekend', drag: 'Drag to move, click for settings' } } }; let cfg = DEF, els = {}, currentEarnings = 0, targetEarnings = 0; const t = k => I18N[cfg.lang]?.[k] || k; const clamp = (v, min, max) => Math.max(min, Math.min(max, v)); const validateTime = t => /^\d{2}:\d{2}$/.test(t) ? t : null; const validate = c => ({ ...DEF, ...c, workStartTime: validateTime(c.workStartTime) || DEF.workStartTime, workEndTime: validateTime(c.workEndTime) || DEF.workEndTime, dailyWage: clamp(+c.dailyWage || DEF.dailyWage, 0, 1e6), payday: clamp(+c.payday || DEF.payday, 1, 31), dailyWorkHours: clamp(+c.dailyWorkHours || DEF.dailyWorkHours, 1, 24), workdays: Array.isArray(c.workdays) && c.workdays.length ? c.workdays : DEF.workdays, lang: ['zh', 'en'].includes(c.lang) ? c.lang : 'zh', visible: typeof c.visible === 'boolean' ? c.visible : DEF.visible }); const load = () => { try { const s = GM_getValue('fishtime_config'); return s ? validate(JSON.parse(s)) : DEF; } catch { return DEF; } }; const save = c => { try { GM_setValue('fishtime_config', JSON.stringify(validate(c))); return true; } catch { return false; } }; const parseTime = t => { try { const parts = t.split(':').map(Number); if (parts.length !== 2 || isNaN(parts[0]) || isNaN(parts[1])) { return [0, 0]; } return parts; } catch { return [0, 0]; } }; const pad = n => String(n).padStart(2, '0'); const countdown = () => { const n = new Date(), end = new Date(n); const [h, m] = parseTime(cfg.workEndTime); end.setHours(h, m, 0, 0); if (n >= end) return { h: 0, m: 0, s: 0, done: true }; const d = end - n; return { h: Math.floor(d / C.HOUR), m: Math.floor(d % C.HOUR / C.MIN), s: Math.floor(d % C.MIN / C.SEC), done: false }; }; const earnings = () => { const n = new Date(), start = new Date(n); const [sh, sm] = parseTime(cfg.workStartTime); start.setHours(sh, sm, 0, 0); if (n < start) return 0; const [eh, em] = parseTime(cfg.workEndTime); const totalMin = cfg.flexibleWork ? cfg.dailyWorkHours * 60 : (eh * 60 + em) - (sh * 60 + sm); const totalSec = totalMin * 60; const workedSec = Math.min(Math.floor((n - start) / C.SEC), totalSec); return workedSec * cfg.dailyWage / totalSec; }; const payday = () => { const n = new Date(), d = n.getDate(); let m = n.getMonth(), y = n.getFullYear(); if (d >= cfg.payday) { m++; if (m > 11) { m = 0; y++; } } return Math.ceil((new Date(y, m, cfg.payday) - n) / C.DAY); }; const weekend = () => { const day = new Date().getDay() || 7; const diff = Math.max(...cfg.workdays) - day; return diff < 0 ? diff + 7 : diff; }; const isWorkday = () => cfg.workdays.includes(new Date().getDay() || 7); const notify = msg => { const el = document.createElement('div'); el.className = 'fishtime-notification'; el.textContent = msg; document.body.appendChild(el); setTimeout(() => { el.style.animation = 'slideIn 0.3s ease reverse'; setTimeout(() => el.remove(), 300); }, 2000); }; let animationId = null; const animateEarnings = () => { const diff = targetEarnings - currentEarnings; if (Math.abs(diff) > 0.001) { currentEarnings += diff * 0.1; if (els.earningsNum) { els.earningsNum.textContent = currentEarnings.toFixed(2); // 添加数字变化时的脉冲效果 if (Math.abs(diff) > 0.01) { els.earningsNum.classList.add('pulse'); setTimeout(() => els.earningsNum.classList.remove('pulse'), 100); } } } animationId = requestAnimationFrame(animateEarnings); }; const stopAnimation = () => { if (animationId) { cancelAnimationFrame(animationId); animationId = null; } }; const update = () => { if (!els.countdown) return; if (!isWorkday()) { els.countdown.textContent = t('restDay'); targetEarnings = 0; } else { const c = countdown(); els.countdown.textContent = c.done ? t('offWork') : `${pad(c.h)}:${pad(c.m)}:${pad(c.s)}`; targetEarnings = earnings(); } els.paydayNum.textContent = payday(); els.weekendNum.textContent = weekend(); }; const updateLang = () => { if (!els.title) return; els.title.textContent = t('title'); els.paydayName.textContent = t('payday'); els.weekendName.textContent = t('weekend'); els.earningsName.textContent = t('earnings'); els.settingsTitle.textContent = t('settings'); els.workStartLabel.textContent = t('workStart'); els.workEndLabel.textContent = t('workEnd'); els.dailyWageLabel.textContent = t('dailyWage'); els.paydayDateLabel.textContent = t('paydayDate'); els.flexibleLabel.textContent = t('flexible'); els.workHoursLabel.textContent = t('workHours'); els.workdaysLabel.textContent = t('workdays'); els.shortcutLabel.textContent = t('shortcut'); els.langLabel.textContent = t('lang'); els.saveBtn.textContent = t('save'); els.yuanUnit.textContent = t('yuan'); els.dayUnit.textContent = t('day'); els.hourUnit.textContent = t('hour'); const weekDays = t('weekDays'); document.querySelectorAll('.workday-btn').forEach((b, i) => { if (weekDays[i]) b.textContent = weekDays[i]; }); // 更新工具提示 const tooltips = t('tooltip'); if (tooltips) { els.panel.title = tooltips.drag; document.querySelectorAll('.fishtime-tooltip').forEach((tooltip, i) => { const keys = ['payday', 'weekend', 'earnings']; if (tooltips[keys[i]]) tooltip.textContent = tooltips[keys[i]]; }); } }; const loadUI = () => { els.workStart.value = cfg.workStartTime; els.workEnd.value = cfg.workEndTime; els.dailyWage.value = cfg.dailyWage; els.paydayDate.value = cfg.payday; els.flexibleWork.checked = cfg.flexibleWork; els.dailyHours.value = cfg.dailyWorkHours; els.shortcut.value = cfg.shortcut; els.langSelect.value = cfg.lang; const f = cfg.flexibleWork; els.flexibleGroup.style.display = f ? 'flex' : 'none'; els.workTimeGroup.style.display = f ? 'none' : 'flex'; els.workEndGroup.style.display = f ? 'none' : 'flex'; document.querySelectorAll('.workday-btn').forEach(b => b.classList.toggle('active', cfg.workdays.includes(+b.dataset.day))); updateLang(); }; const saveUI = () => { cfg = validate({ workStartTime: els.workStart.value, workEndTime: els.workEnd.value, dailyWage: +els.dailyWage.value, payday: +els.paydayDate.value, workdays: [...document.querySelectorAll('.workday-btn.active')].map(b => +b.dataset.day), flexibleWork: els.flexibleWork.checked, dailyWorkHours: +els.dailyHours.value, shortcut: els.shortcut.value.toLowerCase(), lang: els.langSelect.value }); if (save(cfg)) { notify(t('saved')); els.settings.style.display = 'none'; els.panel.style.display = 'flex'; update(); updateLang(); bindShortcut(); loadUI(); } else { notify(t('saveFailed')); } }; const bindShortcut = () => { if (window.fishHandler) document.removeEventListener('keydown', window.fishHandler); const keys = cfg.shortcut.split('+').map(k => k.trim()); window.fishHandler = e => { if (keys.every(k => { switch (k) { case 'alt': return e.altKey; case 'ctrl': case 'control': return e.ctrlKey; case 'shift': return e.shiftKey; case 'meta': return e.metaKey; default: return e.key.toLowerCase() === k; } })) { e.preventDefault(); const isVisible = els.container.style.display !== 'none'; els.container.style.display = isVisible ? 'none' : 'block'; // 保存显示/隐藏状态 cfg.visible = !isVisible; save(cfg); } }; document.addEventListener('keydown', window.fishHandler); }; const init = () => { const container = document.createElement('div'); container.id = 'fishtime-container'; container.innerHTML = `