// Alias db.js functions const dbGetAllEntries = getAllEntries; const dbGetEntry = getEntry; const dbAddEntry = addEntry; const dbUpdateEntry = updateEntry; const dbDeleteEntry = deleteEntry; var BACKEND_MODE = false; var currentUser = null; var favoriteWords = {}; var currentTranslationSettings = { translator: 'local' }; var liveTranslationTimer = null; var liveTranslationSeq = 0; var selectionTimer = null; var selectionSeq = 0; function safeGetAllEntries() { if (BACKEND_MODE) return apiGetEntries(); return dbGetAllEntries(); } function safeAddEntry(entry) { if (BACKEND_MODE) return apiAddEntry(entry); return dbAddEntry(entry); } function safeUpdateEntry(id, entry) { if (BACKEND_MODE) return apiUpdateEntry(id, entry); return dbGetEntry(id).then(function(existing) { Object.assign(existing, entry); existing.updatedAt = new Date().toISOString(); return dbUpdateEntry(existing); }); } function safeDeleteEntry(id) { if (BACKEND_MODE) return apiDeleteEntry(id); return dbDeleteEntry(id); } 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 '
  • ' + '
    ' + escHtml(e.date) + '
    ' + '
    ' + escHtml(preview) + '
    ' + '
    ' + '' + '
    ' + '
  • '; }).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 shouldUseApiTranslation() { return BACKEND_MODE && (currentTranslationSettings.translator || 'local') !== 'local'; } function translateEditorToEnZh(text) { if (!shouldUseApiTranslation()) { var local = translateText(text); return Promise.resolve({ english: local.english, chinese: local.chinese, provider: 'local' }); } return Promise.all([ apiTranslate({ text: text, source: 'es', target: 'en' }), apiTranslate({ text: text, source: 'es', target: 'zh' }) ]).then(function(results) { if (!results[0].translatedText || !results[1].translatedText) { throw new Error('翻译接口未返回结果'); } return { english: results[0].translatedText, chinese: results[1].translatedText, provider: 'api' }; }).catch(function() { var local = translateText(text); return { english: local.english, chinese: local.chinese, provider: 'local' }; }); } function updateLiveTranslation() { var text = document.getElementById('entry-text').value; if (!text.trim()) { clearLiveTranslation(); return; } updateVocabulary(text); if (!shouldUseApiTranslation()) { var result = translateText(text); document.getElementById('editor-translate-en').textContent = result.english; document.getElementById('editor-translate-cn').textContent = result.chinese; return; } if (liveTranslationTimer) clearTimeout(liveTranslationTimer); liveTranslationTimer = setTimeout(function() { fetchLiveTranslation(text); }, 350); } function fetchLiveTranslation(text) { var seq = ++liveTranslationSeq; translateEditorToEnZh(text).then(function(result) { if (seq !== liveTranslationSeq) return; if (document.getElementById('entry-text').value !== text) return; document.getElementById('editor-translate-en').textContent = result.english; document.getElementById('editor-translate-cn').textContent = result.chinese; }); } function clearLiveTranslation() { liveTranslationSeq++; if (liveTranslationTimer) clearTimeout(liveTranslationTimer); selectionSeq++; if (selectionTimer) clearTimeout(selectionTimer); 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 = '
    输入西语后显示单词
    '; return; } container.innerHTML = vocab.map(function(v) { var isFavorite = favoriteWords[v.word.toLowerCase()] ? 'active' : ''; var favBtn = BACKEND_MODE ? '' : ''; return '
    ' + '' + escHtml(v.word) + '' + '' + '' + escHtml(v.english) + '' + '' + '' + escHtml(v.chinese) + '' + '' + favBtn + '
    '; }).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) { selectionSeq++; if (selectionTimer) clearTimeout(selectionTimer); el.style.display = 'none'; return; } if (!shouldUseApiTranslation()) { var result = translateSelection(selText); document.getElementById('selected-translation').textContent = result.english + ' / ' + result.chinese; el.style.display = 'block'; return; } if (selectionTimer) clearTimeout(selectionTimer); selectionTimer = setTimeout(function() { fetchSelectionTranslation(selText); }, 250); } function fetchSelectionTranslation(selText) { var seq = ++selectionSeq; translateEditorToEnZh(selText).then(function(result) { if (seq !== selectionSeq) return; var ta = document.getElementById('entry-text'); var current = ta.value.substring(ta.selectionStart, ta.selectionEnd).trim(); if (current !== selText) return; document.getElementById('selected-translation').textContent = result.english + ' / ' + result.chinese; document.getElementById('selection-translate').style.display = 'block'; }); } // ----- Event handlers ----- function selectEntry(id) { state.currentId = id; var entry = state.entries.find(function(e) { return e.id == id; }); var p = entry ? Promise.resolve(entry) : dbGetEntry(id); p.then(function(found) { if (!found) return; setEditor(found); renderTranslate(found); renderList(); }); } function saveCurrentEntry() { var date = document.getElementById('entry-date').value; var spanish = document.getElementById('entry-text').value.trim(); if (!spanish) { alert('请输入西语内容'); return; } var saveBtn = document.getElementById('btn-save'); saveBtn.textContent = '\u23F3 翻译中...'; translateEditorToEnZh(spanish).then(function(translation) { var data = { date: date, spanish: spanish, english: translation.english, chinese: translation.chinese }; var savePromise; if (state.currentId) { savePromise = safeUpdateEntry(state.currentId, data); } else if (BACKEND_MODE) { savePromise = apiAddEntry(data).then(function(entry) { state.currentId = entry.id; }); } 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; }); } return savePromise.then(function() { return safeGetAllEntries(); }).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); }); }).catch(function() { document.getElementById('btn-save').textContent = '\uD83D\uDCBE 保存'; }); } 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) { safeDeleteEntry(id).then(function() { if (state.currentId === id) clearEditor(); return safeGetAllEntries(); }).then(function(entries) { state.entries = entries; renderList(); hideDeleteModal(); }); } function saveTextareaSelection() { var ta = document.getElementById('entry-text'); savedSelStart = ta.selectionStart; savedSelEnd = ta.selectionEnd; } // ----- Chinese to Spanish helper ----- var helperDebounceTimer = null; var helperRequestSeq = 0; var helperOnlineResult = ''; function openHelperModal() { var ta = document.getElementById('entry-text'); savedSelStart = ta.selectionStart; savedSelEnd = ta.selectionEnd; helperOnlineResult = ''; if (helperDebounceTimer) clearTimeout(helperDebounceTimer); document.getElementById('helper-input').value = ''; document.getElementById('helper-result').textContent = ''; document.getElementById('helper-confirm').disabled = true; document.getElementById('helper-modal-overlay').classList.add('show'); setTimeout(function() { document.getElementById('helper-input').focus(); }, 0); } function closeHelperModal() { if (helperDebounceTimer) clearTimeout(helperDebounceTimer); helperRequestSeq++; document.getElementById('helper-modal-overlay').classList.remove('show'); } function updateHelperPreview() { var zh = document.getElementById('helper-input').value; helperOnlineResult = ''; if (helperDebounceTimer) clearTimeout(helperDebounceTimer); if (!zh.trim()) { document.getElementById('helper-result').textContent = ''; document.getElementById('helper-confirm').disabled = true; return; } var local = translateZhToEs(zh); if (shouldUseApiTranslation()) { document.getElementById('helper-result').textContent = local || '正在调用已配置的翻译接口...'; document.getElementById('helper-confirm').disabled = true; } else { document.getElementById('helper-result').textContent = local || (BACKEND_MODE ? '本地词典未收录' : '本地词典未收录,正在尝试在线翻译...'); document.getElementById('helper-confirm').disabled = !local; } if (shouldUseApiTranslation() || !BACKEND_MODE) { helperDebounceTimer = setTimeout(function() { translateZhOnline(zh); }, 600); } } function translateZhOnline(zh) { var seq = ++helperRequestSeq; var request; if (shouldUseApiTranslation()) { request = apiTranslate({ text: zh, source: 'zh', target: 'es' }).then(function(data) { return { translatedText: data.translatedText }; }); } else if (!BACKEND_MODE) { var url = 'https://api.mymemory.translated.net/get?q=' + encodeURIComponent(zh) + '&langpair=zh-CN|es'; request = fetch(url) .then(function(res) { return res.json(); }) .then(function(data) { return { translatedText: data && data.responseData && data.responseData.translatedText, quotaFinished: data && data.quotaFinished }; }); } else { var localOnly = translateZhToEs(zh); if (localOnly) { document.getElementById('helper-result').textContent = localOnly; document.getElementById('helper-confirm').disabled = false; } return; } request.then(function(result) { if (seq !== helperRequestSeq) return; var text = result && result.translatedText; if (text && !result.quotaFinished) { helperOnlineResult = text.trim(); document.getElementById('helper-result').textContent = helperOnlineResult; document.getElementById('helper-confirm').disabled = false; } else { var local = translateZhToEs(zh); if (local) { document.getElementById('helper-result').textContent = local; document.getElementById('helper-confirm').disabled = false; } else { document.getElementById('helper-result').textContent = '翻译接口未返回结果,且本地词典未收录'; } } }) .catch(function() { if (seq !== helperRequestSeq) return; var local = translateZhToEs(zh); if (local) { document.getElementById('helper-result').textContent = local; document.getElementById('helper-confirm').disabled = false; } else { document.getElementById('helper-result').textContent = '在线翻译暂不可用,且本地词典未收录'; } }); } function getHelperResult() { if (helperOnlineResult) return helperOnlineResult; return translateZhToEs(document.getElementById('helper-input').value); } function insertSpanishAtCursor(es) { var ta = document.getElementById('entry-text'); var start = savedSelStart || ta.selectionStart; var end = savedSelEnd || ta.selectionEnd; var before = ta.value.slice(0, start); var after = ta.value.slice(end); var prefix = ''; var suffix = ''; if (before.length > 0 && !/\s$/.test(before)) prefix = ' '; if (after.length > 0 && !/^\s/.test(after)) suffix = ' '; var insert = prefix + es + suffix; var newPos = start + insert.length; ta.value = before + insert + after; ta.focus(); ta.setSelectionRange(newPos, newPos); savedSelStart = savedSelEnd = newPos; updateLiveTranslation(); } function confirmHelperInsert() { var es = getHelperResult(); if (!es) return; insertSpanishAtCursor(es); closeHelperModal(); } // ----- 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); } // ----- Backend features ----- function renderUserArea() { if (!currentUser) return; document.getElementById('user-name').textContent = currentUser.displayName || currentUser.username; document.getElementById('btn-logout').style.display = ''; document.getElementById('btn-change-password').style.display = ''; if (currentUser.isAdmin) { document.getElementById('admin-link').style.display = ''; } } function loadBackendData() { safeGetAllEntries().then(function(entries) { state.entries = entries; renderList(); setEditor(null); renderTranslate(null); }); loadStats(); loadFavoriteWords(); loadSettingsForm(); } function loadLocalData() { safeGetAllEntries().then(function(entries) { state.entries = entries; renderList(); setEditor(null); renderTranslate(null); }); } function loadStats() { if (!BACKEND_MODE) { document.getElementById('stats-content').textContent = '登录后显示统计'; return; } apiGetStats().then(function(stats) { var dailyHtml = (stats.daily || []).map(function(d) { return d.date + ': ' + d.count + ' 篇'; }).join(' · '); document.getElementById('stats-content').innerHTML = '
    ' + '
    ' + stats.total + '
    总日记
    ' + '
    ' + stats.streak + '
    连续天数
    ' + '
    ' + stats.week + '
    本周
    ' + '
    ' + stats.month + '
    本月
    ' + '
    ' + '
    最近 7 天:' + escHtml(dailyHtml) + '
    ' + '
    累计西语字符:' + stats.totalChars + '
    '; }).catch(function() {}); } function loadFavoriteWords() { if (!BACKEND_MODE) return; apiGetVocabulary().then(function(items) { favoriteWords = {}; items.forEach(function(item) { favoriteWords[item.word.toLowerCase()] = item; }); updateLiveTranslation(); }).catch(function() {}); } function toggleFavorite(btn) { var word = btn.dataset.word; var key = word.toLowerCase(); if (favoriteWords[key]) { apiDeleteVocabulary(favoriteWords[key].id).then(function() { delete favoriteWords[key]; btn.classList.remove('active'); }).catch(function(err) { alert(err.message); }); } else { apiAddVocabulary({ word: word, english: btn.dataset.en || '', chinese: btn.dataset.zh || '', category: '' }).then(function(item) { favoriteWords[key] = item; btn.classList.add('active'); }).catch(function(err) { alert(err.message); }); } } function loadSettingsForm() { if (!BACKEND_MODE) return; apiGetTranslationSettings().then(function(settings) { currentTranslationSettings = settings; document.getElementById('translator-type').value = settings.translator || 'local'; document.getElementById('api-url').value = settings.api_url || ''; document.getElementById('api-key').value = settings.api_key || ''; document.getElementById('extra-config').value = JSON.stringify(settings.extra_config || {}, null, 2); }).catch(function() {}); } function openSettingsModal() { document.getElementById('settings-modal-overlay').classList.add('show'); } function closeSettingsModal() { document.getElementById('settings-modal-overlay').classList.remove('show'); } function openPasswordModal() { document.getElementById('password-msg').textContent = ''; document.getElementById('password-old').value = ''; document.getElementById('password-new').value = ''; document.getElementById('password-confirm').value = ''; document.getElementById('password-modal-overlay').classList.add('show'); setTimeout(function() { document.getElementById('password-old').focus(); }, 0); } function closePasswordModal() { document.getElementById('password-modal-overlay').classList.remove('show'); } function savePasswordModal() { var oldPassword = document.getElementById('password-old').value; var newPassword = document.getElementById('password-new').value; var confirmPassword = document.getElementById('password-confirm').value; var msg = document.getElementById('password-msg'); if (!oldPassword) { msg.textContent = '请输入当前密码'; return; } if (newPassword.length < 6) { msg.textContent = '新密码至少 6 位'; return; } if (newPassword !== confirmPassword) { msg.textContent = '两次输入的新密码不一致'; return; } apiChangePassword({ oldPassword: oldPassword, newPassword: newPassword }).then(function() { closePasswordModal(); alert('密码已修改'); }).catch(function(err) { msg.textContent = err.message || '修改密码失败'; }); } function saveSettingsModal() { var extra = {}; var rawExtra = document.getElementById('extra-config').value.trim(); if (rawExtra) { try { extra = JSON.parse(rawExtra); } catch (e) { alert('扩展配置必须是合法 JSON'); return; } } apiSaveTranslationSettings({ translator: document.getElementById('translator-type').value, api_url: document.getElementById('api-url').value.trim(), api_key: document.getElementById('api-key').value.trim(), extra_config: extra }).then(function(saved) { currentTranslationSettings = saved; closeSettingsModal(); alert('翻译设置已保存'); }).catch(function(err) { alert(err.message); }); } function handleLogout() { apiLogout().then(function() { location.href = 'login.html'; }).catch(function() { location.href = 'login.html'; }); } // ----- Initialization ----- function bindAppEvents() { 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.getElementById('btn-helper').addEventListener('click', openHelperModal); document.getElementById('helper-cancel').addEventListener('click', closeHelperModal); document.getElementById('helper-confirm').addEventListener('click', confirmHelperInsert); document.getElementById('helper-input').addEventListener('input', updateHelperPreview); document.getElementById('btn-settings').addEventListener('click', openSettingsModal); document.getElementById('settings-cancel').addEventListener('click', closeSettingsModal); document.getElementById('settings-save').addEventListener('click', saveSettingsModal); document.getElementById('btn-change-password').addEventListener('click', openPasswordModal); document.getElementById('password-cancel').addEventListener('click', closePasswordModal); document.getElementById('password-save').addEventListener('click', savePasswordModal); document.getElementById('btn-logout').addEventListener('click', handleLogout); document.querySelectorAll('#mobile-nav button').forEach(function(btn) { btn.addEventListener('click', function() { document.body.dataset.view = btn.dataset.view; document.querySelectorAll('#mobile-nav button').forEach(function(b) { b.classList.toggle('active', b === btn); }); }); }); document.querySelectorAll('.btn-speech').forEach(function(btn) { btn.addEventListener('click', function() { handleSpeech(btn.dataset.lang); }); }); document.getElementById('entry-text').addEventListener('input', updateLiveTranslation); document.getElementById('entry-text').addEventListener('mouseup', function(e) { handleSelection(); saveTextareaSelection(); }); document.getElementById('entry-text').addEventListener('keyup', function(e) { handleSelection(); saveTextareaSelection(); }); document.getElementById('editor-vocab').addEventListener('click', function(e) { var fav = e.target.closest('.favorite-btn'); if (fav) { toggleFavorite(fav); return; } var btn = e.target.closest('.vocab-speech'); if (btn) handleVocabSpeech(btn.dataset.word, btn.dataset.lang); }); 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'); } }); 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); }); 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(); }); } function startApp() { if (location.protocol === 'file:') { BACKEND_MODE = false; loadLocalData(); return; } apiGetMe().then(function(me) { BACKEND_MODE = true; currentUser = me; renderUserArea(); loadBackendData(); }).catch(function(err) { if (err && err.status === 401) { location.href = 'login.html'; return; } BACKEND_MODE = false; loadLocalData(); }); } function init() { bindAppEvents(); startApp(); } document.addEventListener('DOMContentLoaded', init);