Files
Toolbox/public/index.js
T
yangxiangyuan ea379116cc feat: 新增思想实验室模块与配套功能
- 新增5个物理仿真实验室:黑洞模拟、日地轨道、3D太阳系、π推导、拉格朗日点
- 新增思想实验室后端路由与鉴权逻辑
- 首页新增思想实验室页签与前端渲染逻辑
- 修复中韩半导体监控标的配置错误
- 新增市场技能推送白名单过滤逻辑与单元测试
2026-08-06 13:39:48 +08:00

1523 lines
55 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.localBgMode = false;
this.localBgFiles = [];
this.localBgIndex = 0;
this.localRotationInterval = null;
// 从 localStorage 恢复状态
try {
this.localBgMode = localStorage.getItem('tb_home_local_bg') === '1';
this.openInModal = localStorage.getItem('tb_home_open_mode') !== '0';
} catch {}
this.themes = [
{
name: '夜空星轨',
background: 'https://images.unsplash.com/photo-1534796636912-3b95b3ab5986?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80',
effect: 'meteor',
cardGradient: 'linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(168, 85, 247, 0.2))',
textColor: '#f8fafc'
},
{
name: '蓝天白云',
background: 'https://images.unsplash.com/photo-1501594907352-04cda38ebc29?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80',
effect: 'meteor',
cardGradient: 'linear-gradient(135deg, rgba(14, 165, 233, 0.2), rgba(56, 189, 248, 0.2))',
textColor: '#1e293b'
},
{
name: '治愈森林',
background: 'https://images.unsplash.com/photo-1448375240586-882707db888b?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80',
effect: 'meteor',
cardGradient: 'linear-gradient(135deg, rgba(34, 197, 94, 0.2), rgba(132, 204, 22, 0.2))',
textColor: '#f8fafc'
},
{
name: '运动风',
background: 'https://images.unsplash.com/photo-1521412644187-9d2e0ad0a2f5?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80',
effect: 'meteor',
cardGradient: 'linear-gradient(135deg, rgba(234, 88, 12, 0.24), rgba(59, 130, 246, 0.24))',
textColor: '#ffffff'
},
{
name: '科技霓虹',
background: 'https://images.unsplash.com/photo-1496307042754-b4aa456c4a2d?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80',
effect: 'meteor',
cardGradient: 'linear-gradient(135deg, rgba(99, 102, 241, 0.26), rgba(236, 72, 153, 0.26))',
textColor: '#ffffff'
},
{
name: '商务简约',
background: 'https://images.unsplash.com/photo-1487015307662-6ce621f0df3f?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80',
effect: 'meteor',
cardGradient: 'linear-gradient(135deg, rgba(226, 232, 240, 0.35), rgba(148, 163, 184, 0.35))',
textColor: '#0b1220'
},
{
name: '暖色日落',
background: 'https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80',
effect: 'meteor',
cardGradient: 'linear-gradient(135deg, rgba(244, 114, 182, 0.25), rgba(251, 146, 60, 0.25))',
textColor: '#ffffff'
},
{
name: '冷色极夜',
background: 'https://images.unsplash.com/photo-1469474968028-56623f02e42e?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80',
effect: 'meteor',
cardGradient: 'linear-gradient(135deg, rgba(14, 165, 233, 0.25), rgba(5, 150, 105, 0.25))',
textColor: '#e6f7ff'
}
];
this.highFreqTools = [];
this.lowFreqTools = [];
this.tools = []; // 保留向后兼容
this.categories = [];
this.bookmarks = [];
this.availableIcons = [];
this.collapsedCategories = {};
this.searchQuery = '';
this.adminVisible = false;
this.editingCategoryCode = '';
this.editingBookmarkId = '';
this.iconPickerTarget = null;
this.activeReminderKey = '';
this.reminderQueue = [];
this.init();
}
init() {
this.createDOM();
this.setupEventListeners();
this.applyTheme(this.currentTheme);
this.startEffects();
this.startBackgroundRotation();
this.loadIcons();
this.loadBookmarks();
this.loadTools();
this.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 = ``;
// <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="labs" type="button">Yang's 思想实验室</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);
// 思想实验室页面(卡片形式与高/低频一致,数据来自 /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 = `
<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);
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 = `
<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);
// 换背景按钮容器(图标按钮 + 本地模式开关,水平排列,放底部)
const bgBtnWrapper = document.createElement('div');
bgBtnWrapper.className = 'bg-btn-wrapper';
bgBtnWrapper.innerHTML = `
<button class="change-background-btn" title="切换背景(或按 B 键)" aria-label="change-background"></button>
<label class="ios-switch bg-local-switch" title="本地默认背景模式">
<input type="checkbox" id="local-bg-switch" aria-label="local-bg-toggle">
<span class="slider"></span>
</label>
`;
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 = `
<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.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 = 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);
}
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 = `
<div class="tool-icon">${lab.icon || '🧪'}</div>
<div class="tool-text">${lab.name || lab.id || '未命名 lab'}</div>
`;
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');
}
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.nextTheme();
});
// 工具卡片点击效果 + 打开方式(委托方式,支持动态加载)
document.querySelector('.tab-pages').addEventListener('click', (e) => {
const card = e.target.closest('.tool-card');
if (!card) return;
card.style.transform = 'scale(0.95)';
setTimeout(() => { card.style.transform = ''; }, 150);
if (this.openInModal) {
e.preventDefault();
window.open(card.getAttribute('href') || '/', '_blank');
}
});
// 键盘快捷键
document.addEventListener('keydown', (e) => {
if (e.key === 'b' || e.key === 'B') {
e.preventDefault();
this.nextTheme();
}
});
// 滑块切换事件
const localBgSwitch = document.getElementById('local-bg-switch');
const openModeSwitch = document.getElementById('open-mode-switch');
if (localBgSwitch) {
localBgSwitch.checked = this.localBgMode;
localBgSwitch.addEventListener('change', () => {
this.localBgMode = !!localBgSwitch.checked;
try { localStorage.setItem('tb_home_local_bg', this.localBgMode ? '1' : '0'); } catch {}
if (this.localBgMode) {
this.stopThemeRotation();
this.startLocalBgRotation();
} else {
this.stopLocalBgRotation();
this.ensureBgLoaded(this.currentTheme, true);
this.startThemeRotation();
}
});
}
if (openModeSwitch) {
openModeSwitch.checked = this.openInModal;
openModeSwitch.addEventListener('change', () => {
this.openInModal = !!openModeSwitch.checked;
try { localStorage.setItem('tb_home_open_mode', this.openInModal ? '1' : '0'); } catch {}
});
}
if (this.tabBar) {
this.tabBar.addEventListener('click', (e) => {
const btn = e.target.closest('.tab-btn');
if (!btn) return;
const tab = btn.dataset.tab;
if (tab) this.setActiveTab(tab);
});
}
const clearBtn = document.getElementById('bookmark-search-clear');
if (this.bookmarkSearchInput) {
this.bookmarkSearchInput.addEventListener('input', () => {
this.searchQuery = this.bookmarkSearchInput.value;
this.renderBookmarks();
});
}
if (clearBtn) {
clearBtn.addEventListener('click', () => {
if (this.bookmarkSearchInput) this.bookmarkSearchInput.value = '';
this.searchQuery = '';
this.renderBookmarks();
});
}
const adminBtn = document.getElementById('bookmark-admin-btn');
if (adminBtn) {
adminBtn.addEventListener('click', () => {
this.openAdmin();
});
}
const adminClose = document.getElementById('bookmark-admin-close');
if (adminClose) {
adminClose.addEventListener('click', () => {
this.closeAdmin();
});
}
if (this.bookmarkContainer) {
this.bookmarkContainer.addEventListener('click', (e) => {
const toggle = e.target.closest('.category-toggle');
if (!toggle) return;
const code = String(toggle.dataset.code || '');
this.collapsedCategories[code] = !this.collapsedCategories[code];
this.renderBookmarks();
});
}
if (this.categoryForm) {
this.categoryForm.addEventListener('submit', async (e) => {
e.preventDefault();
const data = new FormData(this.categoryForm);
const payload = {
code: String(data.get('code') || '').trim(),
name: String(data.get('name') || '').trim(),
sort_order: Number(data.get('sort_order') || 0)
};
try {
if (this.editingCategoryCode) {
await this.apiRequest(`/api/nav_bookmarks/categories/${encodeURIComponent(this.editingCategoryCode)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
} else {
await this.apiRequest('/api/nav_bookmarks/categories', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
}
this.resetCategoryForm();
await this.loadBookmarks();
} catch (err) {
alert(err.message || '保存失败');
}
});
const cancelBtn = this.categoryForm.querySelector('[data-action="cancel-category"]');
if (cancelBtn) {
cancelBtn.addEventListener('click', () => {
this.resetCategoryForm();
});
}
}
if (this.bookmarkForm) {
this.bookmarkForm.addEventListener('submit', async (e) => {
e.preventDefault();
const data = new FormData(this.bookmarkForm);
const payload = {
name: String(data.get('name') || '').trim(),
url: String(data.get('url') || '').trim(),
category_code: String(data.get('category_code') || '').trim(),
icon: String(data.get('icon') || '').trim(),
remark: String(data.get('remark') || '').trim(),
sort_order: Number(data.get('sort_order') || 0)
};
try {
if (this.editingBookmarkId) {
await this.apiRequest(`/api/nav_bookmarks/bookmarks/${encodeURIComponent(this.editingBookmarkId)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
} else {
await this.apiRequest('/api/nav_bookmarks/bookmarks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
}
this.resetBookmarkForm();
await this.loadBookmarks();
} catch (err) {
alert(err.message || '保存失败');
}
});
const cancelBtn = this.bookmarkForm.querySelector('[data-action="cancel-bookmark"]');
if (cancelBtn) {
cancelBtn.addEventListener('click', () => {
this.resetBookmarkForm();
});
}
this.bookmarkForm.addEventListener('input', (e) => {
if (e.target && e.target.name === 'icon') this.updateIconPreview();
});
const pickBtn = this.bookmarkForm.querySelector('[data-action="pick-icon"]');
const clearBtn2 = this.bookmarkForm.querySelector('[data-action="clear-icon"]');
if (pickBtn) {
pickBtn.addEventListener('click', () => {
this.iconPickerTarget = this.bookmarkForm.querySelector('input[name="icon"]');
if (this.iconPicker) this.iconPicker.classList.add('active');
});
}
if (clearBtn2) {
clearBtn2.addEventListener('click', () => {
const input = this.bookmarkForm.querySelector('input[name="icon"]');
if (input) input.value = '';
this.updateIconPreview();
});
}
}
if (this.categoryListEl) {
this.categoryListEl.addEventListener('click', async (e) => {
const btn = e.target.closest('button[data-action]');
if (!btn) return;
const action = btn.dataset.action;
const code = btn.dataset.code;
if (action === 'edit-category') {
const cat = this.categories.find(c => String(c.code) === String(code));
if (cat) this.fillCategoryForm(cat);
}
if (action === 'delete-category') {
if (!confirm('确定删除该分类吗?')) return;
try {
await this.apiRequest(`/api/nav_bookmarks/categories/${encodeURIComponent(code)}`, { method: 'DELETE' });
await this.loadBookmarks();
} catch (err) {
alert(err.message || '删除失败');
}
}
});
}
if (this.bookmarkListEl) {
this.bookmarkListEl.addEventListener('click', async (e) => {
const btn = e.target.closest('button[data-action]');
if (!btn) return;
const action = btn.dataset.action;
const id = btn.dataset.id;
if (action === 'edit-bookmark') {
const item = this.bookmarks.find(b => String(b.id) === String(id));
if (item) this.fillBookmarkForm(item);
}
if (action === 'delete-bookmark') {
if (!confirm('确定删除该书签吗?')) return;
try {
await this.apiRequest(`/api/nav_bookmarks/bookmarks/${encodeURIComponent(id)}`, { method: 'DELETE' });
await this.loadBookmarks();
} catch (err) {
alert(err.message || '删除失败');
}
}
});
}
if (this.iconPicker) {
this.iconPicker.addEventListener('click', (e) => {
const closeBtn = e.target.closest('[data-action="close-icon-picker"]');
if (closeBtn) {
this.iconPicker.classList.remove('active');
return;
}
const btn = e.target.closest('.icon-picker-item');
if (btn && this.iconPickerTarget) {
this.iconPickerTarget.value = btn.dataset.icon || '';
this.updateIconPreview();
this.iconPicker.classList.remove('active');
return;
}
if (e.target === this.iconPicker) {
this.iconPicker.classList.remove('active');
}
});
}
if (this.fitnessDiaryReminder) {
this.fitnessDiaryReminder.addEventListener('click', (e) => {
const actionEl = e.target.closest('[data-action]');
if (!actionEl) return;
const action = actionEl.dataset.action;
if (action === 'close-fitness-diary-reminder') {
this.hideFitnessDiaryReminder();
return;
}
if (action === 'open-fitness-diary') {
this.hideFitnessDiaryReminder();
window.open('/tools/fitness_diary', '_blank', 'noopener');
}
});
}
if (this.weeklyReportReminder) {
this.weeklyReportReminder.addEventListener('click', (e) => {
const actionEl = e.target.closest('[data-action]');
if (!actionEl) return;
const action = actionEl.dataset.action;
if (action === 'close-weekly-report-reminder') {
this.hideWeeklyReportReminder();
return;
}
if (action === 'open-weekly-report') {
this.hideWeeklyReportReminder();
window.open('/tools/weekly', '_blank', 'noopener');
}
});
}
}
queueReminder(reminderKey, openFn) {
if (!reminderKey || typeof openFn !== 'function') return;
if (this.activeReminderKey === reminderKey) return;
if (this.reminderQueue.some(item => item.key === reminderKey)) return;
const reminder = { key: reminderKey, openFn };
if (!this.activeReminderKey) {
this.activeReminderKey = reminderKey;
openFn();
return;
}
this.reminderQueue.push(reminder);
}
finishReminder(reminderKey) {
if (!reminderKey) return;
if (this.activeReminderKey === reminderKey) {
this.activeReminderKey = '';
const nextReminder = this.reminderQueue.shift();
if (nextReminder) {
this.activeReminderKey = nextReminder.key;
nextReminder.openFn();
}
return;
}
this.reminderQueue = this.reminderQueue.filter(item => item.key !== reminderKey);
}
formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
getFitnessDiaryDateRange(days = 5) {
const end = new Date();
const start = new Date(end);
start.setDate(end.getDate() - (days - 1));
return {
start: this.formatDate(start),
end: this.formatDate(end)
};
}
getFitnessDiaryReminderSummary(records, days = 5) {
const existingDates = new Set(
(Array.isArray(records) ? records : [])
.map(item => String(item && item.record_date ? item.record_date : '').slice(0, 10))
.filter(Boolean)
);
const missingDates = [];
const today = new Date();
for (let i = 0; i < days; i += 1) {
const date = new Date(today);
date.setDate(today.getDate() - i);
const dateStr = this.formatDate(date);
if (!existingDates.has(dateStr)) missingDates.push(dateStr);
}
return {
days,
missingDates: missingDates.reverse(),
missingCount: missingDates.length
};
}
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 {
// 【首页提醒专用】直接调后端聚合接口 /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('input[name="url"]');
const catSelect = this.bookmarkForm.querySelector('select[name="category_code"]');
const iconInput = this.bookmarkForm.querySelector('input[name="icon"]');
const remarkInput = this.bookmarkForm.querySelector('textarea[name="remark"]');
const sortInput = this.bookmarkForm.querySelector('input[name="sort_order"]');
if (nameInput) nameInput.value = item.name || '';
if (urlInput) urlInput.value = item.url || '';
if (catSelect) catSelect.value = item.category_code || '';
if (iconInput) iconInput.value = item.icon || '';
if (remarkInput) remarkInput.value = item.remark || '';
if (sortInput) sortInput.value = Number(item.sort_order || 0);
if (this.bookmarkCreatedAtEl) this.bookmarkCreatedAtEl.textContent = item.created_at || '-';
this.updateIconPreview();
}
updateIconPreview() {
if (!this.bookmarkForm || !this.iconPreviewEl) return;
const iconInput = this.bookmarkForm.querySelector('input[name="icon"]');
if (!iconInput) return;
const iconUrl = this.getIconUrl(iconInput.value);
this.iconPreviewEl.innerHTML = '';
if (iconUrl) {
const img = document.createElement('img');
img.src = iconUrl;
img.alt = iconInput.value || '';
this.iconPreviewEl.appendChild(img);
} else {
this.iconPreviewEl.textContent = '未选择';
}
}
applyTheme(themeIndex) {
const theme = this.themes[themeIndex];
// 更新背景图片
document.querySelectorAll('.background-image').forEach((img, index) => {
img.classList.toggle('active', index === themeIndex);
});
this.ensureBgLoaded(themeIndex, true);
// 强制使用纯白文字
document.body.style.color = '#ffffff';
document.documentElement.style.setProperty('--card-text-color', '#ffffff');
// 移除对 tool-card 的个别样式覆盖,统一使用 CSS 类控制
/*
document.querySelectorAll('.tool-card').forEach(card => {
card.style.background = theme.cardGradient;
card.style.color = theme.textColor;
});
*/
// 清除所有特效
this.clearEffects();
// 应用新特效
setTimeout(() => {
this.startEffect(theme.effect);
}, 500);
}
async ensureBgLoaded(themeIndex, preferLocal = true) {
try {
const theme = this.themes[themeIndex];
const imgs = document.querySelectorAll('.background-image');
const img = imgs[themeIndex];
if (!img || !theme) return;
const r = await fetch(`/api/home/bg?theme=${encodeURIComponent(theme.name)}&url=${encodeURIComponent(theme.background)}&prefer_local=${preferLocal ? '1' : '0'}`);
const j = await r.json();
img.src = (j && j.url) ? j.url : theme.background;
} catch {
const theme = this.themes[themeIndex];
const imgs = document.querySelectorAll('.background-image');
const img = imgs[themeIndex];
if (img && theme) img.src = theme.background;
}
}
nextTheme() {
if (this.localBgMode) {
this.nextLocalBg();
return;
}
this.currentTheme = (this.currentTheme + 1) % this.themes.length;
this.applyTheme(this.currentTheme);
// 显示当前主题名称
const themeName = this.themes[this.currentTheme].name;
this.showNotification(`切换到: ${themeName}`);
}
startEffects() {
const currentTheme = this.themes[this.currentTheme];
this.startEffect(currentTheme.effect);
}
startEffect(effectType) {
const effectsContainer = document.querySelector('.effects-container');
switch (effectType) {
case 'meteor':
this.startMeteorEffect(effectsContainer);
break;
default:
this.startMeteorEffect(effectsContainer);
break;
}
}
startMeteorEffect(container) {
// 创建流星
const createMeteor = () => {
const meteor = document.createElement('div');
meteor.className = 'meteor';
// 随机位置
const left = Math.random() * 100;
const top = Math.random() * 50;
meteor.style.left = `${left}%`;
meteor.style.top = `${top}%`;
// 随机大小和速度
const size = 1 + Math.random() * 2;
const duration = 8 + Math.random() * 4;
meteor.style.width = `${size}px`;
meteor.style.height = `${size}px`;
meteor.style.animationDuration = `${duration}s`;
container.appendChild(meteor);
// 移除流星
setTimeout(() => {
if (meteor.parentNode) {
meteor.parentNode.removeChild(meteor);
}
}, duration * 1000);
};
// 定期创建流星
this.meteorInterval = setInterval(createMeteor, 3000);
// 初始创建几个流星
for (let i = 0; i < 3; i++) {
setTimeout(createMeteor, i * 1000);
}
}
startPaperPlaneEffect(container) {
// 创建纸飞机
const createPaperPlane = () => {
const plane = document.createElement('div');
plane.className = 'paper-plane';
plane.innerHTML = '✈️';
// 随机位置
const top = 10 + Math.random() * 70;
plane.style.top = `${top}%`;
// 随机延迟和速度
const delay = Math.random() * 5;
const duration = 12 + Math.random() * 6;
plane.style.animationDelay = `${delay}s`;
plane.style.animationDuration = `${duration}s`;
container.appendChild(plane);
// 移除飞机
setTimeout(() => {
if (plane.parentNode) {
plane.parentNode.removeChild(plane);
}
}, (duration + delay) * 1000);
};
// 定期创建纸飞机
this.planeInterval = setInterval(createPaperPlane, 8000);
// 初始创建几个纸飞机
for (let i = 0; i < 2; i++) {
setTimeout(createPaperPlane, i * 2000);
}
}
startForestEffect(container) {
// 森林主题的飘叶效果
const createLeaf = () => {
const leaf = document.createElement('div');
leaf.className = 'paper-plane';
leaf.innerHTML = '🍃';
leaf.style.fontSize = '16px';
// 随机位置
const left = Math.random() * 100;
const top = Math.random() * 100;
leaf.style.left = `${left}%`;
leaf.style.top = `${top}%`;
// 随机动画参数
const delay = Math.random() * 3;
const duration = 10 + Math.random() * 8;
leaf.style.animationDelay = `${delay}s`;
leaf.style.animationDuration = `${duration}s`;
container.appendChild(leaf);
// 移除叶子
setTimeout(() => {
if (leaf.parentNode) {
leaf.parentNode.removeChild(leaf);
}
}, (duration + delay) * 1000);
};
// 定期创建叶子
this.leafInterval = setInterval(createLeaf, 4000);
// 初始创建几个叶子
for (let i = 0; i < 4; i++) {
setTimeout(createLeaf, i * 1000);
}
}
clearEffects() {
// 清除所有定时器
if (this.meteorInterval) clearInterval(this.meteorInterval);
if (this.planeInterval) clearInterval(this.planeInterval);
if (this.leafInterval) clearInterval(this.leafInterval);
// 清除所有特效元素
const effectsContainer = document.querySelector('.effects-container');
if (effectsContainer) {
effectsContainer.innerHTML = '';
}
}
startBackgroundRotation() {
this.startThemeRotation();
}
startThemeRotation() {
if (this.localBgMode) return;
this.stopThemeRotation();
this.rotationInterval = setInterval(() => {
this.nextTheme();
}, 30000);
}
stopThemeRotation() {
if (this.rotationInterval) {
clearInterval(this.rotationInterval);
this.rotationInterval = null;
}
}
startLocalBgRotation() {
this.loadLocalBgFiles().then(() => {
this.stopLocalBgRotation();
this.localRotationInterval = setInterval(() => {
this.nextLocalBg();
}, 30000);
});
}
stopLocalBgRotation() {
if (this.localRotationInterval) {
clearInterval(this.localRotationInterval);
this.localRotationInterval = null;
}
}
async loadLocalBgFiles() {
if (this.localBgFiles.length > 0) return this.localBgFiles;
try {
const r = await fetch('/api/home/bg/default-list?t=' + Date.now());
const j = await r.json();
if (j.ok && j.files.length) {
this.localBgFiles = j.files;
this.localBgIndex = 0;
// 加载第一张
const imgs = document.querySelectorAll('.background-image');
if (imgs.length) {
imgs.forEach(img => { img.src = `/home-bg/default-c21f969b/${this.localBgFiles[0]}?v=${Date.now()}`; });
}
}
} catch {}
return this.localBgFiles;
}
async nextLocalBg() {
if (!this.localBgFiles.length) {
await this.loadLocalBgFiles();
if (!this.localBgFiles.length) return;
}
// 随机选取,避免重复当前
let newIndex;
if (this.localBgFiles.length > 1) {
do {
newIndex = Math.floor(Math.random() * this.localBgFiles.length);
} while (newIndex === this.localBgIndex);
} else {
newIndex = 0;
}
this.localBgIndex = newIndex;
const file = this.localBgFiles[this.localBgIndex];
const imgs = document.querySelectorAll('.background-image');
if (imgs.length) {
imgs.forEach(img => { img.src = `/home-bg/default-c21f969b/${file}?v=${Date.now()}`; });
}
this.showNotification(`本地背景: ${this.localBgIndex + 1}/${this.localBgFiles.length}`);
}
showNotification(message) {
// 创建通知元素
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 2rem;
left: 50%;
transform: translateX(-50%);
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
color: var(--text-light);
padding: 0.75rem 1.5rem;
border-radius: 25px;
font-size: 0.9rem;
z-index: 1000;
opacity: 0;
transition: opacity 0.3s ease;
`;
notification.textContent = message;
document.body.appendChild(notification);
// 显示动画
setTimeout(() => {
notification.style.opacity = '1';
}, 10);
// 隐藏动画
setTimeout(() => {
notification.style.opacity = '0';
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 300);
}, 2000);
}
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
new ToolboxHome();
});