bbc7ca6523
- 新增小程序个人收藏回收站页面,支持查看已删除收藏、恢复与彻底删除操作 - 修复个人收藏文件上传时的中文名乱码问题,新增原文件名透传参数 - 接入阿里云百炼TTS服务,实现运动提醒模块的云端语音合成与本地缓存,支持系统TTS与云端MP3双降级播放 - 新增语音引擎探测与异常预警UI,覆盖web端与移动端 - 优化web端订单盒页面布局,修复卡片操作按钮换行问题 - 新增调试探针脚本、ESLint配置与相关lint脚本 - 完善后端接口与路由,更新项目依赖与配置文件
1905 lines
72 KiB
JavaScript
1905 lines
72 KiB
JavaScript
// 多宫格导航页面功能 - 炫酷有氛围版本
|
||
|
||
class ToolboxHome {
|
||
constructor() {
|
||
this.currentTheme = 0;
|
||
this.openInModal = true;
|
||
this.localBgMode = false;
|
||
this.localBgFiles = [];
|
||
this.localBgIndex = 0;
|
||
this.localRotationInterval = null;
|
||
// 从 localStorage 恢复状态
|
||
try {
|
||
this.localBgMode = localStorage.getItem('tb_home_local_bg') === '1';
|
||
this.openInModal = localStorage.getItem('tb_home_open_mode') !== '0';
|
||
} catch {}
|
||
this.themes = [
|
||
{
|
||
name: '夜空星轨',
|
||
background: 'https://images.unsplash.com/photo-1534796636912-3b95b3ab5986?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80',
|
||
effect: 'meteor',
|
||
cardGradient: 'linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(168, 85, 247, 0.2))',
|
||
textColor: '#f8fafc'
|
||
},
|
||
{
|
||
name: '蓝天白云',
|
||
background: 'https://images.unsplash.com/photo-1501594907352-04cda38ebc29?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80',
|
||
effect: 'meteor',
|
||
cardGradient: 'linear-gradient(135deg, rgba(14, 165, 233, 0.2), rgba(56, 189, 248, 0.2))',
|
||
textColor: '#1e293b'
|
||
},
|
||
{
|
||
name: '治愈森林',
|
||
background: 'https://images.unsplash.com/photo-1448375240586-882707db888b?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80',
|
||
effect: 'meteor',
|
||
cardGradient: 'linear-gradient(135deg, rgba(34, 197, 94, 0.2), rgba(132, 204, 22, 0.2))',
|
||
textColor: '#f8fafc'
|
||
},
|
||
{
|
||
name: '运动风',
|
||
background: 'https://images.unsplash.com/photo-1521412644187-9d2e0ad0a2f5?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80',
|
||
effect: 'meteor',
|
||
cardGradient: 'linear-gradient(135deg, rgba(234, 88, 12, 0.24), rgba(59, 130, 246, 0.24))',
|
||
textColor: '#ffffff'
|
||
},
|
||
{
|
||
name: '科技霓虹',
|
||
background: 'https://images.unsplash.com/photo-1496307042754-b4aa456c4a2d?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80',
|
||
effect: 'meteor',
|
||
cardGradient: 'linear-gradient(135deg, rgba(99, 102, 241, 0.26), rgba(236, 72, 153, 0.26))',
|
||
textColor: '#ffffff'
|
||
},
|
||
{
|
||
name: '商务简约',
|
||
background: 'https://images.unsplash.com/photo-1487015307662-6ce621f0df3f?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80',
|
||
effect: 'meteor',
|
||
cardGradient: 'linear-gradient(135deg, rgba(226, 232, 240, 0.35), rgba(148, 163, 184, 0.35))',
|
||
textColor: '#0b1220'
|
||
},
|
||
{
|
||
name: '暖色日落',
|
||
background: 'https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80',
|
||
effect: 'meteor',
|
||
cardGradient: 'linear-gradient(135deg, rgba(244, 114, 182, 0.25), rgba(251, 146, 60, 0.25))',
|
||
textColor: '#ffffff'
|
||
},
|
||
{
|
||
name: '冷色极夜',
|
||
background: 'https://images.unsplash.com/photo-1469474968028-56623f02e42e?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80',
|
||
effect: 'meteor',
|
||
cardGradient: 'linear-gradient(135deg, rgba(14, 165, 233, 0.25), rgba(5, 150, 105, 0.25))',
|
||
textColor: '#e6f7ff'
|
||
}
|
||
];
|
||
|
||
this.highFreqTools = [];
|
||
this.lowFreqTools = [];
|
||
this.tools = []; // 保留向后兼容
|
||
|
||
this.categories = [];
|
||
this.bookmarks = [];
|
||
this.availableIcons = [];
|
||
this.collapsedCategories = {};
|
||
this.searchQuery = '';
|
||
this.adminVisible = false;
|
||
this.editingCategoryCode = '';
|
||
this.editingBookmarkId = '';
|
||
this.iconPickerTarget = null;
|
||
// 后台维护状态(tab + 分页 + 筛选)
|
||
this.adminTab = 'bookmarks';
|
||
this.adminCatPage = 1;
|
||
this.adminCatPageSize = 10;
|
||
this.adminBookPage = 1;
|
||
this.adminBookPageSize = 50;
|
||
this.adminBookCatFilter = '';
|
||
this.adminBookQuery = '';
|
||
this.adminCategories = [];
|
||
this.adminBookmarks = [];
|
||
this.adminCatTotal = 0;
|
||
this.adminBookTotal = 0;
|
||
this.adminSearchTimer = null;
|
||
this.activeReminderKey = '';
|
||
this.reminderQueue = [];
|
||
|
||
this.init();
|
||
}
|
||
|
||
init() {
|
||
this.createDOM();
|
||
this.setupEventListeners();
|
||
this.applyTheme(this.currentTheme);
|
||
this.startEffects();
|
||
this.startBackgroundRotation();
|
||
this.loadIcons();
|
||
this.loadBookmarks();
|
||
this.loadTools();
|
||
this.loadThoughtLabs();
|
||
this.loadFitnessDiaryReminder();
|
||
this.loadWeeklyReportReminder();
|
||
}
|
||
|
||
createDOM() {
|
||
// 创建背景容器
|
||
const backgroundContainer = document.createElement('div');
|
||
backgroundContainer.className = 'background-container';
|
||
|
||
// 创建背景图片
|
||
this.themes.forEach((theme, index) => {
|
||
const bgImg = document.createElement('img');
|
||
bgImg.className = 'background-image';
|
||
bgImg.alt = theme.name;
|
||
if (index === 0) bgImg.classList.add('active');
|
||
backgroundContainer.appendChild(bgImg);
|
||
});
|
||
|
||
// 创建背景遮罩
|
||
const overlay = document.createElement('div');
|
||
overlay.className = 'background-overlay';
|
||
backgroundContainer.appendChild(overlay);
|
||
|
||
// 创建特效容器
|
||
const effectsContainer = document.createElement('div');
|
||
effectsContainer.className = 'effects-container';
|
||
|
||
// 创建主容器
|
||
const mainContainer = document.createElement('div');
|
||
mainContainer.className = 'main-container';
|
||
|
||
// 创建标题区域
|
||
const header = document.createElement('div');
|
||
header.className = 'header';
|
||
header.innerHTML = ``;
|
||
// <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-tabs" id="admin-tabs">
|
||
<button class="bookmark-btn admin-tab-btn active" data-adm-tab="bookmarks" type="button">🔖 书签管理</button>
|
||
<button class="bookmark-btn admin-tab-btn" data-adm-tab="categories" type="button">📂 分类管理</button>
|
||
</div>
|
||
<div class="admin-tab-page active" data-adm-page="bookmarks">
|
||
<div class="admin-toolbar">
|
||
<button class="bookmark-btn primary" id="bookmark-add-btn" type="button">新增书签</button>
|
||
<select id="bookmark-admin-category-filter" class="bookmark-filter-select" title="按分类筛选"></select>
|
||
<input id="bookmark-admin-search" class="bookmark-search admin-search" placeholder="搜索名称/网址/备注..." autocomplete="off" />
|
||
<button class="bookmark-btn" id="bookmark-admin-search-clear" type="button">清空</button>
|
||
<div class="admin-count" id="bookmark-count"></div>
|
||
</div>
|
||
<div class="admin-table-wrap">
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th class="th-icon"></th>
|
||
<th>名称</th>
|
||
<th>网址</th>
|
||
<th>分类</th>
|
||
<th>排序</th>
|
||
<th>创建时间</th>
|
||
<th class="th-actions">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="bookmark-table-body"></tbody>
|
||
</table>
|
||
</div>
|
||
<div class="admin-pagination" id="bookmark-pagination"></div>
|
||
</div>
|
||
<div class="admin-tab-page" data-adm-page="categories">
|
||
<div class="admin-toolbar">
|
||
<button class="bookmark-btn primary" id="category-add-btn" type="button">新增分类</button>
|
||
<div class="admin-count" id="category-count"></div>
|
||
</div>
|
||
<div class="admin-table-wrap">
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th>编码</th>
|
||
<th>名称</th>
|
||
<th>排序</th>
|
||
<th>书签数</th>
|
||
<th class="th-actions">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="category-table-body"></tbody>
|
||
</table>
|
||
</div>
|
||
<div class="admin-pagination" id="category-pagination"></div>
|
||
</div>
|
||
`;
|
||
|
||
// 后台维护弹窗(新增/编辑分类、书签共用)
|
||
const adminModal = document.createElement('div');
|
||
adminModal.className = 'nav-modal-overlay';
|
||
adminModal.id = 'nav-admin-modal';
|
||
adminModal.innerHTML = `
|
||
<div class="nav-modal" role="dialog" aria-modal="true" aria-labelledby="nav-admin-modal-title">
|
||
<div class="nav-modal-header">
|
||
<div class="nav-modal-title" id="nav-admin-modal-title">新增分类</div>
|
||
<button class="bookmark-btn nav-modal-close" id="nav-admin-modal-close" type="button">✕</button>
|
||
</div>
|
||
<div class="nav-modal-body">
|
||
<form class="admin-form nav-form" id="admin-category-form">
|
||
<div class="form-row">
|
||
<label>编码</label>
|
||
<input name="code" required placeholder="如 12 / dev / life" />
|
||
</div>
|
||
<div class="form-row">
|
||
<label>名称</label>
|
||
<input name="name" required placeholder="分类名称" />
|
||
</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>
|
||
<form class="admin-form nav-form" id="admin-bookmark-form" style="display:none">
|
||
<div class="form-row">
|
||
<label>名称</label>
|
||
<input name="name" required placeholder="书签名称" />
|
||
</div>
|
||
<div class="form-row">
|
||
<label>网址</label>
|
||
<textarea name="url" rows="3" required placeholder="https://..."></textarea>
|
||
</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>
|
||
</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);
|
||
document.body.appendChild(adminModal);
|
||
|
||
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 = adminModal.querySelector('#admin-category-form');
|
||
this.bookmarkForm = adminModal.querySelector('#admin-bookmark-form');
|
||
this.categoryListEl = bookmarkAdmin.querySelector('#category-table-body');
|
||
this.bookmarkListEl = bookmarkAdmin.querySelector('#bookmark-table-body');
|
||
this.bookmarkSearchInput = bookmarkToolbar.querySelector('#bookmark-search');
|
||
this.bookmarkCreatedAtEl = adminModal.querySelector('[data-field="created_at"]');
|
||
this.iconPreviewEl = adminModal.querySelector('[data-field="icon-preview"]');
|
||
this.navAdminModal = adminModal;
|
||
this.navAdminModalTitle = adminModal.querySelector('#nav-admin-modal-title');
|
||
this.adminTabsEl = bookmarkAdmin.querySelector('#admin-tabs');
|
||
this.categoryPaginationEl = bookmarkAdmin.querySelector('#category-pagination');
|
||
this.bookmarkPaginationEl = bookmarkAdmin.querySelector('#bookmark-pagination');
|
||
this.bookmarkAdminCatFilter = bookmarkAdmin.querySelector('#bookmark-admin-category-filter');
|
||
this.bookmarkAdminSearchInput = bookmarkAdmin.querySelector('#bookmark-admin-search');
|
||
this.categoryCountEl = bookmarkAdmin.querySelector('#category-count');
|
||
this.bookmarkCountEl = bookmarkAdmin.querySelector('#bookmark-count');
|
||
this.iconPicker = iconPicker;
|
||
this.iconPickerGrid = iconPicker.querySelector('.icon-picker-grid');
|
||
this.fitnessDiaryReminder = fitnessDiaryReminder;
|
||
this.fitnessDiaryReminderText = fitnessDiaryReminder.querySelector('#fitness-diary-reminder-text');
|
||
this.fitnessDiaryReminderDates = fitnessDiaryReminder.querySelector('#fitness-diary-reminder-dates');
|
||
this.weeklyReportReminder = weeklyReportReminder;
|
||
this.weeklyReportReminderText = weeklyReportReminder.querySelector('#weekly-report-reminder-text');
|
||
this.weeklyReportReminderDates = weeklyReportReminder.querySelector('#weekly-report-reminder-dates');
|
||
}
|
||
|
||
async loadIcons() {
|
||
try {
|
||
const iconsRes = await fetch('/common/icons_list.json?t=' + Date.now());
|
||
if (iconsRes.ok) {
|
||
const data = await iconsRes.json();
|
||
if (Array.isArray(data)) this.availableIcons = data;
|
||
}
|
||
} catch {}
|
||
this.renderIconPicker();
|
||
}
|
||
|
||
async loadBookmarks() {
|
||
try {
|
||
const res = await fetch('/api/nav_bookmarks/state?t=' + Date.now());
|
||
if (!res.ok) return;
|
||
const data = await res.json();
|
||
if (!data || !data.ok) return;
|
||
this.categories = Array.isArray(data.categories) ? data.categories : [];
|
||
this.bookmarks = Array.isArray(data.bookmarks) ? data.bookmarks : [];
|
||
this.renderBookmarks();
|
||
this.renderAdmin();
|
||
} catch {}
|
||
}
|
||
|
||
renderIconPicker() {
|
||
if (!this.iconPickerGrid) return;
|
||
this.iconPickerGrid.innerHTML = '';
|
||
this.availableIcons.forEach(name => {
|
||
const btn = document.createElement('button');
|
||
btn.type = 'button';
|
||
btn.className = 'icon-picker-item';
|
||
btn.dataset.icon = name;
|
||
const img = document.createElement('img');
|
||
img.src = `/icons/16X16PNG/${name}`;
|
||
img.alt = name;
|
||
btn.appendChild(img);
|
||
this.iconPickerGrid.appendChild(btn);
|
||
});
|
||
}
|
||
|
||
setActiveTab(tab) {
|
||
if (!this.tabBar) return;
|
||
this.tabBar.querySelectorAll('.tab-btn').forEach(btn => {
|
||
btn.classList.toggle('active', btn.dataset.tab === tab);
|
||
});
|
||
(this.tabPages || []).forEach(page => {
|
||
if (!page) return;
|
||
page.classList.toggle('active', page.dataset.tab === tab);
|
||
});
|
||
}
|
||
|
||
async loadTools() {
|
||
try {
|
||
const res = await fetch('/api/tools?t=' + Date.now());
|
||
if (!res.ok) return;
|
||
const data = await res.json();
|
||
const tools = Array.isArray(data.tools) ? data.tools : [];
|
||
this.highFreqTools = tools.filter(t => t.type === 'high');
|
||
this.lowFreqTools = tools.filter(t => t.type === 'low');
|
||
this.renderToolGrids();
|
||
} catch (err) {
|
||
console.error('加载 tools.json 失败:', err);
|
||
}
|
||
}
|
||
|
||
renderToolGrids() {
|
||
const renderGrid = (gridEl, tools) => {
|
||
if (!gridEl) return;
|
||
gridEl.innerHTML = '';
|
||
tools.forEach(tool => {
|
||
const card = document.createElement('a');
|
||
card.className = 'tool-card';
|
||
card.href = tool.href;
|
||
card.innerHTML = `
|
||
<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');
|
||
this.closeAdminModal();
|
||
}
|
||
|
||
getIconUrl(icon) {
|
||
const iconSrc = String(icon || '').trim();
|
||
if (!iconSrc) {
|
||
if (this.availableIcons.length > 0) return `/icons/16X16PNG/${this.availableIcons[0]}`;
|
||
return '';
|
||
}
|
||
if (iconSrc.includes('/')) return iconSrc;
|
||
return `/icons/16X16PNG/${iconSrc}`;
|
||
}
|
||
|
||
renderBookmarks() {
|
||
if (!this.bookmarkContainer) return;
|
||
const query = String(this.searchQuery || '').trim().toLowerCase();
|
||
const categoryMap = new Map();
|
||
this.categories.forEach(c => categoryMap.set(String(c.code), c));
|
||
const buckets = {};
|
||
this.bookmarks.forEach(b => {
|
||
const code = String(b.category_code || '');
|
||
if (!buckets[code]) buckets[code] = [];
|
||
buckets[code].push(b);
|
||
});
|
||
const categories = this.categories.slice().sort((a, b) => {
|
||
const sa = Number(a.sort_order || 0);
|
||
const sb = Number(b.sort_order || 0);
|
||
if (sa !== sb) return sa - sb;
|
||
return String(a.code).localeCompare(String(b.code));
|
||
});
|
||
if (!categoryMap.has('') && buckets['']) {
|
||
categories.push({ code: '', name: '未分类', sort_order: 9999 });
|
||
}
|
||
this.bookmarkContainer.innerHTML = '';
|
||
let hasAny = false;
|
||
categories.forEach(cat => {
|
||
const list = (buckets[String(cat.code)] || []).slice().sort((a, b) => {
|
||
const sa = Number(a.sort_order || 0);
|
||
const sb = Number(b.sort_order || 0);
|
||
if (sa !== sb) return sa - sb;
|
||
return String(a.created_at || '').localeCompare(String(b.created_at || ''));
|
||
});
|
||
const filtered = query ? list.filter(item => {
|
||
const name = String(item.name || '').toLowerCase();
|
||
const url = String(item.url || '').toLowerCase();
|
||
const remark = String(item.remark || '').toLowerCase();
|
||
return name.includes(query) || url.includes(query) || remark.includes(query);
|
||
}) : list;
|
||
if (!filtered.length) return;
|
||
hasAny = true;
|
||
const section = document.createElement('div');
|
||
section.className = 'bookmark-section';
|
||
const header = document.createElement('div');
|
||
header.className = 'bookmark-section-header';
|
||
const title = document.createElement('div');
|
||
title.className = 'bookmark-section-title';
|
||
title.textContent = `${cat.name || '未分类'} (${filtered.length})`;
|
||
const toggle = document.createElement('button');
|
||
toggle.type = 'button';
|
||
toggle.className = 'category-toggle';
|
||
const isCollapsed = !!this.collapsedCategories[String(cat.code)];
|
||
toggle.textContent = isCollapsed ? '+' : '-';
|
||
toggle.dataset.code = String(cat.code);
|
||
header.appendChild(title);
|
||
header.appendChild(toggle);
|
||
const grid = document.createElement('div');
|
||
grid.className = 'bookmark-grid';
|
||
if (isCollapsed) section.classList.add('collapsed');
|
||
filtered.forEach(item => {
|
||
const link = document.createElement('a');
|
||
link.className = 'bookmark-item';
|
||
link.href = item.url || '#';
|
||
link.target = '_blank';
|
||
const iconUrl = this.getIconUrl(item.icon);
|
||
if (iconUrl) {
|
||
const img = document.createElement('img');
|
||
img.src = iconUrl;
|
||
img.className = 'bookmark-icon';
|
||
img.alt = item.name || '';
|
||
link.appendChild(img);
|
||
}
|
||
const span = document.createElement('span');
|
||
span.className = 'bookmark-text';
|
||
span.textContent = item.name || '未命名';
|
||
link.appendChild(span);
|
||
const tooltip = String(item.remark || item.name || '').trim();
|
||
if (tooltip) link.title = tooltip;
|
||
link.addEventListener('click', (e) => {
|
||
if (this.openInModal) {
|
||
e.preventDefault();
|
||
const url = link.getAttribute('href') || '/';
|
||
window.open(url, '_blank');
|
||
}
|
||
});
|
||
grid.appendChild(link);
|
||
});
|
||
section.appendChild(header);
|
||
section.appendChild(grid);
|
||
this.bookmarkContainer.appendChild(section);
|
||
});
|
||
if (this.bookmarkEmpty) {
|
||
this.bookmarkEmpty.style.display = hasAny ? 'none' : 'block';
|
||
}
|
||
}
|
||
|
||
renderAdmin() {
|
||
if (!this.bookmarkAdmin) return;
|
||
if (!this.adminVisible) return;
|
||
this.switchAdminTab(this.adminTab);
|
||
}
|
||
|
||
// 后台维护 tab 切换(分类 / 书签)
|
||
switchAdminTab(tab) {
|
||
if (!this.bookmarkAdmin) return;
|
||
this.adminTab = tab;
|
||
this.bookmarkAdmin.querySelectorAll('.admin-tab-btn').forEach(btn => {
|
||
btn.classList.toggle('active', btn.dataset.admTab === tab);
|
||
});
|
||
this.bookmarkAdmin.querySelectorAll('.admin-tab-page').forEach(p => {
|
||
p.classList.toggle('active', p.dataset.admPage === tab);
|
||
});
|
||
if (tab === 'bookmarks') {
|
||
this.renderAdminCategoryFilter();
|
||
this.loadAdminBookmarks();
|
||
} else {
|
||
this.loadAdminCategories();
|
||
}
|
||
}
|
||
|
||
esc(s) {
|
||
return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||
}
|
||
|
||
// 判断网址是否为文件下载链接(按扩展名 + 下载关键字)
|
||
isFileUrl(url) {
|
||
const raw = String(url || '');
|
||
const path = raw.split(/[?#]/)[0] || '';
|
||
if (/\.(exe|msi|zip|rar|7z|tar|gz|bz2|pdf|doc|docx|xls|xlsx|ppt|pptx|csv|json|apk|ipa|dmg|pkg|iso|jar|torrent|xmind|dwg)(\/|$)/i.test(path)) return true;
|
||
if (/\.(png|jpe?g|gif|webp|svg|bmp|ico|avif|mp3|wav|flac|ogg|aac|mp4|mkv|mov|avi|m4a|wmv|flv|torrent)(\/|$)/i.test(path)) return true;
|
||
if (/\/(download|downloads|dl|get|file|files)\//i.test(path)) return true;
|
||
if (/[?&](download|dl|file|action)=/i.test(raw)) return true;
|
||
return false;
|
||
}
|
||
|
||
// 统一按北京时间展示创建时间
|
||
fmtNavTime(iso) {
|
||
if (!iso) return '-';
|
||
try {
|
||
const parts = new Intl.DateTimeFormat('zh-CN', {
|
||
timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit',
|
||
hour: '2-digit', minute: '2-digit', hour12: false
|
||
}).formatToParts(new Date(iso));
|
||
const get = t => { const p = parts.find(x => x.type === t); return p ? p.value : ''; };
|
||
return `${get('year')}-${get('month')}-${get('day')} ${get('hour')}:${get('minute')}`;
|
||
} catch {
|
||
return String(iso || '').slice(0, 16).replace('T', ' ');
|
||
}
|
||
}
|
||
|
||
getCategoryName(code) {
|
||
const cat = (this.categories || []).find(c => String(c.code) === String(code == null ? '' : code));
|
||
return cat ? cat.name : '未分类';
|
||
}
|
||
|
||
async loadAdminCategories() {
|
||
if (!this.bookmarkAdmin || !this.categoryListEl) return;
|
||
try {
|
||
const res = await fetch(`/api/nav_bookmarks/categories?page=${this.adminCatPage}&pageSize=${this.adminCatPageSize}&t=${Date.now()}`);
|
||
if (!res.ok) return;
|
||
const data = await res.json();
|
||
if (!data || !data.ok) return;
|
||
this.adminCategories = Array.isArray(data.rows) ? data.rows : [];
|
||
this.adminCatTotal = Number(data.total || 0);
|
||
const totalPages = Math.max(1, Number(data.total_pages || 1));
|
||
if (this.adminCatPage > totalPages) {
|
||
this.adminCatPage = totalPages;
|
||
this.loadAdminCategories();
|
||
return;
|
||
}
|
||
this.renderAdminCategoryTable();
|
||
if (this.categoryCountEl) this.categoryCountEl.textContent = `共 ${this.adminCatTotal} 个分类`;
|
||
this.renderAdminPagination(this.categoryPaginationEl, this.adminCatPage, totalPages, p => { this.adminCatPage = p; this.loadAdminCategories(); });
|
||
} catch {}
|
||
}
|
||
|
||
renderAdminCategoryTable() {
|
||
if (!this.categoryListEl) return;
|
||
if (!this.adminCategories.length) {
|
||
this.categoryListEl.innerHTML = '<tr><td colspan="5" class="empty-row">暂无分类</td></tr>';
|
||
return;
|
||
}
|
||
this.categoryListEl.innerHTML = this.adminCategories.map(cat => `
|
||
<tr>
|
||
<td>${this.esc(cat.code)}</td>
|
||
<td>${this.esc(cat.name)}</td>
|
||
<td>${Number(cat.sort_order || 0)}</td>
|
||
<td>${Number(cat.bookmark_count || 0)}</td>
|
||
<td class="td-actions">
|
||
<div class="admin-row-actions">
|
||
<button class="bookmark-btn" type="button" data-action="edit-category" data-code="${this.esc(cat.code)}">编辑</button>
|
||
<button class="bookmark-btn danger" type="button" data-action="delete-category" data-code="${this.esc(cat.code)}">删除</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
|
||
async loadAdminBookmarks() {
|
||
if (!this.bookmarkAdmin || !this.bookmarkListEl) return;
|
||
try {
|
||
const p = new URLSearchParams({ page: String(this.adminBookPage), pageSize: String(this.adminBookPageSize), t: String(Date.now()) });
|
||
if (this.adminBookCatFilter) p.set('category_code', this.adminBookCatFilter);
|
||
if (this.adminBookQuery) p.set('q', this.adminBookQuery);
|
||
const res = await fetch(`/api/nav_bookmarks/bookmarks?${p.toString()}`);
|
||
if (!res.ok) return;
|
||
const data = await res.json();
|
||
if (!data || !data.ok) return;
|
||
this.adminBookmarks = Array.isArray(data.rows) ? data.rows : [];
|
||
this.adminBookTotal = Number(data.total || 0);
|
||
const totalPages = Math.max(1, Number(data.total_pages || 1));
|
||
if (this.adminBookPage > totalPages) {
|
||
this.adminBookPage = totalPages;
|
||
this.loadAdminBookmarks();
|
||
return;
|
||
}
|
||
this.renderAdminBookmarkTable();
|
||
if (this.bookmarkCountEl) this.bookmarkCountEl.textContent = `共 ${this.adminBookTotal} 条`;
|
||
this.renderAdminPagination(this.bookmarkPaginationEl, this.adminBookPage, totalPages, p => { this.adminBookPage = p; this.loadAdminBookmarks(); });
|
||
} catch {}
|
||
}
|
||
|
||
renderAdminBookmarkTable() {
|
||
if (!this.bookmarkListEl) return;
|
||
if (!this.adminBookmarks.length) {
|
||
const hasFilter = this.adminBookCatFilter || String(this.adminBookQuery || '').trim();
|
||
this.bookmarkListEl.innerHTML = `<tr><td colspan="7" class="empty-row">暂无书签${hasFilter ? '(筛选条件无结果)' : ''}</td></tr>`;
|
||
return;
|
||
}
|
||
this.bookmarkListEl.innerHTML = this.adminBookmarks.map(item => {
|
||
const iconUrl = this.getIconUrl(item.icon);
|
||
const iconHtml = iconUrl ? `<img src="${this.esc(iconUrl)}" alt="" loading="lazy" />` : '';
|
||
const isFile = this.isFileUrl(item.url);
|
||
const urlIcon = `<span class="url-type-icon" title="${isFile ? '文件下载' : '网页链接'}">${isFile ? '📄' : '🔗'}</span>`;
|
||
const remarkHtml = item.remark ? `
|
||
<div class="td-remark-wrap">
|
||
<span class="td-sub">${this.esc(item.remark)}</span>
|
||
<button class="remark-toggle" type="button" data-action="toggle-remark" title="展开/收起备注">👇</button>
|
||
</div>` : '';
|
||
return `
|
||
<tr>
|
||
<td class="td-icon">${iconHtml}</td>
|
||
<td class="td-main">
|
||
<div>${this.esc(item.name || '未命名')}</div>
|
||
${remarkHtml}
|
||
</td>
|
||
<td class="td-url">${urlIcon}<a href="${this.esc(item.url || '#')}" target="_blank" rel="noopener">${this.esc(item.url || '-')}</a></td>
|
||
<td>${this.esc(this.getCategoryName(item.category_code))}</td>
|
||
<td>${Number(item.sort_order || 0)}</td>
|
||
<td>${this.fmtNavTime(item.created_at)}</td>
|
||
<td class="td-actions">
|
||
<div class="admin-row-actions">
|
||
<button class="bookmark-btn" type="button" data-action="edit-bookmark" data-id="${this.esc(item.id)}">编辑</button>
|
||
<button class="bookmark-btn danger" type="button" data-action="delete-bookmark" data-id="${this.esc(item.id)}">删除</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}).join('');
|
||
// 备注未超长时隐藏展开按钮,保证行高低稳定
|
||
this.bookmarkListEl.querySelectorAll('.td-remark-wrap').forEach(wrap => {
|
||
const sub = wrap.querySelector('.td-sub');
|
||
const btn = wrap.querySelector('.remark-toggle');
|
||
if (sub && btn && sub.scrollWidth <= sub.clientWidth + 1) btn.style.visibility = 'hidden';
|
||
});
|
||
}
|
||
|
||
// 书签管理 tab 顶部的分类筛选下拉(用全量分类填充)
|
||
renderAdminCategoryFilter() {
|
||
if (!this.bookmarkAdminCatFilter) return;
|
||
const current = this.bookmarkAdminCatFilter.value || this.adminBookCatFilter;
|
||
this.bookmarkAdminCatFilter.innerHTML = '';
|
||
const all = document.createElement('option');
|
||
all.value = '';
|
||
all.textContent = '全部分类';
|
||
this.bookmarkAdminCatFilter.appendChild(all);
|
||
(this.categories || []).forEach(cat => {
|
||
const opt = document.createElement('option');
|
||
opt.value = cat.code;
|
||
opt.textContent = `${cat.code} - ${cat.name}`;
|
||
this.bookmarkAdminCatFilter.appendChild(opt);
|
||
});
|
||
if (current) this.bookmarkAdminCatFilter.value = current;
|
||
}
|
||
|
||
renderAdminPagination(container, page, totalPages, onChange) {
|
||
if (!container) return;
|
||
container.innerHTML = '';
|
||
if (totalPages <= 1) return;
|
||
const mkBtn = (text, p, disabled, active) => {
|
||
const b = document.createElement('button');
|
||
b.type = 'button';
|
||
b.className = 'bookmark-btn pagination-btn' + (active ? ' active' : '');
|
||
b.textContent = text;
|
||
b.disabled = !!disabled;
|
||
if (!disabled) b.addEventListener('click', () => onChange(p));
|
||
return b;
|
||
};
|
||
const mkDots = () => {
|
||
const s = document.createElement('span');
|
||
s.className = 'pagination-dots';
|
||
s.textContent = '…';
|
||
return s;
|
||
};
|
||
container.appendChild(mkBtn('上一页', page - 1, page <= 1, false));
|
||
const start = Math.max(1, page - 2);
|
||
const end = Math.min(totalPages, page + 2);
|
||
if (start > 1) {
|
||
container.appendChild(mkBtn('1', 1, false, false));
|
||
if (start > 2) container.appendChild(mkDots());
|
||
}
|
||
for (let p = start; p <= end; p++) container.appendChild(mkBtn(String(p), p, false, p === page));
|
||
if (end < totalPages) {
|
||
if (end < totalPages - 1) container.appendChild(mkDots());
|
||
container.appendChild(mkBtn(String(totalPages), totalPages, false, false));
|
||
}
|
||
container.appendChild(mkBtn('下一页', page + 1, page >= totalPages, false));
|
||
}
|
||
|
||
openAdminModal(type, title) {
|
||
if (!this.navAdminModal) return;
|
||
const isCat = type === 'category';
|
||
if (this.navAdminModalTitle) this.navAdminModalTitle.textContent = title || (isCat ? '新增分类' : '新增书签');
|
||
if (this.categoryForm) this.categoryForm.style.display = isCat ? '' : 'none';
|
||
if (this.bookmarkForm) this.bookmarkForm.style.display = isCat ? 'none' : '';
|
||
if (!isCat) this.prefillBookmarkSelect();
|
||
if (this.iconPicker) this.iconPicker.classList.remove('active');
|
||
this.navAdminModal.classList.add('active');
|
||
}
|
||
|
||
closeAdminModal() {
|
||
if (this.navAdminModal) this.navAdminModal.classList.remove('active');
|
||
}
|
||
|
||
openCategoryModal(data) {
|
||
this.openAdminModal('category', data ? '编辑分类' : '新增分类');
|
||
if (data) this.fillCategoryForm(data); else this.resetCategoryForm();
|
||
}
|
||
|
||
openBookmarkModal(data) {
|
||
this.openAdminModal('bookmark', data ? '编辑书签' : '新增书签');
|
||
if (data) this.fillBookmarkForm(data); else this.resetBookmarkForm();
|
||
}
|
||
|
||
// 书签表单里的分类下拉(用全量分类填充)
|
||
prefillBookmarkSelect() {
|
||
const sel = this.bookmarkForm ? this.bookmarkForm.querySelector('select[name="category_code"]') : null;
|
||
if (!sel) return;
|
||
const current = sel.value;
|
||
sel.innerHTML = '';
|
||
const emptyOpt = document.createElement('option');
|
||
emptyOpt.value = '';
|
||
emptyOpt.textContent = '未分类';
|
||
sel.appendChild(emptyOpt);
|
||
(this.categories || []).forEach(cat => {
|
||
const opt = document.createElement('option');
|
||
opt.value = cat.code;
|
||
opt.textContent = `${cat.code} - ${cat.name}`;
|
||
sel.appendChild(opt);
|
||
});
|
||
if (current) sel.value = current;
|
||
}
|
||
|
||
setupEventListeners() {
|
||
// 换背景按钮事件
|
||
document.querySelector('.change-background-btn').addEventListener('click', () => {
|
||
this.nextTheme();
|
||
});
|
||
|
||
// 工具卡片点击效果 + 打开方式(委托方式,支持动态加载)
|
||
document.querySelector('.tab-pages').addEventListener('click', (e) => {
|
||
const card = e.target.closest('.tool-card');
|
||
if (!card) return;
|
||
card.style.transform = 'scale(0.95)';
|
||
setTimeout(() => { card.style.transform = ''; }, 150);
|
||
if (this.openInModal) {
|
||
e.preventDefault();
|
||
window.open(card.getAttribute('href') || '/', '_blank');
|
||
}
|
||
});
|
||
|
||
// 键盘快捷键
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'b' || e.key === 'B') {
|
||
e.preventDefault();
|
||
this.nextTheme();
|
||
}
|
||
});
|
||
|
||
// 滑块切换事件
|
||
const localBgSwitch = document.getElementById('local-bg-switch');
|
||
const openModeSwitch = document.getElementById('open-mode-switch');
|
||
if (localBgSwitch) {
|
||
localBgSwitch.checked = this.localBgMode;
|
||
localBgSwitch.addEventListener('change', () => {
|
||
this.localBgMode = !!localBgSwitch.checked;
|
||
try { localStorage.setItem('tb_home_local_bg', this.localBgMode ? '1' : '0'); } catch {}
|
||
if (this.localBgMode) {
|
||
this.stopThemeRotation();
|
||
this.startLocalBgRotation();
|
||
} else {
|
||
this.stopLocalBgRotation();
|
||
this.ensureBgLoaded(this.currentTheme, true);
|
||
this.startThemeRotation();
|
||
}
|
||
});
|
||
}
|
||
if (openModeSwitch) {
|
||
openModeSwitch.checked = this.openInModal;
|
||
openModeSwitch.addEventListener('change', () => {
|
||
this.openInModal = !!openModeSwitch.checked;
|
||
try { localStorage.setItem('tb_home_open_mode', this.openInModal ? '1' : '0'); } catch {}
|
||
});
|
||
}
|
||
|
||
if (this.tabBar) {
|
||
this.tabBar.addEventListener('click', (e) => {
|
||
const btn = e.target.closest('.tab-btn');
|
||
if (!btn) return;
|
||
const tab = btn.dataset.tab;
|
||
if (tab) this.setActiveTab(tab);
|
||
});
|
||
}
|
||
|
||
const clearBtn = document.getElementById('bookmark-search-clear');
|
||
if (this.bookmarkSearchInput) {
|
||
this.bookmarkSearchInput.addEventListener('input', () => {
|
||
this.searchQuery = this.bookmarkSearchInput.value;
|
||
this.renderBookmarks();
|
||
});
|
||
}
|
||
if (clearBtn) {
|
||
clearBtn.addEventListener('click', () => {
|
||
if (this.bookmarkSearchInput) this.bookmarkSearchInput.value = '';
|
||
this.searchQuery = '';
|
||
this.renderBookmarks();
|
||
});
|
||
}
|
||
|
||
const adminBtn = document.getElementById('bookmark-admin-btn');
|
||
if (adminBtn) {
|
||
adminBtn.addEventListener('click', () => {
|
||
this.openAdmin();
|
||
});
|
||
}
|
||
const adminClose = document.getElementById('bookmark-admin-close');
|
||
if (adminClose) {
|
||
adminClose.addEventListener('click', () => {
|
||
this.closeAdmin();
|
||
});
|
||
}
|
||
|
||
// 后台维护:管理员 tab 切换(分类 / 书签)
|
||
if (this.adminTabsEl) {
|
||
this.adminTabsEl.addEventListener('click', (e) => {
|
||
const btn = e.target.closest('.admin-tab-btn');
|
||
if (!btn) return;
|
||
this.switchAdminTab(btn.dataset.admTab);
|
||
});
|
||
}
|
||
|
||
// 新增分类 / 新增书签
|
||
const categoryAddBtn = document.getElementById('category-add-btn');
|
||
if (categoryAddBtn) {
|
||
categoryAddBtn.addEventListener('click', () => {
|
||
this.openCategoryModal(null);
|
||
});
|
||
}
|
||
const bookmarkAddBtn = document.getElementById('bookmark-add-btn');
|
||
if (bookmarkAddBtn) {
|
||
bookmarkAddBtn.addEventListener('click', () => {
|
||
this.openBookmarkModal(null);
|
||
});
|
||
}
|
||
|
||
// 书签管理:分类筛选 + 关键词搜索(300ms 防抖)
|
||
if (this.bookmarkAdminCatFilter) {
|
||
this.bookmarkAdminCatFilter.addEventListener('change', () => {
|
||
this.adminBookCatFilter = this.bookmarkAdminCatFilter.value;
|
||
this.adminBookPage = 1;
|
||
this.loadAdminBookmarks();
|
||
});
|
||
}
|
||
if (this.bookmarkAdminSearchInput) {
|
||
this.bookmarkAdminSearchInput.addEventListener('input', () => {
|
||
clearTimeout(this.adminSearchTimer);
|
||
this.adminSearchTimer = setTimeout(() => {
|
||
this.adminBookQuery = this.bookmarkAdminSearchInput.value.trim();
|
||
this.adminBookPage = 1;
|
||
this.loadAdminBookmarks();
|
||
}, 300);
|
||
});
|
||
}
|
||
const adminSearchClear = document.getElementById('bookmark-admin-search-clear');
|
||
if (adminSearchClear) {
|
||
adminSearchClear.addEventListener('click', () => {
|
||
if (this.bookmarkAdminSearchInput) this.bookmarkAdminSearchInput.value = '';
|
||
this.adminBookQuery = '';
|
||
this.adminBookPage = 1;
|
||
this.loadAdminBookmarks();
|
||
});
|
||
}
|
||
|
||
// 后台维护弹窗:关闭按钮 / 点击遮罩关闭
|
||
const navModalClose = document.getElementById('nav-admin-modal-close');
|
||
if (navModalClose) {
|
||
navModalClose.addEventListener('click', () => {
|
||
this.closeAdminModal();
|
||
});
|
||
}
|
||
if (this.navAdminModal) {
|
||
this.navAdminModal.addEventListener('click', (e) => {
|
||
if (e.target === this.navAdminModal) this.closeAdminModal();
|
||
});
|
||
}
|
||
|
||
if (this.bookmarkContainer) {
|
||
this.bookmarkContainer.addEventListener('click', (e) => {
|
||
const toggle = e.target.closest('.category-toggle');
|
||
if (!toggle) return;
|
||
const code = String(toggle.dataset.code || '');
|
||
this.collapsedCategories[code] = !this.collapsedCategories[code];
|
||
this.renderBookmarks();
|
||
});
|
||
}
|
||
|
||
if (this.categoryForm) {
|
||
const categorySubmitBtn = this.categoryForm.querySelector('button[type="submit"]');
|
||
this.categoryForm.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const data = new FormData(this.categoryForm);
|
||
const payload = {
|
||
code: String(data.get('code') || '').trim(),
|
||
name: String(data.get('name') || '').trim(),
|
||
sort_order: Number(data.get('sort_order') || 0)
|
||
};
|
||
const wasEditing = !!this.editingCategoryCode;
|
||
if (categorySubmitBtn) { categorySubmitBtn.disabled = true; categorySubmitBtn.textContent = '保存中…'; }
|
||
try {
|
||
if (this.editingCategoryCode) {
|
||
await this.apiRequest(`/api/nav_bookmarks/categories/${encodeURIComponent(this.editingCategoryCode)}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload)
|
||
});
|
||
} else {
|
||
await this.apiRequest('/api/nav_bookmarks/categories', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload)
|
||
});
|
||
}
|
||
this.resetCategoryForm();
|
||
this.closeAdminModal();
|
||
if (!wasEditing) this.adminCatPage = 1;
|
||
await this.loadBookmarks();
|
||
await this.loadAdminCategories();
|
||
this.renderAdminCategoryFilter();
|
||
} catch (err) {
|
||
alert(err.message || '保存失败');
|
||
} finally {
|
||
if (categorySubmitBtn) { categorySubmitBtn.disabled = false; categorySubmitBtn.textContent = '保存'; }
|
||
}
|
||
});
|
||
const cancelBtn = this.categoryForm.querySelector('[data-action="cancel-category"]');
|
||
if (cancelBtn) {
|
||
cancelBtn.addEventListener('click', () => {
|
||
this.resetCategoryForm();
|
||
this.closeAdminModal();
|
||
});
|
||
}
|
||
}
|
||
|
||
if (this.bookmarkForm) {
|
||
const bookmarkSubmitBtn = this.bookmarkForm.querySelector('button[type="submit"]');
|
||
this.bookmarkForm.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const data = new FormData(this.bookmarkForm);
|
||
const payload = {
|
||
name: String(data.get('name') || '').trim(),
|
||
url: String(data.get('url') || '').trim(),
|
||
category_code: String(data.get('category_code') || '').trim(),
|
||
icon: String(data.get('icon') || '').trim(),
|
||
remark: String(data.get('remark') || '').trim(),
|
||
sort_order: Number(data.get('sort_order') || 0)
|
||
};
|
||
const wasEditing = !!this.editingBookmarkId;
|
||
if (bookmarkSubmitBtn) { bookmarkSubmitBtn.disabled = true; bookmarkSubmitBtn.textContent = '保存中…'; }
|
||
try {
|
||
if (this.editingBookmarkId) {
|
||
await this.apiRequest(`/api/nav_bookmarks/bookmarks/${encodeURIComponent(this.editingBookmarkId)}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload)
|
||
});
|
||
} else {
|
||
await this.apiRequest('/api/nav_bookmarks/bookmarks', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload)
|
||
});
|
||
}
|
||
this.resetBookmarkForm();
|
||
this.closeAdminModal();
|
||
if (!wasEditing) this.adminBookPage = 1;
|
||
await this.loadBookmarks();
|
||
await this.loadAdminBookmarks();
|
||
} catch (err) {
|
||
alert(err.message || '保存失败');
|
||
} finally {
|
||
if (bookmarkSubmitBtn) { bookmarkSubmitBtn.disabled = false; bookmarkSubmitBtn.textContent = '保存'; }
|
||
}
|
||
});
|
||
const cancelBtn = this.bookmarkForm.querySelector('[data-action="cancel-bookmark"]');
|
||
if (cancelBtn) {
|
||
cancelBtn.addEventListener('click', () => {
|
||
this.resetBookmarkForm();
|
||
this.closeAdminModal();
|
||
});
|
||
}
|
||
this.bookmarkForm.addEventListener('input', (e) => {
|
||
if (e.target && e.target.name === 'icon') this.updateIconPreview();
|
||
});
|
||
const pickBtn = this.bookmarkForm.querySelector('[data-action="pick-icon"]');
|
||
const clearBtn2 = this.bookmarkForm.querySelector('[data-action="clear-icon"]');
|
||
if (pickBtn) {
|
||
pickBtn.addEventListener('click', () => {
|
||
this.iconPickerTarget = this.bookmarkForm.querySelector('input[name="icon"]');
|
||
if (this.iconPicker) this.iconPicker.classList.add('active');
|
||
});
|
||
}
|
||
if (clearBtn2) {
|
||
clearBtn2.addEventListener('click', () => {
|
||
const input = this.bookmarkForm.querySelector('input[name="icon"]');
|
||
if (input) input.value = '';
|
||
this.updateIconPreview();
|
||
});
|
||
}
|
||
}
|
||
|
||
if (this.categoryListEl) {
|
||
this.categoryListEl.addEventListener('click', async (e) => {
|
||
const btn = e.target.closest('button[data-action]');
|
||
if (!btn) return;
|
||
const action = btn.dataset.action;
|
||
const code = btn.dataset.code;
|
||
if (action === 'edit-category') {
|
||
const cat = this.adminCategories.find(c => String(c.code) === String(code)) || this.categories.find(c => String(c.code) === String(code));
|
||
if (cat) this.openCategoryModal(cat);
|
||
}
|
||
if (action === 'delete-category') {
|
||
if (!confirm('确定删除该分类吗?')) return;
|
||
try {
|
||
await this.apiRequest(`/api/nav_bookmarks/categories/${encodeURIComponent(code)}`, { method: 'DELETE' });
|
||
await this.loadBookmarks();
|
||
await this.loadAdminCategories();
|
||
this.renderAdminCategoryFilter();
|
||
} catch (err) {
|
||
alert(err.message || '删除失败');
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
if (this.bookmarkListEl) {
|
||
this.bookmarkListEl.addEventListener('click', (e) => {
|
||
const btn = e.target.closest('button[data-action]');
|
||
if (!btn) return;
|
||
const action = btn.dataset.action;
|
||
const id = btn.dataset.id;
|
||
if (action === 'toggle-remark') {
|
||
const wrap = btn.closest('.td-remark-wrap');
|
||
if (!wrap) return;
|
||
const expanded = wrap.classList.toggle('expanded');
|
||
btn.textContent = expanded ? '👆' : '👇';
|
||
btn.title = expanded ? '收起备注' : '展开/收起备注';
|
||
return;
|
||
}
|
||
if (action === 'edit-bookmark') {
|
||
const item = this.adminBookmarks.find(b => String(b.id) === String(id)) || this.bookmarks.find(b => String(b.id) === String(id));
|
||
if (item) this.openBookmarkModal(item);
|
||
}
|
||
if (action === 'delete-bookmark') {
|
||
if (!confirm('确定删除该书签吗?')) return;
|
||
this.apiRequest(`/api/nav_bookmarks/bookmarks/${encodeURIComponent(id)}`, { method: 'DELETE' }).then(async () => {
|
||
await this.loadBookmarks();
|
||
await this.loadAdminBookmarks();
|
||
}).catch(err => alert(err.message || '删除失败'));
|
||
}
|
||
});
|
||
}
|
||
|
||
if (this.iconPicker) {
|
||
this.iconPicker.addEventListener('click', (e) => {
|
||
const closeBtn = e.target.closest('[data-action="close-icon-picker"]');
|
||
if (closeBtn) {
|
||
this.iconPicker.classList.remove('active');
|
||
return;
|
||
}
|
||
const btn = e.target.closest('.icon-picker-item');
|
||
if (btn && this.iconPickerTarget) {
|
||
this.iconPickerTarget.value = btn.dataset.icon || '';
|
||
this.updateIconPreview();
|
||
this.iconPicker.classList.remove('active');
|
||
return;
|
||
}
|
||
if (e.target === this.iconPicker) {
|
||
this.iconPicker.classList.remove('active');
|
||
}
|
||
});
|
||
}
|
||
|
||
if (this.fitnessDiaryReminder) {
|
||
this.fitnessDiaryReminder.addEventListener('click', (e) => {
|
||
const actionEl = e.target.closest('[data-action]');
|
||
if (!actionEl) return;
|
||
const action = actionEl.dataset.action;
|
||
if (action === 'close-fitness-diary-reminder') {
|
||
this.hideFitnessDiaryReminder();
|
||
return;
|
||
}
|
||
if (action === 'open-fitness-diary') {
|
||
this.hideFitnessDiaryReminder();
|
||
window.open('/tools/fitness_diary', '_blank', 'noopener');
|
||
}
|
||
});
|
||
}
|
||
|
||
if (this.weeklyReportReminder) {
|
||
this.weeklyReportReminder.addEventListener('click', (e) => {
|
||
const actionEl = e.target.closest('[data-action]');
|
||
if (!actionEl) return;
|
||
const action = actionEl.dataset.action;
|
||
if (action === 'close-weekly-report-reminder') {
|
||
this.hideWeeklyReportReminder();
|
||
return;
|
||
}
|
||
if (action === 'open-weekly-report') {
|
||
this.hideWeeklyReportReminder();
|
||
window.open('/tools/weekly', '_blank', 'noopener');
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
queueReminder(reminderKey, openFn) {
|
||
if (!reminderKey || typeof openFn !== 'function') return;
|
||
if (this.activeReminderKey === reminderKey) return;
|
||
if (this.reminderQueue.some(item => item.key === reminderKey)) return;
|
||
const reminder = { key: reminderKey, openFn };
|
||
if (!this.activeReminderKey) {
|
||
this.activeReminderKey = reminderKey;
|
||
openFn();
|
||
return;
|
||
}
|
||
this.reminderQueue.push(reminder);
|
||
}
|
||
|
||
finishReminder(reminderKey) {
|
||
if (!reminderKey) return;
|
||
if (this.activeReminderKey === reminderKey) {
|
||
this.activeReminderKey = '';
|
||
const nextReminder = this.reminderQueue.shift();
|
||
if (nextReminder) {
|
||
this.activeReminderKey = nextReminder.key;
|
||
nextReminder.openFn();
|
||
}
|
||
return;
|
||
}
|
||
this.reminderQueue = this.reminderQueue.filter(item => item.key !== reminderKey);
|
||
}
|
||
|
||
formatDate(date) {
|
||
const year = date.getFullYear();
|
||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||
const day = String(date.getDate()).padStart(2, '0');
|
||
return `${year}-${month}-${day}`;
|
||
}
|
||
|
||
getFitnessDiaryDateRange(days = 5) {
|
||
const end = new Date();
|
||
const start = new Date(end);
|
||
start.setDate(end.getDate() - (days - 1));
|
||
return {
|
||
start: this.formatDate(start),
|
||
end: this.formatDate(end)
|
||
};
|
||
}
|
||
|
||
getFitnessDiaryReminderSummary(records, days = 5) {
|
||
const existingDates = new Set(
|
||
(Array.isArray(records) ? records : [])
|
||
.map(item => String(item && item.record_date ? item.record_date : '').slice(0, 10))
|
||
.filter(Boolean)
|
||
);
|
||
const missingDates = [];
|
||
const today = new Date();
|
||
for (let i = 0; i < days; i += 1) {
|
||
const date = new Date(today);
|
||
date.setDate(today.getDate() - i);
|
||
const dateStr = this.formatDate(date);
|
||
if (!existingDates.has(dateStr)) missingDates.push(dateStr);
|
||
}
|
||
return {
|
||
days,
|
||
missingDates: missingDates.reverse(),
|
||
missingCount: missingDates.length
|
||
};
|
||
}
|
||
|
||
renderFitnessDiaryReminder(summary) {
|
||
if (!this.fitnessDiaryReminder || !this.fitnessDiaryReminderText || !this.fitnessDiaryReminderDates) return;
|
||
const missingCount = Number(summary && summary.missingCount ? summary.missingCount : 0);
|
||
if (missingCount <= 0) {
|
||
this.hideFitnessDiaryReminder();
|
||
return;
|
||
}
|
||
this.fitnessDiaryReminderText.textContent = `最近${summary.days}天(含今天)应写${summary.days}篇运动日记,你现在还缺${missingCount}篇。`;
|
||
this.fitnessDiaryReminderDates.innerHTML = summary.missingDates
|
||
.map(date => `<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('textarea[name="url"]');
|
||
const catSelect = this.bookmarkForm.querySelector('select[name="category_code"]');
|
||
const iconInput = this.bookmarkForm.querySelector('input[name="icon"]');
|
||
const remarkInput = this.bookmarkForm.querySelector('textarea[name="remark"]');
|
||
const sortInput = this.bookmarkForm.querySelector('input[name="sort_order"]');
|
||
if (nameInput) nameInput.value = item.name || '';
|
||
if (urlInput) urlInput.value = item.url || '';
|
||
if (catSelect) catSelect.value = item.category_code || '';
|
||
if (iconInput) iconInput.value = item.icon || '';
|
||
if (remarkInput) remarkInput.value = item.remark || '';
|
||
if (sortInput) sortInput.value = Number(item.sort_order || 0);
|
||
if (this.bookmarkCreatedAtEl) this.bookmarkCreatedAtEl.textContent = item.created_at || '-';
|
||
this.updateIconPreview();
|
||
}
|
||
|
||
updateIconPreview() {
|
||
if (!this.bookmarkForm || !this.iconPreviewEl) return;
|
||
const iconInput = this.bookmarkForm.querySelector('input[name="icon"]');
|
||
if (!iconInput) return;
|
||
const iconUrl = this.getIconUrl(iconInput.value);
|
||
this.iconPreviewEl.innerHTML = '';
|
||
if (iconUrl) {
|
||
const img = document.createElement('img');
|
||
img.src = iconUrl;
|
||
img.alt = iconInput.value || '';
|
||
this.iconPreviewEl.appendChild(img);
|
||
} else {
|
||
this.iconPreviewEl.textContent = '未选择';
|
||
}
|
||
}
|
||
|
||
applyTheme(themeIndex) {
|
||
const theme = this.themes[themeIndex];
|
||
|
||
// 更新背景图片
|
||
document.querySelectorAll('.background-image').forEach((img, index) => {
|
||
img.classList.toggle('active', index === themeIndex);
|
||
});
|
||
this.ensureBgLoaded(themeIndex, true);
|
||
|
||
// 强制使用纯白文字
|
||
document.body.style.color = '#ffffff';
|
||
document.documentElement.style.setProperty('--card-text-color', '#ffffff');
|
||
|
||
// 移除对 tool-card 的个别样式覆盖,统一使用 CSS 类控制
|
||
/*
|
||
document.querySelectorAll('.tool-card').forEach(card => {
|
||
card.style.background = theme.cardGradient;
|
||
card.style.color = theme.textColor;
|
||
});
|
||
*/
|
||
|
||
// 清除所有特效
|
||
this.clearEffects();
|
||
|
||
// 应用新特效
|
||
setTimeout(() => {
|
||
this.startEffect(theme.effect);
|
||
}, 500);
|
||
}
|
||
|
||
async ensureBgLoaded(themeIndex, preferLocal = true) {
|
||
try {
|
||
const theme = this.themes[themeIndex];
|
||
const imgs = document.querySelectorAll('.background-image');
|
||
const img = imgs[themeIndex];
|
||
if (!img || !theme) return;
|
||
const r = await fetch(`/api/home/bg?theme=${encodeURIComponent(theme.name)}&url=${encodeURIComponent(theme.background)}&prefer_local=${preferLocal ? '1' : '0'}`);
|
||
const j = await r.json();
|
||
img.src = (j && j.url) ? j.url : theme.background;
|
||
} catch {
|
||
const theme = this.themes[themeIndex];
|
||
const imgs = document.querySelectorAll('.background-image');
|
||
const img = imgs[themeIndex];
|
||
if (img && theme) img.src = theme.background;
|
||
}
|
||
}
|
||
|
||
nextTheme() {
|
||
if (this.localBgMode) {
|
||
this.nextLocalBg();
|
||
return;
|
||
}
|
||
this.currentTheme = (this.currentTheme + 1) % this.themes.length;
|
||
this.applyTheme(this.currentTheme);
|
||
|
||
// 显示当前主题名称
|
||
const themeName = this.themes[this.currentTheme].name;
|
||
this.showNotification(`切换到: ${themeName}`);
|
||
}
|
||
|
||
startEffects() {
|
||
const currentTheme = this.themes[this.currentTheme];
|
||
this.startEffect(currentTheme.effect);
|
||
}
|
||
|
||
startEffect(effectType) {
|
||
const effectsContainer = document.querySelector('.effects-container');
|
||
|
||
switch (effectType) {
|
||
case 'meteor':
|
||
this.startMeteorEffect(effectsContainer);
|
||
break;
|
||
default:
|
||
this.startMeteorEffect(effectsContainer);
|
||
break;
|
||
}
|
||
}
|
||
|
||
startMeteorEffect(container) {
|
||
// 创建流星
|
||
const createMeteor = () => {
|
||
const meteor = document.createElement('div');
|
||
meteor.className = 'meteor';
|
||
|
||
// 随机位置
|
||
const left = Math.random() * 100;
|
||
const top = Math.random() * 50;
|
||
meteor.style.left = `${left}%`;
|
||
meteor.style.top = `${top}%`;
|
||
|
||
// 随机大小和速度
|
||
const size = 1 + Math.random() * 2;
|
||
const duration = 8 + Math.random() * 4;
|
||
meteor.style.width = `${size}px`;
|
||
meteor.style.height = `${size}px`;
|
||
meteor.style.animationDuration = `${duration}s`;
|
||
|
||
container.appendChild(meteor);
|
||
|
||
// 移除流星
|
||
setTimeout(() => {
|
||
if (meteor.parentNode) {
|
||
meteor.parentNode.removeChild(meteor);
|
||
}
|
||
}, duration * 1000);
|
||
};
|
||
|
||
// 定期创建流星
|
||
this.meteorInterval = setInterval(createMeteor, 3000);
|
||
|
||
// 初始创建几个流星
|
||
for (let i = 0; i < 3; i++) {
|
||
setTimeout(createMeteor, i * 1000);
|
||
}
|
||
}
|
||
|
||
startPaperPlaneEffect(container) {
|
||
// 创建纸飞机
|
||
const createPaperPlane = () => {
|
||
const plane = document.createElement('div');
|
||
plane.className = 'paper-plane';
|
||
plane.innerHTML = '✈️';
|
||
|
||
// 随机位置
|
||
const top = 10 + Math.random() * 70;
|
||
plane.style.top = `${top}%`;
|
||
|
||
// 随机延迟和速度
|
||
const delay = Math.random() * 5;
|
||
const duration = 12 + Math.random() * 6;
|
||
plane.style.animationDelay = `${delay}s`;
|
||
plane.style.animationDuration = `${duration}s`;
|
||
|
||
container.appendChild(plane);
|
||
|
||
// 移除飞机
|
||
setTimeout(() => {
|
||
if (plane.parentNode) {
|
||
plane.parentNode.removeChild(plane);
|
||
}
|
||
}, (duration + delay) * 1000);
|
||
};
|
||
|
||
// 定期创建纸飞机
|
||
this.planeInterval = setInterval(createPaperPlane, 8000);
|
||
|
||
// 初始创建几个纸飞机
|
||
for (let i = 0; i < 2; i++) {
|
||
setTimeout(createPaperPlane, i * 2000);
|
||
}
|
||
}
|
||
|
||
startForestEffect(container) {
|
||
// 森林主题的飘叶效果
|
||
const createLeaf = () => {
|
||
const leaf = document.createElement('div');
|
||
leaf.className = 'paper-plane';
|
||
leaf.innerHTML = '🍃';
|
||
leaf.style.fontSize = '16px';
|
||
|
||
// 随机位置
|
||
const left = Math.random() * 100;
|
||
const top = Math.random() * 100;
|
||
leaf.style.left = `${left}%`;
|
||
leaf.style.top = `${top}%`;
|
||
|
||
// 随机动画参数
|
||
const delay = Math.random() * 3;
|
||
const duration = 10 + Math.random() * 8;
|
||
leaf.style.animationDelay = `${delay}s`;
|
||
leaf.style.animationDuration = `${duration}s`;
|
||
|
||
container.appendChild(leaf);
|
||
|
||
// 移除叶子
|
||
setTimeout(() => {
|
||
if (leaf.parentNode) {
|
||
leaf.parentNode.removeChild(leaf);
|
||
}
|
||
}, (duration + delay) * 1000);
|
||
};
|
||
|
||
// 定期创建叶子
|
||
this.leafInterval = setInterval(createLeaf, 4000);
|
||
|
||
// 初始创建几个叶子
|
||
for (let i = 0; i < 4; i++) {
|
||
setTimeout(createLeaf, i * 1000);
|
||
}
|
||
}
|
||
|
||
clearEffects() {
|
||
// 清除所有定时器
|
||
if (this.meteorInterval) clearInterval(this.meteorInterval);
|
||
if (this.planeInterval) clearInterval(this.planeInterval);
|
||
if (this.leafInterval) clearInterval(this.leafInterval);
|
||
|
||
// 清除所有特效元素
|
||
const effectsContainer = document.querySelector('.effects-container');
|
||
if (effectsContainer) {
|
||
effectsContainer.innerHTML = '';
|
||
}
|
||
}
|
||
|
||
startBackgroundRotation() {
|
||
this.startThemeRotation();
|
||
}
|
||
|
||
startThemeRotation() {
|
||
if (this.localBgMode) return;
|
||
this.stopThemeRotation();
|
||
this.rotationInterval = setInterval(() => {
|
||
this.nextTheme();
|
||
}, 30000);
|
||
}
|
||
|
||
stopThemeRotation() {
|
||
if (this.rotationInterval) {
|
||
clearInterval(this.rotationInterval);
|
||
this.rotationInterval = null;
|
||
}
|
||
}
|
||
|
||
startLocalBgRotation() {
|
||
this.loadLocalBgFiles().then(() => {
|
||
this.stopLocalBgRotation();
|
||
this.localRotationInterval = setInterval(() => {
|
||
this.nextLocalBg();
|
||
}, 30000);
|
||
});
|
||
}
|
||
|
||
stopLocalBgRotation() {
|
||
if (this.localRotationInterval) {
|
||
clearInterval(this.localRotationInterval);
|
||
this.localRotationInterval = null;
|
||
}
|
||
}
|
||
|
||
async loadLocalBgFiles() {
|
||
if (this.localBgFiles.length > 0) return this.localBgFiles;
|
||
try {
|
||
const r = await fetch('/api/home/bg/default-list?t=' + Date.now());
|
||
const j = await r.json();
|
||
if (j.ok && j.files.length) {
|
||
this.localBgFiles = j.files;
|
||
this.localBgIndex = 0;
|
||
// 加载第一张
|
||
const imgs = document.querySelectorAll('.background-image');
|
||
if (imgs.length) {
|
||
imgs.forEach(img => { img.src = `/home-bg/default-c21f969b/${this.localBgFiles[0]}?v=${Date.now()}`; });
|
||
}
|
||
}
|
||
} catch {}
|
||
return this.localBgFiles;
|
||
}
|
||
|
||
async nextLocalBg() {
|
||
if (!this.localBgFiles.length) {
|
||
await this.loadLocalBgFiles();
|
||
if (!this.localBgFiles.length) return;
|
||
}
|
||
// 随机选取,避免重复当前
|
||
let newIndex;
|
||
if (this.localBgFiles.length > 1) {
|
||
do {
|
||
newIndex = Math.floor(Math.random() * this.localBgFiles.length);
|
||
} while (newIndex === this.localBgIndex);
|
||
} else {
|
||
newIndex = 0;
|
||
}
|
||
this.localBgIndex = newIndex;
|
||
const file = this.localBgFiles[this.localBgIndex];
|
||
const imgs = document.querySelectorAll('.background-image');
|
||
if (imgs.length) {
|
||
imgs.forEach(img => { img.src = `/home-bg/default-c21f969b/${file}?v=${Date.now()}`; });
|
||
}
|
||
this.showNotification(`本地背景: ${this.localBgIndex + 1}/${this.localBgFiles.length}`);
|
||
}
|
||
|
||
showNotification(message) {
|
||
// 创建通知元素
|
||
const notification = document.createElement('div');
|
||
notification.style.cssText = `
|
||
position: fixed;
|
||
top: 2rem;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
background: rgba(255, 255, 255, 0.2);
|
||
backdrop-filter: blur(10px);
|
||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||
color: var(--text-light);
|
||
padding: 0.75rem 1.5rem;
|
||
border-radius: 25px;
|
||
font-size: 0.9rem;
|
||
z-index: 1000;
|
||
opacity: 0;
|
||
transition: opacity 0.3s ease;
|
||
`;
|
||
notification.textContent = message;
|
||
|
||
document.body.appendChild(notification);
|
||
|
||
// 显示动画
|
||
setTimeout(() => {
|
||
notification.style.opacity = '1';
|
||
}, 10);
|
||
|
||
// 隐藏动画
|
||
setTimeout(() => {
|
||
notification.style.opacity = '0';
|
||
setTimeout(() => {
|
||
if (notification.parentNode) {
|
||
notification.parentNode.removeChild(notification);
|
||
}
|
||
}, 300);
|
||
}, 2000);
|
||
}
|
||
}
|
||
|
||
// 页面加载完成后初始化
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
new ToolboxHome();
|
||
});
|