feat: 新增西班牙语词汇与口语打字练习页面

This commit is contained in:
guoqingshan
2026-08-02 20:25:54 +08:00
parent 0d37a1c46e
commit 835514f927
6 changed files with 498 additions and 0 deletions
+217
View File
@@ -0,0 +1,217 @@
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();