- BooksPage:色脊排版卡(mood 竖脊+文楷书名+作者+三行截断策展语)
× 年份分组规整网格;zh/en 双路由;阅读轨标题旁「书架 →」入口
- /opensource:全量仓墙(ProjectWall 加 limit prop,首页精选 6 不变)
+ 热力图 + 本站开源叙事段;导航第四项「开源」(i18n nav_opensource)
- 五书 frontmatter 策展字段(含泥潭=刘楚昕)
验证: build 28 页 exit 0;books/opensource zh/en 五路由产物断言过;
五书卡与作者 grep 在案;E2E 30/30 PASS
Spec: 2026-10-25_add-terminal-books-admin
Co-authored-by: GLM-5.3
155 lines
4.6 KiB
Plaintext
155 lines
4.6 KiB
Plaintext
---
|
|
// 年份分组 + 轨道分节的文章列表(首页与 /archives/ 共用)
|
|
// 轨道标签单一事实源:content.config 的 POST_TRACKS,此处不另立词表
|
|
import type { CollectionEntry } from 'astro:content';
|
|
import { POST_TRACKS, type PostCategory } from '../content.config';
|
|
import { t } from '../i18n/ui';
|
|
|
|
interface Props {
|
|
/** 调用方已完成 draft 过滤与 pubDate 倒序 */
|
|
posts: CollectionEntry<'posts'>[];
|
|
locale?: 'zh' | 'en';
|
|
/** slug → 译题(en 列表显示英文标题,视觉复核范围外观察) */
|
|
titleOverrides?: Record<string, string>;
|
|
}
|
|
const { posts, locale = 'zh', titleOverrides } = Astro.props;
|
|
// 轨道标签走字典(en 首页复用本组件时的双语位);zh 保持原词表语义
|
|
const TRACK_LABEL: Record<PostCategory, string> = {
|
|
reading: t('category_reading', locale),
|
|
tech: t('category_tech', locale),
|
|
photo: t('category_photo', locale),
|
|
};
|
|
const hrefOf = (id: string) => (locale === 'en' ? `/en/archives/${id}/` : `/archives/${id}/`);
|
|
|
|
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">
|
|
{TRACK_LABEL[cat]}
|
|
{cat === 'reading' && (
|
|
<a class="shelf-link" href={locale === 'en' ? '/en/books/' : '/books/'}>{locale === 'en' ? 'Books →' : '书架 →'}</a>
|
|
)}
|
|
</h3>
|
|
<ul>
|
|
{rows.map((post) => (
|
|
<li>
|
|
<a class="row tap" href={hrefOf(post.id)}>
|
|
{cat === 'reading' && (
|
|
<i
|
|
class="emo"
|
|
style={`background:${post.data.mood ?? 'var(--mood)'}`}
|
|
title="情绪色"
|
|
aria-hidden="true"
|
|
/>
|
|
)}
|
|
<span class="row-title">{titleOverrides?.[post.id] ?? 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;
|
|
}
|
|
.shelf-link { float: right; font-size: 12.5px; color: var(--accent); font-weight: 400; }
|
|
.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>
|