// 多宫格导航页面功能 - 炫酷有氛围版本 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; // 后台维护状态(tab + 分页 + 筛选) this.adminTab = 'bookmarks'; this.adminCatPage = 1; this.adminCatPageSize = 10; this.adminBookPage = 1; this.adminBookPageSize = 50; this.adminBookCatFilter = ''; this.adminBookQuery = ''; this.adminCategories = []; this.adminBookmarks = []; this.adminCatTotal = 0; this.adminBookTotal = 0; this.adminSearchTimer = 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.loadThoughtLabs(); 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); // 思想实验室页面(卡片形式与高/低频一致,数据来自 /api/thought_lab/labs) const labsPage = document.createElement('div'); labsPage.className = 'tab-page'; labsPage.dataset.tab = 'labs'; const labGrid = document.createElement('div'); labGrid.className = 'tool-grid'; labGrid.id = 'tool-grid-labs'; labsPage.appendChild(labGrid); // 书签导航页面 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 = `
书签后台维护
名称 网址 分类 排序 创建时间 操作
编码 名称 排序 书签数 操作
`; // 后台维护弹窗(新增/编辑分类、书签共用) const adminModal = document.createElement('div'); adminModal.className = 'nav-modal-overlay'; adminModal.id = 'nav-admin-modal'; adminModal.innerHTML = ` `; bookmarksPage.appendChild(bookmarkToolbar); bookmarksPage.appendChild(bookmarkSections); bookmarksPage.appendChild(bookmarkEmpty); bookmarksPage.appendChild(bookmarkAdmin); tabPages.appendChild(highToolsPage); tabPages.appendChild(lowToolsPage); tabPages.appendChild(bookmarksPage); tabPages.appendChild(labsPage); // 组装页面 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); document.body.appendChild(adminModal); 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.labGrid = labGrid; this.toolsPage = highToolsPage; this.bookmarksPage = bookmarksPage; this.labsPage = labsPage; this.labTools = []; this.tabPages = [highToolsPage, lowToolsPage, bookmarksPage, labsPage]; this.bookmarkContainer = bookmarkSections; this.bookmarkEmpty = bookmarkEmpty; this.bookmarkAdmin = bookmarkAdmin; this.categoryForm = adminModal.querySelector('#admin-category-form'); this.bookmarkForm = adminModal.querySelector('#admin-bookmark-form'); this.categoryListEl = bookmarkAdmin.querySelector('#category-table-body'); this.bookmarkListEl = bookmarkAdmin.querySelector('#bookmark-table-body'); this.bookmarkSearchInput = bookmarkToolbar.querySelector('#bookmark-search'); this.bookmarkCreatedAtEl = adminModal.querySelector('[data-field="created_at"]'); this.iconPreviewEl = adminModal.querySelector('[data-field="icon-preview"]'); this.navAdminModal = adminModal; this.navAdminModalTitle = adminModal.querySelector('#nav-admin-modal-title'); this.adminTabsEl = bookmarkAdmin.querySelector('#admin-tabs'); this.categoryPaginationEl = bookmarkAdmin.querySelector('#category-pagination'); this.bookmarkPaginationEl = bookmarkAdmin.querySelector('#bookmark-pagination'); this.bookmarkAdminCatFilter = bookmarkAdmin.querySelector('#bookmark-admin-category-filter'); this.bookmarkAdminSearchInput = bookmarkAdmin.querySelector('#bookmark-admin-search'); this.categoryCountEl = bookmarkAdmin.querySelector('#category-count'); this.bookmarkCountEl = bookmarkAdmin.querySelector('#bookmark-count'); 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); } async loadThoughtLabs() { try { const res = await fetch('/api/thought_lab/labs?t=' + Date.now()); if (!res.ok) return; const data = await res.json(); if (!data || !data.ok) return; this.labTools = Array.isArray(data.labs) ? data.labs : []; this.renderLabs(); } catch (err) { console.error('加载思想实验室列表失败:', err); } } renderLabs() { if (!this.labGrid) return; this.labGrid.innerHTML = ''; this.labTools.forEach(lab => { const card = document.createElement('a'); card.className = 'tool-card'; card.href = lab.href || '#'; if (lab.desc) card.title = lab.desc; card.innerHTML = `
${lab.icon || '🧪'}
${lab.name || lab.id || '未命名 lab'}
`; this.labGrid.appendChild(card); }); } 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'); this.closeAdminModal(); } 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; if (!this.adminVisible) return; this.switchAdminTab(this.adminTab); } // 后台维护 tab 切换(分类 / 书签) switchAdminTab(tab) { if (!this.bookmarkAdmin) return; this.adminTab = tab; this.bookmarkAdmin.querySelectorAll('.admin-tab-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.admTab === tab); }); this.bookmarkAdmin.querySelectorAll('.admin-tab-page').forEach(p => { p.classList.toggle('active', p.dataset.admPage === tab); }); if (tab === 'bookmarks') { this.renderAdminCategoryFilter(); this.loadAdminBookmarks(); } else { this.loadAdminCategories(); } } esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); } // 判断网址是否为文件下载链接(按扩展名 + 下载关键字) isFileUrl(url) { const raw = String(url || ''); const path = raw.split(/[?#]/)[0] || ''; if (/\.(exe|msi|zip|rar|7z|tar|gz|bz2|pdf|doc|docx|xls|xlsx|ppt|pptx|csv|json|apk|ipa|dmg|pkg|iso|jar|torrent|xmind|dwg)(\/|$)/i.test(path)) return true; if (/\.(png|jpe?g|gif|webp|svg|bmp|ico|avif|mp3|wav|flac|ogg|aac|mp4|mkv|mov|avi|m4a|wmv|flv|torrent)(\/|$)/i.test(path)) return true; if (/\/(download|downloads|dl|get|file|files)\//i.test(path)) return true; if (/[?&](download|dl|file|action)=/i.test(raw)) return true; return false; } // 统一按北京时间展示创建时间 fmtNavTime(iso) { if (!iso) return '-'; try { const parts = new Intl.DateTimeFormat('zh-CN', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false }).formatToParts(new Date(iso)); const get = t => { const p = parts.find(x => x.type === t); return p ? p.value : ''; }; return `${get('year')}-${get('month')}-${get('day')} ${get('hour')}:${get('minute')}`; } catch { return String(iso || '').slice(0, 16).replace('T', ' '); } } getCategoryName(code) { const cat = (this.categories || []).find(c => String(c.code) === String(code == null ? '' : code)); return cat ? cat.name : '未分类'; } async loadAdminCategories() { if (!this.bookmarkAdmin || !this.categoryListEl) return; try { const res = await fetch(`/api/nav_bookmarks/categories?page=${this.adminCatPage}&pageSize=${this.adminCatPageSize}&t=${Date.now()}`); if (!res.ok) return; const data = await res.json(); if (!data || !data.ok) return; this.adminCategories = Array.isArray(data.rows) ? data.rows : []; this.adminCatTotal = Number(data.total || 0); const totalPages = Math.max(1, Number(data.total_pages || 1)); if (this.adminCatPage > totalPages) { this.adminCatPage = totalPages; this.loadAdminCategories(); return; } this.renderAdminCategoryTable(); if (this.categoryCountEl) this.categoryCountEl.textContent = `共 ${this.adminCatTotal} 个分类`; this.renderAdminPagination(this.categoryPaginationEl, this.adminCatPage, totalPages, p => { this.adminCatPage = p; this.loadAdminCategories(); }); } catch {} } renderAdminCategoryTable() { if (!this.categoryListEl) return; if (!this.adminCategories.length) { this.categoryListEl.innerHTML = '暂无分类'; return; } this.categoryListEl.innerHTML = this.adminCategories.map(cat => ` ${this.esc(cat.code)} ${this.esc(cat.name)} ${Number(cat.sort_order || 0)} ${Number(cat.bookmark_count || 0)}
`).join(''); } async loadAdminBookmarks() { if (!this.bookmarkAdmin || !this.bookmarkListEl) return; try { const p = new URLSearchParams({ page: String(this.adminBookPage), pageSize: String(this.adminBookPageSize), t: String(Date.now()) }); if (this.adminBookCatFilter) p.set('category_code', this.adminBookCatFilter); if (this.adminBookQuery) p.set('q', this.adminBookQuery); const res = await fetch(`/api/nav_bookmarks/bookmarks?${p.toString()}`); if (!res.ok) return; const data = await res.json(); if (!data || !data.ok) return; this.adminBookmarks = Array.isArray(data.rows) ? data.rows : []; this.adminBookTotal = Number(data.total || 0); const totalPages = Math.max(1, Number(data.total_pages || 1)); if (this.adminBookPage > totalPages) { this.adminBookPage = totalPages; this.loadAdminBookmarks(); return; } this.renderAdminBookmarkTable(); if (this.bookmarkCountEl) this.bookmarkCountEl.textContent = `共 ${this.adminBookTotal} 条`; this.renderAdminPagination(this.bookmarkPaginationEl, this.adminBookPage, totalPages, p => { this.adminBookPage = p; this.loadAdminBookmarks(); }); } catch {} } renderAdminBookmarkTable() { if (!this.bookmarkListEl) return; if (!this.adminBookmarks.length) { const hasFilter = this.adminBookCatFilter || String(this.adminBookQuery || '').trim(); this.bookmarkListEl.innerHTML = `暂无书签${hasFilter ? '(筛选条件无结果)' : ''}`; return; } this.bookmarkListEl.innerHTML = this.adminBookmarks.map(item => { const iconUrl = this.getIconUrl(item.icon); const iconHtml = iconUrl ? `` : ''; const isFile = this.isFileUrl(item.url); const urlIcon = `${isFile ? '📄' : '🔗'}`; const remarkHtml = item.remark ? `
${this.esc(item.remark)}
` : ''; return ` ${iconHtml}
${this.esc(item.name || '未命名')}
${remarkHtml} ${urlIcon}${this.esc(item.url || '-')} ${this.esc(this.getCategoryName(item.category_code))} ${Number(item.sort_order || 0)} ${this.fmtNavTime(item.created_at)}
`; }).join(''); // 备注未超长时隐藏展开按钮,保证行高低稳定 this.bookmarkListEl.querySelectorAll('.td-remark-wrap').forEach(wrap => { const sub = wrap.querySelector('.td-sub'); const btn = wrap.querySelector('.remark-toggle'); if (sub && btn && sub.scrollWidth <= sub.clientWidth + 1) btn.style.visibility = 'hidden'; }); } // 书签管理 tab 顶部的分类筛选下拉(用全量分类填充) renderAdminCategoryFilter() { if (!this.bookmarkAdminCatFilter) return; const current = this.bookmarkAdminCatFilter.value || this.adminBookCatFilter; this.bookmarkAdminCatFilter.innerHTML = ''; const all = document.createElement('option'); all.value = ''; all.textContent = '全部分类'; this.bookmarkAdminCatFilter.appendChild(all); (this.categories || []).forEach(cat => { const opt = document.createElement('option'); opt.value = cat.code; opt.textContent = `${cat.code} - ${cat.name}`; this.bookmarkAdminCatFilter.appendChild(opt); }); if (current) this.bookmarkAdminCatFilter.value = current; } renderAdminPagination(container, page, totalPages, onChange) { if (!container) return; container.innerHTML = ''; if (totalPages <= 1) return; const mkBtn = (text, p, disabled, active) => { const b = document.createElement('button'); b.type = 'button'; b.className = 'bookmark-btn pagination-btn' + (active ? ' active' : ''); b.textContent = text; b.disabled = !!disabled; if (!disabled) b.addEventListener('click', () => onChange(p)); return b; }; const mkDots = () => { const s = document.createElement('span'); s.className = 'pagination-dots'; s.textContent = '…'; return s; }; container.appendChild(mkBtn('上一页', page - 1, page <= 1, false)); const start = Math.max(1, page - 2); const end = Math.min(totalPages, page + 2); if (start > 1) { container.appendChild(mkBtn('1', 1, false, false)); if (start > 2) container.appendChild(mkDots()); } for (let p = start; p <= end; p++) container.appendChild(mkBtn(String(p), p, false, p === page)); if (end < totalPages) { if (end < totalPages - 1) container.appendChild(mkDots()); container.appendChild(mkBtn(String(totalPages), totalPages, false, false)); } container.appendChild(mkBtn('下一页', page + 1, page >= totalPages, false)); } openAdminModal(type, title) { if (!this.navAdminModal) return; const isCat = type === 'category'; if (this.navAdminModalTitle) this.navAdminModalTitle.textContent = title || (isCat ? '新增分类' : '新增书签'); if (this.categoryForm) this.categoryForm.style.display = isCat ? '' : 'none'; if (this.bookmarkForm) this.bookmarkForm.style.display = isCat ? 'none' : ''; if (!isCat) this.prefillBookmarkSelect(); if (this.iconPicker) this.iconPicker.classList.remove('active'); this.navAdminModal.classList.add('active'); } closeAdminModal() { if (this.navAdminModal) this.navAdminModal.classList.remove('active'); } openCategoryModal(data) { this.openAdminModal('category', data ? '编辑分类' : '新增分类'); if (data) this.fillCategoryForm(data); else this.resetCategoryForm(); } openBookmarkModal(data) { this.openAdminModal('bookmark', data ? '编辑书签' : '新增书签'); if (data) this.fillBookmarkForm(data); else this.resetBookmarkForm(); } // 书签表单里的分类下拉(用全量分类填充) prefillBookmarkSelect() { const sel = this.bookmarkForm ? this.bookmarkForm.querySelector('select[name="category_code"]') : null; if (!sel) return; const current = sel.value; sel.innerHTML = ''; const emptyOpt = document.createElement('option'); emptyOpt.value = ''; emptyOpt.textContent = '未分类'; sel.appendChild(emptyOpt); (this.categories || []).forEach(cat => { const opt = document.createElement('option'); opt.value = cat.code; opt.textContent = `${cat.code} - ${cat.name}`; sel.appendChild(opt); }); if (current) sel.value = current; } 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(); }); } // 后台维护:管理员 tab 切换(分类 / 书签) if (this.adminTabsEl) { this.adminTabsEl.addEventListener('click', (e) => { const btn = e.target.closest('.admin-tab-btn'); if (!btn) return; this.switchAdminTab(btn.dataset.admTab); }); } // 新增分类 / 新增书签 const categoryAddBtn = document.getElementById('category-add-btn'); if (categoryAddBtn) { categoryAddBtn.addEventListener('click', () => { this.openCategoryModal(null); }); } const bookmarkAddBtn = document.getElementById('bookmark-add-btn'); if (bookmarkAddBtn) { bookmarkAddBtn.addEventListener('click', () => { this.openBookmarkModal(null); }); } // 书签管理:分类筛选 + 关键词搜索(300ms 防抖) if (this.bookmarkAdminCatFilter) { this.bookmarkAdminCatFilter.addEventListener('change', () => { this.adminBookCatFilter = this.bookmarkAdminCatFilter.value; this.adminBookPage = 1; this.loadAdminBookmarks(); }); } if (this.bookmarkAdminSearchInput) { this.bookmarkAdminSearchInput.addEventListener('input', () => { clearTimeout(this.adminSearchTimer); this.adminSearchTimer = setTimeout(() => { this.adminBookQuery = this.bookmarkAdminSearchInput.value.trim(); this.adminBookPage = 1; this.loadAdminBookmarks(); }, 300); }); } const adminSearchClear = document.getElementById('bookmark-admin-search-clear'); if (adminSearchClear) { adminSearchClear.addEventListener('click', () => { if (this.bookmarkAdminSearchInput) this.bookmarkAdminSearchInput.value = ''; this.adminBookQuery = ''; this.adminBookPage = 1; this.loadAdminBookmarks(); }); } // 后台维护弹窗:关闭按钮 / 点击遮罩关闭 const navModalClose = document.getElementById('nav-admin-modal-close'); if (navModalClose) { navModalClose.addEventListener('click', () => { this.closeAdminModal(); }); } if (this.navAdminModal) { this.navAdminModal.addEventListener('click', (e) => { if (e.target === this.navAdminModal) this.closeAdminModal(); }); } 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) { const categorySubmitBtn = this.categoryForm.querySelector('button[type="submit"]'); 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) }; const wasEditing = !!this.editingCategoryCode; if (categorySubmitBtn) { categorySubmitBtn.disabled = true; categorySubmitBtn.textContent = '保存中…'; } 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(); this.closeAdminModal(); if (!wasEditing) this.adminCatPage = 1; await this.loadBookmarks(); await this.loadAdminCategories(); this.renderAdminCategoryFilter(); } catch (err) { alert(err.message || '保存失败'); } finally { if (categorySubmitBtn) { categorySubmitBtn.disabled = false; categorySubmitBtn.textContent = '保存'; } } }); const cancelBtn = this.categoryForm.querySelector('[data-action="cancel-category"]'); if (cancelBtn) { cancelBtn.addEventListener('click', () => { this.resetCategoryForm(); this.closeAdminModal(); }); } } if (this.bookmarkForm) { const bookmarkSubmitBtn = this.bookmarkForm.querySelector('button[type="submit"]'); 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) }; const wasEditing = !!this.editingBookmarkId; if (bookmarkSubmitBtn) { bookmarkSubmitBtn.disabled = true; bookmarkSubmitBtn.textContent = '保存中…'; } 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(); this.closeAdminModal(); if (!wasEditing) this.adminBookPage = 1; await this.loadBookmarks(); await this.loadAdminBookmarks(); } catch (err) { alert(err.message || '保存失败'); } finally { if (bookmarkSubmitBtn) { bookmarkSubmitBtn.disabled = false; bookmarkSubmitBtn.textContent = '保存'; } } }); const cancelBtn = this.bookmarkForm.querySelector('[data-action="cancel-bookmark"]'); if (cancelBtn) { cancelBtn.addEventListener('click', () => { this.resetBookmarkForm(); this.closeAdminModal(); }); } 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.adminCategories.find(c => String(c.code) === String(code)) || this.categories.find(c => String(c.code) === String(code)); if (cat) this.openCategoryModal(cat); } if (action === 'delete-category') { if (!confirm('确定删除该分类吗?')) return; try { await this.apiRequest(`/api/nav_bookmarks/categories/${encodeURIComponent(code)}`, { method: 'DELETE' }); await this.loadBookmarks(); await this.loadAdminCategories(); this.renderAdminCategoryFilter(); } catch (err) { alert(err.message || '删除失败'); } } }); } if (this.bookmarkListEl) { this.bookmarkListEl.addEventListener('click', (e) => { const btn = e.target.closest('button[data-action]'); if (!btn) return; const action = btn.dataset.action; const id = btn.dataset.id; if (action === 'toggle-remark') { const wrap = btn.closest('.td-remark-wrap'); if (!wrap) return; const expanded = wrap.classList.toggle('expanded'); btn.textContent = expanded ? '👆' : '👇'; btn.title = expanded ? '收起备注' : '展开/收起备注'; return; } if (action === 'edit-bookmark') { const item = this.adminBookmarks.find(b => String(b.id) === String(id)) || this.bookmarks.find(b => String(b.id) === String(id)); if (item) this.openBookmarkModal(item); } if (action === 'delete-bookmark') { if (!confirm('确定删除该书签吗?')) return; this.apiRequest(`/api/nav_bookmarks/bookmarks/${encodeURIComponent(id)}`, { method: 'DELETE' }).then(async () => { await this.loadBookmarks(); await this.loadAdminBookmarks(); }).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 }; } 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 { // 【首页提醒专用】直接调后端聚合接口 /api/weekly/reminder/status。 // 判定由服务端读库完成,不依赖周报 weekly_auth 登录态(仅需 weekly_gate 门禁); // 相对路径同源调用:本机访问即 localhost:8976,外网访问自动走 8975 反代,接口本身未授权一律 401。 const res = await fetch(`/api/weekly/reminder/status?t=${Date.now()}`, { cache: 'no-store' }); if (!res.ok) return; // 无门禁 / 接口异常:静默隐藏提醒 const data = await res.json(); if (!data || data.ok !== true) return; this.renderWeeklyReportReminder({ weeks: Number(data.weeks || 3), missingCount: Number(data.missingCount || 0), missingWeeks: Array.isArray(data.missingWeeks) ? data.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('textarea[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(); });