├── ticket.png ├── images ├── icon128.png ├── icon16.png └── icon48.png ├── .gitignore ├── LICENSE ├── styles.css ├── injected.js ├── shadowInject.js ├── README.md ├── manifest.json ├── ticketLoader.js ├── background.js ├── popup.html ├── earlyLoader.js ├── popup.js └── content.js /ticket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coder220224/Ticket-Filter/HEAD/ticket.png -------------------------------------------------------------------------------- /images/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coder220224/Ticket-Filter/HEAD/images/icon128.png -------------------------------------------------------------------------------- /images/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coder220224/Ticket-Filter/HEAD/images/icon16.png -------------------------------------------------------------------------------- /images/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coder220224/Ticket-Filter/HEAD/images/icon48.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 和編輯器 2 | .vscode/ 3 | .idea/ 4 | *.sublime-project 5 | *.sublime-workspace 6 | 7 | # 系統文件 8 | .DS_Store 9 | Thumbs.db 10 | 11 | # 暫存文件 12 | *.log 13 | *.tmp 14 | *.temp 15 | 16 | # Node.js 17 | node_modules/ 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | 22 | # 打包文件 23 | *.zip 24 | *.rar 25 | *.7z 26 | 27 | # 其他 28 | *.bak 29 | *.swp 30 | *~ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 poning0224 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | width: 300px; 3 | padding: 15px; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; 5 | } 6 | 7 | .container { 8 | display: flex; 9 | flex-direction: column; 10 | gap: 15px; 11 | } 12 | 13 | h2 { 14 | margin: 0 0 15px 0; 15 | color: #333; 16 | font-size: 18px; 17 | text-align: center; 18 | } 19 | 20 | h3 { 21 | margin: 0 0 10px 0; 22 | color: #666; 23 | font-size: 14px; 24 | } 25 | 26 | .filter-section { 27 | padding: 10px; 28 | background-color: #f8f9fa; 29 | border-radius: 5px; 30 | } 31 | 32 | input[type="text"], 33 | input[type="number"] { 34 | width: 100%; 35 | padding: 8px; 36 | border: 1px solid #ddd; 37 | border-radius: 4px; 38 | box-sizing: border-box; 39 | } 40 | 41 | .price-range { 42 | display: flex; 43 | align-items: center; 44 | gap: 10px; 45 | } 46 | 47 | .price-range input { 48 | width: 45%; 49 | } 50 | 51 | small { 52 | display: block; 53 | margin-top: 5px; 54 | color: #666; 55 | font-size: 12px; 56 | } 57 | 58 | button { 59 | width: 100%; 60 | padding: 10px; 61 | background-color: #007bff; 62 | color: white; 63 | border: none; 64 | border-radius: 4px; 65 | cursor: pointer; 66 | font-size: 14px; 67 | transition: background-color 0.2s; 68 | } 69 | 70 | button:hover { 71 | background-color: #0056b3; 72 | } 73 | 74 | label { 75 | display: flex; 76 | align-items: center; 77 | gap: 8px; 78 | cursor: pointer; 79 | } 80 | 81 | /* 高亮显示匹配的座位 */ 82 | .seat-highlight { 83 | background-color: #90EE90 !important; 84 | border: 2px solid #32CD32 !important; 85 | } -------------------------------------------------------------------------------- /injected.js: -------------------------------------------------------------------------------- 1 | // 極簡版:限定網域;先確認不是 Cloudflare challenge,再覆寫 2 | (function() { 3 | const host = location.hostname; 4 | const allowed = /(?:tixcraft\.com|kktix\.com|kktix\.cc|ibon\.com\.tw|cityline\.com|ticket\.com\.tw|ticketplus\.com\.tw|fami\.life|tix\.wdragons\.com|tix\.ctbcsports\.com|tix\.fubonbraves\.com|jkface\.net|kham\.com\.tw|tixcraftweb-pcox\.onrender\.com)$/i; 5 | if (!allowed.test(host)) return; 6 | 7 | function isCfChallenge() { 8 | const doc = document; 9 | const html = doc.documentElement ? doc.documentElement.innerHTML : ''; 10 | return /cdn-cgi\/challenge-platform|cf-challenge|turnstile|cf-\w*token/.test(html) || 11 | !!doc.querySelector('script[src*="cdn-cgi/challenge-platform"], iframe[src*="cdn-cgi/challenge-platform"], input[name="cf-turnstile-response"]'); 12 | } 13 | 14 | function start() { 15 | if (window.__shadowOpenPatched) return; // 若前置腳本已處理就略過 16 | if (isCfChallenge()) return; // 驗證頁直接跳過 17 | window.__shadowOpenPatched = true; 18 | 19 | const original = Element.prototype.attachShadow; 20 | Element.prototype.attachShadow = function(init) { 21 | const opts = Object.assign({}, init || {}, { mode: 'open' }); 22 | return original.call(this, opts); 23 | }; 24 | 25 | // 單次處理當前 DOM,失敗即放棄;無觀察、無輪詢 26 | const root = document.documentElement; 27 | if (!root || !root.querySelectorAll) return; 28 | root.querySelectorAll('[shadowroot="closed"]').forEach(el => { 29 | if (!el.shadowRoot) { 30 | try { el.attachShadow({ mode: 'open' }); } catch (_) {} 31 | } 32 | }); 33 | } 34 | 35 | if (document.readyState === 'loading') { 36 | document.addEventListener('DOMContentLoaded', start, { once: true }); 37 | } else { 38 | start(); 39 | } 40 | })(); -------------------------------------------------------------------------------- /shadowInject.js: -------------------------------------------------------------------------------- 1 | // 極簡版:限定網域;先確認不是 Cloudflare 驗證頁,再決定是否覆寫 2 | (function() { 3 | const host = location.hostname; 4 | const allowed = /(?:tixcraft\.com|kktix\.com|kktix\.cc|ibon\.com\.tw|cityline\.com|ticket\.com\.tw|ticketplus\.com\.tw|fami\.life|tix\.wdragons\.com|tix\.ctbcsports\.com|tix\.fubonbraves\.com|jkface\.net|kham\.com\.tw|tixcraftweb-pcox\.onrender\.com)$/i; 5 | if (!allowed.test(host)) return; 6 | 7 | const script = document.createElement('script'); 8 | script.textContent = ` 9 | (function() { 10 | function isCfChallenge() { 11 | const doc = document; 12 | const html = doc.documentElement ? doc.documentElement.innerHTML : ''; 13 | return /cdn-cgi\\\/challenge-platform|cf-challenge|turnstile|cf-\\w*token/.test(html) || 14 | !!doc.querySelector('script[src*="cdn-cgi/challenge-platform"], iframe[src*="cdn-cgi/challenge-platform"], input[name="cf-turnstile-response"]'); 15 | } 16 | 17 | function start() { 18 | if (window.__shadowOpenPatched) return; 19 | if (isCfChallenge()) return; // 驗證頁直接跳過,不做任何覆寫 20 | window.__shadowOpenPatched = true; 21 | 22 | const original = Element.prototype.attachShadow; 23 | Element.prototype.attachShadow = function(init) { 24 | const opts = Object.assign({}, init || {}, { mode: 'open' }); 25 | return original.call(this, opts); 26 | }; 27 | 28 | // 單次處理當前 DOM 上的 shadowroot="closed" 節點,失敗即放棄 29 | const root = document.documentElement; 30 | if (!root || !root.querySelectorAll) return; 31 | root.querySelectorAll('[shadowroot="closed"]').forEach(el => { 32 | if (!el.shadowRoot) { 33 | try { el.attachShadow({ mode: 'open' }); } catch (_) {} 34 | } 35 | }); 36 | } 37 | 38 | if (document.readyState === 'loading') { 39 | document.addEventListener('DOMContentLoaded', start, { once: true }); 40 | } else { 41 | start(); 42 | } 43 | })(); 44 | `; 45 | (document.head || document.documentElement).appendChild(script); 46 | script.remove(); 47 | 48 | // 不再注入額外的 injected.js,避免重複覆寫與持續掃描 49 | })(); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 圖片描述 搶票柴柴-售票網站篩選器 2 | 3 | 4 | 一個 Chrome 擴充功能,用於在各大售票網站上快速篩選您想要的區域票券。 5 | 6 | ## ✨ 功能特點 7 | 8 | ### 多平台支援 9 | - 搶票柴柴網站(tixcraftweb-pcox.onrender.com) 10 | - 拓元售票網站 (tixcraft.com) 11 | - KKTIX售票網站 (kktix.com) 12 | - ibon售票網站 (ibon.com.tw) 13 | - 遠大售票網站 (ticketplus.com.tw) 14 | - cityline售票網站 (cityline.com) 15 | - 寬宏售票網站 (kham.com.tw) 16 | - 年代售票網站 (ticket.com.tw) 17 | - 富邦悍將售票網站 (fami.life) 18 | - 富邦勇士售票網站(fubonbraves.com) 19 | - 中信育樂售票網站(ctbcsports.com) 20 | - 味全龍售票網站(wdragons.com) 21 | - JKFACE售票網站 (jkface.net) 22 | 23 | ### 篩選功能 24 | - 關鍵字篩選:輸入想要的區域名稱(如:VIP、特A區等),快速找出符合條件的票券 25 | - 黑名單過濾:輸入不想看到的區域名稱或價格,快速過濾不需要的票券 26 | - 價格篩選:輸入價格可以快速找出指定價格的票券 27 | - 已售完票券過濾:可選擇是否顯示已售完的票券 28 | - 一鍵重置:點擊「顯示全部票券」即可恢復原始顯示 29 | - 即時更新:當網頁更新時自動套用篩選條件 30 | 31 | ### 進階功能 32 | - 顯示本地時間:可選擇是否在頁面上顯示當前裝置的時間 33 | - 顯示篩選狀態:可選擇是否在頁面上顯示目前的篩選條件 34 | - 擴充功能開關:可快速開啟/關閉擴充功能 35 | 36 | ## 🧩 安裝方式 37 | ### Chrome商店(版本1.5.1) 38 | 1. 前往 [Chrome商店](https://chromewebstore.google.com/detail/pofndajlpfdonhkefkppngfghocppcck?utm_source=item-share-cb) 39 | 2. 加到Chrome 40 | 3. 新增擴充功能 41 | 42 | ### 📥 本地下載(版本1.5.2) 43 | 1. [點我下載](https://github.com/coder220224/ticket-filter/releases/download/v1.5.2/ticket-filter-v1.5.2.zip) 44 | 2. 解壓縮檔案 45 | 3. 開啟 Chrome 瀏覽器,前往 chrome://extensions/ 46 | 4. 開啟右上角的「開發人員模式」 47 | 5. 點擊「載入未封裝項目」 48 | 6. 選擇解壓縮後的資料夾 49 | 50 | ### Github Releases(版本1.5.2) 51 | 1. 前往 [Releases](https://github.com/coder220224/ticket-filter/releases) 頁面 52 | 2. 下載最新版本的 ZIP 檔案 53 | 3. 解壓縮檔案 54 | 4. 開啟 Chrome 瀏覽器,前往 chrome://extensions/ 55 | 5. 開啟右上角的「開發人員模式」 56 | 6. 點擊「載入未封裝項目」 57 | 7. 選擇解壓縮後的資料夾 58 | 59 | ## 📱 手機安裝方式 60 | - ios : [點此看YT教學](https://youtube.com/shorts/KQwCQwVKBBY?feature=share) 61 | 62 | ## 🔧 使用方式 63 | 64 | ### 基本操作 65 | 1. 點擊擴充功能圖示開啟篩選器 66 | 2. 在關鍵字輸入框中輸入想要篩選的區域名稱或價格 67 | 3. 在黑名單輸入框中輸入想要過濾的區域名稱或價格 68 | 4. 按 Enter 或點擊 + 按鈕新增條件 69 | 5. 可選擇是否顯示已售完的票券 70 | 6. 點擊「顯示全部票券」可重置所有條件 71 | 72 | ### 進階篩選語法 73 | - 使用逗號(,)可以篩選同時符合的票券(AND邏輯) 74 | - 例:`4800,搖滾區` - 尋找 4800 元且在搖滾區的票券 75 | - 使用加號(+)可以篩選任一條件符合的票券(OR邏輯) 76 | - 例:`4500+3200` - 尋找 4500 元或 3200 元的票券 77 | - 複合條件範例: 78 | - `4500,A區+3200,B區` - 尋找 (4500元的A區) 或 (3200元的B區) 的票券 79 | 80 | ### 智慧數字轉換 81 | - 支援中文數字和阿拉伯數字互相轉換 82 | - 輸入「特1區」可以找到「特一區」 83 | - 輸入「特一區」可以找到「特1區」 84 | - 支援價格格式轉換 85 | - 輸入「2800」可以找到「2,800」的票價 86 | 87 | ## 🔥 版本更新 (v1.5.2) 88 | ### 1. 新增平台支援 89 | - 支援年代售票網站(ticket.com.tw) 90 | 91 | ## ⚠️ 注意事項 92 | 93 | - 本擴充功能僅提供視覺化篩選,不影響實際購票功能 94 | - 建議搭配官方購票系統使用 95 | - 請遵守各售票網站的購票規則 96 | 97 | ## 🔒 隱私權政策 98 | 99 | - 本擴充功能不會收集任何個人資料 100 | - 所有設定均儲存在您的瀏覽器本地端 101 | - 不會向任何第三方傳送資料 102 | 103 | ## 🏷️ 版本資訊 104 | 105 | - 目前版本:1.5.2 106 | - 最後更新:2025/12/15 107 | 108 | ## 👨‍💻 開發者資訊 109 | 110 | 如有任何問題或建議,歡迎透過 [GitHub Issues](https://github.com/poning0224/tixcraft-filter/issues) 回報。 111 | 112 | 或著私訊我的社群帳號[摳得柴柴](https://www.threads.net/@coder22022)。 113 | 114 | ## 💝 贊助支持 115 | 如果你喜歡這個項目並希望支持它,可以考慮通過以下方式贊助: 116 | 117 | 歐富寶支付 PayPal 118 | 119 | 每一分支持都對我很有幫助,謝謝! 120 | 121 | 122 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "update_url": "https://clients2.google.com/service/update2/crx", 3 | 4 | "manifest_version": 3, 5 | "name": "搶票柴柴-售票網站篩選器", 6 | "version": "1.5.2", 7 | "description": "在售票網站上快速篩選您想要的區域票券", 8 | "permissions": [ 9 | "storage", 10 | "activeTab", 11 | "commands" 12 | ], 13 | "host_permissions": [ 14 | "*://*.tixcraft.com/*", 15 | "*://*.kktix.com/*", 16 | "*://kktix.com/*", 17 | "*://*.kktix.cc/*", 18 | "*://*.ibon.com.tw/*", 19 | "*://ticket.ibon.com.tw/*", 20 | "*://orders.ibon.com.tw/*", 21 | "*://*.cityline.com/*", 22 | "*://www.cityline.com/*", 23 | "*://*.ticket.com.tw/*", 24 | "*://*.ticketplus.com.tw/*", 25 | "*://*.fami.life/*", 26 | "*://*.tix.wdragons.com/*", 27 | "*://*.tix.ctbcsports.com/*", 28 | "*://*.tix.fubonbraves.com/*", 29 | "*://*.jkface.net/*", 30 | "*://*.kham.com.tw/*", 31 | "*://*.tixcraftweb-pcox.onrender.com/*" 32 | ], 33 | "commands": { 34 | "toggle-extension": { 35 | "suggested_key": { 36 | "default": "Ctrl+Shift+E", 37 | "mac": "Command+Shift+E" 38 | }, 39 | "description": "啟用/停用篩選器" 40 | } 41 | }, 42 | "content_scripts": [ 43 | { 44 | "matches": [ 45 | "*://*.tixcraft.com/*", 46 | "*://*.kktix.com/*", 47 | "*://*.kktix.cc/*", 48 | "*://*.ibon.com.tw/*", 49 | "*://ticket.ibon.com.tw/*", 50 | "*://orders.ibon.com.tw/*", 51 | "*://*.cityline.com/*", 52 | "*://www.cityline.com/*", 53 | "*://*.ticket.com.tw/*", 54 | "*://*.ticketplus.com.tw/*", 55 | "*://*.fami.life/*", 56 | "*://*.tix.wdragons.com/*", 57 | "*://*.tix.ctbcsports.com/*", 58 | "*://*.tix.fubonbraves.com/*", 59 | "*://*.jkface.net/*", 60 | "*://*.kham.com.tw/*", 61 | "*://*.tixcraftweb-pcox.onrender.com/*" 62 | ], 63 | "js": ["content.js"] 64 | }, 65 | { 66 | "matches": [ 67 | "*://orders.ibon.com.tw/*", 68 | "*://ticket.ibon.com.tw/Event/*/*" 69 | ], 70 | "js": ["shadowInject.js"], 71 | "run_at": "document_start", 72 | "all_frames": true, 73 | "world": "MAIN" 74 | }, 75 | 76 | { 77 | "matches": [ 78 | "*://*.tixcraft.com/ticket/area/*", 79 | "*://*.kktix.com/*", 80 | "*://*.kktix.cc/*", 81 | "*://*.ibon.com.tw/*", 82 | "*://*.cityline.com/*", 83 | "*://*.ticket.com.tw/*", 84 | "*://*.ticketplus.com.tw/*", 85 | "*://*.fami.life/*", 86 | "*://*.tix.wdragons.com/*", 87 | "*://*.tix.ctbcsports.com/*", 88 | "*://*.tix.fubonbraves.com/*", 89 | "*://*.jkface.net/*", 90 | "*://*.kham.com.tw/*", 91 | "*://*.tixcraftweb-pcox.onrender.com/*" 92 | ], 93 | "js": ["earlyLoader.js"], 94 | "run_at": "document_start", 95 | "all_frames": true 96 | } 97 | ], 98 | "background": { 99 | "service_worker": "background.js", 100 | "type": "module" 101 | }, 102 | "web_accessible_resources": [{ 103 | "resources": ["injected.js", "styles.css"], 104 | "matches": [ 105 | "*://orders.ibon.com.tw/*", 106 | "*://*.tixcraft.com/*", 107 | "*://*.kktix.com/*", 108 | "*://*.kktix.cc/*", 109 | "*://*.ibon.com.tw/*", 110 | "*://*.cityline.com/*", 111 | "*://*.ticket.com.tw/*", 112 | "*://*.ticketplus.com.tw/*", 113 | "*://*.fami.life/*", 114 | "*://*.tix.wdragons.com/*", 115 | "*://*.tix.ctbcsports.com/*", 116 | "*://*.tix.fubonbraves.com/*", 117 | "*://*.jkface.net/*", 118 | "*://*.kham.com.tw/*", 119 | "*://*.tixcraftweb-pcox.onrender.com/*" 120 | ] 121 | }], 122 | "action": { 123 | "default_popup": "popup.html", 124 | "default_icon": { 125 | "16": "images/icon16.png", 126 | "48": "images/icon48.png", 127 | "128": "images/icon128.png" 128 | } 129 | }, 130 | "icons": { 131 | "16": "images/icon16.png", 132 | "48": "images/icon48.png", 133 | "128": "images/icon128.png" 134 | }, 135 | "content_security_policy": { 136 | "extension_pages": "script-src 'self'; object-src 'self'", 137 | "sandbox": "sandbox allow-scripts allow-forms allow-popups allow-modals; script-src 'self' 'unsafe-inline' 'unsafe-eval'; child-src 'self';" 138 | } 139 | } -------------------------------------------------------------------------------- /ticketLoader.js: -------------------------------------------------------------------------------- 1 | // 在页面加载最早期介入 2 | document.addEventListener('DOMContentLoaded', function() { 3 | // 检查是否是座位选择页面 4 | if (!window.location.href.includes('PERFORMANCE_ID=')) { 5 | return; 6 | } 7 | 8 | // 设置页面加载策略 9 | if (document.documentElement) { 10 | document.documentElement.setAttribute('pageLoadStrategy', 'eager'); 11 | } 12 | 13 | // 设置性能优化 14 | const style = document.createElement('style'); 15 | style.textContent = ` 16 | * { 17 | transition: none !important; 18 | animation: none !important; 19 | } 20 | 21 | /* 隐藏非必要元素 */ 22 | img[src*="banner"], 23 | img[src*="ad"], 24 | iframe:not(.essential-frame), 25 | script[src*="analytics"], 26 | script[src*="tracking"], 27 | script[src*="facebook"], 28 | script[src*="google-analytics"], 29 | script[src*="gtm"], 30 | script[src*="pixel"], 31 | link[href*="font"], 32 | .footer, 33 | .header-banner, 34 | .ad-container, 35 | #topAlert, 36 | .alert-box, 37 | .emergency, 38 | .process-wizard, 39 | .nav-line { 40 | display: none !important; 41 | visibility: hidden !important; 42 | opacity: 0 !important; 43 | width: 0 !important; 44 | height: 0 !important; 45 | position: absolute !important; 46 | pointer-events: none !important; 47 | } 48 | 49 | /* 优化关键元素显示 */ 50 | .area-list, 51 | .seat-map, 52 | #box, 53 | #mapdata, 54 | #ctl00_ContentPlaceHolder1_IMG_MAP, 55 | .main { 56 | visibility: visible !important; 57 | opacity: 1 !important; 58 | display: block !important; 59 | content-visibility: auto !important; 60 | } 61 | 62 | #box img { 63 | image-rendering: auto !important; 64 | object-fit: contain !important; 65 | visibility: visible !important; 66 | opacity: 1 !important; 67 | } 68 | 69 | /* 优化表单元素 */ 70 | input, select, button { 71 | pointer-events: auto !important; 72 | visibility: visible !important; 73 | opacity: 1 !important; 74 | } 75 | `; 76 | document.head.appendChild(style); 77 | 78 | // 阻止不必要的资源加载 79 | const stopLoading = () => { 80 | try { 81 | window.stop(); 82 | console.log('已停止页面加载'); 83 | observer.disconnect(); 84 | clearInterval(checkInterval); 85 | } catch (e) { 86 | console.log('停止加载时发生错误:', e); 87 | } 88 | }; 89 | 90 | // 检查并停止加载 91 | const checkAndStopLoading = () => { 92 | // 检查关键元素 93 | const areaList = document.querySelector('.area-list'); 94 | const seatMap = document.querySelector('#box img'); 95 | const mapData = document.querySelector('#mapdata'); 96 | const mainElements = document.querySelectorAll('.main'); 97 | 98 | // 快速检查是否有任何关键元素 99 | if (!areaList && !seatMap && !mapData && mainElements.length === 0) { 100 | return false; 101 | } 102 | 103 | // 检查价格信息 104 | const priceElements = document.querySelectorAll('.area-list font'); 105 | if (priceElements.length >= 4) { // 降低阈值到4个价格区域 106 | stopLoading(); 107 | return true; 108 | } 109 | 110 | // 检查座位图是否加载 111 | if (seatMap && seatMap.complete && seatMap.naturalWidth > 0) { 112 | stopLoading(); 113 | return true; 114 | } 115 | 116 | return false; 117 | }; 118 | 119 | // 使用 MutationObserver 监听 DOM 变化 120 | const observer = new MutationObserver((mutations) => { 121 | for (const mutation of mutations) { 122 | if (mutation.type === 'childList') { 123 | mutation.addedNodes.forEach(node => { 124 | if (node.nodeName === 'IMG') { 125 | node.loading = 'eager'; 126 | node.fetchPriority = 'high'; 127 | 128 | // 对于座位图特别处理 129 | if (node.id === 'ctl00_ContentPlaceHolder1_IMG_MAP') { 130 | node.style.visibility = 'visible'; 131 | node.style.opacity = '1'; 132 | } 133 | } 134 | 135 | // 自动接受任何弹窗 136 | if (node.nodeName === 'DIALOG' || 137 | (node.className && 138 | typeof node.className === 'string' && 139 | node.className.includes('modal'))) { 140 | node.remove(); 141 | } 142 | }); 143 | } 144 | } 145 | 146 | // 每次DOM变化都检查是否可以停止加载 147 | checkAndStopLoading(); 148 | }); 149 | 150 | // 开始监听 151 | observer.observe(document.documentElement, { 152 | childList: true, 153 | subtree: true, 154 | attributes: true 155 | }); 156 | 157 | // 更频繁地检查加载状态 158 | const checkInterval = setInterval(checkAndStopLoading, 50); 159 | 160 | // 设置更短的超时时间 161 | setTimeout(() => { 162 | if (!checkAndStopLoading()) { 163 | stopLoading(); 164 | } 165 | }, 1000); 166 | 167 | // 拦截和处理所有弹窗 168 | window.alert = function() { return true; }; 169 | window.confirm = function() { return true; }; 170 | window.onbeforeunload = null; 171 | window.onunload = null; 172 | }); 173 | 174 | // 拦截和优化资源加载 175 | const originalFetch = window.fetch; 176 | window.fetch = function(...args) { 177 | const url = args[0]?.url || args[0]; 178 | 179 | // 阻止加载不必要的资源 180 | if (typeof url === 'string' && ( 181 | url.includes('analytics') || 182 | url.includes('tracking') || 183 | url.includes('advertisement') || 184 | url.includes('banner') || 185 | url.includes('.gif') || 186 | url.includes('facebook') || 187 | url.includes('google') || 188 | url.includes('stats') || 189 | url.includes('logger') || 190 | url.includes('pixel') || 191 | url.includes('metrics') || 192 | url.includes('collect') || 193 | url.includes('notification') || 194 | url.includes('gtm') || 195 | url.includes('fonts') 196 | )) { 197 | return Promise.resolve(new Response('', {status: 200})); 198 | } 199 | 200 | // 为关键请求添加高优先级 201 | if (typeof url === 'string' && ( 202 | url.includes('map') || 203 | url.includes('seat') || 204 | url.includes('area') 205 | )) { 206 | const options = args[1] || {}; 207 | options.priority = 'high'; 208 | args[1] = options; 209 | } 210 | 211 | return originalFetch.apply(this, args); 212 | }; 213 | 214 | // 拦截 XMLHttpRequest 215 | const originalOpen = XMLHttpRequest.prototype.open; 216 | XMLHttpRequest.prototype.open = function(method, url, ...args) { 217 | if (typeof url === 'string' && ( 218 | url.includes('analytics') || 219 | url.includes('tracking') || 220 | url.includes('advertisement') || 221 | url.includes('banner') || 222 | url.includes('.gif') || 223 | url.includes('facebook') || 224 | url.includes('google') || 225 | url.includes('stats') || 226 | url.includes('logger') || 227 | url.includes('pixel') || 228 | url.includes('metrics') || 229 | url.includes('collect') || 230 | url.includes('notification') || 231 | url.includes('gtm') || 232 | url.includes('fonts') 233 | )) { 234 | this.abort(); 235 | return; 236 | } 237 | return originalOpen.apply(this, [method, url, ...args]); 238 | }; -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | // 后台脚本 - 负责拦截和修改网络请求 2 | 3 | // 全局设置存储 4 | let settings = { 5 | // 拓元 6 | tixcraftKeywords: [], 7 | tixcraftHideSoldOut: false, 8 | 9 | // KKTIX 10 | kktixKeywords: [], 11 | kktixHideSoldOut: false, 12 | kktixShowAllPrices: true, 13 | 14 | // ibon 15 | ibonKeywords: [], 16 | ibonHideSoldOut: false, 17 | 18 | // Cityline 19 | citylineKeywords: [], 20 | citylineHideSoldOut: false, 21 | 22 | // JKFace 23 | jkfaceKeywords: [], 24 | jkfaceHideSoldOut: false, 25 | 26 | // 寬宏售票 27 | khamKeywords: [], 28 | khamBlacklist: [], 29 | khamHideSoldOut: false, 30 | 31 | // 全局设置 32 | extensionEnabled: true, 33 | showServerTime: true, 34 | showFilterStatus: true 35 | }; 36 | 37 | // Service Worker激活事件 38 | chrome.runtime.onInstalled.addListener(() => { 39 | loadSettings(); 40 | console.log('搶票柴柴扩展已安装/更新'); 41 | }); 42 | 43 | // 确保在service worker启动时也加载设置 44 | self.onload = () => { 45 | loadSettings(); 46 | }; 47 | 48 | // 初始化加载设置 49 | function loadSettings() { 50 | chrome.storage.local.get(null, (result) => { 51 | if (result.keywords) settings.tixcraftKeywords = result.keywords; 52 | if (result.hideSoldOut !== undefined) settings.tixcraftHideSoldOut = result.hideSoldOut; 53 | 54 | if (result.targetKeywords) settings.kktixKeywords = result.targetKeywords; 55 | if (result.showAllPrices !== undefined) settings.kktixShowAllPrices = result.showAllPrices; 56 | if (result.kktixHideSoldOut !== undefined) settings.kktixHideSoldOut = result.kktixHideSoldOut; 57 | 58 | if (result.ibonKeywords) settings.ibonKeywords = result.ibonKeywords; 59 | if (result.ibonHideSoldOut !== undefined) settings.ibonHideSoldOut = result.ibonHideSoldOut; 60 | 61 | if (result.citylineKeywords) settings.citylineKeywords = result.citylineKeywords; 62 | if (result.citylineHideSoldOut !== undefined) settings.citylineHideSoldOut = result.citylineHideSoldOut; 63 | 64 | if (result.jkfaceKeywords) settings.jkfaceKeywords = result.jkfaceKeywords; 65 | if (result.jkfaceHideSoldOut !== undefined) settings.jkfaceHideSoldOut = result.jkfaceHideSoldOut; 66 | 67 | if (result.khamKeywords) settings.khamKeywords = result.khamKeywords; 68 | if (result.khamBlacklist) settings.khamBlacklist = result.khamBlacklist; 69 | if (result.khamHideSoldOut !== undefined) settings.khamHideSoldOut = result.khamHideSoldOut; 70 | 71 | if (result.extensionEnabled !== undefined) settings.extensionEnabled = result.extensionEnabled; 72 | if (result.showServerTime !== undefined) settings.showServerTime = result.showServerTime; 73 | if (result.showFilterStatus !== undefined) settings.showFilterStatus = result.showFilterStatus; 74 | }); 75 | } 76 | 77 | // 监听设置变更 78 | chrome.storage.onChanged.addListener((changes, namespace) => { 79 | for (let key in changes) { 80 | if (key === 'keywords') settings.tixcraftKeywords = changes[key].newValue; 81 | if (key === 'hideSoldOut') settings.tixcraftHideSoldOut = changes[key].newValue; 82 | 83 | if (key === 'targetKeywords') settings.kktixKeywords = changes[key].newValue; 84 | if (key === 'showAllPrices') settings.kktixShowAllPrices = changes[key].newValue; 85 | if (key === 'kktixHideSoldOut') settings.kktixHideSoldOut = changes[key].newValue; 86 | 87 | if (key === 'ibonKeywords') settings.ibonKeywords = changes[key].newValue; 88 | if (key === 'ibonHideSoldOut') settings.ibonHideSoldOut = changes[key].newValue; 89 | 90 | if (key === 'citylineKeywords') settings.citylineKeywords = changes[key].newValue; 91 | if (key === 'citylineHideSoldOut') settings.citylineHideSoldOut = changes[key].newValue; 92 | 93 | if (key === 'jkfaceKeywords') settings.jkfaceKeywords = changes[key].newValue; 94 | if (key === 'jkfaceHideSoldOut') settings.jkfaceHideSoldOut = changes[key].newValue; 95 | 96 | if (key === 'khamKeywords') settings.khamKeywords = changes[key].newValue; 97 | if (key === 'khamBlacklist') settings.khamBlacklist = changes[key].newValue; 98 | if (key === 'khamHideSoldOut') settings.khamHideSoldOut = changes[key].newValue; 99 | 100 | if (key === 'extensionEnabled') settings.extensionEnabled = changes[key].newValue; 101 | if (key === 'showServerTime') settings.showServerTime = changes[key].newValue; 102 | if (key === 'showFilterStatus') settings.showFilterStatus = changes[key].newValue; 103 | } 104 | }); 105 | 106 | // 数字转换函数,供匹配使用 107 | function convertNumber(input) { 108 | const chineseNums = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十']; 109 | const arabicNums = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10']; 110 | 111 | // 移除所有空格 112 | input = input.replace(/\s+/g, ''); 113 | 114 | // 生成所有可能的版本 115 | let versions = new Set([input]); 116 | 117 | // 找出所有数字和中文数字的位置 118 | let matches = []; 119 | // 匹配阿拉伯数字 120 | input.replace(/\d+/g, (match, offset) => { 121 | matches.push({ 122 | type: 'arabic', 123 | value: match, 124 | offset: offset, 125 | length: match.length 126 | }); 127 | return match; 128 | }); 129 | // 匹配中文数字 130 | for (let i = 0; i < chineseNums.length; i++) { 131 | let pos = input.indexOf(chineseNums[i]); 132 | while (pos !== -1) { 133 | matches.push({ 134 | type: 'chinese', 135 | value: chineseNums[i], 136 | offset: pos, 137 | length: 1, 138 | arabic: arabicNums[i] 139 | }); 140 | pos = input.indexOf(chineseNums[i], pos + 1); 141 | } 142 | } 143 | 144 | // 按位置排序 145 | matches.sort((a, b) => a.offset - b.offset); 146 | 147 | // 生成替换版本 148 | if (matches.length > 0) { 149 | // 原始文本转换 150 | let converted = input; 151 | for (let match of matches) { 152 | if (match.type === 'arabic') { 153 | // 将阿拉伯数字转为中文数字 154 | const digits = match.value.split(''); 155 | const chinese = digits.map(d => chineseNums[parseInt(d)]).join(''); 156 | converted = converted.slice(0, match.offset) + chinese + 157 | converted.slice(match.offset + match.length); 158 | } else { 159 | // 将中文数字转为阿拉伯数字 160 | converted = converted.slice(0, match.offset) + match.arabic + 161 | converted.slice(match.offset + match.length); 162 | } 163 | } 164 | versions.add(converted); 165 | } 166 | 167 | return Array.from(versions); 168 | } 169 | 170 | // 检查文本是否包含关键字(考虑数字转换) 171 | function textIncludesKeyword(text, keyword) { 172 | // 移除所有空格并转换为小写再比较 173 | const cleanText = text.replace(/\s+/g, '').toLowerCase(); 174 | const cleanKeyword = keyword.replace(/\s+/g, '').toLowerCase(); 175 | 176 | // 获取文本的所有可能版本 177 | const textVersions = convertNumber(cleanText); 178 | const keywordVersions = convertNumber(cleanKeyword); 179 | 180 | // 交叉比对所有版本 181 | return keywordVersions.some(kw => 182 | textVersions.some(txt => txt.includes(kw)) 183 | ); 184 | } 185 | 186 | // 为内容脚本通信设置 187 | // 注册消息处理程序,以便内容脚本可以直接检查元素是否应该被筛选 188 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 189 | if (!settings.extensionEnabled) { 190 | sendResponse({ shouldShow: true }); // 如果扩展被禁用,所有元素都显示 191 | return true; 192 | } 193 | 194 | // 消息类型:检查内容是否符合筛选条件 195 | if (message.type === 'CHECK_FILTER') { 196 | const { site, text, price, isSoldOut } = message.data; 197 | 198 | // 根据不同网站和条件进行判断 199 | let siteKeywords = []; 200 | let siteBlacklist = []; 201 | let siteHideSoldOut = false; 202 | 203 | if (site === 'tixcraft') { 204 | siteKeywords = settings.tixcraftKeywords; 205 | siteHideSoldOut = settings.tixcraftHideSoldOut; 206 | } else if (site === 'kktix') { 207 | siteKeywords = settings.kktixKeywords; 208 | siteHideSoldOut = settings.kktixHideSoldOut; 209 | // 如果设置为显示所有价格,则不筛选 210 | if (settings.kktixShowAllPrices) { 211 | sendResponse({ shouldShow: !isSoldOut || !siteHideSoldOut }); 212 | return true; 213 | } 214 | } else if (site === 'ibon') { 215 | siteKeywords = settings.ibonKeywords; 216 | siteHideSoldOut = settings.ibonHideSoldOut; 217 | } else if (site === 'cityline') { 218 | siteKeywords = settings.citylineKeywords; 219 | siteHideSoldOut = settings.citylineHideSoldOut; 220 | } else if (site === 'jkface') { 221 | siteKeywords = settings.jkfaceKeywords; 222 | siteHideSoldOut = settings.jkfaceHideSoldOut; 223 | } else if (site === 'kham') { 224 | siteKeywords = settings.khamKeywords; 225 | siteBlacklist = settings.khamBlacklist; 226 | siteHideSoldOut = settings.khamHideSoldOut; 227 | } else { 228 | sendResponse({ shouldShow: true }); // 不支持的网站直接显示 229 | return true; 230 | } 231 | 232 | // 如果设置隐藏已售完,且票券已售完,则隐藏 233 | if (siteHideSoldOut && isSoldOut) { 234 | sendResponse({ shouldShow: false }); 235 | return true; 236 | } 237 | 238 | // 如果没有关键字,则显示所有非售罄票券 239 | if (!siteKeywords.length) { 240 | sendResponse({ shouldShow: true }); 241 | return true; 242 | } 243 | 244 | // 将关键字组合处理为AND/OR逻辑组 245 | const keywordGroups = siteKeywords.map(keyword => { 246 | return keyword.split('+').map(k => { 247 | return k.split(',').map(item => item.trim()).filter(item => item); 248 | }).filter(group => group.length > 0); 249 | }); 250 | 251 | // 根据关键字判断是否显示 252 | const searchTexts = [text, price].filter(t => t && typeof t === 'string'); 253 | const shouldShow = keywordGroups.some(orGroup => 254 | orGroup.some(andGroup => { 255 | return andGroup.every(keyword => { 256 | // 数字匹配 257 | if (!isNaN(keyword)) { 258 | const priceToFind = parseInt(keyword); 259 | return searchTexts.some(text => { 260 | const prices = (text.match(/\d+/g) || []).map(p => parseInt(p)); 261 | return prices.includes(priceToFind); 262 | }); 263 | } else { 264 | // 文本匹配 265 | return searchTexts.some(text => textIncludesKeyword(text, keyword)); 266 | } 267 | }); 268 | }) 269 | ); 270 | 271 | // 检查黑名单 272 | if (shouldShow && siteBlacklist && siteBlacklist.length > 0) { 273 | const isBlacklisted = siteBlacklist.some(blacklistItem => { 274 | return searchTexts.some(text => textIncludesKeyword(text, blacklistItem)); 275 | }); 276 | if (isBlacklisted) { 277 | sendResponse({ shouldShow: false }); 278 | return true; 279 | } 280 | } 281 | 282 | sendResponse({ shouldShow }); 283 | return true; 284 | } 285 | 286 | // 消息类型:将设置应用于当前标签 287 | if (message.type === 'GET_SETTINGS') { 288 | const { site } = message.data; 289 | 290 | if (site === 'tixcraft') { 291 | sendResponse({ 292 | keywords: settings.tixcraftKeywords, 293 | hideSoldOut: settings.tixcraftHideSoldOut, 294 | showServerTime: settings.showServerTime, 295 | showFilterStatus: settings.showFilterStatus, 296 | extensionEnabled: settings.extensionEnabled 297 | }); 298 | } else if (site === 'kktix') { 299 | sendResponse({ 300 | keywords: settings.kktixKeywords, 301 | showAllPrices: settings.kktixShowAllPrices, 302 | hideSoldOut: settings.kktixHideSoldOut, 303 | showServerTime: settings.showServerTime, 304 | showFilterStatus: settings.showFilterStatus, 305 | extensionEnabled: settings.extensionEnabled 306 | }); 307 | } else if (site === 'ibon') { 308 | sendResponse({ 309 | keywords: settings.ibonKeywords, 310 | hideSoldOut: settings.ibonHideSoldOut, 311 | showServerTime: settings.showServerTime, 312 | showFilterStatus: settings.showFilterStatus, 313 | extensionEnabled: settings.extensionEnabled 314 | }); 315 | } else if (site === 'cityline') { 316 | sendResponse({ 317 | keywords: settings.citylineKeywords, 318 | hideSoldOut: settings.citylineHideSoldOut, 319 | showServerTime: settings.showServerTime, 320 | showFilterStatus: settings.showFilterStatus, 321 | extensionEnabled: settings.extensionEnabled 322 | }); 323 | } else if (site === 'jkface') { 324 | sendResponse({ 325 | keywords: settings.jkfaceKeywords, 326 | hideSoldOut: settings.jkfaceHideSoldOut, 327 | showServerTime: settings.showServerTime, 328 | showFilterStatus: settings.showFilterStatus, 329 | extensionEnabled: settings.extensionEnabled 330 | }); 331 | } else if (site === 'kham') { 332 | sendResponse({ 333 | keywords: settings.khamKeywords, 334 | blacklist: settings.khamBlacklist, 335 | hideSoldOut: settings.khamHideSoldOut, 336 | showServerTime: settings.showServerTime, 337 | showFilterStatus: settings.showFilterStatus, 338 | extensionEnabled: settings.extensionEnabled 339 | }); 340 | } else { 341 | sendResponse({}); 342 | } 343 | return true; 344 | } 345 | }); 346 | 347 | // 保持service worker活跃 348 | chrome.runtime.onConnect.addListener((port) => { 349 | port.onDisconnect.addListener(() => { 350 | // 断开连接后可以记录日志或执行其他清理工作 351 | console.log('连接已断开'); 352 | }); 353 | }); 354 | 355 | // 确保service worker不会过早终止 356 | chrome.runtime.onStartup.addListener(() => { 357 | loadSettings(); 358 | console.log('浏览器启动,搶票柴柴扩展已激活'); 359 | }); 360 | 361 | // 监听快捷键命令 362 | chrome.commands.onCommand.addListener((command) => { 363 | // 检查快捷键是否启用 364 | chrome.storage.local.get(['shortcutEnabled'], function(result) { 365 | if (result.shortcutEnabled === false) { 366 | return; // 如果快捷键被禁用,直接返回 367 | } 368 | 369 | if (command === 'toggle-extension') { 370 | chrome.storage.local.get(['extensionEnabled'], function(result) { 371 | const newState = !result.extensionEnabled; 372 | chrome.storage.local.set({ extensionEnabled: newState }, function() { 373 | // 向所有标签页广播状态变化 374 | chrome.tabs.query({}, function(tabs) { 375 | tabs.forEach(function(tab) { 376 | chrome.tabs.sendMessage(tab.id, { 377 | type: 'EXTENSION_STATE_CHANGED', 378 | enabled: newState 379 | }); 380 | }); 381 | }); 382 | }); 383 | }); 384 | } 385 | }); 386 | }); -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 搶票柴柴-售票網站篩選器 6 | 530 | 531 | 532 |
533 | 534 |
535 |
536 |
537 |
538 | 539 | 540 |
541 |
542 |
543 | 票券图标搶票柴柴-售票網站篩選器 544 | 545 | 546 | 547 |
548 |
549 |
550 | 啟用 551 | 555 |
556 |
557 |
558 | 559 | 560 |
561 |
562 | 563 | 564 |
565 |
566 | 567 | 568 |
569 |
570 | 571 | 572 |
573 |
574 |
575 |
⌨️ 快捷鍵設定
576 |
577 | 目前快捷鍵: 578 | Ctrl+Shift+E 579 |
580 | 586 |
587 |

💡 提示:

588 |
    589 |
  • 點擊上方按鈕可直接開啟設定頁面
  • 590 |
  • 在設定頁面中找到「搶票柴柴」
  • 591 |
  • 點擊輸入框並按下想要的按鍵組合
  • 592 |
593 |
594 |
595 |
596 |
597 |
✅關鍵字篩選
598 |
599 | 600 | 601 |
602 | 例如:VIP、3200,A區、3200+4500(按Enter新增) 603 |
604 |
605 | 608 |
609 | 尚未設定篩選條件 610 |
611 |
612 |
613 |
⛔️黑名單過濾
614 |
615 | 616 | 617 |
618 | 例如:VIP、3200,A區、3200+4500(按Enter新增) 619 | 622 |
623 | 尚未設定過濾條件 624 |
625 |
626 |
627 |
628 | 629 | 630 |
631 |
632 |
633 | 634 |
635 | 644 |
645 |
646 | 647 | 648 | -------------------------------------------------------------------------------- /earlyLoader.js: -------------------------------------------------------------------------------- 1 | // 在页面加载的最早阶段执行 2 | (function() { 3 | // 判断当前网站类型 4 | const isIbon = window.location.hostname.includes('ibon.com.tw'); 5 | const isTixcraft = window.location.href.includes('tixcraft.com/ticket/area/') || window.location.hostname.includes('tixcraftweb-pcox.onrender.com'); 6 | const isKktix = window.location.hostname.includes('kktix.com'); 7 | const isCityline = window.location.hostname.includes('cityline.com'); 8 | const isEra = window.location.hostname.includes('ticket.com.tw'); 9 | const isFami = window.location.hostname.includes('fami.life'); 10 | const isWdragons = window.location.hostname.includes('tix.wdragons.com'); 11 | const isCtbcsports = window.location.hostname.includes('tix.ctbcsports.com'); 12 | const isFubonbraves = window.location.hostname.includes('tix.fubonbraves.com'); 13 | const isKham = window.location.hostname.includes('kham.com.tw'); 14 | const isJKFace = window.location.hostname.includes('jkface.net'); 15 | 16 | // 仅在适用的网站上应用筛选 17 | if (!isIbon && !isTixcraft && !isKktix && !isCityline && !isEra && !isFami && !isWdragons && !isCtbcsports && !isFubonbraves && !isKham && !isJKFace) return; 18 | 19 | // 特別處理 ibon 網站 20 | if (isIbon) { 21 | const styleElement = document.createElement('style'); 22 | styleElement.id = 'ticket-filter-early-style-ibon'; 23 | styleElement.textContent = ` 24 | /* ibon 網站 - 使用不透明度和顯示過渡效果 */ 25 | body { 26 | opacity: 0.01 !important; 27 | transition: opacity 0.3s ease-out; 28 | } 29 | body.filter-ready { 30 | opacity: 1 !important; 31 | } 32 | `; 33 | document.documentElement.appendChild(styleElement); 34 | 35 | // 監聽過濾完成的消息 36 | window.addEventListener('message', function(event) { 37 | if (event.data.type === 'FILTER_APPLIED') { 38 | document.body.classList.add('filter-ready'); 39 | } 40 | }); 41 | 42 | // 安全機制:確保不會永久隱藏 43 | setTimeout(() => { 44 | document.body.classList.add('filter-ready'); 45 | }, 800); 46 | 47 | return; // 對 ibon 網站使用專門的處理,不執行後續代碼 48 | } 49 | 50 | // 为不同网站使用不同的隐藏方式 51 | if (isTixcraft) { 52 | // 拓元网站 - 使用visibility隐藏 53 | const styleElement = document.createElement('style'); 54 | styleElement.id = 'ticket-filter-early-style'; 55 | styleElement.textContent = ` 56 | /* 拓元网站 */ 57 | .area-list li, 58 | .zone-label[data-id] { 59 | visibility: hidden !important; 60 | } 61 | `; 62 | document.documentElement.appendChild(styleElement); 63 | } else if (isKktix) { 64 | // KKTIX网站 - 使用较轻量的透明度处理,避免页面空白 65 | const styleElement = document.createElement('style'); 66 | styleElement.id = 'ticket-filter-early-style-kktix'; 67 | styleElement.textContent = ` 68 | /* KKTIX网站 - 只减少不透明度而不是完全隐藏 */ 69 | .ticket-unit { 70 | opacity: 0.01; 71 | transition: opacity 0.3s ease-in; 72 | } 73 | `; 74 | document.documentElement.appendChild(styleElement); 75 | 76 | // 设置一个较短的超时,确保KKTIX可以快速显示 77 | setTimeout(() => { 78 | // 如果筛选还没完成,先显示所有元素 79 | if (!window.ticketFilterComplete) { 80 | document.querySelectorAll('.ticket-unit').forEach(el => { 81 | el.style.opacity = '1'; 82 | }); 83 | } 84 | }, 800); // 800ms后如果筛选未完成就显示 85 | } else if (isCityline) { 86 | // Cityline网站 - 使用更快速的处理方式 87 | const styleElement = document.createElement('style'); 88 | styleElement.id = 'ticket-filter-early-style-cityline'; 89 | styleElement.textContent = ` 90 | /* Cityline网站 - 使用更高效的隐藏方式并添加过渡效果 */ 91 | .price-box1, .form-check { 92 | visibility: hidden !important; 93 | opacity: 0 !important; 94 | transition: opacity 0.15s ease-out !important; 95 | } 96 | 97 | /* 隐藏票价区域,确保用户在筛选完成前看不到 */ 98 | .price-box1 > div.price { 99 | visibility: hidden !important; 100 | } 101 | 102 | /* 应用更快的过渡效果,在筛选完成后显示 */ 103 | .price-box1.filter-ready, .form-check.filter-ready { 104 | visibility: visible !important; 105 | opacity: 1 !important; 106 | transition: opacity 0.15s ease-out !important; 107 | } 108 | `; 109 | document.documentElement.appendChild(styleElement); 110 | 111 | // 创建一个变量记录筛选是否已经开始 112 | window.citylineFilterStarted = false; 113 | 114 | // 通知 content.js 脚本立即开始筛选,不要等待 115 | setTimeout(() => { 116 | window.postMessage({ type: 'CITYLINE_READY_FOR_FILTER' }, '*'); 117 | }, 10); 118 | 119 | // 立即运行 MutationObserver 以捕获任何动态添加的票券元素 120 | const citylineObserver = new MutationObserver((mutations) => { 121 | // 如果筛选已经完成,不进行处理 122 | if (window.ticketFilterComplete) return; 123 | 124 | // 筛选已经开始但尚未完成 125 | if (window.citylineFilterStarted) { 126 | let ticketElementsFound = false; 127 | 128 | // 仅处理新添加的节点 129 | for (const mutation of mutations) { 130 | if (mutation.addedNodes && mutation.addedNodes.length) { 131 | for (const node of mutation.addedNodes) { 132 | if (node.nodeType === 1) { // 元素节点 133 | const ticketElements = node.querySelectorAll ? 134 | node.querySelectorAll('.price-box1, .form-check') : []; 135 | 136 | if (ticketElements.length > 0) { 137 | ticketElementsFound = true; 138 | // 这里不做任何处理,保持其隐藏状态 139 | } 140 | 141 | // 如果节点本身就是票券元素 142 | if (node.matches && (node.matches('.price-box1') || node.matches('.form-check'))) { 143 | ticketElementsFound = true; 144 | } 145 | } 146 | } 147 | } 148 | } 149 | 150 | // 如果找到票券元素,通知 content.js 有新的票券要处理 151 | if (ticketElementsFound) { 152 | window.postMessage({ type: 'NEW_TICKETS_FOUND' }, '*'); 153 | } 154 | } else { 155 | // 筛选尚未开始,但看到了票券元素,通知 content.js 立即开始筛选 156 | const ticketElements = document.querySelectorAll('.price-box1, .form-check'); 157 | if (ticketElements.length > 0) { 158 | window.citylineFilterStarted = true; 159 | window.postMessage({ type: 'CITYLINE_READY_FOR_FILTER' }, '*'); 160 | } 161 | } 162 | }); 163 | 164 | // 使用更高效的配置,只观察必要的变化 165 | citylineObserver.observe(document.body, { 166 | childList: true, 167 | subtree: true, 168 | attributes: false, 169 | characterData: false 170 | }); 171 | 172 | // 设置一个较短的超时,确保Cityline可以更快地显示 173 | setTimeout(() => { 174 | // 如果筛选还没完成,但处理已经开始,给予更多时间 175 | if (window.citylineFilterStarted && !window.ticketFilterComplete) { 176 | // 再等待一段时间,因为筛选已经开始 177 | return; 178 | } 179 | 180 | // 如果筛选还没完成且未开始,为避免永久隐藏,显示所有元素 181 | if (!window.ticketFilterComplete) { 182 | document.querySelectorAll('.price-box1, .form-check').forEach(el => { 183 | el.classList.add('filter-ready'); 184 | }); 185 | } 186 | }, 800); // 800ms后检查状态 187 | 188 | // 设置最终超时作为安全机制 189 | setTimeout(() => { 190 | if (!window.ticketFilterComplete) { 191 | document.querySelectorAll('.price-box1, .form-check').forEach(el => { 192 | el.classList.add('filter-ready'); 193 | }); 194 | // 断开观察者以提高性能 195 | citylineObserver.disconnect(); 196 | } 197 | }, 1500); // 1.5秒后如果筛选未完成就显示 198 | } else if (isEra) { 199 | // 年代售票网站 - 使用visibility隐藏 200 | const styleElement = document.createElement('style'); 201 | styleElement.id = 'ticket-filter-early-style-era'; 202 | styleElement.textContent = ` 203 | /* 年代售票网站 */ 204 | .area-list li { 205 | visibility: hidden !important; 206 | } 207 | .area-list li.filter-ready { 208 | visibility: visible !important; 209 | transition: opacity 0.2s ease-out; 210 | } 211 | `; 212 | document.documentElement.appendChild(styleElement); 213 | } else if (isFami || isWdragons || isCtbcsports || isFubonbraves) { 214 | // Fami Life 网站及相关网站 - 使用visibility和opacity结合的方式 215 | const styleElement = document.createElement('style'); 216 | styleElement.id = 'ticket-filter-early-style-fami'; 217 | styleElement.textContent = ` 218 | /* Fami Life 网站及相关网站 - 初始隐藏所有票券 */ 219 | .f1 .saleTr { 220 | visibility: hidden !important; 221 | opacity: 0 !important; 222 | transition: visibility 0s, opacity 0.2s ease-out !important; 223 | } 224 | 225 | /* 当筛选完成后显示 */ 226 | .f1 .saleTr.filter-ready { 227 | visibility: visible !important; 228 | opacity: 1 !important; 229 | } 230 | 231 | /* 确保被筛选隐藏的票券保持隐藏 */ 232 | .f1 .saleTr.filter-ready[style*="display: none"] { 233 | display: none !important; 234 | visibility: hidden !important; 235 | opacity: 0 !important; 236 | } 237 | `; 238 | document.documentElement.appendChild(styleElement); 239 | } else if (isKham) { 240 | // 寬宏售票网站 - 使用visibility和opacity结合的方式 241 | const styleElement = document.createElement('style'); 242 | styleElement.id = 'ticket-filter-early-style-kham'; 243 | styleElement.textContent = ` 244 | /* 寬宏售票网站 - 初始隐藏所有票券 */ 245 | tr.status_tr { 246 | visibility: hidden !important; 247 | opacity: 0 !important; 248 | transition: visibility 0s, opacity 0.2s ease-out !important; 249 | } 250 | 251 | /* 当筛选完成后显示 */ 252 | tr.status_tr.filter-ready { 253 | visibility: visible !important; 254 | opacity: 1 !important; 255 | } 256 | 257 | /* 确保被筛选隐藏的票券保持隐藏 */ 258 | tr.status_tr.filter-ready[style*="display: none"] { 259 | display: none !important; 260 | visibility: hidden !important; 261 | opacity: 0 !important; 262 | } 263 | `; 264 | document.documentElement.appendChild(styleElement); 265 | } else if (isJKFace) { 266 | // JKFace网站 - 使用visibility和opacity结合的方式 267 | const styleElement = document.createElement('style'); 268 | styleElement.id = 'ticket-filter-early-style-jkface'; 269 | styleElement.textContent = ` 270 | /* JKFace网站 - 初始隐藏所有票券 */ 271 | body:not(.extension-disabled) section.mx-3 { 272 | visibility: hidden !important; 273 | opacity: 0 !important; 274 | transition: visibility 0s, opacity 0.2s ease-out !important; 275 | } 276 | 277 | /* 当筛选完成后显示 */ 278 | body:not(.extension-disabled) section.mx-3.filter-ready { 279 | visibility: visible !important; 280 | opacity: 1 !important; 281 | } 282 | 283 | /* 确保被筛选隐藏的票券保持隐藏 */ 284 | body:not(.extension-disabled) section.mx-3.filter-ready[style*="display: none"] { 285 | display: none !important; 286 | visibility: hidden !important; 287 | opacity: 0 !important; 288 | } 289 | 290 | /* 停用時顯示所有票券 */ 291 | body.extension-disabled section.mx-3 { 292 | visibility: visible !important; 293 | opacity: 1 !important; 294 | } 295 | `; 296 | document.documentElement.appendChild(styleElement); 297 | 298 | // 監聽擴充功能啟用狀態變化 299 | chrome.storage.onChanged.addListener(function(changes) { 300 | if (changes.extensionEnabled) { 301 | if (changes.extensionEnabled.newValue === false) { 302 | document.body.classList.add('extension-disabled'); 303 | // 移除所有篩選相關的 class 304 | document.querySelectorAll('section.mx-3').forEach(section => { 305 | section.classList.remove('filter-ready'); 306 | section.style.removeProperty('display'); 307 | }); 308 | } else { 309 | document.body.classList.remove('extension-disabled'); 310 | } 311 | } 312 | }); 313 | 314 | // 初始化時檢查擴充功能狀態 315 | chrome.storage.local.get(['extensionEnabled'], function(result) { 316 | if (result.extensionEnabled === false) { 317 | document.body.classList.add('extension-disabled'); 318 | } 319 | }); 320 | 321 | // 监听日期按钮点击和动态内容更新 322 | const jkfaceObserver = new MutationObserver((mutations) => { 323 | // 檢查擴充功能是否已停用 324 | if (document.body.classList.contains('extension-disabled')) { 325 | return; 326 | } 327 | 328 | for (const mutation of mutations) { 329 | if (mutation.addedNodes && mutation.addedNodes.length) { 330 | mutation.addedNodes.forEach(node => { 331 | if (node.nodeType === 1 && // 元素节点 332 | node.matches && // 确保有matches方法 333 | node.matches('section.mx-3')) { 334 | // 确保新添加的section保持隐藏状态 335 | node.style.visibility = 'hidden'; 336 | node.style.opacity = '0'; 337 | node.classList.remove('filter-ready'); 338 | } 339 | }); 340 | } 341 | } 342 | }); 343 | 344 | // 观察整个文档的变化 345 | jkfaceObserver.observe(document.documentElement, { 346 | childList: true, 347 | subtree: true 348 | }); 349 | 350 | // 设置一个较短的超时,确保不会永久隱藏 351 | setTimeout(() => { 352 | if (!window.ticketFilterComplete && !document.body.classList.contains('extension-disabled')) { 353 | document.querySelectorAll('section.mx-3').forEach(el => { 354 | if (el.style.display !== 'none') { 355 | el.classList.add('filter-ready'); 356 | } 357 | }); 358 | } 359 | }, 1500); 360 | } 361 | 362 | // 记录是否已收到筛选完成消息 363 | let filterApplied = false; 364 | 365 | // Shadow DOM处理相关变量 366 | let shadowObserver = null; 367 | const shadowStyleCache = new Set(); // 缓存所有添加的style元素 368 | 369 | // 处理Shadow DOM的函数 - 为ibon网站 370 | function handleIbonShadowDOM() { 371 | // 尝试向shadowRoot中注入样式的函数 372 | function injectStyleToShadowRoot(shadowRoot) { 373 | if (!shadowRoot) return; 374 | 375 | // 避免重复注入 376 | if (shadowRoot.querySelector('#ticket-filter-shadow-style')) return; 377 | 378 | try { 379 | const shadowStyle = document.createElement('style'); 380 | shadowStyle.id = 'ticket-filter-shadow-style'; 381 | shadowStyle.textContent = ` 382 | /* ibon网站 - 仅在筛选未完成前隐藏 */ 383 | tr[id^="B"] { 384 | opacity: 0; 385 | transition: opacity 0.2s; 386 | } 387 | `; 388 | shadowRoot.appendChild(shadowStyle); 389 | shadowStyleCache.add(shadowRoot); // 记录已注入样式的shadowRoot 390 | } catch (err) { 391 | // 静默错误 392 | } 393 | } 394 | 395 | // 递归处理所有可能包含shadowRoot的元素 396 | function checkShadowRoots(element) { 397 | if (!element) return; 398 | 399 | // 处理当前元素的shadowRoot 400 | if (element.shadowRoot) { 401 | injectStyleToShadowRoot(element.shadowRoot); 402 | } 403 | 404 | // 递归处理子元素 405 | const children = element.children; 406 | if (children && children.length) { 407 | for (let i = 0; i < children.length; i++) { 408 | checkShadowRoots(children[i]); 409 | } 410 | } 411 | } 412 | 413 | // 初始处理 414 | checkShadowRoots(document); 415 | 416 | // 监听DOM变化以处理动态添加的元素 417 | shadowObserver = new MutationObserver((mutations) => { 418 | if (filterApplied) { 419 | shadowObserver.disconnect(); 420 | return; 421 | } 422 | 423 | for (const mutation of mutations) { 424 | if (mutation.addedNodes && mutation.addedNodes.length) { 425 | for (const node of mutation.addedNodes) { 426 | if (node.nodeType === 1) { // 元素节点 427 | checkShadowRoots(node); 428 | } 429 | } 430 | } 431 | } 432 | }); 433 | 434 | shadowObserver.observe(document.documentElement, { 435 | childList: true, 436 | subtree: true 437 | }); 438 | } 439 | 440 | // 显示所有元素的函数 441 | function showAllElements() { 442 | filterApplied = true; 443 | // 告诉window对象筛选已完成 444 | window.ticketFilterComplete = true; 445 | 446 | // 针对不同网站处理样式 447 | if (isTixcraft) { 448 | const earlyStyle = document.getElementById('ticket-filter-early-style'); 449 | if (earlyStyle) { 450 | earlyStyle.remove(); 451 | } 452 | } else if (isKktix) { 453 | // 对于KKTIX,修改样式使元素显示出来,而不是移除 454 | const kktixStyle = document.getElementById('ticket-filter-early-style-kktix'); 455 | if (kktixStyle) { 456 | kktixStyle.textContent = ` 457 | .ticket-unit { 458 | opacity: 1 !important; 459 | transition: opacity 0.2s ease-out; 460 | } 461 | `; 462 | } 463 | 464 | // 直接修改所有票券元素的样式 465 | document.querySelectorAll('.ticket-unit').forEach(el => { 466 | el.style.opacity = '1'; 467 | }); 468 | } else if (isCityline) { 469 | // 对于Cityline,使用class切换来显示元素 470 | document.querySelectorAll('.price-box1, .form-check').forEach(el => { 471 | el.classList.add('filter-ready'); 472 | }); 473 | 474 | // 确保所有需要显示的元素都被正确显示(使用更短的延迟) 475 | setTimeout(() => { 476 | // 获取当前所有应该显示的元素(不隐藏的) 477 | document.querySelectorAll('.form-check').forEach(el => { 478 | if (el.style.display !== 'none') { 479 | el.style.visibility = 'visible'; 480 | el.style.opacity = '1'; 481 | } 482 | }); 483 | }, 10); // 更短的延迟确保 DOM 快速更新 484 | } else if (isEra) { 485 | // 对于年代售票,使用class切换来显示元素 486 | document.querySelectorAll('.area-list li').forEach(el => { 487 | el.classList.add('filter-ready'); 488 | }); 489 | } else if (isFami || isWdragons || isCtbcsports || isFubonbraves) { 490 | // 对于 Fami Life 及相关网站,使用 class 切换来显示元素 491 | document.querySelectorAll('.saleTr').forEach(el => { 492 | if (el.style.display !== 'none') { 493 | el.classList.add('filter-ready'); 494 | } 495 | }); 496 | } else if (isKham) { 497 | // 对于寬宏售票,使用 class 切换来显示元素 498 | document.querySelectorAll('.status_tr').forEach(el => { 499 | if (el.style.display !== 'none') { 500 | el.classList.add('filter-ready'); 501 | } 502 | }); 503 | } else if (isJKFace) { 504 | // 对于 JKFace,使用 class 切换来显示元素 505 | document.querySelectorAll('section.mx-3').forEach(el => { 506 | if (el.style.display !== 'none') { 507 | el.classList.add('filter-ready'); 508 | } 509 | }); 510 | 511 | // 如果有观察者,在筛选完成后断开连接 512 | const jkfaceObserver = document.querySelector('#ticket-filter-early-style-jkface')?.jkfaceObserver; 513 | if (jkfaceObserver) { 514 | jkfaceObserver.disconnect(); 515 | } 516 | } 517 | 518 | // 处理ibon的Shadow DOM 519 | if (isIbon) { 520 | // 停止观察者 521 | if (shadowObserver) { 522 | shadowObserver.disconnect(); 523 | } 524 | 525 | // 从Shadow DOM中移除样式,或者将所有tr设为可见 526 | shadowStyleCache.forEach(shadowRoot => { 527 | try { 528 | const style = shadowRoot.querySelector('#ticket-filter-shadow-style'); 529 | if (style) { 530 | // 修改样式使元素显示,而不是直接移除 531 | style.textContent = ` 532 | tr[id^="B"] { 533 | opacity: 1 !important; 534 | transition: opacity 0.2s; 535 | } 536 | `; 537 | } 538 | } catch (err) { 539 | // 静默错误 540 | } 541 | }); 542 | 543 | // 额外措施:通过contentWindow直接执行脚本使票券可见 544 | try { 545 | const showAllRows = ` 546 | function makeAllRowsVisible(root) { 547 | if (root.shadowRoot) { 548 | const rows = root.shadowRoot.querySelectorAll('tr[id^="B"]'); 549 | rows.forEach(row => { 550 | row.style.removeProperty('display'); 551 | row.style.opacity = '1'; 552 | row.style.visibility = 'visible'; 553 | }); 554 | } 555 | 556 | if (root.children) { 557 | Array.from(root.children).forEach(child => { 558 | makeAllRowsVisible(child); 559 | }); 560 | } 561 | } 562 | 563 | makeAllRowsVisible(document.documentElement); 564 | `; 565 | 566 | const scriptTag = document.createElement('script'); 567 | scriptTag.textContent = showAllRows; 568 | document.documentElement.appendChild(scriptTag); 569 | document.documentElement.removeChild(scriptTag); 570 | } catch (e) { 571 | // 静默错误 572 | } 573 | } 574 | } 575 | 576 | // 如果是ibon网站,启用Shadow DOM处理 577 | if (isIbon) { 578 | // 延迟一点点再执行,等待iframe和shadowDOM创建 579 | setTimeout(() => { 580 | handleIbonShadowDOM(); 581 | }, 10); 582 | } 583 | 584 | // 监听content.js加载完成的消息 585 | window.addEventListener('message', function(event) { 586 | // 筛选应用完成 587 | if (event.data && event.data.type === 'FILTER_APPLIED') { 588 | showAllElements(); 589 | } 590 | 591 | // content.js已加载但尚未完成筛选,延长超时时间 592 | if (event.data && event.data.type === 'CONTENT_JS_LOADED' && !filterApplied) { 593 | // Cityline网站的特殊处理,主动通知可以开始筛选 594 | if (isCityline) { 595 | setTimeout(() => { 596 | window.postMessage({ type: 'CITYLINE_READY_FOR_FILTER' }, '*'); 597 | }, 10); 598 | } 599 | 600 | // 设置更长的超时时间 601 | setTimeout(() => { 602 | if (!filterApplied) { // 如果还没有应用过筛选 603 | showAllElements(); 604 | } 605 | }, 2000); // 2秒后如果筛选未完成就显示 606 | } 607 | }); 608 | 609 | // 设置安全超时,防止永久隐藏 610 | setTimeout(() => { 611 | if (!filterApplied) { 612 | showAllElements(); 613 | } 614 | }, 1500); // 更短的超时时间,确保元素不会长时间隐藏 615 | })(); -------------------------------------------------------------------------------- /popup.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', () => { 2 | const areaFilter = document.getElementById('areaFilter'); 3 | const blacklistFilter = document.getElementById('blacklistFilter'); 4 | const keywordsContainer = document.getElementById('keywordsContainer'); 5 | const blacklistContainer = document.getElementById('blacklistContainer'); 6 | const showSoldOut = document.getElementById('showSoldOut'); 7 | const showAll = document.getElementById('showAll'); 8 | const addKeyword = document.getElementById('addKeyword'); 9 | const addBlacklist = document.getElementById('addBlacklist'); 10 | const currentFilter = document.getElementById('currentFilter'); 11 | const filterText = document.getElementById('filterText'); 12 | const showServerTime = document.getElementById('showServerTime'); 13 | const showFilterStatus = document.getElementById('showFilterStatus'); 14 | const showHelpText = document.getElementById('showHelpText'); 15 | const extensionSwitch = document.getElementById('extensionEnabled'); 16 | const settingsIcon = document.querySelector('.settings-icon'); 17 | const helpTexts = document.querySelectorAll('.help-text'); 18 | 19 | // 控制所有設定項目的啟用/禁用狀態 20 | function toggleControls(enabled) { 21 | const controls = [ 22 | areaFilter, 23 | showSoldOut, 24 | showAll, 25 | addKeyword, 26 | addBlacklist, 27 | showServerTime, 28 | showFilterStatus, 29 | showHelpText, 30 | settingsIcon 31 | ]; 32 | 33 | controls.forEach(control => { 34 | if (control) { 35 | control.disabled = !enabled; 36 | if (control === settingsIcon) { 37 | control.style.opacity = enabled ? '1' : '0.5'; 38 | control.style.pointerEvents = enabled ? 'auto' : 'none'; 39 | } 40 | } 41 | }); 42 | 43 | // 設定關鍵字容器的樣式 44 | if (keywordsContainer) { 45 | keywordsContainer.style.opacity = enabled ? '1' : '0.5'; 46 | keywordsContainer.style.pointerEvents = enabled ? 'auto' : 'none'; 47 | } 48 | 49 | // 設定整個內容區域的樣式 50 | const contentArea = document.querySelector('.content'); 51 | if (contentArea) { 52 | const sections = contentArea.querySelectorAll('.filter-section, .time-toggle, .settings-panel'); 53 | sections.forEach(section => { 54 | section.style.opacity = enabled ? '1' : '0.5'; 55 | section.style.pointerEvents = enabled ? 'auto' : 'none'; 56 | }); 57 | } 58 | } 59 | 60 | // 載入擴充功能狀態並設定控制項狀態 61 | chrome.storage.local.get(['extensionEnabled'], function(result) { 62 | extensionSwitch.checked = result.extensionEnabled !== false; 63 | toggleControls(result.extensionEnabled !== false); 64 | }); 65 | 66 | // 監聽主開關變化 67 | extensionSwitch.addEventListener('change', function(e) { 68 | const enabled = e.target.checked; 69 | chrome.storage.local.set({ extensionEnabled: enabled }); 70 | toggleControls(enabled); 71 | }); 72 | 73 | // 載入提示訊息顯示設定 74 | chrome.storage.local.get(['showHelpText'], function(result) { 75 | showHelpText.checked = result.showHelpText !== false; 76 | toggleHelpText(result.showHelpText !== false); 77 | }); 78 | 79 | // 監聽提示訊息顯示設定變化 80 | showHelpText.addEventListener('change', function(e) { 81 | const show = e.target.checked; 82 | chrome.storage.local.set({ showHelpText: show }); 83 | toggleHelpText(show); 84 | }); 85 | 86 | // 切換提示訊息顯示狀態 87 | function toggleHelpText(show) { 88 | helpTexts.forEach(text => { 89 | text.style.display = show ? '' : 'none'; 90 | }); 91 | } 92 | 93 | // 添加搜尋說明 94 | const helpText = document.createElement('div'); 95 | helpText.className = 'help-text'; 96 | helpText.innerHTML = ` 97 |

98 | 搜尋格式:3200,A區 (同時符合) 或 4500+3200 (任一符合) 99 |

100 | `; 101 | 102 | // 將說明插入到輸入框上方 103 | const exampleText = areaFilter.previousElementSibling; 104 | if (exampleText) { 105 | exampleText.parentNode.insertBefore(helpText, exampleText.nextSibling); 106 | } 107 | 108 | let keywords = new Set(); 109 | let blacklist = new Set(); 110 | let currentSite = null; 111 | 112 | // 檢查當前網站類型 113 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 114 | if (tabs[0]) { 115 | const url = tabs[0].url; 116 | if (url.includes('tixcraft.com') || url.includes('tixcraftweb-pcox.onrender.com')) { 117 | currentSite = 'tixcraft'; 118 | loadTixcraftSettings(); 119 | document.body.setAttribute('data-site', 'tixcraft'); 120 | } else if (url.includes('kktix.com')) { 121 | currentSite = 'kktix'; 122 | loadKKTIXSettings(); 123 | document.body.setAttribute('data-site', 'kktix'); 124 | } else if (url.includes('ibon.com.tw')) { 125 | currentSite = 'ibon'; 126 | loadIbonSettings(); 127 | document.body.setAttribute('data-site', 'ibon'); 128 | } else if (url.includes('cityline.com')) { 129 | currentSite = 'cityline'; 130 | loadCitylineSettings(); 131 | document.body.setAttribute('data-site', 'cityline'); 132 | } else if (url.includes('ticket.com.tw')) { 133 | currentSite = 'ticket'; 134 | loadTicketSettings(); 135 | document.body.setAttribute('data-site', 'ticket'); 136 | } else if (url.includes('ticketplus.com.tw')) { 137 | currentSite = 'ticketplus'; 138 | loadTicketPlusSettings(); 139 | document.body.setAttribute('data-site', 'ticketplus'); 140 | } else if (url.includes('fami.life')) { 141 | console.log('检测到 Fami Life 网站,开始加载设置'); 142 | currentSite = 'fami'; 143 | document.body.setAttribute('data-site', 'fami'); 144 | } else if (url.includes('tix.wdragons.com') || url.includes('tix.ctbcsports.com') || url.includes('tix.fubonbraves.com')) { 145 | console.log('检测到 Fami Life 相关网站,开始加载设置'); 146 | currentSite = 'fami'; 147 | document.body.setAttribute('data-site', 'fami'); 148 | // 确保在设置 data-site 后再加载设置 149 | chrome.storage.local.get(['famiKeywords', 'famiBlacklist', 'famiHideSoldOut'], (result) => { 150 | console.log('加载到的设置:', result); 151 | if (result.famiKeywords) { 152 | keywords = new Set(result.famiKeywords); 153 | } else { 154 | keywords = new Set(); 155 | } 156 | if (result.famiBlacklist) { 157 | blacklist = new Set(result.famiBlacklist); 158 | } else { 159 | blacklist = new Set(); 160 | } 161 | showSoldOut.checked = result.famiHideSoldOut || false; 162 | 163 | // 确保 UI 更新 164 | renderKeywords(); 165 | renderBlacklist(); 166 | updateFilterLabel(); 167 | }); 168 | } else if (url.includes('jkface.net')) { 169 | console.log('检测到 JKFace 网站,开始加载设置'); 170 | currentSite = 'jkface'; 171 | document.body.setAttribute('data-site', 'jkface'); 172 | // 加载 JKFace 设置 173 | chrome.storage.local.get(['jkfaceKeywords', 'jkfaceBlacklist', 'jkfaceHideSoldOut'], (result) => { 174 | console.log('加载到的设置:', result); 175 | if (result.jkfaceKeywords) { 176 | keywords = new Set(result.jkfaceKeywords); 177 | } else { 178 | keywords = new Set(); 179 | } 180 | if (result.jkfaceBlacklist) { 181 | blacklist = new Set(result.jkfaceBlacklist); 182 | } else { 183 | blacklist = new Set(); 184 | } 185 | showSoldOut.checked = result.jkfaceHideSoldOut || false; 186 | 187 | // 确保 UI 更新 188 | renderKeywords(); 189 | renderBlacklist(); 190 | updateFilterLabel(); 191 | }); 192 | } else if (url.includes('kham.com.tw')) { 193 | currentSite = 'kham'; 194 | document.body.setAttribute('data-site', 'kham'); 195 | // 加载 Kham 设置 196 | chrome.storage.local.get(['khamKeywords', 'khamBlacklist', 'khamHideSoldOut'], (result) => { 197 | console.log('加载到的设置:', result); 198 | if (result.khamKeywords) { 199 | keywords = new Set(result.khamKeywords); 200 | } else { 201 | keywords = new Set(); 202 | } 203 | if (result.khamBlacklist) { 204 | blacklist = new Set(result.khamBlacklist); 205 | } else { 206 | blacklist = new Set(); 207 | } 208 | showSoldOut.checked = result.khamHideSoldOut || false; 209 | 210 | // 确保 UI 更新 211 | renderKeywords(); 212 | renderBlacklist(); 213 | updateFilterLabel(); 214 | }); 215 | } 216 | } 217 | }); 218 | 219 | // 載入拓元設定 220 | function loadTixcraftSettings() { 221 | chrome.storage.local.get(['keywords', 'blacklist', 'hideSoldOut'], (result) => { 222 | if (result.keywords) { 223 | keywords = new Set(result.keywords); 224 | renderKeywords(); 225 | updateFilterLabel(); 226 | } 227 | if (result.blacklist) { 228 | blacklist = new Set(result.blacklist); 229 | renderBlacklist(); 230 | } 231 | if (result.hideSoldOut !== undefined) { 232 | showSoldOut.checked = result.hideSoldOut; 233 | } 234 | }); 235 | } 236 | 237 | // 載入KKTIX設定 238 | function loadKKTIXSettings() { 239 | chrome.storage.sync.get(['targetKeywords', 'blacklist', 'hideSoldOut'], (result) => { 240 | if (result.targetKeywords) { 241 | keywords = new Set(result.targetKeywords); 242 | renderKeywords(); 243 | updateFilterLabel(); 244 | } 245 | if (result.blacklist) { 246 | blacklist = new Set(result.blacklist); 247 | renderBlacklist(); 248 | } 249 | if (result.hideSoldOut !== undefined) { 250 | showSoldOut.checked = result.hideSoldOut; 251 | } 252 | }); 253 | } 254 | 255 | // 載入ibon設定 256 | function loadIbonSettings() { 257 | chrome.storage.local.get(['ibonKeywords', 'ibonBlacklist', 'ibonHideSoldOut'], (result) => { 258 | if (result.ibonKeywords) { 259 | keywords = new Set(result.ibonKeywords); 260 | renderKeywords(); 261 | updateFilterLabel(); 262 | } 263 | if (result.ibonBlacklist) { 264 | blacklist = new Set(result.ibonBlacklist); 265 | renderBlacklist(); 266 | } 267 | if (result.ibonHideSoldOut !== undefined) { 268 | showSoldOut.checked = result.ibonHideSoldOut; 269 | } 270 | }); 271 | } 272 | 273 | // 載入Cityline設定 274 | function loadCitylineSettings() { 275 | chrome.storage.local.get(['citylineKeywords', 'citylineBlacklist', 'citylineHideSoldOut'], (result) => { 276 | if (result.citylineKeywords) { 277 | keywords = new Set(result.citylineKeywords); 278 | renderKeywords(); 279 | updateFilterLabel(); 280 | } 281 | if (result.citylineBlacklist) { 282 | blacklist = new Set(result.citylineBlacklist); 283 | renderBlacklist(); 284 | } 285 | if (result.citylineHideSoldOut !== undefined) { 286 | showSoldOut.checked = result.citylineHideSoldOut; 287 | } 288 | }); 289 | } 290 | 291 | // 載入年代售票設定 292 | function loadTicketSettings() { 293 | chrome.storage.local.get(['ticketKeywords', 'ticketBlacklist', 'ticketHideSoldOut'], (result) => { 294 | if (result.ticketKeywords) { 295 | keywords = new Set(result.ticketKeywords); 296 | renderKeywords(); 297 | updateFilterLabel(); 298 | } 299 | if (result.ticketBlacklist) { 300 | blacklist = new Set(result.ticketBlacklist); 301 | renderBlacklist(); 302 | } 303 | if (result.ticketHideSoldOut !== undefined) { 304 | showSoldOut.checked = result.ticketHideSoldOut; 305 | } 306 | }); 307 | } 308 | 309 | // 載入遠大售票平台設定 310 | function loadTicketPlusSettings() { 311 | chrome.storage.local.get(['ticketplusKeywords', 'ticketplusBlacklist', 'ticketplusHideSoldOut'], (result) => { 312 | if (result.ticketplusKeywords) { 313 | keywords = new Set(result.ticketplusKeywords); 314 | renderKeywords(); 315 | updateFilterLabel(); 316 | } 317 | if (result.ticketplusBlacklist) { 318 | blacklist = new Set(result.ticketplusBlacklist); 319 | renderBlacklist(); 320 | } 321 | if (result.ticketplusHideSoldOut !== undefined) { 322 | showSoldOut.checked = result.ticketplusHideSoldOut; 323 | } 324 | }); 325 | } 326 | 327 | // 載入 Fami Life 設定 328 | function loadFamiSettings() { 329 | chrome.storage.local.get(['famiKeywords', 'famiBlacklist', 'famiHideSoldOut'], (result) => { 330 | if (result.famiKeywords) { 331 | keywords = new Set(result.famiKeywords); 332 | renderKeywords(); 333 | updateFilterLabel(); 334 | } 335 | if (result.famiBlacklist) { 336 | blacklist = new Set(result.famiBlacklist); 337 | renderBlacklist(); 338 | } 339 | if (result.famiHideSoldOut !== undefined) { 340 | showSoldOut.checked = result.famiHideSoldOut; 341 | } 342 | }); 343 | } 344 | 345 | // Update filter label 346 | function updateFilterLabel() { 347 | if (keywords.size > 0) { 348 | currentFilter.style.display = 'inline-block'; 349 | filterText.textContent = Array.from(keywords).join('、'); 350 | } else { 351 | currentFilter.style.display = 'none'; 352 | } 353 | } 354 | 355 | // Render keyword tags 356 | function renderKeywords() { 357 | keywordsContainer.innerHTML = ''; 358 | 359 | if (keywords.size === 0) { 360 | keywordsContainer.innerHTML = '尚未設定篩選條件'; 361 | 362 | // 只在用户主动清除时才隐藏标签 363 | if (currentFilter) { 364 | currentFilter.style.display = 'none'; 365 | } 366 | if (filterText) { 367 | filterText.textContent = ''; 368 | } 369 | return; 370 | } 371 | 372 | keywords.forEach(keyword => { 373 | const tag = document.createElement('div'); 374 | tag.className = 'keyword-tag'; 375 | tag.innerHTML = ` 376 | ${keyword} 377 | × 378 | `; 379 | keywordsContainer.appendChild(tag); 380 | }); 381 | 382 | // Add event listeners for remove buttons 383 | document.querySelectorAll('.keyword-tag .remove').forEach(removeBtn => { 384 | removeBtn.addEventListener('click', (e) => { 385 | const keyword = e.target.dataset.keyword; 386 | keywords.delete(keyword); 387 | renderKeywords(); 388 | updateFilterLabel(); 389 | // 只在用户主动删除时应用过滤 390 | applyFilters(); 391 | }); 392 | }); 393 | 394 | // 更新關鍵字顯示標籤 395 | if (currentFilter && filterText) { 396 | currentFilter.style.display = 'inline-block'; 397 | filterText.textContent = Array.from(keywords).join('、'); 398 | } 399 | } 400 | 401 | // Render blacklist tags 402 | function renderBlacklist() { 403 | blacklistContainer.innerHTML = ''; 404 | 405 | if (blacklist.size === 0) { 406 | blacklistContainer.innerHTML = '尚未設定黑名單'; 407 | const currentBlacklist = document.getElementById('currentBlacklist'); 408 | if (currentBlacklist) { 409 | currentBlacklist.style.display = 'none'; 410 | } 411 | return; 412 | } 413 | 414 | blacklist.forEach(keyword => { 415 | const tag = document.createElement('div'); 416 | tag.className = 'keyword-tag'; 417 | tag.innerHTML = ` 418 | ${keyword} 419 | × 420 | `; 421 | blacklistContainer.appendChild(tag); 422 | }); 423 | 424 | // 更新黑名單顯示標籤 425 | const currentBlacklist = document.getElementById('currentBlacklist'); 426 | const blacklistText = document.getElementById('blacklistText'); 427 | if (currentBlacklist && blacklistText) { 428 | currentBlacklist.style.display = 'inline-block'; 429 | blacklistText.textContent = Array.from(blacklist).join('、'); 430 | } 431 | 432 | // Add event listeners for remove buttons 433 | document.querySelectorAll('#blacklistContainer .keyword-tag .remove').forEach(removeBtn => { 434 | removeBtn.addEventListener('click', (e) => { 435 | const keyword = e.target.dataset.keyword; 436 | blacklist.delete(keyword); 437 | renderBlacklist(); 438 | // 只在用户主动删除时应用过滤 439 | applyFilters(); 440 | }); 441 | }); 442 | } 443 | 444 | // 自定義警告框函數 445 | function showCustomAlert(message) { 446 | return new Promise((resolve) => { 447 | const overlay = document.querySelector('.alert-overlay'); 448 | const alert = document.querySelector('.custom-alert'); 449 | const content = alert.querySelector('.alert-content'); 450 | const confirmBtn = alert.querySelector('.alert-confirm'); 451 | const cancelBtn = alert.querySelector('.alert-cancel'); 452 | 453 | content.textContent = message; 454 | overlay.style.display = 'block'; 455 | alert.style.display = 'block'; 456 | 457 | const handleConfirm = () => { 458 | overlay.style.display = 'none'; 459 | alert.style.display = 'none'; 460 | cleanup(); 461 | resolve(true); 462 | }; 463 | 464 | const handleCancel = () => { 465 | overlay.style.display = 'none'; 466 | alert.style.display = 'none'; 467 | cleanup(); 468 | resolve(false); 469 | }; 470 | 471 | const cleanup = () => { 472 | confirmBtn.removeEventListener('click', handleConfirm); 473 | cancelBtn.removeEventListener('click', handleCancel); 474 | overlay.removeEventListener('click', handleCancel); 475 | }; 476 | 477 | confirmBtn.addEventListener('click', handleConfirm); 478 | cancelBtn.addEventListener('click', handleCancel); 479 | overlay.addEventListener('click', handleCancel); 480 | }); 481 | } 482 | 483 | // 修改添加關鍵字的函數 484 | async function addKeywordToFilter() { 485 | const value = areaFilter.value.trim(); 486 | if (value) { 487 | const hasConflict = Array.from(blacklist).some(blacklistItem => { 488 | if (blacklistItem === value) return true; 489 | 490 | // 處理 OR 條件 (+) 491 | const blacklistOrParts = blacklistItem.split('+').map(p => p.trim()); 492 | const valueOrParts = value.split('+').map(p => p.trim()); 493 | 494 | // 處理 AND 條件 (,) 495 | const blacklistAndParts = blacklistOrParts.flatMap(p => p.split(',').map(p => p.trim())); 496 | const valueAndParts = valueOrParts.flatMap(p => p.split(',').map(p => p.trim())); 497 | 498 | return blacklistAndParts.some(bp => valueAndParts.includes(bp)); 499 | }); 500 | 501 | if (hasConflict) { 502 | const shouldAdd = await showCustomAlert('此條件與黑名單中的條件重複或衝突,是否仍要新增?'); 503 | if (!shouldAdd) return; 504 | } 505 | keywords.add(value); 506 | areaFilter.value = ''; 507 | renderKeywords(); 508 | updateFilterLabel(); 509 | applyFilters(); 510 | } 511 | } 512 | 513 | // Add keyword on Enter 514 | if (areaFilter) { 515 | areaFilter.addEventListener('keypress', (e) => { 516 | if (e.key === 'Enter') { 517 | e.preventDefault(); 518 | addKeywordToFilter(); 519 | } 520 | }); 521 | } 522 | 523 | // Add keyword on button click 524 | if (addKeyword) { 525 | addKeyword.addEventListener('click', (e) => { 526 | e.preventDefault(); 527 | addKeywordToFilter(); 528 | }); 529 | } 530 | 531 | // 修改添加黑名單的函數 532 | async function addBlacklistKeyword() { 533 | const value = blacklistFilter.value.trim(); 534 | if (value) { 535 | const hasConflict = Array.from(keywords).some(keyword => { 536 | if (keyword === value) return true; 537 | 538 | // 處理 OR 條件 (+) 539 | const keywordOrParts = keyword.split('+').map(p => p.trim()); 540 | const valueOrParts = value.split('+').map(p => p.trim()); 541 | 542 | // 處理 AND 條件 (,) 543 | const keywordAndParts = keywordOrParts.flatMap(p => p.split(',').map(p => p.trim())); 544 | const valueAndParts = valueOrParts.flatMap(p => p.split(',').map(p => p.trim())); 545 | 546 | return keywordAndParts.some(kp => valueAndParts.includes(kp)); 547 | }); 548 | 549 | if (hasConflict) { 550 | const shouldAdd = await showCustomAlert('此條件與關鍵字篩選中的條件重複或衝突,是否仍要新增?'); 551 | if (!shouldAdd) return; 552 | } 553 | blacklist.add(value); 554 | blacklistFilter.value = ''; 555 | renderBlacklist(); 556 | applyFilters(); 557 | } 558 | } 559 | 560 | // Add blacklist on Enter 561 | if (blacklistFilter) { 562 | blacklistFilter.addEventListener('keypress', (e) => { 563 | if (e.key === 'Enter') { 564 | e.preventDefault(); 565 | addBlacklistKeyword(); 566 | } 567 | }); 568 | } 569 | 570 | // Add blacklist on button click 571 | if (addBlacklist) { 572 | addBlacklist.addEventListener('click', (e) => { 573 | e.preventDefault(); 574 | addBlacklistKeyword(); 575 | }); 576 | } 577 | 578 | // Show all button click event 579 | if (showAll) { 580 | showAll.addEventListener('click', (e) => { 581 | e.preventDefault(); 582 | keywords.clear(); 583 | blacklist.clear(); 584 | renderKeywords(); 585 | renderBlacklist(); 586 | showSoldOut.checked = false; 587 | 588 | if (currentSite === 'tixcraft') { 589 | chrome.storage.local.set({ 590 | keywords: [], 591 | blacklist: [], 592 | hideSoldOut: false 593 | }, () => { 594 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 595 | if (tabs[0]) { 596 | chrome.tabs.sendMessage(tabs[0].id, { 597 | type: 'SHOW_ALL' 598 | }); 599 | } 600 | }); 601 | }); 602 | } else if (currentSite === 'kktix') { 603 | // 先清除所有設定 604 | chrome.storage.sync.remove(['targetKeywords', 'blacklist', 'showAllPrices', 'hideSoldOut'], () => { 605 | // 設置新的空值 606 | chrome.storage.sync.set({ 607 | targetKeywords: [], 608 | blacklist: [], 609 | showAllPrices: true, 610 | hideSoldOut: false 611 | }, () => { 612 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 613 | if (tabs[0]) { 614 | chrome.tabs.sendMessage(tabs[0].id, { 615 | action: 'showAllPrices', 616 | settings: { 617 | hideSoldOut: false 618 | } 619 | }, () => { 620 | // 強制重新載入設定 621 | loadKKTIXSettings(); 622 | // 強制更新標籤顯示 623 | renderKeywords(); 624 | renderBlacklist(); 625 | updateFilterLabel(); 626 | }); 627 | } 628 | }); 629 | }); 630 | }); 631 | } else if (currentSite === 'ibon') { 632 | // 先清除 storage 633 | chrome.storage.local.remove(['ibonKeywords', 'ibonBlacklist', 'ibonHideSoldOut'], () => { 634 | // 然後設置新的空值 635 | chrome.storage.local.set({ 636 | ibonKeywords: [], 637 | ibonBlacklist: [], 638 | ibonHideSoldOut: false 639 | }, () => { 640 | // 發送消息到 content script 641 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 642 | if (tabs[0]) { 643 | chrome.tabs.sendMessage(tabs[0].id, { 644 | type: 'UPDATE_IBON_SETTINGS', 645 | settings: { 646 | keywords: [], 647 | blacklist: [], 648 | hideSoldOut: false 649 | } 650 | }, () => { 651 | // 強制重新載入設定 652 | loadIbonSettings(); 653 | }); 654 | } 655 | }); 656 | }); 657 | }); 658 | } else if (currentSite === 'cityline') { 659 | chrome.storage.local.set({ 660 | citylineKeywords: [], 661 | citylineBlacklist: [], 662 | citylineHideSoldOut: false 663 | }, () => { 664 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 665 | if (tabs[0]) { 666 | chrome.tabs.sendMessage(tabs[0].id, { 667 | type: 'UPDATE_CITYLINE_SETTINGS', 668 | settings: { 669 | keywords: [], 670 | blacklist: [], 671 | hideSoldOut: false 672 | } 673 | }); 674 | } 675 | }); 676 | }); 677 | } else if (currentSite === 'ticket') { 678 | chrome.storage.local.set({ 679 | ticketKeywords: [], 680 | ticketBlacklist: [], 681 | ticketHideSoldOut: false 682 | }, () => { 683 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 684 | if (tabs[0]) { 685 | chrome.tabs.sendMessage(tabs[0].id, { 686 | type: 'UPDATE_TICKET_SETTINGS', 687 | settings: { 688 | keywords: [], 689 | blacklist: [], 690 | hideSoldOut: false 691 | } 692 | }); 693 | } 694 | }); 695 | }); 696 | } else if (currentSite === 'ticketplus') { 697 | chrome.storage.local.set({ 698 | ticketplusKeywords: [], 699 | ticketplusBlacklist: [], 700 | ticketplusHideSoldOut: false 701 | }, () => { 702 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 703 | if (tabs[0]) { 704 | chrome.tabs.sendMessage(tabs[0].id, { 705 | type: 'UPDATE_TICKETPLUS_SETTINGS', 706 | settings: { 707 | keywords: [], 708 | blacklist: [], 709 | hideSoldOut: false 710 | } 711 | }); 712 | } 713 | }); 714 | }); 715 | } else if (currentSite === 'fami' || currentSite === 'wdragons' || currentSite === 'ctbcsports' || currentSite === 'fubonbraves') { 716 | chrome.storage.local.set({ 717 | famiKeywords: [], 718 | famiBlacklist: [], 719 | famiHideSoldOut: false 720 | }, () => { 721 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 722 | if (tabs[0]) { 723 | chrome.tabs.sendMessage(tabs[0].id, { 724 | type: 'UPDATE_FAMI_SETTINGS', 725 | settings: { 726 | keywords: [], 727 | blacklist: [], 728 | hideSoldOut: false 729 | } 730 | }); 731 | } 732 | }); 733 | }); 734 | } else if (currentSite === 'jkface') { 735 | chrome.storage.local.set({ 736 | jkfaceKeywords: [], 737 | jkfaceBlacklist: [], 738 | jkfaceHideSoldOut: false 739 | }, () => { 740 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 741 | if (tabs[0]) { 742 | chrome.tabs.sendMessage(tabs[0].id, { 743 | type: 'UPDATE_JKFACE_SETTINGS', 744 | settings: { 745 | keywords: [], 746 | blacklist: [], 747 | hideSoldOut: false 748 | } 749 | }); 750 | } 751 | }); 752 | }); 753 | } else if (currentSite === 'kham') { 754 | chrome.storage.local.set({ 755 | khamKeywords: [], 756 | khamBlacklist: [], 757 | khamHideSoldOut: false 758 | }, () => { 759 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 760 | if (tabs[0]) { 761 | chrome.tabs.sendMessage(tabs[0].id, { 762 | type: 'UPDATE_KHAM_SETTINGS', 763 | settings: { 764 | keywords: [], 765 | blacklist: [], 766 | hideSoldOut: false 767 | } 768 | }); 769 | } 770 | }); 771 | }); 772 | } 773 | }); 774 | } 775 | 776 | // Show sold out checkbox change event 777 | if (showSoldOut) { 778 | showSoldOut.addEventListener('change', () => { 779 | applyFilters(); 780 | }); 781 | } 782 | 783 | // Apply filters 784 | function applyFilters() { 785 | const keywordArray = Array.from(keywords); 786 | const blacklistArray = Array.from(blacklist); 787 | const site = document.body.getAttribute('data-site'); 788 | 789 | if (site === 'tixcraft') { 790 | const settings = { 791 | keywords: keywordArray, 792 | blacklist: blacklistArray, 793 | hideSoldOut: showSoldOut.checked 794 | }; 795 | chrome.storage.local.set({ 796 | keywords: keywordArray, 797 | blacklist: blacklistArray, 798 | hideSoldOut: showSoldOut.checked 799 | }, () => { 800 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 801 | if (tabs[0]) { 802 | chrome.tabs.sendMessage(tabs[0].id, { 803 | type: 'UPDATE_SETTINGS', 804 | settings: settings 805 | }); 806 | } 807 | }); 808 | }); 809 | } else if (site === 'kktix') { 810 | const settings = { 811 | keywords: keywordArray, 812 | blacklist: blacklistArray, 813 | hideSoldOut: showSoldOut.checked 814 | }; 815 | chrome.storage.sync.set({ 816 | targetKeywords: keywordArray, 817 | blacklist: blacklistArray, 818 | showAllPrices: false, 819 | hideSoldOut: showSoldOut.checked 820 | }, () => { 821 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 822 | if (tabs[0]) { 823 | chrome.tabs.sendMessage(tabs[0].id, { 824 | action: 'updateFilter', 825 | keywords: keywordArray, 826 | blacklist: blacklistArray, 827 | settings: settings 828 | }); 829 | } 830 | }); 831 | }); 832 | } else if (site === 'ibon') { 833 | const settings = { 834 | keywords: keywordArray, 835 | blacklist: blacklistArray, 836 | hideSoldOut: showSoldOut.checked 837 | }; 838 | chrome.storage.local.set({ 839 | ibonKeywords: keywordArray, 840 | ibonBlacklist: blacklistArray, 841 | ibonHideSoldOut: showSoldOut.checked 842 | }, () => { 843 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 844 | if (tabs[0]) { 845 | chrome.tabs.sendMessage(tabs[0].id, { 846 | type: 'UPDATE_IBON_SETTINGS', 847 | settings: settings 848 | }); 849 | } 850 | }); 851 | }); 852 | } else if (site === 'cityline') { 853 | const settings = { 854 | keywords: keywordArray, 855 | blacklist: blacklistArray, 856 | hideSoldOut: showSoldOut.checked 857 | }; 858 | chrome.storage.local.set({ 859 | citylineKeywords: keywordArray, 860 | citylineBlacklist: blacklistArray, 861 | citylineHideSoldOut: showSoldOut.checked 862 | }, () => { 863 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 864 | if (tabs[0]) { 865 | chrome.tabs.sendMessage(tabs[0].id, { 866 | type: 'UPDATE_CITYLINE_SETTINGS', 867 | settings: settings 868 | }); 869 | } 870 | }); 871 | }); 872 | } else if (site === 'ticket') { 873 | const settings = { 874 | keywords: keywordArray, 875 | blacklist: blacklistArray, 876 | hideSoldOut: showSoldOut.checked 877 | }; 878 | 879 | // 儲存設定 880 | chrome.storage.local.set({ 881 | ticketKeywords: keywordArray, 882 | ticketBlacklist: blacklistArray, 883 | ticketHideSoldOut: showSoldOut.checked 884 | }, () => { 885 | // 發送設定到content.js 886 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 887 | if (tabs[0]) { 888 | chrome.tabs.sendMessage(tabs[0].id, { 889 | type: 'UPDATE_TICKET_SETTINGS', 890 | settings: settings 891 | }); 892 | } 893 | }); 894 | }); 895 | } else if (site === 'ticketplus') { 896 | const settings = { 897 | keywords: keywordArray, 898 | blacklist: blacklistArray, 899 | hideSoldOut: showSoldOut.checked 900 | }; 901 | 902 | chrome.storage.local.set({ 903 | ticketplusKeywords: keywordArray, 904 | ticketplusBlacklist: blacklistArray, 905 | ticketplusHideSoldOut: showSoldOut.checked 906 | }, () => { 907 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 908 | if (tabs[0]) { 909 | chrome.tabs.sendMessage(tabs[0].id, { 910 | type: 'UPDATE_TICKETPLUS_SETTINGS', 911 | settings: settings 912 | }); 913 | } 914 | }); 915 | }); 916 | } else if (site === 'fami' || site === 'wdragons' || site === 'ctbcsports' || site === 'fubonbraves') { 917 | const settings = { 918 | keywords: keywordArray, 919 | blacklist: blacklistArray, 920 | hideSoldOut: showSoldOut.checked 921 | }; 922 | 923 | chrome.storage.local.set({ 924 | famiKeywords: keywordArray, 925 | famiBlacklist: blacklistArray, 926 | famiHideSoldOut: showSoldOut.checked 927 | }, () => { 928 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 929 | if (tabs[0]) { 930 | chrome.tabs.sendMessage(tabs[0].id, { 931 | type: 'UPDATE_FAMI_SETTINGS', 932 | settings: settings 933 | }); 934 | } 935 | }); 936 | }); 937 | } else if (site === 'jkface') { 938 | const settings = { 939 | keywords: keywordArray, 940 | blacklist: blacklistArray, 941 | hideSoldOut: showSoldOut.checked 942 | }; 943 | 944 | chrome.storage.local.set({ 945 | jkfaceKeywords: keywordArray, 946 | jkfaceBlacklist: blacklistArray, 947 | jkfaceHideSoldOut: showSoldOut.checked 948 | }, () => { 949 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 950 | if (tabs[0]) { 951 | chrome.tabs.sendMessage(tabs[0].id, { 952 | type: 'UPDATE_JKFACE_SETTINGS', 953 | settings: settings 954 | }); 955 | } 956 | }); 957 | }); 958 | } else if (site === 'kham') { 959 | const settings = { 960 | keywords: keywordArray, 961 | blacklist: blacklistArray, 962 | hideSoldOut: showSoldOut.checked 963 | }; 964 | 965 | chrome.storage.local.set({ 966 | khamKeywords: keywordArray, 967 | khamBlacklist: blacklistArray, 968 | khamHideSoldOut: showSoldOut.checked 969 | }, () => { 970 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 971 | if (tabs[0]) { 972 | chrome.tabs.sendMessage(tabs[0].id, { 973 | type: 'UPDATE_KHAM_SETTINGS', 974 | settings: settings 975 | }); 976 | } 977 | }); 978 | }); 979 | } 980 | } 981 | 982 | // 处理服务器时间显示开关 983 | showServerTime.addEventListener('change', function(e) { 984 | chrome.storage.local.set({ showServerTime: e.target.checked }); 985 | }); 986 | 987 | // 加载时设置开关状态 988 | chrome.storage.local.get(['showServerTime'], function(result) { 989 | // 如果从未设置过,默认为开启 990 | const serverTimeEnabled = result.showServerTime === undefined ? true : result.showServerTime; 991 | showServerTime.checked = serverTimeEnabled; 992 | }); 993 | 994 | // 设置面板相关 995 | const settingsPanel = document.querySelector('.settings-panel'); 996 | 997 | // 点击设置图标显示/隐藏设置面板 998 | settingsIcon.addEventListener('click', (e) => { 999 | e.stopPropagation(); 1000 | settingsPanel.classList.toggle('show'); 1001 | }); 1002 | 1003 | // 点击其他地方关闭设置面板 1004 | document.addEventListener('click', (e) => { 1005 | if (!settingsPanel.contains(e.target) && !settingsIcon.contains(e.target)) { 1006 | settingsPanel.classList.remove('show'); 1007 | } 1008 | }); 1009 | 1010 | // 处理筛选条件显示设置 1011 | showFilterStatus.addEventListener('change', (e) => { 1012 | chrome.storage.local.set({ showFilterStatus: e.target.checked }); 1013 | }); 1014 | 1015 | // 加载筛选条件显示设置 1016 | chrome.storage.local.get(['showFilterStatus'], function(result) { 1017 | const showFilterStatus = result.showFilterStatus === undefined ? true : result.showFilterStatus; 1018 | document.getElementById('showFilterStatus').checked = showFilterStatus; 1019 | }); 1020 | 1021 | // 监听设置变化 1022 | chrome.storage.onChanged.addListener(function(changes, namespace) { 1023 | if (changes.showServerTime) { 1024 | if (changes.showServerTime.newValue === false) { 1025 | const existingDisplay = document.querySelector('.server-time-display'); 1026 | if (existingDisplay) { 1027 | existingDisplay.remove(); 1028 | } 1029 | } else { 1030 | showServerTime(); 1031 | } 1032 | // 重新显示筛选状态,以更新位置 1033 | showFilterStatus(); 1034 | } 1035 | 1036 | // 当筛选条件显示设置改变时,重新显示筛选状态 1037 | if (changes.showFilterStatus !== undefined) { 1038 | if (changes.showFilterStatus.newValue === false) { 1039 | // 如果设置为不显示,移除现有的筛选条件显示 1040 | const existingNotification = document.querySelector('.ticket-filter-notification'); 1041 | if (existingNotification) { 1042 | existingNotification.remove(); 1043 | } 1044 | } else { 1045 | // 如果设置为显示,重新显示筛选条件 1046 | showFilterStatus(); 1047 | } 1048 | } 1049 | }); 1050 | 1051 | function isIbonTicketPage(url) { 1052 | return url.includes('ibon.com.tw/application/UTK0201_000.aspx') || 1053 | url.match(/ibon\.com\.tw\/Event\/[A-Z0-9]+\/[A-Z0-9]+/); // 添加新格式匹配 1054 | } 1055 | 1056 | // 添加快捷键设置按钮的点击事件 1057 | document.getElementById('openShortcutSettings').addEventListener('click', function() { 1058 | // 使用chrome.tabs.create打开快捷键设置页面 1059 | chrome.tabs.create({ 1060 | url: 'chrome://extensions/shortcuts' 1061 | }); 1062 | }); 1063 | 1064 | // 获取当前快捷键设置 1065 | function updateCurrentShortcut() { 1066 | chrome.commands.getAll(commands => { 1067 | const toggleCommand = commands.find(command => command.name === 'toggle-extension'); 1068 | const shortcutSpan = document.getElementById('currentShortcut'); 1069 | if (toggleCommand && shortcutSpan) { 1070 | shortcutSpan.textContent = toggleCommand.shortcut || '未設定'; 1071 | } 1072 | }); 1073 | } 1074 | 1075 | // 初始化时获取当前快捷键 1076 | updateCurrentShortcut(); 1077 | 1078 | // 当popup打开时更新快捷键显示 1079 | chrome.commands.getAll(commands => { 1080 | const toggleCommand = commands.find(command => command.name === 'toggle-extension'); 1081 | const shortcutSpan = document.getElementById('currentShortcut'); 1082 | if (toggleCommand && shortcutSpan) { 1083 | shortcutSpan.textContent = toggleCommand.shortcut || '未設定'; 1084 | } 1085 | }); 1086 | 1087 | // 处理快捷键启用/禁用 1088 | const enableShortcut = document.getElementById('enableShortcut'); 1089 | const shortcutInfoContainer = document.getElementById('shortcutInfoContainer'); 1090 | const openShortcutSettings = document.getElementById('openShortcutSettings'); 1091 | 1092 | // 加载快捷键启用状态 1093 | chrome.storage.local.get(['shortcutEnabled'], function(result) { 1094 | const enabled = result.shortcutEnabled !== false; // 默认启用 1095 | enableShortcut.checked = enabled; 1096 | updateShortcutUIState(enabled); 1097 | }); 1098 | 1099 | // 更新快捷键UI状态 1100 | function updateShortcutUIState(enabled) { 1101 | if (shortcutInfoContainer) { 1102 | shortcutInfoContainer.style.opacity = enabled ? '1' : '0.5'; 1103 | shortcutInfoContainer.style.pointerEvents = enabled ? 'auto' : 'none'; 1104 | } 1105 | if (openShortcutSettings) { 1106 | openShortcutSettings.style.opacity = enabled ? '1' : '0.5'; 1107 | openShortcutSettings.style.pointerEvents = enabled ? 'auto' : 'none'; 1108 | } 1109 | } 1110 | 1111 | // 监听快捷键启用状态变化 1112 | enableShortcut.addEventListener('change', function(e) { 1113 | const enabled = e.target.checked; 1114 | chrome.storage.local.set({ shortcutEnabled: enabled }); 1115 | updateShortcutUIState(enabled); 1116 | 1117 | // 更新快捷键命令状态 1118 | chrome.commands.getAll(commands => { 1119 | const toggleCommand = commands.find(command => command.name === 'toggle-extension'); 1120 | if (toggleCommand) { 1121 | if (!enabled) { 1122 | // 禁用快捷键时,保存当前的快捷键设置 1123 | chrome.storage.local.set({ 1124 | savedShortcut: toggleCommand.shortcut 1125 | }, () => { 1126 | // 然后清除快捷键 1127 | chrome.commands.update({ 1128 | name: 'toggle-extension', 1129 | shortcut: '' 1130 | }); 1131 | document.getElementById('currentShortcut').textContent = '已停用'; 1132 | }); 1133 | } else { 1134 | // 启用快捷键时,恢复之前保存的快捷键设置 1135 | chrome.storage.local.get(['savedShortcut'], function(result) { 1136 | if (result.savedShortcut) { 1137 | chrome.commands.update({ 1138 | name: 'toggle-extension', 1139 | shortcut: result.savedShortcut 1140 | }); 1141 | document.getElementById('currentShortcut').textContent = result.savedShortcut; 1142 | } else { 1143 | // 如果没有保存的快捷键,使用默认值 1144 | document.getElementById('currentShortcut').textContent = 'Ctrl+Shift+E'; 1145 | } 1146 | }); 1147 | } 1148 | } 1149 | }); 1150 | }); 1151 | }); -------------------------------------------------------------------------------- /content.js: -------------------------------------------------------------------------------- 1 | // 等待DOM載入完成 2 | function waitForElement(selector) { 3 | return new Promise(resolve => { 4 | if (document.querySelector(selector)) { 5 | return resolve(document.querySelector(selector)); 6 | } 7 | 8 | const observer = new MutationObserver(mutations => { 9 | if (document.querySelector(selector)) { 10 | observer.disconnect(); 11 | resolve(document.querySelector(selector)); 12 | } 13 | }); 14 | 15 | observer.observe(document.body, { 16 | childList: true, 17 | subtree: true 18 | }); 19 | }); 20 | } 21 | 22 | // 通知早期加载脚本,content.js已加载 23 | window.addEventListener('DOMContentLoaded', () => { 24 | window.postMessage({ type: 'CONTENT_JS_LOADED' }, '*'); 25 | 26 | // 尽早执行筛选 27 | setTimeout(() => { 28 | const site = getCurrentSite(); 29 | if (site) { 30 | loadSettings(); 31 | // 如果是 ibon 的 WEB網站入口頁面,立即執行篩選 32 | if (site === 'ibon' && window.location.href.includes('WEB網站入口')) { 33 | filterTickets(); 34 | } else { 35 | filterTickets(); 36 | } 37 | } 38 | }, 0); 39 | 40 | // 特別處理年代售票網站 41 | if (getCurrentSite() === 'ticket') { 42 | // 監聽頁面變化 43 | const observer = new MutationObserver((mutations) => { 44 | for (const mutation of mutations) { 45 | if (mutation.type === 'childList' || mutation.type === 'attributes') { 46 | // 檢查是否有票區列表 47 | const areaItems = document.querySelectorAll('.area-list li.main'); 48 | if (areaItems.length > 0) { 49 | filterTickets(); 50 | break; // 找到後立即退出循環 51 | } 52 | } 53 | } 54 | }); 55 | 56 | // 監視整個文檔的變化 57 | observer.observe(document.documentElement, { 58 | childList: true, 59 | subtree: true, 60 | attributes: true 61 | }); 62 | 63 | // 定期檢查並重新應用篩選(更頻繁的檢查) 64 | setInterval(() => { 65 | const areaItems = document.querySelectorAll('.area-list li.main'); 66 | if (areaItems.length > 0) { 67 | filterTickets(); 68 | } 69 | }, 500); // 每500ms檢查一次 70 | } 71 | }); 72 | 73 | // 判斷當前網站 74 | function getCurrentSite() { 75 | const url = window.location.href; 76 | if (url.includes('tixcraft.com') || url.includes('tixcraftweb-pcox.onrender.com')) { 77 | return 'tixcraft'; 78 | } else if (url.includes('kktix.com')) { 79 | return 'kktix'; 80 | } else if (url.includes('ibon.com.tw')) { 81 | return 'ibon'; 82 | } else if (url.includes('cityline.com')) { 83 | return 'cityline'; 84 | } else if (url.includes('ticket.com.tw')) { 85 | return 'ticket'; 86 | } else if (url.includes('ticketplus.com.tw')) { 87 | return 'ticketplus'; 88 | } else if (url.includes('fami.life')) { 89 | return 'fami'; 90 | } else if (url.includes('jkface.net')) { 91 | return 'jkface'; 92 | } else if (url.includes('kham.com.tw')) { 93 | return 'kham'; 94 | } else if (url.includes('tix.wdragons.com')) { 95 | return 'fami'; 96 | } else if (url.includes('tix.ctbcsports.com')) { 97 | return 'fami'; 98 | } else if (url.includes('tix.fubonbraves.com')) { 99 | return 'fami'; 100 | } 101 | return null; 102 | } 103 | 104 | // 判斷是否為 Cloudflare 驗證頁(即使網址未變) 105 | function isCloudflareChallengePage() { 106 | const doc = document; 107 | return !!doc.querySelector( 108 | 'script[src*="cdn-cgi/challenge-platform"], iframe[src*="cdn-cgi/challenge-platform"], input[name="cf-turnstile-response"], div#cf-challenge, form#challenge-form' 109 | ); 110 | } 111 | 112 | // 數字轉換函數 113 | function convertNumber(input) { 114 | const chineseNums = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十']; 115 | const arabicNums = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10']; 116 | 117 | // 移除所有空格 118 | input = input.replace(/\s+/g, ''); 119 | 120 | // 生成所有可能的版本 121 | let versions = new Set([input]); 122 | 123 | // 找出所有數字和中文數字的位置 124 | let matches = []; 125 | // 匹配阿拉伯數字 126 | input.replace(/\d+/g, (match, offset) => { 127 | matches.push({ 128 | type: 'arabic', 129 | value: match, 130 | offset: offset, 131 | length: match.length 132 | }); 133 | }); 134 | // 匹配中文數字 135 | for (let i = 0; i < chineseNums.length; i++) { 136 | let pos = input.indexOf(chineseNums[i]); 137 | while (pos !== -1) { 138 | matches.push({ 139 | type: 'chinese', 140 | value: chineseNums[i], 141 | offset: pos, 142 | length: 1, 143 | arabic: arabicNums[i] 144 | }); 145 | pos = input.indexOf(chineseNums[i], pos + 1); 146 | } 147 | } 148 | 149 | // 按位置排序 150 | matches.sort((a, b) => a.offset - b.offset); 151 | 152 | // 生成替換版本 153 | if (matches.length > 0) { 154 | // 原始文本轉換 155 | let converted = input; 156 | for (let match of matches) { 157 | if (match.type === 'arabic') { 158 | // 將阿拉伯數字轉為中文數字 159 | const digits = match.value.split(''); 160 | const chinese = digits.map(d => chineseNums[parseInt(d)]).join(''); 161 | converted = converted.slice(0, match.offset) + chinese + 162 | converted.slice(match.offset + match.length); 163 | } else { 164 | // 將中文數字轉為阿拉伯數字 165 | converted = converted.slice(0, match.offset) + match.arabic + 166 | converted.slice(match.offset + match.length); 167 | } 168 | } 169 | versions.add(converted); 170 | } 171 | 172 | return Array.from(versions); 173 | } 174 | 175 | // 添加全角转半角函数 176 | function toHalfWidth(str) { 177 | return str.replace(/[\uFF01-\uFF5E]/g, function(char) { 178 | return String.fromCharCode(char.charCodeAt(0) - 0xFEE0); 179 | }); 180 | } 181 | 182 | // 修改文本匹配函数 183 | function textIncludesKeyword(text, keyword) { 184 | // 移除所有空格和逗號,並轉換為小寫再比較 185 | const cleanText = toHalfWidth(text.replace(/[\s,]+/g, '')).toLowerCase(); 186 | 187 | // 處理 OR 邏輯(用 + 分隔) 188 | const orParts = keyword.split('+').map(part => part.trim()); 189 | 190 | // 如果任一 OR 條件符合就返回 true 191 | return orParts.some(orPart => { 192 | // 處理 AND 邏輯(用 , 分隔) 193 | const andParts = orPart.split(',').map(part => part.trim()); 194 | 195 | // 所有 AND 條件都要符合才返回 true 196 | return andParts.every(andPart => { 197 | // 移除關鍵字中的逗號,转换为半角 198 | andPart = toHalfWidth(andPart.replace(/,/g, '')).toLowerCase(); 199 | 200 | // 如果是单个字符的搜索,直接进行包含匹配 201 | if (andPart.length === 1) { 202 | return cleanText.includes(andPart.toLowerCase()); 203 | } 204 | 205 | // 獲取文本的所有可能版本(包含數字轉換) 206 | const textVersions = convertNumber(cleanText); 207 | const keywordVersions = convertNumber(andPart); 208 | 209 | // 交叉比對所有版本 210 | return keywordVersions.some(kw => 211 | textVersions.some(txt => txt.includes(kw)) 212 | ); 213 | }); 214 | }); 215 | } 216 | 217 | let settings = { 218 | keywords: [], 219 | blacklist: [], // 新增黑名單設定 220 | hideSoldOut: false, 221 | isProcessing: false, 222 | showAllPrices: true 223 | }; 224 | 225 | // 紀錄 Cloudflare 驗證狀態,方便從驗證返回後立即套用篩選 226 | let __lastCfChallenge = isCloudflareChallengePage(); 227 | 228 | // 從storage載入設定 229 | function loadSettings() { 230 | const site = getCurrentSite(); 231 | 232 | if (site === 'tixcraft') { 233 | chrome.storage.local.get(['keywords', 'blacklist', 'hideSoldOut'], (result) => { 234 | settings = { 235 | ...settings, 236 | ...result, 237 | blacklist: result.blacklist || [] 238 | }; 239 | if (document.readyState === 'complete') { 240 | filterTickets(); 241 | showFilterStatus(); 242 | } else { 243 | window.addEventListener('load', () => { 244 | filterTickets(); 245 | showFilterStatus(); 246 | }); 247 | } 248 | }); 249 | } else if (site === 'kktix') { 250 | chrome.storage.sync.get(['targetKeywords', 'blacklist', 'showAllPrices', 'hideSoldOut'], (result) => { 251 | if (result.targetKeywords) { 252 | settings.keywords = result.targetKeywords; 253 | } 254 | if (result.blacklist) { 255 | settings.blacklist = result.blacklist; 256 | } 257 | if (result.showAllPrices !== undefined) { 258 | settings.showAllPrices = result.showAllPrices; 259 | } 260 | if (result.hideSoldOut !== undefined) { 261 | settings.hideSoldOut = result.hideSoldOut; 262 | } 263 | if (document.readyState === 'complete') { 264 | filterTickets(); 265 | showFilterStatus(); 266 | } else { 267 | window.addEventListener('load', () => { 268 | filterTickets(); 269 | showFilterStatus(); 270 | }); 271 | } 272 | }); 273 | } else if (site === 'ibon') { 274 | chrome.storage.local.get(['ibonKeywords', 'ibonBlacklist', 'ibonHideSoldOut'], (result) => { 275 | if (result.ibonKeywords) { 276 | settings.keywords = result.ibonKeywords; 277 | } 278 | if (result.ibonBlacklist) { 279 | settings.blacklist = result.ibonBlacklist; 280 | } 281 | if (result.ibonHideSoldOut !== undefined) { 282 | settings.hideSoldOut = result.ibonHideSoldOut; 283 | } 284 | if (document.readyState === 'complete') { 285 | filterTickets(); 286 | showFilterStatus(); 287 | } else { 288 | window.addEventListener('load', () => { 289 | filterTickets(); 290 | showFilterStatus(); 291 | }); 292 | } 293 | }); 294 | } else if (site === 'cityline') { 295 | chrome.storage.local.get(['citylineKeywords', 'citylineBlacklist', 'citylineHideSoldOut'], (result) => { 296 | if (result.citylineKeywords) { 297 | settings.keywords = result.citylineKeywords; 298 | } 299 | if (result.citylineBlacklist) { 300 | settings.blacklist = result.citylineBlacklist; 301 | } 302 | if (result.citylineHideSoldOut !== undefined) { 303 | settings.hideSoldOut = result.citylineHideSoldOut; 304 | } 305 | if (document.readyState === 'complete') { 306 | filterTickets(); 307 | showFilterStatus(); 308 | } else { 309 | window.addEventListener('load', () => { 310 | filterTickets(); 311 | showFilterStatus(); 312 | }); 313 | } 314 | }); 315 | } else if (site === 'ticket') { 316 | chrome.storage.local.get(['ticketKeywords', 'ticketBlacklist', 'ticketHideSoldOut'], (result) => { 317 | if (result.ticketKeywords) { 318 | settings.keywords = result.ticketKeywords; 319 | } 320 | if (result.ticketBlacklist) { 321 | settings.blacklist = result.ticketBlacklist; 322 | } 323 | if (result.ticketHideSoldOut !== undefined) { 324 | settings.hideSoldOut = result.ticketHideSoldOut; 325 | } 326 | if (document.readyState === 'complete') { 327 | filterTickets(); 328 | showFilterStatus(); 329 | } else { 330 | window.addEventListener('load', () => { 331 | filterTickets(); 332 | showFilterStatus(); 333 | }); 334 | } 335 | }); 336 | } else if (site === 'ticketplus') { 337 | chrome.storage.local.get(['ticketplusKeywords', 'ticketplusBlacklist', 'ticketplusHideSoldOut'], (result) => { 338 | if (result.ticketplusKeywords) { 339 | settings.keywords = result.ticketplusKeywords; 340 | } 341 | if (result.ticketplusBlacklist) { 342 | settings.blacklist = result.ticketplusBlacklist; 343 | } 344 | if (result.ticketplusHideSoldOut !== undefined) { 345 | settings.hideSoldOut = result.ticketplusHideSoldOut; 346 | } 347 | if (document.readyState === 'complete') { 348 | filterTickets(); 349 | showFilterStatus(); 350 | } else { 351 | window.addEventListener('load', () => { 352 | filterTickets(); 353 | showFilterStatus(); 354 | }); 355 | } 356 | }); 357 | } else if (site === 'fami') { 358 | chrome.storage.local.get(['famiKeywords', 'famiBlacklist', 'famiHideSoldOut'], (result) => { 359 | if (result.famiKeywords) { 360 | settings.keywords = result.famiKeywords; 361 | } 362 | if (result.famiBlacklist) { 363 | settings.blacklist = result.famiBlacklist; 364 | } 365 | if (result.famiHideSoldOut !== undefined) { 366 | settings.hideSoldOut = result.famiHideSoldOut; 367 | } 368 | if (document.readyState === 'complete') { 369 | filterTickets(); 370 | showFilterStatus(); 371 | } else { 372 | window.addEventListener('load', () => { 373 | filterTickets(); 374 | showFilterStatus(); 375 | }); 376 | } 377 | }); 378 | } else if (site === 'jkface') { 379 | chrome.storage.local.get(['jkfaceKeywords', 'jkfaceBlacklist', 'jkfaceHideSoldOut'], (result) => { 380 | if (result.jkfaceKeywords) { 381 | settings.keywords = result.jkfaceKeywords; 382 | } 383 | if (result.jkfaceBlacklist) { 384 | settings.blacklist = result.jkfaceBlacklist; 385 | } 386 | if (result.jkfaceHideSoldOut !== undefined) { 387 | settings.hideSoldOut = result.jkfaceHideSoldOut; 388 | } 389 | if (document.readyState === 'complete') { 390 | filterTickets(); 391 | showFilterStatus(); 392 | } else { 393 | window.addEventListener('load', () => { 394 | filterTickets(); 395 | showFilterStatus(); 396 | }); 397 | } 398 | }); 399 | } else if (site === 'kham') { 400 | chrome.storage.local.get(['khamKeywords', 'khamBlacklist', 'khamHideSoldOut'], (result) => { 401 | if (result.khamKeywords) { 402 | settings.keywords = result.khamKeywords; 403 | } 404 | if (result.khamBlacklist) { 405 | settings.blacklist = result.khamBlacklist; 406 | } 407 | if (result.khamHideSoldOut !== undefined) { 408 | settings.hideSoldOut = result.khamHideSoldOut; 409 | } 410 | 411 | // 立即应用筛选 412 | filterKhamTickets(); 413 | showFilterStatus(); 414 | 415 | // 初始化观察器 416 | initKhamObserver(); 417 | }); 418 | } 419 | } 420 | 421 | // 初始化 422 | loadSettings(); 423 | 424 | // 设置网站标识 425 | const currentSite = getCurrentSite(); 426 | if (currentSite) { 427 | document.body.setAttribute('data-site', currentSite); 428 | } 429 | 430 | // 確保預設設置被正確設置 431 | chrome.storage.local.get(['showServerTime', 'showFilterStatus'], function(result) { 432 | // 如果設置尚未初始化,設置預設值 433 | const updates = {}; 434 | if (result.showServerTime === undefined) { 435 | updates.showServerTime = true; 436 | } 437 | if (result.showFilterStatus === undefined) { 438 | updates.showFilterStatus = true; 439 | } 440 | 441 | // 如果有需要更新的設置,保存到storage 442 | if (Object.keys(updates).length > 0) { 443 | chrome.storage.local.set(updates); 444 | } 445 | }); 446 | 447 | // 注入CSS樣式 448 | const style = document.createElement('style'); 449 | style.textContent = ` 450 | .ticket-filter-notification { 451 | position: fixed; 452 | right: 10px; 453 | padding: 8px 12px; 454 | background-color: #2684FF; 455 | color: #fff; 456 | border-radius: 4px; 457 | box-shadow: 0 2px 5px rgba(0,0,0,0.2); 458 | z-index: 9999; 459 | font-size: 14px; 460 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft JhengHei", sans-serif; 461 | } 462 | 463 | .server-time-display { 464 | position: fixed; 465 | top: 10px; 466 | right: 10px; 467 | padding: 8px 12px; 468 | background-color: #2684FF; 469 | color: #fff; 470 | border-radius: 4px; 471 | box-shadow: 0 2px 5px rgba(0,0,0,0.2); 472 | z-index: 10000; 473 | font-size: 14px; 474 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft JhengHei", sans-serif; 475 | } 476 | 477 | /* 寬宏售票平台特定樣式 */ 478 | body[data-site="kham"] .server-time-display { 479 | min-width: 120px; 480 | text-align: center; 481 | white-space: nowrap; 482 | font-variant-numeric: tabular-nums; 483 | -webkit-font-variant-numeric: tabular-nums; 484 | } 485 | 486 | /* 遠大售票平台平滑過渡效果 */ 487 | .v-expansion-panels.seats-area .v-expansion-panel { 488 | opacity: 0.1; 489 | transition: opacity 0.15s ease-out; 490 | } 491 | 492 | .v-expansion-panels.seats-area .v-expansion-panel.filter-processed { 493 | opacity: 1; 494 | } 495 | 496 | /* 新版ibon售票网站样式 */ 497 | tr.ng-star-inserted.hidden-by-extension { 498 | display: none !important; 499 | } 500 | 501 | /* 确保隐藏的行真的被隐藏 */ 502 | tr.hidden-by-extension, 503 | tr.hidden-by-extension * { 504 | display: none !important; 505 | } 506 | 507 | /* Fami Life 平台初始化隐藏样式 */ 508 | body.fami-loading .saleTr { 509 | visibility: hidden !important; 510 | opacity: 0 !important; 511 | transition: opacity 0.2s ease-in-out !important; 512 | } 513 | 514 | body.fami-ready .saleTr { 515 | visibility: visible !important; 516 | transition: opacity 0.2s ease-in-out !important; 517 | } 518 | `; 519 | document.head.appendChild(style); 520 | 521 | // 顯示篩選狀態 522 | function showFilterStatus() { 523 | chrome.storage.local.get(['showServerTime', 'showFilterStatus', 'extensionEnabled'], function(result) { 524 | // 如果擴展被停用,移除所有顯示 525 | if (result.extensionEnabled === false) { 526 | const existingNotification = document.querySelector('.ticket-filter-notification'); 527 | if (existingNotification) { 528 | existingNotification.remove(); 529 | } 530 | const existingTimeDisplay = document.querySelector('.server-time-display'); 531 | if (existingTimeDisplay) { 532 | existingTimeDisplay.remove(); 533 | } 534 | return; 535 | } 536 | 537 | if (result.showFilterStatus === false) { 538 | const existingNotification = document.querySelector('.ticket-filter-notification'); 539 | if (existingNotification) { 540 | existingNotification.remove(); 541 | } 542 | return; 543 | } 544 | 545 | let notification = document.querySelector('.ticket-filter-notification'); 546 | if (!notification) { 547 | notification = document.createElement('div'); 548 | notification.className = 'ticket-filter-notification'; 549 | document.body.appendChild(notification); 550 | } 551 | 552 | if (result.showServerTime) { 553 | notification.style.top = '50px'; // 在時間顯示下方 554 | } else { 555 | notification.style.top = '10px'; // 原本的位置 556 | } 557 | 558 | let statusText = []; 559 | const site = getCurrentSite(); 560 | 561 | if (site === 'tixcraft') { 562 | let hasFilter = false; 563 | if (settings.keywords.length > 0) { 564 | statusText.push(`關鍵字篩選:${settings.keywords.join('、')}`); 565 | hasFilter = true; 566 | } 567 | if (settings.blacklist && settings.blacklist.length > 0) { 568 | statusText.push(`黑名單:${settings.blacklist.join('、')}`); 569 | hasFilter = true; 570 | } 571 | if (!hasFilter) { 572 | statusText.push('無篩選條件'); 573 | } 574 | if (settings.hideSoldOut) { 575 | statusText.push('隱藏已售完票券'); 576 | } 577 | } else if (site === 'kktix') { 578 | let hasFilter = false; 579 | if (settings.keywords.length > 0) { 580 | statusText.push(`關鍵字篩選:${settings.keywords.join('、')}`); 581 | hasFilter = true; 582 | } 583 | if (settings.blacklist && settings.blacklist.length > 0) { 584 | statusText.push(`黑名單:${settings.blacklist.join('、')}`); 585 | hasFilter = true; 586 | } 587 | if (!hasFilter) { 588 | statusText.push('無篩選條件'); 589 | } 590 | if (settings.hideSoldOut) { 591 | statusText.push('隱藏已售完票券'); 592 | } 593 | } else if (site === 'ibon') { 594 | let hasFilter = false; 595 | if (settings.keywords && settings.keywords.length > 0) { 596 | statusText.push(`關鍵字篩選:${settings.keywords.join('、')}`); 597 | hasFilter = true; 598 | } 599 | if (settings.blacklist && settings.blacklist.length > 0) { 600 | statusText.push(`黑名單:${settings.blacklist.join('、')}`); 601 | hasFilter = true; 602 | } 603 | if (!hasFilter) { 604 | statusText.push('無篩選條件'); 605 | } 606 | if (settings.hideSoldOut) { 607 | statusText.push('隱藏已售完票券'); 608 | } 609 | } else if (site === 'cityline') { 610 | let hasFilter = false; 611 | if (settings.keywords && settings.keywords.length > 0) { 612 | statusText.push(`關鍵字篩選:${settings.keywords.join('、')}`); 613 | hasFilter = true; 614 | } 615 | if (settings.blacklist && settings.blacklist.length > 0) { 616 | statusText.push(`黑名單:${settings.blacklist.join('、')}`); 617 | hasFilter = true; 618 | } 619 | if (!hasFilter) { 620 | statusText.push('無篩選條件'); 621 | } 622 | if (settings.hideSoldOut) { 623 | statusText.push('隱藏已售完票券'); 624 | } 625 | } else if (site === 'ticket') { 626 | let hasFilter = false; 627 | if (settings.keywords && settings.keywords.length > 0) { 628 | statusText.push(`關鍵字篩選:${settings.keywords.join('、')}`); 629 | hasFilter = true; 630 | } 631 | if (settings.blacklist && settings.blacklist.length > 0) { 632 | statusText.push(`黑名單:${settings.blacklist.join('、')}`); 633 | hasFilter = true; 634 | } 635 | if (!hasFilter) { 636 | statusText.push('無篩選條件'); 637 | } 638 | if (settings.hideSoldOut) { 639 | statusText.push('隱藏已售完票券'); 640 | } 641 | } else if (site === 'ticketplus') { 642 | let hasFilter = false; 643 | if (settings.keywords && settings.keywords.length > 0) { 644 | statusText.push(`關鍵字篩選:${settings.keywords.join('、')}`); 645 | hasFilter = true; 646 | } 647 | if (settings.blacklist && settings.blacklist.length > 0) { 648 | statusText.push(`黑名單:${settings.blacklist.join('、')}`); 649 | hasFilter = true; 650 | } 651 | if (!hasFilter) { 652 | statusText.push('無篩選條件'); 653 | } 654 | if (settings.hideSoldOut) { 655 | statusText.push('隱藏已售完票券'); 656 | } 657 | } else if (site === 'fami') { 658 | let hasFilter = false; 659 | if (settings.keywords && settings.keywords.length > 0) { 660 | statusText.push(`關鍵字篩選:${settings.keywords.join('、')}`); 661 | hasFilter = true; 662 | } 663 | if (settings.blacklist && settings.blacklist.length > 0) { 664 | statusText.push(`黑名單:${settings.blacklist.join('、')}`); 665 | hasFilter = true; 666 | } 667 | if (!hasFilter) { 668 | statusText.push('無篩選條件'); 669 | } 670 | if (settings.hideSoldOut) { 671 | statusText.push('隱藏已售完票券'); 672 | } 673 | } else if (site === 'jkface') { 674 | let hasFilter = false; 675 | if (settings.keywords && settings.keywords.length > 0) { 676 | statusText.push(`關鍵字篩選:${settings.keywords.join('、')}`); 677 | hasFilter = true; 678 | } 679 | if (settings.blacklist && settings.blacklist.length > 0) { 680 | statusText.push(`黑名單:${settings.blacklist.join('、')}`); 681 | hasFilter = true; 682 | } 683 | if (!hasFilter) { 684 | statusText.push('無篩選條件'); 685 | } 686 | if (settings.hideSoldOut) { 687 | statusText.push('隱藏已售完票券'); 688 | } 689 | } else if (site === 'kham') { 690 | let hasFilter = false; 691 | if (settings.keywords && settings.keywords.length > 0) { 692 | statusText.push(`關鍵字篩選:${settings.keywords.join('、')}`); 693 | hasFilter = true; 694 | } 695 | if (settings.blacklist && settings.blacklist.length > 0) { 696 | statusText.push(`黑名單:${settings.blacklist.join('、')}`); 697 | hasFilter = true; 698 | } 699 | if (!hasFilter) { 700 | statusText.push('無篩選條件'); 701 | } 702 | if (settings.hideSoldOut) { 703 | statusText.push('隱藏已售完票券'); 704 | } 705 | } 706 | 707 | const newText = statusText.join(' | '); 708 | if (notification.textContent !== newText) { 709 | notification.textContent = newText; 710 | } 711 | }); 712 | } 713 | 714 | // 顯示所有區域 715 | function showAllAreas() { 716 | const site = getCurrentSite(); 717 | if (site === 'tixcraft') { 718 | document.querySelectorAll('.area-list li').forEach(item => { 719 | item.style.display = ''; 720 | }); 721 | document.querySelectorAll('.zone-label').forEach(label => { 722 | label.style.display = ''; 723 | }); 724 | } else if (site === 'kktix') { 725 | document.querySelectorAll('.ticket-unit').forEach(item => { 726 | item.style.display = ''; 727 | }); 728 | } else if (site === 'ibon') { 729 | function showAllTickets(element) { 730 | if (element.shadowRoot) { 731 | // 同时支持新旧两种选择器 732 | const rows = element.shadowRoot.querySelectorAll('tr[id^="B"], tr[_ngcontent-tpp-c65].ng-star-inserted, tr[_ngcontent-hxg-c65].ng-star-inserted, tr[_ngcontent-njn-c65].ng-star-inserted'); 733 | rows.forEach(row => { 734 | // 移除所有由扩充功能添加的样式和类别 735 | row.style.removeProperty('display'); 736 | row.classList.remove('hidden-by-extension'); 737 | row.classList.remove('filter-processed'); 738 | Array.from(row.children).forEach(cell => { 739 | cell.style.removeProperty('display'); 740 | }); 741 | }); 742 | 743 | // 移除 shadow root 中的过滤样式 744 | const filterStyle = element.shadowRoot.querySelector('.extension-filter-style'); 745 | if (filterStyle) { 746 | filterStyle.remove(); 747 | } 748 | } 749 | Array.from(element.children || []).forEach(showAllTickets); 750 | } 751 | showAllTickets(document.documentElement); 752 | 753 | // 强制更新页面状态 754 | setTimeout(() => { 755 | window.dispatchEvent(new Event('resize')); 756 | }, 100); 757 | } else if (site === 'cityline') { 758 | document.querySelectorAll('.form-check').forEach(item => { 759 | item.style.display = ''; 760 | }); 761 | } else if (site === 'ticket') { 762 | document.querySelectorAll('.area-list li').forEach(item => { 763 | item.style.display = ''; 764 | }); 765 | } else if (site === 'ticketplus') { 766 | document.querySelectorAll('.v-expansion-panels.seats-area .v-expansion-panel').forEach(panel => { 767 | panel.style.display = ''; 768 | panel.classList.add('filter-processed'); 769 | }); 770 | } else if (site === 'fami') { 771 | // 移除所有已应用的样式 772 | document.querySelectorAll('.saleTr').forEach(row => { 773 | // 移除所有可能影响显示的样式 774 | row.style.removeProperty('display'); 775 | row.style.removeProperty('opacity'); 776 | row.style.removeProperty('visibility'); 777 | row.style.removeProperty('transition'); 778 | row.classList.remove('filter-ready'); 779 | }); 780 | 781 | // 移除可能存在的全局样式 782 | const earlyStyle = document.getElementById('ticket-filter-early-style-fami'); 783 | if (earlyStyle) { 784 | earlyStyle.remove(); 785 | } 786 | } else if (site === 'jkface') { 787 | // 显示所有票券 788 | document.querySelectorAll('form').forEach(form => { 789 | const parentElement = form.closest('.border'); 790 | if (parentElement) { 791 | parentElement.style.display = ''; 792 | } 793 | }); 794 | } else if (site === 'kham') { 795 | // 显示所有票券 796 | document.querySelectorAll('tr.status_tr').forEach(row => { 797 | row.style.display = ''; 798 | }); 799 | } 800 | 801 | // 通知早期加载脚本,已显示所有票券 802 | window.postMessage({ type: 'FILTER_APPLIED' }, '*'); 803 | } 804 | 805 | // 主要篩選功能 806 | async function filterTickets() { 807 | // 檢查擴展是否被停用 808 | const result = await new Promise(resolve => { 809 | chrome.storage.local.get(['extensionEnabled'], resolve); 810 | }); 811 | 812 | if (result.extensionEnabled === false) { 813 | showAllAreas(); 814 | return; 815 | } 816 | 817 | if (settings.isProcessing) return; 818 | settings.isProcessing = true; 819 | 820 | try { 821 | const site = getCurrentSite(); 822 | 823 | // 根據不同網站執行對應的過濾邏輯 824 | if (site === 'tixcraft' && (window.location.href.includes('/ticket/area/') || window.location.hostname.includes('tixcraftweb-pcox.onrender.com'))) { 825 | await filterTixcraftTickets(); 826 | } else if (site === 'kktix' && document.querySelector('.ticket-unit')) { 827 | await filterKKTIXTickets(); 828 | } else if (site === 'ibon') { 829 | await filterIbonTickets(); 830 | } else if (site === 'cityline' && document.querySelector('.price-box1')) { 831 | await filterCitylineTickets(); 832 | } else if (site === 'ticket') { 833 | await filterTicketComTickets(); 834 | } else if (site === 'ticketplus') { 835 | await filterTicketPlusTickets(); 836 | } else if (site === 'fami') { 837 | await filterFamiTickets(); 838 | } else if (site === 'jkface') { 839 | await filterJKFaceTickets(); 840 | } else if (site === 'kham') { 841 | await filterKhamTickets(); // 作为一个分支加入通用的 filterTickets 函数 842 | } 843 | 844 | // 通知早期加載腳本,過濾已完成 845 | window.postMessage({ type: 'FILTER_APPLIED' }, '*'); 846 | } catch (error) { 847 | console.error('過濾票券時發生錯誤:', error); 848 | // 即使出錯也要通知移除隱藏樣式 849 | window.postMessage({ type: 'FILTER_APPLIED' }, '*'); 850 | } finally { 851 | settings.isProcessing = false; 852 | } 853 | } 854 | 855 | // KKTIX票券篩選 856 | async function filterKKTIXTickets() { 857 | const ticketRows = document.querySelectorAll('.ticket-unit'); 858 | if (!ticketRows.length) return; 859 | 860 | showAllAreas(); 861 | 862 | // 如果沒有任何篩選條件,只處理是否隱藏已售完 863 | if (settings.keywords.length === 0 && (!settings.blacklist || settings.blacklist.length === 0)) { 864 | ticketRows.forEach(row => { 865 | const isSoldOut = row.textContent.includes('已售完'); 866 | if (settings.hideSoldOut && isSoldOut) { 867 | row.style.display = 'none'; 868 | } else { 869 | row.style.display = ''; 870 | } 871 | }); 872 | return; 873 | } 874 | 875 | // 處理每個票券 876 | ticketRows.forEach(row => { 877 | const priceElement = row.querySelector('.ticket-price'); 878 | const nameElement = row.querySelector('.ticket-name'); 879 | 880 | if (priceElement && nameElement) { 881 | const price = priceElement.textContent.trim(); 882 | // 获取票区名称并清理多余空格和注释 883 | const name = nameElement.textContent 884 | .replace(//g, '') // 移除 HTML 注释 885 | .replace(/\s+/g, '') // 移除所有空格 886 | .trim(); 887 | 888 | const text = `${name} ${price}`; 889 | const isSoldOut = row.textContent.includes('已售完'); 890 | 891 | // 檢查是否在黑名單中 892 | const isBlacklisted = settings.blacklist && settings.blacklist.length > 0 && 893 | settings.blacklist.some(blacklistItem => { 894 | const orParts = blacklistItem.split('+').map(part => part.trim()); 895 | return orParts.some(orPart => { 896 | const andParts = orPart.split(',').map(part => part.trim()); 897 | return andParts.every(andPart => textIncludesKeyword(text, andPart)); 898 | }); 899 | }); 900 | 901 | // 檢查是否符合關鍵字 902 | const matchesKeyword = settings.keywords.length === 0 || 903 | settings.keywords.some(keyword => { 904 | const orParts = keyword.split('+').map(part => part.trim()); 905 | return orParts.some(orPart => { 906 | const andParts = orPart.split(',').map(part => part.trim()); 907 | return andParts.every(andPart => { 908 | // 如果是单字符,使用更宽松的匹配 909 | if (andPart.length === 1) { 910 | return text.toLowerCase().includes(andPart.toLowerCase()); 911 | } 912 | return textIncludesKeyword(text, andPart); 913 | }); 914 | }); 915 | }); 916 | 917 | // 決定是否顯示 918 | let shouldShow = true; 919 | 920 | // 如果在黑名單中,不顯示 921 | if (isBlacklisted) { 922 | shouldShow = false; 923 | } 924 | 925 | // 如果有關鍵字且不符合關鍵字,不顯示 926 | if (settings.keywords.length > 0 && !matchesKeyword) { 927 | shouldShow = false; 928 | } 929 | 930 | // 如果設定隱藏已售完且已售完,不顯示 931 | if (settings.hideSoldOut && isSoldOut) { 932 | shouldShow = false; 933 | } 934 | 935 | // 立即應用顯示/隱藏狀態 936 | row.style.setProperty('display', shouldShow ? '' : 'none', 'important'); 937 | } 938 | }); 939 | } 940 | 941 | // 拓元票券篩選 942 | async function filterTixcraftTickets() { 943 | // 等待任一種格式的區域列表出現 944 | await Promise.race([ 945 | waitForElement('.zone-label[data-id]'), 946 | waitForElement('.zone.area-list ul.area-list') 947 | ]); 948 | 949 | // 先顯示所有區域 950 | showAllAreas(); 951 | 952 | // 如果沒有任何篩選條件,只處理是否隱藏已售完 953 | if (settings.keywords.length === 0 && (!settings.blacklist || settings.blacklist.length === 0)) { 954 | if (settings.hideSoldOut) { 955 | document.querySelectorAll('.area-list li').forEach(item => { 956 | const remainingText = item.querySelector('font')?.textContent.trim() || ''; 957 | if (remainingText.includes('已售完')) { 958 | item.style.display = 'none'; 959 | } else { 960 | item.style.display = ''; 961 | } 962 | }); 963 | } 964 | return; 965 | } 966 | 967 | // 處理有價格區標題的格式 968 | const areaGroups = document.querySelectorAll('.zone-label[data-id]'); 969 | if (areaGroups.length > 0) { 970 | areaGroups.forEach(group => { 971 | const groupId = group.dataset.id; 972 | const areaList = document.getElementById(groupId); 973 | if (!areaList) return; 974 | 975 | const items = areaList.querySelectorAll('li'); 976 | let hasVisibleItems = false; 977 | 978 | const groupTitle = group.querySelector('b')?.textContent.trim() || ''; 979 | 980 | items.forEach(item => { 981 | const areaText = item.textContent.trim(); 982 | const remainingText = item.querySelector('font')?.textContent.trim() || ''; 983 | const isSoldOut = remainingText.includes('已售完'); 984 | const fullText = groupTitle ? `${groupTitle} ${areaText}` : areaText; 985 | 986 | let shouldShow = processTicketItem(fullText, isSoldOut); 987 | item.style.display = shouldShow ? '' : 'none'; 988 | if (shouldShow) hasVisibleItems = true; 989 | }); 990 | 991 | group.style.display = hasVisibleItems ? '' : 'none'; 992 | }); 993 | } else { 994 | // 處理沒有價格區標題的格式 995 | const areaLists = document.querySelectorAll('.zone.area-list ul.area-list'); 996 | areaLists.forEach(list => { 997 | const items = list.querySelectorAll('li'); 998 | items.forEach(item => { 999 | const areaText = item.textContent.trim(); 1000 | const remainingText = item.querySelector('font')?.textContent.trim() || ''; 1001 | const isSoldOut = remainingText.includes('已售完'); 1002 | 1003 | let shouldShow = processTicketItem(areaText, isSoldOut); 1004 | item.style.display = shouldShow ? '' : 'none'; 1005 | }); 1006 | }); 1007 | } 1008 | } 1009 | 1010 | // 處理單個票券項目的邏輯 1011 | function processTicketItem(text, isSoldOut) { 1012 | // 檢查是否在黑名單中 1013 | const isBlacklisted = settings.blacklist && settings.blacklist.length > 0 && 1014 | settings.blacklist.some(blacklistItem => { 1015 | const orParts = blacklistItem.split('+').map(part => part.trim()); 1016 | return orParts.some(orPart => { 1017 | const andParts = orPart.split(',').map(part => part.trim()); 1018 | return andParts.every(andPart => textIncludesKeyword(text, andPart)); 1019 | }); 1020 | }); 1021 | 1022 | // 檢查是否符合關鍵字 1023 | const matchesKeyword = settings.keywords.length === 0 || 1024 | settings.keywords.some(keyword => { 1025 | const orParts = keyword.split('+').map(part => part.trim()); 1026 | return orParts.some(orPart => { 1027 | const andParts = orPart.split(',').map(part => part.trim()); 1028 | return andParts.every(andPart => textIncludesKeyword(text, andPart)); 1029 | }); 1030 | }); 1031 | 1032 | // 決定是否顯示 1033 | let shouldShow = true; 1034 | 1035 | if (isBlacklisted) shouldShow = false; 1036 | if (settings.keywords.length > 0 && !matchesKeyword) shouldShow = false; 1037 | if (settings.hideSoldOut && isSoldOut) shouldShow = false; 1038 | 1039 | return shouldShow; 1040 | } 1041 | 1042 | // ibon票券篩選 1043 | async function filterIbonTickets() { 1044 | const url = window.location.href; 1045 | const isNewVersion = url.match(/Event\/[A-Z0-9]+\/[A-Z0-9]+/); 1046 | const isOldVersion = url.includes('UTK0201_000.aspx'); 1047 | 1048 | // 调试信息 1049 | console.log('当前URL:', url); 1050 | console.log('是否新版:', isNewVersion); 1051 | console.log('是否旧版:', isOldVersion); 1052 | console.log('当前设置:', settings); 1053 | 1054 | if (isNewVersion) { 1055 | // 新版 ibon 处理逻辑 1056 | async function handleNewVersion() { 1057 | // 确保页面处于加载状态 1058 | document.body.classList.add('ibon-loading'); 1059 | 1060 | // 等待表格加载 1061 | const table = await waitForElement('table.table'); 1062 | if (!table) { 1063 | console.log('未找到表格'); 1064 | return; 1065 | } 1066 | 1067 | // 等待行加载完成 1068 | await new Promise(resolve => { 1069 | const checkRows = () => { 1070 | const rows = table.querySelectorAll('tbody > tr.ng-star-inserted'); 1071 | if (rows.length > 0) { 1072 | resolve(); 1073 | } else { 1074 | setTimeout(checkRows, 50); 1075 | } 1076 | }; 1077 | checkRows(); 1078 | }); 1079 | 1080 | // 获取所有行 1081 | const rows = Array.from(table.querySelectorAll('tbody > tr.ng-star-inserted')); 1082 | console.log('找到有效行数:', rows.length); 1083 | 1084 | // 先重置所有行的状态 1085 | rows.forEach(row => { 1086 | row.style.removeProperty('display'); 1087 | row.classList.remove('hidden-by-extension'); 1088 | }); 1089 | 1090 | // 处理筛选 1091 | if (settings.keywords.length > 0 || 1092 | (settings.blacklist && settings.blacklist.length > 0) || 1093 | settings.hideSoldOut) { 1094 | 1095 | rows.forEach((row, index) => { 1096 | const areaCell = row.querySelector('td[data-title="票區"]') || row.cells[0]; 1097 | const priceCell = row.querySelector('td[data-title="票價(NT$)"]') || row.cells[1]; 1098 | const statusCell = row.querySelector('td[data-title="空位"]') || row.cells[2]; 1099 | 1100 | if (areaCell && priceCell && statusCell) { 1101 | const areaText = areaCell.textContent.trim(); 1102 | const priceText = priceCell.textContent.replace(/,/g, '').trim(); 1103 | const statusText = statusCell.textContent.trim(); 1104 | const fullText = `${areaText} ${priceText}`; 1105 | 1106 | // 检查售完状态 1107 | const isSoldOut = row.classList.contains('disabled') || 1108 | statusText.includes('已售完') || 1109 | statusText.includes('0'); 1110 | 1111 | // 检查是否在黑名单中 1112 | const isBlacklisted = settings.blacklist && settings.blacklist.length > 0 && 1113 | settings.blacklist.some(blacklistItem => { 1114 | const orParts = blacklistItem.split('+').map(part => part.trim()); 1115 | return orParts.some(orPart => { 1116 | const andParts = orPart.split(',').map(part => part.trim()); 1117 | return andParts.every(andPart => textIncludesKeyword(fullText, andPart)); 1118 | }); 1119 | }); 1120 | 1121 | // 检查是否匹配关键字 1122 | const matchesKeyword = settings.keywords.length === 0 || 1123 | settings.keywords.some(keyword => { 1124 | const orParts = keyword.split('+').map(part => part.trim()); 1125 | return orParts.some(orPart => { 1126 | const andParts = orPart.split(',').map(part => part.trim()); 1127 | return andParts.every(andPart => textIncludesKeyword(fullText, andPart)); 1128 | }); 1129 | }); 1130 | 1131 | if (isBlacklisted || 1132 | (settings.keywords.length > 0 && !matchesKeyword) || 1133 | (settings.hideSoldOut && isSoldOut)) { 1134 | row.style.setProperty('display', 'none', 'important'); 1135 | row.classList.add('hidden-by-extension'); 1136 | } 1137 | } 1138 | }); 1139 | } 1140 | 1141 | // 完成处理后移除加载状态 1142 | requestAnimationFrame(() => { 1143 | document.body.classList.remove('ibon-loading'); 1144 | }); 1145 | } 1146 | 1147 | // 在 DOMContentLoaded 时立即执行一次 1148 | if (document.readyState === 'loading') { 1149 | document.addEventListener('DOMContentLoaded', handleNewVersion); 1150 | } else { 1151 | handleNewVersion(); 1152 | } 1153 | 1154 | // 设置防抖定时器 1155 | let debounceTimer; 1156 | 1157 | // 设置观察器以处理动态加载的内容 1158 | const observer = new MutationObserver(() => { 1159 | if (debounceTimer) { 1160 | clearTimeout(debounceTimer); 1161 | } 1162 | 1163 | debounceTimer = setTimeout(handleNewVersion, 250); 1164 | }); 1165 | 1166 | // 观察整个文档的变化 1167 | observer.observe(document.documentElement, { 1168 | childList: true, 1169 | subtree: true, 1170 | attributes: true, 1171 | attributeFilter: ['style', 'class', 'disabled'] 1172 | }); 1173 | 1174 | return; 1175 | } 1176 | 1177 | if (isOldVersion) { 1178 | // 旧版 ibon 处理逻辑 1179 | function processTicketAreas(element) { 1180 | if (element.shadowRoot) { 1181 | const rows = element.shadowRoot.querySelectorAll('tr[id^="B"]'); 1182 | if (rows.length > 0) { 1183 | // 先隐藏所有行 1184 | rows.forEach(row => { 1185 | row.style.setProperty('display', 'none', 'important'); 1186 | }); 1187 | processOldVersionRows(rows); 1188 | } 1189 | } 1190 | Array.from(element.children || []).forEach(processTicketAreas); 1191 | } 1192 | 1193 | processTicketAreas(document.documentElement); 1194 | } 1195 | 1196 | // 旧版 ibon 的行处理函数 1197 | function processOldVersionRows(rows) { 1198 | rows.forEach(row => { 1199 | const areaCell = row.cells[1]; 1200 | const priceCell = row.cells[2]; 1201 | const statusCell = row.cells[3]; 1202 | 1203 | if (areaCell && priceCell) { 1204 | const areaText = areaCell.textContent.trim(); 1205 | const priceText = priceCell.textContent.replace(/,/g, '').trim(); 1206 | const fullText = `${areaText} ${priceText}`; 1207 | 1208 | // 检查售完状态 1209 | const isSoldOut = row.classList.contains('disabled') || 1210 | (statusCell && statusCell.textContent.includes('已售完')); 1211 | 1212 | // 检查是否在黑名单中 1213 | const isBlacklisted = settings.blacklist && settings.blacklist.length > 0 && 1214 | settings.blacklist.some(blacklistItem => { 1215 | const orParts = blacklistItem.split('+').map(part => part.trim()); 1216 | return orParts.some(orPart => { 1217 | const andParts = orPart.split(',').map(part => part.trim()); 1218 | return andParts.every(andPart => textIncludesKeyword(fullText, andPart)); 1219 | }); 1220 | }); 1221 | 1222 | // 检查是否符合关键字 1223 | const matchesKeyword = settings.keywords.length === 0 || 1224 | settings.keywords.some(keyword => { 1225 | const orParts = keyword.split('+').map(part => part.trim()); 1226 | return orParts.some(orPart => { 1227 | const andParts = orPart.split(',').map(part => part.trim()); 1228 | return andParts.every(andPart => textIncludesKeyword(fullText, andPart)); 1229 | }); 1230 | }); 1231 | 1232 | // 决定是否显示 1233 | let shouldShow = true; 1234 | 1235 | if (isBlacklisted) shouldShow = false; 1236 | if (settings.keywords.length > 0 && !matchesKeyword) shouldShow = false; 1237 | if (settings.hideSoldOut && isSoldOut) shouldShow = false; 1238 | 1239 | // 应用显示/隐藏状态 1240 | if (shouldShow) { 1241 | row.style.removeProperty('display'); 1242 | } else { 1243 | row.style.setProperty('display', 'none', 'important'); 1244 | } 1245 | } 1246 | }); 1247 | } 1248 | } 1249 | 1250 | // Cityline票券篩選 1251 | async function filterCitylineTickets() { 1252 | const ticketItems = document.querySelectorAll('.form-check'); 1253 | if (!ticketItems.length) return; 1254 | 1255 | // 標記所有票券元素為"準備好被篩選" 1256 | document.querySelectorAll('.price-box1, .form-check').forEach(el => { 1257 | el.classList.add('filter-ready'); 1258 | }); 1259 | 1260 | // 如果沒有任何篩選條件,只處理是否隱藏已售完 1261 | if (settings.keywords.length === 0 && (!settings.blacklist || settings.blacklist.length === 0)) { 1262 | if (settings.hideSoldOut) { 1263 | ticketItems.forEach(item => { 1264 | const isSoldOut = item.textContent.includes('售罄') || 1265 | item.querySelector('input[data-disabled="true"]') !== null; 1266 | item.style.display = isSoldOut ? 'none' : ''; 1267 | if (!isSoldOut) { 1268 | item.style.visibility = 'visible'; 1269 | item.style.opacity = '1'; 1270 | } 1271 | }); 1272 | } else { 1273 | ticketItems.forEach(item => { 1274 | item.style.display = ''; 1275 | item.style.visibility = 'visible'; 1276 | item.style.opacity = '1'; 1277 | }); 1278 | } 1279 | return; 1280 | } 1281 | 1282 | ticketItems.forEach(item => { 1283 | const priceText = item.querySelector('.price-num')?.textContent.trim() || ''; 1284 | const degreeText = item.querySelector('.price-degree')?.textContent.trim() || ''; 1285 | const isSoldOut = item.textContent.includes('售罄') || 1286 | item.querySelector('input[data-disabled="true"]') !== null; 1287 | 1288 | const fullText = `${degreeText} ${priceText}`; 1289 | 1290 | // 檢查是否在黑名單中 1291 | const isBlacklisted = settings.blacklist && settings.blacklist.length > 0 && 1292 | settings.blacklist.some(blacklistItem => textIncludesKeyword(fullText, blacklistItem)); 1293 | 1294 | // 檢查是否符合關鍵字 1295 | const matchesKeyword = settings.keywords.length === 0 || 1296 | settings.keywords.some(keyword => textIncludesKeyword(fullText, keyword)); 1297 | 1298 | // 決定是否顯示 1299 | let shouldShow = true; 1300 | 1301 | // 如果在黑名單中,不顯示 1302 | if (isBlacklisted) { 1303 | shouldShow = false; 1304 | } 1305 | 1306 | // 如果有關鍵字且不符合關鍵字,不顯示 1307 | if (settings.keywords.length > 0 && !matchesKeyword) { 1308 | shouldShow = false; 1309 | } 1310 | 1311 | // 如果設定隱藏已售完且已售完,不顯示 1312 | if (settings.hideSoldOut && isSoldOut) { 1313 | shouldShow = false; 1314 | } 1315 | 1316 | // 立即應用顯示/隱藏狀態 1317 | if (shouldShow) { 1318 | item.style.display = ''; 1319 | item.style.visibility = 'visible'; 1320 | item.style.opacity = '1'; 1321 | } else { 1322 | item.style.display = 'none'; 1323 | } 1324 | }); 1325 | } 1326 | 1327 | // 年代售票網站篩選 1328 | async function filterTicketComTickets() { 1329 | // 使用輪詢機制等待票區列表加載 1330 | let retryCount = 0; 1331 | const maxRetries = 20; // 最多等待20次 1332 | const retryInterval = 100; // 每100ms檢查一次,總共最多等待2秒 1333 | 1334 | async function waitForTicketList() { 1335 | return new Promise((resolve) => { 1336 | const checkElements = () => { 1337 | // 包含無 main 的灰字 Sold out 票區,僅抓有 id 的實際票區列 1338 | const areaItems = document.querySelectorAll('.area-list li[id]'); 1339 | if (areaItems.length > 0) { 1340 | resolve(true); 1341 | } else if (retryCount < maxRetries) { 1342 | retryCount++; 1343 | setTimeout(checkElements, retryInterval); 1344 | } else { 1345 | resolve(false); 1346 | } 1347 | }; 1348 | checkElements(); 1349 | }); 1350 | } 1351 | 1352 | // 等待票區列表加載 1353 | const isLoaded = await waitForTicketList(); 1354 | if (!isLoaded) { 1355 | console.log('票區列表加載超時'); 1356 | return; 1357 | } 1358 | 1359 | // 包含灰字 Sold out,排除說明文字(說明列通常沒有 id) 1360 | const areaItems = document.querySelectorAll('.area-list li[id]'); 1361 | if (!areaItems.length) return; 1362 | 1363 | // 使用 setProperty 來確保樣式被正確應用 1364 | function setDisplayStyle(element, show) { 1365 | if (show) { 1366 | element.style.setProperty('display', '', 'important'); 1367 | element.style.setProperty('visibility', 'visible', 'important'); 1368 | } else { 1369 | element.style.setProperty('display', 'none', 'important'); 1370 | } 1371 | } 1372 | 1373 | // 先移除空白/空字串的關鍵字,避免刪除條件後殘留空值 1374 | const normalizedKeywords = (settings.keywords || []) 1375 | .map(k => (k || '').trim()) 1376 | .filter(k => k.length > 0); 1377 | 1378 | const normalizedBlacklist = (settings.blacklist || []) 1379 | .map(k => (k || '').trim()) 1380 | .filter(k => k.length > 0); 1381 | 1382 | // 如果沒有有效關鍵字,則只處理黑名單與是否隱藏已售完 1383 | if (normalizedKeywords.length === 0) { 1384 | areaItems.forEach(item => { 1385 | const areaText = item.querySelector('font[color="#333"]')?.textContent?.trim() || 1386 | item.querySelector('font[color="#AAAAAA"]')?.textContent?.trim() || 1387 | item.textContent?.trim() || ''; 1388 | const statusText = item.querySelector('font[color="#FF0000"]')?.textContent?.trim() || ''; 1389 | const badgeText = item.querySelector('.badge')?.textContent?.trim() || ''; 1390 | 1391 | const statusLower = `${statusText} ${badgeText}`.toLowerCase(); 1392 | const remainMatch = statusLower.match(/餘位\s*(\d+)/); 1393 | const badgeRemainMatch = statusLower.match(/剩位\s*(\d+)/); 1394 | const remainNum = remainMatch ? parseInt(remainMatch[1], 10) : 1395 | badgeRemainMatch ? parseInt(badgeRemainMatch[1], 10) : null; 1396 | 1397 | const areaLower = areaText.toLowerCase(); 1398 | const isSoldOut = 1399 | statusLower.includes('sold out') || 1400 | statusLower.includes('已售完') || 1401 | statusLower.includes('售完') || 1402 | areaLower.includes('sold out') || 1403 | areaLower.includes('已售完') || 1404 | areaLower.includes('售完') || 1405 | (remainNum !== null && remainNum === 0); 1406 | 1407 | // 黑名單判斷(支援 AND/OR),數字則比對票價數字 1408 | const prices = (areaText.match(/\d+/g) || []).map(p => parseInt(p, 10)); 1409 | const isBlacklisted = normalizedBlacklist.some(blk => { 1410 | const orParts = blk.split('+').map(part => part.trim()).filter(Boolean); 1411 | return orParts.some(orPart => { 1412 | const andParts = orPart.split(',').map(part => part.trim()).filter(Boolean); 1413 | return andParts.every(andPart => { 1414 | const clean = andPart.replace(/,/g, ''); 1415 | if (!isNaN(clean) && clean !== '') { 1416 | return prices.includes(parseInt(clean, 10)); 1417 | } 1418 | return textIncludesKeyword(areaText, andPart); 1419 | }); 1420 | }); 1421 | }); 1422 | 1423 | let shouldShow = true; 1424 | if (isBlacklisted) shouldShow = false; 1425 | if (settings.hideSoldOut && isSoldOut) shouldShow = false; 1426 | setDisplayStyle(item, shouldShow); 1427 | }); 1428 | return; 1429 | } 1430 | 1431 | // 支援AND/OR邏輯的關鍵字處理 1432 | const keywordGroups = normalizedKeywords.map(keyword => { 1433 | return keyword.split('+').map(k => { 1434 | return k.split(',').map(item => item.trim()).filter(item => item); 1435 | }).filter(group => group.length > 0); 1436 | }); 1437 | 1438 | const blacklistGroups = normalizedBlacklist.map(keyword => { 1439 | return keyword.split('+').map(k => { 1440 | return k.split(',').map(item => item.trim()).filter(item => item); 1441 | }).filter(group => group.length > 0); 1442 | }); 1443 | 1444 | areaItems.forEach(item => { 1445 | // 票區名稱:優先取黑字,否則用整段文字 1446 | const areaText = item.querySelector('font[color="#333"]')?.textContent?.trim() || 1447 | item.querySelector('font[color="#AAAAAA"]')?.textContent?.trim() || 1448 | item.textContent?.trim() || ''; 1449 | const statusText = item.querySelector('font[color="#FF0000"]')?.textContent?.trim() || ''; 1450 | const badgeText = item.querySelector('.badge')?.textContent?.trim() || ''; 1451 | 1452 | const statusLower = `${statusText} ${badgeText}`.toLowerCase(); 1453 | const remainMatch = statusLower.match(/餘位\s*(\d+)/); 1454 | const remainNum = remainMatch ? parseInt(remainMatch[1], 10) : null; 1455 | 1456 | const areaLower = areaText.toLowerCase(); 1457 | 1458 | const prices = (areaText.match(/\d+/g) || []).map(p => parseInt(p, 10)); 1459 | 1460 | // 售完判斷:明確含 Sold out/已售完/售完(名稱或狀態都檢查),或餘位=0;單純「餘位XX」「剩位」視為可售 1461 | const isSoldOut = 1462 | statusLower.includes('sold out') || 1463 | statusLower.includes('已售完') || 1464 | statusLower.includes('售完') || 1465 | areaLower.includes('sold out') || 1466 | areaLower.includes('已售完') || 1467 | areaLower.includes('售完') || 1468 | (remainNum !== null && remainNum === 0); 1469 | 1470 | const isBlacklisted = blacklistGroups.some(orGroup => 1471 | orGroup.some(andGroup => { 1472 | return andGroup.every(keyword => { 1473 | const clean = keyword.replace(/,/g, ''); 1474 | if (!isNaN(clean) && clean !== '') { 1475 | return prices.includes(parseInt(clean, 10)); 1476 | } 1477 | return textIncludesKeyword(areaText, keyword); 1478 | }); 1479 | }) 1480 | ); 1481 | 1482 | const shouldShow = keywordGroups.some(orGroup => 1483 | orGroup.some(andGroup => { 1484 | return andGroup.every(keyword => { 1485 | if (!isNaN(keyword)) { 1486 | // 數字比對 1487 | const priceToFind = parseInt(keyword); 1488 | const prices = (areaText.match(/\d+/g) || []).map(p => parseInt(p)); 1489 | return prices.includes(priceToFind); 1490 | } else { 1491 | // 文字比對 1492 | return textIncludesKeyword(areaText, keyword); 1493 | } 1494 | }); 1495 | }) 1496 | ) && !isBlacklisted; 1497 | 1498 | setDisplayStyle(item, shouldShow && (!isSoldOut || !settings.hideSoldOut)); 1499 | }); 1500 | } 1501 | 1502 | // 遠大售票平台票券篩選 1503 | async function filterTicketPlusTickets() { 1504 | // 等待票券元素出現 1505 | const waitForTickets = () => { 1506 | return new Promise((resolve) => { 1507 | const check = () => { 1508 | const tickets = document.querySelectorAll('.v-expansion-panels.seats-area .v-expansion-panel'); 1509 | if (tickets.length > 0) { 1510 | resolve(tickets); 1511 | } else { 1512 | requestAnimationFrame(check); 1513 | } 1514 | }; 1515 | check(); 1516 | }); 1517 | }; 1518 | 1519 | const ticketPanels = await waitForTickets(); 1520 | 1521 | // 如果沒有任何篩選條件,顯示所有票券 1522 | if (settings.keywords.length === 0 && (!settings.blacklist || settings.blacklist.length === 0)) { 1523 | ticketPanels.forEach(panel => { 1524 | const soldOutText = panel.querySelector('.soldout'); 1525 | const remainingText = panel.querySelector('small'); 1526 | 1527 | if (settings.hideSoldOut && (soldOutText || (remainingText && remainingText.textContent.includes('剩餘 0')))) { 1528 | panel.style.display = 'none'; 1529 | } else { 1530 | panel.style.display = ''; 1531 | panel.classList.add('filter-processed'); 1532 | } 1533 | }); 1534 | return; 1535 | } 1536 | 1537 | ticketPanels.forEach(panel => { 1538 | const areaText = panel.querySelector('.d-flex.align-center')?.textContent.trim() || ''; 1539 | const priceText = panel.querySelector('.text-right.col.col-4')?.textContent.trim() || ''; 1540 | const soldOutText = panel.querySelector('.soldout'); 1541 | const remainingText = panel.querySelector('small'); 1542 | 1543 | const isSoldOut = soldOutText || (remainingText && remainingText.textContent.includes('剩餘 0')); 1544 | const fullText = `${areaText} ${priceText}`; 1545 | 1546 | // 檢查是否在黑名單中 1547 | const isBlacklisted = settings.blacklist && settings.blacklist.length > 0 && 1548 | settings.blacklist.some(blacklistItem => { 1549 | const orParts = blacklistItem.split('+').map(part => part.trim()); 1550 | return orParts.some(orPart => { 1551 | const andParts = orPart.split(',').map(part => part.trim()); 1552 | return andParts.every(andPart => textIncludesKeyword(fullText, andPart)); 1553 | }); 1554 | }); 1555 | 1556 | // 檢查是否符合關鍵字 1557 | const matchesKeyword = settings.keywords.length === 0 || 1558 | settings.keywords.some(keyword => { 1559 | const orParts = keyword.split('+').map(part => part.trim()); 1560 | return orParts.some(orPart => { 1561 | const andParts = orPart.split(',').map(part => part.trim()); 1562 | return andParts.every(andPart => textIncludesKeyword(fullText, andPart)); 1563 | }); 1564 | }); 1565 | 1566 | // 決定是否顯示 1567 | let shouldShow = true; 1568 | 1569 | if (isBlacklisted) shouldShow = false; 1570 | if (settings.keywords.length > 0 && !matchesKeyword) shouldShow = false; 1571 | if (settings.hideSoldOut && isSoldOut) shouldShow = false; 1572 | 1573 | // 應用顯示/隱藏狀態 1574 | if (shouldShow) { 1575 | panel.style.display = ''; 1576 | panel.classList.add('filter-processed'); 1577 | } else { 1578 | panel.style.display = 'none'; 1579 | panel.classList.remove('filter-processed'); 1580 | } 1581 | }); 1582 | } 1583 | 1584 | // Fami Life 票券篩選 1585 | async function filterFamiTickets() { 1586 | // 等待票券列表加載 1587 | await waitForElement('.saleTr'); 1588 | 1589 | const ticketRows = document.querySelectorAll('.saleTr'); 1590 | if (!ticketRows.length) return; 1591 | 1592 | // 处理过滤逻辑 1593 | ticketRows.forEach(row => { 1594 | const areaCell = row.querySelector('[data-title="票區:"]'); 1595 | const priceCell = row.querySelector('.textPrice'); 1596 | const statusCell = row.querySelector('#SEAT'); 1597 | 1598 | if (areaCell && priceCell) { 1599 | const areaText = areaCell.textContent.trim(); 1600 | const priceText = priceCell.textContent.trim(); 1601 | const fullText = `${areaText} ${priceText}`; 1602 | const isSoldOut = statusCell && statusCell.textContent.trim().includes('售完'); 1603 | 1604 | // 檢查是否在黑名單中 1605 | const isBlacklisted = settings.blacklist && settings.blacklist.length > 0 && 1606 | settings.blacklist.some(blacklistItem => { 1607 | const orParts = blacklistItem.split('+').map(part => part.trim()); 1608 | return orParts.some(orPart => { 1609 | const andParts = orPart.split(',').map(part => part.trim()); 1610 | return andParts.every(andPart => textIncludesKeyword(fullText, andPart)); 1611 | }); 1612 | }); 1613 | 1614 | // 檢查是否符合關鍵字 1615 | const matchesKeyword = settings.keywords.length === 0 || 1616 | settings.keywords.some(keyword => { 1617 | const orParts = keyword.split('+').map(part => part.trim()); 1618 | return orParts.some(orPart => { 1619 | const andParts = orPart.split(',').map(part => part.trim()); 1620 | return andParts.every(andPart => textIncludesKeyword(fullText, andPart)); 1621 | }); 1622 | }); 1623 | 1624 | // 決定是否顯示 1625 | let shouldShow = true; 1626 | 1627 | if (isBlacklisted) shouldShow = false; 1628 | if (settings.keywords.length > 0 && !matchesKeyword) shouldShow = false; 1629 | if (settings.hideSoldOut && isSoldOut) shouldShow = false; 1630 | 1631 | // 应用显示/隐藏状态 1632 | if (!shouldShow) { 1633 | row.style.setProperty('display', 'none', 'important'); 1634 | } else { 1635 | row.style.removeProperty('display'); 1636 | } 1637 | } 1638 | }); 1639 | 1640 | // 通知 earlyLoader 筛选已完成 1641 | window.postMessage({ type: 'FILTER_APPLIED' }, '*'); 1642 | } 1643 | 1644 | // 在页面加载时初始化 Fami Life 平台 1645 | if (getCurrentSite() === 'fami') { 1646 | // 在 DOMContentLoaded 时执行初始化 1647 | if (document.readyState === 'loading') { 1648 | document.addEventListener('DOMContentLoaded', () => { 1649 | filterFamiTickets(); 1650 | }); 1651 | } else { 1652 | filterFamiTickets(); 1653 | } 1654 | 1655 | // 监听消息以处理动态加载的内容 1656 | window.addEventListener('message', function(event) { 1657 | if (event.data.type === 'NEW_TICKETS_FOUND') { 1658 | filterFamiTickets(); 1659 | } 1660 | }); 1661 | } 1662 | 1663 | // 新增:檢查文本是否在黑名單中 1664 | function isInBlacklist(text, blacklist) { 1665 | if (!blacklist || blacklist.length === 0) return false; 1666 | 1667 | // 將文本轉換為小寫並移除多餘空格,以便比對 1668 | const normalizedText = text.toLowerCase().trim(); 1669 | 1670 | // 檢查每個黑名單關鍵字 1671 | return blacklist.some(blacklistItem => { 1672 | // 處理 OR 邏輯(用 + 分隔) 1673 | const orParts = blacklistItem.split('+').map(part => part.trim()); 1674 | 1675 | // 如果任一 OR 條件符合就返回 true 1676 | return orParts.some(orPart => { 1677 | // 處理 AND 邏輯(用 , 分隔) 1678 | const andParts = orPart.split(',').map(part => part.trim()); 1679 | 1680 | // 所有 AND 條件都要符合才返回 true 1681 | return andParts.every(andPart => { 1682 | if (!andPart) return false; // 空字串不納入比對 1683 | 1684 | // 使用 textIncludesKeyword 進行比對,支援數字轉換 1685 | return textIncludesKeyword(normalizedText, andPart); 1686 | }); 1687 | }); 1688 | }); 1689 | } 1690 | 1691 | // 监听来自background.js的消息 1692 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 1693 | try { 1694 | // 处理快捷键触发的扩展状态变化 1695 | if (request.type === 'EXTENSION_STATE_CHANGED') { 1696 | const enabled = request.enabled; 1697 | const site = getCurrentSite(); 1698 | 1699 | // 更新UI状态 1700 | if (enabled) { 1701 | // 重新应用过滤器 1702 | filterTickets(); 1703 | showFilterStatus(); 1704 | showServerTime(); 1705 | } else { 1706 | // 显示所有区域 1707 | showAllAreas(); 1708 | // 移除状态显示 1709 | const notification = document.querySelector('.ticket-filter-notification'); 1710 | const timeDisplay = document.querySelector('.server-time-display'); 1711 | if (notification) notification.remove(); 1712 | if (timeDisplay) timeDisplay.remove(); 1713 | 1714 | // 特别处理 Fami Life 平台 1715 | if (site === 'fami') { 1716 | // 移除所有已应用的样式 1717 | document.querySelectorAll('.saleTr').forEach(row => { 1718 | row.style.removeProperty('display'); 1719 | row.style.removeProperty('opacity'); 1720 | row.style.removeProperty('visibility'); 1721 | row.style.removeProperty('transition'); 1722 | row.classList.remove('filter-ready'); 1723 | }); 1724 | } 1725 | } 1726 | 1727 | return true; 1728 | } 1729 | 1730 | const site = getCurrentSite(); 1731 | 1732 | // 處理拓元的消息 1733 | if (site === 'tixcraft') { 1734 | if (request.type === 'UPDATE_SETTINGS') { 1735 | settings = { ...settings, ...request.settings }; 1736 | filterTickets(); 1737 | showFilterStatus(); 1738 | sendResponse({ success: true }); 1739 | } else if (request.type === 'SHOW_ALL') { 1740 | settings.keywords = []; 1741 | settings.blacklist = []; 1742 | settings.hideSoldOut = false; 1743 | 1744 | // 通知 popup 更新標籤 1745 | chrome.runtime.sendMessage({ 1746 | type: 'UPDATE_POPUP_LABELS', 1747 | settings: { 1748 | keywords: [], 1749 | blacklist: [], 1750 | hideSoldOut: false 1751 | } 1752 | }); 1753 | 1754 | showAllAreas(); 1755 | showFilterStatus(); 1756 | sendResponse({ success: true }); 1757 | } 1758 | } 1759 | 1760 | // 處理KKTIX的消息 1761 | if (site === 'kktix') { 1762 | if (request.action === 'updateFilter') { 1763 | settings.keywords = request.keywords || []; 1764 | settings.blacklist = request.blacklist || []; // 確保更新黑名單 1765 | settings.showAllPrices = false; 1766 | if (request.settings && request.settings.hideSoldOut !== undefined) { 1767 | settings.hideSoldOut = request.settings.hideSoldOut; 1768 | } 1769 | 1770 | // 立即執行篩選 1771 | filterTickets(); 1772 | showFilterStatus(); 1773 | sendResponse({ success: true }); 1774 | return true; 1775 | } 1776 | if (request.action === 'showAllPrices') { 1777 | settings.keywords = []; 1778 | settings.blacklist = []; // 清空黑名單 1779 | settings.showAllPrices = true; 1780 | settings.hideSoldOut = false; 1781 | 1782 | filterTickets(); 1783 | showFilterStatus(); 1784 | sendResponse({ success: true }); 1785 | return true; 1786 | } 1787 | if (request.action === 'updateBlacklist') { // 新增:處理黑名單更新 1788 | settings.blacklist = request.blacklist || []; 1789 | filterTickets(); 1790 | showFilterStatus(); 1791 | sendResponse({ success: true }); 1792 | return true; 1793 | } 1794 | } 1795 | 1796 | // 處理ibon的消息 1797 | if (site === 'ibon') { 1798 | if (request.type === 'UPDATE_IBON_SETTINGS') { 1799 | console.log('收到ibon設定更新:', request.settings); 1800 | // 重置所有設定 1801 | settings = { 1802 | keywords: request.settings.keywords || [], 1803 | blacklist: request.settings.blacklist || [], 1804 | hideSoldOut: request.settings.hideSoldOut || false, 1805 | isProcessing: false 1806 | }; 1807 | 1808 | // 清除所有過濾狀態 1809 | showAllAreas(); 1810 | 1811 | // 更新顯示狀態 1812 | showFilterStatus(); 1813 | 1814 | // 如果有新的篩選條件,則重新應用 1815 | if (settings.keywords.length > 0 || settings.blacklist.length > 0 || settings.hideSoldOut) { 1816 | filterTickets(); 1817 | } 1818 | 1819 | sendResponse({ success: true }); 1820 | return true; 1821 | } 1822 | } 1823 | 1824 | // 處理cityline的消息 1825 | if (site === 'cityline') { 1826 | if (request.type === 'UPDATE_CITYLINE_SETTINGS') { 1827 | settings.keywords = request.settings.keywords || []; 1828 | settings.blacklist = request.settings.blacklist || []; // 添加黑名單設定 1829 | settings.hideSoldOut = request.settings.hideSoldOut || false; 1830 | filterTickets(); 1831 | showFilterStatus(); 1832 | sendResponse({ success: true }); 1833 | } 1834 | } 1835 | 1836 | // 處理年代售票網站的消息 1837 | if (site === 'ticket') { 1838 | if (request.type === 'UPDATE_TICKET_SETTINGS') { 1839 | settings.keywords = request.settings.keywords || []; 1840 | settings.blacklist = request.settings.blacklist || []; 1841 | settings.hideSoldOut = request.settings.hideSoldOut || false; 1842 | filterTickets(); 1843 | showFilterStatus(); 1844 | sendResponse({ success: true }); 1845 | } 1846 | } 1847 | 1848 | // 處理遠大售票平台的消息 1849 | if (site === 'ticketplus') { 1850 | if (request.type === 'UPDATE_TICKETPLUS_SETTINGS') { 1851 | settings.keywords = request.settings.keywords || []; 1852 | settings.blacklist = request.settings.blacklist || []; 1853 | settings.hideSoldOut = request.settings.hideSoldOut || false; 1854 | filterTickets(); 1855 | showFilterStatus(); 1856 | sendResponse({ success: true }); 1857 | } 1858 | } 1859 | 1860 | // 在 ibon 票券頁面時觸發重整 1861 | if (site === 'ibon' && ( 1862 | location.href.includes('UTK0201_000.aspx') || 1863 | location.href.match(/Event\/[A-Z0-9]+\/[A-Z0-9]+/) 1864 | )) { 1865 | console.log("ibon票券頁面,跳過重整,直接套用篩選"); 1866 | setTimeout(() => { 1867 | filterTickets(); 1868 | showFilterStatus(); 1869 | }, 50); 1870 | } 1871 | 1872 | // 處理 Fami Life 的消息 1873 | if (site === 'fami') { 1874 | if (request.type === 'UPDATE_FAMI_SETTINGS') { 1875 | settings.keywords = request.settings.keywords || []; 1876 | settings.blacklist = request.settings.blacklist || []; 1877 | settings.hideSoldOut = request.settings.hideSoldOut || false; 1878 | filterTickets(); 1879 | showFilterStatus(); 1880 | sendResponse({ success: true }); 1881 | } 1882 | } 1883 | 1884 | // JKFace票券篩選 1885 | if (site === 'jkface') { 1886 | if (request.type === 'UPDATE_JKFACE_SETTINGS') { 1887 | settings.keywords = request.settings.keywords || []; 1888 | settings.blacklist = request.settings.blacklist || []; 1889 | settings.hideSoldOut = request.settings.hideSoldOut || false; 1890 | filterTickets(); 1891 | showFilterStatus(); 1892 | sendResponse({ success: true }); 1893 | } 1894 | } 1895 | 1896 | // 在 chrome.runtime.onMessage.addListener 中添加对寬宏售票的处理 1897 | if (site === 'kham') { 1898 | if (request.type === 'UPDATE_KHAM_SETTINGS') { 1899 | settings.keywords = request.settings.keywords || []; 1900 | settings.blacklist = request.settings.blacklist || []; 1901 | settings.hideSoldOut = request.settings.hideSoldOut || false; 1902 | 1903 | // 改用通用的 filterTickets 1904 | filterTickets(); 1905 | showFilterStatus(); 1906 | sendResponse({ success: true }); 1907 | } 1908 | } 1909 | } catch (error) { 1910 | console.error('處理訊息時發生錯誤:', error); 1911 | sendResponse({ success: false, error: error.message }); 1912 | } 1913 | return true; 1914 | }); 1915 | 1916 | // 初始執行 1917 | const site = getCurrentSite(); 1918 | if (site === 'tixcraft' || site === 'kktix' || site === 'ibon' || site === 'cityline' || 1919 | site === 'ticket' || site === 'ticketplus' || site === 'fami' || site === 'jkface' || site === 'kham') { // 添加 kham 1920 | if (document.readyState === 'complete') { 1921 | showFilterStatus(); 1922 | filterTickets(); 1923 | } else { 1924 | window.addEventListener('load', () => { 1925 | showFilterStatus(); 1926 | filterTickets(); 1927 | }); 1928 | } 1929 | } 1930 | 1931 | // 監聽DOM變化 1932 | let filterDebounceTimer = null; 1933 | let statusDebounceTimer = null; 1934 | const observer = new MutationObserver((mutations) => { 1935 | const site = getCurrentSite(); 1936 | if (!site) return; 1937 | 1938 | // 若目前是 Cloudflare 驗證頁,暫停所有篩選;從驗證返回時立即套用 1939 | const nowCf = isCloudflareChallengePage(); 1940 | if (nowCf) { 1941 | __lastCfChallenge = true; 1942 | return; 1943 | } 1944 | if (__lastCfChallenge && !nowCf) { 1945 | __lastCfChallenge = false; 1946 | filterTickets(); 1947 | showFilterStatus(); 1948 | return; 1949 | } 1950 | 1951 | // 使用不同的計時器處理篩選和狀態顯示 1952 | if (filterDebounceTimer) { 1953 | clearTimeout(filterDebounceTimer); 1954 | } 1955 | if (statusDebounceTimer) { 1956 | clearTimeout(statusDebounceTimer); 1957 | } 1958 | 1959 | // 根據網站定義不同的延遲時間 1960 | const debounceTime = getDebounceTime(); 1961 | 1962 | // 篩選功能的防抖 1963 | filterDebounceTimer = setTimeout(() => { 1964 | if (site === 'tixcraft' && window.location.href.includes('/ticket/area/')) { 1965 | filterTickets(); 1966 | } else if (site === 'kktix' && document.querySelector('.ticket-unit')) { 1967 | filterTickets(); 1968 | } else if (site === 'ibon') { 1969 | filterTickets(); 1970 | } else if (site === 'cityline' && document.querySelector('.price-box1')) { 1971 | filterTickets(); 1972 | } else if (site === 'ticket') { 1973 | filterTickets(); 1974 | } else if (site === 'ticketplus') { 1975 | filterTickets(); 1976 | } else if (site === 'fami') { 1977 | filterTickets(); 1978 | } else if (site === 'jkface') { 1979 | filterTickets(); 1980 | } else if (site === 'kham') { 1981 | filterTickets(); 1982 | } 1983 | }, debounceTime); 1984 | 1985 | // 狀態顯示的防抖,使用相同的延遲時間 1986 | statusDebounceTimer = setTimeout(() => { 1987 | showFilterStatus(); 1988 | }, debounceTime); 1989 | }); 1990 | 1991 | // 使用更高效的觀察配置 1992 | observer.observe(document.body, { 1993 | childList: true, 1994 | subtree: true, 1995 | attributes: true, 1996 | attributeFilter: ['style', 'class', 'disabled'], 1997 | characterData: false 1998 | }); 1999 | 2000 | // 處理頁面從背景切回前景時的情況 2001 | document.addEventListener('visibilitychange', function() { 2002 | if (document.visibilityState === 'visible') { 2003 | loadSettings(); 2004 | } 2005 | }); 2006 | 2007 | // 顯示時間 2008 | function showServerTime() { 2009 | chrome.storage.local.get(['showServerTime', 'extensionEnabled'], function(result) { 2010 | if (result.showServerTime === false || result.extensionEnabled === false) { 2011 | const existingDisplay = document.querySelector('.server-time-display'); 2012 | if (existingDisplay) { 2013 | existingDisplay.remove(); 2014 | } 2015 | 2016 | // 時間顯示框被移除,更新篩選條件框位置 2017 | const notification = document.querySelector('.ticket-filter-notification'); 2018 | if (notification) { 2019 | notification.style.top = '10px'; 2020 | } 2021 | 2022 | return; 2023 | } 2024 | 2025 | let timeDisplay = document.querySelector('.server-time-display'); 2026 | if (!timeDisplay) { 2027 | timeDisplay = document.createElement('div'); 2028 | timeDisplay.className = 'server-time-display'; 2029 | document.body.appendChild(timeDisplay); 2030 | 2031 | // 時間顯示框添加後,更新篩選條件框位置 2032 | const notification = document.querySelector('.ticket-filter-notification'); 2033 | if (notification) { 2034 | notification.style.top = '50px'; 2035 | } 2036 | } 2037 | 2038 | // 定時更新顯示本地時間(CST) 2039 | function updateDisplay() { 2040 | const currentTime = new Date(); 2041 | timeDisplay.textContent = `本地時間:${currentTime.toLocaleString('zh-TW', { 2042 | timeZone: 'Asia/Taipei', 2043 | hour12: false, 2044 | hour: '2-digit', 2045 | minute: '2-digit', 2046 | second: '2-digit' 2047 | })}`; 2048 | } 2049 | 2050 | // 每秒更新一次時間 2051 | const displayInterval = setInterval(updateDisplay, 1000); 2052 | 2053 | // 立即顯示時間 2054 | updateDisplay(); 2055 | 2056 | // 清理函數 2057 | function cleanup() { 2058 | clearInterval(displayInterval); 2059 | } 2060 | 2061 | // 監聽元素移除 2062 | const observer = new MutationObserver((mutations) => { 2063 | if (!document.contains(timeDisplay)) { 2064 | cleanup(); 2065 | observer.disconnect(); 2066 | } 2067 | }); 2068 | 2069 | observer.observe(document.body, { 2070 | childList: true, 2071 | subtree: true 2072 | }); 2073 | }); 2074 | } 2075 | 2076 | // 監聽設置變化 2077 | chrome.storage.onChanged.addListener(function(changes, namespace) { 2078 | if (changes.showServerTime) { 2079 | if (changes.showServerTime.newValue === false) { 2080 | const existingDisplay = document.querySelector('.server-time-display'); 2081 | if (existingDisplay) { 2082 | existingDisplay.remove(); 2083 | } 2084 | } else { 2085 | showServerTime(); 2086 | } 2087 | // 重新顯示篩選狀態,以更新位置 2088 | showFilterStatus(); 2089 | } 2090 | 2091 | // 當篩選條件顯示設置改變時,重新顯示篩選狀態 2092 | if (changes.showFilterStatus !== undefined) { 2093 | if (changes.showFilterStatus.newValue === false) { 2094 | // 如果設置為不顯示,移除現有的篩選條件顯示 2095 | const existingNotification = document.querySelector('.ticket-filter-notification'); 2096 | if (existingNotification) { 2097 | existingNotification.remove(); 2098 | } 2099 | } else { 2100 | // 如果設置為顯示,重新顯示篩選條件 2101 | showFilterStatus(); 2102 | } 2103 | } 2104 | 2105 | // 監聽擴展啟用狀態的變化 2106 | if (changes.extensionEnabled !== undefined) { 2107 | const site = getCurrentSite(); 2108 | if (!site) return; 2109 | 2110 | if (changes.extensionEnabled.newValue === false) { 2111 | // 擴展被停用 2112 | // 1. 移除所有顯示 2113 | const notification = document.querySelector('.ticket-filter-notification'); 2114 | const timeDisplay = document.querySelector('.server-time-display'); 2115 | if (notification) notification.remove(); 2116 | if (timeDisplay) timeDisplay.remove(); 2117 | 2118 | // 2. 重置所有設定 2119 | settings.keywords = []; 2120 | settings.blacklist = []; // 確保黑名單也被清空 2121 | settings.hideSoldOut = false; 2122 | settings.showAllPrices = true; 2123 | settings.isProcessing = false; 2124 | 2125 | // 3. 強制顯示所有區域 2126 | showAllAreas(); 2127 | 2128 | // 4. 對於 ibon 網站,額外處理 2129 | if (site === 'ibon') { 2130 | // 移除所有由擴充功能添加的樣式 2131 | function removeStyles(element) { 2132 | if (element.shadowRoot) { 2133 | const styles = element.shadowRoot.querySelectorAll('style.extension-filter-style'); 2134 | styles.forEach(style => style.remove()); 2135 | 2136 | // 清除所有票券的隱藏狀態 2137 | const rows = element.shadowRoot.querySelectorAll('tr[id^="B"]'); 2138 | rows.forEach(row => { 2139 | row.style.removeProperty('display'); 2140 | row.classList.remove('hidden-by-extension'); 2141 | Array.from(row.children).forEach(cell => { 2142 | cell.style.removeProperty('display'); 2143 | }); 2144 | }); 2145 | } 2146 | Array.from(element.children || []).forEach(removeStyles); 2147 | } 2148 | removeStyles(document.documentElement); 2149 | } 2150 | } else { 2151 | // 擴展被啟用,重新初始化所有功能 2152 | loadSettings(); 2153 | showServerTime(); 2154 | showFilterStatus(); 2155 | filterTickets(); 2156 | } 2157 | } 2158 | }); 2159 | 2160 | // 在頁面加載完成後顯示時間(只保留这一个初始化点) 2161 | if (document.readyState === 'complete') { 2162 | showServerTime(); 2163 | } else { 2164 | window.addEventListener('load', showServerTime); 2165 | } 2166 | 2167 | // 设置页面加载策略 2168 | document.documentElement.setAttribute('pageLoadStrategy', 'eager'); 2169 | 2170 | // 在頁面載入和切換時重新載入設定 2171 | document.addEventListener('DOMContentLoaded', () => { 2172 | const site = getCurrentSite(); 2173 | if (site) { 2174 | // 设置网站标识 2175 | document.body.setAttribute('data-site', site); 2176 | 2177 | if (site === 'ibon') { 2178 | // 同时支持新旧两种URL格式 2179 | if (location.href.includes('UTK0201_000.aspx') || 2180 | location.href.match(/Event\/[A-Z0-9]+\/[A-Z0-9]+/)) { 2181 | chrome.storage.local.get(['ibonKeywords', 'ibonBlacklist', 'ibonHideSoldOut'], (result) => { 2182 | settings.keywords = result.ibonKeywords || []; 2183 | settings.blacklist = result.ibonBlacklist || []; 2184 | settings.hideSoldOut = result.ibonHideSoldOut || false; 2185 | filterTickets(); 2186 | showFilterStatus(); 2187 | }); 2188 | } 2189 | } 2190 | } 2191 | }); 2192 | 2193 | // 根據網站定義不同的延遲時間 2194 | function getDebounceTime() { 2195 | const site = getCurrentSite(); 2196 | switch(site) { 2197 | case 'ibon': 2198 | return 250; // ibon 網站需要較長的延遲 2199 | case 'ticketplus': 2200 | return 50; // 遠大售票也需要稍長延遲 2201 | default: 2202 | return 50; // 其他網站使用短延遲 2203 | } 2204 | } 2205 | 2206 | function isIbonTicketPage(url) { 2207 | return url.includes('ibon.com.tw/application/UTK0201_000.aspx') || 2208 | url.includes('ibon.com.tw/Event/'); // 简化匹配规则 2209 | } 2210 | 2211 | // 在页面加载最开始就注入样式 2212 | const earlyStyle = document.createElement('style'); 2213 | earlyStyle.textContent = ` 2214 | /* 新版ibon售票网站初始化样式 */ 2215 | body.ibon-loading table.table, 2216 | body.ibon-loading tr.ng-star-inserted { 2217 | display: none !important; 2218 | } 2219 | 2220 | tr.ng-star-inserted.hidden-by-extension { 2221 | display: none !important; 2222 | } 2223 | `; 2224 | document.head.appendChild(earlyStyle); 2225 | 2226 | // 尽早执行初始化隐藏 2227 | function initializeIbonHiding() { 2228 | if (window.location.href.match(/ibon\.com\.tw\/Event\/[A-Z0-9]+\/[A-Z0-9]+/)) { 2229 | document.body.classList.add('ibon-loading'); 2230 | } 2231 | } 2232 | 2233 | // 在最早可能的时机执行隐藏 2234 | initializeIbonHiding(); 2235 | document.addEventListener('DOMContentLoaded', initializeIbonHiding); 2236 | 2237 | // 确保在动态加载的表格上也应用隐藏 2238 | const earlyObserver = new MutationObserver((mutations) => { 2239 | for (const mutation of mutations) { 2240 | if (mutation.type === 'childList') { 2241 | mutation.addedNodes.forEach(node => { 2242 | if (node.nodeName === 'TABLE' && node.classList.contains('table')) { 2243 | node.classList.add('ibon-early-hide'); 2244 | } 2245 | }); 2246 | } 2247 | } 2248 | }); 2249 | 2250 | earlyObserver.observe(document.documentElement, { 2251 | childList: true, 2252 | subtree: true 2253 | }); 2254 | 2255 | // JKFace票券篩選 2256 | async function filterJKFaceTickets() { 2257 | // 检查是否在活动详情页面 2258 | const isEventPage = window.location.href.includes('/events/'); 2259 | 2260 | if (isEventPage) { 2261 | // 处理活动详情页面的场次列表 2262 | const eventSections = document.querySelectorAll('section.mx-3'); 2263 | if (!eventSections.length) return; 2264 | 2265 | eventSections.forEach(section => { 2266 | // 获取完整的场次信息,包括日期、时间和表演者名字 2267 | const timeAndPerformerText = section.querySelector('span[class*="text-"][class*="leading-"]')?.textContent.trim() || ''; 2268 | const statusButton = section.querySelector('button[disabled]'); 2269 | const isSoldOut = statusButton && ( 2270 | statusButton.textContent.includes('已售完') || 2271 | statusButton.textContent.includes('售完') || 2272 | statusButton.textContent.includes('全數售罄') || 2273 | statusButton.textContent.includes('尚未開始') 2274 | ); 2275 | 2276 | let shouldHide = false; 2277 | 2278 | // 检查关键字匹配 2279 | if (settings.keywords && settings.keywords.length > 0) { 2280 | const matchesKeyword = settings.keywords.some(keyword => { 2281 | // 处理数字关键字 - 移除逗号后比较 2282 | const cleanKeyword = keyword.replace(/,/g, ''); 2283 | if (!isNaN(cleanKeyword)) { 2284 | // 如果是数字,尝试匹配时间 2285 | return timeAndPerformerText.includes(cleanKeyword); 2286 | } 2287 | 2288 | // 非数字关键字进行 OR/AND 处理 2289 | const orParts = keyword.split('+').map(part => part.trim()); 2290 | return orParts.some(orPart => { 2291 | const andParts = orPart.split(',').map(part => part.trim()); 2292 | return andParts.every(andPart => { 2293 | // 如果是单个字符,使用简单包含匹配 2294 | if (andPart.length === 1) { 2295 | return timeAndPerformerText.toLowerCase().includes(andPart.toLowerCase()); 2296 | } 2297 | return textIncludesKeyword(timeAndPerformerText, andPart); 2298 | }); 2299 | }); 2300 | }); 2301 | 2302 | if (!matchesKeyword) shouldHide = true; 2303 | } 2304 | 2305 | // 检查黑名单匹配 2306 | if (settings.blacklist && settings.blacklist.length > 0) { 2307 | const isBlacklisted = settings.blacklist.some(blacklistItem => { 2308 | // 处理数字黑名单 - 移除逗号后比较 2309 | const cleanBlacklistItem = blacklistItem.replace(/,/g, ''); 2310 | if (!isNaN(cleanBlacklistItem)) { 2311 | return timeAndPerformerText.includes(cleanBlacklistItem); 2312 | } 2313 | 2314 | const orParts = blacklistItem.split('+').map(part => part.trim()); 2315 | return orParts.some(orPart => { 2316 | const andParts = orPart.split(',').map(part => part.trim()); 2317 | return andParts.every(andPart => { 2318 | // 如果是单个字符,使用简单包含匹配 2319 | if (andPart.length === 1) { 2320 | return timeAndPerformerText.toLowerCase().includes(andPart.toLowerCase()); 2321 | } 2322 | return textIncludesKeyword(timeAndPerformerText, andPart); 2323 | }); 2324 | }); 2325 | }); 2326 | if (isBlacklisted) shouldHide = true; 2327 | } 2328 | 2329 | // 检查是否隐藏已售完 2330 | if (settings.hideSoldOut && isSoldOut) { 2331 | shouldHide = true; 2332 | } 2333 | 2334 | // 设置显示状态,保持原有的HTML结构 2335 | if (shouldHide) { 2336 | section.style.setProperty('display', 'none', 'important'); 2337 | } else { 2338 | section.style.removeProperty('display'); 2339 | } 2340 | }); 2341 | } else { 2342 | // 原有的票券列表页面处理逻辑 2343 | const ticketForms = document.querySelectorAll('form'); 2344 | if (!ticketForms.length) return; 2345 | 2346 | ticketForms.forEach(form => { 2347 | const ticketNameElement = form.querySelector('.font-medium span'); 2348 | const ticketName = ticketNameElement ? ticketNameElement.textContent.trim() : ''; 2349 | 2350 | // 更精确地获取价格元素 2351 | const priceElement = form.querySelector('.opacity-80 span.mx-2'); 2352 | let priceText = ''; 2353 | if (priceElement) { 2354 | // 移除 TWD$ 前缀,移除逗号,移除所有空格 2355 | priceText = priceElement.textContent 2356 | .replace('TWD$', '') 2357 | .replace(/,/g, '') 2358 | .replace(/\s+/g, '') 2359 | .trim(); 2360 | } 2361 | 2362 | const fullText = `${ticketName} ${priceText}`; 2363 | 2364 | // 检查售完状态 - 先找到最后一个按钮 2365 | const buttons = form.querySelectorAll('button'); 2366 | const lastButton = buttons[buttons.length - 1]; 2367 | const isSoldOut = lastButton && ( 2368 | lastButton.textContent.includes('已售完') || 2369 | lastButton.textContent.includes('售完') || 2370 | lastButton.textContent.includes('全數售罄') || 2371 | lastButton.textContent.includes('尚未開始') 2372 | ); 2373 | 2374 | let shouldHide = false; 2375 | 2376 | // 檢查關鍵字 - 支持 AND/OR 逻辑 2377 | if (settings.keywords && settings.keywords.length > 0) { 2378 | const matchesKeyword = settings.keywords.some(keyword => { 2379 | // 处理数字关键字 - 移除逗号后比较 2380 | const cleanKeyword = keyword.replace(/,/g, ''); 2381 | if (!isNaN(cleanKeyword)) { 2382 | const isMatch = priceText === cleanKeyword; 2383 | console.log('数字比较:', { 2384 | keyword: keyword, 2385 | cleanKeyword: cleanKeyword, 2386 | priceText: priceText, 2387 | isMatch: isMatch 2388 | }); 2389 | return isMatch; 2390 | } 2391 | 2392 | // 非数字关键字才进行 OR/AND 处理 2393 | const orParts = keyword.split('+').map(part => part.trim()); 2394 | return orParts.some(orPart => { 2395 | const andParts = orPart.split(',').map(part => part.trim()); 2396 | return andParts.every(andPart => { 2397 | // 如果是数字,移除逗号后比较 2398 | const cleanAndPart = andPart.replace(/,/g, ''); 2399 | if (!isNaN(cleanAndPart)) { 2400 | return priceText === cleanAndPart; 2401 | } 2402 | return textIncludesKeyword(fullText, andPart); 2403 | }); 2404 | }); 2405 | }); 2406 | 2407 | if (!matchesKeyword) shouldHide = true; 2408 | } 2409 | 2410 | // 檢查黑名單 - 支持 AND/OR 逻辑 2411 | if (settings.blacklist && settings.blacklist.length > 0) { 2412 | const isBlacklisted = settings.blacklist.some(blacklistItem => { 2413 | // 处理数字黑名单 - 移除逗号后比较 2414 | const cleanBlacklistItem = blacklistItem.replace(/,/g, ''); 2415 | if (!isNaN(cleanBlacklistItem)) { 2416 | return priceText === cleanBlacklistItem; 2417 | } 2418 | 2419 | const orParts = blacklistItem.split('+').map(part => part.trim()); 2420 | return orParts.some(orPart => { 2421 | const andParts = orPart.split(',').map(part => part.trim()); 2422 | return andParts.every(andPart => { 2423 | // 如果是数字,移除逗号后比较 2424 | const cleanAndPart = andPart.replace(/,/g, ''); 2425 | if (!isNaN(cleanAndPart)) { 2426 | return priceText === cleanAndPart; 2427 | } 2428 | return textIncludesKeyword(fullText, andPart); 2429 | }); 2430 | }); 2431 | }); 2432 | if (isBlacklisted) shouldHide = true; 2433 | } 2434 | 2435 | // 檢查是否要隱藏已售完 2436 | if (settings.hideSoldOut && isSoldOut) { 2437 | shouldHide = true; 2438 | } 2439 | 2440 | // 設置顯示狀態 2441 | const parentElement = form.closest('.border'); 2442 | if (parentElement) { 2443 | if (shouldHide) { 2444 | parentElement.style.display = 'none'; 2445 | } else { 2446 | parentElement.style.display = ''; 2447 | } 2448 | } 2449 | }); 2450 | } 2451 | } 2452 | 2453 | // 修改寬宏售票的筛选函数 2454 | async function filterKhamTickets() { 2455 | // 移除调试信息 2456 | // console.log('当前筛选设置:', settings); 2457 | 2458 | // 获取所有票券行 2459 | const ticketRows = document.querySelectorAll('tr.status_tr'); 2460 | if (!ticketRows.length) return; 2461 | 2462 | try { 2463 | // 遍历每个票券行 2464 | ticketRows.forEach(row => { 2465 | const areaText = row.querySelector('td[data-title="票區:"]')?.textContent?.trim() || ''; 2466 | const priceElement = row.querySelector('td[data-title="票價:"]'); 2467 | const statusText = row.querySelector('td[data-title="空位:"]')?.textContent?.trim() || ''; 2468 | 2469 | // 处理价格文本 - 移除逗号和空格 2470 | let priceText = ''; 2471 | if (priceElement) { 2472 | priceText = priceElement.textContent 2473 | .replace(/,/g, '') 2474 | .replace(/\s+/g, '') 2475 | .trim(); 2476 | } 2477 | 2478 | const fullText = `${areaText} ${priceText}`; 2479 | const isSoldOut = statusText === '已售完'; 2480 | 2481 | let shouldHide = false; 2482 | 2483 | // 检查关键字 - 支持 AND/OR 逻辑 2484 | if (settings.keywords && settings.keywords.length > 0) { 2485 | const matchesKeyword = settings.keywords.some(keyword => { 2486 | // 处理数字关键字 - 移除逗号后比较 2487 | const cleanKeyword = keyword.replace(/,/g, ''); 2488 | if (!isNaN(cleanKeyword)) { 2489 | return priceText === cleanKeyword; 2490 | } 2491 | 2492 | // 非数字关键字才进行 OR/AND 处理 2493 | const orParts = keyword.split('+').map(part => part.trim()); 2494 | return orParts.some(orPart => { 2495 | const andParts = orPart.split(',').map(part => part.trim()); 2496 | return andParts.every(andPart => { 2497 | // 如果是数字,移除逗号后比较 2498 | const cleanAndPart = andPart.replace(/,/g, ''); 2499 | if (!isNaN(cleanAndPart)) { 2500 | return priceText === cleanAndPart; 2501 | } 2502 | return textIncludesKeyword(fullText, andPart); 2503 | }); 2504 | }); 2505 | }); 2506 | 2507 | if (!matchesKeyword) shouldHide = true; 2508 | } 2509 | 2510 | // 检查黑名单 - 支持 AND/OR 逻辑 2511 | if (settings.blacklist && settings.blacklist.length > 0) { 2512 | const isBlacklisted = settings.blacklist.some(blacklistItem => { 2513 | // 处理数字黑名单 - 移除逗号后比较 2514 | const cleanBlacklistItem = blacklistItem.replace(/,/g, ''); 2515 | if (!isNaN(cleanBlacklistItem)) { 2516 | return priceText === cleanBlacklistItem; 2517 | } 2518 | 2519 | const orParts = blacklistItem.split('+').map(part => part.trim()); 2520 | return orParts.some(orPart => { 2521 | const andParts = orPart.split(',').map(part => part.trim()); 2522 | return andParts.every(andPart => { 2523 | // 如果是数字,移除逗号后比较 2524 | const cleanAndPart = andPart.replace(/,/g, ''); 2525 | if (!isNaN(cleanAndPart)) { 2526 | return priceText === cleanAndPart; 2527 | } 2528 | return textIncludesKeyword(fullText, andPart); 2529 | }); 2530 | }); 2531 | }); 2532 | if (isBlacklisted) shouldHide = true; 2533 | } 2534 | 2535 | // 检查是否隐藏已售完 2536 | if (settings.hideSoldOut && isSoldOut) { 2537 | shouldHide = true; 2538 | } 2539 | 2540 | // 设置显示状态 2541 | row.style.display = shouldHide ? 'none' : ''; 2542 | }); 2543 | } finally { 2544 | // 确保处理完成后重置 isProcessing 状态 2545 | settings.isProcessing = false; 2546 | } 2547 | } 2548 | 2549 | // 在 filterKhamTickets 函数后添加 2550 | // 监听寬宏售票网站的票券列表变化 2551 | function initKhamObserver() { 2552 | const site = getCurrentSite(); 2553 | if (site !== 'kham') return; 2554 | 2555 | // 创建一个观察器实例 2556 | const observer = new MutationObserver((mutations) => { 2557 | // 检查变化是否涉及票券列表 2558 | const hasTicketChanges = mutations.some(mutation => { 2559 | return mutation.target.classList?.contains('status_tr') || 2560 | mutation.target.querySelector?.('tr.status_tr'); 2561 | }); 2562 | 2563 | if (hasTicketChanges) { 2564 | filterKhamTickets(); 2565 | } 2566 | }); 2567 | 2568 | // 配置观察选项 2569 | const config = { 2570 | childList: true, // 观察子节点的添加或删除 2571 | subtree: true, // 观察所有后代节点 2572 | attributes: true, // 观察属性变化 2573 | attributeFilter: ['style', 'class'] // 只观察 style 和 class 属性的变化 2574 | }; 2575 | 2576 | // 开始观察票券列表容器 2577 | const ticketContainer = document.querySelector('tbody'); 2578 | if (ticketContainer) { 2579 | observer.observe(ticketContainer, config); 2580 | } 2581 | } 2582 | 2583 | // 在页面加载完成后初始化观察器 2584 | if (document.readyState === 'loading') { 2585 | document.addEventListener('DOMContentLoaded', initKhamObserver); 2586 | } else { 2587 | initKhamObserver(); 2588 | } --------------------------------------------------------------------------------