Skip to content

Commit 075e88b

Browse files
oooooo
1 parent beef772 commit 075e88b

2 files changed

Lines changed: 70 additions & 98 deletions

File tree

public/app.js

Lines changed: 70 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,63 @@
33
// ========================================
44

55
// 初始化
6-
let radarChartInstance = null;
7-
let barChartInstance = null;
6+
// 全域 Turnstile 管理器
7+
const GlobalTurnstileManager = {
8+
widgetId: null,
9+
10+
async init() {
11+
try {
12+
const res = await fetch('/api/turnstile-site-key');
13+
const data = await res.json();
14+
const siteKey = data.siteKey || '';
15+
if (!siteKey) return;
16+
17+
const waitForTurnstile = () => new Promise((resolve) => {
18+
if (typeof turnstile !== 'undefined') return resolve();
19+
const interval = setInterval(() => {
20+
if (typeof turnstile !== 'undefined') {
21+
clearInterval(interval);
22+
resolve();
23+
}
24+
}, 100);
25+
});
26+
27+
await waitForTurnstile();
28+
29+
let container = document.getElementById('globalTurnstileContainer');
30+
if (!container) {
31+
container = document.createElement('div');
32+
container.id = 'globalTurnstileContainer';
33+
container.style.display = 'none';
34+
document.body.appendChild(container);
35+
}
36+
37+
this.widgetId = turnstile.render(container, {
38+
sitekey: siteKey,
39+
theme: 'dark',
40+
size: 'invisible'
41+
});
42+
} catch (e) {
43+
console.warn('Failed to init global Turnstile:', e);
44+
}
45+
},
46+
47+
getToken() {
48+
if (typeof turnstile !== 'undefined' && this.widgetId !== null) {
49+
return turnstile.getResponse(this.widgetId) || '';
50+
}
51+
return '';
52+
},
53+
54+
reset() {
55+
if (typeof turnstile !== 'undefined' && this.widgetId !== null) {
56+
turnstile.reset(this.widgetId);
57+
}
58+
}
59+
};
860

961
// HTML 跳脫輔助函數,防範 XSS
62+
1063
function escapeHTML(str) {
1164
if (str === null || str === undefined) return '';
1265
const text = String(str);
@@ -21,6 +74,7 @@ function escapeHTML(str) {
2174
}
2275

2376
document.addEventListener('DOMContentLoaded', () => {
77+
GlobalTurnstileManager.init();
2478
checkDisclaimer();
2579
loadGradesData();
2680
setupPasteImport();
@@ -751,56 +805,6 @@ function setupSyncFeature() {
751805

752806
const API_BASE = '/api';
753807
let availableStructure = {}; // Store the loaded structure
754-
let turnstileSiteKey = '';
755-
let turnstileWidgetId = null;
756-
757-
// 獲取 Turnstile site key
758-
async function fetchTurnstileSiteKey() {
759-
if (turnstileSiteKey) return turnstileSiteKey;
760-
try {
761-
const res = await fetch(`${API_BASE}/turnstile-site-key`);
762-
const data = await res.json();
763-
turnstileSiteKey = data.siteKey || '';
764-
} catch (e) {
765-
console.warn('Failed to fetch Turnstile site key:', e);
766-
}
767-
return turnstileSiteKey;
768-
}
769-
770-
// 渲染 Turnstile widget
771-
async function renderTurnstile() {
772-
const container = document.getElementById('turnstileContainer');
773-
if (!container) return;
774-
775-
// 先重置舊的 widget
776-
if (turnstileWidgetId !== null && typeof turnstile !== 'undefined') {
777-
try { turnstile.remove(turnstileWidgetId); } catch (e) { /* ignore */ }
778-
turnstileWidgetId = null;
779-
}
780-
container.innerHTML = '';
781-
782-
const siteKey = await fetchTurnstileSiteKey();
783-
if (!siteKey) return;
784-
785-
// 等待 Turnstile API 載入
786-
const waitForTurnstile = () => new Promise((resolve) => {
787-
if (typeof turnstile !== 'undefined') return resolve();
788-
const interval = setInterval(() => {
789-
if (typeof turnstile !== 'undefined') {
790-
clearInterval(interval);
791-
resolve();
792-
}
793-
}, 100);
794-
});
795-
796-
await waitForTurnstile();
797-
798-
turnstileWidgetId = turnstile.render(container, {
799-
sitekey: siteKey,
800-
theme: 'dark',
801-
size: 'invisible',
802-
});
803-
}
804808

805809
// Helper to toggle modal
806810
const toggleModal = (modal, show) => {
@@ -853,13 +857,10 @@ function setupSyncFeature() {
853857
}
854858

855859
// 取得 Turnstile token
856-
let turnstileResponse = '';
857-
if (typeof turnstile !== 'undefined' && turnstileWidgetId !== null) {
858-
turnstileResponse = turnstile.getResponse(turnstileWidgetId) || '';
859-
if (!turnstileResponse) {
860-
showStatus(loginStatus, '請稍候,系統安全驗證處理中...', 'error');
861-
return;
862-
}
860+
const turnstileResponse = GlobalTurnstileManager.getToken();
861+
if (!turnstileResponse) {
862+
showStatus(loginStatus, '請稍候,系統安全驗證處理中...', 'error');
863+
return;
863864
}
864865

865866
showStatus(loginStatus, '登入中...', 'normal');
@@ -884,11 +885,11 @@ function setupSyncFeature() {
884885
}, 500);
885886
} else {
886887
showStatus(loginStatus, data.message || '登入失敗', 'error');
887-
if (typeof turnstile !== 'undefined' && turnstileWidgetId !== null) turnstile.reset(turnstileWidgetId);
888+
GlobalTurnstileManager.reset();
888889
}
889890
} catch (error) {
890891
showStatus(loginStatus, '連線錯誤: ' + error.message, 'error');
891-
if (typeof turnstile !== 'undefined' && turnstileWidgetId !== null) turnstile.reset(turnstileWidgetId);
892+
GlobalTurnstileManager.reset();
892893
} finally {
893894
confirmLogin.disabled = false;
894895
}
@@ -931,7 +932,7 @@ function setupSyncFeature() {
931932
toggleModal(selectExamModal, false);
932933
toggleModal(loginModal, true);
933934
usernameInput.focus();
934-
renderTurnstile();
935+
GlobalTurnstileManager.reset();
935936
return;
936937
}
937938

@@ -1102,8 +1103,7 @@ function setupShareFeature() {
11021103
</svg>
11031104
建立分享連結
11041105
</button>
1105-
<div id="shareTurnstileContainer" style="display: flex; justify-content: center; margin-top: 12px;"></div>
1106-
<div id="shareStatus" class="status-msg"></div>
1106+
<div id="shareStatus" class="status-msg" style="margin-top: 12px;"></div>
11071107
</div>
11081108
`;
11091109

@@ -1112,28 +1112,6 @@ function setupShareFeature() {
11121112
const shareLinkInput = document.getElementById('shareLinkInput');
11131113
const copyLinkBtn = document.getElementById('copyLinkBtn');
11141114
const shareStatus = document.getElementById('shareStatus');
1115-
const shareTurnstileContainer = document.getElementById('shareTurnstileContainer');
1116-
1117-
// 彈窗開啟時立即渲染 Turnstile Widget
1118-
if (typeof turnstile !== 'undefined' && shareTurnstileContainer) {
1119-
(async () => {
1120-
try {
1121-
const siteKeyRes = await fetch('/api/turnstile-site-key');
1122-
const siteKeyData = await siteKeyRes.json();
1123-
const siteKey = siteKeyData.siteKey;
1124-
if (siteKey) {
1125-
const widgetId = turnstile.render(shareTurnstileContainer, {
1126-
sitekey: siteKey,
1127-
theme: 'dark',
1128-
size: 'invisible',
1129-
});
1130-
shareTurnstileContainer.dataset.widgetId = widgetId;
1131-
}
1132-
} catch (e) {
1133-
console.warn('Failed to render share Turnstile:', e);
1134-
}
1135-
})();
1136-
}
11371115

11381116
createLinkBtn.addEventListener('click', async () => {
11391117
const gradesData = getStoredGrades();
@@ -1144,14 +1122,11 @@ function setupShareFeature() {
11441122
}
11451123

11461124
// 取得 Turnstile token
1147-
let turnstileResponse = '';
1148-
if (typeof turnstile !== 'undefined' && shareTurnstileContainer && shareTurnstileContainer.dataset.widgetId) {
1149-
turnstileResponse = turnstile.getResponse(shareTurnstileContainer.dataset.widgetId) || '';
1150-
if (!turnstileResponse) {
1151-
shareStatus.textContent = '請稍候,系統安全驗證處理中...';
1152-
shareStatus.className = 'status-msg error';
1153-
return;
1154-
}
1125+
const turnstileResponse = GlobalTurnstileManager.getToken();
1126+
if (!turnstileResponse) {
1127+
shareStatus.textContent = '請稍候,系統安全驗證處理中...';
1128+
shareStatus.className = 'status-msg error';
1129+
return;
11551130
}
11561131

11571132
createLinkBtn.disabled = true;
@@ -1172,8 +1147,6 @@ function setupShareFeature() {
11721147
shareLinkInput.value = link;
11731148
linkContainer.style.display = 'block';
11741149
createLinkBtn.style.display = 'none';
1175-
// 隱藏 Turnstile
1176-
if (shareTurnstileContainer) shareTurnstileContainer.style.display = 'none';
11771150
shareStatus.textContent = '連結建立成功!';
11781151
shareStatus.className = 'status-msg success';
11791152
} else {
@@ -1185,6 +1158,7 @@ function setupShareFeature() {
11851158
shareStatus.className = 'status-msg error';
11861159
createLinkBtn.disabled = false;
11871160
createLinkBtn.textContent = '建立分享連結';
1161+
GlobalTurnstileManager.reset();
11881162
}
11891163
});
11901164

public/index.html

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,6 @@ <h3 style="display: flex; align-items: center; gap: 8px;">
191191
</div>
192192
</div>
193193
<div id="loginStatus" class="status-msg"></div>
194-
<div id="turnstileContainer" style="display: flex; justify-content: center; margin-top: 12px;">
195-
</div>
196194
<p class="privacy-notice"
197195
style="text-align: center; margin-top: 16px; margin-bottom: 0; font-size: 13px; color: var(--color-text-dim);">
198196
若您繼續,即代表您已閱讀並同意本網站的<a href="/privacy.html" target="_blank"

0 commit comments

Comments
 (0)