Files
rust-imager/assets/js/app.js
2026-07-21 00:43:40 +08:00

1686 lines
58 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.

// 全局变量
let allAlbums = []; // 存储所有相册数据
let displayedAlbums = []; // 当前显示的相册数据
let filteredAlbums = null; // 过滤后的相册数据(有筛选时使用,null=无筛选)
let searchTimeout = null;
let currentDisplayPage = 1; // 前端显示的分页
let albumsPerPage = 50; // 每次显示50个相册
let isLoadingMore = false;
let hasMoreAlbums = true;
let lazyLoadObserver = null;
let isAllAlbumsLoaded = false; // 标记是否已加载所有相册数据
let isInFavoritesPage = false; // 标记当前是否在收藏页
// 标签和人物筛选
let currentFilter = null; // { type: 'tag'|'person', value: string }
let collapsedSections = { people: false, tags: false };
// 侧边栏状态(持久化)
let sidebarCollapsed = localStorage.getItem('sidebarCollapsed') === 'true';
// API基础URL
const API_BASE = '/api';
// 图片加载器类
class ImageLoader {
constructor() {
this.cache = new Map();
}
// 加载图片
async loadImage(imageId, element) {
// 检查缓存
if (this.cache.has(imageId)) {
const cachedData = this.cache.get(imageId);
this.setImageSrc(element, cachedData.blobUrl, cachedData.mimeType);
return;
}
try {
const response = await fetch(`${API_BASE}/image/${imageId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const contentType = response.headers.get('content-type');
const imageData = await response.blob();
// 创建Blob URL
const mimeType = contentType || 'image/jpeg';
const blob = new Blob([imageData], { type: mimeType });
const blobUrl = URL.createObjectURL(blob);
// 缓存结果
this.cache.set(imageId, { blobUrl, mimeType });
// 设置图片源
this.setImageSrc(element, blobUrl, mimeType);
} catch (error) {
console.error('图片加载失败:', error);
// 加载失败时使用占位图
element.src = '/assets/placeholder.svg';
}
}
// 设置图片源
setImageSrc(element, blobUrl, mimeType) {
// 创建新的Image对象进行预加载
const img = new Image();
img.onload = () => {
element.src = blobUrl;
element.classList.add('loaded');
};
img.onerror = () => {
console.error('图片解码失败');
element.src = '/assets/placeholder.svg';
};
img.src = blobUrl;
}
// 清理缓存
clearCache() {
// 清理所有Blob URL
for (const [imageId, cached] of this.cache.entries()) {
URL.revokeObjectURL(cached.blobUrl);
}
this.cache.clear();
}
}
// 创建全局图片加载器实例
const imageLoader = new ImageLoader();
// 设备检测
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function () {
// 添加设备类到body
if (isMobile) {
document.body.classList.add('mobile-device');
}
if (isTouchDevice) {
document.body.classList.add('touch-device');
}
initSearch();
initInfiniteScroll();
initLazyLoading(); // 初始化图片懒加载
initSidebarSections(); // 初始化侧边栏区块折叠状态
// 检查URL中是否有search参数
const urlParams = new URLSearchParams(window.location.search);
const searchQuery = urlParams.get('search');
if (searchQuery) {
const searchInput = document.getElementById('search-input');
searchInput.value = searchQuery;
// 直接搜索,不先加载全部相册
searchAlbums();
} else {
loadAlbums();
}
});
// 初始化搜索功能
function initSearch() {
const searchInput = document.getElementById('search-input');
// 防抖搜索
searchInput.addEventListener('input', function () {
clearTimeout(searchTimeout);
// 优化:根据输入长度动态调整延迟
const inputLength = this.value.length;
const debounceDelay = inputLength > 3 ? 200 : 500; // 长文本快速搜索,短文本慢速搜索
searchTimeout = setTimeout(debouncedSearch, debounceDelay);
});
// 移动端优化:搜索框聚焦时自动滚动到可视区域
if (isMobile) {
searchInput.addEventListener('focus', function () {
setTimeout(() => {
searchInput.scrollIntoView({behavior: 'smooth', block: 'center'});
}, 100);
});
}
}
// 初始化无限滚动
function initInfiniteScroll() {
let scrollTimeout;
window.addEventListener('scroll', function () {
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(handleScroll, 100);
});
}
// 初始化图片懒加载
function initLazyLoading() {
// 使用Intersection Observer实现懒加载
if ('IntersectionObserver' in window) {
if (lazyLoadObserver) {
lazyLoadObserver.disconnect();
}
lazyLoadObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
const imageId = img.getAttribute('data-image-id');
if (imageId && !img.classList.contains('loaded')) {
imageLoader.loadImage(imageId, img);
}
lazyLoadObserver.unobserve(img);
}
});
}, {
rootMargin: '100px 0px', // 提前100px开始加载
threshold: 0.1
});
// 观察所有带有data-image-id的图片
document.querySelectorAll('img[data-image-id]').forEach(img => {
if (!img.classList.contains('loaded')) {
lazyLoadObserver.observe(img);
}
});
} else {
// 不支持Intersection Observer的回退方案
console.warn('浏览器不支持Intersection Observer,使用回退懒加载方案');
initFallbackLazyLoading();
}
}
// 回退懒加载方案
function initFallbackLazyLoading() {
let scrollTimeout;
function checkImagesInView() {
const images = document.querySelectorAll('img[data-image-id]:not(.loaded)');
const viewportHeight = window.innerHeight;
const scrollTop = window.scrollY;
images.forEach(img => {
const rect = img.getBoundingClientRect();
const isInView = rect.top < viewportHeight + 200 && rect.bottom > -200;
if (isInView) {
const imageId = img.getAttribute('data-image-id');
if (imageId) {
imageLoader.loadImage(imageId, img);
}
}
});
}
window.addEventListener('scroll', () => {
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(checkImagesInView, 100);
});
// 初始检查
checkImagesInView();
}
// 处理滚动事件
function handleScroll() {
// 如果在收藏页,禁用无限滚动
if (isInFavoritesPage) return;
if (isLoadingMore || !hasMoreAlbums) return;
// 检查是否滚动到底部
const scrollTop = window.scrollY || document.documentElement.scrollTop;
const scrollHeight = document.documentElement.scrollHeight;
const clientHeight = window.innerHeight;
// 当距离底部还有200px时开始加载
if (scrollTop + clientHeight >= scrollHeight - 200) {
loadMoreAlbums();
}
}
// 加载更多相册(从前端已加载的数据中获取)
async function loadMoreAlbums() {
// 如果在收藏页,不加载更多数据
if (isInFavoritesPage) return;
if (isLoadingMore || !hasMoreAlbums) return;
// 全量模式下(无筛选)需等 isAllAlbumsLoaded;过滤模式下直接使用 filteredAlbums
if (!currentFilter && !isAllAlbumsLoaded) return;
isLoadingMore = true;
try {
// 显示加载更多指示器
showLoadingMore();
// 增加当前显示页数
currentDisplayPage++;
// 显示下一页数据
displayCurrentPage();
// 初始化图片懒加载
initLazyLoading();
// 预加载下一批图片
preloadNextBatchImages();
} catch (error) {
console.error('加载更多相册失败:', error);
showLoadMoreError();
} finally {
isLoadingMore = false;
hideLoadingMore();
}
}
// 显示加载更多错误
function showLoadMoreError() {
const container = document.getElementById('albums-container');
const errorElement = document.createElement('div');
errorElement.className = 'load-more-error';
errorElement.innerHTML = `
<div class="error-content">
<i class="fas fa-exclamation-triangle"></i>
<span>加载失败,<a href="javascript:void(0)" onclick="loadMoreAlbums()">点击重试</a></span>
</div>
`;
const existingError = container.querySelector('.load-more-error');
if (existingError) {
existingError.remove();
}
container.appendChild(errorElement);
}
// 显示骨架屏
function showSkeleton(count = 6) {
const container = document.getElementById('albums-container');
container.innerHTML = Array(count).fill(0).map(() => `
<div class="skeleton-card">
<div class="skeleton-cover skeleton"></div>
<div class="skeleton-content">
<div class="skeleton-title skeleton"></div>
<div class="skeleton-meta">
<div class="skeleton-meta-item skeleton"></div>
<div class="skeleton-meta-item skeleton"></div>
</div>
<div class="skeleton-button skeleton"></div>
</div>
</div>
`).join('');
}
// 显示详细加载状态
function showLoadingState(message = '加载中...', progress = 0) {
const container = document.getElementById('albums-container');
const progressWidth = Math.min(Math.max(progress, 0), 100);
container.innerHTML = `
<div class="loading-state">
<i class="fas fa-spinner fa-spin"></i>
<p>${message}</p>
<div class="progress-bar">
<div class="progress" style="width: ${progressWidth}%"></div>
</div>
</div>
`;
}
// 显示加载更多指示器
function showLoadingMore() {
const container = document.getElementById('albums-container');
const loadingIndicator = document.createElement('div');
loadingIndicator.id = 'loading-more';
loadingIndicator.className = 'loading-more';
loadingIndicator.innerHTML = `
<div class="loading-spinner">
<i class="fas fa-spinner fa-spin"></i>
<span>加载更多相册...</span>
</div>
`;
container.appendChild(loadingIndicator);
}
// 隐藏加载更多指示器
function hideLoadingMore() {
const loadingIndicator = document.getElementById('loading-more');
if (loadingIndicator) {
loadingIndicator.remove();
}
}
// 追加相册到列表
function appendAlbums(albums) {
const container = document.getElementById('albums-container');
const newAlbumsHTML = albums.map(album => {
// 处理封面图片URL - 使用懒加载
let coverImageUrl = '/assets/placeholder.svg';
let dataSrc = '';
if (album.cover_image_id) {
dataSrc = `${API_BASE}/image/${album.cover_image_id}`;
coverImageUrl = '/assets/placeholder.svg'; // 初始使用占位图
}
// 格式化日期
const createdDate = new Date(album.created_at).toLocaleDateString('zh-CN');
return `
<div class="album-card" data-album-id="${album.id}" onclick="openAlbum('${album.id}')">
<div class="album-cover">
<img
src="${coverImageUrl}"
${album.cover_image_id ? `data-image-id="${album.cover_image_id}"` : ''}
alt="${album.title}"
class="lazy"
onerror="this.src='/assets/placeholder.svg'"
>
</div>
<div class="album-info">
<h3>${album.title}</h3>
<div class="album-meta">
<span><i class="fas fa-images"></i> ${album.image_count} 张图片</span>
<span><i class="fas fa-calendar"></i> ${createdDate}</span>
</div>
</div>
</div>
`;
}).join('');
// 移除加载指示器并添加新相册
hideLoadingMore();
container.insertAdjacentHTML('beforeend', newAlbumsHTML);
// 重新初始化懒加载,观察新添加的图片
initLazyLoading();
}
// 加载所有相册数据(一次性加载全部)
async function loadAlbums(reset = false) {
try {
if (reset) {
currentDisplayPage = 1;
allAlbums = [];
displayedAlbums = [];
hasMoreAlbums = true;
isAllAlbumsLoaded = false;
const container = document.getElementById('albums-container');
container.innerHTML = '';
}
// 如果已经加载过所有数据,直接显示当前页
if (isAllAlbumsLoaded) {
displayCurrentPage();
return;
}
// 显示骨架屏
showSkeleton();
// 一次性加载所有相册数据
const response = await fetch(`${API_BASE}/albums`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
allAlbums = await response.json();
isAllAlbumsLoaded = true;
// 显示第一页数据
displayCurrentPage();
updateStats(allAlbums);
// 初始化图片懒加载
initLazyLoading();
// 预加载下一批图片
preloadNextBatchImages();
// 加载热门标签和人物
loadDiscoverData();
} catch (error) {
console.error('加载相册失败:', error);
showError('加载相册数据失败,请刷新页面重试');
}
}
// 显示当前页的相册数据
function displayCurrentPage() {
// 有筛选时用 filteredAlbums,否则用 allAlbums
const source = filteredAlbums || allAlbums;
const startIndex = (currentDisplayPage - 1) * albumsPerPage;
const endIndex = startIndex + albumsPerPage;
// 获取当前页要显示的相册
displayedAlbums = source.slice(startIndex, endIndex);
// 如果是第一页,直接显示;否则追加显示
if (currentDisplayPage === 1) {
displayAlbums(displayedAlbums);
} else {
appendAlbums(displayedAlbums);
}
// 检查是否还有更多数据可以显示
hasMoreAlbums = (currentDisplayPage * albumsPerPage) < source.length;
}
// 搜索相册
// 防抖搜索函数
async function debouncedSearch() {
const searchTerm = document.getElementById('search-input').value.toLowerCase().trim();
if (searchTerm === '') {
// 搜索框为空 → 清除过滤状态,显示所有相册
currentFilter = null;
filteredAlbums = null;
displayAlbums(allAlbums);
updateStats();
return;
}
// 直接使用后端搜索API进行全站搜索
const results = await performServerSearchWithResult(searchTerm);
if (results && results.length > 0) {
filteredAlbums = results;
currentDisplayPage = 1;
hasMoreAlbums = results.length > albumsPerPage;
displayAlbums(results);
updateStats(results);
initLazyLoading();
}
}
// 在当前已加载的数据集中搜索
function searchInLoadedAlbums(searchTerm) {
if (!allAlbums.length) return [];
return allAlbums.filter(album => {
// 搜索标题和描述
const titleMatch = album.title.toLowerCase().includes(searchTerm);
const descriptionMatch = album.description && album.description.toLowerCase().includes(searchTerm);
return titleMatch || descriptionMatch;
});
}
// 显示搜索提示
function showSearchHint(searchTerm) {
const container = document.getElementById('albums-container');
container.innerHTML = `
<div class="search-hint">
<i class="fas fa-search"></i>
<h3>未在当前页面找到 "${searchTerm}"</h3>
<p>尝试:</p>
<ul>
<li>继续滚动加载更多相册</li>
<li>使用更精确的搜索词</li>
<li><button class="btn-primary" onclick="performServerSearch('${searchTerm}')">在全站搜索 "${searchTerm}"</button></li>
</ul>
</div>
`;
document.getElementById('stats').innerHTML = `在当前页面中未找到包含 "${searchTerm}" 的相册`;
}
// 执行服务器端搜索(全站搜索)
async function performServerSearch(searchTerm) {
const results = await performServerSearchWithResult(searchTerm);
if (results && results.length > 0) {
displayAlbums(results);
const oldStats = document.getElementById('stats');
if (oldStats) {
oldStats.innerHTML = `在全站找到 ${results.length} 个包含 "${searchTerm}" 的相册`;
}
updateStats(results);
initLazyLoading();
}
}
/// 执行服务器端搜索并返回结果数组(不直接渲染)
async function performServerSearchWithResult(searchTerm) {
try {
const response = await fetch(`${API_BASE}/search?q=${encodeURIComponent(searchTerm)}&page=1&limit=200`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const serverResults = await response.json();
if (serverResults.length === 0) {
return [];
}
return serverResults;
} catch (error) {
console.error('全站搜索失败:', error);
return [];
}
}
// 显示相册列表
function displayAlbums(albums) {
const container = document.getElementById('albums-container');
if (albums.length === 0) {
container.innerHTML = '<div class="no-results"><i class="fas fa-search"></i><br>没有找到匹配的相册</div>';
return;
}
container.innerHTML = albums.map(album => {
// 处理封面图片URL - 使用懒加载
let coverImageUrl = '/assets/placeholder.svg';
let dataSrc = '';
if (album.cover_image_id) {
dataSrc = `${API_BASE}/image/${album.cover_image_id}`;
coverImageUrl = '/assets/placeholder.svg'; // 初始使用占位图
}
// 格式化日期
const createdDate = new Date(album.created_at).toLocaleDateString('zh-CN');
return `
<div class="album-card" data-album-id="${album.id}" onclick="openAlbum('${album.id}')">
<div class="album-cover">
<img
src="${coverImageUrl}"
${album.cover_image_id ? `data-image-id="${album.cover_image_id}"` : ''}
alt="${album.title}"
class="lazy"
onerror="this.src='/assets/placeholder.svg'"
>
</div>
<div class="album-info">
<h3>${album.title}</h3>
<div class="album-meta">
<span><i class="fas fa-images"></i> ${album.image_count} 张图片</span>
<span><i class="fas fa-calendar"></i> ${createdDate}</span>
</div>
</div>
</div>
`;
}).join('');
}
// 预加载下一批图片
function preloadNextBatchImages() {
const imagesToPreload = [];
const visibleImages = document.querySelectorAll('img[data-image-id]:not(.loaded)');
// 预加载前5张可见图片
visibleImages.forEach((img, index) => {
if (index < 5) {
const imageId = img.getAttribute('data-image-id');
if (imageId) {
imagesToPreload.push(imageId);
}
}
});
// 使用图片加载器预加载
imagesToPreload.forEach(imageId => {
const tempImg = new Image();
imageLoader.loadImage(imageId, tempImg);
});
}
// 更新统计信息
function updateStats(albums) {
if (!albums) {
albums = allAlbums;
}
const totalImages = albums.reduce((sum, album) => sum + album.image_count, 0);
// 更新统计条
const statAlbums = document.getElementById('stat-albums');
const statImages = document.getElementById('stat-images');
if (statAlbums) statAlbums.textContent = albums.length.toLocaleString();
if (statImages) statImages.textContent = totalImages.toLocaleString();
// 兼容旧版 stats
const oldStats = document.getElementById('stats');
if (oldStats) oldStats.innerHTML = `共发现 ${albums.length} 个相册,包含 ${totalImages} 张图片`;
// 获取人物数量(用 metadata stats API)
fetch(`${API_BASE}/metadata/stats`).then(r => r.ok && r.json()).then(data => {
if (data && data.total_persons !== undefined) {
const statPeople = document.getElementById('stat-people');
if (statPeople) statPeople.textContent = data.total_persons.toLocaleString();
}
}).catch(() => {});
}
// 显示错误信息
function showError(message) {
const container = document.getElementById('albums-container');
container.innerHTML = `<div class="no-results"><i class="fas fa-exclamation-triangle"></i><br>${message}</div>`;
const oldStats = document.getElementById('stats');
if (oldStats) oldStats.innerHTML = '';
}
// 搜索相册函数
async function searchAlbums() {
await debouncedSearch();
}
// 性能监控
function logPerformance() {
if ('performance' in window) {
// 使用更可靠的方式获取页面加载时间
const calculateLoadTime = () => {
const navigationTiming = performance.getEntriesByType('navigation')[0];
if (navigationTiming && navigationTiming.loadEventEnd > 0 && navigationTiming.navigationStart > 0) {
const loadTime = navigationTiming.loadEventEnd - navigationTiming.navigationStart;
console.log('页面加载时间:', loadTime, 'ms');
} else {
// 备用方案:使用DOMContentLoaded时间
const domContentLoadedTime = performance.timing?.domContentLoadedEventEnd - performance.timing?.navigationStart;
if (domContentLoadedTime && domContentLoadedTime > 0) {
console.log('DOM加载时间:', domContentLoadedTime, 'ms');
} else {
console.log('页面加载时间: 无法计算');
}
}
};
// 如果页面已经加载完成,直接计算
if (document.readyState === 'complete') {
calculateLoadTime();
} else {
// 否则等待页面加载完成
window.addEventListener('load', calculateLoadTime);
}
}
}
// 移动端优化:防止双击缩放
function preventDoubleTapZoom() {
if (isTouchDevice) {
let lastTouchEnd = 0;
document.addEventListener('touchend', function (event) {
const now = (new Date()).getTime();
if (now - lastTouchEnd <= 300) {
event.preventDefault();
}
lastTouchEnd = now;
}, false);
}
}
// 预加载关键资源
function preloadCriticalResources() {
// 预加载字体文件(CSS文件不应该预加载,因为已经在HTML中引用了)
const fontResources = [
'/assets/webfonts/fa-solid-900.woff2',
'/assets/webfonts/fa-brands-400.woff2',
'/assets/webfonts/fa-regular-400.woff2'
];
fontResources.forEach(resource => {
const link = document.createElement('link');
link.rel = 'preload';
link.href = resource;
link.as = 'font';
link.crossOrigin = 'anonymous';
document.head.appendChild(link);
});
// 预加载第一屏可能需要的图片
setTimeout(() => {
const firstScreenImages = [
'/assets/placeholder.svg'
];
firstScreenImages.forEach(img => {
const preload = document.createElement('link');
preload.rel = 'preload';
preload.href = img;
preload.as = 'image';
document.head.appendChild(preload);
});
}, 100);
}
// 优化滚动性能
function optimizeScrollPerformance() {
// 使用requestAnimationFrame优化滚动
let ticking = false;
function updateOnScroll() {
// 在滚动时暂停非关键任务
if (!ticking) {
requestAnimationFrame(() => {
// 延迟加载非关键图片
delayNonCriticalImages();
ticking = false;
});
ticking = true;
}
}
window.addEventListener('scroll', updateOnScroll, {passive: true});
}
// 延迟加载非关键图片
function delayNonCriticalImages() {
const nonCriticalImages = document.querySelectorAll('img[data-delayed]');
nonCriticalImages.forEach(img => {
if (isElementInViewport(img)) {
const src = img.getAttribute('data-delayed');
if (src) {
img.src = src;
img.removeAttribute('data-delayed');
}
}
});
}
// 检查元素是否在视口中
function isElementInViewport(el) {
const rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
// 初始化性能监控和移动端优化
logPerformance();
preventDoubleTapZoom();
// 预加载关键资源
preloadCriticalResources();
// 优化滚动性能
optimizeScrollPerformance();
// 打开相册详情
function openAlbum(albumId) {
window.location.href = `/album?id=${albumId}`;
}
// 随机打开相册
function openRandomAlbum() {
// 添加随机动画效果 - 兼容侧边栏导航项
const navItems = document.querySelectorAll('.nav-item');
let randomButton = null;
navItems.forEach(item => {
if (item.textContent.includes('随机')) randomButton = item;
});
// 也试试顶部操作按钮
if (!randomButton) {
randomButton = document.querySelector('.action-btn[onclick*="openRandomAlbum"]');
}
if (randomButton) {
randomButton.style.transform = 'scale(0.95)';
}
// 调用新的随机相册API
fetch(`${API_BASE}/album/random`)
.then(response => {
if (!response.ok) {
if (response.status === 404) {
throw new Error('暂无相册数据,请稍后再试');
}
throw new Error('获取随机相册失败');
}
return response.json();
})
.then(randomAlbum => {
// 恢复按钮状态
if (randomButton) {
setTimeout(() => {
randomButton.style.transform = '';
}, 200);
}
// 跳转到随机相册
window.location.href = `/album?id=${randomAlbum.id}`;
})
.catch(error => {
// 恢复按钮状态
if (randomButton) {
setTimeout(() => {
randomButton.style.transform = '';
}, 200);
}
alert(error.message);
});
}
// 显示收藏夹
function showFavorites() {
// 设置当前在收藏页
isInFavoritesPage = true;
// 切换侧边栏选中状态
document.querySelectorAll('.nav-item').forEach(item => item.classList.remove('active'));
const favNav = document.querySelector('.nav-item[data-view="favorites"]');
if (favNav) favNav.classList.add('active');
// 从后端API获取收藏列表
fetch(`${API_BASE}/favorites`)
.then(response => {
if (!response.ok) {
throw new Error('获取收藏列表失败');
}
return response.json();
})
.then(favoriteImages => {
// 始终显示收藏夹视图(包括空状态)
displayFavorites(favoriteImages);
})
.catch(error => {
console.error('获取收藏列表失败:', error);
alert('获取收藏列表失败,请稍后重试');
});
}
// 显示收藏夹内容
function displayFavorites(favoriteImages) {
const container = document.getElementById('albums-container');
container.innerHTML = `
${favoriteImages.length === 0 ? `
<div class="empty-favorites">
<div class="empty-icon">
<i class="fas fa-heart-broken"></i>
</div>
<h3>收藏夹空空如也</h3>
<p>快去收藏一些喜欢的图片吧!</p>
<button class="explore-button" onclick="loadAlbums()">
<i class="fas fa-compass"></i> 探索相册
</button>
</div>
` : `${favoriteImages.map((fav, index) => `
<div class="favorite-card" onclick="openFavoriteImage('${fav.favorite.album_id}')" tabindex="0">
<div class="card-image">
<img src="/assets/placeholder.svg"
data-image-id="${fav.image.id}"
alt="${fav.image.title || '收藏的图片'}"
loading="lazy">
<div class="card-overlay">
<div class="overlay-content">
<button class="quick-view" onclick="event.stopPropagation(); previewFavorite(${index})" aria-label="预览图片">
<i class="fas fa-expand"></i>
</button>
<button class="remove-favorite" onclick="removeFavorite(event, '${fav.favorite.id}')" aria-label="移除收藏">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<div class="favorite-badge">
<i class="fas fa-heart"></i>
</div>
</div>
<div class="card-content">
<div class="image-meta">
<span class="meta-item">
<i class="fas fa-calendar"></i>
${formatDate(fav.favorite.created_at)}
</span>
<span class="meta-item">
<i class="fas fa-ruler-combined"></i>
${fav.image.width}×${fav.image.height}
</span>
</div>
</div>
</div>
`).join('')}
`}
`;
// 更新统计信息 - 显示收藏数量(兼容新旧版布局)
const statsEl = document.getElementById('stats');
if (statsEl) {
statsEl.innerHTML = `<div class="favorites-header">
<div class="header-content">
<div class="header-left">
<div class="favorites-icon">
<i class="fas fa-heart"></i>
</div>
<div class="header-info">
<h1>我的收藏夹</h1>
<p class="favorites-count">共收藏了 <span class="count-number">${favoriteImages.length}</span> 张图片</p>
</div>
</div>
<div class="header-right">
<div class="action-buttons">
<button class="back-button" onclick="backToHome()">
<i class="fas fa-arrow-left"></i> 返回首页
</button>
</div>
</div>
</div>
</div>`;
}
// 初始化图片懒加载,确保收藏的图片被加载
initLazyLoading();
}
// 格式化日期
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
}
// 返回首页
function backToHome() {
// 重置收藏页标记
isInFavoritesPage = false;
// 切换导航高亮到首页
document.querySelectorAll('.nav-item').forEach(item => {
item.classList.toggle('active', item.dataset.view === 'home');
});
// 清除筛选
clearFilter();
}
// 全局变量存储收藏列表和当前预览索引
let currentFavorites = [];
let currentPreviewIndex = -1;
// 预览收藏的图片
function previewFavorite(index) {
// 获取收藏列表
fetch(`${API_BASE}/favorites`)
.then(response => {
if (!response.ok) {
throw new Error('获取收藏列表失败');
}
return response.json();
})
.then(favorites => {
if (favorites && favorites.length > index) {
currentFavorites = favorites;
currentPreviewIndex = index;
showPreviewModal(index);
} else {
alert('图片不存在或已被删除');
}
})
.catch(error => {
console.error('预览图片失败:', error);
alert('预览图片失败,请稍后重试');
});
}
// 显示预览模态框
function showPreviewModal(index) {
const favorite = currentFavorites[index];
// 构建模态框HTML
const modalHtml = `
<div class="preview-modal" id="previewModal">
<div class="preview-overlay" onclick="closePreviewModal()"></div>
<div class="preview-container">
<div class="preview-header">
<div class="preview-title">
<span class="preview-info">来自: ${favorite.album_title}</span>
</div>
<div class="preview-actions">
<button class="preview-nav preview-prev" onclick="navigatePreview(-1)" ${index === 0 ? 'disabled' : ''}>
<i class="fas fa-chevron-left"></i>
</button>
<span class="preview-counter">${index + 1} / ${currentFavorites.length}</span>
<button class="preview-nav preview-next" onclick="navigatePreview(1)" ${index === currentFavorites.length - 1 ? 'disabled' : ''}>
<i class="fas fa-chevron-right"></i>
</button>
<button class="preview-close" onclick="closePreviewModal()">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<div class="preview-content">
<div class="preview-image-container">
<img src="${API_BASE}/image/${favorite.image.id}"
alt="${favorite.image.title || '预览图片'}"
class="preview-image"
id="preview-image"
loading="eager">
</div>
</div>
<div class="preview-footer">
<div class="preview-meta">
<span class="meta-item">
<i class="fas fa-calendar"></i>
${formatDate(favorite.favorite.created_at)}
</span>
<span class="meta-item">
<i class="fas fa-ruler-combined"></i>
${favorite.image.width} × ${favorite.image.height}
</span>
<span class="meta-item">
<i class="fas fa-heart"></i>
已收藏
</span>
</div>
<div class="preview-controls">
<button class="preview-action" onclick="openFavoriteImage('${favorite.favorite.album_id}')">
<i class="fas fa-folder-open"></i> 打开相册
</button>
<button class="preview-action preview-remove" onclick="removeFavoriteFromPreview('${favorite.favorite.id}')">
<i class="fas fa-trash"></i> 取消收藏
</button>
</div>
</div>
</div>
</div>
`;
// 添加到页面
document.body.insertAdjacentHTML('beforeend', modalHtml);
// 显示模态框
setTimeout(() => {
const modal = document.getElementById('previewModal');
modal.classList.add('active');
}, 10);
// 监听键盘事件
document.addEventListener('keydown', handlePreviewKeydown);
}
// 关闭预览模态框
function closePreviewModal() {
const modal = document.getElementById('previewModal');
if (modal) {
modal.classList.remove('active');
setTimeout(() => {
modal.remove();
}, 300);
}
// 移除键盘事件监听
document.removeEventListener('keydown', handlePreviewKeydown);
}
// 导航预览图片
function navigatePreview(direction) {
const newIndex = currentPreviewIndex + direction;
if (newIndex >= 0 && newIndex < currentFavorites.length) {
updatePreviewContent(newIndex);
}
}
// 更新预览内容(不关闭面板)
function updatePreviewContent(newIndex) {
const favorite = currentFavorites[newIndex];
currentPreviewIndex = newIndex;
// 更新标题和相册信息
const titleElement = document.querySelector('.preview-title h3');
const infoElement = document.querySelector('.preview-info');
if (titleElement) titleElement.textContent = favorite.image.title || '未命名图片';
if (infoElement) infoElement.textContent = `来自: ${favorite.album_title}`;
// 更新计数器
const counterElement = document.querySelector('.preview-counter');
if (counterElement) counterElement.textContent = `${newIndex + 1} / ${currentFavorites.length}`;
// 更新导航按钮状态
const prevButton = document.querySelector('.preview-prev');
const nextButton = document.querySelector('.preview-next');
if (prevButton) prevButton.disabled = newIndex === 0;
if (nextButton) nextButton.disabled = newIndex === currentFavorites.length - 1;
// 更新元数据
const metaElements = document.querySelectorAll('.preview-meta .meta-item');
if (metaElements.length >= 3) {
metaElements[0].innerHTML = `<i class="fas fa-calendar"></i> ${formatDate(favorite.favorite.created_at)}`;
metaElements[1].innerHTML = `<i class="fas fa-ruler-combined"></i> ${favorite.image.width} × ${favorite.image.height}`;
}
// 更新操作按钮
const openAlbumButton = document.querySelector('.preview-action:first-child');
const removeButton = document.querySelector('.preview-remove');
if (openAlbumButton) {
openAlbumButton.onclick = () => openFavoriteImage(favorite.favorite.album_id);
}
if (removeButton) {
removeButton.onclick = () => removeFavoriteFromPreview(favorite.favorite.id);
}
// 更新图片
const imageElement = document.getElementById('preview-image');
if (imageElement) {
// 显示加载状态
imageElement.style.opacity = '0';
// 加载新图片
const newImage = new Image();
newImage.src = `${API_BASE}/image/${favorite.image.id}`;
newImage.alt = favorite.image.title || '预览图片';
newImage.className = 'preview-image';
newImage.onload = function () {
// 替换图片
if (imageElement.parentNode) {
imageElement.parentNode.replaceChild(newImage, imageElement);
// 重新设置ID
newImage.id = 'preview-image';
}
newImage.style.opacity = '1';
};
newImage.onerror = function () {
// 图片加载失败
imageElement.innerHTML = '<i class="fas fa-exclamation-triangle"></i><span>图片加载失败</span>';
};
}
}
// 处理键盘事件
function handlePreviewKeydown(event) {
if (event.key === 'Escape') {
closePreviewModal();
} else if (event.key === 'ArrowLeft') {
navigatePreview(-1);
} else if (event.key === 'ArrowRight') {
navigatePreview(1);
}
}
// 从预览中移除收藏
function removeFavoriteFromPreview(favoriteId) {
if (confirm('确定要从收藏夹移除这张图片吗?')) {
fetch(`${API_BASE}/favorites/${favoriteId}`, {
method: 'DELETE'
})
.then(response => {
if (!response.ok) {
throw new Error('取消收藏失败');
}
return response.json();
})
.then(() => {
closePreviewModal();
// 重新显示收藏夹
showFavorites();
})
.catch(error => {
console.error('取消收藏失败:', error);
alert('取消收藏失败,请稍后重试');
});
}
}
// 从收藏夹移除图片
function removeFavorite(event, favoriteId) {
event.stopPropagation(); // 阻止事件冒泡
if (confirm('确定要从收藏夹移除这张图片吗?')) {
fetch(`${API_BASE}/favorites/${favoriteId}`, {
method: 'DELETE'
})
.then(response => {
if (!response.ok) {
throw new Error('取消收藏失败');
}
return response.json();
})
.then(() => {
// 重新显示收藏夹
showFavorites();
})
.catch(error => {
console.error('取消收藏失败:', error);
alert('取消收藏失败,请稍后重试');
});
}
}
// 打开收藏的图片
function openFavoriteImage(favoriteId) {
// 这里可以跳转到对应的相册页面
// 在实际实现中,我们需要根据favoriteId获取相册信息
window.location.href = `/album?id=${favoriteId}`;
}
// ============================================================
// 标签和人物发现功能
// ============================================================
// 加载热门标签和人物数据
async function loadDiscoverData() {
try {
// 并行加载
const [tagsResp, peopleResp] = await Promise.all([
fetch(`${API_BASE}/tags/popular?limit=40`),
fetch(`${API_BASE}/recommend/popular-persons?limit=30`)
]);
if (tagsResp.ok) {
const tagsData = await tagsResp.json();
// 兼容两种格式:直接数组 或 { total, tags }
const tags = Array.isArray(tagsData) ? tagsData : (tagsData.tags || tagsData.items || []);
if (tags && tags.length > 0) {
renderTagCloud(tags);
}
}
if (peopleResp.ok) {
const people = await peopleResp.json();
if (people && people.length > 0) {
renderPeopleCloud(people);
}
}
} catch (error) {
console.warn('加载发现数据失败:', error);
}
}
// 渲染标签(侧边栏列表)
function renderTagCloud(tags) {
// 新版:侧边栏标签列表
const sidebarTags = document.getElementById('sidebar-tags');
if (sidebarTags && tags && tags.length) {
sidebarTags.innerHTML = tags.slice(0, 25).map(tag => {
const count = tag.album_cnt || tag.count || 1;
const name = tag.tag || tag.name || '';
const isActive = currentFilter && currentFilter.type === 'tag' && currentFilter.value === name;
return `<div class="sidebar-tag ${isActive ? 'active' : ''}" onclick="filterByTag('${name}')">
<span class="sidebar-tag-name">${name}</span>
<span class="sidebar-tag-count">${count}</span>
</div>`;
}).join('');
}
// 兼容旧版:标签云区域
const section = document.getElementById('tags-section');
const cloud = document.getElementById('tag-cloud');
if (section && cloud && tags && tags.length) {
section.style.display = 'block';
const maxCount = Math.max(...tags.map(t => t.album_cnt || t.count || 1));
const minCount = Math.min(...tags.map(t => t.album_cnt || t.count || 1));
cloud.innerHTML = tags.map(tag => {
const count = tag.album_cnt || tag.count || 1;
const name = tag.tag || tag.name || '';
const ratio = maxCount === minCount ? 0.5 : (count - minCount) / (maxCount - minCount);
const fontSize = 12 + ratio * 10;
const hue = 210 - ratio * 50;
const opacity = 0.6 + ratio * 0.4;
return `<span class="tag-chip"
style="font-size:${fontSize}px; color:hsl(${hue}, 70%, 45%); opacity:${opacity};"
onclick="filterByTag('${name}')"
title="${count} 个相册">
${name} <small>(${count})</small>
</span>`;
}).join('');
}
}
// 渲染热门人物(横向滚动条)
function renderPeopleCloud(people) {
// 侧边栏人物列表
const sidebarPeople = document.getElementById('sidebar-people');
if (sidebarPeople && people && people.length) {
sidebarPeople.innerHTML = people.slice(0, 25).map(p => {
const name = p.name || p.display_name || '';
const albumCount = p.total_albums || 0;
const isActive = currentFilter && currentFilter.type === 'person' && currentFilter.value === name;
return `<div class="sidebar-person ${isActive ? 'active' : ''}" onclick="filterByPerson('${name}')" title="${albumCount} 个相册">
<div class="sidebar-person-avatar">${name.charAt(0)}</div>
<div class="sidebar-person-info">
<div class="sidebar-person-name">${name}</div>
<div class="sidebar-person-count">${albumCount} 相册</div>
</div>
</div>`;
}).join('');
}
// 横向滚动条
const strip = document.getElementById('people-strip');
const scroll = document.getElementById('people-scroll');
if (strip && scroll && people && people.length) {
strip.style.display = 'flex';
scroll.innerHTML = people.slice(0, 20).map(p => {
const name = p.name || p.display_name || '';
const albumCount = p.total_albums || 0;
const isActive = currentFilter && currentFilter.type === 'person' && currentFilter.value === name;
return `<div class="person-chip-scroll ${isActive ? 'active' : ''}" onclick="filterByPerson('${name}')" title="${albumCount} 个相册">
<div class="person-chip-avatar">${name.charAt(0)}</div>
<div class="person-chip-info">
<div class="person-chip-name">${name}</div>
<div class="person-chip-count">${albumCount} 相册</div>
</div>
</div>`;
}).join('');
}
// 兼容旧版:人物卡片区域
const section = document.getElementById('people-section');
const cloud = document.getElementById('people-cloud');
if (section && cloud && people && people.length) {
section.style.display = 'block';
cloud.innerHTML = people.map(p => {
const name = p.name || p.display_name || '';
const albumCount = p.total_albums || 0;
const imageCount = p.total_images || 0;
return `<div class="person-chip" onclick="filterByPerson('${name}')" title="${albumCount} 个相册, ${imageCount} 张图片">
<div class="person-avatar">${name.charAt(0)}</div>
<div class="person-info">
<div class="person-name">${name}</div>
<div class="person-meta">${albumCount} 相册 · ${imageCount} 图</div>
</div>
</div>`;
}).join('');
}
}
// 侧边栏区块折叠/展开
function toggleSidebarSection(section) {
const group = document.querySelector(`.sidebar-section-group [data-section="${section}"]`)?.closest('.sidebar-section-group');
if (!group) return;
group.classList.toggle('collapsed');
// 保存状态到 localStorage
try {
const state = JSON.parse(localStorage.getItem('sidebarSections') || '{}');
state[section] = group.classList.contains('collapsed');
localStorage.setItem('sidebarSections', JSON.stringify(state));
} catch(e) {}
}
// 初始化侧边栏区块折叠状态
function initSidebarSections() {
try {
const state = JSON.parse(localStorage.getItem('sidebarSections') || '{}');
Object.keys(state).forEach(section => {
if (state[section]) {
const group = document.querySelector(`.sidebar-section-group [data-section="${section}"]`)?.closest('.sidebar-section-group');
if (group) group.classList.add('collapsed');
}
});
} catch(e) {}
}
// 按标签筛选
async function filterByTag(tag) {
currentFilter = { type: 'tag', value: tag };
showFilterStatus(`标签: ${tag}`);
renderTagCloud(await fetchTags().catch(() => []));
try {
showSkeleton(6);
const response = await fetch(`${API_BASE}/tags/search?tag=${encodeURIComponent(tag)}&limit=200`);
if (!response.ok) throw new Error('搜索失败');
const results = await response.json();
// 保存过滤结果到 filteredAlbums,重置分页
filteredAlbums = results;
currentDisplayPage = 1;
hasMoreAlbums = filteredAlbums.length > albumsPerPage;
displayAlbums(results);
updateStats(results);
initLazyLoading();
} catch (error) {
console.error('标签筛选失败:', error);
showError('标签筛选失败');
}
}
// 获取标签(辅助函数)
async function fetchTags() {
const resp = await fetch(`${API_BASE}/tags/popular?limit=25`);
if (!resp.ok) return [];
return resp.json();
}
// 按人物筛选
async function filterByPerson(personName) {
currentFilter = { type: 'person', value: personName };
showFilterStatus(`人物: ${personName}`);
// 刷新侧边栏人物高亮
try {
const peopleResp = await fetch(`${API_BASE}/recommend/popular-persons?limit=30`);
if (peopleResp.ok) {
const peopleData = await peopleResp.json();
renderPeopleCloud(peopleData);
}
} catch(e) {}
try {
showSkeleton(6);
const response = await fetch(`${API_BASE}/people/search?q=${encodeURIComponent(personName)}`);
if (!response.ok) throw new Error('搜索失败');
const personData = await response.json();
const persons = personData.persons || personData;
if (persons && persons.length > 0) {
try {
const detailResp = await fetch(`${API_BASE}/people/${encodeURIComponent(personName)}`);
if (detailResp.ok) {
const detail = await detailResp.json();
if (detail && detail.albums) {
// 保存过滤结果到 filteredAlbums,重置分页
filteredAlbums = detail.albums;
currentDisplayPage = 1;
hasMoreAlbums = filteredAlbums.length > albumsPerPage;
displayAlbums(detail.albums);
updateStats(detail.albums);
initLazyLoading();
return;
}
}
} catch (e) { /* 回退 */ }
}
// 回退到全站搜索
filteredAlbums = await performServerSearchWithResult(personName);
} catch (error) {
console.error('人物筛选失败:', error);
filteredAlbums = await performServerSearchWithResult(personName);
}
// 无论回退搜索是否找到结果,都要渲染页面(移除骨架屏)
if (filteredAlbums !== null) {
currentDisplayPage = 1;
hasMoreAlbums = filteredAlbums.length > albumsPerPage;
displayAlbums(filteredAlbums);
updateStats(filteredAlbums);
initLazyLoading();
}
}
// 显示筛选状态
function showFilterStatus(text) {
// 新版筛选条
const bar = document.getElementById('filter-bar');
const pill = document.getElementById('current-filter-pill');
if (bar && pill) {
pill.textContent = text;
bar.style.display = 'flex';
}
// 兼容旧版
const status = document.getElementById('filter-status');
const label = document.getElementById('current-filter');
if (status && label) {
label.textContent = text;
status.style.display = 'flex';
}
}
// 清除筛选
function clearFilter() {
currentFilter = null;
filteredAlbums = null; // 清除过滤结果缓存
currentDisplayPage = 1;
// 新版筛选条
const bar = document.getElementById('filter-bar');
if (bar) bar.style.display = 'none';
// 兼容旧版
const status = document.getElementById('filter-status');
if (status) status.style.display = 'none';
// 清空搜索框
const searchInput = document.getElementById('search-input');
if (searchInput) {
searchInput.value = '';
updateSearchClear();
}
// 重新渲染标签和人物(清除高亮)
loadDiscoverData();
// 重新加载所有相册
loadAlbums(true);
}
// ============ 侧边栏交互 ============
// 初始化侧边栏状态
function initSidebar() {
const sidebar = document.getElementById('sidebar');
if (sidebar && sidebarCollapsed) {
sidebar.classList.add('collapsed');
}
}
// 切换侧边栏折叠
function toggleSidebar() {
const sidebar = document.getElementById('sidebar');
if (!sidebar) return;
sidebarCollapsed = !sidebarCollapsed;
sidebar.classList.toggle('collapsed', sidebarCollapsed);
localStorage.setItem('sidebarCollapsed', sidebarCollapsed);
}
// 移动端切换侧边栏
function toggleMobileSidebar() {
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('mobile-overlay');
if (!sidebar || !overlay) return;
sidebar.classList.toggle('mobile-open');
overlay.classList.toggle('active');
}
// 切换视图(首页/收藏夹)
function switchView(view) {
// 更新导航高亮
document.querySelectorAll('.nav-item').forEach(item => {
item.classList.toggle('active', item.dataset.view === view);
});
// 移动端关闭侧边栏
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('mobile-overlay');
if (sidebar && sidebar.classList.contains('mobile-open')) {
sidebar.classList.remove('mobile-open');
overlay.classList.remove('active');
}
if (view === 'home') {
backToHome();
} else if (view === 'favorites') {
showFavorites();
}
}
// 搜索框清除按钮
function updateSearchClear() {
const input = document.getElementById('search-input');
const clear = document.getElementById('search-clear');
if (input && clear) {
clear.style.display = input.value.length > 0 ? 'flex' : 'none';
}
}
// 清除搜索
function clearSearch() {
const input = document.getElementById('search-input');
if (input) {
input.value = '';
updateSearchClear();
if (!currentFilter) {
loadAlbums(true);
}
}
}
// 折叠/展开区域(兼容旧版)
function toggleSection(name) {
collapsedSections[name] = !collapsedSections[name];
const cloudId = name === 'people' ? 'people-cloud' : 'tag-cloud';
const cloud = document.getElementById(cloudId);
const btn = document.querySelector(`#${name}-section .section-toggle i`);
if (cloud) {
if (collapsedSections[name]) {
cloud.style.display = 'none';
btn.className = 'fas fa-chevron-down';
} else {
cloud.style.display = '';
btn.className = 'fas fa-chevron-up';
}
}
}
// ============ 初始化 ============
document.addEventListener('DOMContentLoaded', function() {
initSidebar();
// 搜索框输入事件
const searchInput = document.getElementById('search-input');
if (searchInput) {
searchInput.addEventListener('input', updateSearchClear);
}
// 键盘快捷键:ESC 关闭筛选/搜索
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
if (currentFilter) clearFilter();
}
});
});
// 导出函数供全局使用
window.searchAlbums = searchAlbums;
window.openRandomAlbum = openRandomAlbum;
window.showFavorites = showFavorites;
window.openAlbum = openAlbum;
window.backToHome = backToHome;
window.loadAlbums = loadAlbums;
window.removeFavorite = removeFavorite;
window.previewFavorite = previewFavorite;
window.openFavoriteImage = openFavoriteImage;
window.removeFavoriteFromPreview = removeFavoriteFromPreview;
window.filterByTag = filterByTag;
window.filterByPerson = filterByPerson;
window.clearFilter = clearFilter;
window.toggleSection = toggleSection;
window.toggleSidebar = toggleSidebar;
window.toggleMobileSidebar = toggleMobileSidebar;
window.switchView = switchView;
window.clearSearch = clearSearch;