diff --git a/README.md b/README.md index c0f76e7..22b0825 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ - 🌐 实时翻译:输入西语自动显示英语和中文对照 - 🔊 发音朗读:支持西语、英语、中文三种语言(Web Speech API) - 📖 单词本:自动提取输入中的关键词并给出解释,支持点击发音 +- 📕 专用单词本页面:支持搜索、分类筛选、添加、编辑、删除和三语朗读 - 📚 语法知识库:按教材整理的 40 个语音和语法知识点 ## 使用 @@ -84,4 +85,5 @@ server/data/spanish_diary.db - LibreTranslate 等自定义接口默认使用 `zh-hans` 表示简体中文,可通过扩展配置 `zh_code` 覆盖 - 每用户日记统计 - 每用户单词收藏 +- 移动端左上菜单、编辑区优先、左滑显示翻译 - 手机、平板、桌面响应式适配 diff --git a/api.js b/api.js index 7aec478..772bf29 100644 --- a/api.js +++ b/api.js @@ -72,6 +72,10 @@ function apiAddVocabulary(data) { return apiFetch('/api/vocabulary', { method: 'POST', body: data }); } +function apiUpdateVocabulary(id, data) { + return apiFetch('/api/vocabulary/' + id, { method: 'PUT', body: data }); +} + function apiDeleteVocabulary(id) { return apiFetch('/api/vocabulary/' + id, { method: 'DELETE' }); } diff --git a/app.js b/app.js index 3f5515f..db5cd9b 100644 --- a/app.js +++ b/app.js @@ -8,6 +8,7 @@ const dbDeleteEntry = deleteEntry; var BACKEND_MODE = false; var currentUser = null; var favoriteWords = {}; +var LOCAL_WB_KEY = 'spanish_diary_wordbook'; var currentTranslationSettings = { translator: 'local' }; var liveTranslationTimer = null; var liveTranslationSeq = 0; @@ -181,9 +182,6 @@ function updateVocabulary(text) { } container.innerHTML = vocab.map(function(v) { var isFavorite = favoriteWords[v.word.toLowerCase()] ? 'active' : ''; - var favBtn = BACKEND_MODE - ? '' - : ''; return '
' + '' + escHtml(v.word) + '' + '' + @@ -191,7 +189,7 @@ function updateVocabulary(text) { '' + '' + escHtml(v.chinese) + '' + '' + - favBtn + + '' + '
'; }).join(''); } @@ -252,6 +250,7 @@ function selectEntry(id) { setEditor(found); renderTranslate(found); renderList(); + showEditorView(); }); } @@ -312,13 +311,48 @@ function saveCurrentEntry() { function newEntry() { clearEditor(); document.getElementById('entry-date').value = todayStr(); + showEditorView(); document.getElementById('entry-text').focus(); } function showDiaryListView() { document.body.dataset.view = 'list'; - document.querySelectorAll('#mobile-nav button').forEach(function(btn) { - btn.classList.toggle('active', btn.dataset.view === 'list'); + closeMobileMenu(); +} + +function showEditorView() { + document.body.dataset.view = 'editor'; + closeMobileMenu(); +} + +function showTranslationView() { + document.body.dataset.view = 'translate'; + closeMobileMenu(); +} + +function hideTranslationView() { + document.body.dataset.view = 'editor'; +} + +function showStatsView() { + document.body.dataset.view = 'stats'; + closeMobileMenu(); +} + +function openMobileMenu() { + refreshMobileMenuAuth(); + document.getElementById('mobile-menu-overlay').classList.add('show'); +} + +function closeMobileMenu() { + document.getElementById('mobile-menu-overlay').classList.remove('show'); +} + +function refreshMobileMenuAuth() { + var loggedIn = !!currentUser; + ['password', 'settings', 'logout'].forEach(function(action) { + var el = document.querySelector('#mobile-menu [data-menu="' + action + '"]'); + if (el) el.style.display = loggedIn ? '' : 'none'; }); } @@ -507,6 +541,7 @@ function renderUserArea() { if (currentUser.isAdmin) { document.getElementById('admin-link').style.display = ''; } + refreshMobileMenuAuth(); } function loadBackendData() { @@ -522,6 +557,7 @@ function loadBackendData() { } function loadLocalData() { + loadLocalFavoriteWords(); safeGetAllEntries().then(function(entries) { state.entries = entries; renderList(); @@ -530,6 +566,25 @@ function loadLocalData() { }); } +function getLocalWordbook() { + try { + return JSON.parse(localStorage.getItem(LOCAL_WB_KEY) || '[]'); + } catch (e) { + return []; + } +} + +function saveLocalWordbook(items) { + localStorage.setItem(LOCAL_WB_KEY, JSON.stringify(items)); +} + +function loadLocalFavoriteWords() { + favoriteWords = {}; + getLocalWordbook().forEach(function(item) { + favoriteWords[String(item.word || '').toLowerCase()] = item; + }); +} + function loadStats() { if (!BACKEND_MODE) { document.getElementById('stats-content').textContent = '登录后显示统计'; @@ -565,6 +620,30 @@ function loadFavoriteWords() { function toggleFavorite(btn) { var word = btn.dataset.word; var key = word.toLowerCase(); + if (!BACKEND_MODE) { + var localItems = getLocalWordbook(); + if (favoriteWords[key]) { + localItems = localItems.filter(function(item) { + return String(item.word || '').toLowerCase() !== key; + }); + delete favoriteWords[key]; + btn.classList.remove('active'); + } else { + var localItem = { + id: 'local-' + Date.now(), + word: word, + english: btn.dataset.en || '', + chinese: btn.dataset.zh || '', + category: '', + created_at: new Date().toISOString() + }; + localItems.unshift(localItem); + favoriteWords[key] = localItem; + btn.classList.add('active'); + } + saveLocalWordbook(localItems); + return; + } if (favoriteWords[key]) { apiDeleteVocabulary(favoriteWords[key].id).then(function() { delete favoriteWords[key]; @@ -688,6 +767,40 @@ function bindAppEvents() { }); document.getElementById('btn-new').addEventListener('click', newEntry); + document.getElementById('btn-back-list').addEventListener('click', showEditorView); + document.getElementById('menu-btn').addEventListener('click', openMobileMenu); + document.getElementById('translate-btn').addEventListener('click', function() { + if (document.body.dataset.view === 'translate') { + hideTranslationView(); + } else { + showTranslationView(); + } + }); + document.getElementById('mobile-menu-overlay').addEventListener('click', function(e) { + if (e.target === this) closeMobileMenu(); + }); + document.getElementById('mobile-menu').addEventListener('click', function(e) { + var item = e.target.closest('[data-menu]'); + if (!item) return; + var action = item.dataset.menu; + if (action === 'diary') { + showDiaryListView(); + } else if (action === 'stats') { + showStatsView(); + } else if (action === 'password') { + closeMobileMenu(); + openPasswordModal(); + } else if (action === 'settings') { + closeMobileMenu(); + openSettingsModal(); + } else if (action === 'logout') { + closeMobileMenu(); + handleLogout(); + } else { + closeMobileMenu(); + } + }); + document.getElementById('btn-save').addEventListener('click', saveCurrentEntry); document.getElementById('btn-helper').addEventListener('click', openHelperModal); document.getElementById('helper-cancel').addEventListener('click', closeHelperModal); @@ -702,15 +815,6 @@ function bindAppEvents() { 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); }); }); @@ -769,6 +873,39 @@ function bindAppEvents() { document.addEventListener('keydown', function(e) { if (e.ctrlKey && e.key === 'Enter') saveCurrentEntry(); }); + + var swipeStartX = null; + var swipeStartY = null; + var swipeActive = false; + var appElement = document.getElementById('app'); + appElement.addEventListener('touchstart', function(e) { + if (e.touches.length !== 1) return; + swipeStartX = e.touches[0].clientX; + swipeStartY = e.touches[0].clientY; + swipeActive = true; + }, { passive: true }); + appElement.addEventListener('touchmove', function(e) { + if (!swipeActive || e.touches.length !== 1) return; + var dx = e.touches[0].clientX - swipeStartX; + var dy = e.touches[0].clientY - swipeStartY; + if (Math.abs(dx) > 30 && Math.abs(dx) > Math.abs(dy)) { + e.preventDefault(); + } + }, { passive: false }); + appElement.addEventListener('touchend', function(e) { + if (!swipeActive) return; + var dx = e.changedTouches[0].clientX - swipeStartX; + var dy = e.changedTouches[0].clientY - swipeStartY; + var view = document.body.dataset.view; + if (dx < -55 && Math.abs(dx) > Math.abs(dy) * 1.2 && view === 'editor') { + showTranslationView(); + } else if (dx > 55 && Math.abs(dx) > Math.abs(dy) * 1.2 && (view === 'translate' || view === 'stats')) { + hideTranslationView(); + } + swipeStartX = null; + swipeStartY = null; + swipeActive = false; + }); } function startApp() { diff --git a/index.html b/index.html index 280da13..33758d3 100644 --- a/index.html +++ b/index.html @@ -6,17 +6,29 @@ Mi Diario — 西班牙语日记 - - + +
+ + Mi Diario + +
+
+
+
Mi Diario
+ + 📖 单词本 + 📚 语法速查 + + + + +
+