feat(pages): 五页型落地——首页/文章双模板/图库 masonry/瞬间/关于

- 首页:名片+身份陈述空槽+按年分组分轨列表(情绪色/mono标签)+影像格+项目墙
- 文章 /archives/{slug}(与 Halo 旧 URL 1:1):reading→衬线纸墨 / tech→
  TOC 侧栏(≥1120 sticky)+2px 进度条+Artalk 容器占位
- 图库:grid masonry(构建期 aspect-ratio 零 JS)+原生 dialog lightbox
  (←→/Esc+EXIF 底栏)+分组筛选
- 瞬间时间线 / 关于(三轨版图+构建说明)
- sharp ^0.35.5 入依赖(Astro 7 不内置,astro:assets 缩略图管线前提)
- 裸 blockquote 默认吃 A 墨线样式(markdown 原生输出无类名的缝隙)
- 车道 30min 超时,产物完整,主会话补 sharp+断言脚本递归修正后收编

验证: pnpm astro build exit 0(7 页);五路由产物断言过;
      scripts/check-responsive.sh 11/11 ok(1120/820/760/560 全档)

Spec: 2026-10-24_add-blog-skeleton
Co-authored-by: GLM-5.3
This commit is contained in:
wait4xandGLM-5.3 committed 2026-10-25 00:11:34 +08:00
1 parent 80ff67e8fa
commit 945a9ee610
14 files changed
+1281 -18

No files matched your search

+42
View File
@@ -0,0 +1,42 @@
---
// 名片头像占位:内联 SVG 渐变 60px;真实头像接入后整体替换本组件即可
---
<svg
class="avatar"
viewBox="0 0 60 60"
width="60"
height="60"
role="img"
aria-label="头像占位"
>
<defs>
<linearGradient id="avatar-grad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="var(--mood-3)"></stop>
<stop offset="1" stop-color="var(--accent)"></stop>
</linearGradient>
</defs>
<rect width="60" height="60" rx="14" fill="url(#avatar-grad)"></rect>
<circle cx="44" cy="15" r="10" fill="#fff" opacity=".25"></circle>
<path d="M0 46 20 28 34 40 46 32 60 46 60 60 0 60Z" fill="#000" opacity=".14"></path>
<text
x="30"
y="38"
text-anchor="middle"
font-size="16"
font-weight="700"
fill="#fff"
opacity=".92"
font-family="var(--font-mono)"
>
w4
</text>
</svg>
<style>
.avatar {
flex: none;
border-radius: 14px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
</style>
+27
View File
@@ -0,0 +1,27 @@
---
// 名片区(首页/关于共用):渐变头像占位 + 称呼;图标行等内容由使用页经 slot 注入
import Avatar from './Avatar.astro';
---
<div class="idcard">
<Avatar />
<div class="idcard-main">
<p class="idcard-name">wait4x</p>
<!-- 身份陈述:用户后续撰写(一句话自我介绍,落在称呼下方) -->
<slot />
</div>
</div>
<style>
.idcard {
display: flex;
align-items: center;
gap: var(--space-4);
margin-block: var(--space-5);
}
.idcard-name {
font-size: 21px;
font-weight: 700;
color: var(--ink-1);
}
</style>
+137
View File
@@ -0,0 +1,137 @@
---
// 年份分组 + 轨道分节的文章列表(首页与 /archives/ 共用)
// 轨道标签单一事实源:content.config 的 POST_TRACKS,此处不另立词表
import type { CollectionEntry } from 'astro:content';
import { POST_TRACKS, trackOf, type PostCategory } from '../content.config';
interface Props {
/** 调用方已完成 draft 过滤与 pubDate 倒序 */
posts: CollectionEntry<'posts'>[];
}
const { posts } = Astro.props;
const byYear = new Map<number, CollectionEntry<'posts'>[]>();
for (const post of posts) {
const year = post.data.pubDate.getUTCFullYear();
const bucket = byYear.get(year);
if (bucket) bucket.push(post);
else byYear.set(year, [post]);
}
const mmdd = (d: Date) =>
`${String(d.getUTCMonth() + 1).padStart(2, '0')}-${String(d.getUTCDate()).padStart(2, '0')}`;
---
<div class="post-list">
{[...byYear.entries()].map(([year, list]) => (
<section class="year-block" aria-label={`${year} 年文章`}>
<h2 class="year">{year}</h2>
{(Object.keys(POST_TRACKS) as PostCategory[]).map((cat) => {
const rows = list.filter((post) => post.data.category === cat);
if (rows.length === 0) return null;
return (
<div class="track-block">
<h3 class="track">{trackOf(cat)}</h3>
<ul>
{rows.map((post) => (
<li>
<a class="row tap" href={`/archives/${post.id}/`}>
{cat === 'reading' && (
<i
class="emo"
style={`background:${post.data.mood ?? 'var(--mood)'}`}
title="情绪色"
aria-hidden="true"
/>
)}
<span class="row-title">{post.data.title}</span>
{cat !== 'reading' &&
post.data.tags.map((tag) => <code class="tag">{`#${tag}`}</code>)}
<time class="row-date" datetime={post.data.pubDate.toISOString()}>
{mmdd(post.data.pubDate)}
</time>
</a>
</li>
))}
</ul>
</div>
);
})}
</section>
))}
</div>
<style>
.year-block + .year-block { margin-top: var(--space-5); }
.year {
font-size: 15px;
font-weight: 600;
color: var(--ink-3);
font-variant-numeric: tabular-nums;
}
.track {
display: flex;
align-items: center;
gap: var(--space-2);
margin: var(--space-3) 0 var(--space-1);
font-size: 12.5px;
font-weight: 400;
color: var(--ink-4);
letter-spacing: 0.06em;
}
.track::before {
content: '';
width: 7px;
height: 7px;
border-radius: 2px;
background: var(--ink-4);
}
ul {
list-style: none;
margin: 0;
padding: 0;
}
.row {
display: flex;
align-items: center;
gap: var(--space-3);
width: 100%;
border-bottom: 1px solid var(--line);
}
li:last-child > .row { border-bottom: none; }
.emo {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 3px;
flex: none;
}
.row-title {
flex: 1;
min-width: 0;
font-size: 16px;
color: var(--ink-2);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: color var(--dur) var(--ease);
}
.row:hover .row-title { color: var(--accent); }
.tag {
flex: none;
font-family: var(--font-mono);
font-size: 12px;
color: var(--ink-3);
background: var(--bg-soft);
border-radius: 5px;
padding: 1px 7px;
white-space: nowrap;
}
.row-date {
flex: none;
font-family: var(--font-mono);
font-size: 13px;
color: var(--ink-4);
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
</style>