diff --git a/assets/css/style.css b/assets/css/style.css
index e3bc870..4cf1503 100644
--- a/assets/css/style.css
+++ b/assets/css/style.css
@@ -1084,6 +1084,219 @@ body {
border: 1px solid var(--medium-gray);
}
+/* ============================================================
+ 发现区域 - 热门人物 & 热门标签
+ ============================================================ */
+
+.discover-section {
+ background: var(--white);
+ border-radius: var(--border-radius-lg);
+ box-shadow: var(--shadow-sm);
+ border: 1px solid var(--medium-gray);
+ margin-bottom: 30px;
+ overflow: hidden;
+}
+
+.section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 18px 24px;
+ background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
+ border-bottom: 1px solid var(--medium-gray);
+}
+
+.section-header h3 {
+ margin: 0;
+ font-size: 1.1rem;
+ font-weight: 700;
+ color: var(--text-dark);
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.section-header h3 i {
+ color: var(--secondary-blue);
+}
+
+.section-toggle {
+ background: none;
+ border: none;
+ color: var(--text-light);
+ cursor: pointer;
+ padding: 6px 10px;
+ border-radius: 6px;
+ transition: all 0.2s ease;
+ font-size: 0.9rem;
+}
+
+.section-toggle:hover {
+ background: rgba(59, 130, 246, 0.1);
+ color: var(--secondary-blue);
+}
+
+/* 标签云 */
+.tag-cloud {
+ padding: 20px 24px;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ justify-content: center;
+}
+
+.tag-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 6px 14px;
+ background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
+ border: 1px solid #bae6fd;
+ border-radius: 20px;
+ cursor: pointer;
+ font-weight: 600;
+ transition: all 0.25s ease;
+ user-select: none;
+}
+
+.tag-chip small {
+ color: var(--text-light);
+ font-weight: 500;
+ opacity: 0.8;
+}
+
+.tag-chip:hover {
+ background: linear-gradient(135deg, var(--secondary-blue) 0%, var(--primary-blue) 100%);
+ color: var(--white) !important;
+ border-color: var(--secondary-blue);
+ transform: translateY(-2px);
+ box-shadow: var(--shadow-md);
+}
+
+.tag-chip:hover small {
+ color: rgba(255, 255, 255, 0.85);
+}
+
+/* 热门人物 */
+.people-cloud {
+ padding: 20px 24px;
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
+ gap: 12px;
+}
+
+.person-chip {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 10px 14px;
+ background: linear-gradient(135deg, #fdf4ff 0%, #fae8ff 100%);
+ border: 1px solid #f5d0fe;
+ border-radius: 12px;
+ cursor: pointer;
+ transition: all 0.25s ease;
+}
+
+.person-chip:hover {
+ background: linear-gradient(135deg, #a855f7 0%, #9333ea 100%);
+ border-color: #9333ea;
+ transform: translateY(-2px);
+ box-shadow: var(--shadow-md);
+}
+
+.person-chip:hover .person-name,
+.person-chip:hover .person-meta {
+ color: var(--white);
+}
+
+.person-avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #a855f7 0%, #7c3aed 100%);
+ color: var(--white);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 700;
+ font-size: 1.1rem;
+ flex-shrink: 0;
+}
+
+.person-chip:hover .person-avatar {
+ background: rgba(255, 255, 255, 0.25);
+}
+
+.person-info {
+ flex: 1;
+ min-width: 0;
+}
+
+.person-name {
+ font-weight: 700;
+ color: var(--text-dark);
+ font-size: 0.95rem;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.person-meta {
+ font-size: 0.8rem;
+ color: var(--text-light);
+ margin-top: 2px;
+}
+
+/* 筛选状态条 */
+.filter-status {
+ display: none;
+ align-items: center;
+ gap: 12px;
+ padding: 14px 20px;
+ background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
+ border: 1px solid #fcd34d;
+ border-radius: var(--border-radius);
+ margin-bottom: 24px;
+ box-shadow: var(--shadow-sm);
+}
+
+.filter-label {
+ font-weight: 600;
+ color: #92400e;
+ font-size: 0.95rem;
+}
+
+.filter-tag {
+ background: var(--white);
+ padding: 4px 12px;
+ border-radius: 16px;
+ font-weight: 700;
+ color: #92400e;
+ font-size: 0.9rem;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+.filter-clear {
+ margin-left: auto;
+ padding: 6px 14px;
+ background: #92400e;
+ color: var(--white);
+ border: none;
+ border-radius: 16px;
+ font-weight: 600;
+ font-size: 0.85rem;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.filter-clear:hover {
+ background: #78350f;
+ transform: translateY(-1px);
+}
+
/* 响应式网格布局 - 区分PC和手机 */
.albums-grid {
display: grid;
diff --git a/assets/index.html b/assets/index.html
index 9dd48e5..e884a2f 100644
--- a/assets/index.html
+++ b/assets/index.html
@@ -33,6 +33,39 @@
正在加载相册数据...
+
+
+
+
+
+
+
+
+
+ 当前筛选:
+
+
+
diff --git a/assets/js/app.js b/assets/js/app.js
index 3709235..975fa60 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -10,6 +10,10 @@ let lazyLoadObserver = null;
let isAllAlbumsLoaded = false; // 标记是否已加载所有相册数据
let isInFavoritesPage = false; // 标记当前是否在收藏页
+// 标签和人物筛选
+let currentFilter = null; // { type: 'tag'|'person', value: string }
+let collapsedSections = { people: false, tags: false };
+
// API基础URL
const API_BASE = '/api';
@@ -437,6 +441,9 @@ async function loadAlbums(reset = false) {
// 预加载下一批图片
preloadNextBatchImages();
+ // 加载热门标签和人物
+ loadDiscoverData();
+
} catch (error) {
console.error('加载相册失败:', error);
showError('加载相册数据失败,请刷新页面重试');
@@ -1199,6 +1206,192 @@ function openFavoriteImage(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 tags = await tagsResp.json();
+ 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 section = document.getElementById('tags-section');
+ const cloud = document.getElementById('tag-cloud');
+ if (!section || !cloud || !tags.length) return;
+
+ 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 || '';
+ // 计算字体大小 (12px - 22px)
+ 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 `
+ ${name} (${count})
+ `;
+ }).join('');
+}
+
+// 渲染热门人物
+function renderPeopleCloud(people) {
+ const section = document.getElementById('people-section');
+ const cloud = document.getElementById('people-cloud');
+ if (!section || !cloud || !people.length) return;
+
+ 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 `
+
${name.charAt(0)}
+
+
${name}
+
${albumCount} 相册 · ${imageCount} 图
+
+
`;
+ }).join('');
+}
+
+// 按标签筛选
+async function filterByTag(tag) {
+ currentFilter = { type: 'tag', value: tag };
+ showFilterStatus(`标签: ${tag}`);
+
+ 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();
+
+ displayAlbums(results);
+ updateStats(results);
+ initLazyLoading();
+ } catch (error) {
+ console.error('标签筛选失败:', error);
+ showError('标签筛选失败');
+ }
+}
+
+// 按人物筛选
+async function filterByPerson(personName) {
+ currentFilter = { type: 'person', value: personName };
+ showFilterStatus(`人物: ${personName}`);
+
+ try {
+ showSkeleton(6);
+ const response = await fetch(`${API_BASE}/people/search?q=${encodeURIComponent(personName)}`);
+ if (!response.ok) throw new Error('搜索失败');
+ const persons = await response.json();
+
+ if (persons && persons.length > 0) {
+ // 先尝试通过人物详情获取相册
+ const personId = persons[0].id;
+ try {
+ const detailResp = await fetch(`${API_BASE}/people/${encodeURIComponent(personName)}`);
+ if (detailResp.ok) {
+ const detail = await detailResp.json();
+ if (detail && detail.albums) {
+ displayAlbums(detail.albums);
+ updateStats(detail.albums);
+ initLazyLoading();
+ return;
+ }
+ }
+ } catch (e) {
+ // 回退到标题搜索
+ }
+ }
+
+ // 回退:使用标题搜索
+ await performServerSearch(personName);
+ } catch (error) {
+ console.error('人物筛选失败:', error);
+ // 回退到标题搜索
+ await performServerSearch(personName);
+ }
+}
+
+// 显示筛选状态
+function showFilterStatus(text) {
+ 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;
+ const status = document.getElementById('filter-status');
+ if (status) status.style.display = 'none';
+
+ // 清空搜索框
+ const searchInput = document.getElementById('search-input');
+ if (searchInput) searchInput.value = '';
+
+ // 重新加载所有相册
+ 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';
+ }
+ }
+}
+
// 导出函数供全局使用
window.searchAlbums = searchAlbums;
window.openRandomAlbum = openRandomAlbum;
@@ -1209,4 +1402,8 @@ window.loadAlbums = loadAlbums;
window.removeFavorite = removeFavorite;
window.previewFavorite = previewFavorite;
window.openFavoriteImage = openFavoriteImage;
-window.removeFavoriteFromPreview = removeFavoriteFromPreview;
\ No newline at end of file
+window.removeFavoriteFromPreview = removeFavoriteFromPreview;
+window.filterByTag = filterByTag;
+window.filterByPerson = filterByPerson;
+window.clearFilter = clearFilter;
+window.toggleSection = toggleSection;
\ No newline at end of file