诗歌总览页是海子诗歌网站的核心页面之一,提供诗歌浏览、筛选和搜索功能。
访问路径:/poetry
核心功能:
页面布局:
┌─────────────────────────────────────────────┐
│ 导航栏 (Navbar) │
├──────────────┬──────────────────────────────┤
│ │ 搜索栏 (Search Bar) │
│ 筛选栏 │ ┌────────┐ │
│ (Sidebar) │ └────────┘ │
│ │ │
│ ┌ 筛选 ┐ │ • 诗歌标题1 │
│ [类型] │ • 诗歌标题2 │
│ ☑ 短诗 │ • 诗歌标题3 │
│ □ 长诗 │ • 诗歌标题4 │
│ │ • 诗歌标题5 │
│ [时间] │ ... │
│ ☑ 1989 │ │
│ □ 1988 │ │
│ │ │
└──────────────┴──────────────────────────────┘
| 文件 | 路径 | 说明 |
|---|---|---|
| 页面文件 | index.astro | 页面结构和交互逻辑 |
| 样式文件 | ../../styles/pages/poetry-index.css | 页面专属样式 |
| 数据源 | ../../content/data.ts | 诗歌数据定义 |
| 布局组件 | ../../layouts/BaseLayout.astro | 基础布局 |
| 全局样式 | ../../styles/global.css | CSS变量定义 |
index.astro
├─ import BaseLayout from '../../layouts/BaseLayout.astro'
├─ import { samplePoems, poemTypes, poemYears } from '../../content/data'
└─ import '../../styles/pages/poetry-index.css'
└─ 使用 var(--colorPrimary) 等全局CSS变量
支持的类型:
export const poemTypes = [
{ key: 'short', label: '短诗' },
{ key: 'long', label: '长诗' },
{ key: 'sun', label: '太阳·七部书' },
{ key: 'essay', label: '文论' },
];
筛选逻辑:
年份范围:1983-1989(海子创作时期)
export const poemYears = [
'1983', '1984', '1985', '1986', '1987', '1988', '1989'
];
筛选特点:
HTML结构:
<div class="filter-group">
<button class="filter-group-toggle" data-group="type">
<span>类型</span>
<svg class="toggle-icon">...</svg>
</button>
<div class="filter-options" data-group-content="type">
<!-- 筛选选项 -->
</div>
</div>
交互行为:
位置:筛选栏顶部右侧
功能:
实现代码:
clearButton?.addEventListener('click', () => {
checkboxes.forEach(checkbox => {
checkbox.checked = false;
});
searchInput.value = '';
filterPoems();
});
触发方式:input事件(每次输入都触发)
搜索范围:诗歌标题
匹配规则:
实现代码:
searchInput?.addEventListener('input', filterPoems);
function filterPoems() {
const searchTerm = searchInput.value.toLowerCase();
poems?.forEach(poem => {
const title = poem.getAttribute('data-title')?.toLowerCase() || '';
const matchesSearch = !searchTerm || title.includes(searchTerm);
// ...
});
}
CSS实现:
.search-bar {
position: sticky;
top: var(--navHeight); /* 70px */
z-index: 10;
background: var(--colorBackground);
}
效果:
HTML结构:
<a
href={`/poetry/${poem.id}`}
class="poem-item"
data-type={poem.type}
data-year={poem.year}
data-title={poem.title}
>
<span class="poem-dot"></span>
<span class="poem-title">{poem.title}</span>
</a>
设计元素:
用途:存储诗歌元数据,供JavaScript过滤使用
| 属性 | 说明 | 示例 |
|---|---|---|
data-type | 诗歌类型 | short, long, sun, essay |
data-year | 创作年份 | 1989, 1988, … |
data-title | 诗歌标题 | 面朝大海,春暖花开 |
优势:
目标页面:/poetry/[id](诗歌详情页)
路由示例:
/poetry/facing-the-sea → 《面朝大海,春暖花开》
/poetry/haizi-diary → 《海子小夜曲》
export interface Poem {
id: string; // 唯一标识符,用于URL路由
title: string; // 诗歌标题
content: string; // 诗歌完整内容(多行文本)
type?: string; // 类型: 'short' | 'long' | 'sun' | 'essay'
year?: string; // 年份: '1983'-'1989'
tags: string[]; // 标签数组(暂未使用)
}
{
id: 'facing-the-sea',
title: '面朝大海,春暖花开',
content: `从明天起,做一个幸福的人
喂马,劈柴,周游世界
从明天起,关心粮食和蔬菜
我有一所房子,面朝大海,春暖花开`,
type: 'short',
year: '1989',
tags: ['短诗', '1989']
}
颜色:
--colorPrimary: #600000; /* 深红色(主题色) */
--colorPrimaryLight: #8B0000; /* 浅红色(悬停) */
--colorText: #1a1a1a; /* 正文文字 */
--colorTextLight: #666666; /* 次要文字 */
--colorBackground: #ffffff; /* 背景白色 */
--colorBorder: #e5e5e5; /* 边框灰色 */
字体:
--fontDisplay: 'SimSun', 'Songti SC', serif; /* 宋体(标题) */
--fontBody: 'Satoshi', 'SimSun', 'Songti SC', sans-serif; /* Satoshi(正文) */
--fontSizeBase: 1rem; /* 16px */
--fontSizeSm: 0.875rem; /* 14px */
间距:
--spacingMd: 1rem; /* 16px */
--spacingLg: 1.5rem; /* 24px */
--spacingXl: 2rem; /* 32px */
--spacing2xl: 3rem; /* 48px */
布局:
--navHeight: 70px; /* 导航栏高度 */
--sidebarWidth: 320px; /* 侧边栏宽度 */
动画:
--transitionFast: 150ms ease; /* 快速过渡 */
--transitionNormal: 250ms ease; /* 标准过渡 */
两列Flexbox布局:
.poetry-page {
display: flex; /* 左右布局 */
}
.filter-sidebar {
width: var(--sidebarWidth); /* 固定宽度320px */
position: sticky; /* 粘性定位 */
top: var(--navHeight); /* 在导航栏下方 */
}
.poetry-main {
flex: 1; /* 占据剩余空间 */
}
实现原理:
<input type="checkbox">.checkbox-custom伪元素模拟:checked伪类控制样式关键代码:
/* 隐藏原生复选框 */
.filter-option input {
position: absolute;
opacity: 0;
}
/* 自定义复选框外观 */
.checkbox-custom {
width: 16px;
height: 16px;
border: 1.5px solid var(--colorBorder);
border-radius: 3px;
}
/* 选中状态 */
.filter-option input:checked + .checkbox-custom {
background-color: var(--colorPrimary);
border-color: var(--colorPrimary);
}
/* 勾选符号(CSS绘制) */
.filter-option input:checked + .checkbox-custom::after {
content: '';
width: 4px;
height: 8px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg) translateY(-1px);
}
function filterPoems() {
// 1. 获取搜索关键词
const searchTerm = searchInput.value.toLowerCase();
// 2. 获取选中的类型
const selectedTypes = Array.from(
document.querySelectorAll('input[name="type"]:checked')
).map(input => input.value);
// 3. 获取选中的年份
const selectedYears = Array.from(
document.querySelectorAll('input[name="year"]:checked')
).map(input => input.value);
// 4. 遍历所有诗歌项
const poems = poemList?.querySelectorAll('.poem-item');
poems?.forEach(poem => {
const title = poem.getAttribute('data-title')?.toLowerCase() || '';
const type = poem.getAttribute('data-type') || '';
const year = poem.getAttribute('data-year') || '';
// 5. 三重条件判断(AND逻辑)
const matchesSearch = !searchTerm || title.includes(searchTerm);
const matchesType = selectedTypes.length === 0 || selectedTypes.includes(type);
const matchesYear = selectedYears.length === 0 || selectedYears.includes(year);
// 6. 显示或隐藏
if (matchesSearch && matchesType && matchesYear) {
poem.classList.remove('hidden');
} else {
poem.classList.add('hidden');
}
});
}
过滤逻辑:
// 复选框变化 → 触发过滤
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', filterPoems);
});
// 搜索输入 → 实时过滤
searchInput?.addEventListener('input', filterPoems);
// 清除按钮 → 重置 + 过滤
clearButton?.addEventListener('click', () => {
checkboxes.forEach(cb => cb.checked = false);
searchInput.value = '';
filterPoems();
});
// 筛选组折叠/展开
filterToggles.forEach(toggle => {
toggle.addEventListener('click', () => {
const group = toggle.getAttribute('data-group');
const content = document.querySelector(`[data-group-content="${group}"]`);
toggle.classList.toggle('collapsed');
content?.classList.toggle('hidden');
});
});
用户操作
↓
┌─────────────────┬──────────────────┬───────────────┐
│ 输入搜索关键词 │ 勾选/取消复选框 │ 点击清除按钮 │
└─────────────────┴──────────────────┴───────────────┘
↓ ↓ ↓
input事件 change事件 click事件
↓ ↓ ↓
└──────────────────┴────────────────────┘
↓
filterPoems() 函数
↓
┌──────────────┼──────────────┐
↓ ↓ ↓
读取搜索词 读取选中类型 读取选中年份
↓ ↓ ↓
└──────────────┴──────────────┘
↓
遍历所有 .poem-item 元素
↓
读取 data-title, data-type, data-year
↓
三重条件判断(AND逻辑)
↓
┌─────────────┴─────────────┐
↓ ↓
全部匹配 至少一项不匹配
↓ ↓
移除 .hidden 类 添加 .hidden 类
↓ ↓
诗歌项显示 诗歌项隐藏
| 设备 | 屏幕宽度 | 布局特点 |
|---|---|---|
| 桌面 | > 1024px | 两列(320px + flex:1) |
| 平板 | 768px - 1024px | 两列(260px + flex:1) |
| 手机 | < 768px | 单列堆叠 |
.poetry-page {
display: flex;
flex-direction: row; /* 左右布局 */
}
.filter-sidebar {
width: 320px;
min-width: 320px;
position: sticky;
top: 70px;
}
.poetry-main {
flex: 1;
padding: 48px;
}
@media (max-width: 1024px) {
.filter-sidebar {
width: 260px; /* 缩小侧边栏 */
min-width: 260px;
padding: 24px; /* 减小内边距 */
}
}
@media (max-width: 768px) {
.poetry-page {
flex-direction: column; /* 竖向堆叠 */
}
.filter-sidebar {
width: 100%; /* 全宽 */
min-width: 100%;
height: auto; /* 自适应高度 */
position: relative; /* 取消粘性定位 */
top: 0;
border-right: none;
border-bottom: 1px solid var(--colorBorderLight);
}
.poetry-main {
padding: 24px;
}
.search-bar {
flex-direction: column; /* 搜索栏竖排 */
}
.search-button {
width: 100%; /* 按钮全宽 */
}
}
// src/content/data.ts
export const samplePoems: Poem[] = [
// ... 现有诗歌
{
id: 'new-poem-id', // 使用小写字母和连字符
title: '新诗歌标题',
content: `第一行
第二行
第三行`,
type: 'short', // short | long | sun | essay
year: '1989', // 1983-1989
tags: ['短诗', '1989']
}
];
由于使用了getStaticPaths(),添加数据后Astro会自动生成对应的详情页路由。
/poetry 查看列表/poetry/new-poem-id// src/content/data.ts
export const poemTypes = [
{ key: 'short', label: '短诗' },
{ key: 'long', label: '长诗' },
{ key: 'sun', label: '太阳·七部书' },
{ key: 'essay', label: '文论' },
{ key: 'new-type', label: '新类型' }, // ← 添加新类型
];
无需修改页面代码,筛选选项会自动生成。
// src/content/data.ts
export const poemYears = [
'1983', '1984', '1985', '1986', '1987', '1988', '1989',
'1990' // ← 添加新年份
];
/* src/styles/global.css */
:root {
--colorPrimary: #600000; /* 改为其他颜色 */
}
所有使用var(--colorPrimary)的地方会自动更新。
/* src/styles/global.css */
:root {
--sidebarWidth: 320px; /* 改为其他宽度,如280px */
}
/* src/styles/pages/poetry-index.css */
.poem-item {
padding: var(--spacingMd) var(--spacingLg);
border-radius: var(--radiusSm);
/* 添加自定义样式 */
font-size: 18px;
font-weight: 500;
}
.poem-dot {
width: 8px; /* 改变圆点大小 */
height: 8px;
background: var(--colorPrimary);
}
全局变量:src/styles/global.css
页面专属样式:src/styles/pages/poetry-index.css
步骤1:在data.ts中定义标签数据
export const poemTags = ['爱情', '自然', '哲思', '生活'];
步骤2:在index.astro中添加筛选组
<div class="filter-group">
<button class="filter-group-toggle" data-group="tag">
<span>标签</span>
<svg class="toggle-icon">...</svg>
</button>
<div class="filter-options" data-group-content="tag">
{poemTags.map((tag) => (
<label class="filter-option">
<input type="checkbox" name="tag" value={tag} />
<span class="checkbox-custom"></span>
<span class="option-label">{tag}</span>
</label>
))}
</div>
</div>
步骤3:在filterPoems()中添加过滤逻辑
const selectedTags = Array.from(
document.querySelectorAll('input[name="tag"]:checked')
).map(input => input.value);
const tags = poem.getAttribute('data-tags')?.split(',') || [];
const matchesTag = selectedTags.length === 0 ||
selectedTags.some(tag => tags.includes(tag));
// 在最终判断中添加
if (matchesSearch && matchesType && matchesYear && matchesTag) {
poem.classList.remove('hidden');
} else {
poem.classList.add('hidden');
}
步骤4:在诗歌列表项中添加data属性
<a
href={`/poetry/${poem.id}`}
class="poem-item"
data-type={poem.type}
data-year={poem.year}
data-title={poem.title}
data-tags={poem.tags.join(',')} ← 新增
>
对于大量诗歌,频繁的过滤操作可能影响性能。
优化方案:添加防抖(debounce)
let debounceTimer;
searchInput?.addEventListener('input', () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(filterPoems, 300); // 300ms延迟
});
目前每个复选框都有独立监听器,数量多时可能影响性能。
优化方案:使用事件委托
// 替代为:
document.querySelector('.filter-sidebar')?.addEventListener('change', (e) => {
const target = e.target as HTMLInputElement;
if (target.type === 'checkbox') {
filterPoems();
}
});
如果诗歌数量超过1000首,可考虑虚拟滚动。
推荐库:
@tanstack/virtualreact-window(需要React集成)避免重排重绘:
/* 使用 transform 代替 top/left */
.poem-item {
transform: translateY(0);
transition: transform var(--transitionFast);
}
.poem-item:hover {
transform: translateY(-2px); /* 比修改 margin-top 更高效 */
}
检查清单:
type和year字段data-type和data-year属性是否正确设置原因:IME输入法可能触发多次input事件
解决方案:使用compositionend事件
let isComposing = false;
searchInput?.addEventListener('compositionstart', () => {
isComposing = true;
});
searchInput?.addEventListener('compositionend', () => {
isComposing = false;
filterPoems();
});
searchInput?.addEventListener('input', () => {
if (!isComposing) {
filterPoems();
}
});
解决方案:添加高度限制和滚动
@media (max-width: 768px) {
.filter-sidebar {
max-height: 50vh;
overflow-y: auto;
}
}
淡入动画:
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.poem-item {
animation: fadeIn 0.3s ease;
}
.poem-item.hidden {
display: none;
}
| 技术 | 用途 |
|---|---|
| Astro | 框架和SSG(静态站点生成) |
| TypeScript | 客户端脚本类型安全 |
| CSS Variables | 主题系统和设计令牌 |
| Flexbox | 响应式布局 |
| Media Queries | 断点适配 |
| Data Attributes | 元数据存储和DOM查询 |
| Event Delegation | 事件处理优化 |
文档版本:1.0 最后更新:2026年1月 维护者:海子诗歌网站开发团队