// ==UserScript== // @name 再不摸鱼就下班了 // @namespace http://tampermonkey.net/ // @version 0.0.1 // @description 追踪工作时间、倒计时下班、计算今日收入 // @author Yangshengzhou // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @icon https://acgnhome.com/wp-content/themes/nav/images/favicon.png // @run-at document-end // ==/UserScript== (function () { 'use strict'; const C = { HOUR: 3600000, MIN: 60000, DAY: 86400000, SEC: 1000, UPDATE: 1000 }; const DEF = { workStartTime: '09:00', workEndTime: '18:00', dailyWage: 300, payday: 15, workdays: [1, 2, 3, 4, 5], flexibleWork: false, dailyWorkHours: 8, shortcut: 'alt+ctrl', lang: 'zh' }; const I18N = { zh: { title: '再不摸鱼就下班了', countdown: '下班倒计时', earnings: '今日已赚', payday: '距发薪日', offWork: '已下班', restDay: '休息日', days: '天', settings: '设置', workStart: '上班时间', workEnd: '下班时间', dailyWage: '每天工资', paydayDate: '发薪日', flexible: '弹性工作制', workHours: '工作时长', workdays: '工作日', shortcut: '快捷键', save: '保存设置', lang: '语言', saved: '设置已保存', saveFailed: '保存失败', yuan: '元', day: '日', hour: '小时', weekDays: ['一', '二', '三', '四', '五', '六', '日'] }, en: { title: 'FishTime', countdown: 'Off-work', earnings: 'Today\'s Earnings', payday: 'Days to Payday', 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'] } }; let cfg = load(); let els = {}; let currentEarnings = 0; let targetEarnings = 0; let animationId = null; const t = key => I18N[cfg.lang]?.[key] || key; const clamp = (v, min, max) => Math.max(min, Math.min(max, v)); const validate = c => ({ ...DEF, ...c, 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' }); function load() { try { const s = GM_getValue('fishtime_config'); return s ? validate(JSON.parse(s)) : DEF; } catch (e) { return DEF; } } function save(c) { try { GM_setValue('fishtime_config', JSON.stringify(validate(c))); return true; } catch (e) { return false; } } const parseTime = time => time.split(':').map(Number); const pad = n => String(n).padStart(2, '0'); const $ = id => document.getElementById(id); function countdown() { const n = new Date(); const [h, m] = parseTime(cfg.workEndTime); const end = new Date(n); 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 }; } function earnings() { const n = new Date(); const [sh, sm] = parseTime(cfg.workStartTime); const [eh, em] = parseTime(cfg.workEndTime); const start = new Date(n); start.setHours(sh, sm, 0, 0); if (n < start) return 0; 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; } function payday() { const n = new Date(); const d = n.getDate(); let m = n.getMonth(); let y = n.getFullYear(); if (d >= cfg.payday) { m++; if (m > 11) { m = 0; y++; } } const p = new Date(y, m, cfg.payday); return Math.ceil((p - n) / C.DAY); } function isWorkday() { const d = new Date().getDay(); return cfg.workdays.includes(d === 0 ? 7 : d); } function notify(msg) { const el = document.createElement('div'); el.className = 'notification'; el.textContent = msg; document.body.appendChild(el); setTimeout(() => { el.style.animation = 'slideIn 0.3s ease reverse'; setTimeout(() => el.remove(), 300); }, 2000); } function animateEarnings() { const diff = targetEarnings - currentEarnings; if (Math.abs(diff) > 0.001) { currentEarnings += diff * 0.1; if (els.earnings) { els.earnings.textContent = `¥${currentEarnings.toFixed(2)}`; } } animationId = requestAnimationFrame(animateEarnings); } function update() { if (!els.countdown || !els.earnings || !els.payday) 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.payday.textContent = `${payday()}${t('days')}`; } function updateLang() { if (!els.title) return; els.title.textContent = t('title'); els.countdownLabel.textContent = t('countdown'); els.earningsLabel.textContent = t('earnings'); els.paydayLabel.textContent = t('payday'); 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]; }); } function 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(); } function 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 = 'block'; update(); updateLang(); bindShortcut(); } else { notify(t('saveFailed')); } } function bindShortcut() { if (window.fishHandler) { document.removeEventListener('keydown', window.fishHandler); } const keys = cfg.shortcut.split('+').map(k => k.trim()); window.fishHandler = e => { const m = 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; } }); if (m) { e.preventDefault(); els.container.style.display = els.container.style.display === 'none' ? 'block' : 'none'; } }; document.addEventListener('keydown', window.fishHandler); } function init() { const html = `