Files
Toolbox/public/index.js
T
yangxiangyuan 5fb62f7900 chore: add大量静态资源文件
新增了图标、背景图、验证码素材以及音频素材等大量静态资源文件
2026-07-08 10:49:14 +08:00

1470 lines
54 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 多宫格导航页面功能 - 炫酷有氛围版本
class ToolboxHome {
constructor() {
this.currentTheme = 0;
this.openInModal = true;
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 = ``;
// <h1>Yang's Toolbox</h1>
// <p>个人工具箱入口 - 简洁高效</p>
const tabBar = document.createElement('div');
tabBar.className = 'tab-bar';
tabBar.innerHTML = `
<button class="tab-btn active" data-tab="high" type="button">Yang's 高频 Tools</button>
<button class="tab-btn" data-tab="low" type="button">Yang's 低频 Tools</button>
<button class="tab-btn" data-tab="bookmarks" type="button">Yang's 书签导航</button>
`;
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 = `
<input id="bookmark-search" class="bookmark-search" placeholder="搜索书签..." />
<button class="bookmark-btn" id="bookmark-search-clear" type="button">清空</button>
<button class="bookmark-btn primary" id="bookmark-admin-btn" type="button">维护书签</button>
`;
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 = `
<div class="admin-header">
<div class="admin-title">书签后台维护</div>
<button class="bookmark-btn" id="bookmark-admin-close" type="button">返回书签</button>
</div>
<div class="admin-columns">
<div class="admin-panel">
<div class="admin-panel-title">分类维护</div>
<form class="admin-form" id="category-form">
<div class="form-row">
<label>编码</label>
<input name="code" required />
</div>
<div class="form-row">
<label>名称</label>
<input name="name" required />
</div>
<div class="form-row">
<label>排序</label>
<input name="sort_order" type="number" value="0" />
</div>
<div class="form-actions">
<button class="bookmark-btn primary" type="submit">保存</button>
<button class="bookmark-btn" type="button" data-action="cancel-category">取消</button>
</div>
</form>
<div class="admin-list" id="category-list"></div>
</div>
<div class="admin-panel">
<div class="admin-panel-title">书签维护</div>
<form class="admin-form" id="bookmark-form">
<div class="form-row">
<label>名称</label>
<input name="name" required />
</div>
<div class="form-row">
<label>网址</label>
<input name="url" required />
</div>
<div class="form-row">
<label>分类</label>
<select name="category_code"></select>
</div>
<div class="form-row">
<label>图标</label>
<div class="icon-input-row">
<input name="icon" placeholder="选择图标或手动输入" />
<button class="bookmark-btn" type="button" data-action="pick-icon">选择</button>
<button class="bookmark-btn" type="button" data-action="clear-icon">清空</button>
</div>
<div class="icon-preview" data-field="icon-preview"></div>
</div>
<div class="form-row">
<label>备注</label>
<textarea name="remark" rows="2"></textarea>
</div>
<div class="form-row">
<label>排序</label>
<input name="sort_order" type="number" value="0" />
</div>
<div class="form-row">
<label>创建时间</label>
<div class="readonly" data-field="created_at">-</div>
</div>
<div class="form-actions">
<button class="bookmark-btn primary" type="submit">保存</button>
<button class="bookmark-btn" type="button" data-action="cancel-bookmark">取消</button>
</div>
</form>
<div class="admin-list" id="bookmark-list"></div>
</div>
</div>
`;
bookmarksPage.appendChild(bookmarkToolbar);
bookmarksPage.appendChild(bookmarkSections);
bookmarksPage.appendChild(bookmarkEmpty);
bookmarksPage.appendChild(bookmarkAdmin);
tabPages.appendChild(highToolsPage);
tabPages.appendChild(lowToolsPage);
tabPages.appendChild(bookmarksPage);
// 创建换背景按钮
const changeBgBtn = document.createElement('button');
changeBgBtn.className = 'change-background-btn';
changeBgBtn.innerHTML = '🎨 换背景';
// 组装页面
mainContainer.appendChild(header);
mainContainer.appendChild(tabBar);
mainContainer.appendChild(tabPages);
document.body.innerHTML = '';
document.body.appendChild(backgroundContainer);
document.body.appendChild(effectsContainer);
document.body.appendChild(mainContainer);
document.body.appendChild(changeBgBtn);
// 顶部右侧滑块(弹窗/本地切换)
const controls = document.createElement('div');
controls.className = 'top-right-controls';
controls.innerHTML = `
<label class="ios-switch" title="弹窗打开/本地替换">
<input type="checkbox" id="open-mode-switch" aria-label="open-mode-toggle" checked>
<span class="slider"></span>
</label>
`;
document.body.appendChild(controls);
this.ensureBgLoaded(0, true);
const iconPicker = document.createElement('div');
iconPicker.className = 'icon-picker';
iconPicker.innerHTML = `
<div class="icon-picker-panel">
<div class="icon-picker-header">
<div>选择图标</div>
<button class="bookmark-btn" type="button" data-action="close-icon-picker">关闭</button>
</div>
<div class="icon-picker-grid"></div>
</div>
`;
document.body.appendChild(iconPicker);
const fitnessDiaryReminder = document.createElement('div');
fitnessDiaryReminder.className = 'fitness-diary-reminder';
fitnessDiaryReminder.innerHTML = `
<div class="fitness-diary-reminder-backdrop" data-action="close-fitness-diary-reminder"></div>
<div class="fitness-diary-reminder-panel" role="dialog" aria-modal="true" aria-labelledby="fitness-diary-reminder-title">
<button class="fitness-diary-reminder-close" type="button" data-action="close-fitness-diary-reminder" aria-label="关闭提醒">×</button>
<div class="fitness-diary-reminder-badge">每日打卡提醒</div>
<h3 id="fitness-diary-reminder-title" class="fitness-diary-reminder-title">运动日记待补写</h3>
<p class="fitness-diary-reminder-text" id="fitness-diary-reminder-text"></p>
<div class="fitness-diary-reminder-dates" id="fitness-diary-reminder-dates"></div>
<div class="fitness-diary-reminder-actions">
<button class="bookmark-btn" type="button" data-action="close-fitness-diary-reminder">稍后再说</button>
<button class="bookmark-btn primary" type="button" data-action="open-fitness-diary">去写</button>
</div>
</div>
`;
document.body.appendChild(fitnessDiaryReminder);
const weeklyReportReminder = document.createElement('div');
weeklyReportReminder.className = 'weekly-report-reminder';
weeklyReportReminder.innerHTML = `
<div class="weekly-report-reminder-backdrop" data-action="close-weekly-report-reminder"></div>
<div class="weekly-report-reminder-panel" role="dialog" aria-modal="true" aria-labelledby="weekly-report-reminder-title">
<button class="weekly-report-reminder-close" type="button" data-action="close-weekly-report-reminder" aria-label="关闭提醒">×</button>
<div class="weekly-report-reminder-badge">每周周报提醒</div>
<h3 id="weekly-report-reminder-title" class="weekly-report-reminder-title">周报待补写</h3>
<p class="weekly-report-reminder-text" id="weekly-report-reminder-text"></p>
<div class="weekly-report-reminder-dates" id="weekly-report-reminder-dates"></div>
<div class="weekly-report-reminder-actions">
<button class="bookmark-btn" type="button" data-action="close-weekly-report-reminder">稍后再说</button>
<button class="bookmark-btn primary" type="button" data-action="open-weekly-report">去写</button>
</div>
</div>
`;
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 = `
<div class="tool-icon">${tool.icon}</div>
<div class="tool-text">${tool.name}</div>
`;
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 = `
<div class="admin-row-main">
<div class="admin-row-title">${cat.code} - ${cat.name}</div>
<div class="admin-row-meta">排序 ${cat.sort_order || 0}</div>
</div>
<div class="admin-row-actions">
<button class="bookmark-btn" type="button" data-action="edit-category" data-code="${cat.code}">编辑</button>
<button class="bookmark-btn danger" type="button" data-action="delete-category" data-code="${cat.code}">删除</button>
</div>
`;
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 = `
<div class="admin-row-main">
<div class="admin-row-title">${item.name || '未命名'}</div>
<div class="admin-row-meta">${cat.name} · 排序 ${item.sort_order || 0}</div>
</div>
<div class="admin-row-actions">
<button class="bookmark-btn" type="button" data-action="edit-bookmark" data-id="${item.id}">编辑</button>
<button class="bookmark-btn danger" type="button" data-action="delete-bookmark" data-id="${item.id}">删除</button>
</div>
`;
this.bookmarkListEl.appendChild(row);
});
}
this.updateIconPreview();
}
setupEventListeners() {
// 换背景按钮事件
document.querySelector('.change-background-btn').addEventListener('click', () => {
this.ensureBgLoaded(this.currentTheme, false);
});
// 工具卡片点击效果 + 打开方式(委托方式,支持动态加载)
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 switchEl = document.getElementById('open-mode-switch');
if (switchEl) {
switchEl.addEventListener('change', () => {
this.openInModal = !!switchEl.checked;
});
}
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 => `<span class="fitness-diary-reminder-date">${date}</span>`)
.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 => `<span class="weekly-report-reminder-date">${item.week_id} (${item.start} ~ ${item.end})</span>`)
.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() {
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() {
// 每30秒自动切换背景
this.rotationInterval = setInterval(() => {
this.nextTheme();
}, 30000);
}
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();
});