// 多宫格导航页面功能 - 炫酷有氛围版本 class ToolboxHome { constructor() { this.currentTheme = 0; this.openInModal = true; this.localBgMode = false; this.localBgFiles = []; this.localBgIndex = 0; this.localRotationInterval = null; // 从 localStorage 恢复状态 try { this.localBgMode = localStorage.getItem('tb_home_local_bg') === '1'; this.openInModal = localStorage.getItem('tb_home_open_mode') !== '0'; } catch {} this.themes = [ { name: '夜空星轨', background: 'https://images.unsplash.com/photo-1534796636912-3b95b3ab5986?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80', effect: 'meteor', cardGradient: 'linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(168, 85, 247, 0.2))', textColor: '#f8fafc' }, { name: '蓝天白云', background: 'https://images.unsplash.com/photo-1501594907352-04cda38ebc29?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80', effect: 'meteor', cardGradient: 'linear-gradient(135deg, rgba(14, 165, 233, 0.2), rgba(56, 189, 248, 0.2))', textColor: '#1e293b' }, { name: '治愈森林', background: 'https://images.unsplash.com/photo-1448375240586-882707db888b?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80', effect: 'meteor', cardGradient: 'linear-gradient(135deg, rgba(34, 197, 94, 0.2), rgba(132, 204, 22, 0.2))', textColor: '#f8fafc' }, { name: '运动风', background: 'https://images.unsplash.com/photo-1521412644187-9d2e0ad0a2f5?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80', effect: 'meteor', cardGradient: 'linear-gradient(135deg, rgba(234, 88, 12, 0.24), rgba(59, 130, 246, 0.24))', textColor: '#ffffff' }, { name: '科技霓虹', background: 'https://images.unsplash.com/photo-1496307042754-b4aa456c4a2d?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80', effect: 'meteor', cardGradient: 'linear-gradient(135deg, rgba(99, 102, 241, 0.26), rgba(236, 72, 153, 0.26))', textColor: '#ffffff' }, { name: '商务简约', background: 'https://images.unsplash.com/photo-1487015307662-6ce621f0df3f?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80', effect: 'meteor', cardGradient: 'linear-gradient(135deg, rgba(226, 232, 240, 0.35), rgba(148, 163, 184, 0.35))', textColor: '#0b1220' }, { name: '暖色日落', background: 'https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80', effect: 'meteor', cardGradient: 'linear-gradient(135deg, rgba(244, 114, 182, 0.25), rgba(251, 146, 60, 0.25))', textColor: '#ffffff' }, { name: '冷色极夜', background: 'https://images.unsplash.com/photo-1469474968028-56623f02e42e?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80', effect: 'meteor', cardGradient: 'linear-gradient(135deg, rgba(14, 165, 233, 0.25), rgba(5, 150, 105, 0.25))', textColor: '#e6f7ff' } ]; this.highFreqTools = []; this.lowFreqTools = []; this.tools = []; // 保留向后兼容 this.categories = []; this.bookmarks = []; this.availableIcons = []; this.collapsedCategories = {}; this.searchQuery = ''; this.adminVisible = false; this.editingCategoryCode = ''; this.editingBookmarkId = ''; this.iconPickerTarget = null; this.activeReminderKey = ''; this.reminderQueue = []; this.init(); } init() { this.createDOM(); this.setupEventListeners(); this.applyTheme(this.currentTheme); this.startEffects(); this.startBackgroundRotation(); this.loadIcons(); this.loadBookmarks(); this.loadTools(); this.loadFitnessDiaryReminder(); this.loadWeeklyReportReminder(); } createDOM() { // 创建背景容器 const backgroundContainer = document.createElement('div'); backgroundContainer.className = 'background-container'; // 创建背景图片 this.themes.forEach((theme, index) => { const bgImg = document.createElement('img'); bgImg.className = 'background-image'; bgImg.alt = theme.name; if (index === 0) bgImg.classList.add('active'); backgroundContainer.appendChild(bgImg); }); // 创建背景遮罩 const overlay = document.createElement('div'); overlay.className = 'background-overlay'; backgroundContainer.appendChild(overlay); // 创建特效容器 const effectsContainer = document.createElement('div'); effectsContainer.className = 'effects-container'; // 创建主容器 const mainContainer = document.createElement('div'); mainContainer.className = 'main-container'; // 创建标题区域 const header = document.createElement('div'); header.className = 'header'; header.innerHTML = ``; //

Yang's Toolbox

//

个人工具箱入口 - 简洁高效

const tabBar = document.createElement('div'); tabBar.className = 'tab-bar'; tabBar.innerHTML = ` `; const tabPages = document.createElement('div'); tabPages.className = 'tab-pages'; // 高频 Tools 页面 const highToolsPage = document.createElement('div'); highToolsPage.className = 'tab-page active'; highToolsPage.dataset.tab = 'high'; const highToolGrid = document.createElement('div'); highToolGrid.className = 'tool-grid'; highToolGrid.id = 'tool-grid-high'; highToolsPage.appendChild(highToolGrid); // 低频 Tools 页面 const lowToolsPage = document.createElement('div'); lowToolsPage.className = 'tab-page'; lowToolsPage.dataset.tab = 'low'; const lowToolGrid = document.createElement('div'); lowToolGrid.className = 'tool-grid'; lowToolGrid.id = 'tool-grid-low'; lowToolsPage.appendChild(lowToolGrid); // 书签导航页面 const bookmarksPage = document.createElement('div'); bookmarksPage.className = 'tab-page'; bookmarksPage.dataset.tab = 'bookmarks'; const bookmarkToolbar = document.createElement('div'); bookmarkToolbar.className = 'bookmark-toolbar'; bookmarkToolbar.innerHTML = ` `; const bookmarkSections = document.createElement('div'); bookmarkSections.className = 'bookmark-sections'; const bookmarkEmpty = document.createElement('div'); bookmarkEmpty.className = 'bookmark-empty'; bookmarkEmpty.textContent = '暂无书签'; const bookmarkAdmin = document.createElement('div'); bookmarkAdmin.className = 'bookmark-admin'; bookmarkAdmin.innerHTML = `
书签后台维护
分类维护
书签维护
-
`; bookmarksPage.appendChild(bookmarkToolbar); bookmarksPage.appendChild(bookmarkSections); bookmarksPage.appendChild(bookmarkEmpty); bookmarksPage.appendChild(bookmarkAdmin); tabPages.appendChild(highToolsPage); tabPages.appendChild(lowToolsPage); tabPages.appendChild(bookmarksPage); // 组装页面 mainContainer.appendChild(header); mainContainer.appendChild(tabBar); mainContainer.appendChild(tabPages); document.body.innerHTML = ''; document.body.appendChild(backgroundContainer); document.body.appendChild(effectsContainer); document.body.appendChild(mainContainer); // 顶部右侧滑块(弹窗/本地切换) const controls = document.createElement('div'); controls.className = 'top-right-controls'; controls.innerHTML = ` `; document.body.appendChild(controls); // 换背景按钮容器(图标按钮 + 本地模式开关,水平排列,放底部) const bgBtnWrapper = document.createElement('div'); bgBtnWrapper.className = 'bg-btn-wrapper'; bgBtnWrapper.innerHTML = ` `; document.body.appendChild(bgBtnWrapper); // 根据 localStorage 状态初始化背景模式 if (this.localBgMode) { this.startLocalBgRotation(); } else { this.ensureBgLoaded(0, true); this.startThemeRotation(); } const iconPicker = document.createElement('div'); iconPicker.className = 'icon-picker'; iconPicker.innerHTML = `
选择图标
`; document.body.appendChild(iconPicker); const fitnessDiaryReminder = document.createElement('div'); fitnessDiaryReminder.className = 'fitness-diary-reminder'; fitnessDiaryReminder.innerHTML = `
`; document.body.appendChild(fitnessDiaryReminder); const weeklyReportReminder = document.createElement('div'); weeklyReportReminder.className = 'weekly-report-reminder'; weeklyReportReminder.innerHTML = `
`; document.body.appendChild(weeklyReportReminder); this.tabBar = tabBar; this.highToolGrid = highToolGrid; this.lowToolGrid = lowToolGrid; this.toolsPage = highToolsPage; this.bookmarksPage = bookmarksPage; this.tabPages = [highToolsPage, lowToolsPage, bookmarksPage]; this.bookmarkContainer = bookmarkSections; this.bookmarkEmpty = bookmarkEmpty; this.bookmarkAdmin = bookmarkAdmin; this.categoryForm = bookmarkAdmin.querySelector('#category-form'); this.bookmarkForm = bookmarkAdmin.querySelector('#bookmark-form'); this.categoryListEl = bookmarkAdmin.querySelector('#category-list'); this.bookmarkListEl = bookmarkAdmin.querySelector('#bookmark-list'); this.bookmarkSearchInput = bookmarkToolbar.querySelector('#bookmark-search'); this.bookmarkCreatedAtEl = bookmarkAdmin.querySelector('[data-field="created_at"]'); this.iconPreviewEl = bookmarkAdmin.querySelector('[data-field="icon-preview"]'); this.iconPicker = iconPicker; this.iconPickerGrid = iconPicker.querySelector('.icon-picker-grid'); this.fitnessDiaryReminder = fitnessDiaryReminder; this.fitnessDiaryReminderText = fitnessDiaryReminder.querySelector('#fitness-diary-reminder-text'); this.fitnessDiaryReminderDates = fitnessDiaryReminder.querySelector('#fitness-diary-reminder-dates'); this.weeklyReportReminder = weeklyReportReminder; this.weeklyReportReminderText = weeklyReportReminder.querySelector('#weekly-report-reminder-text'); this.weeklyReportReminderDates = weeklyReportReminder.querySelector('#weekly-report-reminder-dates'); } async loadIcons() { try { const iconsRes = await fetch('/common/icons_list.json?t=' + Date.now()); if (iconsRes.ok) { const data = await iconsRes.json(); if (Array.isArray(data)) this.availableIcons = data; } } catch {} this.renderIconPicker(); } async loadBookmarks() { try { const res = await fetch('/api/nav_bookmarks/state?t=' + Date.now()); if (!res.ok) return; const data = await res.json(); if (!data || !data.ok) return; this.categories = Array.isArray(data.categories) ? data.categories : []; this.bookmarks = Array.isArray(data.bookmarks) ? data.bookmarks : []; this.renderBookmarks(); this.renderAdmin(); } catch {} } renderIconPicker() { if (!this.iconPickerGrid) return; this.iconPickerGrid.innerHTML = ''; this.availableIcons.forEach(name => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'icon-picker-item'; btn.dataset.icon = name; const img = document.createElement('img'); img.src = `/icons/16X16PNG/${name}`; img.alt = name; btn.appendChild(img); this.iconPickerGrid.appendChild(btn); }); } setActiveTab(tab) { if (!this.tabBar) return; this.tabBar.querySelectorAll('.tab-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.tab === tab); }); (this.tabPages || []).forEach(page => { if (!page) return; page.classList.toggle('active', page.dataset.tab === tab); }); } async loadTools() { try { const res = await fetch('/api/tools?t=' + Date.now()); if (!res.ok) return; const data = await res.json(); const tools = Array.isArray(data.tools) ? data.tools : []; this.highFreqTools = tools.filter(t => t.type === 'high'); this.lowFreqTools = tools.filter(t => t.type === 'low'); this.renderToolGrids(); } catch (err) { console.error('加载 tools.json 失败:', err); } } renderToolGrids() { const renderGrid = (gridEl, tools) => { if (!gridEl) return; gridEl.innerHTML = ''; tools.forEach(tool => { const card = document.createElement('a'); card.className = 'tool-card'; card.href = tool.href; card.innerHTML = `
${tool.icon}
${tool.name}
`; gridEl.appendChild(card); }); }; renderGrid(this.highToolGrid, this.highFreqTools); renderGrid(this.lowToolGrid, this.lowFreqTools); } openAdmin() { this.adminVisible = true; if (this.bookmarkAdmin) this.bookmarkAdmin.classList.add('active'); if (this.bookmarksPage) this.bookmarksPage.classList.add('admin-visible'); this.renderAdmin(); this.bookmarkAdmin.scrollIntoView({ behavior: 'smooth', block: 'start' }); } closeAdmin() { this.adminVisible = false; if (this.bookmarkAdmin) this.bookmarkAdmin.classList.remove('active'); if (this.bookmarksPage) this.bookmarksPage.classList.remove('admin-visible'); } getIconUrl(icon) { const iconSrc = String(icon || '').trim(); if (!iconSrc) { if (this.availableIcons.length > 0) return `/icons/16X16PNG/${this.availableIcons[0]}`; return ''; } if (iconSrc.includes('/')) return iconSrc; return `/icons/16X16PNG/${iconSrc}`; } renderBookmarks() { if (!this.bookmarkContainer) return; const query = String(this.searchQuery || '').trim().toLowerCase(); const categoryMap = new Map(); this.categories.forEach(c => categoryMap.set(String(c.code), c)); const buckets = {}; this.bookmarks.forEach(b => { const code = String(b.category_code || ''); if (!buckets[code]) buckets[code] = []; buckets[code].push(b); }); const categories = this.categories.slice().sort((a, b) => { const sa = Number(a.sort_order || 0); const sb = Number(b.sort_order || 0); if (sa !== sb) return sa - sb; return String(a.code).localeCompare(String(b.code)); }); if (!categoryMap.has('') && buckets['']) { categories.push({ code: '', name: '未分类', sort_order: 9999 }); } this.bookmarkContainer.innerHTML = ''; let hasAny = false; categories.forEach(cat => { const list = (buckets[String(cat.code)] || []).slice().sort((a, b) => { const sa = Number(a.sort_order || 0); const sb = Number(b.sort_order || 0); if (sa !== sb) return sa - sb; return String(a.created_at || '').localeCompare(String(b.created_at || '')); }); const filtered = query ? list.filter(item => { const name = String(item.name || '').toLowerCase(); const url = String(item.url || '').toLowerCase(); const remark = String(item.remark || '').toLowerCase(); return name.includes(query) || url.includes(query) || remark.includes(query); }) : list; if (!filtered.length) return; hasAny = true; const section = document.createElement('div'); section.className = 'bookmark-section'; const header = document.createElement('div'); header.className = 'bookmark-section-header'; const title = document.createElement('div'); title.className = 'bookmark-section-title'; title.textContent = `${cat.name || '未分类'} (${filtered.length})`; const toggle = document.createElement('button'); toggle.type = 'button'; toggle.className = 'category-toggle'; const isCollapsed = !!this.collapsedCategories[String(cat.code)]; toggle.textContent = isCollapsed ? '+' : '-'; toggle.dataset.code = String(cat.code); header.appendChild(title); header.appendChild(toggle); const grid = document.createElement('div'); grid.className = 'bookmark-grid'; if (isCollapsed) section.classList.add('collapsed'); filtered.forEach(item => { const link = document.createElement('a'); link.className = 'bookmark-item'; link.href = item.url || '#'; link.target = '_blank'; const iconUrl = this.getIconUrl(item.icon); if (iconUrl) { const img = document.createElement('img'); img.src = iconUrl; img.className = 'bookmark-icon'; img.alt = item.name || ''; link.appendChild(img); } const span = document.createElement('span'); span.className = 'bookmark-text'; span.textContent = item.name || '未命名'; link.appendChild(span); const tooltip = String(item.remark || item.name || '').trim(); if (tooltip) link.title = tooltip; link.addEventListener('click', (e) => { if (this.openInModal) { e.preventDefault(); const url = link.getAttribute('href') || '/'; window.open(url, '_blank'); } }); grid.appendChild(link); }); section.appendChild(header); section.appendChild(grid); this.bookmarkContainer.appendChild(section); }); if (this.bookmarkEmpty) { this.bookmarkEmpty.style.display = hasAny ? 'none' : 'block'; } } renderAdmin() { if (!this.bookmarkAdmin) return; const categorySelect = this.bookmarkForm ? this.bookmarkForm.querySelector('select[name="category_code"]') : null; if (categorySelect) { categorySelect.innerHTML = ''; const emptyOpt = document.createElement('option'); emptyOpt.value = ''; emptyOpt.textContent = '未分类'; categorySelect.appendChild(emptyOpt); this.categories.forEach(cat => { const opt = document.createElement('option'); opt.value = cat.code; opt.textContent = `${cat.code} - ${cat.name}`; categorySelect.appendChild(opt); }); } if (this.categoryListEl) { this.categoryListEl.innerHTML = ''; this.categories.forEach(cat => { const row = document.createElement('div'); row.className = 'admin-row'; row.innerHTML = `
${cat.code} - ${cat.name}
排序 ${cat.sort_order || 0}
`; this.categoryListEl.appendChild(row); }); } if (this.bookmarkListEl) { this.bookmarkListEl.innerHTML = ''; this.bookmarks.forEach(item => { const cat = this.categories.find(c => String(c.code) === String(item.category_code)) || { name: '未分类' }; const row = document.createElement('div'); row.className = 'admin-row'; row.innerHTML = `
${item.name || '未命名'}
${cat.name} · 排序 ${item.sort_order || 0}
`; this.bookmarkListEl.appendChild(row); }); } this.updateIconPreview(); } setupEventListeners() { // 换背景按钮事件 document.querySelector('.change-background-btn').addEventListener('click', () => { this.nextTheme(); }); // 工具卡片点击效果 + 打开方式(委托方式,支持动态加载) document.querySelector('.tab-pages').addEventListener('click', (e) => { const card = e.target.closest('.tool-card'); if (!card) return; card.style.transform = 'scale(0.95)'; setTimeout(() => { card.style.transform = ''; }, 150); if (this.openInModal) { e.preventDefault(); window.open(card.getAttribute('href') || '/', '_blank'); } }); // 键盘快捷键 document.addEventListener('keydown', (e) => { if (e.key === 'b' || e.key === 'B') { e.preventDefault(); this.nextTheme(); } }); // 滑块切换事件 const localBgSwitch = document.getElementById('local-bg-switch'); const openModeSwitch = document.getElementById('open-mode-switch'); if (localBgSwitch) { localBgSwitch.checked = this.localBgMode; localBgSwitch.addEventListener('change', () => { this.localBgMode = !!localBgSwitch.checked; try { localStorage.setItem('tb_home_local_bg', this.localBgMode ? '1' : '0'); } catch {} if (this.localBgMode) { this.stopThemeRotation(); this.startLocalBgRotation(); } else { this.stopLocalBgRotation(); this.ensureBgLoaded(this.currentTheme, true); this.startThemeRotation(); } }); } if (openModeSwitch) { openModeSwitch.checked = this.openInModal; openModeSwitch.addEventListener('change', () => { this.openInModal = !!openModeSwitch.checked; try { localStorage.setItem('tb_home_open_mode', this.openInModal ? '1' : '0'); } catch {} }); } if (this.tabBar) { this.tabBar.addEventListener('click', (e) => { const btn = e.target.closest('.tab-btn'); if (!btn) return; const tab = btn.dataset.tab; if (tab) this.setActiveTab(tab); }); } const clearBtn = document.getElementById('bookmark-search-clear'); if (this.bookmarkSearchInput) { this.bookmarkSearchInput.addEventListener('input', () => { this.searchQuery = this.bookmarkSearchInput.value; this.renderBookmarks(); }); } if (clearBtn) { clearBtn.addEventListener('click', () => { if (this.bookmarkSearchInput) this.bookmarkSearchInput.value = ''; this.searchQuery = ''; this.renderBookmarks(); }); } const adminBtn = document.getElementById('bookmark-admin-btn'); if (adminBtn) { adminBtn.addEventListener('click', () => { this.openAdmin(); }); } const adminClose = document.getElementById('bookmark-admin-close'); if (adminClose) { adminClose.addEventListener('click', () => { this.closeAdmin(); }); } if (this.bookmarkContainer) { this.bookmarkContainer.addEventListener('click', (e) => { const toggle = e.target.closest('.category-toggle'); if (!toggle) return; const code = String(toggle.dataset.code || ''); this.collapsedCategories[code] = !this.collapsedCategories[code]; this.renderBookmarks(); }); } if (this.categoryForm) { this.categoryForm.addEventListener('submit', async (e) => { e.preventDefault(); const data = new FormData(this.categoryForm); const payload = { code: String(data.get('code') || '').trim(), name: String(data.get('name') || '').trim(), sort_order: Number(data.get('sort_order') || 0) }; try { if (this.editingCategoryCode) { await this.apiRequest(`/api/nav_bookmarks/categories/${encodeURIComponent(this.editingCategoryCode)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); } else { await this.apiRequest('/api/nav_bookmarks/categories', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); } this.resetCategoryForm(); await this.loadBookmarks(); } catch (err) { alert(err.message || '保存失败'); } }); const cancelBtn = this.categoryForm.querySelector('[data-action="cancel-category"]'); if (cancelBtn) { cancelBtn.addEventListener('click', () => { this.resetCategoryForm(); }); } } if (this.bookmarkForm) { this.bookmarkForm.addEventListener('submit', async (e) => { e.preventDefault(); const data = new FormData(this.bookmarkForm); const payload = { name: String(data.get('name') || '').trim(), url: String(data.get('url') || '').trim(), category_code: String(data.get('category_code') || '').trim(), icon: String(data.get('icon') || '').trim(), remark: String(data.get('remark') || '').trim(), sort_order: Number(data.get('sort_order') || 0) }; try { if (this.editingBookmarkId) { await this.apiRequest(`/api/nav_bookmarks/bookmarks/${encodeURIComponent(this.editingBookmarkId)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); } else { await this.apiRequest('/api/nav_bookmarks/bookmarks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); } this.resetBookmarkForm(); await this.loadBookmarks(); } catch (err) { alert(err.message || '保存失败'); } }); const cancelBtn = this.bookmarkForm.querySelector('[data-action="cancel-bookmark"]'); if (cancelBtn) { cancelBtn.addEventListener('click', () => { this.resetBookmarkForm(); }); } this.bookmarkForm.addEventListener('input', (e) => { if (e.target && e.target.name === 'icon') this.updateIconPreview(); }); const pickBtn = this.bookmarkForm.querySelector('[data-action="pick-icon"]'); const clearBtn2 = this.bookmarkForm.querySelector('[data-action="clear-icon"]'); if (pickBtn) { pickBtn.addEventListener('click', () => { this.iconPickerTarget = this.bookmarkForm.querySelector('input[name="icon"]'); if (this.iconPicker) this.iconPicker.classList.add('active'); }); } if (clearBtn2) { clearBtn2.addEventListener('click', () => { const input = this.bookmarkForm.querySelector('input[name="icon"]'); if (input) input.value = ''; this.updateIconPreview(); }); } } if (this.categoryListEl) { this.categoryListEl.addEventListener('click', async (e) => { const btn = e.target.closest('button[data-action]'); if (!btn) return; const action = btn.dataset.action; const code = btn.dataset.code; if (action === 'edit-category') { const cat = this.categories.find(c => String(c.code) === String(code)); if (cat) this.fillCategoryForm(cat); } if (action === 'delete-category') { if (!confirm('确定删除该分类吗?')) return; try { await this.apiRequest(`/api/nav_bookmarks/categories/${encodeURIComponent(code)}`, { method: 'DELETE' }); await this.loadBookmarks(); } catch (err) { alert(err.message || '删除失败'); } } }); } if (this.bookmarkListEl) { this.bookmarkListEl.addEventListener('click', async (e) => { const btn = e.target.closest('button[data-action]'); if (!btn) return; const action = btn.dataset.action; const id = btn.dataset.id; if (action === 'edit-bookmark') { const item = this.bookmarks.find(b => String(b.id) === String(id)); if (item) this.fillBookmarkForm(item); } if (action === 'delete-bookmark') { if (!confirm('确定删除该书签吗?')) return; try { await this.apiRequest(`/api/nav_bookmarks/bookmarks/${encodeURIComponent(id)}`, { method: 'DELETE' }); await this.loadBookmarks(); } catch (err) { alert(err.message || '删除失败'); } } }); } if (this.iconPicker) { this.iconPicker.addEventListener('click', (e) => { const closeBtn = e.target.closest('[data-action="close-icon-picker"]'); if (closeBtn) { this.iconPicker.classList.remove('active'); return; } const btn = e.target.closest('.icon-picker-item'); if (btn && this.iconPickerTarget) { this.iconPickerTarget.value = btn.dataset.icon || ''; this.updateIconPreview(); this.iconPicker.classList.remove('active'); return; } if (e.target === this.iconPicker) { this.iconPicker.classList.remove('active'); } }); } if (this.fitnessDiaryReminder) { this.fitnessDiaryReminder.addEventListener('click', (e) => { const actionEl = e.target.closest('[data-action]'); if (!actionEl) return; const action = actionEl.dataset.action; if (action === 'close-fitness-diary-reminder') { this.hideFitnessDiaryReminder(); return; } if (action === 'open-fitness-diary') { this.hideFitnessDiaryReminder(); window.open('/tools/fitness_diary', '_blank', 'noopener'); } }); } if (this.weeklyReportReminder) { this.weeklyReportReminder.addEventListener('click', (e) => { const actionEl = e.target.closest('[data-action]'); if (!actionEl) return; const action = actionEl.dataset.action; if (action === 'close-weekly-report-reminder') { this.hideWeeklyReportReminder(); return; } if (action === 'open-weekly-report') { this.hideWeeklyReportReminder(); window.open('/tools/weekly', '_blank', 'noopener'); } }); } } queueReminder(reminderKey, openFn) { if (!reminderKey || typeof openFn !== 'function') return; if (this.activeReminderKey === reminderKey) return; if (this.reminderQueue.some(item => item.key === reminderKey)) return; const reminder = { key: reminderKey, openFn }; if (!this.activeReminderKey) { this.activeReminderKey = reminderKey; openFn(); return; } this.reminderQueue.push(reminder); } finishReminder(reminderKey) { if (!reminderKey) return; if (this.activeReminderKey === reminderKey) { this.activeReminderKey = ''; const nextReminder = this.reminderQueue.shift(); if (nextReminder) { this.activeReminderKey = nextReminder.key; nextReminder.openFn(); } return; } this.reminderQueue = this.reminderQueue.filter(item => item.key !== reminderKey); } formatDate(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } getFitnessDiaryDateRange(days = 5) { const end = new Date(); const start = new Date(end); start.setDate(end.getDate() - (days - 1)); return { start: this.formatDate(start), end: this.formatDate(end) }; } getFitnessDiaryReminderSummary(records, days = 5) { const existingDates = new Set( (Array.isArray(records) ? records : []) .map(item => String(item && item.record_date ? item.record_date : '').slice(0, 10)) .filter(Boolean) ); const missingDates = []; const today = new Date(); for (let i = 0; i < days; i += 1) { const date = new Date(today); date.setDate(today.getDate() - i); const dateStr = this.formatDate(date); if (!existingDates.has(dateStr)) missingDates.push(dateStr); } return { days, missingDates: missingDates.reverse(), missingCount: missingDates.length }; } htmlToPlainText(html) { const temp = document.createElement('div'); temp.innerHTML = String(html || ''); return String(temp.textContent || temp.innerText || '') .replace(/\u00a0/g, ' ') .replace(/\s+/g, ' ') .trim(); } getWeeklyTemplatePlainText() { return [ '1、本周核心重点事项(置顶)', '①置顶优先级,避免杂事分散精力', '②本周 1-3 件最高优先级事(工作 / 家庭各列,如公司项目节点、孩子升学事宜)', '③必做底线项(如固定会议、家庭日常、记账 / 复盘打卡)', '2、上周复盘(工作)', '-----', '①公司工作完成项(标注是否达标、核心成果)', '②未完成项 + 原因(客观 / 主观,如时间不足、需求变更)', '③规则事务完成情况(重点记未完成的关键项)', '3、上周复盘(个人 + 家庭)', '-----', '①个人 / 家庭事务完成情况(重点记未完成的关键项)', '4、公司工作(计划)', '-----', '①核心任务(按重要 / 紧急排序,标注截止时间、协作人)', '②常规工作(日常运维、对接、汇报等固定事项)', '③待协调 / 需支持项(如需要同事配合、领导审批的内容)', '5、个人和家庭 事务(计划,含理财)', '-----', '①自我提升(学习、运动、习惯打卡,如读 1 章书、运动 3 次)', '②个人琐事(办事、缴费、社交等,标注时间节点)', '③投资理财(盯盘节点、条件单调整、复盘上周收益 / 操作)', '④家人相关(孩子学习 / 活动、老人照料、家庭聚餐等)', '⑤家庭琐事(家务、采购、物业对接等,可标注分工)', '⑥家庭规划(短期家庭事项,如周末出游、家电维修)', '6、待解决问题 / 待协调项', '-----', '①工作中可能遇到的问题(如项目延期风险、资源不足)', '②个人 / 家庭的待处理问题(如时间冲突、资金安排)', '③初步应对思路(简单标注,避免临时慌乱)', '7、本周小目标 / 打卡项(轻量化激励,易落地)', '①工作小目标(如高效完成 1 个核心任务、减少无效沟通)', '②个人 / 家庭小目标(如坚持记账、周末全家运动 1 次)' ].join(' '); } isWeeklyAutoTitle(title, weekId = '') { const normalizedTitle = String(title || '').trim(); if (!normalizedTitle) return true; if (normalizedTitle.includes('(自动新增)')) return true; if (weekId && normalizedTitle === `${weekId} 周报`) return true; return false; } isWeeklyMeaningfulReport(report, expectedWeekId = '') { if (!report || typeof report !== 'object') return false; const weekId = String(report.week_id || expectedWeekId || '').trim(); const title = String(report.title || '').trim(); const contentText = this.htmlToPlainText(report.content || ''); if (!title || !contentText) return false; if (this.isWeeklyAutoTitle(title, weekId)) return false; if (contentText === '这是自动新增的周报。你可以修改。') return false; if (contentText === this.getWeeklyTemplatePlainText()) return false; return true; } async getRecentWeeklyWindows(count = 3) { const today = this.formatDate(new Date()); const currentYear = new Date().getFullYear(); const years = [currentYear - 1, currentYear]; const results = await Promise.all( years.map(async (year) => { const res = await fetch(`/api/weekly/weeks?year=${year}&t=${Date.now()}`, { cache: 'no-store' }); if (!res.ok) throw new Error(`weekly_weeks_${res.status}`); const data = await res.json(); return Array.isArray(data && data.weeks) ? data.weeks : []; }) ); const allWeeks = results .flat() .filter(item => item && item.week_id && item.start && item.end) .sort((a, b) => String(a.start).localeCompare(String(b.start))); if (allWeeks.length === 0) return []; const currentIndex = allWeeks.findIndex(item => String(item.start) <= today && today <= String(item.end)); if (currentIndex < 0) return allWeeks.slice(-count); return allWeeks.slice(Math.max(0, currentIndex - count + 1), currentIndex + 1); } renderFitnessDiaryReminder(summary) { if (!this.fitnessDiaryReminder || !this.fitnessDiaryReminderText || !this.fitnessDiaryReminderDates) return; const missingCount = Number(summary && summary.missingCount ? summary.missingCount : 0); if (missingCount <= 0) { this.hideFitnessDiaryReminder(); return; } this.fitnessDiaryReminderText.textContent = `最近${summary.days}天(含今天)应写${summary.days}篇运动日记,你现在还缺${missingCount}篇。`; this.fitnessDiaryReminderDates.innerHTML = summary.missingDates .map(date => `${date}`) .join(''); this.queueReminder('fitness-diary', () => { if (this.fitnessDiaryReminder) this.fitnessDiaryReminder.classList.add('active'); }); } hideFitnessDiaryReminder() { if (!this.fitnessDiaryReminder) return; this.fitnessDiaryReminder.classList.remove('active'); this.finishReminder('fitness-diary'); } async loadFitnessDiaryReminder() { try { const range = this.getFitnessDiaryDateRange(5); const res = await fetch(`/api/fitness_diary/records?start=${range.start}&end=${range.end}&t=${Date.now()}`, { cache: 'no-store' }); if (!res.ok) return; const data = await res.json(); if (!data || data.ok !== true) return; const summary = this.getFitnessDiaryReminderSummary(data.data, 5); this.renderFitnessDiaryReminder(summary); } catch (err) { console.warn('加载运动日记提醒失败:', err); } } renderWeeklyReportReminder(summary) { if (!this.weeklyReportReminder || !this.weeklyReportReminderText || !this.weeklyReportReminderDates) return; const missingCount = Number(summary && summary.missingCount ? summary.missingCount : 0); if (missingCount <= 0) { this.hideWeeklyReportReminder(); return; } this.weeklyReportReminderText.textContent = `最近${summary.weeks}周(含本周)应写${summary.weeks}篇周报,你现在还缺${missingCount}篇。`; this.weeklyReportReminderDates.innerHTML = (summary.missingWeeks || []) .map(item => `${item.week_id} (${item.start} ~ ${item.end})`) .join(''); this.queueReminder('weekly-report', () => { if (this.weeklyReportReminder) this.weeklyReportReminder.classList.add('active'); }); } hideWeeklyReportReminder() { if (!this.weeklyReportReminder) return; this.weeklyReportReminder.classList.remove('active'); this.finishReminder('weekly-report'); } async loadWeeklyReportReminder() { try { const weeks = await this.getRecentWeeklyWindows(3); if (!Array.isArray(weeks) || weeks.length === 0) return; const reportResults = await Promise.all( weeks.map(async (week) => { const res = await fetch(`/api/weekly/report?week_id=${encodeURIComponent(week.week_id)}&t=${Date.now()}`, { cache: 'no-store' }); if (!res.ok) { if (res.status === 401) return { unauthorized: true, week }; return { week, report: null }; } const data = await res.json(); return { week, report: data && data.ok === true ? data.report : null }; }) ); if (reportResults.some(item => item && item.unauthorized)) return; const missingWeeks = reportResults .filter(item => !this.isWeeklyMeaningfulReport(item.report, item.week && item.week.week_id)) .map(item => item.week) .filter(Boolean); this.renderWeeklyReportReminder({ weeks: 3, missingCount: missingWeeks.length, missingWeeks }); } catch (err) { console.warn('加载周报提醒失败:', err); } } async apiRequest(url, options) { const res = await fetch(url, options || {}); let data = null; try { data = await res.json(); } catch {} if (!res.ok || (data && data.ok === false)) { const msg = data && data.error ? data.error : res.statusText; throw new Error(msg || '请求失败'); } return data; } resetCategoryForm() { if (!this.categoryForm) return; this.editingCategoryCode = ''; this.categoryForm.reset(); const codeInput = this.categoryForm.querySelector('input[name="code"]'); if (codeInput) codeInput.disabled = false; } resetBookmarkForm() { if (!this.bookmarkForm) return; this.editingBookmarkId = ''; this.bookmarkForm.reset(); if (this.bookmarkCreatedAtEl) this.bookmarkCreatedAtEl.textContent = '-'; this.updateIconPreview(); } fillCategoryForm(cat) { if (!this.categoryForm) return; this.editingCategoryCode = String(cat.code); const codeInput = this.categoryForm.querySelector('input[name="code"]'); const nameInput = this.categoryForm.querySelector('input[name="name"]'); const sortInput = this.categoryForm.querySelector('input[name="sort_order"]'); if (codeInput) { codeInput.value = cat.code; codeInput.disabled = true; } if (nameInput) nameInput.value = cat.name || ''; if (sortInput) sortInput.value = Number(cat.sort_order || 0); } fillBookmarkForm(item) { if (!this.bookmarkForm) return; this.editingBookmarkId = String(item.id); const nameInput = this.bookmarkForm.querySelector('input[name="name"]'); const urlInput = this.bookmarkForm.querySelector('input[name="url"]'); const catSelect = this.bookmarkForm.querySelector('select[name="category_code"]'); const iconInput = this.bookmarkForm.querySelector('input[name="icon"]'); const remarkInput = this.bookmarkForm.querySelector('textarea[name="remark"]'); const sortInput = this.bookmarkForm.querySelector('input[name="sort_order"]'); if (nameInput) nameInput.value = item.name || ''; if (urlInput) urlInput.value = item.url || ''; if (catSelect) catSelect.value = item.category_code || ''; if (iconInput) iconInput.value = item.icon || ''; if (remarkInput) remarkInput.value = item.remark || ''; if (sortInput) sortInput.value = Number(item.sort_order || 0); if (this.bookmarkCreatedAtEl) this.bookmarkCreatedAtEl.textContent = item.created_at || '-'; this.updateIconPreview(); } updateIconPreview() { if (!this.bookmarkForm || !this.iconPreviewEl) return; const iconInput = this.bookmarkForm.querySelector('input[name="icon"]'); if (!iconInput) return; const iconUrl = this.getIconUrl(iconInput.value); this.iconPreviewEl.innerHTML = ''; if (iconUrl) { const img = document.createElement('img'); img.src = iconUrl; img.alt = iconInput.value || ''; this.iconPreviewEl.appendChild(img); } else { this.iconPreviewEl.textContent = '未选择'; } } applyTheme(themeIndex) { const theme = this.themes[themeIndex]; // 更新背景图片 document.querySelectorAll('.background-image').forEach((img, index) => { img.classList.toggle('active', index === themeIndex); }); this.ensureBgLoaded(themeIndex, true); // 强制使用纯白文字 document.body.style.color = '#ffffff'; document.documentElement.style.setProperty('--card-text-color', '#ffffff'); // 移除对 tool-card 的个别样式覆盖,统一使用 CSS 类控制 /* document.querySelectorAll('.tool-card').forEach(card => { card.style.background = theme.cardGradient; card.style.color = theme.textColor; }); */ // 清除所有特效 this.clearEffects(); // 应用新特效 setTimeout(() => { this.startEffect(theme.effect); }, 500); } async ensureBgLoaded(themeIndex, preferLocal = true) { try { const theme = this.themes[themeIndex]; const imgs = document.querySelectorAll('.background-image'); const img = imgs[themeIndex]; if (!img || !theme) return; const r = await fetch(`/api/home/bg?theme=${encodeURIComponent(theme.name)}&url=${encodeURIComponent(theme.background)}&prefer_local=${preferLocal ? '1' : '0'}`); const j = await r.json(); img.src = (j && j.url) ? j.url : theme.background; } catch { const theme = this.themes[themeIndex]; const imgs = document.querySelectorAll('.background-image'); const img = imgs[themeIndex]; if (img && theme) img.src = theme.background; } } nextTheme() { if (this.localBgMode) { this.nextLocalBg(); return; } this.currentTheme = (this.currentTheme + 1) % this.themes.length; this.applyTheme(this.currentTheme); // 显示当前主题名称 const themeName = this.themes[this.currentTheme].name; this.showNotification(`切换到: ${themeName}`); } startEffects() { const currentTheme = this.themes[this.currentTheme]; this.startEffect(currentTheme.effect); } startEffect(effectType) { const effectsContainer = document.querySelector('.effects-container'); switch (effectType) { case 'meteor': this.startMeteorEffect(effectsContainer); break; default: this.startMeteorEffect(effectsContainer); break; } } startMeteorEffect(container) { // 创建流星 const createMeteor = () => { const meteor = document.createElement('div'); meteor.className = 'meteor'; // 随机位置 const left = Math.random() * 100; const top = Math.random() * 50; meteor.style.left = `${left}%`; meteor.style.top = `${top}%`; // 随机大小和速度 const size = 1 + Math.random() * 2; const duration = 8 + Math.random() * 4; meteor.style.width = `${size}px`; meteor.style.height = `${size}px`; meteor.style.animationDuration = `${duration}s`; container.appendChild(meteor); // 移除流星 setTimeout(() => { if (meteor.parentNode) { meteor.parentNode.removeChild(meteor); } }, duration * 1000); }; // 定期创建流星 this.meteorInterval = setInterval(createMeteor, 3000); // 初始创建几个流星 for (let i = 0; i < 3; i++) { setTimeout(createMeteor, i * 1000); } } startPaperPlaneEffect(container) { // 创建纸飞机 const createPaperPlane = () => { const plane = document.createElement('div'); plane.className = 'paper-plane'; plane.innerHTML = '✈️'; // 随机位置 const top = 10 + Math.random() * 70; plane.style.top = `${top}%`; // 随机延迟和速度 const delay = Math.random() * 5; const duration = 12 + Math.random() * 6; plane.style.animationDelay = `${delay}s`; plane.style.animationDuration = `${duration}s`; container.appendChild(plane); // 移除飞机 setTimeout(() => { if (plane.parentNode) { plane.parentNode.removeChild(plane); } }, (duration + delay) * 1000); }; // 定期创建纸飞机 this.planeInterval = setInterval(createPaperPlane, 8000); // 初始创建几个纸飞机 for (let i = 0; i < 2; i++) { setTimeout(createPaperPlane, i * 2000); } } startForestEffect(container) { // 森林主题的飘叶效果 const createLeaf = () => { const leaf = document.createElement('div'); leaf.className = 'paper-plane'; leaf.innerHTML = '🍃'; leaf.style.fontSize = '16px'; // 随机位置 const left = Math.random() * 100; const top = Math.random() * 100; leaf.style.left = `${left}%`; leaf.style.top = `${top}%`; // 随机动画参数 const delay = Math.random() * 3; const duration = 10 + Math.random() * 8; leaf.style.animationDelay = `${delay}s`; leaf.style.animationDuration = `${duration}s`; container.appendChild(leaf); // 移除叶子 setTimeout(() => { if (leaf.parentNode) { leaf.parentNode.removeChild(leaf); } }, (duration + delay) * 1000); }; // 定期创建叶子 this.leafInterval = setInterval(createLeaf, 4000); // 初始创建几个叶子 for (let i = 0; i < 4; i++) { setTimeout(createLeaf, i * 1000); } } clearEffects() { // 清除所有定时器 if (this.meteorInterval) clearInterval(this.meteorInterval); if (this.planeInterval) clearInterval(this.planeInterval); if (this.leafInterval) clearInterval(this.leafInterval); // 清除所有特效元素 const effectsContainer = document.querySelector('.effects-container'); if (effectsContainer) { effectsContainer.innerHTML = ''; } } startBackgroundRotation() { this.startThemeRotation(); } startThemeRotation() { if (this.localBgMode) return; this.stopThemeRotation(); this.rotationInterval = setInterval(() => { this.nextTheme(); }, 30000); } stopThemeRotation() { if (this.rotationInterval) { clearInterval(this.rotationInterval); this.rotationInterval = null; } } startLocalBgRotation() { this.loadLocalBgFiles().then(() => { this.stopLocalBgRotation(); this.localRotationInterval = setInterval(() => { this.nextLocalBg(); }, 30000); }); } stopLocalBgRotation() { if (this.localRotationInterval) { clearInterval(this.localRotationInterval); this.localRotationInterval = null; } } async loadLocalBgFiles() { if (this.localBgFiles.length > 0) return this.localBgFiles; try { const r = await fetch('/api/home/bg/default-list?t=' + Date.now()); const j = await r.json(); if (j.ok && j.files.length) { this.localBgFiles = j.files; this.localBgIndex = 0; // 加载第一张 const imgs = document.querySelectorAll('.background-image'); if (imgs.length) { imgs.forEach(img => { img.src = `/home-bg/default-c21f969b/${this.localBgFiles[0]}?v=${Date.now()}`; }); } } } catch {} return this.localBgFiles; } async nextLocalBg() { if (!this.localBgFiles.length) { await this.loadLocalBgFiles(); if (!this.localBgFiles.length) return; } // 随机选取,避免重复当前 let newIndex; if (this.localBgFiles.length > 1) { do { newIndex = Math.floor(Math.random() * this.localBgFiles.length); } while (newIndex === this.localBgIndex); } else { newIndex = 0; } this.localBgIndex = newIndex; const file = this.localBgFiles[this.localBgIndex]; const imgs = document.querySelectorAll('.background-image'); if (imgs.length) { imgs.forEach(img => { img.src = `/home-bg/default-c21f969b/${file}?v=${Date.now()}`; }); } this.showNotification(`本地背景: ${this.localBgIndex + 1}/${this.localBgFiles.length}`); } showNotification(message) { // 创建通知元素 const notification = document.createElement('div'); notification.style.cssText = ` position: fixed; top: 2rem; left: 50%; transform: translateX(-50%); background: rgba(255, 255, 255, 0.2); backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.3); color: var(--text-light); padding: 0.75rem 1.5rem; border-radius: 25px; font-size: 0.9rem; z-index: 1000; opacity: 0; transition: opacity 0.3s ease; `; notification.textContent = message; document.body.appendChild(notification); // 显示动画 setTimeout(() => { notification.style.opacity = '1'; }, 10); // 隐藏动画 setTimeout(() => { notification.style.opacity = '0'; setTimeout(() => { if (notification.parentNode) { notification.parentNode.removeChild(notification); } }, 300); }, 2000); } } // 页面加载完成后初始化 document.addEventListener('DOMContentLoaded', () => { new ToolboxHome(); });