792 lines
27 KiB
JavaScript
792 lines
27 KiB
JavaScript
// 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 '<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 + '">🗑 删除</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 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 = '<div style="font-size:0.75rem;color:var(--text-muted);font-style:italic">输入西语后显示单词</div>';
|
|
return;
|
|
}
|
|
container.innerHTML = vocab.map(function(v) {
|
|
var isFavorite = favoriteWords[v.word.toLowerCase()] ? 'active' : '';
|
|
var favBtn = BACKEND_MODE
|
|
? '<button class="favorite-btn ' + isFavorite + '" data-word="' + escHtml(v.word) + '" data-en="' + escHtml(v.english) + '" data-zh="' + escHtml(v.chinese) + '" title="收藏单词">★</button>'
|
|
: '';
|
|
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>' +
|
|
favBtn +
|
|
'</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) {
|
|
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 =
|
|
'<div class="stat-grid">' +
|
|
'<div class="stat-item"><div class="stat-value">' + stats.total + '</div><div class="stat-label">总日记</div></div>' +
|
|
'<div class="stat-item"><div class="stat-value">' + stats.streak + '</div><div class="stat-label">连续天数</div></div>' +
|
|
'<div class="stat-item"><div class="stat-value">' + stats.week + '</div><div class="stat-label">本周</div></div>' +
|
|
'<div class="stat-item"><div class="stat-value">' + stats.month + '</div><div class="stat-label">本月</div></div>' +
|
|
'</div>' +
|
|
'<div class="daily-stats">最近 7 天:' + escHtml(dailyHtml) + '</div>' +
|
|
'<div class="daily-stats">累计西语字符:' + stats.totalChars + '</div>';
|
|
}).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);
|