Lashtw commited on
Commit
c4dff99
·
verified ·
1 Parent(s): c64d2e4

Upload 8 files

Browse files
Files changed (9) hide show
  1. .gitattributes +2 -0
  2. firebase-config.js +9 -0
  3. index.html +517 -18
  4. m1.GIF +3 -0
  5. m2.GIF +0 -0
  6. m3.GIF +0 -0
  7. m4.GIF +3 -0
  8. m5.GIF +0 -0
  9. student.html +226 -0
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ m1.GIF filter=lfs diff=lfs merge=lfs -text
37
+ m4.GIF filter=lfs diff=lfs merge=lfs -text
firebase-config.js ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ export const firebaseConfig = {
2
+ // ⚠️ 老師請注意:請將 Firebase 控制台取得的 Config(金鑰物件)貼在底下覆蓋這段 ⚠️
3
+ apiKey: "AIzaSyA2fxxaGAdWSR_QOH2Hm92kttGZmLDH8-w",
4
+ authDomain: "pi-search-89a08.firebaseapp.com",
5
+ projectId: "pi-search-89a08",
6
+ storageBucket: "pi-search-89a08.firebasestorage.app",
7
+ messagingSenderId: "1003005654079",
8
+ appId: "1:1003005654079:web:636235436f432376d8748a"
9
+ };
index.html CHANGED
@@ -1,19 +1,518 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-TW">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>圓周率探索 - 老師儀表板</title>
7
+ <!-- 引入外部套件 -->
8
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
9
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.6.0/dist/confetti.browser.min.js"></script>
11
+ <style>
12
+ :root {
13
+ --primary: #f59e0b;
14
+ --primary-hover: #d97706;
15
+ --bg: #111827;
16
+ --panel: rgba(31, 41, 55, 0.4);
17
+ --border: rgba(255, 255, 255, 0.1);
18
+ --text: #f9fafb;
19
+ }
20
+ body {
21
+ margin: 0;
22
+ padding: 0;
23
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
24
+ background: radial-gradient(circle at top right, #374151, #111827);
25
+ color: var(--text);
26
+ min-height: 100vh;
27
+ }
28
+ .container { padding: 2rem; max-width: 1400px; margin: 0 auto; }
29
+
30
+ /* 玻璃面板 */
31
+ .glass-panel {
32
+ background: var(--panel);
33
+ backdrop-filter: blur(16px);
34
+ border: 1px solid var(--border);
35
+ border-radius: 1rem;
36
+ padding: 2rem;
37
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
38
+ margin-bottom: 2rem;
39
+ }
40
+
41
+ /* 步驟 1:建立房間 */
42
+ #setupScreen {
43
+ max-width: 500px;
44
+ margin: 15vh auto;
45
+ text-align: center;
46
+ }
47
+ input.room-input {
48
+ width: 100%;
49
+ padding: 1rem;
50
+ font-size: 1.5rem;
51
+ text-align: center;
52
+ background: rgba(0,0,0,0.3);
53
+ border: 2px solid rgba(255,255,255,0.2);
54
+ border-radius: 0.75rem;
55
+ color: white;
56
+ margin-bottom: 1.5rem;
57
+ text-transform: uppercase;
58
+ letter-spacing: 2px;
59
+ box-sizing: border-box;
60
+ }
61
+ button.btn {
62
+ background: linear-gradient(135deg, var(--primary), var(--primary-hover));
63
+ color: white;
64
+ border: none;
65
+ padding: 1rem 2rem;
66
+ font-size: 1.25rem;
67
+ border-radius: 0.75rem;
68
+ cursor: pointer;
69
+ font-weight: bold;
70
+ transition: all 0.2s;
71
+ width: 100%;
72
+ box-shadow: 0 4px 6px rgba(0,0,0,0.3);
73
+ }
74
+ button.btn:hover { transform: translateY(-2px); box-shadow: 0 10px 15px -3px rgba(245, 158, 11, 0.4); }
75
+ button.btn:disabled { background: #6b7280; transform: none; cursor: not-allowed; }
76
+
77
+ /* 步驟 2:儀表板 */
78
+ #dashboardScreen { display: none; }
79
+ .header {
80
+ display: flex;
81
+ justify-content: space-between;
82
+ align-items: flex-start;
83
+ flex-wrap: wrap;
84
+ gap: 2rem;
85
+ }
86
+ .header-left { display: flex; align-items: center; gap: 2rem; }
87
+ #qrcodeArea {
88
+ background: white;
89
+ padding: 1rem;
90
+ border-radius: 1rem;
91
+ box-shadow: 0 10px 25px rgba(0,0,0,0.5);
92
+ transition: transform 0.3s;
93
+ }
94
+ #qrcodeArea:hover { transform: scale(1.05); }
95
+ .room-info h1 { margin: 0 0 0.5rem 0; font-size: 2.5rem; color: #fbbf24; }
96
+ .room-info p { margin: 0 0 0.5rem 0; color: #9ca3af; font-size: 1.2rem; }
97
+ .controls select {
98
+ padding: 0.75rem 1rem;
99
+ background: rgba(0,0,0,0.5);
100
+ border: 1px solid var(--border);
101
+ color: white;
102
+ border-radius: 0.5rem;
103
+ font-size: 1.1rem;
104
+ cursor: pointer;
105
+ }
106
+
107
+ /* 學生標籤網格 (瀑布流卡片) */
108
+ .students-grid {
109
+ display: flex;
110
+ flex-wrap: wrap;
111
+ gap: 1.25rem;
112
+ min-height: 200px;
113
+ }
114
+ .student-chip {
115
+ background: rgba(255, 255, 255, 0.05);
116
+ border: 1px solid var(--border);
117
+ padding: 1.25rem;
118
+ border-radius: 1rem;
119
+ display: flex;
120
+ flex-direction: column;
121
+ align-items: center;
122
+ gap: 0.5rem;
123
+ animation: fadeIn 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards;
124
+ transition: transform 0.3s, box-shadow 0.3s, background-color 0.3s;
125
+ min-width: 140px;
126
+ box-shadow: 0 4px 6px rgba(0,0,0,0.2);
127
+ }
128
+ .student-chip:hover { transform: translateY(-5px); box-shadow: 0 15px 20px rgba(0,0,0,0.4); }
129
+ .student-chip .name { font-weight: bold; font-size: 1.3rem; }
130
+ .student-chip .number { color: #fbbf24; font-family: monospace; font-size: 1.5rem; font-weight: bold; letter-spacing: 1px;}
131
+ .student-chip .result {
132
+ display: none; width: 100%; text-align: center;
133
+ margin-top: 0.5rem; padding-top: 0.5rem;
134
+ border-top: 1px dashed rgba(255,255,255,0.2);
135
+ font-size: 1rem; font-weight: bold; color: #34d399;
136
+ }
137
+
138
+ /* 狀態顏色 */
139
+ .student-chip.searching { background: rgba(59, 130, 246, 0.2); border-color: #3b82f6; }
140
+ .student-chip.done { background: rgba(16, 185, 129, 0.2); border-color: #10b981; }
141
+ .student-chip.error { background: rgba(239, 68, 68, 0.2); border-color: #ef4444; }
142
+
143
+ @keyframes fadeIn { from { opacity: 0; transform: scale(0.8) translateY(20px); } to { opacity: 1; transform: scale(1) translateY(0); } }
144
+
145
+ /* Loading 動畫 */
146
+ .spinner {
147
+ width: 24px; height: 24px; margin: 0 auto;
148
+ border: 3px solid rgba(255,255,255,0.2); border-top: 3px solid white;
149
+ border-radius: 50%; animation: spin 1s linear infinite;
150
+ }
151
+ @keyframes spin { 100% { transform: rotate(360deg); } }
152
+
153
+ /* 步驟 3:長條圖與頒獎台 */
154
+ #resultsArea { display: none; }
155
+ .podium-container {
156
+ display: flex; justify-content: center; align-items: flex-end;
157
+ margin: 3rem 0; height: 300px; gap: 1rem;
158
+ }
159
+ .podium {
160
+ display: flex; flex-direction: column; align-items: center; justify-content: flex-end;
161
+ width: 140px; opacity: 0; transform: translateY(50px);
162
+ }
163
+ .podium-info { text-align: center; margin-bottom: 0.5rem; }
164
+ .podium-name { font-weight: bold; font-size: 1.4rem; color: #fff; text-shadow: 0 2px 4px rgba(0,0,0,0.5); }
165
+ .podium-val { color: #fbbf24; font-size: 1rem; font-weight: bold; }
166
+
167
+ .podium-step {
168
+ width: 100%; display: flex; align-items: center; justify-content: center;
169
+ font-weight: bold; color: #111; font-size: 2rem;
170
+ border-top-left-radius: 1rem; border-top-right-radius: 1rem;
171
+ box-shadow: inset 0 -10px 20px rgba(0,0,0,0.2), 0 10px 15px -3px rgba(0,0,0,0.5);
172
+ }
173
+ /* 頒獎台動畫與高度設定 */
174
+ .podium.show { animation: slideUp 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards; }
175
+ .podium.first { animation-delay: 0.6s; }
176
+ .podium.second { animation-delay: 0.3s; }
177
+ .podium.third { animation-delay: 0s; }
178
+ .podium.first .podium-step { height: 220px; background: linear-gradient(135deg, #fef08a, #ca8a04); border: 2px solid #fef08a; }
179
+ .podium.second .podium-step { height: 160px; background: linear-gradient(135deg, #f3f4f6, #9ca3af); border: 2px solid #fff; }
180
+ .podium.third .podium-step { height: 110px; background: linear-gradient(135deg, #fed7aa, #c2410c); border: 2px solid #fed7aa; }
181
+ @keyframes slideUp { to { opacity: 1; transform: translateY(0); } }
182
+
183
+ </style>
184
+ </head>
185
+ <body>
186
+ <div class="container">
187
+ <!-- 畫面 1:設定房間 -->
188
+ <div id="setupScreen" class="glass-panel">
189
+ <h1 style="color: #fbbf24; font-size: 2.5rem; margin-top: 0;">數學城市<br><span style="color: white">圓周率探索儀表板</span></h1>
190
+ <p style="color: #9ca3af; margin-bottom: 2rem;">請為您的教室建立一個獨一無二的代碼</p>
191
+ <input type="text" id="roomInput" class="room-input" placeholder="例如: 805, MATH-1" autocomplete="off">
192
+ <button class="btn" id="startRoomBtn">進入教室</button>
193
+
194
+ <div id="systemWarning" style="color: #ef4444; background: rgba(239, 68, 68, 0.1); padding: 1rem; border-radius: 0.5rem; margin-top: 1.5rem; display: none; text-align: left;">
195
+ ⚠️ <strong>警告:無法讀取網路 IP</strong><br>您目前似乎是直接點擊檔案 (file://) 開啟。<br>這會導致 QRCode 網址出錯,學生手機將無法連線掃描。<br>👉 <strong>解決方法</strong>:請使用 VScode 的 Live Server 或 Python 本機伺服器開啟。
196
+ </div>
197
+ </div>
198
+
199
+ <!-- 畫面 2:主儀表板 -->
200
+ <div id="dashboardScreen">
201
+ <div class="header glass-panel">
202
+ <div class="header-left">
203
+ <div id="qrcodeArea"></div>
204
+ <div class="room-info">
205
+ <h1>教室代碼:<span id="displayRoomCode" style="padding: 0.2rem 1rem; background: rgba(0,0,0,0.3); border-radius: 0.5rem; border: 1px solid rgba(255,255,255,0.2);"></span></h1>
206
+ <p>請讓學生用平板或手機掃描左側條碼</p>
207
+ <p style="font-size: 0.9rem; color: #6b7280;">連線網址:<span id="studentUrlDisplay" style="font-family: monospace;"></span></p>
208
+ </div>
209
+ </div>
210
+ <div class="controls">
211
+ <label style="display: block; margin-bottom: 0.5rem; color: #9ca3af; font-weight: bold;">API 查詢設定 (補零長度)</label>
212
+ <select id="digitSelect">
213
+ <option value="4">統一補為 4 位數</option>
214
+ <option value="5">統一補為 5 位數</option>
215
+ <option value="6">統一補為 6 位數</option>
216
+ <option value="7" selected>統一補為 7 位數</option>
217
+ <option value="8">統一補為 8 位數</option>
218
+ <option value="9">統一補為 9 位數</option>
219
+ <option value="10">統一補為 10 位數</option>
220
+ </select>
221
+ </div>
222
+ </div>
223
+
224
+ <div class="glass-panel" style="margin-bottom: 2rem;">
225
+ <div style="display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 1.5rem; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 1rem;">
226
+ <div>
227
+ <h2 style="margin: 0; font-size: 1.8rem;">👨‍🎓 學生等待區</h2>
228
+ <p style="margin: 0.5rem 0 0 0; color: #9ca3af;">目前已收到 <strong id="studentCount" style="color: white; font-size: 1.2rem;">0</strong> 位學生的幸運數字</p>
229
+ </div>
230
+ <button class="btn" id="searchApiBtn" style="width: auto; padding: 1rem 3rem; font-size: 1.5rem;">
231
+ 🚀 開始環遊圓周率
232
+ </button>
233
+ </div>
234
+ <!-- 卡片列表動態產生於此 -->
235
+ <div class="students-grid" id="studentsGrid"></div>
236
+ </div>
237
+
238
+ <!-- 畫面 3:搜尋結果與圖表 -->
239
+ <div id="resultsArea" class="glass-panel">
240
+ <h1 style="text-align: center; color: #fde047; font-size: 2.5rem; margin-top: 0; text-shadow: 0 4px 10px rgba(0,0,0,0.5);">🏆 尋獲深處的贏家 🏆</h1>
241
+
242
+ <div class="podium-container" id="podiumArea">
243
+ <!-- 頒獎台動態產生於此 -->
244
+ </div>
245
+
246
+ <div style="background: rgba(0,0,0,0.3); border-radius: 1rem; padding: 2rem; margin-top: 3rem;">
247
+ <h2 style="margin-top: 0; border-left: 4px solid #34d399; padding-left: 1rem;">📊 詳細找尋結果 (位數越深排名越高)</h2>
248
+ <canvas id="resultChart"></canvas>
249
+ </div>
250
+ </div>
251
+ </div>
252
+ </div>
253
+
254
+ <!-- Firebase 邏輯 -->
255
+ <script type="module">
256
+ import { initializeApp } from "https://www.gstatic.com/firebasejs/10.8.1/firebase-app.js";
257
+ import { getFirestore, collection, onSnapshot, query, orderBy } from "https://www.gstatic.com/firebasejs/10.8.1/firebase-firestore.js";
258
+ import { firebaseConfig } from './firebase-config.js';
259
+
260
+ let app, db;
261
+ let isTestMode = false;
262
+
263
+ try {
264
+ if(firebaseConfig.apiKey === "YOUR_API_KEY") {
265
+ isTestMode = true;
266
+ console.warn("⚠️ Firebase 金鑰未設定,將以測試模式執行 (產生虛擬學生)。");
267
+ } else {
268
+ app = initializeApp(firebaseConfig);
269
+ db = getFirestore(app);
270
+ }
271
+ } catch(e) { console.error("Firebase 初始化失敗:", e); }
272
+
273
+ let currentRoom = "";
274
+ let studentsData = []; // [{ id, name, number, paddedNumber, resultVal, status }]
275
+
276
+ // UI 元件
277
+ const startRoomBtn = document.getElementById('startRoomBtn');
278
+ const searchApiBtn = document.getElementById('searchApiBtn');
279
+ const setupScreen = document.getElementById('setupScreen');
280
+ const dashboardScreen = document.getElementById('dashboardScreen');
281
+ const studentsGrid = document.getElementById('studentsGrid');
282
+ const resultsArea = document.getElementById('resultsArea');
283
+
284
+ // 警告 file:// 使用者
285
+ if (window.location.protocol === 'file:') {
286
+ document.getElementById('systemWarning').style.display = 'block';
287
+ }
288
+
289
+ // --- 1. 建立房間 ---
290
+ startRoomBtn.addEventListener('click', () => {
291
+ currentRoom = document.getElementById('roomInput').value.trim().toUpperCase();
292
+ if(!currentRoom) { alert("請輸入教室代碼!"); return; }
293
+
294
+ // 介面轉換
295
+ setupScreen.style.display = 'none';
296
+ dashboardScreen.style.display = 'block';
297
+ document.getElementById('displayRoomCode').innerText = currentRoom;
298
+
299
+ // 產生 QRCode (指向同一網路位置的 student.html)
300
+ const currentUrl = new URL(window.location.href);
301
+ // ��保網址是把 index.html 替換成 student.html,並加上 room 參數
302
+ let studentPath = currentUrl.pathname.replace('index.html', 'student.html');
303
+ if (studentPath === currentUrl.pathname) { // 如果網址隱藏了 index.html (如 Hugging Face)
304
+ if (!studentPath.endsWith('/')) {
305
+ studentPath += '/';
306
+ }
307
+ studentPath += 'student.html';
308
+ }
309
+ const studentUrl = `${currentUrl.origin}${studentPath}?room=${encodeURIComponent(currentRoom)}`;
310
+ document.getElementById('studentUrlDisplay').innerText = studentUrl;
311
+
312
+ new QRCode(document.getElementById("qrcodeArea"), {
313
+ text: studentUrl, width: 150, height: 150, colorDark : "#000000", colorLight : "#ffffff", correctLevel : QRCode.CorrectLevel.L
314
+ });
315
+
316
+ // 啟動資料庫監聽
317
+ if(isTestMode) {
318
+ alert("目前為【測試模式】(未設定Firebase)\n系統即將自動產生三個虛擬同學加入。");
319
+ setTimeout(() => mockJoin("王小明", "1234"), 1500);
320
+ setTimeout(() => mockJoin("陳大華", "7777777"), 3000);
321
+ setTimeout(() => mockJoin("林老師", "1314"), 4500);
322
+ } else {
323
+ startListening(currentRoom);
324
+ }
325
+ });
326
+
327
+ // --- Firebase 監聽器 ---
328
+ function startListening(room) {
329
+ const q = query(collection(db, "rooms", room, "students"), orderBy("timestamp", "asc"));
330
+ onSnapshot(q, (snapshot) => {
331
+ snapshot.docChanges().forEach((change) => {
332
+ if (change.type === "added") {
333
+ const data = change.doc.data();
334
+ addStudentChip(change.doc.id, data.name, data.number);
335
+ }
336
+ });
337
+ }, (error) => {
338
+ console.error("監聽 Firebase 失敗:", error);
339
+ alert("無法連線到資料庫,請檢查網路或金鑰權限設定!");
340
+ });
341
+ }
342
+
343
+ // 虛擬測試用
344
+ let mockId = 0;
345
+ function mockJoin(name, num) { addStudentChip(`mock_${mockId++}`, name, num); }
346
+
347
+ // --- 2. 顯示學生卡片 ---
348
+ function addStudentChip(id, name, number) {
349
+ if(studentsData.find(s => s.id === id)) return; // 防呆:重複加入
350
+
351
+ studentsData.push({ id, name, number, resultVal: null });
352
+ document.getElementById('studentCount').innerText = studentsData.length;
353
+
354
+ const div = document.createElement('div');
355
+ div.className = 'student-chip';
356
+ div.id = `chip_${id}`;
357
+ div.innerHTML = `
358
+ <div class="name">${name}</div>
359
+ <div class="number">${number}</div>
360
+ <div class="result" id="res_${id}"></div>
361
+ `;
362
+ studentsGrid.appendChild(div);
363
+ // 當新同學加入時捲動到最底
364
+ window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
365
+ }
366
+
367
+ // --- 3. 開始環遊圓周率 (API 批量處理) ---
368
+ searchApiBtn.addEventListener('click', async () => {
369
+ if(studentsData.length === 0) { alert("尚未有學生加入!"); return; }
370
+ searchApiBtn.disabled = true;
371
+ searchApiBtn.innerText = "⏳ 瘋狂搜尋圓周率中...";
372
+
373
+ const selectedLength = parseInt(document.getElementById('digitSelect').value);
374
+
375
+ // Promise.allSettled 並以批次執行,防止一口氣發送 400 條請求被瀏覽器擋下或被 Pi API 阻斷
376
+ const batchSize = 10;
377
+ for (let i = 0; i < studentsData.length; i += batchSize) {
378
+ const batch = studentsData.slice(i, i + batchSize);
379
+ await Promise.allSettled(batch.map(student => fetchPiForStudent(student, selectedLength)));
380
+ // 可加入微小延遲降低 server 負載
381
+ await new Promise(r => setTimeout(r, 200));
382
+ }
383
+
384
+ searchApiBtn.innerText = "✅ 搜尋完畢";
385
+ showResults();
386
+ });
387
+
388
+ async function fetchPiForStudent(student, selectedLength) {
389
+ const chip = document.getElementById(`chip_${student.id}`);
390
+ const resDiv = document.getElementById(`res_${student.id}`);
391
+
392
+ // 變更卡片 UI 為「搜尋中」
393
+ chip.classList.add('searching');
394
+ resDiv.style.display = 'block';
395
+ resDiv.innerHTML = '<div class="spinner"></div>'; // 顯示旋轉圈圈
396
+
397
+ let luckyNum = String(student.number);
398
+ let finalOutput = null; // 最終顯示文字
399
+ let finalVal = null; // 最終數字結果
400
+
401
+ if (luckyNum.length > selectedLength) {
402
+ finalOutput = "字數過長不符規定";
403
+ } else {
404
+ luckyNum = luckyNum.padStart(selectedLength, "0");
405
+ student.paddedNumber = luckyNum;
406
+ // 更新 UI 將數字補零呈現
407
+ chip.querySelector('.number').innerText = luckyNum;
408
+
409
+ try {
410
+ const response = await fetch(`https://www.angio.net/newpi/piquery?q=${luckyNum}`);
411
+ if (!response.ok) throw new Error("API Server Error");
412
+ const data = await response.json();
413
+
414
+ if (data.status === 'OK' && data.r && data.r.length > 0) {
415
+ finalVal = data.r[0].p;
416
+ finalOutput = `在第 ${finalVal} 位`;
417
+ } else {
418
+ finalOutput = "在圓周率中未找到";
419
+ }
420
+ } catch (e) {
421
+ console.error("API Error", e);
422
+ finalOutput = "查詢失敗";
423
+ }
424
+ }
425
+
426
+ // 更新結果
427
+ student.resultVal = finalVal;
428
+ chip.classList.remove('searching');
429
+ resDiv.innerHTML = "";
430
+
431
+ if(typeof finalVal === 'number'){
432
+ chip.classList.add('done');
433
+ resDiv.innerText = finalOutput;
434
+ } else {
435
+ chip.classList.add('error');
436
+ resDiv.innerText = finalOutput;
437
+ }
438
+ }
439
+
440
+ // --- 4. 繪圖與頒獎 ---
441
+ let chartInstance = null;
442
+ function showResults() {
443
+ resultsArea.style.display = 'block';
444
+ resultsArea.scrollIntoView({ behavior: 'smooth' });
445
+
446
+ // 篩出有找到數字的人
447
+ const validData = studentsData.filter(s => typeof s.resultVal === 'number');
448
+ // 按數字大小從大排到小 (越深的人排名越高)
449
+ validData.sort((a, b) => b.resultVal - a.resultVal);
450
+
451
+ drawPodium(validData);
452
+
453
+ if(validData.length > 0) {
454
+ // 發射紙花特效 (Premium UI 微互動) 🎉
455
+ setTimeout(() => {
456
+ confetti({ particleCount: 200, spread: 100, origin: { y: 0.6 }, colors: ['#fde047', '#34d399', '#60a5fa'] });
457
+ }, 500);
458
+
459
+ // 長條圖繪製
460
+ const ctx = document.getElementById('resultChart').getContext('2d');
461
+ if (chartInstance) chartInstance.destroy();
462
+ chartInstance = new Chart(ctx, {
463
+ type: 'bar',
464
+ data: {
465
+ labels: validData.map(s => `${s.name} (${s.paddedNumber})`),
466
+ datasets: [{
467
+ label: '圓周率內躲藏的位置深度',
468
+ data: validData.map(s => s.resultVal),
469
+ backgroundColor: 'rgba(52, 211, 153, 0.7)',
470
+ borderColor: '#34d399',
471
+ borderWidth: 2,
472
+ borderRadius: 6
473
+ }]
474
+ },
475
+ options: {
476
+ responsive: true,
477
+ plugins: { legend: { labels: { color: '#f9fafb', font: {size: 14} } } },
478
+ scales: {
479
+ y: { ticks: { color: '#9ca3af' }, grid: { color: 'rgba(255,255,255,0.05)' } },
480
+ x: { ticks: { color: '#d1d5db', maxRotation: 45, minRotation: 45 }, grid: { display: false } }
481
+ }
482
+ }
483
+ });
484
+ }
485
+ }
486
+
487
+ function drawPodium(validData) {
488
+ const podiumArea = document.getElementById('podiumArea');
489
+ podiumArea.innerHTML = "";
490
+
491
+ if(validData.length === 0) {
492
+ podiumArea.innerHTML = "<h3 style='color:#fbbf24'>沒有人找到號碼,無頒獎台 QQ</h3>"; return;
493
+ }
494
+
495
+ const top3 = validData.slice(0, 3);
496
+
497
+ // 順序: 2 -> 1 -> 3
498
+ const order = [];
499
+ if(top3.length >= 2) order.push({ data: top3[1], rank: 'second', num: 2 });
500
+ if(top3.length >= 1) order.push({ data: top3[0], rank: 'first', num: 1 });
501
+ if(top3.length >= 3) order.push({ data: top3[2], rank: 'third', num: 3 });
502
+
503
+ order.forEach(item => {
504
+ const div = document.createElement('div');
505
+ div.className = `podium ${item.rank} show`;
506
+ div.innerHTML = `
507
+ <div class="podium-info">
508
+ <div class="podium-name">${item.data.name}</div>
509
+ <div class="podium-val">深度 ${item.data.resultVal}</div>
510
+ </div>
511
+ <div class="podium-step">${item.num}</div>
512
+ `;
513
+ podiumArea.appendChild(div);
514
+ });
515
+ }
516
+ </script>
517
+ </body>
518
  </html>
m1.GIF ADDED

Git LFS Details

  • SHA256: 46a693c4fc5cc49e8a9062d2547eed1047fa14963b44d9c0f14cfbcde5071b42
  • Pointer size: 131 Bytes
  • Size of remote file: 113 kB
m2.GIF ADDED
m3.GIF ADDED
m4.GIF ADDED

Git LFS Details

  • SHA256: 4e0b277aa2b88129f18d075f8d2cbf55acc4c7afb63de4d25e6c19a8f20d6706
  • Pointer size: 131 Bytes
  • Size of remote file: 411 kB
m5.GIF ADDED
student.html ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-TW">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>幸運數字輸入 - 圓周率探索</title>
7
+ <style>
8
+ :root {
9
+ --primary: #8b5cf6;
10
+ --primary-hover: #7c3aed;
11
+ --bg: #0f172a;
12
+ --surface: rgba(30, 41, 59, 0.7);
13
+ --text: #f8fafc;
14
+ }
15
+ * { box-sizing: border-box; }
16
+ body {
17
+ margin: 0;
18
+ padding: 20px;
19
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
20
+ background: linear-gradient(135deg, #0f172a 0%, #1e1b4b 100%);
21
+ color: var(--text);
22
+ min-height: 100vh;
23
+ display: flex;
24
+ justify-content: center;
25
+ align-items: center;
26
+ }
27
+ .container {
28
+ width: 100%;
29
+ max-width: 400px;
30
+ background: rgba(255, 255, 255, 0.05);
31
+ backdrop-filter: blur(15px);
32
+ border: 1px solid rgba(255, 255, 255, 0.1);
33
+ border-radius: 1.5rem;
34
+ padding: 2rem;
35
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
36
+ animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1);
37
+ }
38
+ @keyframes slideUp {
39
+ from { opacity: 0; transform: translateY(30px); }
40
+ to { opacity: 1; transform: translateY(0); }
41
+ }
42
+ h1 {
43
+ text-align: center;
44
+ font-size: 1.5rem;
45
+ margin-top: 0;
46
+ margin-bottom: 0.5rem;
47
+ }
48
+ .room-badge {
49
+ display: block;
50
+ text-align: center;
51
+ font-size: 0.875rem;
52
+ color: #cbd5e1;
53
+ margin-bottom: 2rem;
54
+ padding: 0.5rem;
55
+ background: rgba(0,0,0,0.2);
56
+ border-radius: 0.5rem;
57
+ }
58
+ .form-group {
59
+ margin-bottom: 1.5rem;
60
+ }
61
+ label {
62
+ display: block;
63
+ margin-bottom: 0.5rem;
64
+ font-weight: 500;
65
+ color: #cbd5e1;
66
+ }
67
+ input {
68
+ width: 100%;
69
+ padding: 0.875rem 1rem;
70
+ background: rgba(0, 0, 0, 0.2);
71
+ border: 1px solid rgba(255, 255, 255, 0.1);
72
+ border-radius: 0.75rem;
73
+ color: white;
74
+ font-size: 1rem;
75
+ transition: all 0.3s ease;
76
+ }
77
+ input:focus {
78
+ outline: none;
79
+ border-color: var(--primary);
80
+ box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.3);
81
+ }
82
+ button {
83
+ width: 100%;
84
+ padding: 1rem;
85
+ background: linear-gradient(135deg, var(--primary), var(--primary-hover));
86
+ border: none;
87
+ border-radius: 0.75rem;
88
+ color: white;
89
+ font-size: 1.125rem;
90
+ font-weight: bold;
91
+ cursor: pointer;
92
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
93
+ display: flex;
94
+ justify-content: center;
95
+ align-items: center;
96
+ }
97
+ button:hover {
98
+ transform: translateY(-2px);
99
+ box-shadow: 0 10px 15px -3px rgba(139, 92, 246, 0.4);
100
+ }
101
+ button:active {
102
+ transform: translateY(0);
103
+ }
104
+ .spinner {
105
+ border: 3px solid rgba(255,255,255,0.3);
106
+ border-top: 3px solid white;
107
+ border-radius: 50%;
108
+ width: 24px;
109
+ height: 24px;
110
+ animation: spin 1s linear infinite;
111
+ display: none;
112
+ }
113
+ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
114
+
115
+ .success-state {
116
+ display: none;
117
+ text-align: center;
118
+ }
119
+ .success-icon {
120
+ font-size: 4rem;
121
+ margin-bottom: 1rem;
122
+ animation: popIn 0.5s cubic-bezier(0.16, 1, 0.3, 1);
123
+ }
124
+ @keyframes popIn {
125
+ 0% { transform: scale(0); }
126
+ 80% { transform: scale(1.1); }
127
+ 100% { transform: scale(1); }
128
+ }
129
+ </style>
130
+ </head>
131
+ <body>
132
+ <div class="container" id="mainContainer">
133
+ <h1>探索圓周率的秘密</h1>
134
+ <span class="room-badge" id="roomBadge">載入中...</span>
135
+
136
+ <div id="inputForm">
137
+ <div class="form-group">
138
+ <label for="studentName">姓名 (或座號)</label>
139
+ <input type="text" id="studentName" placeholder="例如:王小明" required>
140
+ </div>
141
+ <div class="form-group">
142
+ <label for="luckyNumber">你的幸運數字</label>
143
+ <!-- 注意:這裡改用 text,避免手機輸入前導零 (0007) 被瀏覽器吃掉 -->
144
+ <input type="text" id="luckyNumber" inputmode="numeric" placeholder="例如:01314" required>
145
+ </div>
146
+ <button id="submitBtn">
147
+ <span id="btnText">送出數字</span>
148
+ <div class="spinner" id="btnSpinner"></div>
149
+ </button>
150
+ </div>
151
+
152
+ <div class="success-state" id="successState">
153
+ <div class="success-icon">✅</div>
154
+ <h2>傳送成功!</h2>
155
+ <p>請抬頭看老師的大螢幕<br>準備開始環遊圓周率!</p>
156
+ </div>
157
+ </div>
158
+
159
+ <!-- Firebase SDK (Web Modules) -->
160
+ <script type="module">
161
+ import { initializeApp } from "https://www.gstatic.com/firebasejs/10.8.1/firebase-app.js";
162
+ import { getFirestore, collection, addDoc, serverTimestamp } from "https://www.gstatic.com/firebasejs/10.8.1/firebase-firestore.js";
163
+ import { firebaseConfig } from './firebase-config.js';
164
+
165
+ // 取得網址的 room 參數
166
+ const urlParams = new URLSearchParams(window.location.search);
167
+ const roomCode = urlParams.get('room') || '未指定教室';
168
+
169
+ document.getElementById('roomBadge').innerHTML = `教室代碼:<strong>${roomCode}</strong>`;
170
+
171
+ let db = null;
172
+ try {
173
+ if(firebaseConfig.apiKey !== "YOUR_API_KEY") {
174
+ const app = initializeApp(firebaseConfig);
175
+ db = getFirestore(app);
176
+ }
177
+ } catch(e) { console.error("Firebase 初始化失敗:", e); }
178
+
179
+ const submitBtn = document.getElementById('submitBtn');
180
+ const btnText = document.getElementById('btnText');
181
+ const btnSpinner = document.getElementById('btnSpinner');
182
+
183
+ submitBtn.addEventListener('click', async () => {
184
+ if(firebaseConfig.apiKey === "YOUR_API_KEY"){
185
+ alert("系統錯誤:老師尚未設定 Firebase!");
186
+ return;
187
+ }
188
+
189
+ const name = document.getElementById('studentName').value.trim();
190
+ const number = document.getElementById('luckyNumber').value.trim();
191
+
192
+ if (!name || !number) {
193
+ alert('請填寫完整姓名與幸運數字!');
194
+ return;
195
+ }
196
+
197
+ // UI 狀態:載入中
198
+ submitBtn.disabled = true;
199
+ btnText.style.display = 'none';
200
+ btnSpinner.style.display = 'block';
201
+
202
+ try {
203
+ // 將資料寫入 Firebase 中對應教室的 Subcollection
204
+ const studentsRef = collection(db, "rooms", roomCode, "students");
205
+ await addDoc(studentsRef, {
206
+ name: name,
207
+ number: number,
208
+ timestamp: serverTimestamp()
209
+ });
210
+
211
+ // UI 狀態:成功
212
+ document.getElementById('inputForm').style.display = 'none';
213
+ document.getElementById('successState').style.display = 'block';
214
+ } catch (error) {
215
+ console.error("寫入發生錯誤: ", error);
216
+ alert("發生錯誤,請稍後再試!\n" + error.message);
217
+
218
+ // 還原 UI
219
+ submitBtn.disabled = false;
220
+ btnText.style.display = 'block';
221
+ btnSpinner.style.display = 'none';
222
+ }
223
+ });
224
+ </script>
225
+ </body>
226
+ </html>