feat: 西班牙语日记学习工具与知识库

This commit is contained in:
guoqingshan
2026-07-31 14:50:37 +08:00
commit 0d5b0919da
15 changed files with 2140 additions and 0 deletions
+329
View File
@@ -0,0 +1,329 @@
// Alias db.js functions
const dbGetAllEntries = getAllEntries;
const dbGetEntry = getEntry;
const dbAddEntry = addEntry;
const dbUpdateEntry = updateEntry;
const dbDeleteEntry = deleteEntry;
var savedSelStart = 0;
var savedSelEnd = 0;
let state = {
entries: [],
currentId: null
};
function uuid() {
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
}
function todayStr() {
const d = new Date();
return d.getFullYear() + '-' +
String(d.getMonth() + 1).padStart(2, '0') + '-' +
String(d.getDate()).padStart(2, '0');
}
// ----- Render functions -----
function renderList() {
const ul = document.getElementById('entry-list');
ul.innerHTML = state.entries.map(e => {
const preview = e.spanish.slice(0, 30) + (e.spanish.length > 30 ? '...' : '');
const active = e.id === state.currentId ? 'active' : '';
return '<li class="' + active + '" data-id="' + e.id + '">' +
'<div class="entry-date">' + escHtml(e.date) + '</div>' +
'<div class="entry-preview">' + escHtml(preview) + '</div>' +
'<div class="entry-actions">' +
'<button class="btn-delete" data-id="' + e.id + '">&#128465; 删除</button>' +
'</div>' +
'</li>';
}).join('');
}
function escHtml(s) {
const div = document.createElement('div');
div.textContent = s;
return div.innerHTML;
}
function renderTranslate(entry) {
document.getElementById('translate-en').textContent = entry ? entry.english : '-';
document.getElementById('translate-cn').textContent = entry ? entry.chinese : '-';
}
function setEditor(entry) {
document.getElementById('entry-date').value = entry ? entry.date : todayStr();
document.getElementById('entry-text').value = entry ? entry.spanish : '';
const hint = document.getElementById('entry-hint');
hint.textContent = entry && entry.spanish
? '已保存。修改后点击保存更新。'
: '输入西语后实时显示翻译';
updateLiveTranslation();
}
function clearEditor() {
state.currentId = null;
document.querySelectorAll('#entry-list li').forEach(function(el) { el.classList.remove('active'); });
setEditor(null);
renderTranslate(null);
clearLiveTranslation();
}
// ----- Live translation & vocabulary -----
function updateLiveTranslation() {
var text = document.getElementById('entry-text').value;
if (!text.trim()) {
clearLiveTranslation();
return;
}
var result = translateText(text);
document.getElementById('editor-translate-en').textContent = result.english;
document.getElementById('editor-translate-cn').textContent = result.chinese;
updateVocabulary(text);
}
function clearLiveTranslation() {
document.getElementById('editor-translate-en').textContent = '-';
document.getElementById('editor-translate-cn').textContent = '-';
document.getElementById('vocab-list').innerHTML = '';
document.getElementById('selection-translate').style.display = 'none';
}
function updateVocabulary(text) {
var vocab = getVocabulary(text);
var container = document.getElementById('vocab-list');
if (vocab.length === 0) {
container.innerHTML = '<div style="font-size:0.75rem;color:var(--text-muted);font-style:italic">输入西语后显示单词</div>';
return;
}
container.innerHTML = vocab.map(function(v) {
return '<div class="vocab-item">' +
'<span class="vocab-word">' + escHtml(v.word) + '</span>' +
'<button class="vocab-speech" data-word="' + escHtml(v.word) + '" data-lang="es">' + '\uD83D\uDD0A' + '</button>' +
'<span class="vocab-en">' + escHtml(v.english) + '</span>' +
'<button class="vocab-speech" data-word="' + escHtml(v.word) + '" data-lang="en">🔊</button>' +
'<span class="vocab-zh">' + escHtml(v.chinese) + '</span>' +
'<button class="vocab-speech" data-word="' + escHtml(v.word) + '" data-lang="zh">🔊</button>' +
'</div>';
}).join('');
}
function handleVocabSpeech(word, lang) {
var entry = getVocabForWord(word);
if (entry) {
if (lang === 'en') speakText(entry.en, 'en');
else if (lang === 'zh') speakText(entry.zh, 'zh');
else speakText(word, 'es');
}
}
function handleSelection() {
var ta = document.getElementById('entry-text');
var selText = ta.value.substring(ta.selectionStart, ta.selectionEnd).trim();
var el = document.getElementById('selection-translate');
if (selText && selText.length > 0) {
var result = translateSelection(selText);
document.getElementById('selected-translation').textContent =
result.english + ' / ' + result.chinese;
el.style.display = 'block';
} else {
el.style.display = 'none';
}
}
// ----- Event handlers -----
function selectEntry(id) {
state.currentId = id;
dbGetEntry(id).then(function(entry) {
if (entry) {
setEditor(entry);
renderTranslate(entry);
renderList();
}
});
}
function saveCurrentEntry() {
var date = document.getElementById('entry-date').value;
var spanish = document.getElementById('entry-text').value.trim();
if (!spanish) { alert('请输入西语内容'); return; }
var translation = translateText(spanish);
var savePromise;
if (state.currentId) {
savePromise = dbGetEntry(state.currentId).then(function(existing) {
existing.date = date;
existing.spanish = spanish;
existing.english = translation.english;
existing.chinese = translation.chinese;
existing.updatedAt = new Date().toISOString();
return dbUpdateEntry(existing);
});
} else {
var entry = {
id: uuid(),
date: date,
spanish: spanish,
english: translation.english,
chinese: translation.chinese,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
savePromise = dbAddEntry(entry).then(function() {
state.currentId = entry.id;
});
}
savePromise.then(function() {
return dbGetAllEntries();
}).then(function(entries) {
state.entries = entries;
renderList();
renderTranslate({ english: translation.english, chinese: translation.chinese });
var found = entries.find(function(e) { return e.id === state.currentId; });
if (found) setEditor(found);
document.getElementById('btn-save').textContent = '\uD83D\uDCBE 已保存 \u2713';
setTimeout(function() { document.getElementById('btn-save').textContent = '\uD83D\uDCBE 保存'; }, 1500);
});
}
function newEntry() {
clearEditor();
document.getElementById('entry-date').value = todayStr();
document.getElementById('entry-text').focus();
}
// ----- Modal -----
var pendingDeleteId = null;
function showDeleteModal(id) {
pendingDeleteId = id;
document.getElementById('modal-overlay').classList.add('show');
}
function hideDeleteModal() {
pendingDeleteId = null;
document.getElementById('modal-overlay').classList.remove('show');
}
function confirmDelete(id) {
dbDeleteEntry(id).then(function() {
if (state.currentId === id) clearEditor();
return dbGetAllEntries();
}).then(function(entries) {
state.entries = entries;
renderList();
hideDeleteModal();
});
}
function saveTextareaSelection() {
var ta = document.getElementById('entry-text');
savedSelStart = ta.selectionStart;
savedSelEnd = ta.selectionEnd;
}
// ----- Speech handler -----
function handleSpeech(lang) {
var text = lang === 'en'
? document.getElementById('translate-en').textContent
: document.getElementById('translate-cn').textContent;
if (text && text !== '-') speakText(text, lang);
}
// ----- Initialization -----
function init() {
dbGetAllEntries().then(function(entries) {
state.entries = entries;
renderList();
setEditor(null);
renderTranslate(null);
});
document.getElementById('entry-list').addEventListener('click', function(e) {
var li = e.target.closest('li[data-id]');
if (!li) return;
if (e.target.closest('.btn-delete')) {
showDeleteModal(li.dataset.id);
return;
}
selectEntry(li.dataset.id);
});
document.getElementById('btn-new').addEventListener('click', newEntry);
document.getElementById('btn-save').addEventListener('click', saveCurrentEntry);
document.querySelectorAll('.btn-speech').forEach(function(btn) {
btn.addEventListener('click', function() { handleSpeech(btn.dataset.lang); });
});
// Live translation on input
document.getElementById('entry-text').addEventListener('input', updateLiveTranslation);
// Selection translation
document.getElementById('entry-text').addEventListener('mouseup', function(e) { handleSelection(); saveTextareaSelection(); });
document.getElementById('entry-text').addEventListener('keyup', function(e) { handleSelection(); saveTextareaSelection(); });
// Vocabulary speech delegation
document.getElementById('editor-vocab').addEventListener('click', function(e) {
var btn = e.target.closest('.vocab-speech');
if (btn) handleVocabSpeech(btn.dataset.word, btn.dataset.lang);
});
// Vocabulary double-click to auto-read
document.getElementById('editor-vocab').addEventListener('dblclick', function(e) {
var item = e.target.closest('.vocab-item');
if (!item) return;
var word = item.querySelector('.vocab-word').textContent;
var el = e.target;
if (el.classList.contains('vocab-en') || el.closest('.vocab-en')) {
speakText(item.querySelector('.vocab-en').textContent, 'en');
} else if (el.classList.contains('vocab-zh') || el.closest('.vocab-zh')) {
speakText(item.querySelector('.vocab-zh').textContent, 'zh');
} else {
speakText(word, 'es');
}
});
// Textarea double-click auto-read
document.getElementById('entry-text').addEventListener('dblclick', function() {
var ta = document.getElementById('entry-text');
setTimeout(function() {
var txt = ta.value.substring(ta.selectionStart, ta.selectionEnd).trim();
if (txt) speakText(txt, 'es');
}, 50);
});
// Speak selected button - reads selection aloud
document.getElementById('btn-speak-selected').addEventListener('click', function() {
var ta = document.getElementById('entry-text');
var start = savedSelStart || ta.selectionStart;
var end = savedSelEnd || ta.selectionEnd;
if (start === end) { start = 0; end = ta.value.length; }
var sel = ta.value.substring(start, end).trim();
if (sel) {
speakText(sel, 'es');
document.getElementById('selected-translation').textContent = sel;
document.getElementById('selection-translate').style.display = 'block';
}
});
document.querySelector('#modal-box .btn-cancel').addEventListener('click', hideDeleteModal);
document.querySelector('#modal-box .btn-confirm').addEventListener('click', function() {
if (pendingDeleteId) confirmDelete(pendingDeleteId);
});
document.addEventListener('keydown', function(e) {
if (e.ctrlKey && e.key === 'Enter') saveCurrentEntry();
});
}
document.addEventListener('DOMContentLoaded', init);