var typingMode = 'vocab'; var typingItems = []; var typingIndex = 0; var typingStats = { correct: 0, errors: 0 }; var typed = ''; var KEY_ROWS = [ [ { key: 'º', sub: '`' }, { key: '1' }, { key: '2' }, { key: '3' }, { key: '4' }, { key: '5' }, { key: '6' }, { key: '7' }, { key: '8' }, { key: '9' }, { key: '0' }, { key: "'", sub: '-' }, { key: '¡', sub: '=' } ], [ { key: 'q' }, { key: 'w' }, { key: 'e' }, { key: 'r' }, { key: 't' }, { key: 'y' }, { key: 'u' }, { key: 'i' }, { key: 'o' }, { key: 'p' }, { key: '`', sub: '[' }, { key: '+', sub: ']' }, { key: 'ç', sub: '\\' } ], [ { key: 'a' }, { key: 's' }, { key: 'd' }, { key: 'f' }, { key: 'g' }, { key: 'h' }, { key: 'j' }, { key: 'k' }, { key: 'l' }, { key: 'ñ', sub: ';' }, { key: '´', sub: "'" } ], [ { key: 'Shift' }, { key: '<' }, { key: 'z' }, { key: 'x' }, { key: 'c' }, { key: 'v' }, { key: 'b' }, { key: 'n' }, { key: 'm' }, { key: ',' }, { key: '.' }, { key: '-', sub: '/' }, { key: 'Shift' } ], [ { key: 'Ctrl' }, { key: 'Win' }, { key: 'Alt' }, { key: 'Space' }, { key: 'AltGr' }, { key: 'Ctrl' } ] ]; var SPECIAL_HINTS = { 'á': '先按 L 右边的 ´ 键,再按 a', 'é': '先按 L 右边的 ´ 键,再按 e', 'í': '先按 L 右边的 ´ 键,再按 i', 'ó': '先按 L 右边的 ´ 键,再按 o', 'ú': '先按 L 右边的 ´ 键,再按 u', 'Á': '先按 L 右边的 ´ 键,再按 Shift + A', 'É': '先按 L 右边的 ´ 键,再按 Shift + E', 'Í': '先按 L 右边的 ´ 键,再按 Shift + I', 'Ó': '先按 L 右边的 ´ 键,再按 Shift + O', 'Ú': '先按 L 右边的 ´ 键,再按 Shift + U', 'ü': 'Shift + L 右边的 ´ 键,松开后按 u', 'Ü': 'Shift + L 右边的 ´ 键,松开后按 Shift + U', 'ñ': '直接按 L 右边的 ñ 键', 'Ñ': 'Shift + L 右边的 ñ 键', '¿': 'Shift + ¡ 键(标准键盘上是 Shift + 加号键)', '¡': '直接按 ¡ 键(标准键盘上是加号键)', '?': 'Shift + \' 键(标准键盘上是 Shift + 减号键)', '!': 'Shift + 数字 1', ':': 'Shift + 句号键', ';': 'Shift + 逗号键' }; function keyPressForChar(ch) { if (!ch) return null; if (ch === ' ') return { keys: ['Space'], shift: false }; var lower = ch.toLowerCase(); if (/[a-z0-9]/.test(lower)) { return { keys: [lower], shift: ch !== lower }; } var map = { 'á': { keys: ['´'], shift: false }, 'é': { keys: ['´'], shift: false }, 'í': { keys: ['´'], shift: false }, 'ó': { keys: ['´'], shift: false }, 'ú': { keys: ['´'], shift: false }, 'Á': { keys: ['´'], shift: false }, 'É': { keys: ['´'], shift: false }, 'Í': { keys: ['´'], shift: false }, 'Ó': { keys: ['´'], shift: false }, 'Ú': { keys: ['´'], shift: false }, 'ü': { keys: ['´'], shift: true }, 'Ü': { keys: ['´'], shift: true }, 'ñ': { keys: ['ñ'], shift: false }, 'Ñ': { keys: ['ñ'], shift: true }, '¿': { keys: ['¡'], shift: true }, '¡': { keys: ['¡'], shift: false }, '?': { keys: ["'"], shift: true }, '!': { keys: ['1'], shift: true }, ':': { keys: ['.'], shift: true }, ';': { keys: [','], shift: true }, '/': { keys: ['7'], shift: true }, '"': { keys: ['2'], shift: true }, '(': { keys: ['8'], shift: true }, ')': { keys: ['9'], shift: true }, '_': { keys: ['-'], shift: true }, '-': { keys: ['-'], shift: false }, "'": { keys: ["'"], shift: false }, ',': { keys: [','], shift: false }, '.': { keys: ['.'], shift: false }, '<': { keys: ['<'], shift: false }, '>': { keys: ['<'], shift: true }, '`': { keys: ['`'], shift: false }, '^': { keys: ['`'], shift: true }, '+': { keys: ['+'], shift: false }, '*': { keys: ['+'], shift: true }, 'º': { keys: ['º'], shift: false }, 'ª': { keys: ['º'], shift: true }, 'ç': { keys: ['ç'], shift: false }, 'Ç': { keys: ['ç'], shift: true } }; return map[ch] || null; } function renderSpecialHint(ch) { var el = document.getElementById('special-hint'); if (!ch) { el.textContent = ''; return; } if (SPECIAL_HINTS[ch]) { el.textContent = '输入提示:' + SPECIAL_HINTS[ch]; return; } if (ch === ' ') { el.textContent = '输入提示:按空格键'; return; } var press = keyPressForChar(ch); if (press && press.shift) { el.textContent = '输入提示:先按 Shift,再按 ' + press.keys.join(' + ') + ' 键'; return; } if (ch !== ch.toLowerCase() && ch === ch.toUpperCase() && /[a-z]/i.test(ch)) { el.textContent = '输入提示:先按 Shift,再按 ' + ch.toLowerCase(); return; } el.textContent = '输入提示:直接按 ' + (press ? press.keys.join(' + ') : ch) + ' 键'; } function escTyping(s) { var div = document.createElement('div'); div.textContent = s == null ? '' : String(s); return div.innerHTML; } function currentCategory() { var id = document.getElementById('category-select').value; return window.TYPING_DATA[typingMode].categories.find(function(c) { return c.id === id; }); } function renderCategories() { var data = window.TYPING_DATA[typingMode]; var select = document.getElementById('category-select'); select.innerHTML = data.categories.map(function(cat) { return ''; }).join(''); } function startTyping() { var category = currentCategory(); if (!category) return; var source = category.items.map(function(item, idx) { return Object.assign({}, item, { id: idx + 1 }); }); if (document.getElementById('order-select').value === 'random') { for (var i = source.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var t = source[i]; source[i] = source[j]; source[j] = t; } } var count = document.getElementById('count-select').value; typingItems = count === 'all' ? source : source.slice(0, parseInt(count, 10) || 10); typingIndex = 0; typingStats = { correct: 0, errors: 0 }; typed = ''; document.getElementById('typing-input').style.display = ''; document.getElementById('setup-view').style.display = 'none'; document.getElementById('practice-view').style.display = 'block'; renderPractice(); } function renderPractice() { if (typingIndex >= typingItems.length) { renderFinish(); return; } var item = typingItems[typingIndex]; var target = item.es; var targetHtml = ''; for (var i = 0; i < target.length; i++) { var ch = escTyping(target[i]); if (i < typed.length) { targetHtml += '' + ch + ''; } else if (i === typed.length) { targetHtml += '' + ch + ''; } else { targetHtml += ch; } } document.getElementById('target-text').innerHTML = targetHtml; document.getElementById('chinese-text').textContent = item.zh || ''; document.getElementById('progress-text').textContent = (typingIndex + 1) + ' / ' + typingItems.length; var totalKeys = typingStats.correct + typingStats.errors; document.getElementById('accuracy-text').textContent = '正确按键 ' + typingStats.correct + ',错误 ' + typingStats.errors + ',正确率 ' + (totalKeys ? Math.round(typingStats.correct * 100 / totalKeys) : 100) + '%'; document.getElementById('progress-fill').style.width = (typingIndex * 100 / typingItems.length) + '%'; var input = document.getElementById('typing-input'); input.value = typed; renderNextKey(target); renderKeyboard(target); setTimeout(function() { input.focus(); }, 50); } function renderNextKey(target) { var next = target[typed.length]; var label = next === ' ' ? '空格' : next; document.getElementById('next-key').innerHTML = typed.length >= target.length ? '已完成' : '下一键:' + escTyping(label) + ''; } function renderTypingAccuracy() { var totalKeys = typingStats.correct + typingStats.errors; document.getElementById('accuracy-text').textContent = '正确按键 ' + typingStats.correct + ',错误 ' + typingStats.errors + ',正确率 ' + (totalKeys ? Math.round(typingStats.correct * 100 / totalKeys) : 100) + '%'; } function renderKeyboard(target) { var next = target[typed.length] || ''; var press = keyPressForChar(next); var highlightKeys = {}; var highlightShift = false; if (press) { press.keys.forEach(function(key) { highlightKeys[key.toLowerCase()] = true; }); highlightShift = press.shift; } var container = document.getElementById('keyboard'); container.innerHTML = KEY_ROWS.map(function(row) { return '
完成 ' + typingItems.length + ' 个,正确率 ' + accuracy + '%