-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstu_gmail_practise.html
More file actions
42 lines (42 loc) · 10.6 KB
/
Copy pathstu_gmail_practise.html
File metadata and controls
42 lines (42 loc) · 10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<!doctype html>
<html lang="zh-TW">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>課前準備|帳號與密碼輸入練習</title>
<style>
:root{font-family:system-ui,-apple-system,"Noto Sans TC","Microsoft JhengHei",sans-serif;color:#1e293b;background:#f8fafc}*{box-sizing:border-box}body{min-height:100vh;margin:0;padding:28px 16px;background:radial-gradient(circle at top right,#e0e7ff,transparent 34%),radial-gradient(circle at 15% 12%,#cffafe,transparent 30%),#f8fafc}.wrap{width:min(100%,720px);margin:0 auto}.topbar{display:flex;align-items:center;justify-content:space-between;margin:0 0 18px;color:#4f46e5;font-size:13px;font-weight:900}.topbar a{color:#4f46e5;text-decoration:none}.card{border:1px solid #dbeafe;border-radius:28px;background:#fff;padding:28px;box-shadow:0 20px 42px #312e811c}.hero{margin:-28px -28px 26px;border-radius:27px 27px 22px 22px;padding:27px 28px;background:linear-gradient(135deg,#0f766e,#0ea5e9);color:#fff}.eyebrow{margin:0;color:#cffafe;font-size:12px;font-weight:900;letter-spacing:.16em}.hero h1{margin:9px 0 6px;font-size:28px}.hero p{margin:0;color:#e0f2fe;line-height:1.6}.hidden{display:none!important}.step{display:inline-block;border-radius:999px;background:#ecfeff;padding:6px 10px;color:#0f766e;font-size:12px;font-weight:900}.card h2{margin:12px 0 6px;font-size:21px}.muted{margin:0;color:#64748b;line-height:1.65}.field{display:block;margin-top:22px;color:#334155;font-size:14px;font-weight:800}.field input,.typing-input{display:block;width:100%;margin-top:8px;border:1px solid #cbd5e1;border-radius:14px;padding:14px;font:inherit;font-size:18px;letter-spacing:.05em;text-align:center}.field input:focus,.typing-input:focus{outline:3px solid #a5f3fc;border-color:#0f766e}.actions{display:flex;flex-wrap:wrap;justify-content:center;gap:10px;margin-top:18px}.button{border:0;border-radius:14px;padding:13px 18px;background:#0f766e;color:#fff;font:inherit;font-weight:900;cursor:pointer}.button:hover{background:#0b5e57}.button.secondary{background:#ecfeff;color:#0f766e}.button:disabled{background:#cbd5e1;color:#fff;cursor:not-allowed}.error{min-height:20px;margin:8px 0 0;color:#be123c;font-size:13px;font-weight:800}.confirm-id{margin:20px 0;border-radius:18px;background:#f0fdfa;padding:16px;color:#0f766e;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:25px;font-weight:900}.timer{display:inline-block;margin:12px 0;border-radius:999px;background:#ecfeff;padding:8px 13px;color:#0f766e;font-weight:900}.prompt{overflow-wrap:anywhere;margin:12px 0 18px;border:1px solid #e2e8f0;border-radius:18px;background:#f8fafc;padding:18px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:20px;line-height:1.65}.prompt .done{color:#94a3b8}.prompt .current{border-bottom:3px solid #f59e0b;color:#e11d48;font-weight:900}.stats{margin-top:10px;color:#64748b;font-size:14px;font-weight:700}.keyboard{margin-top:22px}.keyboard-row{display:flex;justify-content:center;gap:4px;margin:4px 0}.key{display:grid;width:43px;height:48px;place-items:center;border:1px solid #cbd5e1;border-radius:10px;background:#f8fafc;color:#475569;font-size:16px;font-weight:800}.key.active{border-color:#34d399;background:#d1fae5;color:#047857}.key.shift{border-color:#fbbf24;background:#fef3c7;color:#92400e}.summary-list{display:grid;gap:10px;margin-top:22px}.copy-item{position:relative;width:100%;border:1px solid #bae6fd;border-radius:16px;background:#f0f9ff;padding:15px 16px;color:#0c4a6e;text-align:left;font:inherit;cursor:pointer}.copy-item:hover{border-color:#38bdf8;background:#fff}.copy-item span{display:block;font-size:12px;font-weight:900;color:#64748b}.copy-item b{display:block;margin-top:4px;overflow-wrap:anywhere;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:15px}.copy-hint{min-height:20px;margin:12px 0 0;color:#047857;font-size:13px;font-weight:800}.notice{margin-top:22px;border-radius:16px;background:#f8fafc;padding:14px;color:#64748b;font-size:13px;line-height:1.65}.notice b{color:#334155}@media(max-width:520px){body{padding:16px 12px}.card{padding:22px 18px}.hero{margin:-22px -18px 22px;padding:23px 18px}.hero h1{font-size:24px}.key{width:31px;height:40px;font-size:13px}.prompt{font-size:16px}}
</style>
</head>
<body>
<main class="wrap">
<div class="topbar"><span>前峰國中 · 校務帳號準備</span></div>
<section class="card" aria-live="polite">
<header class="hero"><p class="eyebrow">STAGE 1 · TYPING PRACTICE</p><h1>帳號與密碼輸入練習</h1><p>第一階段:練習輸入校務 Gmail、初始密碼與 Scratch 帳號。</p></header>
<section id="step-1"><span class="step">第一階段 · 步驟 1/3</span><h2>輸入七碼學號</h2><p class="muted">系統將依學號建立三組練習文字。</p><label class="field" for="student-id">學號<input id="student-id" inputmode="numeric" autocomplete="off" maxlength="7" placeholder="例如:1512026"></label><p id="error" class="error" role="alert"></p><div class="actions"><button id="generate" class="button" type="button" disabled>產生練習內容</button></div></section>
<section id="step-2" class="hidden"><span class="step">步驟 2/3</span><h2>確認後開始</h2><p class="muted">依序輸入三組文字。輸入錯誤不會中斷練習。</p><div id="confirm-id" class="confirm-id"></div><div class="actions"><button id="reset" class="button secondary" type="button">重新輸入學號</button><button id="start" class="button" type="button">開始練習</button></div></section>
<section id="step-3" class="hidden"><span class="step">練習進行中</span><h2>跟著紅色底線輸入</h2><div id="timer" class="timer"></div><div id="prompt" class="prompt"></div><input id="typing-input" class="typing-input" inputmode="text" autocomplete="off" autocapitalize="off" spellcheck="false" maxlength="1" aria-label="輸入目前字元"><p id="stats" class="stats"></p><div id="keyboard" class="keyboard" aria-hidden="true"></div></section>
<section id="step-4" class="hidden"><span class="step">第一階段 · 步驟 3/3</span><h2>練習完成</h2><p class="muted">點選下方文字可複製。完成後請依教師指示開啟第二階段登入頁。</p><div id="summary-list" class="summary-list"></div><p id="copy-hint" class="copy-hint"></p><div class="actions"><a class="button secondary" href="stu_gmail_practise.html">重新練習</a></div></section>
<p class="notice"><b>提示:</b>這是校務帳號準備活動;完成後即可使用 Gmail、Classroom 與 Scratch 帳號。</p>
</section>
</main>
<script>
const keys = [['!','@','#','$','%','^','&','*','(',')'],['1','2','3','4','5','6','7','8','9','0'],['Q','W','E','R','T','Y','U','I','O','P'],['A','S','D','F','G','H','J','K','L'],['Z','X','C','V','B','N','M','.','Shift']];
let studentId = '', prompts = [], level = 0, index = 0, correct = 0, wrong = 0, timeLeft = 180, timer = null;
const byId = id => document.getElementById(id);
const validId = value => /^15[12]\d{4}$/.test(value);
const escapeHtml = value => String(value).replace(/[&<>"']/g, char => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[char]));
function show(step) { [1,2,3,4].forEach(number => byId(`step-${number}`).classList.toggle('hidden', number !== step)); }
function updateGenerate() { const id = byId('student-id').value.replace(/\D/g, ''); byId('student-id').value = id; byId('generate').disabled = !validId(id); byId('error').textContent = id && !validId(id) ? '請輸入目前學生使用的七碼學號,例如 1512026。' : ''; }
function generate() { studentId = byId('student-id').value; if (!validId(studentId)) return; prompts = [`qfm${studentId}@mail.qfm.kh.edu.tw`, `Hereisqfm${studentId}`, `qfm${studentId}`]; byId('confirm-id').textContent = `學號:${studentId}`; show(2); }
function start() { level = 0; index = 0; correct = 0; wrong = 0; timeLeft = 180; show(3); updatePrompt(); byId('typing-input').focus(); clearInterval(timer); timer = setInterval(() => { timeLeft -= 1; updateTimer(); if (timeLeft <= 0) finish(); }, 1000); }
function updateTimer() { byId('timer').textContent = `剩餘時間:${Math.max(0, timeLeft)} 秒`; }
function updatePrompt() { const text = prompts[level] || ''; byId('prompt').innerHTML = [...text].map((char, position) => `<span class="${position < index ? 'done' : (position === index ? 'current' : '')}">${escapeHtml(char)}</span>`).join(''); byId('stats').textContent = `已輸入 ${correct + wrong} 字元|正確 ${correct}|錯誤 ${wrong}`; updateTimer(); renderKeyboard(); }
function renderKeyboard() { const expected = (prompts[level] || '')[index] || ''; byId('keyboard').innerHTML = keys.map(row => `<div class="keyboard-row">${row.map(key => { const active = key.toLowerCase() === expected.toLowerCase(); const shift = key === 'Shift' && (/[A-Z]/.test(expected) || /[!@#$%^&*()]/.test(expected)); return `<span class="key${active ? ' active' : ''}${shift ? ' shift' : ''}">${key}</span>`; }).join('')}</div>`).join(''); }
function type() { const field = byId('typing-input'); const value = field.value; const expected = (prompts[level] || '')[index]; field.value = ''; if (!value || !expected) return; if (value === expected) { correct += 1; index += 1; } else wrong += 1; if (index >= prompts[level].length) { level += 1; index = 0; if (level >= prompts.length) { finish(); return; } } updatePrompt(); }
function finish() { clearInterval(timer); const labels = ['校務 Gmail 帳號', '初始密碼', 'Scratch 帳號']; byId('summary-list').innerHTML = prompts.map((text, position) => `<button class="copy-item" type="button" data-copy="${escapeHtml(text)}"><span>${labels[position]}</span><b>${escapeHtml(text)}</b></button>`).join(''); byId('summary-list').querySelectorAll('[data-copy]').forEach(button => button.addEventListener('click', () => navigator.clipboard?.writeText(button.dataset.copy).then(() => { byId('copy-hint').textContent = '已複製到剪貼簿。'; }).catch(() => { byId('copy-hint').textContent = '無法自動複製,請長按文字後手動複製。'; }))); show(4); }
function reset() { clearInterval(timer); studentId = ''; prompts = []; byId('student-id').value = ''; byId('generate').disabled = true; byId('error').textContent = ''; show(1); }
byId('student-id').addEventListener('input', updateGenerate); byId('student-id').addEventListener('keydown', event => { if (event.key === 'Enter' && !byId('generate').disabled) generate(); }); byId('generate').addEventListener('click', generate); byId('start').addEventListener('click', start); byId('reset').addEventListener('click', reset); byId('typing-input').addEventListener('input', type);
</script>
</body>
</html>