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 — 西班牙语日记
-
-
+
+
+