💡 提示:
588 |-
589 |
- 點擊上方按鈕可直接開啟設定頁面 590 |
- 在設定頁面中找到「搶票柴柴」 591 |
- 點擊輸入框並按下想要的按鍵組合 592 |
├── 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 |
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 |
搶票柴柴-售票網站篩選器
544 |
547 | 💡 提示:
588 |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 | } --------------------------------------------------------------------------------