Spaces:
Running
Running
Upload 8 files
Browse files- .gitattributes +2 -0
- firebase-config.js +9 -0
- index.html +517 -18
- m1.GIF +3 -0
- m2.GIF +0 -0
- m3.GIF +0 -0
- m4.GIF +3 -0
- m5.GIF +0 -0
- 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 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
m2.GIF
ADDED
|
|
m3.GIF
ADDED
|
|
m4.GIF
ADDED
|
|
Git LFS Details
|
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>
|