218 lines
7.8 KiB
JavaScript
218 lines
7.8 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','ñ'],
|
||
['z','x','c','v','b','n','m',',','.',';'],
|
||
['á','é','í','ó','ú','ü','¿','¡',':','?','!',' ']
|
||
];
|
||
|
||
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];
|
||
document.getElementById('next-key').innerHTML = typed.length >= target.length
|
||
? '已完成'
|
||
: '下一键:<b>' + escTyping(next) + '</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 container = document.getElementById('keyboard');
|
||
container.innerHTML = KEY_ROWS.map(function(row) {
|
||
return '<div class="kb-row">' + row.map(function(key) {
|
||
var label = key === ' ' ? '␣' : key;
|
||
var isHint = next && (
|
||
key === next ||
|
||
key.toLowerCase() === next.toLowerCase() ||
|
||
(next === ' ' && key === ' ')
|
||
);
|
||
return '<button type="button" class="kb-key' + (isHint ? ' hint' : '') + (key === ' ' ? ' space' : '') + '" data-key="' + escTyping(key) + '">' + escTyping(label) + '</button>';
|
||
}).join('') + '</div>';
|
||
}).join('');
|
||
}
|
||
|
||
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);
|
||
document.getElementById('keyboard').addEventListener('click', function(e) {
|
||
var btn = e.target.closest('.kb-key');
|
||
if (!btn) return;
|
||
var input = document.getElementById('typing-input');
|
||
var key = btn.dataset.key;
|
||
var next = typingItems[typingIndex] && typingItems[typingIndex].es[typed.length];
|
||
if (next && key.toLowerCase() === next.toLowerCase() && key !== next) {
|
||
key = next;
|
||
}
|
||
input.value += key;
|
||
handleTypingInput();
|
||
input.focus();
|
||
});
|
||
|
||
renderCategories();
|