var typingMode = 'vocab'; var typingItems = []; var typingIndex = 0; var typingStats = { correct: 0, errors: 0 }; var typed = ''; var KEY_ROWS = [ ['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',';','\''], ['Shift','z','x','c','v','b','n','m',',','.','/','Shift'], ['Ctrl','Alt','Space','AltGr','Ctrl'] ]; var SPECIAL_HINTS = { 'á': 'Windows 美式国际:先按 \' 再按 a;macOS:Option + e 再按 a;手机长按 a', 'é': 'Windows 美式国际:先按 \' 再按 e;macOS:Option + e 再按 e;手机长按 e', 'í': 'Windows 美式国际:先按 \' 再按 i;macOS:Option + e 再按 i;手机长按 i', 'ó': 'Windows 美式国际:先按 \' 再按 o;macOS:Option + e 再按 o;手机长按 o', 'ú': 'Windows 美式国际:先按 \' 再按 u;macOS:Option + e 再按 u;手机长按 u', 'ü': 'Windows 美式国际:先按 " 再按 u;macOS:Option + u 再按 u;手机长按 u', 'ñ': 'Windows:AltGr + n 或先按 ~ 再按 n;macOS:Option + n 再按 n;手机长按 n', '¿': 'Windows:AltGr + ?;macOS:Option + Shift + ?;Linux:Compose ? ?', '¡': 'Windows:AltGr + 1;macOS:Option + 1;Linux:Compose ! !', '?': 'Shift + /', '!': 'Shift + 1', ':': 'Shift + ;' }; function standardKeyForChar(ch) { if (ch === ' ') return 'Space'; var lower = ch.toLowerCase(); if (/[a-z0-9]/.test(lower)) return lower; var map = { 'á':'a','é':'e','í':'i','ó':'o','ú':'u','ü':'u','ñ':'n', '?':'/','¿':'/','!':'1','¡':'1', ',':',','.':'.',';':';',':':';','-':'-','=':'=', '[':'[',']':']','\\':'\\',"'":"'",'\"':"'" }; return map[ch] || ''; } 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; } if (ch !== ch.toLowerCase() && ch === ch.toUpperCase() && /[a-z]/i.test(ch)) { el.textContent = '输入提示:先按 Shift,再按 ' + ch.toLowerCase(); return; } el.textContent = '输入提示:直接按标准键盘上的 ' + (standardKeyForChar(ch) || 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 base = standardKeyForChar(next); var shiftNeeded = next && next !== next.toLowerCase() && next === next.toUpperCase() && /[a-z]/i.test(next); var specialNeeded = next && /[áéíóúüñ¿¡]/.test(next); var shiftPunctuation = next === '?' || next === '!' || next === ':'; var container = document.getElementById('keyboard'); container.innerHTML = KEY_ROWS.map(function(row) { return '
完成 ' + typingItems.length + ' 个,正确率 ' + accuracy + '%