Files
spanish-diary/typing.js
T

262 lines
9.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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',
'¿': '西班牙语键盘:Shift + 加号键;美式国际键盘:AltGr + ?;macOS:Option + Shift + ?;数字键盘:Alt + 0191',
'¡': '西班牙语键盘:直接按加号键;美式国际键盘:AltGr + 1;macOS:Option + 1;数字键盘:Alt + 0161',
'?': '西班牙语键盘:Shift + 减号键;美式国际键盘: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();