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+
1063function escapeHTML ( str ) {
1164 if ( str === null || str === undefined ) return '' ;
1265 const text = String ( str ) ;
@@ -21,6 +74,7 @@ function escapeHTML(str) {
2174}
2275
2376document . 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
0 commit comments