/** * 主应用逻辑 */ class PFPApp { constructor() { this.loader = new PFPLoader(); this.pagination = null; this.drawer = new Drawer(); // 抽屉组件 this.lazyLoader = new LazyLoader({ rootMargin: '200px' }); // 图片懒加载 this.featuredFilter = new FeaturedFilter(); this.searchFilter = new SearchFilter(); this.featureFilter = null; // 将在 init() 中初始化 this.featureFilterUI = null; this.currentMode = 'all'; // 'all' 或 'featured' this.currentPage = 1; this.perPage = this.getPerPageCount(); // 动态计算每页数量 this.allData = null; this.currentData = null; } /** * 根据屏幕宽度动态计算每页显示数量 * @returns {number} 每页数量 */ getPerPageCount() { const width = window.innerWidth; if (width < 768) return 24; // 移动端: 2列 x 12行 else if (width < 1200) return 30; // 平板: 3列 x 10行 else if (width < 1600) return 40; // 小桌面: 4列 x 10行 else return 50; // 大桌面: 5列 x 10行 } /** * 初始化滚动收缩效果 */ initScrollEffect() { const header = document.querySelector('.header'); const collapsedBar = document.getElementById('header-collapsed-bar'); const expandBtn = document.getElementById('expand-header-btn'); const toggleBtn = document.getElementById('toggle-header-btn'); const collapsedInfo = document.getElementById('collapsed-info'); if (!header) return; let isCollapsed = false; let isManualMode = false; // 手动模式:用户主动点击收缩/展开 const scrollThreshold = 150; // 提高阈值 const scrollHysteresis = 80; // 增大滞后区间,避免频闪 // 更新收缩栏的信息文字 const updateCollapsedInfo = () => { if (!collapsedInfo) return; const indicators = []; if (this.currentMode === 'featured') { indicators.push('精选'); } else { indicators.push('全部'); } if (this.searchFilter && this.searchFilter.isActive()) { indicators.push(`搜索:${this.searchFilter.getSearchTerm()}`); } if (this.featureFilter && this.featureFilter.isActive()) { const count = this.featureFilter.getSelectedFeatures().size; indicators.push(`特征:${count}项`); } collapsedInfo.textContent = `当前筛选: ${indicators.join(' + ')}`; }; // 收缩头部 const collapseHeader = () => { if (isCollapsed) return; isCollapsed = true; updateCollapsedInfo(); header.classList.add('collapsed'); }; // 展开头部 const expandHeader = () => { if (!isCollapsed) return; isCollapsed = false; header.classList.remove('collapsed'); }; // 滚动处理 - 稳定版 let lastScrollY = window.scrollY; let scrollDirection = 'none'; // 'up', 'down', 'none' let stableCount = 0; // 稳定计数器 const STABLE_THRESHOLD = 3; // 需要连续3次同方向才触发 const onScroll = () => { // 手动模式下不响应滚动 if (isManualMode) return; const currentScrollY = window.scrollY; const delta = currentScrollY - lastScrollY; // 忽略微小滚动(可能是收缩引起的页面跳动) if (Math.abs(delta) < 5) { lastScrollY = currentScrollY; return; } // 判断滚动方向 const newDirection = delta > 0 ? 'down' : 'up'; // 方向一致则累加,否则重置 if (newDirection === scrollDirection) { stableCount++; } else { scrollDirection = newDirection; stableCount = 1; } lastScrollY = currentScrollY; // 只有连续同方向滚动才触发状态变化 if (stableCount < STABLE_THRESHOLD) return; // 向下滚动超过阈值 -> 收缩 if (!isCollapsed && scrollDirection === 'down' && currentScrollY > scrollThreshold) { collapseHeader(); stableCount = 0; } // 向上滚动且位置低于阈值 -> 展开 else if (isCollapsed && scrollDirection === 'up' && currentScrollY < (scrollThreshold - scrollHysteresis)) { expandHeader(); stableCount = 0; } }; // 使用节流而非防抖 let throttleTimer = null; const throttledScroll = () => { if (throttleTimer) return; throttleTimer = setTimeout(() => { throttleTimer = null; onScroll(); }, 30); }; window.addEventListener('scroll', throttledScroll, { passive: true }); // 点击收缩栏展开 if (collapsedBar) { collapsedBar.addEventListener('click', () => { isManualMode = true; // 进入手动模式 expandHeader(); }); } // 展开按钮 if (expandBtn) { expandBtn.addEventListener('click', (e) => { e.stopPropagation(); isManualMode = true; expandHeader(); }); } // 手动收缩按钮 if (toggleBtn) { toggleBtn.addEventListener('click', (e) => { e.stopPropagation(); if (isCollapsed) { isManualMode = true; expandHeader(); } else { isManualMode = true; collapseHeader(); } }); } // 滚动时退出手动模式(滚动到顶部时) const checkManualMode = () => { if (isManualMode && window.scrollY < 20) { isManualMode = false; } }; window.addEventListener('scroll', checkManualMode, { passive: true }); // 暴露更新方法供外部调用 this.updateCollapsedInfo = updateCollapsedInfo; } /** * 初始化应用 */ async init() { try { this.showLoading('正在加载数据...'); // 加载PFP数据 this.allData = await this.loader.loadAllPFP(); // 初始化分页 this.pagination = new Pagination({ currentPage: 1, totalPages: 1, onPageChange: (page) => this.handlePageChange(page) }); // 初始化特征筛选 this.featureFilter = new FeatureFilter(); this.featureFilterUI = new FeatureFilterUI( this.featureFilter, () => this.handleFilterChange() ); // 后台加载特征数据(非阻塞) this.featureFilter.loadCategoryData() .then(() => this.featureFilterUI.init()) .catch(error => { console.error('特征筛选初始化失败:', error); // 非关键功能,应用继续运行 }); // 初始显示第一页 await this.showPage(1, 'all'); // 绑定事件 this.bindEvents(); // 监听窗口resize,动态调整每页数量 let resizeTimeout; window.addEventListener('resize', () => { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { const newPerPage = this.getPerPageCount(); if (newPerPage !== this.perPage) { this.perPage = newPerPage; this.showPage(1); // 重新渲染第1页 } }, 300); // 防抖300ms }); // 监听页面滚动,实现头部收缩效果 this.initScrollEffect(); this.hideLoading(); console.log('应用初始化完成'); } catch (error) { console.error('应用初始化失败:', error); this.showError('数据加载失败:' + error.message); } } /** * 显示指定页的数据 * @param {number} page - 页码 * @param {string} mode - 显示模式('all' 或 'featured') */ async showPage(page, mode = null) { try { if (mode) { this.currentMode = mode; } this.currentPage = page; // 步骤1: 基础数据集 let workingData; if (this.currentMode === 'featured') { const featuredList = await this.featuredFilter.loadFeaturedList(); workingData = this.loader.filterByNums(this.allData, featuredList); } else { workingData = Object.values(this.allData); } // 步骤2: 应用搜索筛选 if (this.searchFilter.isActive()) { workingData = this.searchFilter.filterByNumber( workingData, this.searchFilter.getSearchTerm(), this.searchFilter.getSearchMode() ); } // 步骤3: 应用特征筛选 if (this.featureFilter && this.featureFilter.isActive()) { workingData = this.featureFilter.filterByFeatures( workingData, this.featureFilter.getSelectedFeatures(), this.featureFilter.getLogicMode() ); } // 步骤4: 按编号排序(Z字型排列) workingData.sort((a, b) => { const numA = parseInt(a.num) || 0; const numB = parseInt(b.num) || 0; return numA - numB; }); // 步骤5: 分页 const start = (page - 1) * this.perPage; const end = start + this.perPage; const dataToShow = workingData.slice(start, end); const totalPages = Math.ceil(workingData.length / this.perPage); // 步骤6: 渲染 this.renderPFPList(dataToShow); this.pagination.update(page, totalPages); this.pagination.render(document.getElementById('pagination-container')); // 步骤7: 更新统计和筛选指示器 this.updateStats({ total: workingData.length, totalPages }); this.updateFilterIndicators(); } catch (error) { console.error('显示页面失败:', error); this.showError('显示数据失败:' + error.message); } } /** * 渲染PFP列表 * @param {Array} data - PFP数据数组 */ renderPFPList(data) { const container = document.getElementById('pfp-list'); container.innerHTML = ''; if (!data || data.length === 0) { container.innerHTML = '
'; return; } data.forEach(pfp => { const card = this.createPFPCard(pfp); container.appendChild(card); }); // 图片使用懒加载,由LazyLoader自动处理 } /** * 创建PFP卡片 * @param {Object} pfp - PFP数据对象 * @returns {HTMLElement} 卡片元素 */ createPFPCard(pfp) { const card = document.createElement('div'); card.className = 'pfp-card'; // 编号 const numDiv = document.createElement('div'); numDiv.className = 'pfp-number'; numDiv.textContent = `#${pfp.num}`; // 名称 const nameDiv = document.createElement('div'); nameDiv.className = 'pfp-name'; nameDiv.textContent = pfp.name || `山海 #${pfp.num}`; // 图片(使用封面图,点击后显示全身图) const img = document.createElement('img'); img.className = 'pfp-image'; img.dataset.src = pfp.cover || pfp.src; // 使用 data-src 进行懒加载 img.dataset.fullSrc = pfp.src; // 保存全身图URL img.alt = `PFP #${pfp.num}`; img.referrerPolicy = 'no-referrer'; // 防止防盗链限制 img.style.cursor = 'pointer'; // 鼠标指针样式 // 注册到懒加载器 this.lazyLoader.observe(img); // 点击图片显示全身图 img.addEventListener('click', () => { this.showFullImage(pfp); }); // 特征列表 const featuresDiv = document.createElement('div'); featuresDiv.className = 'pfp-features'; const features = this.loader.formatFeatures(pfp.features); features.forEach(feature => { const featureItem = document.createElement('div'); featureItem.className = 'feature-item'; featureItem.innerHTML = ` ${feature.category} ${feature.name} ${feature.value} `; featuresDiv.appendChild(featureItem); }); card.appendChild(numDiv); card.appendChild(nameDiv); card.appendChild(img); card.appendChild(featuresDiv); return card; } /** * 绑定事件 */ bindEvents() { // 全部显示按钮 document.getElementById('btn-show-all')?.addEventListener('click', () => { this.showPage(1, 'all'); this.updateActiveButton('all'); }); // 精选展示按钮 document.getElementById('btn-show-featured')?.addEventListener('click', () => { this.showPage(1, 'featured'); this.updateActiveButton('featured'); }); // 刷新按钮 document.getElementById('btn-refresh')?.addEventListener('click', async () => { await this.forceRefresh(); }); // 搜索事件 this.bindSearchEvents(); } /** * 绑定搜索事件 */ bindSearchEvents() { const searchInput = document.getElementById('search-input'); const clearBtn = document.getElementById('search-clear-btn'); const radioButtons = document.querySelectorAll('input[name="search-mode"]'); if (!searchInput) return; // 防抖搜索输入 let searchTimeout; searchInput.addEventListener('input', (e) => { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { this.searchFilter.setSearchTerm(e.target.value); this.showPage(1); // 重置到第1页 this.updateSearchUI(); }, 300); }); // 清除按钮 if (clearBtn) { clearBtn.addEventListener('click', () => { this.searchFilter.clear(); searchInput.value = ''; this.showPage(1); this.updateSearchUI(); }); } // 搜索模式切换 radioButtons.forEach(radio => { radio.addEventListener('change', (e) => { this.searchFilter.setSearchMode(e.target.value); if (this.searchFilter.isActive()) { this.showPage(1); } }); }); } /** * 更新搜索UI */ updateSearchUI() { const clearBtn = document.getElementById('search-clear-btn'); if (clearBtn) { clearBtn.style.display = this.searchFilter.isActive() ? 'block' : 'none'; } } /** * 处理筛选变化 */ handleFilterChange() { this.showPage(1); // 重置到第1页 } /** * 更新筛选状态指示器 */ updateFilterIndicators() { const indicators = []; // 精选筛选 if (this.currentMode === 'featured') { indicators.push('精选'); } // 搜索筛选 if (this.searchFilter.isActive()) { const term = this.searchFilter.getSearchTerm(); const mode = this.searchFilter.getSearchMode() === 'exact' ? '精确' : '模糊'; indicators.push(`搜索: ${term} (${mode})`); } // 特征筛选 if (this.featureFilter && this.featureFilter.isActive()) { const count = this.featureFilter.getSelectedFeatures().size; const logic = this.featureFilter.getLogicMode(); indicators.push(`特征: ${count}项 (${logic})`); } // 更新UI const indicatorEl = document.getElementById('active-filters'); if (indicatorEl) { if (indicators.length > 0) { indicatorEl.innerHTML = `当前筛选: ${indicators.join(' + ')}`; indicatorEl.style.display = 'block'; } else { indicatorEl.style.display = 'none'; } } // 同步更新收缩栏的信息 if (this.updateCollapsedInfo) { this.updateCollapsedInfo(); } } /** * 更新按钮激活状态 * @param {string} mode - 当前模式 */ updateActiveButton(mode) { document.querySelectorAll('.filter-btn').forEach(btn => { btn.classList.remove('active'); }); if (mode === 'all') { document.getElementById('btn-show-all')?.classList.add('active'); } else if (mode === 'featured') { document.getElementById('btn-show-featured')?.classList.add('active'); } } /** * 处理分页变化 * @param {number} page - 新页码 */ handlePageChange(page) { this.showPage(page); } /** * 更新统计信息 * @param {Object} info - 分页信息 */ updateStats(info) { const statsEl = document.getElementById('stats'); if (statsEl) { statsEl.textContent = `共 ${info.total} 条数据 | 第 ${this.currentPage} / ${info.totalPages} 页 (点击封面图查看完整版)`; } } /** * 显示加载中 * @param {string} message - 加载消息 */ showLoading(message = '加载中...') { const loading = document.getElementById('loading'); if (loading) { loading.textContent = message; loading.style.display = 'block'; } } /** * 隐藏加载中 */ hideLoading() { const loading = document.getElementById('loading'); if (loading) { loading.style.display = 'none'; } } /** * 显示错误 * @param {string} message - 错误消息 */ showError(message) { this.hideLoading(); alert('错误: ' + message); } /** * 显示PFP详情(使用抽屉组件) * @param {Object} pfp - PFP数据对象 */ showFullImage(pfp) { // 使用抽屉组件显示详情 this.drawer.open(pfp); } /** * 强制刷新数据 */ async forceRefresh() { if (confirm('确定要清除缓存并重新加载数据吗?')) { this.showLoading('正在刷新数据...'); try { this.allData = await this.loader.forceRefresh(); await this.showPage(1, this.currentMode); this.hideLoading(); alert('刷新成功!'); } catch (error) { this.showError('刷新失败:' + error.message); } } } } // 页面加载完成后初始化应用 document.addEventListener('DOMContentLoaded', () => { const app = new PFPApp(); app.init(); // 挂载到全局对象,方便调试 window.pfpApp = app; });