Update index.html
Browse files- index.html +259 -47
index.html
CHANGED
|
@@ -259,6 +259,7 @@
|
|
| 259 |
];
|
| 260 |
|
| 261 |
// --- Data: Categories & Problems ---
|
|
|
|
| 262 |
const ALL_CATEGORIES = [
|
| 263 |
{
|
| 264 |
id: "basic",
|
|
@@ -266,12 +267,36 @@
|
|
| 266 |
desc: "萬丈高樓平地起,找出共同的寶藏。",
|
| 267 |
color: "from-blue-500 to-cyan-500",
|
| 268 |
problems: [
|
| 269 |
-
{
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
]
|
| 276 |
},
|
| 277 |
{
|
|
@@ -280,12 +305,36 @@
|
|
| 280 |
desc: "分進合擊!尋找隱藏的括號。",
|
| 281 |
color: "from-emerald-500 to-teal-500",
|
| 282 |
problems: [
|
| 283 |
-
{
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
]
|
| 290 |
},
|
| 291 |
{
|
|
@@ -294,11 +343,31 @@
|
|
| 294 |
desc: "敵人的敵人就是朋友,善用負號。",
|
| 295 |
color: "from-amber-500 to-orange-500",
|
| 296 |
problems: [
|
| 297 |
-
{
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
]
|
| 303 |
},
|
| 304 |
{
|
|
@@ -307,11 +376,52 @@
|
|
| 307 |
desc: "一加一減,成雙成對。",
|
| 308 |
color: "from-rose-500 to-pink-500",
|
| 309 |
problems: [
|
| 310 |
-
{
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
]
|
| 316 |
},
|
| 317 |
{
|
|
@@ -320,11 +430,31 @@
|
|
| 320 |
desc: "頭平方,尾平方,2倍頭尾在中央。",
|
| 321 |
color: "from-violet-500 to-purple-500",
|
| 322 |
problems: [
|
| 323 |
-
{
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 328 |
]
|
| 329 |
}
|
| 330 |
];
|
|
@@ -417,6 +547,7 @@
|
|
| 417 |
const [manualRoomInput, setManualRoomInput] = useState(""); // 新增:手動輸入的狀態
|
| 418 |
const [roomHistory, setRoomHistory] = useState([]); // 新增:房間歷史紀錄
|
| 419 |
const [selectedStudent, setSelectedStudent] = useState(null);
|
|
|
|
| 420 |
|
| 421 |
// 新增:載入歷史紀錄
|
| 422 |
useEffect(() => {
|
|
@@ -453,11 +584,11 @@
|
|
| 453 |
}
|
| 454 |
};
|
| 455 |
|
| 456 |
-
// 新增:產生帶參數的網址與 QR Code 連結
|
| 457 |
-
const
|
| 458 |
const baseUrl = window.location.origin + window.location.pathname;
|
| 459 |
const fullUrl = `${baseUrl}?room=${roomCode}`;
|
| 460 |
-
return `https://api.qrserver.com/v1/create-qr-code/?size
|
| 461 |
};
|
| 462 |
|
| 463 |
// Filter data by room
|
|
@@ -498,6 +629,24 @@
|
|
| 498 |
|
| 499 |
return (
|
| 500 |
<div className="min-h-screen flex flex-col p-4 md:p-6 relative bg-slate-900 text-slate-200 font-sans">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 501 |
<div className="flex justify-between items-center mb-6">
|
| 502 |
<h2 className="text-2xl font-bold flex items-center gap-2 text-amber-400"><Icons.Teacher /> 教師管理後台</h2>
|
| 503 |
<button onClick={onClose} className="bg-slate-700 hover:bg-slate-600 px-4 py-2 rounded-lg">返回遊戲</button>
|
|
@@ -512,9 +661,15 @@
|
|
| 512 |
<div className="text-xs text-slate-400 uppercase tracking-widest mb-2">目前房間代碼</div>
|
| 513 |
{roomCode ? (
|
| 514 |
<>
|
| 515 |
-
<div className="text-4xl font-mono font-bold text-yellow-400 tracking-widest mb-4">{roomCode}</div>
|
| 516 |
-
<div
|
| 517 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 518 |
</div>
|
| 519 |
<div className="mt-2 text-xs text-slate-500">學生掃描後可自動填入代碼</div>
|
| 520 |
</>
|
|
@@ -1077,23 +1232,80 @@
|
|
| 1077 |
if (navigator.vibrate) navigator.vibrate(200);
|
| 1078 |
}
|
| 1079 |
};
|
| 1080 |
-
|
| 1081 |
const insertSymbol = (symbol) => {
|
| 1082 |
-
const input = inputRef.current;
|
| 1083 |
-
|
| 1084 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1085 |
const newText = text.substring(0, start) + symbol + text.substring(end);
|
| 1086 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1087 |
};
|
|
|
|
| 1088 |
const deleteSymbol = () => {
|
| 1089 |
-
const input = inputRef.current;
|
| 1090 |
-
|
| 1091 |
-
|
| 1092 |
-
|
| 1093 |
-
|
| 1094 |
-
input.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1095 |
};
|
| 1096 |
-
|
| 1097 |
const renderMath = (text) => <span className="font-math text-3xl md:text-4xl">{text.split(/(\^[0-9]+)/g).map((part, i) => part.startsWith("^") ? <sup key={i} className="text-xs font-bold relative -top-3 left-0.5" style={{verticalAlign: 'super'}}>{part.substring(1)}</sup> : part)}</span>;
|
| 1098 |
const getDynamicKeyboard = (question) => { const vars = new Set(); (question.match(/[a-z]/g) || []).forEach(m => vars.add(m)); if (vars.size === 0) vars.add('x'); return Array.from(vars).sort(); };
|
| 1099 |
|
|
@@ -1242,8 +1454,8 @@
|
|
| 1242 |
</div>
|
| 1243 |
<div className={`absolute bottom-0 left-0 right-0 bg-[#16101a] border-t border-slate-800 p-2 z-50 transition-transform duration-500 ${chestOpen ? 'translate-y-full' : 'translate-y-0'}`}>
|
| 1244 |
<div className="max-w-2xl mx-auto">
|
| 1245 |
-
<div className="flex gap-1 mb-1.5 justify-center">{[...dynamicVars, '+', '-', '(', ')', '^2'].map(sym => <button key={sym} onClick={() => insertSymbol(sym)} className="flex-1 h-10 md:h-12 bg-[#2d2033] hover:bg-[#3e2d45] active:bg-[#251a2b] text-amber-100 rounded border-b-2 border-[#1a101f] active:border-b-0 active:translate-y-[2px] transition-all font-mono text-lg md:text-xl font-bold shadow-sm">{sym === '^2' ? <span>^<sup className="text-xs font-bold relative -top-1">2</sup></span> : sym}</button>)}<button onClick={deleteSymbol} className="flex-none w-14 h-10 md:h-12 bg-slate-700 hover:bg-slate-600 text-slate-200 rounded border-b-2 border-slate-800 active:border-b-0 active:translate-y-[2px] flex items-center justify-center"><Icons.Backspace /></button><button onClick={() => { setInputAnswer(""); setCursorPos(0); inputRef.current?.focus(); }} className="flex-none w-14 h-10 md:h-12 bg-red-900/30 hover:bg-red-900/50 text-red-400 rounded border-b-2 border-red-900/50 active:border-b-0 active:translate-y-[2px] text-xs font-bold">清除</button></div>
|
| 1246 |
-
<div className="flex gap-1 justify-center">{['1','2','3','4','5','6','7','8','9','0'].map(num => <button key={num} onClick={() => insertSymbol(num)} className="flex-1 h-10 md:h-12 bg-[#1f1623] hover:bg-[#2d2033] active:bg-[#16101a] text-slate-400 hover:text-amber-200 rounded border-b-2 border-black active:border-b-0 active:translate-y-[2px] transition-all font-mono text-lg md:text-xl font-bold shadow-sm">{num}</button>)}</div>
|
| 1247 |
</div>
|
| 1248 |
</div>
|
| 1249 |
</div>
|
|
|
|
| 259 |
];
|
| 260 |
|
| 261 |
// --- Data: Categories & Problems ---
|
| 262 |
+
// Expanded Answer Bank to cover all valid commutative and sign variations
|
| 263 |
const ALL_CATEGORIES = [
|
| 264 |
{
|
| 265 |
id: "basic",
|
|
|
|
| 267 |
desc: "萬丈高樓平地起,找出共同的寶藏。",
|
| 268 |
color: "from-blue-500 to-cyan-500",
|
| 269 |
problems: [
|
| 270 |
+
{
|
| 271 |
+
id: 101, q: "3b^2 + 2b",
|
| 272 |
+
a: ["b(3b+2)", "(3b+2)b", "b(2+3b)", "(2+3b)b"],
|
| 273 |
+
h: "提出公因式 b"
|
| 274 |
+
},
|
| 275 |
+
{
|
| 276 |
+
id: 102, q: "5x^2 + 2x",
|
| 277 |
+
a: ["x(5x+2)", "(5x+2)x", "x(2+5x)", "(2+5x)x"],
|
| 278 |
+
h: "提出公因式 x"
|
| 279 |
+
},
|
| 280 |
+
{
|
| 281 |
+
id: 103, q: "6y^2 - 2y",
|
| 282 |
+
a: ["2y(3y-1)", "(3y-1)2y", "2y(-1+3y)", "(-1+3y)2y", "-2y(1-3y)", "(1-3y)(-2y)"],
|
| 283 |
+
h: "係數 6 和 2 都有公因數 2"
|
| 284 |
+
},
|
| 285 |
+
{
|
| 286 |
+
id: 104, q: "4x^2 - 8x",
|
| 287 |
+
a: ["4x(x-2)", "x(4x-8)", "4(x^2-2x)", "2x(2x-4)", "2(2x^2-4x)", "4x(-2+x)", "(-2+x)4x", "(4x-8)x", "(x-2)4x", "-4x(2-x)"],
|
| 288 |
+
h: "提出 4x"
|
| 289 |
+
},
|
| 290 |
+
{
|
| 291 |
+
id: 105, q: "ax + bx",
|
| 292 |
+
a: ["x(a+b)", "(a+b)x", "x(b+a)", "(b+a)x"],
|
| 293 |
+
h: "最簡單的提公因式"
|
| 294 |
+
},
|
| 295 |
+
{
|
| 296 |
+
id: 106, q: "3x^2 + 6x",
|
| 297 |
+
a: ["3x(x+2)", "x(3x+6)", "3(x^2+2x)", "3x(2+x)", "(x+2)3x", "(3x+6)x", "(2+x)3x"],
|
| 298 |
+
h: "提出 3x"
|
| 299 |
+
}
|
| 300 |
]
|
| 301 |
},
|
| 302 |
{
|
|
|
|
| 305 |
desc: "分進合擊!尋找隱藏的括號。",
|
| 306 |
color: "from-emerald-500 to-teal-500",
|
| 307 |
problems: [
|
| 308 |
+
{
|
| 309 |
+
id: 201, q: "2x(x-2) + 5(x-2)",
|
| 310 |
+
a: ["(x-2)(2x+5)", "(2x+5)(x-2)", "(x-2)(5+2x)", "(5+2x)(x-2)", "(-x+2)(-2x-5)", "(-2x-5)(-x+2)", "-(2-x)(2x+5)", "-(2x+5)(2-x)"],
|
| 311 |
+
h: "把 (x-2) 當作一個整體"
|
| 312 |
+
},
|
| 313 |
+
{
|
| 314 |
+
id: 202, q: "x(3x-7) + 4(3x-7)",
|
| 315 |
+
a: ["(3x-7)(x+4)", "(x+4)(3x-7)", "(3x-7)(4+x)", "(4+x)(3x-7)", "(7-3x)(-x-4)", "(-x-4)(7-3x)", "-(7-3x)(x+4)", "-(x+4)(7-3x)"],
|
| 316 |
+
h: "提出 (3x-7)"
|
| 317 |
+
},
|
| 318 |
+
{
|
| 319 |
+
id: 203, q: "a(x+y) + b(x+y)",
|
| 320 |
+
a: ["(x+y)(a+b)", "(a+b)(x+y)", "(y+x)(a+b)", "(x+y)(b+a)", "(a+b)(y+x)", "(b+a)(x+y)", "(b+a)(y+x)"],
|
| 321 |
+
h: "都有 (x+y)"
|
| 322 |
+
},
|
| 323 |
+
{
|
| 324 |
+
id: 204, q: "x^2 + 2x + xy + 2y",
|
| 325 |
+
a: ["(x+2)(x+y)", "(x+y)(x+2)", "(2+x)(y+x)", "(x+y)(2+x)", "(y+x)(x+2)", "(y+x)(2+x)", "(2+x)(x+y)"],
|
| 326 |
+
h: "前兩項一組,後兩項一組"
|
| 327 |
+
},
|
| 328 |
+
{
|
| 329 |
+
id: 205, q: "ac + ad + bc + bd",
|
| 330 |
+
a: ["(c+d)(a+b)", "(a+b)(c+d)", "(d+c)(a+b)", "(c+d)(b+a)", "(a+b)(d+c)", "(b+a)(c+d)", "(b+a)(d+c)"],
|
| 331 |
+
h: "分組:a(c+d) + b(c+d)"
|
| 332 |
+
},
|
| 333 |
+
{
|
| 334 |
+
id: 206, q: "3x(2x-5) + (2x-5)(x+8)",
|
| 335 |
+
a: ["(2x-5)(4x+8)", "4(2x-5)(x+2)", "(4x+8)(2x-5)", "(2x-5)(8+4x)", "4(2x-5)(2+x)", "4(x+2)(2x-5)", "4(2+x)(2x-5)", "(5-2x)(-4x-8)", "-(5-2x)(4x+8)"],
|
| 336 |
+
h: "合併剩下的:3x + (x+8)"
|
| 337 |
+
}
|
| 338 |
]
|
| 339 |
},
|
| 340 |
{
|
|
|
|
| 343 |
desc: "敵人的敵人就是朋友,善用負號。",
|
| 344 |
color: "from-amber-500 to-orange-500",
|
| 345 |
problems: [
|
| 346 |
+
{
|
| 347 |
+
id: 301, q: "x(2x-1) + (1-2x)",
|
| 348 |
+
a: ["(2x-1)(x-1)", "(x-1)(2x-1)", "(1-2x)(1-x)", "(1-x)(1-2x)", "-(1-2x)(x-1)", "-(x-1)(1-2x)", "-(2x-1)(1-x)"],
|
| 349 |
+
h: "(1-2x) 變號為 -(2x-1)"
|
| 350 |
+
},
|
| 351 |
+
{
|
| 352 |
+
id: 302, q: "x(a-b) + y(b-a)",
|
| 353 |
+
a: ["(a-b)(x-y)", "(x-y)(a-b)", "(b-a)(y-x)", "(y-x)(b-a)", "-(b-a)(x-y)", "-(x-y)(b-a)", "-(a-b)(y-x)"],
|
| 354 |
+
h: "(b-a) 變號為 -(a-b)"
|
| 355 |
+
},
|
| 356 |
+
{
|
| 357 |
+
id: 303, q: "x(2x-3) - 3(3-2x)",
|
| 358 |
+
a: ["(2x-3)(x+3)", "(x+3)(2x-3)", "(3-2x)(-x-3)", "(-x-3)(3-2x)", "(3+x)(2x-3)", "(2x-3)(3+x)", "-(3-2x)(x+3)"],
|
| 359 |
+
h: "減去負的變成加"
|
| 360 |
+
},
|
| 361 |
+
{
|
| 362 |
+
id: 304, q: "(x-y)^2 + 2(y-x)",
|
| 363 |
+
a: ["(x-y)(x-y-2)", "(x-y-2)(x-y)", "(y-x)(y-x+2)", "(y-x+2)(y-x)", "(x-y-2)(x-y)", "-(y-x)(x-y-2)", "(-x+y+2)(-x+y)"],
|
| 364 |
+
h: "(y-x) 提出負號變成 -(x-y)"
|
| 365 |
+
},
|
| 366 |
+
{
|
| 367 |
+
id: 305, q: "a(x-y) - b(y-x)",
|
| 368 |
+
a: ["(x-y)(a+b)", "(a+b)(x-y)", "(y-x)(-a-b)", "(-a-b)(y-x)", "(x-y)(b+a)", "(b+a)(x-y)", "-(y-x)(a+b)"],
|
| 369 |
+
h: "變號後提出 (x-y)"
|
| 370 |
+
}
|
| 371 |
]
|
| 372 |
},
|
| 373 |
{
|
|
|
|
| 376 |
desc: "一加一減,成雙成對。",
|
| 377 |
color: "from-rose-500 to-pink-500",
|
| 378 |
problems: [
|
| 379 |
+
{
|
| 380 |
+
id: 401, q: "x^2 - 49",
|
| 381 |
+
a: ["(x+7)(x-7)", "(x-7)(x+7)", "(7+x)(x-7)", "(x-7)(7+x)", "(-x-7)(7-x)", "(7-x)(-x-7)", "-(7-x)(x+7)"],
|
| 382 |
+
h: "7 的平方是 49"
|
| 383 |
+
},
|
| 384 |
+
{
|
| 385 |
+
id: 402, q: "x^2 - 1",
|
| 386 |
+
a: ["(x+1)(x-1)", "(x-1)(x+1)", "(1+x)(x-1)", "(x-1)(1+x)", "(-x-1)(1-x)", "(1-x)(-x-1)", "-(1-x)(x+1)"],
|
| 387 |
+
h: "1 的平方還是 1"
|
| 388 |
+
},
|
| 389 |
+
{
|
| 390 |
+
id: 403, q: "4x^2 - 9",
|
| 391 |
+
a: ["(2x+3)(2x-3)", "(2x-3)(2x+3)", "(3+2x)(2x-3)", "(2x-3)(3+2x)", "(-2x-3)(3-2x)", "-(3-2x)(2x+3)"],
|
| 392 |
+
h: "(2x)^2 - 3^2"
|
| 393 |
+
},
|
| 394 |
+
{
|
| 395 |
+
id: 404, q: "25x^2 - 36y^2",
|
| 396 |
+
a: ["(5x+6y)(5x-6y)", "(5x-6y)(5x+6y)", "(6y+5x)(5x-6y)", "(5x-6y)(6y+5x)", "-(6y-5x)(5x+6y)"],
|
| 397 |
+
h: "(5x)^2 - (6y)^2"
|
| 398 |
+
},
|
| 399 |
+
{
|
| 400 |
+
id: 405, q: "49 - (y-1)^2",
|
| 401 |
+
a: [
|
| 402 |
+
// 基本正解: (8-y)(y+6) 及其變體
|
| 403 |
+
"(8-y)(y+6)", "(y+6)(8-y)",
|
| 404 |
+
"(6+y)(8-y)", "(8-y)(6+y)",
|
| 405 |
+
"(-y+8)(y+6)", "(y+6)(-y+8)",
|
| 406 |
+
"(-y+8)(6+y)", "(6+y)(-y+8)",
|
| 407 |
+
|
| 408 |
+
// 負負得正系列: (-y-6)(y-8)
|
| 409 |
+
"(-y-6)(y-8)", "(y-8)(-y-6)",
|
| 410 |
+
"(-6-y)(y-8)", "(y-8)(-6-y)",
|
| 411 |
+
"(-y-6)(-8+y)", "(-8+y)(-y-6)",
|
| 412 |
+
|
| 413 |
+
// 提出負號系列 1: -(y-8)(y+6)
|
| 414 |
+
"-(y-8)(y+6)", "-(y+6)(y-8)",
|
| 415 |
+
"-(y-8)(6+y)", "-(6+y)(y-8)",
|
| 416 |
+
"-(-8+y)(y+6)", "-(y+6)(-8+y)",
|
| 417 |
+
"-(-8+y)(6+y)", "-(6+y)(-8+y)",
|
| 418 |
+
|
| 419 |
+
// 提出負號系列 2: -(8-y)(-y-6)
|
| 420 |
+
"-(8-y)(-y-6)", "-(-y-6)(8-y)",
|
| 421 |
+
"-(8-y)(-6-y)", "-(-6-y)(8-y)"
|
| 422 |
+
],
|
| 423 |
+
h: "A=7, B=(y-1)"
|
| 424 |
+
}
|
| 425 |
]
|
| 426 |
},
|
| 427 |
{
|
|
|
|
| 430 |
desc: "頭平方,尾平方,2倍頭尾在中央。",
|
| 431 |
color: "from-violet-500 to-purple-500",
|
| 432 |
problems: [
|
| 433 |
+
{
|
| 434 |
+
id: 501, q: "x^2 + 2x + 1",
|
| 435 |
+
a: ["(x+1)^2", "(x+1)(x+1)", "(1+x)^2", "(1+x)(1+x)", "(-x-1)^2", "(-x-1)(-x-1)"],
|
| 436 |
+
h: "1 是 1^2,中間是 2*x*1"
|
| 437 |
+
},
|
| 438 |
+
{
|
| 439 |
+
id: 502, q: "x^2 + 8x + 16",
|
| 440 |
+
a: ["(x+4)^2", "(x+4)(x+4)", "(4+x)^2", "(4+x)(4+x)", "(-x-4)^2"],
|
| 441 |
+
h: "16 是 4^2"
|
| 442 |
+
},
|
| 443 |
+
{
|
| 444 |
+
id: 503, q: "x^2 - 4x + 4",
|
| 445 |
+
a: ["(x-2)^2", "(2-x)^2", "(x-2)(x-2)", "(2-x)(2-x)", "-(x-2)(2-x)"],
|
| 446 |
+
h: "中間是負號"
|
| 447 |
+
},
|
| 448 |
+
{
|
| 449 |
+
id: 504, q: "x^2 - 10x + 25",
|
| 450 |
+
a: ["(x-5)^2", "(5-x)^2", "(x-5)(x-5)", "(5-x)(5-x)"],
|
| 451 |
+
h: "25 是 5^2"
|
| 452 |
+
},
|
| 453 |
+
{
|
| 454 |
+
id: 505, q: "16x^2 - 24x + 9",
|
| 455 |
+
a: ["(4x-3)^2", "(3-4x)^2", "(4x-3)(4x-3)", "(3-4x)(3-4x)"],
|
| 456 |
+
h: "(4x)^2 和 3^2"
|
| 457 |
+
}
|
| 458 |
]
|
| 459 |
}
|
| 460 |
];
|
|
|
|
| 547 |
const [manualRoomInput, setManualRoomInput] = useState(""); // 新增:手動輸入的狀態
|
| 548 |
const [roomHistory, setRoomHistory] = useState([]); // 新增:房間歷史紀錄
|
| 549 |
const [selectedStudent, setSelectedStudent] = useState(null);
|
| 550 |
+
const [showLargeQr, setShowLargeQr] = useState(false); // 新增:控制大圖顯示狀態
|
| 551 |
|
| 552 |
// 新增:載入歷史紀錄
|
| 553 |
useEffect(() => {
|
|
|
|
| 584 |
}
|
| 585 |
};
|
| 586 |
|
| 587 |
+
// 新增:產生帶參數的網址與 QR Code 連結 (size 參數可調整)
|
| 588 |
+
const getQrUrl = (size = "150x150") => {
|
| 589 |
const baseUrl = window.location.origin + window.location.pathname;
|
| 590 |
const fullUrl = `${baseUrl}?room=${roomCode}`;
|
| 591 |
+
return `https://api.qrserver.com/v1/create-qr-code/?size=${size}&data=${encodeURIComponent(fullUrl)}`;
|
| 592 |
};
|
| 593 |
|
| 594 |
// Filter data by room
|
|
|
|
| 629 |
|
| 630 |
return (
|
| 631 |
<div className="min-h-screen flex flex-col p-4 md:p-6 relative bg-slate-900 text-slate-200 font-sans">
|
| 632 |
+
{/* Large QR Modal */}
|
| 633 |
+
{showLargeQr && (
|
| 634 |
+
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/90 backdrop-blur-md p-4" onClick={() => setShowLargeQr(false)}>
|
| 635 |
+
<div className="bg-white p-8 rounded-3xl animate-pop text-center max-w-lg w-full shadow-2xl relative" onClick={e => e.stopPropagation()}>
|
| 636 |
+
<button onClick={() => setShowLargeQr(false)} className="absolute top-4 right-4 text-slate-400 hover:text-slate-600">
|
| 637 |
+
<Icons.Check className="rotate-45" />
|
| 638 |
+
</button>
|
| 639 |
+
<h3 className="text-2xl font-bold text-slate-800 mb-2">加入房間</h3>
|
| 640 |
+
<div className="text-6xl font-mono font-bold text-amber-600 tracking-widest mb-6 select-all">{roomCode}</div>
|
| 641 |
+
<div className="bg-white p-2 rounded-xl inline-block border-4 border-slate-100 mb-4">
|
| 642 |
+
<img src={getQrUrl("500x500")} alt="Large Room QR" className="w-full max-w-[400px] h-auto aspect-square object-contain" />
|
| 643 |
+
</div>
|
| 644 |
+
<p className="text-slate-500 font-bold">請學生掃描 QR Code 或輸入上方代碼</p>
|
| 645 |
+
<div className="text-slate-400 text-xs mt-2">點擊任意處關閉</div>
|
| 646 |
+
</div>
|
| 647 |
+
</div>
|
| 648 |
+
)}
|
| 649 |
+
|
| 650 |
<div className="flex justify-between items-center mb-6">
|
| 651 |
<h2 className="text-2xl font-bold flex items-center gap-2 text-amber-400"><Icons.Teacher /> 教師管理後台</h2>
|
| 652 |
<button onClick={onClose} className="bg-slate-700 hover:bg-slate-600 px-4 py-2 rounded-lg">返回遊戲</button>
|
|
|
|
| 661 |
<div className="text-xs text-slate-400 uppercase tracking-widest mb-2">目前房間代碼</div>
|
| 662 |
{roomCode ? (
|
| 663 |
<>
|
| 664 |
+
<div className="text-4xl font-mono font-bold text-yellow-400 tracking-widest mb-4 select-all">{roomCode}</div>
|
| 665 |
+
<div
|
| 666 |
+
className="bg-white p-2 rounded-lg inline-block mx-auto cursor-zoom-in hover:scale-105 transition-transform group relative"
|
| 667 |
+
onClick={() => setShowLargeQr(true)}
|
| 668 |
+
>
|
| 669 |
+
<img src={getQrUrl("150x150")} alt="Room QR" className="w-32 h-32" />
|
| 670 |
+
<div className="absolute inset-0 flex items-center justify-center bg-black/0 group-hover:bg-black/20 transition-colors rounded-lg">
|
| 671 |
+
<span className="opacity-0 group-hover:opacity-100 bg-black/70 text-white text-xs px-2 py-1 rounded backdrop-blur-sm pointer-events-none">點擊放大</span>
|
| 672 |
+
</div>
|
| 673 |
</div>
|
| 674 |
<div className="mt-2 text-xs text-slate-500">學生掃描後可自動填入代碼</div>
|
| 675 |
</>
|
|
|
|
| 1232 |
if (navigator.vibrate) navigator.vibrate(200);
|
| 1233 |
}
|
| 1234 |
};
|
| 1235 |
+
|
| 1236 |
const insertSymbol = (symbol) => {
|
| 1237 |
+
const input = inputRef.current;
|
| 1238 |
+
|
| 1239 |
+
// Determine insertion point: prefer current cursor, fallback to end if lost
|
| 1240 |
+
let start = cursorPos;
|
| 1241 |
+
let end = cursorPos;
|
| 1242 |
+
|
| 1243 |
+
// If input is actually focused in DOM, use its real selection properties
|
| 1244 |
+
if (input && document.activeElement === input) {
|
| 1245 |
+
start = input.selectionStart;
|
| 1246 |
+
end = input.selectionEnd;
|
| 1247 |
+
} else if (input) {
|
| 1248 |
+
// If not focused (e.g. lost focus to button click), assume state cursorPos is valid
|
| 1249 |
+
// or default to end of string if unsure
|
| 1250 |
+
if (start > inputAnswer.length) start = inputAnswer.length;
|
| 1251 |
+
end = start;
|
| 1252 |
+
}
|
| 1253 |
+
|
| 1254 |
+
const text = inputAnswer;
|
| 1255 |
const newText = text.substring(0, start) + symbol + text.substring(end);
|
| 1256 |
+
const newPos = start + symbol.length;
|
| 1257 |
+
|
| 1258 |
+
setInputAnswer(newText);
|
| 1259 |
+
setCursorPos(newPos);
|
| 1260 |
+
|
| 1261 |
+
// Force focus and set selection after render cycle
|
| 1262 |
+
setTimeout(() => {
|
| 1263 |
+
if (inputRef.current) {
|
| 1264 |
+
inputRef.current.focus();
|
| 1265 |
+
inputRef.current.setSelectionRange(newPos, newPos);
|
| 1266 |
+
}
|
| 1267 |
+
}, 0);
|
| 1268 |
};
|
| 1269 |
+
|
| 1270 |
const deleteSymbol = () => {
|
| 1271 |
+
const input = inputRef.current;
|
| 1272 |
+
|
| 1273 |
+
let start = cursorPos;
|
| 1274 |
+
let end = cursorPos;
|
| 1275 |
+
|
| 1276 |
+
if (input && document.activeElement === input) {
|
| 1277 |
+
start = input.selectionStart;
|
| 1278 |
+
end = input.selectionEnd;
|
| 1279 |
+
}
|
| 1280 |
+
|
| 1281 |
+
const text = inputAnswer;
|
| 1282 |
+
let newText = text;
|
| 1283 |
+
let newPos = start;
|
| 1284 |
+
|
| 1285 |
+
if (start !== end) {
|
| 1286 |
+
newText = text.substring(0, start) + text.substring(end);
|
| 1287 |
+
newPos = start;
|
| 1288 |
+
} else if (start > 0) {
|
| 1289 |
+
newText = text.substring(0, start - 1) + text.substring(end);
|
| 1290 |
+
newPos = start - 1;
|
| 1291 |
+
}
|
| 1292 |
+
|
| 1293 |
+
setInputAnswer(newText);
|
| 1294 |
+
setCursorPos(newPos);
|
| 1295 |
+
|
| 1296 |
+
setTimeout(() => {
|
| 1297 |
+
if (inputRef.current) {
|
| 1298 |
+
inputRef.current.focus();
|
| 1299 |
+
inputRef.current.setSelectionRange(newPos, newPos);
|
| 1300 |
+
}
|
| 1301 |
+
}, 0);
|
| 1302 |
+
};
|
| 1303 |
+
|
| 1304 |
+
const handleSelect = (e) => {
|
| 1305 |
+
// Sync React state with DOM state on user interaction
|
| 1306 |
+
setCursorPos(e.target.selectionStart);
|
| 1307 |
};
|
| 1308 |
+
|
| 1309 |
const renderMath = (text) => <span className="font-math text-3xl md:text-4xl">{text.split(/(\^[0-9]+)/g).map((part, i) => part.startsWith("^") ? <sup key={i} className="text-xs font-bold relative -top-3 left-0.5" style={{verticalAlign: 'super'}}>{part.substring(1)}</sup> : part)}</span>;
|
| 1310 |
const getDynamicKeyboard = (question) => { const vars = new Set(); (question.match(/[a-z]/g) || []).forEach(m => vars.add(m)); if (vars.size === 0) vars.add('x'); return Array.from(vars).sort(); };
|
| 1311 |
|
|
|
|
| 1454 |
</div>
|
| 1455 |
<div className={`absolute bottom-0 left-0 right-0 bg-[#16101a] border-t border-slate-800 p-2 z-50 transition-transform duration-500 ${chestOpen ? 'translate-y-full' : 'translate-y-0'}`}>
|
| 1456 |
<div className="max-w-2xl mx-auto">
|
| 1457 |
+
<div className="flex gap-1 mb-1.5 justify-center">{[...dynamicVars, '+', '-', '(', ')', '^2'].map(sym => <button onMouseDown={(e) => e.preventDefault()} onTouchStart={(e) => e.preventDefault()} key={sym} onClick={() => insertSymbol(sym)} className="flex-1 h-10 md:h-12 bg-[#2d2033] hover:bg-[#3e2d45] active:bg-[#251a2b] text-amber-100 rounded border-b-2 border-[#1a101f] active:border-b-0 active:translate-y-[2px] transition-all font-mono text-lg md:text-xl font-bold shadow-sm">{sym === '^2' ? <span>^<sup className="text-xs font-bold relative -top-1">2</sup></span> : sym}</button>)}<button onMouseDown={(e) => e.preventDefault()} onTouchStart={(e) => e.preventDefault()} onClick={deleteSymbol} className="flex-none w-14 h-10 md:h-12 bg-slate-700 hover:bg-slate-600 text-slate-200 rounded border-b-2 border-slate-800 active:border-b-0 active:translate-y-[2px] flex items-center justify-center"><Icons.Backspace /></button><button onMouseDown={(e) => e.preventDefault()} onTouchStart={(e) => e.preventDefault()} onClick={() => { setInputAnswer(""); setCursorPos(0); inputRef.current?.focus(); }} className="flex-none w-14 h-10 md:h-12 bg-red-900/30 hover:bg-red-900/50 text-red-400 rounded border-b-2 border-red-900/50 active:border-b-0 active:translate-y-[2px] text-xs font-bold">清除</button></div>
|
| 1458 |
+
<div className="flex gap-1 justify-center">{['1','2','3','4','5','6','7','8','9','0'].map(num => <button onMouseDown={(e) => e.preventDefault()} onTouchStart={(e) => e.preventDefault()} key={num} onClick={() => insertSymbol(num)} className="flex-1 h-10 md:h-12 bg-[#1f1623] hover:bg-[#2d2033] active:bg-[#16101a] text-slate-400 hover:text-amber-200 rounded border-b-2 border-black active:border-b-0 active:translate-y-[2px] transition-all font-mono text-lg md:text-xl font-bold shadow-sm">{num}</button>)}</div>
|
| 1459 |
</div>
|
| 1460 |
</div>
|
| 1461 |
</div>
|