This commit is contained in:
2026-01-01 23:55:35 +08:00
commit 0fe55441b8
3944 changed files with 62522 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
#!/bin/bash
# 生成图片列表 JSON
cd "$(dirname "$0")/../" || exit 1
OUTPUT="images.json"
echo "[" > "$OUTPUT"
FIRST=true
# 使用 awk 替代 shell 循环以提高性能
find . -type f \( -iname "*.webp" -o -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.gif" -o -iname "*.svg" \) | sort -r | awk '
BEGIN {
print "["
first = 1
}
{
# 去掉开头的 ./
sub(/^\.\//, "", $0)
path = $0
# 分割路径
n = split(path, parts, "/")
# 仅处理符合 year/month/day/filename 结构的文件 (至少4部分)
# 这样可以自动过滤掉 cache 目录或其他不符合规范的图片
if (n >= 4) {
year = parts[1]
month = parts[2]
day = parts[3]
filename = parts[n]
# 简单的数字检查,确保是日期目录
if (year ~ /^[0-9]+$/ && month ~ /^[0-9]+$/) {
# 转义 JSON 特殊字符
gsub(/\\/, "\\\\", path)
gsub(/"/, "\\\"", path)
gsub(/\\/, "\\\\", filename)
gsub(/"/, "\\\"", filename)
if (first == 0) {
print ","
}
first = 0
print " {"
print " \"url\": \"/api/i/" path "\","
print " \"filename\": \"" filename "\","
print " \"year\": \"" year "\","
print " \"month\": \"" month "\","
print " \"day\": \"" day "\","
print " \"date\": \"" year "-" month "-" day "\""
printf " }"
}
}
}
END {
print "\n]"
}
' > "$OUTPUT"
TOTAL=$(grep -c '"url"' "$OUTPUT" || echo 0)
echo "✓ 已生成 $TOTAL 张图片的列表: $(pwd)/$OUTPUT"

View File

@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>时光相册 - Image Gallery</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<header class="header">
<a href="/" class="back-link">← 返回首页</a>
<h1>✨ 时光相册</h1>
<p>记录每一个精彩瞬间</p>
</header>
<div class="loading-container" id="loading">
<div class="loading-spinner"></div>
<div class="loading-text">正在扫描图片...</div>
<div class="loading-progress" id="loadingProgress"></div>
</div>
<div id="content" class="hidden">
<div class="stats">
<div class="stat-card">
<div class="icon">📸</div>
<div class="label">总图片数</div>
<div class="value" id="totalCount">0</div>
</div>
<div class="stat-card">
<div class="icon">📅</div>
<div class="label">年份数</div>
<div class="value" id="yearCount">0</div>
</div>
<div class="stat-card">
<div class="icon">📆</div>
<div class="label">月份数</div>
<div class="value" id="monthCount">0</div>
</div>
</div>
<div class="filter-bar">
<div class="filter-group">
<label>📅 年份:</label>
<select id="yearFilter" onchange="filterGallery()">
<option value="">全部年份</option>
</select>
</div>
<div class="filter-group">
<label>📆 月份:</label>
<select id="monthFilter" onchange="filterGallery()">
<option value="">全部月份</option>
</select>
</div>
<div class="filter-group" style="flex: 1;">
<label>🔍</label>
<input type="text" id="searchInput" placeholder="搜索文件名..." oninput="filterGallery()"
style="flex: 1;">
</div>
</div>
<div class="timeline" id="timeline"></div>
</div>
</div>
<div class="lightbox" id="lightbox">
<button class="close" onclick="closeLightbox()">&times;</button>
<button class="nav prev" onclick="navigateImage(-1)">&#10094;</button>
<button class="nav next" onclick="navigateImage(1)">&#10095;</button>
<div class="lightbox-content">
<img id="lightboxImage" src="" alt="">
</div>
<div class="lightbox-info">
<div class="filename" id="lightboxFilename"></div>
<div class="meta" id="lightboxMeta"></div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>

View File

@@ -0,0 +1,343 @@
let allImages = [];
let visibleImages = [];
let currentIndex = 0;
let imageObserver = null;
const expandedYears = new Set();
const monthNames = {
"01": "一月",
"02": "二月",
"03": "三月",
"04": "四月",
"05": "五月",
"06": "六月",
"07": "七月",
"08": "八月",
"09": "九月",
10: "十月",
11: "十一月",
12: "十二月",
};
// 初始化懒加载观察器
function initLazyLoading() {
if (imageObserver) return;
imageObserver = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target;
const src = img.dataset.src;
if (src) {
img.src = src;
img.classList.add("loaded");
imageObserver.unobserve(img);
}
}
});
},
{
rootMargin: "50px 0px",
threshold: 0.1,
},
);
}
// 观察图片元素
function observeImage(img) {
if (imageObserver) {
imageObserver.observe(img);
}
}
async function discoverImages() {
const loading = document.getElementById("loading");
const progressText = document.getElementById("loadingProgress");
const content = document.getElementById("content");
try {
progressText.textContent = "正在加载图片列表...";
const response = await fetch("/api/i/images.json");
if (!response.ok) throw new Error("Failed to load images.json");
allImages = await response.json();
// Sort by date descending
allImages.sort((a, b) => b.date.localeCompare(a.date));
loading.classList.add("hidden");
content.classList.remove("hidden");
document.getElementById("totalCount").textContent =
allImages.length.toLocaleString();
const _years = [...new Set(allImages.map((img) => img.year))];
document.getElementById("yearCount").textContent = _years.length;
const months = [
...new Set(allImages.map((img) => `${img.year}-${img.month}`)),
];
document.getElementById("monthCount").textContent = months.length;
populateFilters();
// 初始化懒加载
initLazyLoading();
// 默认只展开最近的年份
const years = [...new Set(allImages.map((img) => img.year))]
.sort()
.reverse();
if (years.length > 0) {
expandedYears.add(years[0]); // 展开最新年份
}
renderGallery();
} catch (error) {
console.error("Error loading gallery:", error);
progressText.textContent = "加载失败,请检查网络或稍后重试";
progressText.style.color = "#ef4444";
}
}
function populateFilters() {
const years = [...new Set(allImages.map((img) => img.year))].sort().reverse();
const yearSelect = document.getElementById("yearFilter");
years.forEach((year) => {
const option = document.createElement("option");
option.value = year;
option.textContent = `${year}`;
yearSelect.appendChild(option);
});
updateMonthFilter();
document
.getElementById("yearFilter")
.addEventListener("change", updateMonthFilter);
}
function updateMonthFilter() {
const year = document.getElementById("yearFilter").value;
const monthSelect = document.getElementById("monthFilter");
monthSelect.innerHTML = '<option value="">全部月份</option>';
if (year) {
const months = [
...new Set(
allImages.filter((img) => img.year === year).map((img) => img.month),
),
].sort();
months.forEach((month) => {
const option = document.createElement("option");
option.value = month;
option.textContent = `${Number.parseInt(month)}`;
monthSelect.appendChild(option);
});
}
filterGallery();
}
function filterGallery() {
const year = document.getElementById("yearFilter").value;
const month = document.getElementById("monthFilter").value;
const search = document.getElementById("searchInput").value.toLowerCase();
visibleImages = allImages.filter((img) => {
if (year && img.year !== year) return false;
if (month && img.month !== month) return false;
if (search && !img.filename.toLowerCase().includes(search)) return false;
return true;
});
renderGallery();
}
function renderGallery() {
const timeline = document.getElementById("timeline");
timeline.innerHTML = "";
if (visibleImages.length === 0) {
timeline.innerHTML =
'<div style="text-align: center; padding: 4rem; color: var(--text-gray);">没有找到匹配的图片</div>';
return;
}
const byYear = {};
visibleImages.forEach((img) => {
if (!byYear[img.year]) byYear[img.year] = {};
if (!byYear[img.year][img.month]) byYear[img.year][img.month] = [];
byYear[img.year][img.month].push(img);
});
Object.keys(byYear)
.sort()
.reverse()
.forEach((year) => {
const yearEl = document.createElement("div");
yearEl.className = "timeline-year";
yearEl.dataset.year = year;
const yearCount = Object.values(byYear[year]).reduce(
(sum, imgs) => sum + imgs.length,
0,
);
const isExpanded = expandedYears.has(year);
const toggleIcon = isExpanded ? "▼" : "▶";
yearEl.innerHTML = `
<div class="timeline-year-marker">${year.slice(-2)}</div>
<div class="timeline-year-header">
<h2>${year}年</h2>
<span class="count">${yearCount} 张照片</span>
<span class="toggle-icon">${toggleIcon}</span>
</div>
`;
const yearHeader = yearEl.querySelector(".timeline-year-header");
yearHeader?.addEventListener("click", () => toggleYear(year));
if (isExpanded) {
Object.keys(byYear[year])
.sort()
.reverse()
.forEach((month) => {
const monthImages = byYear[year][month];
const monthEl = document.createElement("div");
monthEl.className = "month-group";
monthEl.dataset.month = month;
monthEl.innerHTML = `
<div class="month-header">
<span class="icon">📆</span>
<span>${monthNames[month] || `${month}`}</span>
<span style="color: var(--text-gray); font-size: 0.875rem;">(${monthImages.length} 张)</span>
</div>
<div class="gallery-masonry"></div>
`;
const masonry = monthEl.querySelector(".gallery-masonry");
monthImages.forEach((img) => {
const card = document.createElement("div");
card.className = "image-card";
card.dataset.filename = img.filename;
card.dataset.date = img.date;
card.onclick = () => openLightbox(img);
// 创建包装器
const wrapper = document.createElement("div");
wrapper.className = "wrapper";
// 创建占位符
const placeholder = document.createElement("div");
placeholder.className = "image-placeholder";
placeholder.innerHTML = "📷";
// 创建图片元素(懒加载)
const imgElement = document.createElement("img");
imgElement.dataset.src = img.url;
imgElement.alt = img.filename;
imgElement.className = "lazy-image";
imgElement.onload = () => {
placeholder.style.display = "none";
imgElement.classList.add("loaded");
};
imgElement.onerror = () => {
placeholder.innerHTML = "❌";
};
observeImage(imgElement);
// 创建覆盖层
const overlay = document.createElement("div");
overlay.className = "overlay";
overlay.innerHTML = '<span class="icon">🔍</span>';
// 组装包装器
wrapper.appendChild(placeholder);
wrapper.appendChild(imgElement);
wrapper.appendChild(overlay);
// 创建信息区域
const info = document.createElement("div");
info.className = "info";
info.innerHTML = `
<div class="filename" title="${img.filename}">${img.filename}</div>
<div class="date">${img.date}</div>
`;
// 组装卡片
card.appendChild(wrapper);
card.appendChild(info);
masonry.appendChild(card);
});
yearEl.appendChild(monthEl);
});
}
timeline.appendChild(yearEl);
});
}
// 切换年份展开/折叠
function toggleYear(year) {
if (expandedYears.has(year)) {
expandedYears.delete(year);
} else {
expandedYears.add(year);
}
renderGallery();
}
function openLightbox(img) {
const lightbox = document.getElementById("lightbox");
document.getElementById("lightboxImage").src = img.url;
document.getElementById("lightboxFilename").textContent = img.filename;
document.getElementById("lightboxMeta").textContent = img.date;
currentIndex = visibleImages.indexOf(img);
lightbox.classList.add("active");
}
function closeLightbox() {
document.getElementById("lightbox").classList.remove("active");
}
function navigateImage(direction) {
currentIndex += direction;
if (currentIndex < 0) currentIndex = visibleImages.length - 1;
if (currentIndex >= visibleImages.length) currentIndex = 0;
const img = visibleImages[currentIndex];
if (img) {
openLightbox(img);
}
}
document.addEventListener("keydown", (e) => {
if (document.getElementById("lightbox").classList.contains("active")) {
if (e.key === "Escape") closeLightbox();
if (e.key === "ArrowLeft") navigateImage(-1);
if (e.key === "ArrowRight") navigateImage(1);
}
});
document.getElementById("lightbox").addEventListener("click", (e) => {
if (e.target.id === "lightbox") closeLightbox();
});
// 兼容内联事件处理器index.html 里的 onchange/onclick
window.filterGallery = filterGallery;
window.closeLightbox = closeLightbox;
window.navigateImage = navigateImage;
window.toggleYear = toggleYear;
discoverImages();

View File

@@ -0,0 +1,687 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary: #6366f1;
--primary-light: #818cf8;
--bg-dark: #0f172a;
--bg-card: #1e293b;
--bg-card-hover: #334155;
--text-white: #f1f5f9;
--text-gray: #94a3b8;
--border: #334155;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: var(--bg-dark);
color: var(--text-white);
line-height: 1.6;
min-height: 100vh;
}
.container {
max-width: 1600px;
margin: 0 auto;
padding: 2rem;
}
.header {
text-align: center;
margin-bottom: 3rem;
animation: fadeInDown 0.8s ease;
position: relative;
}
.back-link {
position: absolute;
left: 0;
top: 0;
color: var(--text-gray);
text-decoration: none;
font-size: 0.9rem;
transition: color 0.3s ease;
display: flex;
align-items: center;
gap: 0.5rem;
}
.back-link:hover {
color: var(--primary);
}
@media (max-width: 768px) {
.back-link {
position: static;
display: block;
margin-bottom: 1rem;
}
}
.header h1 {
font-size: 3rem;
font-weight: 800;
background: linear-gradient(135deg, #6366f1 0%, #a855f7 50%, #ec4899 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
letter-spacing: -0.02em;
}
.header p {
color: var(--text-gray);
font-size: 1.1rem;
}
.loading-container {
text-align: center;
padding: 4rem 2rem;
}
.loading-spinner {
width: 60px;
height: 60px;
border: 4px solid var(--bg-card);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 1rem;
}
.loading-text {
color: var(--text-gray);
font-size: 1.1rem;
}
.loading-progress {
margin-top: 1rem;
color: var(--primary);
font-weight: 600;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
animation: fadeInUp 0.8s ease;
}
.stat-card {
background: linear-gradient(135deg, var(--bg-card) 0%, var(--bg-card-hover) 100%);
padding: 1.5rem;
border-radius: 16px;
border: 1px solid var(--border);
text-align: center;
transition: all 0.3s ease;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 20px 40px rgba(99, 102, 241, 0.2);
border-color: var(--primary);
}
.stat-card .icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.stat-card .label {
color: var(--text-gray);
font-size: 0.875rem;
margin-bottom: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.stat-card .value {
font-size: 2.5rem;
font-weight: 700;
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.filter-bar {
background: var(--bg-card);
padding: 1rem 1.5rem;
border-radius: 16px;
border: 1px solid var(--border);
margin-bottom: 2rem;
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
animation: fadeInUp 0.8s ease 0.1s both;
}
.filter-group {
display: flex;
align-items: center;
gap: 0.75rem;
}
.filter-group label {
font-weight: 600;
color: var(--text-white);
font-size: 0.875rem;
}
.filter-group select,
.filter-group input {
padding: 0.625rem 1rem;
background: var(--bg-dark);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-white);
font-size: 0.875rem;
transition: all 0.3s ease;
min-width: 120px;
}
.filter-group select:focus,
.filter-group input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
}
.filter-group select option {
background: var(--bg-card);
color: var(--text-white);
}
.timeline {
position: relative;
padding-left: 2rem;
animation: fadeInUp 0.8s ease 0.2s both;
}
.timeline::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: linear-gradient(180deg, var(--primary) 0%, transparent 100%);
border-radius: 3px;
}
.timeline-year {
margin-bottom: 3rem;
position: relative;
}
.timeline-year-marker {
position: absolute;
left: -2.75rem;
top: 0;
width: 3.5rem;
height: 3.5rem;
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
font-weight: 700;
box-shadow: 0 0 20px rgba(99, 102, 241, 0.4);
}
.timeline-year-header {
background: var(--bg-card);
padding: 1rem 1.5rem;
border-radius: 12px;
border: 1px solid var(--border);
margin-bottom: 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
}
.timeline-year-header h2 {
font-size: 1.5rem;
font-weight: 700;
}
.timeline-year-header .count {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
color: white;
padding: 0.375rem 1rem;
border-radius: 20px;
font-size: 0.875rem;
font-weight: 600;
}
.month-group {
margin-bottom: 2rem;
padding-left: 1.5rem;
}
.month-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
color: var(--text-gray);
font-size: 1.125rem;
font-weight: 600;
}
.month-header .icon {
font-size: 1.5rem;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 1rem;
}
.image-card {
background: var(--bg-card);
border-radius: 12px;
overflow: hidden;
border: 1px solid var(--border);
cursor: pointer;
transition: all 0.3s ease;
animation: fadeIn 0.5s ease;
}
.image-card:hover {
transform: translateY(-8px) scale(1.02);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
border-color: var(--primary);
z-index: 1;
}
.image-card .wrapper {
position: relative;
aspect-ratio: 16 / 10;
overflow: hidden;
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
}
.image-card img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease;
}
.image-card:hover img {
transform: scale(1.1);
}
.image-card .overlay {
position: absolute;
inset: 0;
background: linear-gradient(180deg, transparent 0%, rgba(15, 23, 42, 0.8) 100%);
opacity: 0;
transition: opacity 0.3s ease;
display: flex;
align-items: flex-end;
padding: 1rem;
}
.image-card:hover .overlay {
opacity: 1;
}
.image-card .overlay .icon {
color: white;
font-size: 1.5rem;
}
.image-card .info {
padding: 0.75rem;
}
.image-card .filename {
font-size: 0.75rem;
color: var(--text-gray);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.image-card .date {
font-size: 0.625rem;
color: var(--text-gray);
opacity: 0.7;
margin-top: 0.25rem;
}
.lightbox {
display: none;
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.98);
z-index: 1000;
animation: fadeIn 0.3s ease;
backdrop-filter: blur(10px);
}
.lightbox.active {
display: flex;
align-items: center;
justify-content: center;
}
.lightbox-content {
position: relative;
max-width: 90vw;
max-height: 90vh;
}
.lightbox img {
max-width: 100%;
max-height: 85vh;
object-fit: contain;
border-radius: 12px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
}
.lightbox .close {
position: fixed;
top: 2rem;
right: 2rem;
background: rgba(255, 255, 255, 0.1);
color: white;
border: none;
width: 56px;
height: 56px;
border-radius: 50%;
font-size: 2rem;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.lightbox .close:hover {
background: rgba(255, 255, 255, 0.2);
transform: rotate(90deg) scale(1.1);
}
.lightbox .nav {
position: fixed;
top: 50%;
transform: translateY(-50%);
background: rgba(255, 255, 255, 0.1);
color: white;
border: none;
width: 64px;
height: 64px;
border-radius: 50%;
font-size: 2rem;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.lightbox .nav:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateY(-50%) scale(1.1);
}
.lightbox .nav.prev {
left: 2rem;
}
.lightbox .nav.next {
right: 2rem;
}
.lightbox-info {
position: fixed;
bottom: 2rem;
left: 50%;
transform: translateX(-50%);
background: rgba(30, 41, 59, 0.9);
padding: 1rem 2rem;
border-radius: 12px;
border: 1px solid var(--border);
text-align: center;
backdrop-filter: blur(10px);
max-width: 90%;
}
.lightbox-info .filename {
font-weight: 600;
margin-bottom: 0.25rem;
}
.lightbox-info .meta {
font-size: 0.875rem;
color: var(--text-gray);
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes spin {
to { transform: rotate(360deg); }
}
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-track {
background: var(--bg-dark);
}
::-webkit-scrollbar-thumb {
background: var(--bg-card-hover);
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--primary);
}
.hidden {
display: none !important;
}
@media (max-width: 768px) {
.container {
padding: 1rem;
}
.header h1 {
font-size: 2rem;
}
.stats {
grid-template-columns: 1fr;
}
.gallery-grid {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
}
.timeline {
padding-left: 1.5rem;
}
.timeline::before {
width: 2px;
}
.timeline-year-marker {
width: 2.5rem;
height: 2.5rem;
left: -2rem;
font-size: 0.75rem;
}
.lightbox .nav {
width: 44px;
height: 44px;
font-size: 1.25rem;
}
.lightbox .nav.prev {
left: 0.5rem;
}
.lightbox .nav.next {
right: 0.5rem;
}
.lightbox .close {
width: 40px;
height: 40px;
font-size: 1.25rem;
top: 1rem;
right: 1rem;
}
.lightbox-info {
bottom: 1rem;
padding: 0.75rem 1rem;
font-size: 0.875rem;
}
.filter-bar {
flex-direction: column;
align-items: stretch;
}
.filter-group {
flex-direction: column;
align-items: stretch;
}
.filter-group select,
.filter-group input {
width: 100%;
}
}
/* 瀑布流布局 */
.gallery-masonry {
column-count: 3;
column-gap: 1rem;
margin-bottom: 1rem;
}
@media (max-width: 1024px) {
.gallery-masonry {
column-count: 2;
}
}
@media (max-width: 640px) {
.gallery-masonry {
column-count: 1;
}
}
.gallery-masonry .image-card {
break-inside: avoid;
margin-bottom: 1rem;
display: inline-block;
width: 100%;
}
/* 年份折叠样式 */
.timeline-year-header {
cursor: pointer;
transition: all 0.3s ease;
}
.timeline-year-header:hover {
background: var(--bg-card-hover);
}
.timeline-year-header .toggle-icon {
font-size: 1.25rem;
color: var(--primary);
transition: transform 0.3s ease;
}
.timeline-year-header.collapsed .toggle-icon {
transform: rotate(-90deg);
}
/* 懒加载样式 */
.lazy-image {
opacity: 0;
transition: opacity 0.3s ease;
}
.lazy-image.loaded {
opacity: 1;
}
.image-placeholder {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
color: var(--text-gray);
background: var(--bg-card);
z-index: 1;
}
.lazy-image.loaded + .image-placeholder {
display: none;
}
/* 折叠动画 */
.month-group {
animation: slideDown 0.3s ease;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}