feat(pages): /books 书架与 /opensource 开源页——导航第四项(T2/T5)

- 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
This commit is contained in:
wait4xandGLM-5.3 committed 2026-10-25 13:46:56 +08:00
1 parent 3bd8369eda
commit 90574edce7
9 files changed
+180 -2

No files matched your search

+1
View File
@@ -13,6 +13,7 @@ const nav = [
{ href: '/archives/', label: t('nav_posts', curLocale) },
{ href: '/gallery/', label: t('nav_gallery', curLocale) },
{ href: '/about/', label: t('nav_about', curLocale) },
{ href: '/opensource/', label: t('nav_opensource', curLocale) },
];
const switchHref = localePath(path, curLocale === 'zh' ? 'en' : 'zh');
const switchLabel = curLocale === 'zh' ? 'EN' : '中';
+7 -1
View File
@@ -41,7 +41,12 @@ const mmdd = (d: Date) =>
if (rows.length === 0) return null;
return (
<div class="track-block">
<h3 class="track">{TRACK_LABEL[cat]}</h3>
<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>
@@ -79,6 +84,7 @@ const mmdd = (d: Date) =>
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;
+6 -1
View File
@@ -3,6 +3,11 @@
// 双源数据见 src/lib/projects.ts;语言色点用 linguist 常用色映射
import { loadProjects, type RepoCard } from '../../lib/projects';
interface Props {
/** 展示上限:首页精选 6;/opensource 全量传大数 */
limit?: number;
}
const { limit = 6 } = Astro.props;
const LANG_COLORS: Record<string, string> = {
Astro: '#ff5a03', TypeScript: '#3178c6', JavaScript: '#f1e05a',
Python: '#3572a5', Go: '#00add8', Rust: '#dea584', Shell: '#89e051',
@@ -11,7 +16,7 @@ const LANG_COLORS: Record<string, string> = {
let projects: RepoCard[] = [];
try {
projects = await loadProjects();
projects = (await loadProjects()).slice(0, limit);
} catch {
projects = []; // 兜底已在 lib 内处理,此处再守一层:空墙不破构建
}
+116
View File
@@ -0,0 +1,116 @@
---
// 书评索引画廊(/books):色脊排版卡 × 年份分组规整网格
// 预研方案 B:色脊天然安全(mood 深浅跨亮暗对比度风险回避),零 JS
import { getCollection } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import { t } from '../../i18n/ui';
import type { Locale } from '../../lib/locale';
interface Props { locale?: Locale }
const { locale = 'zh' } = Astro.props;
const books = (await getCollection('posts', ({ data }) => !data.draft && data.category === 'reading' && !data.tags.includes('hidden')))
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
const years = [...new Set(books.map((b) => b.data.pubDate.getFullYear()))].sort((a, b) => b - a);
const L = {
shelf: locale === 'zh' ? '书架' : 'Books',
count: (n: number) => (locale === 'zh' ? `${n} 本` : `${n} books`),
};
---
<BaseLayout title={`${L.shelf} · wait4x`} description={locale === 'zh' ? '读过的书与一句策展' : 'Books read, one line each'}>
<h1 class="page-title">{L.shelf}<span class="count mono">{L.count(books.length)}</span></h1>
{years.map((year) => (
<section class="year-group">
<h2 class="y-label mono">{year}</h2>
<ul class="shelf">
{books.filter((b) => b.data.pubDate.getFullYear() === year).map((b) => (
<li>
<a
class={`book ${locale === 'en' ? 'en' : ''}`}
href={locale === 'en' ? `/en/archives/${b.id}/` : `/archives/${b.id}/`}
style={`--mood:${b.data.mood ?? 'var(--ink-4)'}`}
>
<i class="spine" aria-hidden="true"></i>
<div class="bk-body">
<strong class="bk-title">{b.data.title}</strong>
{b.data.bookAuthor && <span class="bk-author">{b.data.bookAuthor}</span>}
{b.data.description && <span class="bk-desc">{b.data.description}</span>}
</div>
</a>
</li>
))}
</ul>
</section>
))}
</BaseLayout>
<style>
.page-title {
font-size: var(--text-h1);
color: var(--ink-1);
display: flex;
align-items: baseline;
gap: var(--space-3);
}
.count { font-size: 13px; color: var(--ink-3); }
.year-group { margin-top: var(--space-5); }
.y-label { font-size: 16px; color: var(--ink-3); margin-bottom: var(--space-3); }
.shelf {
list-style: none;
padding: 0;
margin: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: var(--space-3);
}
/* 色脊卡:纸底 + mood 竖脊 + 文楷书名;hover 边框转 mood(.repo 同语言) */
.book {
display: flex;
min-height: 120px;
background: var(--paper);
border: 1px solid var(--paper-line);
border-radius: 10px;
overflow: hidden;
text-decoration: none;
transition: border-color var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.book:hover { border-color: var(--mood); transform: translateY(-2px); }
.spine {
flex: none;
width: 10px;
background: var(--mood);
border-radius: 10px 0 0 10px;
}
.bk-body {
display: flex;
flex-direction: column;
gap: 3px;
padding: var(--space-3) var(--space-4);
min-width: 0;
}
.bk-title {
font-family: var(--font-kai);
font-size: 17px;
color: var(--paper-ink);
line-height: 1.35;
}
.en .bk-title { font-family: var(--font-serif); }
.bk-author {
font-size: 12.5px;
color: var(--ink-3);
font-family: var(--font-sans);
}
.bk-desc {
margin-top: 4px;
font-size: 13px;
color: var(--ink-2);
font-family: var(--font-sans);
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>