From 3eabdc9f32f34863d68886dd36d18439d79e9a02 Mon Sep 17 00:00:00 2001 From: guoqingshan Date: Sat, 1 Aug 2026 21:41:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=BB=9F=E4=B8=80=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E7=BF=BB=E8=AF=91=E8=B0=83=E7=94=A8=E8=B5=B0=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + app.js | 224 +++++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 171 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 3b596d0..e656de5 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ server/data/spanish_diary.db - 用户日记数据隔离 - 共享知识库 - 每用户翻译配置(本地 / MyMemory / 自定义接口) +- 实时翻译、选中翻译、保存和中译西统一调用每用户翻译配置 - 每用户日记统计 - 每用户单词收藏 - 手机、平板、桌面响应式适配 diff --git a/app.js b/app.js index ab7c239..78cbbc8 100644 --- a/app.js +++ b/app.js @@ -8,6 +8,11 @@ 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(); @@ -100,20 +105,67 @@ function clearEditor() { // ----- 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; } - var result = translateText(text); - document.getElementById('editor-translate-en').textContent = result.english; - document.getElementById('editor-translate-cn').textContent = result.chinese; - 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 = ''; @@ -157,14 +209,36 @@ 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) { + 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'; - } else { - el.style.display = 'none'; + 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 ----- @@ -186,45 +260,50 @@ function saveCurrentEntry() { var spanish = document.getElementById('entry-text').value.trim(); if (!spanish) { alert('请输入西语内容'); return; } - var translation = translateText(spanish); - 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(), + 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, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString() + chinese: translation.chinese }; - savePromise = dbAddEntry(entry).then(function() { - state.currentId = entry.id; - }); - } + 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; + }); + } - 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); + 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 保存'; }); } @@ -300,26 +379,61 @@ function updateHelperPreview() { return; } var local = translateZhToEs(zh); - document.getElementById('helper-result').textContent = local || '本地词典未收录,正在尝试在线翻译...'; - document.getElementById('helper-confirm').disabled = !local; - helperDebounceTimer = setTimeout(function() { - translateZhOnline(zh); - }, 600); + 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 url = 'https://api.mymemory.translated.net/get?q=' + - encodeURIComponent(zh) + '&langpair=zh-CN|es'; - fetch(url) - .then(function(res) { return res.json(); }) - .then(function(data) { + 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 = data && data.responseData && data.responseData.translatedText; - if (text && !data.quotaFinished) { + 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() { @@ -463,6 +577,7 @@ function toggleFavorite(btn) { 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 || ''; @@ -535,7 +650,8 @@ function saveSettingsModal() { api_url: document.getElementById('api-url').value.trim(), api_key: document.getElementById('api-key').value.trim(), extra_config: extra - }).then(function() { + }).then(function(saved) { + currentTranslationSettings = saved; closeSettingsModal(); alert('翻译设置已保存'); }).catch(function(err) { alert(err.message); });