// ==UserScript== // @name 電子發票平台 - 年度發票儀表板 // @namespace https://github.com/downwarjers/WebTweaks // @version 3.1 // @description 自動查詢近 7 個月區間發票 // @author downwarjers // @license MIT // @match https://*.einvoice.nat.gov.tw/* // @grant none // @downloadURL https://raw.githubusercontent.com/downwarjers/WebTweaks/main/UserScripts/einvoice-dashboard-export/einvoice-dashboard-export.user.js // @updateURL https://raw.githubusercontent.com/downwarjers/WebTweaks/main/UserScripts/einvoice-dashboard-export/einvoice-dashboard-export.user.js // ==/UserScript== (function() { 'use strict'; // ========================================== // ⚙️ 全域設定 // ========================================== const STORAGE_KEY = 'EINVOICE_V6_CONFIG'; const API_KEYWORD_JWT = 'getSearchCarrierInvoiceListJWT'; const API_KEYWORD_SEARCH = 'searchCarrierInvoice'; // ========================================== // 🎨 UI 樣式 // ========================================== const STYLES = ` #dashboard-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.85); z-index: 99999; display: flex; flex-direction: column; align-items: center; padding-top: 30px; font-family: "Microsoft JhengHei", sans-serif; } #dashboard-container { width: 95%; max-width: 1200px; background: #fff; border-radius: 8px; box-shadow: 0 0 20px rgba(0,0,0,0.5); padding: 15px; height: 90vh; display: flex; flex-direction: column; } .dash-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 2px solid #eee; padding-bottom: 10px; margin-bottom: 10px; flex-shrink: 0; } .dash-title { font-size: 20px; font-weight: bold; color: #333; } .dash-controls { display: flex; gap: 8px; align-items: center; } .btn-dash { height: 34px; padding: 0 12px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; font-weight: bold; color: white; transition: 0.2s; display: flex; align-items: center; justify-content: center; } .btn-close { background: #dc3545; } .btn-close:hover { background: #c82333; } .btn-run { background: #007bff; } .btn-run:hover { background: #0069d9; } .btn-export { background: #28a745; } .btn-export:hover { background: #218838; } .btn-dash:disabled { opacity: 0.5; cursor: not-allowed; background: #6c757d; } #progress-area { margin-bottom: 10px; background: #f8f9fa; padding: 10px; border-radius: 4px; flex-shrink: 0; display: flex; flex-direction: column; gap: 5px; } .progress-row { display: flex; justify-content: space-between; align-items: center; font-size: 13px; } .progress-bar { height: 10px; background: #e9ecef; border-radius: 5px; overflow: hidden; width: 100%; margin-top: 2px; } .progress-fill { height: 100%; background: #0d6efd; width: 0%; transition: width 0.3s; } .log-text { font-family: monospace; font-size: 12px; color: #666; height: 50px; overflow-y: auto; border: 1px solid #ddd; padding: 4px; background: #fff; white-space: pre-wrap; resize: none; } #data-table-wrapper { flex: 1; overflow: auto; border: 1px solid #ddd; position: relative; } table.custom-table { width: 100%; border-collapse: collapse; font-size: 13px; } table.custom-table th, table.custom-table td { border: 1px solid #ddd; padding: 6px 8px; text-align: left; } table.custom-table th { background: #f2f2f2; position: sticky; top: 0; z-index: 10; box-shadow: 0 2px 2px rgba(0,0,0,0.05); } .row-month { font-weight: bold; color: #0056b3; white-space: nowrap; } .amount-col { text-align: right; font-family: monospace; font-weight: bold; } /* 整合版按鈕樣式 */ #floating-trigger { display: flex; justify-content: center; align-items: center; width: 50px; height: 50px; /* 配合原本網站按鈕大小 */ border-radius: 50%; cursor: pointer; font-weight: bold; font-size: 24px; transition: all 0.3s; color: white; text-decoration: none; box-shadow: 0 2px 5px rgba(0,0,0,0.2); margin: 5px auto; /* 確保在 li 裡面置中 */ } #floating-trigger.status-ready { background: #28a745; } #floating-trigger.status-wait { background: #dc3545; opacity: 0.9; } #floating-trigger:hover { transform: scale(1.1); box-shadow: 0 4px 8px rgba(0,0,0,0.3); } /* Fallback: 如果找不到側邊欄,改回懸浮樣式 */ #floating-trigger.fallback-mode { position: fixed; bottom: 20px; right: 20px; z-index: 9999; border: 2px solid white; box-shadow: 0 4px 15px rgba(0,0,0,0.3); width: auto; height: auto; padding: 12px 18px; border-radius: 50px; font-size: 15px; } `; const styleEl = document.createElement('style'); styleEl.innerHTML = STYLES; document.head.appendChild(styleEl); // ========================================== // 🕵️♂️ 核心邏輯:設定儲存 // ========================================== function saveConfig(headers, payload, url) { try { const { searchStartDate, searchEndDate, ...baseParams } = payload; const urlSearch = url.replace(API_KEYWORD_JWT, API_KEYWORD_SEARCH) + "?page=0&size=1000"; const config = { headers: headers, params: baseParams, urlJwt: url, urlSearch: urlSearch, timestamp: new Date().getTime() }; localStorage.setItem(STORAGE_KEY, JSON.stringify(config)); console.log('✅ [Dashboard] 設定已儲存', config); updateButtonStatus(true); } catch (e) { console.error('[Dashboard] 設定儲存失敗', e); } } // ========================================== // 🕵️♂️ 雙模攔截器 // ========================================== const originalXHROpen = XMLHttpRequest.prototype.open; const originalXHRSend = XMLHttpRequest.prototype.send; const originalXHRSetHeader = XMLHttpRequest.prototype.setRequestHeader; XMLHttpRequest.prototype.open = function(method, url) { this._url = url; this._capturedHeaders = {}; return originalXHROpen.apply(this, arguments); }; XMLHttpRequest.prototype.setRequestHeader = function(header, value) { if (this._capturedHeaders) this._capturedHeaders[header.toLowerCase()] = value; return originalXHRSetHeader.apply(this, arguments); }; XMLHttpRequest.prototype.send = function(body) { if (this._url && this._url.includes(API_KEYWORD_JWT)) { try { saveConfig(this._capturedHeaders, JSON.parse(body), this._url); } catch (e) {} } return originalXHRSend.apply(this, arguments); }; const originalFetch = window.fetch; window.fetch = async function(url, options) { const urlStr = url.toString(); if (urlStr.includes(API_KEYWORD_JWT) && options && options.method === 'POST') { try { let headers = {}; if (options.headers instanceof Headers) options.headers.forEach((v, k) => headers[k] = v); else headers = { ...options.headers }; saveConfig(headers, JSON.parse(options.body), urlStr); } catch (e) {} } return originalFetch(url, options); }; // ========================================== // 🧠 智慧日期計算 // ========================================== function getSmartDateRanges() { const ranges = []; const now = new Date(); for (let i = 0; i <= 7; i++) { const year = now.getFullYear(); const month = now.getMonth(); const targetFirstDay = new Date(year, month - i, 1, 0, 0, 0); const y = targetFirstDay.getFullYear(); const m = targetFirstDay.getMonth(); let targetLastDay; if (i === 0) targetLastDay = now; else targetLastDay = new Date(y, m + 1, 0, 23, 59, 59); ranges.push({ y: y, m: m + 1, start: targetFirstDay.toISOString(), end: targetLastDay.toISOString() }); } return ranges; } // ========================================== // 🚀 UI 介面 // ========================================== function createFloatingButton() { if (document.getElementById('floating-trigger')) return; const btn = document.createElement('a'); btn.id = 'floating-trigger'; btn.href = "javascript:void(0);"; btn.innerHTML = '⚡'; // 預設圖示 btn.className = 'status-wait'; btn.title = "發票小幫手 (未激活) - 請先執行一次查詢"; btn.onclick = (e) => { e.preventDefault(); if (btn.classList.contains('status-wait')) { alert('⚠️ 尚未取得查詢權限!\n\n請先在網頁左側隨便選一個日期,按下原本的「查詢」按鈕。\n等待右下角按鈕變綠色 (🚀) 後再點擊。'); } else { openDashboard(); } }; // 嘗試尋找原網頁的側邊欄容器 const hotkeyContainer = document.querySelector('ul.hotkey'); if (hotkeyContainer) { // ✅ 找到側邊欄,插入到最後 const li = document.createElement('li'); li.style.marginBottom = "5px"; // 微調間距 li.appendChild(btn); hotkeyContainer.appendChild(li); } else { // ⚠️ 找不到側邊欄 (可能頁面結構變了),使用 Fallback 懸浮模式 console.warn('[Dashboard] 未找到 ul.hotkey,改用懸浮模式'); btn.classList.add('fallback-mode'); btn.innerHTML = '⚡ 發票小幫手'; document.body.appendChild(btn); } } function updateButtonStatus(ready) { if (!document.getElementById('floating-trigger')) createFloatingButton(); const configStr = localStorage.getItem(STORAGE_KEY); const el = document.getElementById('floating-trigger'); if (!el) return; if (ready || configStr) { // 判斷是否為 Fallback 模式來決定顯示文字還是純圖示 if (el.classList.contains('fallback-mode')) { el.innerHTML = '🚀 開啟儀表板'; } else { el.innerHTML = '🚀'; } el.className = el.className.replace('status-wait', '') + ' status-ready'; el.title = "點擊開啟:近半年發票儀表板"; } else { if (el.classList.contains('fallback-mode')) { el.innerHTML = '⚡ 發票小幫手 (未激活)'; } else { el.innerHTML = '⚡'; } el.className = el.className.replace('status-ready', '') + ' status-wait'; el.title = "發票小幫手 (未激活) - 請先執行一次查詢"; } } function openDashboard() { if (document.getElementById('dashboard-overlay')) return; const overlay = document.createElement('div'); overlay.id = 'dashboard-overlay'; overlay.innerHTML = `
| 月份 | 發票號碼 | 日期 | 商店名稱 | 載具 | 金額 |
|---|---|---|---|---|---|
| 請點擊「開始掃描」載入資料 | |||||