262 lines
9.7 KiB
JavaScript
262 lines
9.7 KiB
JavaScript
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 '<option value="' + escTyping(cat.id) + '">' +
|
||
escTyping(cat.name) + '(' + cat.items.length + ')</option>';
|
||
}).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 += '<span class="done">' + ch + '</span>';
|
||
} else if (i === typed.length) {
|
||
targetHtml += '<span class="current">' + ch + '</span>';
|
||
} 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
|
||
? '已完成'
|
||
: '下一键:<b>' + escTyping(label) + '</b>';
|
||
}
|
||
|
||
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 '<div class="kb-row">' + row.map(function(key) {
|
||
var isHint = key.toLowerCase() === String(base || '').toLowerCase() ||
|
||
(key === 'Shift' && (shiftNeeded || shiftPunctuation)) ||
|
||
(key === 'AltGr' && specialNeeded);
|
||
var cls = ['kb-key'];
|
||
if (isHint) cls.push('hint');
|
||
if (key === 'Shift' || key === 'Ctrl' || key === 'Alt') cls.push('wide');
|
||
if (key === 'AltGr') cls.push('altgr');
|
||
if (key === 'Space') cls.push('space');
|
||
return '<div class="' + cls.join(' ') + '">' + escTyping(key) + '</div>';
|
||
}).join('') + '</div>';
|
||
}).join('');
|
||
renderSpecialHint(next);
|
||
}
|
||
|
||
function handleTypingInput() {
|
||
var input = document.getElementById('typing-input');
|
||
var value = input.value;
|
||
var item = typingItems[typingIndex];
|
||
if (!item) return;
|
||
var target = item.es;
|
||
var oldLength = typed.length;
|
||
typed = value;
|
||
if (target.indexOf(typed) === 0) {
|
||
typingStats.correct += Math.max(0, typed.length - oldLength);
|
||
renderTargetProgress(target);
|
||
} else {
|
||
typingStats.errors++;
|
||
typed = target.slice(0, oldLength);
|
||
input.value = typed;
|
||
renderTargetProgress(target);
|
||
}
|
||
renderNextKey(target);
|
||
renderTypingAccuracy();
|
||
if (typed.length >= target.length) {
|
||
typingStats.correct += Math.max(0, target.length - typed.length);
|
||
speakText(item.es, 'es');
|
||
typingIndex++;
|
||
typed = '';
|
||
setTimeout(renderPractice, 700);
|
||
}
|
||
}
|
||
|
||
function renderTargetProgress(target) {
|
||
var targetHtml = '';
|
||
for (var i = 0; i < target.length; i++) {
|
||
var ch = escTyping(target[i]);
|
||
if (i < typed.length) {
|
||
targetHtml += '<span class="done">' + ch + '</span>';
|
||
} else if (i === typed.length) {
|
||
targetHtml += '<span class="current">' + ch + '</span>';
|
||
} else {
|
||
targetHtml += ch;
|
||
}
|
||
}
|
||
document.getElementById('target-text').innerHTML = targetHtml;
|
||
}
|
||
|
||
function renderFinish() {
|
||
var totalKeys = typingStats.correct + typingStats.errors;
|
||
var accuracy = totalKeys ? Math.round(typingStats.correct * 100 / totalKeys) : 100;
|
||
document.getElementById('target-text').innerHTML = '';
|
||
document.getElementById('chinese-text').textContent = '';
|
||
document.getElementById('next-key').textContent = '';
|
||
document.getElementById('keyboard').innerHTML = '';
|
||
document.getElementById('progress-text').textContent = '';
|
||
document.getElementById('accuracy-text').textContent = '';
|
||
document.getElementById('typing-input').style.display = 'none';
|
||
document.getElementById('progress-fill').style.width = '100%';
|
||
document.getElementById('target-text').innerHTML =
|
||
'<div class="finish-box"><h2>练习完成</h2><p>完成 ' + typingItems.length +
|
||
' 个,正确率 ' + accuracy + '%</p><button id="btn-again">再练一次</button></div>';
|
||
document.getElementById('btn-again').addEventListener('click', function() {
|
||
document.getElementById('typing-input').style.display = '';
|
||
document.getElementById('setup-view').style.display = 'block';
|
||
document.getElementById('practice-view').style.display = 'none';
|
||
});
|
||
}
|
||
|
||
document.querySelectorAll('.mode-select button').forEach(function(btn) {
|
||
btn.addEventListener('click', function() {
|
||
typingMode = btn.dataset.mode;
|
||
document.querySelectorAll('.mode-select button').forEach(function(b) {
|
||
b.classList.toggle('active', b === btn);
|
||
});
|
||
renderCategories();
|
||
});
|
||
});
|
||
|
||
document.getElementById('btn-start').addEventListener('click', startTyping);
|
||
document.getElementById('btn-restart').addEventListener('click', startTyping);
|
||
document.getElementById('typing-input').addEventListener('input', handleTypingInput);
|
||
|
||
renderCategories();
|