青岛演示第一版

This commit is contained in:
Sin Lee 2025-12-03 22:36:38 +08:00
parent 0e67a2f254
commit 080a4ac07a
20 changed files with 894 additions and 567 deletions

141
GALLERY-README.md Normal file
View File

@ -0,0 +1,141 @@
# Gallery 配置驱动系统
## ✨ 已完成的改造
### 新增文件结构
```
gallery/
├── src/
│ ├── data/
│ │ └── gallery-config.js ← 核心配置文件(所有图片路径在这里)
│ ├── components/
│ │ └── sections/
│ │ ├── HeroSection.astro ← 首屏大图组件
│ │ ├── FullBleedSection.astro ← 全屏单图组件
│ │ ├── QuadGridSection.astro ← 四宫格组件
│ │ ├── DualSection.astro ← 双栏组件
│ │ └── GridSection.astro ← 网格布局组件
│ └── pages/
│ └── index.astro ← 主页(动态渲染)
└── public/
└── media/ ← 所有图片资源存放处
```
## 🚀 快速开始
### 1. 复制图片到项目
```bash
cd /Users/sinlee/Documents/git-repos/gallery
chmod +x copy-photos.sh
./copy-photos.sh
```
### 2. 启动开发服务器
```bash
pnpm dev
```
然后访问 `http://localhost:4321`
## 📸 当前图片布局
根据你的图片命名,我已经这样组织了内容:
1. **首屏** - 四人合照 (`0123_1.JPG`)
2. **全屏单图** - 另一张合照 (`0123_2.JPG`)
3. **四宫格** - 0号人物的四张照片 (`0_1` 到 `0_4`)
4. **双栏** - 23号合照 + 1号单人
5. **网格** - 2号和3号的照片
6. **全屏结尾** - 0号照片
## 🎨 如何修改布局
### 只需编辑一个文件:`src/data/gallery-config.js`
#### 示例 1修改首屏图片
```javascript
hero: {
image: '/media/0123_2.JPG', // 改成其他图片
title: '新的标题',
subtitle: '新的副标题'
}
```
#### 示例 2添加新的四宫格
`sections` 数组中添加:
```javascript
{
type: 'quad-grid',
title: '新的四宫格',
images: [
'/media/1_1.JPG',
'/media/2_1.JPG',
'/media/2_2.JPG',
'/media/3_1.JPG'
]
}
```
#### 示例 3添加双栏布局
```javascript
{
type: 'dual',
cards: [
{
image: '/media/0_1.JPG',
title: '左边标题',
description: '左边描述'
},
{
image: '/media/1_1.JPG',
title: '右边标题',
description: '右边描述'
}
]
}
```
## 📦 支持的布局类型
- `hero` - 首屏大图
- `full-bleed` - 全屏单图(支持 left/right/center 对齐)
- `quad-grid` - 四宫格2x2
- `dual` - 双栏布局
- `grid` - 灵活网格(支持 large 和 normal 尺寸)
## 🔄 部署到 VPS
修改完成后:
```bash
git add .
git commit -m "更新 gallery 配置"
git push
```
自动部署会触发,几秒后就能在 `https://show.leexxx.com` 看到更新!
## 💡 提示
- **图片路径**:相对于 `public/` 目录,所以 `/media/xxx.jpg` 对应 `public/media/xxx.jpg`
- **图片格式**:支持 JPG, PNG, WebP
- **添加图片**:只需把新图片放到 `public/media/` 然后在配置文件中引用即可
- **调整顺序**:在 `sections` 数组中调整顺序即可改变页面布局顺序
## 🎯 图片命名规则(你的照片)
- `0_X.JPG` - 0号人物的照片
- `1_X.JPG` - 1号人物的照片
- `2_X.JPG` - 2号人物的照片
- `3_X.JPG` - 3号人物的照片
- `0123_X.JPG` - 四人合照
- `23_X.JPG` - 2号和3号的合照
你可以根据这些照片自由组织任何布局!

36
copy-photos.sh Executable file
View File

@ -0,0 +1,36 @@
#!/bin/bash
# 复制图片到 gallery 项目
echo "开始复制图片..."
# 源目录
SOURCE_DIR="/Users/sinlee/photo_test"
# 目标目录
TARGET_DIR="/Users/sinlee/Documents/git-repos/gallery/public/media"
# 检查源目录是否存在
if [ ! -d "$SOURCE_DIR" ]; then
echo "错误: 源目录不存在: $SOURCE_DIR"
exit 1
fi
# 检查目标目录是否存在,不存在则创建
if [ ! -d "$TARGET_DIR" ]; then
mkdir -p "$TARGET_DIR"
echo "创建目标目录: $TARGET_DIR"
fi
# 复制所有 JPG 文件
cp "$SOURCE_DIR"/*.JPG "$TARGET_DIR"/
if [ $? -eq 0 ]; then
echo "✅ 图片复制成功!"
echo "已复制的文件:"
ls -lh "$TARGET_DIR"/*.JPG
else
echo "❌ 复制失败"
exit 1
fi
echo ""
echo "下一步: 在项目目录执行 pnpm dev 启动开发服务器"

BIN
public/media/0123_1.JPG Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 MiB

BIN
public/media/0123_2.JPG Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 MiB

BIN
public/media/0_1.JPG Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 MiB

BIN
public/media/0_2.JPG Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 MiB

BIN
public/media/0_3.JPG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 MiB

BIN
public/media/0_4.JPG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 MiB

BIN
public/media/1_1.JPG Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 MiB

BIN
public/media/23_1.JPG Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 MiB

BIN
public/media/2_1.JPG Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 MiB

BIN
public/media/2_2.JPG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 MiB

BIN
public/media/3_1.JPG Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 MiB

View File

@ -0,0 +1,113 @@
---
interface Card {
image: string;
title: string;
description: string;
}
interface Props {
cards: Card[];
}
const { cards } = Astro.props;
---
<section class="dual-section fade-in-section">
<div class="dual-container">
{cards.map((card, index) => (
<div class="dual-card" style={`--delay: ${index * 0.2}s`}>
<div class="dual-image-wrapper">
<img src={card.image} alt={card.title} loading="lazy" />
</div>
<div class="dual-text">
<h3>{card.title}</h3>
<p>{card.description}</p>
</div>
</div>
))}
</div>
</section>
<style>
.dual-section {
padding: 6rem 2rem;
background: linear-gradient(to bottom, #f8f9fa 0%, #ffffff 100%);
opacity: 0;
transform: translateY(50px);
transition: opacity 0.8s ease, transform 0.8s ease;
}
.dual-section.visible {
opacity: 1;
transform: translateY(0);
}
.dual-container {
max-width: 1400px;
margin: 0 auto;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 3rem;
}
.dual-card {
opacity: 0;
transform: translateY(30px);
animation: fadeInUp 0.6s ease forwards;
animation-delay: var(--delay);
}
.dual-image-wrapper {
aspect-ratio: 4/5;
overflow: hidden;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
margin-bottom: 1.5rem;
}
.dual-image-wrapper img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease;
}
.dual-card:hover .dual-image-wrapper img {
transform: scale(1.05);
}
.dual-text {
padding: 0 1rem;
}
.dual-text h3 {
font-size: 1.8rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: #1a1a1a;
}
.dual-text p {
font-size: 1.1rem;
color: #666;
line-height: 1.6;
}
@keyframes fadeInUp {
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 968px) {
.dual-container {
grid-template-columns: 1fr;
gap: 2rem;
}
.dual-section {
padding: 4rem 1.5rem;
}
}
</style>

View File

@ -0,0 +1,109 @@
---
interface Props {
image: string;
title: string;
description: string;
align: 'left' | 'right' | 'center';
}
const { image, title, description, align } = Astro.props;
---
<section class="full-bleed-section fade-in-section" data-align={align}>
<div class="full-bleed-image-wrapper">
<img src={image} alt={title} loading="lazy" />
</div>
<div class="full-bleed-content">
<h2>{title}</h2>
<p>{description}</p>
</div>
</section>
<style>
.full-bleed-section {
position: relative;
height: 100vh;
overflow: hidden;
opacity: 0;
transform: translateY(50px);
transition: opacity 0.8s ease, transform 0.8s ease;
}
.full-bleed-section.visible {
opacity: 1;
transform: translateY(0);
}
.full-bleed-image-wrapper {
position: absolute;
inset: 0;
}
.full-bleed-image-wrapper img {
width: 100%;
height: 100%;
object-fit: cover;
}
.full-bleed-image-wrapper::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(
to bottom,
rgba(0, 0, 0, 0.2) 0%,
rgba(0, 0, 0, 0.6) 100%
);
}
.full-bleed-content {
position: relative;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
padding: 4rem;
color: white;
z-index: 1;
}
.full-bleed-section[data-align="left"] .full-bleed-content {
align-items: flex-start;
text-align: left;
}
.full-bleed-section[data-align="right"] .full-bleed-content {
align-items: flex-end;
text-align: right;
}
.full-bleed-section[data-align="center"] .full-bleed-content {
align-items: center;
text-align: center;
}
.full-bleed-content h2 {
font-size: clamp(2.5rem, 6vw, 4rem);
font-weight: 700;
margin-bottom: 1rem;
max-width: 600px;
}
.full-bleed-content p {
font-size: clamp(1.1rem, 2vw, 1.5rem);
font-weight: 300;
max-width: 500px;
line-height: 1.6;
}
@media (max-width: 768px) {
.full-bleed-content {
padding: 2rem;
}
.full-bleed-section[data-align="right"] .full-bleed-content {
align-items: flex-start;
text-align: left;
}
}
</style>

View File

@ -0,0 +1,112 @@
---
interface GridItem {
image: string;
size: 'large' | 'normal';
}
interface Props {
items: GridItem[];
}
const { items } = Astro.props;
---
<section class="grid-section fade-in-section">
<div class="grid-container">
{items.map((item, index) => (
<div
class={`grid-item ${item.size === 'large' ? 'span-large' : ''}`}
style={`--delay: ${index * 0.1}s`}
>
<img src={item.image} alt={`Gallery image ${index + 1}`} loading="lazy" />
</div>
))}
</div>
</section>
<style>
.grid-section {
padding: 6rem 2rem;
opacity: 0;
transform: translateY(50px);
transition: opacity 0.8s ease, transform 0.8s ease;
}
.grid-section.visible {
opacity: 1;
transform: translateY(0);
}
.grid-container {
max-width: 1400px;
margin: 0 auto;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
grid-auto-flow: dense;
}
.grid-item {
overflow: hidden;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
opacity: 0;
transform: scale(0.9);
animation: scaleIn 0.6s ease forwards;
animation-delay: var(--delay);
}
.grid-item.span-large {
grid-column: span 2;
grid-row: span 2;
}
.grid-item img {
width: 100%;
height: 100%;
object-fit: cover;
aspect-ratio: 1;
transition: transform 0.5s ease;
}
.grid-item.span-large img {
aspect-ratio: 2/1;
}
.grid-item:hover img {
transform: scale(1.05);
}
@keyframes scaleIn {
to {
opacity: 1;
transform: scale(1);
}
}
@media (max-width: 968px) {
.grid-container {
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.grid-item.span-large {
grid-column: span 2;
grid-row: span 1;
}
.grid-section {
padding: 4rem 1.5rem;
}
}
@media (max-width: 640px) {
.grid-container {
grid-template-columns: 1fr;
}
.grid-item.span-large {
grid-column: span 1;
}
}
</style>

View File

@ -0,0 +1,141 @@
---
interface Props {
image: string;
title: string;
subtitle: string;
}
const { image, title, subtitle } = Astro.props;
---
<section class="hero-section">
<div class="hero-image-wrapper">
<img src={image} alt={title} class="hero-image" />
<div class="hero-overlay"></div>
</div>
<div class="hero-content">
<h1 class="hero-title">{title}</h1>
<p class="hero-subtitle">{subtitle}</p>
<div class="scroll-indicator">
<span>向下探索</span>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M12 5v14M19 12l-7 7-7-7"></path>
</svg>
</div>
</div>
</section>
<style>
.hero-section {
position: relative;
height: 100vh;
overflow: hidden;
}
.hero-image-wrapper {
position: absolute;
inset: 0;
}
.hero-image {
width: 100%;
height: 100%;
object-fit: cover;
transform: scale(1.1);
}
.hero-overlay {
position: absolute;
inset: 0;
background: linear-gradient(
to bottom,
rgba(0, 0, 0, 0.3) 0%,
rgba(0, 0, 0, 0.5) 100%
);
}
.hero-content {
position: relative;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: white;
text-align: center;
padding: 2rem;
z-index: 1;
}
.hero-title {
font-size: clamp(3rem, 8vw, 6rem);
font-weight: 700;
letter-spacing: 0.02em;
margin-bottom: 1rem;
animation: fadeInUp 1s ease 0.3s both;
}
.hero-subtitle {
font-size: clamp(1.2rem, 2.5vw, 1.8rem);
font-weight: 300;
letter-spacing: 0.05em;
opacity: 0.95;
animation: fadeInUp 1s ease 0.6s both;
}
.scroll-indicator {
position: absolute;
bottom: 3rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
cursor: pointer;
animation: fadeInUp 1s ease 1s both, bounce 2s ease-in-out 2s infinite;
}
.scroll-indicator span {
font-size: 0.9rem;
letter-spacing: 0.1em;
text-transform: uppercase;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
</style>
<script>
// 视差效果
window.addEventListener('scroll', () => {
const scrolled = window.pageYOffset;
const hero = document.querySelector('.hero-image') as HTMLElement;
if (hero && scrolled < window.innerHeight) {
hero.style.transform = `scale(1.1) translateY(${scrolled * 0.5}px)`;
}
});
// 滚动指示器点击
document.querySelector('.scroll-indicator')?.addEventListener('click', () => {
window.scrollTo({
top: window.innerHeight,
behavior: 'smooth'
});
});
</script>

View File

@ -0,0 +1,89 @@
---
interface Props {
title?: string;
images: string[];
}
const { title, images } = Astro.props;
---
<section class="quad-grid-section fade-in-section">
{title && <h2 class="section-title">{title}</h2>}
<div class="quad-grid">
{images.map((img, index) => (
<div class="quad-item" style={`--delay: ${index * 0.1}s`}>
<img src={img} alt={`Image ${index + 1}`} loading="lazy" />
</div>
))}
</div>
</section>
<style>
.quad-grid-section {
padding: 6rem 2rem;
max-width: 1400px;
margin: 0 auto;
opacity: 0;
transform: translateY(50px);
transition: opacity 0.8s ease, transform 0.8s ease;
}
.quad-grid-section.visible {
opacity: 1;
transform: translateY(0);
}
.section-title {
text-align: center;
font-size: clamp(2rem, 5vw, 3rem);
font-weight: 700;
margin-bottom: 3rem;
color: #1a1a1a;
}
.quad-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
}
.quad-item {
aspect-ratio: 1;
overflow: hidden;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
opacity: 0;
transform: translateY(30px);
animation: fadeInUp 0.6s ease forwards;
animation-delay: var(--delay);
}
.quad-item img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease;
}
.quad-item:hover img {
transform: scale(1.08);
}
@keyframes fadeInUp {
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 768px) {
.quad-grid-section {
padding: 4rem 1.5rem;
}
.quad-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
}
</style>

View File

@ -0,0 +1,79 @@
// 画廊配置 - 所有布局和资源的单一数据源
export const galleryConfig = {
// 首屏大图 - 使用四人合照
hero: {
type: 'hero',
image: '/media/0123_1.JPG',
title: '时光印记',
subtitle: '定格美好瞬间,记录青春故事'
},
// 内容区块 - 按顺序渲染
sections: [
// 1. 全屏单图 - 另一张合照
{
type: 'full-bleed',
align: 'left',
image: '/media/0123_2.JPG',
title: '相聚时刻',
description: '珍贵的回忆,永恒的友谊'
},
// 2. 四宫格 - 0号人物的四张照片
{
type: 'quad-grid',
title: '个人时光',
images: [
'/media/0_1.JPG',
'/media/0_2.JPG',
'/media/0_3.JPG',
'/media/0_4.JPG'
]
},
// 3. 左右双栏 - 2&3号合照 和 1号单人
{
type: 'dual',
cards: [
{
image: '/media/23_1.JPG',
title: '双人组合',
description: '默契的搭档'
},
{
image: '/media/1_1.JPG',
title: '独特视角',
description: '一个人的精彩'
}
]
},
// 4. 网格布局 - 混合展示
{
type: 'grid',
items: [
{
image: '/media/2_1.JPG',
size: 'large'
},
{
image: '/media/2_2.JPG',
size: 'normal'
},
{
image: '/media/3_1.JPG',
size: 'normal'
}
]
},
// 5. 全屏单图 - 收尾
{
type: 'full-bleed',
align: 'right',
image: '/media/0_1.JPG',
title: '青春不散场',
description: '愿时光不老,我们不散'
}
]
};

View File

@ -1,281 +1,84 @@
--- ---
import Layout from '../layouts/Layout.astro'; import { galleryConfig } from '../data/gallery-config.js';
// Hero 大画幅展示图片 // 导入所有组件
const heroImage = { import HeroSection from '../components/sections/HeroSection.astro';
src: 'https://images.unsplash.com/photo-1682687220742-aba13b6e50ba?w=2400&h=1600&fit=crop', import FullBleedSection from '../components/sections/FullBleedSection.astro';
title: '视界之外', import DualSection from '../components/sections/DualSection.astro';
subtitle: '每一次快门,都是时间的诗', import GridSection from '../components/sections/GridSection.astro';
import QuadGridSection from '../components/sections/QuadGridSection.astro';
// 组件映射表
const componentMap = {
'hero': HeroSection,
'full-bleed': FullBleedSection,
'dual': DualSection,
'grid': GridSection,
'quad-grid': QuadGridSection
}; };
// 展示作品集合 - 支持不同布局类型
const sections = [
{
type: 'full-bleed', // 全出血大图
image: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=2000&h=1200&fit=crop',
title: '山川湖海',
description: '在自然中寻找内心的宁静',
align: 'left'
},
{
type: 'dual', // 双图并排
images: [
{
src: 'https://images.unsplash.com/photo-1514565131-fce0801e5785?w=1000&h=1200&fit=crop',
title: '都市脉搏',
description: '霓虹闪烁的夜晚'
},
{
src: 'https://images.unsplash.com/photo-1511988617509-a57c8a288659?w=1000&h=1200&fit=crop',
title: '人间烟火',
description: '最真实的生活瞬间'
}
]
},
{
type: 'stacked', // 层叠卡片
images: [
{
src: 'https://images.unsplash.com/photo-1486718448742-163732cd1544?w=1200&h=800&fit=crop',
title: '几何诗篇',
offset: { x: 0, y: 0, rotate: -2 }
},
{
src: 'https://images.unsplash.com/photo-1541701494587-cb58502866ab?w=1200&h=800&fit=crop',
title: '色彩交响',
offset: { x: 40, y: 40, rotate: 3 }
},
{
src: 'https://images.unsplash.com/photo-1505142468610-359e7d316be0?w=1200&h=800&fit=crop',
title: '海天一色',
offset: { x: 80, y: 80, rotate: -1 }
}
]
},
{
type: 'grid', // 网格展示
images: [
{ src: 'https://images.unsplash.com/photo-1682687220742-aba13b6e50ba?w=800&h=800&fit=crop', span: 'large' },
{ src: 'https://images.unsplash.com/photo-1682687221038-404670f09439?w=600&h=600&fit=crop' },
{ src: 'https://images.unsplash.com/photo-1682687220063-4742bd7fd538?w=600&h=600&fit=crop' },
{ src: 'https://images.unsplash.com/photo-1682687220923-c58b9a4592ae?w=600&h=600&fit=crop' },
{ src: 'https://images.unsplash.com/photo-1682687221080-5cb261c645cb?w=600&h=600&fit=crop' },
]
},
{
type: 'full-bleed',
image: 'https://images.unsplash.com/photo-1682687220015-186f63b8850a?w=2000&h=1200&fit=crop',
title: '时光印记',
description: '定格永恒的瞬间',
align: 'right'
}
];
--- ---
<Layout title="Gallery | 视觉艺术"> <!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<title>Gallery | 时光印记</title>
<meta name="description" content="定格美好瞬间,记录青春故事">
</head>
<body>
<main> <main>
<!-- Hero Section - 大画幅开场 --> <!-- 渲染首屏 -->
<section class="hero-section"> <HeroSection {...galleryConfig.hero} />
<div class="hero-image-wrapper">
<img src={heroImage.src} alt={heroImage.title} class="hero-image" />
<div class="hero-overlay"></div>
</div>
<div class="hero-content">
<h1 class="hero-title">{heroImage.title}</h1>
<p class="hero-subtitle">{heroImage.subtitle}</p>
<div class="scroll-indicator">
<span>向下探索</span>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M12 5v14M19 12l-7 7-7-7"/>
</svg>
</div>
</div>
</section>
<!-- Dynamic Sections --> <!-- 动态渲染所有 sections -->
{sections.map((section, index) => { {galleryConfig.sections.map((section, index) => {
if (section.type === 'full-bleed') { const Component = componentMap[section.type];
return ( return Component ? (
<section class={`full-bleed-section fade-in-section align-${section.align}`} data-index={index}> <Component {...section} data-index={index} />
<div class="full-bleed-image-wrapper"> ) : null;
<img src={section.image} alt={section.title} loading="lazy" />
</div>
<div class="full-bleed-content">
<h2>{section.title}</h2>
<p>{section.description}</p>
</div>
</section>
);
}
if (section.type === 'dual') {
return (
<section class="dual-section fade-in-section" data-index={index}>
<div class="dual-container">
{section.images.map((img, i) => (
<div class="dual-card" style={`--delay: ${i * 0.2}s`}>
<div class="dual-image-wrapper">
<img src={img.src} alt={img.title} loading="lazy" />
</div>
<div class="dual-text">
<h3>{img.title}</h3>
<p>{img.description}</p>
</div>
</div>
))}
</div>
</section>
);
}
if (section.type === 'stacked') {
return (
<section class="stacked-section fade-in-section" data-index={index}>
<div class="stacked-container">
{section.images.map((img, i) => (
<div
class="stacked-card"
style={`--offset-x: ${img.offset.x}px; --offset-y: ${img.offset.y}px; --rotate: ${img.offset.rotate}deg; --index: ${i};`}
data-card-index={i}
>
<img src={img.src} alt={img.title} loading="lazy" />
<div class="stacked-title">{img.title}</div>
</div>
))}
</div>
</section>
);
}
if (section.type === 'grid') {
return (
<section class="grid-section fade-in-section" data-index={index}>
<div class="grid-container">
{section.images.map((img, i) => (
<div class={`grid-item ${img.span === 'large' ? 'span-large' : ''}`} style={`--delay: ${i * 0.1}s`}>
<img src={img.src} alt={`Gallery image ${i + 1}`} loading="lazy" />
</div>
))}
</div>
</section>
);
}
})} })}
<!-- End Section --> <!-- 结尾区块 -->
<section class="end-section"> <section class="end-section">
<div class="end-content"> <div class="end-content">
<h2>探索更多</h2> <h2>时光不老,我们不散</h2>
<p>每一张作品背后,都有一个独特的故事</p> <p>感谢每一个美好的瞬间</p>
<button class="cta-button">联系我们</button>
</div> </div>
</section> </section>
</main> </main>
</Layout> </body>
</html>
<style> <style is:global>
:root { * {
--hero-height: 100vh; margin: 0;
--section-padding: clamp(60px, 10vw, 120px); padding: 0;
box-sizing: border-box;
} }
main { html {
background: #000; scroll-behavior: smooth;
color: #fff; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: #1a1a1a;
background: #ffffff;
overflow-x: hidden; overflow-x: hidden;
} }
/* Hero Section */ main {
.hero-section {
position: relative;
height: var(--hero-height);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.hero-image-wrapper {
position: absolute;
inset: 0;
z-index: 0;
}
.hero-image {
width: 100%; width: 100%;
height: 100%;
object-fit: cover;
transform: scale(1.1);
animation: hero-zoom 20s ease-out forwards;
} }
@keyframes hero-zoom { /* 淡入动画通用样式 */
to {
transform: scale(1);
}
}
.hero-overlay {
position: absolute;
inset: 0;
background: linear-gradient(to bottom, rgba(0,0,0,0.3), rgba(0,0,0,0.6));
}
.hero-content {
position: relative;
z-index: 2;
text-align: center;
padding: 2rem;
animation: hero-fade-in 1.5s ease-out 0.5s both;
}
@keyframes hero-fade-in {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.hero-title {
font-size: clamp(3rem, 8vw, 7rem);
font-weight: 700;
letter-spacing: -0.03em;
margin-bottom: 1rem;
text-shadow: 0 4px 20px rgba(0,0,0,0.5);
}
.hero-subtitle {
font-size: clamp(1.2rem, 3vw, 2rem);
font-weight: 300;
opacity: 0.9;
margin-bottom: 3rem;
text-shadow: 0 2px 10px rgba(0,0,0,0.5);
}
.scroll-indicator {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
opacity: 0.7;
animation: bounce 2s infinite;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(10px); }
}
/* Fade In Sections */
.fade-in-section { .fade-in-section {
opacity: 0; opacity: 0;
transform: translateY(50px); transform: translateY(50px);
transition: opacity 1s ease-out, transform 1s ease-out; transition: opacity 0.8s ease, transform 0.8s ease;
} }
.fade-in-section.visible { .fade-in-section.visible {
@ -283,318 +86,39 @@ const sections = [
transform: translateY(0); transform: translateY(0);
} }
/* Full Bleed Section */ /* 结尾区块 */
.full-bleed-section {
position: relative;
min-height: 100vh;
display: flex;
align-items: center;
margin: var(--section-padding) 0;
}
.full-bleed-image-wrapper {
position: absolute;
inset: 0;
overflow: hidden;
}
.full-bleed-image-wrapper img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease-out;
}
.full-bleed-section.visible .full-bleed-image-wrapper img {
transform: scale(1.05);
}
.full-bleed-content {
position: relative;
z-index: 2;
max-width: 600px;
padding: 4rem;
background: rgba(0,0,0,0.5);
backdrop-filter: blur(20px);
border-radius: 20px;
margin: 0 4rem;
}
.full-bleed-section.align-right .full-bleed-content {
margin-left: auto;
}
.full-bleed-content h2 {
font-size: clamp(2.5rem, 5vw, 4rem);
font-weight: 700;
margin-bottom: 1rem;
letter-spacing: -0.02em;
}
.full-bleed-content p {
font-size: clamp(1.1rem, 2vw, 1.5rem);
font-weight: 300;
opacity: 0.9;
line-height: 1.6;
}
/* Dual Section */
.dual-section {
padding: var(--section-padding) 2rem;
}
.dual-container {
max-width: 1400px;
margin: 0 auto;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
gap: 3rem;
}
.dual-card {
position: relative;
border-radius: 20px;
overflow: hidden;
background: #111;
transform: translateY(30px);
opacity: 0;
transition: all 0.8s ease-out;
transition-delay: var(--delay);
}
.dual-section.visible .dual-card {
transform: translateY(0);
opacity: 1;
}
.dual-image-wrapper {
aspect-ratio: 4/5;
overflow: hidden;
}
.dual-image-wrapper img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.6s ease;
}
.dual-card:hover .dual-image-wrapper img {
transform: scale(1.1);
}
.dual-text {
padding: 2rem;
}
.dual-text h3 {
font-size: 2rem;
margin-bottom: 0.5rem;
font-weight: 600;
}
.dual-text p {
font-size: 1.1rem;
opacity: 0.8;
line-height: 1.6;
}
/* Stacked Section */
.stacked-section {
padding: var(--section-padding) 2rem;
min-height: 80vh;
display: flex;
align-items: center;
justify-content: center;
}
.stacked-container {
position: relative;
width: 100%;
max-width: 900px;
aspect-ratio: 3/2;
}
.stacked-card {
position: absolute;
width: 70%;
aspect-ratio: 3/2;
border-radius: 20px;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
cursor: pointer;
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
left: 50%;
top: 50%;
transform: translate(-50%, -50%)
translate(var(--offset-x), var(--offset-y))
rotate(var(--rotate));
}
.stacked-section.visible .stacked-card {
animation: stack-reveal 0.8s ease-out both;
animation-delay: calc(var(--index) * 0.15s);
}
@keyframes stack-reveal {
from {
opacity: 0;
transform: translate(-50%, -50%)
translate(var(--offset-x), calc(var(--offset-y) + 50px))
rotate(var(--rotate))
scale(0.9);
}
to {
opacity: 1;
transform: translate(-50%, -50%)
translate(var(--offset-x), var(--offset-y))
rotate(var(--rotate))
scale(1);
}
}
.stacked-card:hover {
transform: translate(-50%, -50%)
translate(var(--offset-x), var(--offset-y))
rotate(0deg)
scale(1.05);
z-index: 10;
}
.stacked-card img {
width: 100%;
height: 100%;
object-fit: cover;
}
.stacked-title {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 2rem;
background: linear-gradient(to top, rgba(0,0,0,0.9), transparent);
font-size: 1.5rem;
font-weight: 600;
opacity: 0;
transition: opacity 0.3s ease;
}
.stacked-card:hover .stacked-title {
opacity: 1;
}
/* Grid Section */
.grid-section {
padding: var(--section-padding) 2rem;
}
.grid-container {
max-width: 1600px;
margin: 0 auto;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
}
.grid-item {
position: relative;
aspect-ratio: 1;
border-radius: 16px;
overflow: hidden;
background: #111;
opacity: 0;
transform: scale(0.9);
transition: all 0.6s ease-out;
transition-delay: var(--delay);
}
.grid-section.visible .grid-item {
opacity: 1;
transform: scale(1);
}
.grid-item.span-large {
grid-column: span 2;
grid-row: span 2;
}
.grid-item img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease;
}
.grid-item:hover img {
transform: scale(1.1);
}
/* End Section */
.end-section { .end-section {
padding: calc(var(--section-padding) * 2) 2rem; padding: 8rem 2rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
text-align: center; text-align: center;
} }
.end-content {
max-width: 800px;
margin: 0 auto;
color: white;
}
.end-content h2 { .end-content h2 {
font-size: clamp(2.5rem, 5vw, 4rem); font-size: clamp(2rem, 5vw, 3rem);
margin-bottom: 1rem;
font-weight: 700; font-weight: 700;
margin-bottom: 1rem;
} }
.end-content p { .end-content p {
font-size: clamp(1.2rem, 2vw, 1.5rem); font-size: clamp(1.1rem, 2vw, 1.5rem);
opacity: 0.8; opacity: 0.95;
margin-bottom: 2rem;
}
.cta-button {
padding: 1.2rem 3rem;
font-size: 1.1rem;
font-weight: 600;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 50px;
cursor: pointer;
transition: all 0.3s ease;
}
.cta-button:hover {
transform: translateY(-3px);
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
}
/* Responsive */
@media (max-width: 1024px) {
.dual-container {
grid-template-columns: 1fr;
}
.stacked-container {
max-width: 600px;
}
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.full-bleed-content { .end-section {
margin: 0 2rem; padding: 4rem 1.5rem;
padding: 2rem;
}
.grid-item.span-large {
grid-column: span 1;
grid-row: span 1;
}
.stacked-card {
width: 85%;
} }
} }
</style> </style>
<script> <script>
// Intersection Observer for fade-in animations // Intersection Observer 用于滚动动画
const observerOptions = { const observerOptions = {
threshold: 0.15, threshold: 0.15,
rootMargin: '0px 0px -100px 0px' rootMargin: '0px 0px -100px 0px'
@ -608,25 +132,8 @@ const sections = [
}); });
}, observerOptions); }, observerOptions);
// Observe all fade-in sections // 观察所有需要动画的元素
document.querySelectorAll('.fade-in-section').forEach(section => { document.querySelectorAll('.fade-in-section').forEach(el => {
observer.observe(section); observer.observe(el);
});
// Parallax effect for hero
window.addEventListener('scroll', () => {
const scrolled = window.pageYOffset;
const heroImage = document.querySelector('.hero-image');
if (heroImage && scrolled < window.innerHeight) {
heroImage.style.transform = `scale(1.1) translateY(${scrolled * 0.5}px)`;
}
});
// Smooth scroll for indicator
document.querySelector('.scroll-indicator')?.addEventListener('click', () => {
window.scrollTo({
top: window.innerHeight,
behavior: 'smooth'
});
}); });
</script> </script>