增加风景显示

This commit is contained in:
Sin Lee 2025-12-06 18:23:41 +08:00
parent 080a4ac07a
commit 1876b69d48
20 changed files with 688 additions and 4 deletions

View File

@ -102,6 +102,26 @@ hero: {
} }
``` ```
#### 示例 4添加风景照片展示新功能
```javascript
{
type: 'scenery',
enabled: true, // 开关控制
title: '风景搅影',
description: '记录自然之美',
categories: ['nature', 'urban', 'plants', 'animals'], // 要展示的类别
groupCount: 2, // 展示几个照片组
random: true // 是否随机选择
}
```
**风景类别说明:**
- `nature` - 纯自然风景 (5_x_x.JPG)
- `urban` - 非自然风景/城市 (6_x_x.JPG)
- `plants` - 植物 (7_x_x.JPG)
- `animals` - 动物 (8_x_x.JPG)
## 📦 支持的布局类型 ## 📦 支持的布局类型
- `hero` - 首屏大图 - `hero` - 首屏大图
@ -109,6 +129,7 @@ hero: {
- `quad-grid` - 四宫格2x2 - `quad-grid` - 四宫格2x2
- `dual` - 双栏布局 - `dual` - 双栏布局
- `grid` - 灵活网格(支持 large 和 normal 尺寸) - `grid` - 灵活网格(支持 large 和 normal 尺寸)
- `scenery` - 风景照片展示(新增!支持随机选择和分类过滤)
## 🔄 部署到 VPS ## 🔄 部署到 VPS
@ -129,8 +150,9 @@ git push
- **添加图片**:只需把新图片放到 `public/media/` 然后在配置文件中引用即可 - **添加图片**:只需把新图片放到 `public/media/` 然后在配置文件中引用即可
- **调整顺序**:在 `sections` 数组中调整顺序即可改变页面布局顺序 - **调整顺序**:在 `sections` 数组中调整顺序即可改变页面布局顺序
## 🎯 图片命名规则(你的照片) ## 🎯 图片命名规则
### 人物照片
- `0_X.JPG` - 0号人物的照片 - `0_X.JPG` - 0号人物的照片
- `1_X.JPG` - 1号人物的照片 - `1_X.JPG` - 1号人物的照片
- `2_X.JPG` - 2号人物的照片 - `2_X.JPG` - 2号人物的照片
@ -138,4 +160,23 @@ git push
- `0123_X.JPG` - 四人合照 - `0123_X.JPG` - 四人合照
- `23_X.JPG` - 2号和3号的合照 - `23_X.JPG` - 2号和3号的合照
### 风景照片(新增)
- `5_Y_Z.JPG` - 纯自然风景
- `6_Y_Z.JPG` - 非自然风景(建筑、城市等)
- `7_Y_Z.JPG` - 植物
- `8_Y_Z.JPG` - 动物
**编号说明:**
- 第一位类型5/6/7/8
- 第二位Y关联组号相同数字代表有关联的照片
- 第三位Z顺序编号
**示例:**
```
5_1_1.JPG ← 纯自然风景第1组第1张
5_1_2.JPG ← 纯自然风景第1组第2张与上一张相关
5_2_1.JPG ← 纯自然风景第2组第1张独立场景
6_1_1.JPG ← 非自然风景第1组第1张
```
你可以根据这些照片自由组织任何布局! 你可以根据这些照片自由组织任何布局!

81
SCENERY-GUIDE.md Normal file
View File

@ -0,0 +1,81 @@
# 🌄 风景照片功能使用指南
## 快速开始
### 1. 添加风景照片
将风景照片放到 `public/media/` 目录,按照命名规则命名:
```bash
public/media/
├── 5_1_1.JPG # 纯自然风景第1组第1张
├── 5_1_2.JPG # 纯自然风景第1组第2张
├── 6_1_1.jpeg # 非自然风景第1组第1张
└── 7_1_1.jpeg # 植物第1组第1张
```
### 2. 更新照片列表
```bash
npm run scan-photos
```
### 3. 在配置中启用
编辑 `src/data/gallery-config.js`
```javascript
{
type: 'scenery',
enabled: true,
title: '风景掠影',
description: '记录自然之美,定格时光瞬间',
categories: ['nature', 'urban', 'plants', 'animals'],
groupCount: 2,
random: true
}
```
## 命名规则
### 格式
```
[类型]_[组号]_[序号].[扩展名]
```
- **类型**5=自然风景, 6=城市风景, 7=植物, 8=动物
- **组号**:相同数字代表关联照片
- **序号**:组内排序
### 示例
```
5_1_1.JPG ← 第1组自然风景第1张
5_1_2.JPG ← 第1组自然风景第2张与上一张相关
5_2_1.JPG ← 第2组自然风景第1张独立场景
```
## 配置选项
```javascript
{
enabled: true, // 开关控制
categories: ['nature'], // 类别过滤
groupCount: 2, // 显示几组
random: true // 是否随机
}
```
## 布局效果
- 1张居中大图
- 2张左右平铺
- 3张一大两小
- 4张田字格
- 5+张:瀑布流
## 常见问题
**Q: 如何添加新照片?**
A: 放到 `public/media/`,运行 `npm run scan-photos`
**Q: 可以临时关闭吗?**
A: 设置 `enabled: false`
**Q: 支持哪些格式?**
A: JPG, JPEG, PNG, WebP

52
SCENERY-IMPLEMENTATION.md Normal file
View File

@ -0,0 +1,52 @@
# 🎨 风景照片功能 - 实现总结
## ✅ 完成的功能
### 核心组件
- ScenerySection.astro - 风景展示组件
- 自动识别照片类型5/6/7/8
- 智能分组和布局
- 响应式设计
### 配置系统
- scenery-photos.js - 照片配置
- gallery-config.js - 主配置
- enabled 开关控制
### 自动化工具
- scan-scenery-photos.js - 扫描脚本
- npm scripts 集成
## 文件结构
```
gallery/
├── src/
│ ├── components/sections/
│ │ └── ScenerySection.astro
│ └── data/
│ ├── scenery-photos.js
│ └── gallery-config.js
├── scripts/
│ └── scan-scenery-photos.js
└── SCENERY-GUIDE.md
```
## 使用方法
```bash
# 1. 扫描照片
npm run scan-photos
# 2. 启动开发
npm run dev
# 3. 访问
http://localhost:4321
```
## 核心特性
- ✅ 智能分组
- ✅ 随机展示
- ✅ 类别过滤
- ✅ 响应式布局
- ✅ 优雅动画

View File

@ -1,7 +1,7 @@
import { defineConfig } from 'astro/config'; import { defineConfig } from 'astro/config';
export default defineConfig({ export default defineConfig({
site: 'https://gallery.yourdomain.com', site: 'https://leexxx.com',
// 或者如果是主域名:'https://yourdomain.com' // 或者如果是主域名:'https://yourdomain.com'
// 图片优化 // 图片优化

View File

@ -6,7 +6,8 @@
"dev": "astro dev", "dev": "astro dev",
"build": "astro build", "build": "astro build",
"preview": "astro preview", "preview": "astro preview",
"astro": "astro" "astro": "astro",
"scan-photos": "node scripts/scan-scenery-photos.js"
}, },
"dependencies": { "dependencies": {
"@astrojs/mdx": "^4.3.12", "@astrojs/mdx": "^4.3.12",

BIN
public/media/0123_3.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

BIN
public/media/5_1_1.JPG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

BIN
public/media/5_1_2.JPG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

BIN
public/media/5_1_3.JPG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

BIN
public/media/5_1_4.JPG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 MiB

BIN
public/media/5_1_5.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 MiB

BIN
public/media/5_2_1.JPG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

BIN
public/media/6_1_1.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

BIN
public/media/6_2_1.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

BIN
public/media/7_1_1.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

114
scripts/scan-scenery-photos.js Executable file
View File

@ -0,0 +1,114 @@
#!/usr/bin/env node
/**
* 自动扫描风景照片脚本
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const MEDIA_DIR = path.join(__dirname, '../public/media');
const OUTPUT_FILE = path.join(__dirname, '../src/data/scenery-photos.js');
// 风景照片的命名规则5_、6_、7_、8_ 开头
const SCENERY_PATTERN = /^[5-8]_\d+_\d+\.(jpg|jpeg|png|webp)$/i;
// 分类映射
const CATEGORIES = {
'5': { name: '纯自然风景', files: [] },
'6': { name: '非自然风景', files: [] },
'7': { name: '植物', files: [] },
'8': { name: '动物', files: [] }
};
function scanPhotos() {
console.log('🔍 正在扫描风景照片...');
const files = fs.readdirSync(MEDIA_DIR);
const sceneryFiles = files.filter(file => SCENERY_PATTERN.test(file));
console.log(`✅ 找到 ${sceneryFiles.length} 张风景照片`);
sceneryFiles.forEach(file => {
const category = file[0];
if (CATEGORIES[category]) {
CATEGORIES[category].files.push(file);
}
});
console.log('\n📊 照片分类统计:');
Object.entries(CATEGORIES).forEach(([key, { name, files }]) => {
console.log(` ${key}_ ${name}: ${files.length}`);
});
return sceneryFiles.sort();
}
function generateConfigFile() {
const timestamp = new Date().toISOString();
const lines = [];
Object.entries(CATEGORIES).forEach(([key, { name, files }]) => {
if (files.length > 0) {
lines.push(` // ${name} (${key}_)`);
files.forEach(file => lines.push(` '${file}',`));
lines.push('');
}
});
if (lines.length > 0 && lines[lines.length - 1] === '') {
lines.pop();
lines[lines.length - 1] = lines[lines.length - 1].replace(/,$/, '');
}
return `/**
* 风景照片配置文件
*
* 此文件由脚本自动生成请勿手动编辑
* 生成时间: ${timestamp}
*
* 命名规则
* - 5_Y_Z.JPG - 纯自然风景
* - 6_Y_Z.JPG - 非自然风景建筑城市等
* - 7_Y_Z.JPG - 植物
* - 8_Y_Z.JPG - 动物
*/
export const sceneryPhotos = [
${lines.join('\n')}
];
`;
}
function main() {
console.log('🎨 Gallery 风景照片扫描工具\n');
if (!fs.existsSync(MEDIA_DIR)) {
console.error(`❌ 错误: 找不到 media 目录: ${MEDIA_DIR}`);
process.exit(1);
}
const photos = scanPhotos();
if (photos.length === 0) {
console.log('\n⚠ 警告: 未找到符合命名规则的风景照片');
return;
}
const content = generateConfigFile();
const dir = path.dirname(OUTPUT_FILE);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(OUTPUT_FILE, content, 'utf8');
console.log(`\n✅ 配置文件已更新: ${path.relative(process.cwd(), OUTPUT_FILE)}`);
console.log('\n🎉 完成!\n');
}
main();

View File

@ -0,0 +1,349 @@
---
/**
* 风景照片展示组件
* 支持随机选择风景照片组进行展示
*/
interface Props {
title?: string;
description?: string;
categories?: ('nature' | 'urban' | 'plants' | 'animals')[];
groupCount?: number;
random?: boolean;
}
const {
title = '风景掠影',
description = '记录自然之美,定格时光瞬间',
categories = ['nature', 'urban', 'plants', 'animals'],
groupCount = 2,
random = true
} = Astro.props;
// 类别映射
const categoryPrefixes: Record<string, string> = {
'nature': '5',
'urban': '6',
'plants': '7',
'animals': '8'
};
const categoryNames: Record<string, string> = {
'nature': '自然风光',
'urban': '城市印象',
'plants': '花草世界',
'animals': '生灵物语'
};
// 导入风景照片配置
import { sceneryPhotos } from '../../data/scenery-photos.js';
const mediaPath = '/media/';
// 解析照片信息
interface PhotoInfo {
filename: string;
path: string;
category: string;
categoryName: string;
groupId: string;
sequence: string;
}
function parsePhoto(filename: string): PhotoInfo | null {
const match = filename.match(/^([5-8])_(\d+)_(\d+)\./);
if (!match) return null;
const [, typeId, groupId, sequence] = match;
const categoryKey = Object.keys(categoryPrefixes).find(
key => categoryPrefixes[key] === typeId
);
if (!categoryKey) return null;
return {
filename,
path: mediaPath + filename,
category: categoryKey,
categoryName: categoryNames[categoryKey],
groupId,
sequence
};
}
// 按类别和组号分组照片
interface PhotoGroup {
category: string;
categoryName: string;
groupId: string;
photos: PhotoInfo[];
}
function groupPhotos(photos: PhotoInfo[]): PhotoGroup[] {
const groups = new Map<string, PhotoGroup>();
photos.forEach(photo => {
const key = `${photo.category}_${photo.groupId}`;
if (!groups.has(key)) {
groups.set(key, {
category: photo.category,
categoryName: photo.categoryName,
groupId: photo.groupId,
photos: []
});
}
groups.get(key)!.photos.push(photo);
});
groups.forEach(group => {
group.photos.sort((a, b) => parseInt(a.sequence) - parseInt(b.sequence));
});
return Array.from(groups.values());
}
// 处理照片
const parsedPhotos = sceneryPhotos
.map(parsePhoto)
.filter((p): p is PhotoInfo => p !== null)
.filter(p => categories.includes(p.category as any));
const photoGroups = groupPhotos(parsedPhotos);
// 随机或按顺序选择照片组
let selectedGroups = photoGroups;
if (random && photoGroups.length > groupCount) {
selectedGroups = [...photoGroups]
.sort(() => Math.random() - 0.5)
.slice(0, groupCount);
} else {
selectedGroups = photoGroups.slice(0, groupCount);
}
---
{selectedGroups.length > 0 && (
<section class="scenery-section fade-in-section">
<div class="scenery-header">
<h2 class="scenery-title">{title}</h2>
{description && <p class="scenery-description">{description}</p>}
</div>
<div class="scenery-groups">
{selectedGroups.map((group, groupIndex) => (
<div
class="scenery-group"
style={`--group-delay: ${groupIndex * 0.2}s`}
>
<div class="group-label">
<span class="category-badge">{group.categoryName}</span>
</div>
<div class={`group-photos photos-${group.photos.length}`}>
{group.photos.map((photo, photoIndex) => (
<div
class="photo-item"
style={`--photo-delay: ${groupIndex * 0.2 + photoIndex * 0.1}s`}
>
<img
src={photo.path}
alt={`${photo.categoryName} ${photo.groupId}-${photo.sequence}`}
loading="lazy"
/>
</div>
))}
</div>
</div>
))}
</div>
</section>
)}
<style>
.scenery-section {
padding: 6rem 2rem;
background: linear-gradient(to bottom, #f8f9fa 0%, #ffffff 100%);
}
.scenery-header {
max-width: 1400px;
margin: 0 auto 4rem;
text-align: center;
}
.scenery-title {
font-size: clamp(2rem, 5vw, 3.5rem);
font-weight: 700;
color: #1a1a1a;
margin-bottom: 1rem;
}
.scenery-description {
font-size: clamp(1rem, 2vw, 1.25rem);
color: #666;
max-width: 600px;
margin: 0 auto;
}
.scenery-groups {
max-width: 1400px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 4rem;
}
.scenery-group {
opacity: 0;
transform: translateY(30px);
animation: groupFadeIn 0.8s ease forwards;
animation-delay: var(--group-delay);
}
.group-label {
margin-bottom: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
}
.category-badge {
display: inline-block;
padding: 0.5rem 1.5rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 50px;
font-size: 0.9rem;
font-weight: 600;
letter-spacing: 0.5px;
}
.group-photos {
display: grid;
gap: 1rem;
border-radius: 12px;
overflow: hidden;
}
/* 单张照片 - 居中大图 */
.photos-1 {
grid-template-columns: 1fr;
max-width: 800px;
margin: 0 auto;
}
/* 两张照片 - 左右布局 */
.photos-2 {
grid-template-columns: repeat(2, 1fr);
}
/* 三张照片 - 一大两小 */
.photos-3 {
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
}
.photos-3 .photo-item:first-child {
grid-row: span 2;
}
/* 四张照片 - 田字格 */
.photos-4 {
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
}
/* 五张及以上 - 瀑布流 */
.photos-5,
.photos-6,
.photos-7,
.photos-8 {
grid-template-columns: repeat(3, 1fr);
grid-auto-rows: 250px;
}
.photos-5 .photo-item:first-child,
.photos-6 .photo-item:first-child,
.photos-7 .photo-item:first-child,
.photos-8 .photo-item:first-child {
grid-column: span 2;
grid-row: span 2;
}
.photo-item {
overflow: hidden;
background: #f0f0f0;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
opacity: 0;
transform: scale(0.95);
animation: photoFadeIn 0.6s ease forwards;
animation-delay: var(--photo-delay);
}
.photo-item img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease;
}
.photo-item:hover img {
transform: scale(1.08);
}
@keyframes groupFadeIn {
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes photoFadeIn {
to {
opacity: 1;
transform: scale(1);
}
}
/* 响应式布局 */
@media (max-width: 968px) {
.scenery-section {
padding: 4rem 1.5rem;
}
.scenery-groups {
gap: 3rem;
}
.photos-2,
.photos-3,
.photos-4 {
grid-template-columns: 1fr;
grid-template-rows: auto;
}
.photos-3 .photo-item:first-child,
.photos-5 .photo-item:first-child,
.photos-6 .photo-item:first-child {
grid-column: span 1;
grid-row: span 1;
}
.photos-5,
.photos-6,
.photos-7,
.photos-8 {
grid-template-columns: repeat(2, 1fr);
grid-auto-rows: 200px;
}
}
@media (max-width: 640px) {
.photos-5,
.photos-6,
.photos-7,
.photos-8 {
grid-template-columns: 1fr;
}
}
</style>

View File

@ -1,4 +1,5 @@
// 画廊配置 - 所有布局和资源的单一数据源 // 画廊配置 - 所有布局和资源的单一数据源
// 支持的布局类型: hero, full-bleed, quad-grid, dual, grid, scenery
export const galleryConfig = { export const galleryConfig = {
// 首屏大图 - 使用四人合照 // 首屏大图 - 使用四人合照
hero: { hero: {
@ -74,6 +75,17 @@ export const galleryConfig = {
image: '/media/0_1.JPG', image: '/media/0_1.JPG',
title: '青春不散场', title: '青春不散场',
description: '愿时光不老,我们不散' description: '愿时光不老,我们不散'
},
// 6. 风景照片展示(新增)- 可通过 enabled 开关控制
{
type: 'scenery',
enabled: true, // 开关控制,设为 false 可关闭
title: '风景掠影',
description: '记录自然之美,定格时光瞒间',
categories: ['nature', 'urban', 'plants', 'animals'], // 要展示的类别
groupCount: 2, // 展示几个照片组
random: true // 是否随机选择
} }
] ]
}; };

View File

@ -0,0 +1,29 @@
/**
* 风景照片配置文件
*
* 此文件由脚本自动生成请勿手动编辑
* 生成时间: 2025-12-06T10:14:14.920Z
*
* 命名规则
* - 5_Y_Z.JPG - 纯自然风景
* - 6_Y_Z.JPG - 非自然风景建筑城市等
* - 7_Y_Z.JPG - 植物
* - 8_Y_Z.JPG - 动物
*/
export const sceneryPhotos = [
// 纯自然风景 (5_)
'5_1_1.JPG',
'5_1_2.JPG',
'5_1_3.JPG',
'5_1_4.JPG',
'5_1_5.jpeg',
'5_2_1.JPG',
// 非自然风景 (6_)
'6_1_1.jpeg',
'6_2_1.jpeg',
// 植物 (7_)
'7_1_1.jpeg'
];

View File

@ -7,6 +7,7 @@ import FullBleedSection from '../components/sections/FullBleedSection.astro';
import DualSection from '../components/sections/DualSection.astro'; import DualSection from '../components/sections/DualSection.astro';
import GridSection from '../components/sections/GridSection.astro'; import GridSection from '../components/sections/GridSection.astro';
import QuadGridSection from '../components/sections/QuadGridSection.astro'; import QuadGridSection from '../components/sections/QuadGridSection.astro';
import ScenerySection from '../components/sections/ScenerySection.astro';
// 组件映射表 // 组件映射表
const componentMap = { const componentMap = {
@ -14,7 +15,8 @@ const componentMap = {
'full-bleed': FullBleedSection, 'full-bleed': FullBleedSection,
'dual': DualSection, 'dual': DualSection,
'grid': GridSection, 'grid': GridSection,
'quad-grid': QuadGridSection 'quad-grid': QuadGridSection,
'scenery': ScenerySection
}; };
--- ---
@ -34,6 +36,9 @@ const componentMap = {
<!-- 动态渲染所有 sections --> <!-- 动态渲染所有 sections -->
{galleryConfig.sections.map((section, index) => { {galleryConfig.sections.map((section, index) => {
// 如果 section 有 enabled 属性且为 false则跳过
if (section.enabled === false) return null;
const Component = componentMap[section.type]; const Component = componentMap[section.type];
return Component ? ( return Component ? (
<Component {...section} data-index={index} /> <Component {...section} data-index={index} />