// 多宫格导航页面功能 - 炫酷有氛围版本
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.init();
}
init() {
this.createDOM();
this.setupEventListeners();
this.applyTheme(this.currentTheme);
this.startEffects();
this.startBackgroundRotation();
this.loadIcons();
this.loadBookmarks();
this.loadTools();
}
createDOM() {
// 创建背景容器
const backgroundContainer = document.createElement('div');
backgroundContainer.className = 'background-container';
// 创建背景图片
this.themes.forEach((theme, index) => {
const bgImg = document.createElement('img');
bgImg.className = 'background-image';
bgImg.alt = theme.name;
if (index === 0) bgImg.classList.add('active');
backgroundContainer.appendChild(bgImg);
});
// 创建背景遮罩
const overlay = document.createElement('div');
overlay.className = 'background-overlay';
backgroundContainer.appendChild(overlay);
// 创建特效容器
const effectsContainer = document.createElement('div');
effectsContainer.className = 'effects-container';
// 创建主容器
const mainContainer = document.createElement('div');
mainContainer.className = 'main-container';
// 创建标题区域
const header = document.createElement('div');
header.className = 'header';
header.innerHTML = ``;
//
Yang's Toolbox
// 个人工具箱入口 - 简洁高效
const tabBar = document.createElement('div');
tabBar.className = 'tab-bar';
tabBar.innerHTML = `
`;
const tabPages = document.createElement('div');
tabPages.className = 'tab-pages';
// 高频 Tools 页面
const highToolsPage = document.createElement('div');
highToolsPage.className = 'tab-page active';
highToolsPage.dataset.tab = 'high';
const highToolGrid = document.createElement('div');
highToolGrid.className = 'tool-grid';
highToolGrid.id = 'tool-grid-high';
highToolsPage.appendChild(highToolGrid);
// 低频 Tools 页面
const lowToolsPage = document.createElement('div');
lowToolsPage.className = 'tab-page';
lowToolsPage.dataset.tab = 'low';
const lowToolGrid = document.createElement('div');
lowToolGrid.className = 'tool-grid';
lowToolGrid.id = 'tool-grid-low';
lowToolsPage.appendChild(lowToolGrid);
// 书签导航页面
const bookmarksPage = document.createElement('div');
bookmarksPage.className = 'tab-page';
bookmarksPage.dataset.tab = 'bookmarks';
const bookmarkToolbar = document.createElement('div');
bookmarkToolbar.className = 'bookmark-toolbar';
bookmarkToolbar.innerHTML = `
`;
const bookmarkSections = document.createElement('div');
bookmarkSections.className = 'bookmark-sections';
const bookmarkEmpty = document.createElement('div');
bookmarkEmpty.className = 'bookmark-empty';
bookmarkEmpty.textContent = '暂无书签';
const bookmarkAdmin = document.createElement('div');
bookmarkAdmin.className = 'bookmark-admin';
bookmarkAdmin.innerHTML = `
`;
bookmarksPage.appendChild(bookmarkToolbar);
bookmarksPage.appendChild(bookmarkSections);
bookmarksPage.appendChild(bookmarkEmpty);
bookmarksPage.appendChild(bookmarkAdmin);
tabPages.appendChild(highToolsPage);
tabPages.appendChild(lowToolsPage);
tabPages.appendChild(bookmarksPage);
// 创建换背景按钮
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 = `
`;
document.body.appendChild(controls);
this.ensureBgLoaded(0, true);
const iconPicker = document.createElement('div');
iconPicker.className = 'icon-picker';
iconPicker.innerHTML = `
`;
document.body.appendChild(iconPicker);
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');
}
async loadIcons() {
try {
const iconsRes = await fetch('/common/icons_list.json?t=' + Date.now());
if (iconsRes.ok) {
const data = await iconsRes.json();
if (Array.isArray(data)) this.availableIcons = data;
}
} catch {}
this.renderIconPicker();
}
async loadBookmarks() {
try {
const res = await fetch('/api/nav_bookmarks/state?t=' + Date.now());
if (!res.ok) return;
const data = await res.json();
if (!data || !data.ok) return;
this.categories = Array.isArray(data.categories) ? data.categories : [];
this.bookmarks = Array.isArray(data.bookmarks) ? data.bookmarks : [];
this.renderBookmarks();
this.renderAdmin();
} catch {}
}
renderIconPicker() {
if (!this.iconPickerGrid) return;
this.iconPickerGrid.innerHTML = '';
this.availableIcons.forEach(name => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'icon-picker-item';
btn.dataset.icon = name;
const img = document.createElement('img');
img.src = `/icons/16X16PNG/${name}`;
img.alt = name;
btn.appendChild(img);
this.iconPickerGrid.appendChild(btn);
});
}
setActiveTab(tab) {
if (!this.tabBar) return;
this.tabBar.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.tab === tab);
});
(this.tabPages || []).forEach(page => {
if (!page) return;
page.classList.toggle('active', page.dataset.tab === tab);
});
}
async loadTools() {
try {
const res = await fetch('/api/tools?t=' + Date.now());
if (!res.ok) return;
const data = await res.json();
const tools = Array.isArray(data.tools) ? data.tools : [];
this.highFreqTools = tools.filter(t => t.type === 'high');
this.lowFreqTools = tools.filter(t => t.type === 'low');
this.renderToolGrids();
} catch (err) {
console.error('加载 tools.json 失败:', err);
}
}
renderToolGrids() {
const renderGrid = (gridEl, tools) => {
if (!gridEl) return;
gridEl.innerHTML = '';
tools.forEach(tool => {
const card = document.createElement('a');
card.className = 'tool-card';
card.href = tool.href;
card.innerHTML = `
${tool.icon}
${tool.name}
`;
gridEl.appendChild(card);
});
};
renderGrid(this.highToolGrid, this.highFreqTools);
renderGrid(this.lowToolGrid, this.lowFreqTools);
}
openAdmin() {
this.adminVisible = true;
if (this.bookmarkAdmin) this.bookmarkAdmin.classList.add('active');
if (this.bookmarksPage) this.bookmarksPage.classList.add('admin-visible');
this.renderAdmin();
this.bookmarkAdmin.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
closeAdmin() {
this.adminVisible = false;
if (this.bookmarkAdmin) this.bookmarkAdmin.classList.remove('active');
if (this.bookmarksPage) this.bookmarksPage.classList.remove('admin-visible');
}
getIconUrl(icon) {
const iconSrc = String(icon || '').trim();
if (!iconSrc) {
if (this.availableIcons.length > 0) return `/icons/16X16PNG/${this.availableIcons[0]}`;
return '';
}
if (iconSrc.includes('/')) return iconSrc;
return `/icons/16X16PNG/${iconSrc}`;
}
renderBookmarks() {
if (!this.bookmarkContainer) return;
const query = String(this.searchQuery || '').trim().toLowerCase();
const categoryMap = new Map();
this.categories.forEach(c => categoryMap.set(String(c.code), c));
const buckets = {};
this.bookmarks.forEach(b => {
const code = String(b.category_code || '');
if (!buckets[code]) buckets[code] = [];
buckets[code].push(b);
});
const categories = this.categories.slice().sort((a, b) => {
const sa = Number(a.sort_order || 0);
const sb = Number(b.sort_order || 0);
if (sa !== sb) return sa - sb;
return String(a.code).localeCompare(String(b.code));
});
if (!categoryMap.has('') && buckets['']) {
categories.push({ code: '', name: '未分类', sort_order: 9999 });
}
this.bookmarkContainer.innerHTML = '';
let hasAny = false;
categories.forEach(cat => {
const list = (buckets[String(cat.code)] || []).slice().sort((a, b) => {
const sa = Number(a.sort_order || 0);
const sb = Number(b.sort_order || 0);
if (sa !== sb) return sa - sb;
return String(a.created_at || '').localeCompare(String(b.created_at || ''));
});
const filtered = query ? list.filter(item => {
const name = String(item.name || '').toLowerCase();
const url = String(item.url || '').toLowerCase();
const remark = String(item.remark || '').toLowerCase();
return name.includes(query) || url.includes(query) || remark.includes(query);
}) : list;
if (!filtered.length) return;
hasAny = true;
const section = document.createElement('div');
section.className = 'bookmark-section';
const header = document.createElement('div');
header.className = 'bookmark-section-header';
const title = document.createElement('div');
title.className = 'bookmark-section-title';
title.textContent = `${cat.name || '未分类'} (${filtered.length})`;
const toggle = document.createElement('button');
toggle.type = 'button';
toggle.className = 'category-toggle';
const isCollapsed = !!this.collapsedCategories[String(cat.code)];
toggle.textContent = isCollapsed ? '+' : '-';
toggle.dataset.code = String(cat.code);
header.appendChild(title);
header.appendChild(toggle);
const grid = document.createElement('div');
grid.className = 'bookmark-grid';
if (isCollapsed) section.classList.add('collapsed');
filtered.forEach(item => {
const link = document.createElement('a');
link.className = 'bookmark-item';
link.href = item.url || '#';
link.target = '_blank';
const iconUrl = this.getIconUrl(item.icon);
if (iconUrl) {
const img = document.createElement('img');
img.src = iconUrl;
img.className = 'bookmark-icon';
img.alt = item.name || '';
link.appendChild(img);
}
const span = document.createElement('span');
span.className = 'bookmark-text';
span.textContent = item.name || '未命名';
link.appendChild(span);
const tooltip = String(item.remark || item.name || '').trim();
if (tooltip) link.title = tooltip;
link.addEventListener('click', (e) => {
if (this.openInModal) {
e.preventDefault();
const url = link.getAttribute('href') || '/';
window.open(url, '_blank');
}
});
grid.appendChild(link);
});
section.appendChild(header);
section.appendChild(grid);
this.bookmarkContainer.appendChild(section);
});
if (this.bookmarkEmpty) {
this.bookmarkEmpty.style.display = hasAny ? 'none' : 'block';
}
}
renderAdmin() {
if (!this.bookmarkAdmin) return;
const categorySelect = this.bookmarkForm ? this.bookmarkForm.querySelector('select[name="category_code"]') : null;
if (categorySelect) {
categorySelect.innerHTML = '';
const emptyOpt = document.createElement('option');
emptyOpt.value = '';
emptyOpt.textContent = '未分类';
categorySelect.appendChild(emptyOpt);
this.categories.forEach(cat => {
const opt = document.createElement('option');
opt.value = cat.code;
opt.textContent = `${cat.code} - ${cat.name}`;
categorySelect.appendChild(opt);
});
}
if (this.categoryListEl) {
this.categoryListEl.innerHTML = '';
this.categories.forEach(cat => {
const row = document.createElement('div');
row.className = 'admin-row';
row.innerHTML = `
${cat.code} - ${cat.name}
排序 ${cat.sort_order || 0}
`;
this.categoryListEl.appendChild(row);
});
}
if (this.bookmarkListEl) {
this.bookmarkListEl.innerHTML = '';
this.bookmarks.forEach(item => {
const cat = this.categories.find(c => String(c.code) === String(item.category_code)) || { name: '未分类' };
const row = document.createElement('div');
row.className = 'admin-row';
row.innerHTML = `
${item.name || '未命名'}
${cat.name} · 排序 ${item.sort_order || 0}
`;
this.bookmarkListEl.appendChild(row);
});
}
this.updateIconPreview();
}
setupEventListeners() {
// 换背景按钮事件
document.querySelector('.change-background-btn').addEventListener('click', () => {
this.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');
}
});
}
}
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();
});