342 lines
12 KiB
JavaScript
342 lines
12 KiB
JavaScript
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 '<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 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 '<div class="kb-row">' + row.map(function(item) {
|
||
var key = item.key;
|
||
var sub = item.sub || '';
|
||
var isHint = highlightKeys[key.toLowerCase()] ||
|
||
(key === 'Shift' && highlightShift);
|
||
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');
|
||
var face = sub
|
||
? '<span class="kb-main">' + escTyping(key) + '</span><span class="kb-sub">' + escTyping(sub) + '</span>'
|
||
: escTyping(key);
|
||
return '<div class="' + cls.join(' ') + '">' + face + '</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();
|