gallery
This commit is contained in:
63
public/api/i/gallery-meow/generate.sh
Executable file
63
public/api/i/gallery-meow/generate.sh
Executable 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"
|
||||
84
public/api/i/gallery-meow/index.html
Executable file
84
public/api/i/gallery-meow/index.html
Executable 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()">×</button>
|
||||
<button class="nav prev" onclick="navigateImage(-1)">❮</button>
|
||||
<button class="nav next" onclick="navigateImage(1)">❯</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>
|
||||
343
public/api/i/gallery-meow/script.js
Executable file
343
public/api/i/gallery-meow/script.js
Executable 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();
|
||||
687
public/api/i/gallery-meow/style.css
Executable file
687
public/api/i/gallery-meow/style.css
Executable 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user