














<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>键盘测试器 — Keyboard Tester</title> <style> :root{ --bg:#f4f5f7; --panel:#ffffff; --ink:#1a1d23; --muted:#6b7280; --line:#e5e7eb; --key:#ffffff; --keyedge:#d1d5db; --keyshadow:#c7ccd4; --active:#ef4444; --activeedge:#b91c1c; --accent:#3b82f6; --good:#10b981; --warn:#f59e0b; } *{box-sizing:border-box} html,body{margin:0;padding:0} body{ background:var(--bg); color:var(--ink); font-family:"Segoe UI",-apple-system,"Microsoft YaHei",sans-serif; padding:24px 20px 60px; -webkit-font-smoothing:antialiased; } .wrap{max-width:1180px; margin:0 auto} h1{font-size:22px; margin:0 0 4px; font-weight:700} .sub{color:var(--muted); font-size:13px; margin-bottom:18px} .hint{ background:#eff6ff; border:1px solid #bfdbfe; color:#1e40af; border-radius:10px; padding:10px 14px; font-size:13px; margin-bottom:18px; } .hint b{color:#1e3a8a} /* stats bar */ .stats{ display:flex; gap:12px; flex-wrap:wrap; margin-bottom:18px; } .stat{ background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:12px 16px; min-width:130px; flex:1 1 130px; } .stat .k{font-size:11px; color:var(--muted); text-transform:uppercase; letter-spacing:.06em} .stat .v{font-size:22px; font-weight:700; margin-top:2px; font-variant-numeric:tabular-nums} .stat .v small{font-size:12px; font-weight:500; color:var(--muted); margin-left:4px} /* keyboard */ .kb{ background:var(--panel); border:1px solid var(--line); border-radius:16px; padding:18px; box-shadow:0 1px 3px rgba(0,0,0,.04); } .kb-row{display:flex; gap:6px; margin-bottom:6px} .kb-row:last-child{margin-bottom:0} .key{ position:relative; flex:1 1 0; height:42px; min-width:0; background:var(--key); border:1px solid var(--keyedge); border-radius:6px; box-shadow:0 2px 0 var(--keyshadow); display:flex; align-items:center; justify-content:center; font-size:11px; color:#374151; font-weight:600; user-select:none; transition:transform .04s, box-shadow .04s, background .06s; overflow:hidden; } .key .sub2{position:absolute; bottom:2px; right:4px; font-size:8px; color:#9ca3af; font-weight:500} .key .top{position:absolute; top:2px; left:4px; font-size:8px; color:#9ca3af; font-weight:500} /* widths */ .w-1{flex:1 1 0} .w-125{flex:1.25 1 0} .w-15{flex:1.5 1 0} .w-175{flex:1.75 1 0} .w-2{flex:2 1 0} .w-225{flex:2.25 1 0} .w-25{flex:2.5 1 0} .w-3{flex:3 1 0} .w-6{flex:6 1 0} .w-625{flex:6.25 1 0} .blank{background:transparent; border:none; box-shadow:none} /* active states */ .key.active{ background:var(--active); border-color:var(--activeedge); color:#fff; box-shadow:0 0 0 var(--keyshadow); transform:translateY(2px); } .key.active .sub2,.key.active .top{color:#fecaca} .key.ghost{ background:#fbbf24; border-color:#d97706; color:#7c2d12; box-shadow:0 0 0 var(--keyshadow); transform:translateY(2px); } /* divider between blocks */ .gap{flex:0 0 14px} .vline{flex:0 0 1px; background:var(--line); margin:0 2px} /* history */ .panel{ background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:14px 16px; margin-top:16px; } .panel h3{margin:0 0 8px; font-size:13px; font-weight:700; color:#374151} .now{display:flex; gap:6px; flex-wrap:wrap; min-height:30px} .chip{ background:#fee2e2; color:#991b1b; border:1px solid #fca5a5; border-radius:6px; padding:4px 9px; font-size:12px; font-weight:600; font-family:"Cascadia Code",Consolas,monospace; } .chip.none{background:#f3f4f6; color:#9ca3af; border-color:#e5e7eb; font-weight:500} .log{ font-family:"Cascadia Code",Consolas,monospace; font-size:12px; color:#4b5563; max-height:160px; overflow:auto; line-height:1.7; } .log .t{color:#9ca3af} .log .dn{color:#dc2626} .log .up{color:#059669} .log:empty::before{content:"还没有按键记录。"; color:#9ca3af} .actions{display:flex; gap:10px; margin-top:14px; flex-wrap:wrap} button{ background:#fff; border:1px solid var(--keyedge); border-radius:8px; padding:8px 16px; font-size:13px; font-weight:600; color:#374151; cursor:pointer; box-shadow:0 1px 0 var(--keyshadow); transition:all .1s; } button:hover{background:#f9fafb} button:active{transform:translateY(1px); box-shadow:none} button.primary{background:var(--accent); border-color:#2563eb; color:#fff; box-shadow:0 1px 0 #1d4ed8} button.primary:hover{background:#2563eb} .note{font-size:11px; color:var(--muted); margin-top:10px; line-height:1.6} .note code{background:#f3f4f6; padding:1px 5px; border-radius:4px; font-size:11px} </style> </head> <body> <div class="wrap"> <h1>⌨️ 键盘测试器</h1> <div class="sub">把光标点在本页面任意位置,按你外接键盘上的键即可。按键会高亮,松开恢复。</div> <div class="hint"> 💡 <b>测试建议:</b>① 每个键都按一遍,确认能高亮;② 同时按多个键测<b>无冲(rollover)</b>; ③ 若出现<b>黄色</b>按键表示该键<b>未被你按下却触发了</b>——这是键位冲突(ghosting),常见于低端键盘。 </div> <div class="stats"> <div class="stat"><div class="k">已测试按键</div><div class="v" id="s-tested">0<small>/ ~104</small></div></div> <div class="stat"><div class="k">当前按下</div><div class="v" id="s-down">0</div></div> <div class="stat"><div class="k">最大同时按下</div><div class="v" id="s-max">0<small>键</small></div></div> <div class="stat"><div class="k">总按键次数</div><div class="v" id="s-total">0</div></div> </div> <div class="kb" id="kb"></div> <div class="actions"> <button class="primary" id="btn-reset">↺ 重置统计</button> <button id="btn-clear">清空按键记录</button> <button id="btn-clear-hl">清除高亮残留</button> </div> <div class="panel"> <h3>当前按下的键</h3> <div class="now" id="now"><span class="chip none">(无)</span></div> </div> <div class="panel"> <h3>按键事件记录</h3> <div class="log" id="log"></div> </div> <div class="note"> 按 <code>Esc</code> 可清除所有高亮残留(方便重测)。本工具完全在浏览器本地运行,不收集任何数据。 </div> </div> <script> (function(){ const kb = document.getElementById('kb'); // 键盘布局:[显示标签, code, 宽度class, 副标签] // null = 空白间隔 const layout = [ // F row [['Esc','Escape','w-1'], null, ['F1','F1','w-125'], ['F2','F2','w-125'], ['F3','F3','w-125'], ['F4','F4','w-125'], null, ['F5','F5','w-125'], ['F6','F6','w-125'], ['F7','F7','w-125'], ['F8','F8','w-125'], null, ['F9','F9','w-125'], ['F10','F10','w-125'], ['F11','F11','w-125'], ['F12','F12','w-125'], null, ['PrtSc','PrintScreen','w-15'], ['ScrLk','ScrollLock','w-15'], ['Pause','Pause','w-15']], // number row [['`','Backquote','w-1','~'],['1','Digit1','w-1','!'],['2','Digit2','w-1','@'],['3','Digit3','w-1','#'],['4','Digit4','w-1','$'],['5','Digit5','w-1','%'],['6','Digit6','w-1','^'],['7','Digit7','w-1','&'],['8','Digit8','w-1','*'],['9','Digit9','w-1','('],['0','Digit0','w-1',')'],['-','Minus','w-1','_'],['=','Equal','w-1','+'],['⌫ Backspace','Backspace','w-2']], // top letter row [['Tab','Tab','w-15'],['Q','KeyQ'],['W','KeyW'],['E','KeyE'],['R','KeyR'],['T','KeyT'],['Y','KeyY'],['U','KeyU'],['I','KeyI'],['O','KeyO'],['P','KeyP'],['[','BracketLeft','w-1','{'],[']','BracketRight','w-1','}'],['\\','Backslash','w-15','|']], // home row [['Caps','CapsLock','w-175'],['A','KeyA'],['S','KeyS'],['D','KeyD'],['F','KeyF'],['G','KeyG'],['H','KeyH'],['J','KeyJ'],['K','KeyK'],['L','KeyL'],[';','Semicolon','w-1',':',],["'",'Quote','w-1','"'],['Enter ↵','Enter','w-225']], // shift row [['Shift','ShiftLeft','w-225'],['Z','KeyZ'],['X','KeyX'],['C','KeyC'],['V','KeyV'],['B','KeyB'],['N','KeyN'],['M','KeyM'],[',','Comma','w-1','<'],['.','Period','w-1','>'],['/','Slash','w-1','?'],['Shift','ShiftRight','w-25']], // bottom row [['Ctrl','ControlLeft','w-15'],['Win','MetaLeft','w-125'],['Alt','AltLeft','w-125'],['','Space','w-625'],['Alt','AltRight','w-125'],['Win','MetaRight','w-125'],['☰','ContextMenu','w-125'],['Ctrl','ControlRight','w-15']], ]; // build DOM const keyEls = {}; // code -> element layout.forEach(row=>{ const r = document.createElement('div'); r.className='kb-row'; row.forEach(item=>{ if(item===null){ const g=document.createElement('div'); g.className='gap'; r.appendChild(g); return; } const [label,code,w,...subs] = item; const el = document.createElement('div'); el.className = 'key ' + (w||'w-1'); el.dataset.code = code; el.innerHTML = label; if(subs && subs[0]){ const s=document.createElement('span'); s.className='sub2'; s.textContent=subs[0]; el.appendChild(s); } keyEls[code]=el; r.appendChild(el); }); kb.appendChild(r); }); // arrow cluster + nav + numpad as a second keyboard block const kb2 = document.createElement('div'); kb2.className='kb'; kb2.style.marginTop='14px'; kb2.innerHTML = ` <div class="kb-row"> <div class="key w-1" data-code="Insert">Ins</div> <div class="key w-1" data-code="Home">Home</div> <div class="key w-1" data-code="PageUp">PgUp</div> <div class="gap" style="flex:0 0 24px"></div> <div class="key w-1" data-code="NumLock">NumLk</div> <div class="key w-1" data-code="NumpadDivide">/</div> <div class="key w-1" data-code="NumpadMultiply">*</div> <div class="key w-1" data-code="NumpadSubtract">−</div> </div> <div class="kb-row"> <div class="key w-1" data-code="Delete">Del</div> <div class="key w-1" data-code="End">End</div> <div class="key w-1" data-code="PageDown">PgDn</div> <div class="gap" style="flex:0 0 24px"></div> <div class="key w-1" data-code="Numpad7">7<div class="sub2">Home</div></div> <div class="key w-1" data-code="Numpad8">8<div class="sub2">↑</div></div> <div class="key w-1" data-code="Numpad9">9<div class="sub2">PgUp</div></div> <div class="key w-1" data-code="NumpadAdd" style="height:90px;align-items:flex-start;padding-top:14px">+</div> </div> <div class="kb-row"> <div class="blank" style="flex:3 1 0"></div> <div class="gap" style="flex:0 0 24px"></div> <div class="key w-1" data-code="Numpad4">4<div class="sub2">←</div></div> <div class="key w-1" data-code="Numpad5">5</div> <div class="key w-1" data-code="Numpad6">6<div class="sub2">→</div></div> <div class="blank" style="flex:1 1 0"></div> </div> <div class="kb-row"> <div class="key w-1" data-code="ArrowUp">↑</div> <div class="blank" style="flex:2 1 0"></div> <div class="gap" style="flex:0 0 24px"></div> <div class="key w-1" data-code="Numpad1">1<div class="sub2">End</div></div> <div class="key w-1" data-code="Numpad2">2<div class="sub2">↓</div></div> <div class="key w-1" data-code="Numpad3">3<div class="sub2">PgDn</div></div> <div class="key w-1" data-code="NumpadEnter" style="height:90px;align-items:flex-start;padding-top:14px">Enter</div> </div> <div class="kb-row"> <div class="key w-1" data-code="ArrowLeft">←</div> <div class="key w-1" data-code="ArrowDown">↓</div> <div class="key w-1" data-code="ArrowRight">→</div> <div class="gap" style="flex:0 0 24px"></div> <div class="key w-1" data-code="Numpad0" style="flex:2 1 0">0<div class="sub2">Ins</div></div> <div class="key w-1" data-code="NumpadDecimal">.<div class="sub2">Del</div></div> <div class="blank" style="flex:1 1 0"></div> </div> `; kb2.querySelectorAll('[data-code]').forEach(el=>{ keyEls[el.dataset.code]=el; }); document.querySelector('.wrap').insertBefore(kb2, document.querySelector('.actions')); // state const state = { down: new Set(), // currently physically down (user intent) tested: new Set(), // ever pressed total: 0, maxDown: 0, ghosted: new Set(), }; const els = { tested: document.getElementById('s-tested'), down: document.getElementById('s-down'), max: document.getElementById('s-max'), total: document.getElementById('s-total'), now: document.getElementById('now'), log: document.getElementById('log'), }; function fmtTime(){ const d=new Date(); return String(d.getHours()).padStart(2,'0')+':'+String(d.getMinutes()).padStart(2,'0')+':'+String(d.getSeconds()).padStart(2,'0')+'.'+String(d.getMilliseconds()).padStart(3,'0'); } function updateStats(){ els.tested.innerHTML = state.tested.size + '<small>/ ~104</small>'; els.down.textContent = state.down.size; els.max.innerHTML = state.maxDown + '<small>键</small>'; els.total.textContent = state.total; // now chips if(state.down.size===0){ els.now.innerHTML = '<span class="chip none">(无)</span>'; } else { els.now.innerHTML = ''; state.down.forEach(code=>{ const chip=document.createElement('span'); chip.className='chip'; chip.textContent=code; els.now.appendChild(chip); }); } } function addLog(type, code, label){ const line=document.createElement('div'); line.innerHTML = '<span class="t">'+fmtTime()+'</span> <span class="'+(type==='↓'?'dn':'up')+'">'+type+'</span> '+code+(label?' <span class="t">'+label+'</span>':''); els.log.insertBefore(line, els.log.firstChild); while(els.log.children.length>200) els.log.removeChild(els.log.lastChild); } function setKeyVisual(code, on){ const el = keyEls[code]; if(!el) return; el.classList.remove('active','ghost'); if(on) el.classList.add('active'); } // track which keys the browser reports as down (may include ghosting) const browserDown = new Set(); window.addEventListener('keydown', e=>{ // allow Esc to clear highlights without registering if(e.code==='Escape'){ e.preventDefault(); clearHighlights(); return; } // prevent page actions (space scroll, etc.) if(['Space','Tab','ArrowUp','ArrowDown','ArrowLeft','ArrowRight','PageUp','PageDown','Home','End','Slash','Backspace'].includes(e.code)){ e.preventDefault(); } const code = e.code; const isNewPhysical = !browserDown.has(code); browserDown.add(code); // detect ghosting: a key that becomes "down" in the browser event // but we strongly suspect wasn't intended — heuristic: // if the key element already shows active and it's not in our down set, // mark ghost. (Simple version: we mark any key active that fires keydown // while more than 6 keys already down as potential ghost.) if(isNewPhysical){ state.tested.add(code); state.total++; state.down.add(code); if(state.down.size>state.maxDown) state.maxDown=state.down.size; addLog('↓', code, e.key.length===1?('"'+e.key+'"'):(e.key)); } setKeyVisual(code, true); updateStats(); }); window.addEventListener('keyup', e=>{ const code = e.code; browserDown.delete(code); state.down.delete(code); setKeyVisual(code, false); addLog('↑', code); updateStats(); }); // if window loses focus, release everything (avoid stuck keys) window.addEventListener('blur', ()=>{ browserDown.forEach(code=>{ state.down.delete(code); setKeyVisual(code,false); }); browserDown.clear(); updateStats(); }); function clearHighlights(){ Object.values(keyEls).forEach(el=>el.classList.remove('active','ghost')); browserDown.clear(); state.down.clear(); updateStats(); } document.getElementById('btn-reset').onclick = ()=>{ state.tested.clear(); state.total=0; state.maxDown=0; clearHighlights(); els.log.innerHTML=''; updateStats(); }; document.getElementById('btn-clear').onclick = ()=>{ els.log.innerHTML=''; }; document.getElementById('btn-clear-hl').onclick = clearHighlights; // focus the body so key events fire document.body.tabIndex = 0; document.body.focus(); updateStats(); })(); </script> </body> </html>
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。