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:
1 parent
3bd8369eda
commit
90574edce7
9 files changed
+180
-2
No files matched your search
@@ -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' : '中';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 内处理,此处再守一层:空墙不破构建
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -10,6 +10,7 @@ const dict = {
|
||||
nav_posts: { zh: '文章', en: 'Posts' },
|
||||
nav_gallery: { zh: '图库', en: 'Gallery' },
|
||||
nav_about: { zh: '关于', en: 'About' },
|
||||
nav_opensource: { zh: '开源', en: 'Open Source' },
|
||||
search_label: { zh: '搜索', en: 'Search' },
|
||||
search_placeholder: { zh: '搜索文章…(Esc 关闭)', en: 'Search posts… (Esc to close)' },
|
||||
search_hint: { zh: '索引由 Pagefind 构建期生成 · 静态站零后端', en: 'Index built by Pagefind · zero-backend static site' },
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
import BooksPage from '../components/pages/BooksPage.astro';
|
||||
---
|
||||
<BooksPage locale="zh" />
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
import BooksPage from '../../components/pages/BooksPage.astro';
|
||||
---
|
||||
<BooksPage locale="en" />
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
import ProjectWall from '../../components/geek/ProjectWall.astro';
|
||||
import Heatmap from '../../components/geek/Heatmap.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { t } from '../../i18n/ui';
|
||||
---
|
||||
<BaseLayout title="Open Source · wait4x" description="My open-source activity: repos, contributions, and this site's source">
|
||||
<h1 class="page-title">Open Source</h1>
|
||||
<p class="blurb">This site is itself open source: <a class="link" href="https://git.wait4x.online/wait4x/blog">git.wait4x.online/wait4x/blog</a> — the Astro SSG, dual-writing workflow, and the full build & deploy pipeline live in the repo. Below are all my public repositories on self-hosted gitea and GitHub, plus contributions from the past year.</p>
|
||||
<Heatmap labels={{
|
||||
title: t('heatmap_title', 'en'), summary: t('heatmap_summary', 'en'),
|
||||
less: t('heatmap_less', 'en'), more: t('heatmap_more', 'en'), note: t('heatmap_note', 'en'),
|
||||
}} />
|
||||
<ProjectWall limit={99} />
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.page-title { font-size: var(--text-h1); color: var(--ink-1); }
|
||||
.blurb { margin-top: var(--space-3); margin-bottom: var(--space-5); font-size: 15px; color: var(--ink-2); line-height: 1.7; max-width: 640px; }
|
||||
</style>
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
import ProjectWall from '../components/geek/ProjectWall.astro';
|
||||
import Heatmap from '../components/geek/Heatmap.astro';
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import { t } from '../i18n/ui';
|
||||
---
|
||||
<BaseLayout title="开源 · wait4x" description="我的开源活动:仓库、贡献与本站源码">
|
||||
<h1 class="page-title">开源<span class="count mono">Open Source</span></h1>
|
||||
<p class="blurb">本站本身即开源:<a class="link" href="https://git.wait4x.online/wait4x/blog">git.wait4x.online/wait4x/blog</a> —— Astro SSG、双写作流、构建发布全链皆在仓中。以下是我在自托管 gitea 与 GitHub 上的全部公开仓库,以及最近一年的贡献活动。</p>
|
||||
<Heatmap labels={{
|
||||
title: t('heatmap_title', 'zh'), summary: t('heatmap_summary', 'zh'),
|
||||
less: t('heatmap_less', 'zh'), more: t('heatmap_more', 'zh'), note: t('heatmap_note', 'zh'),
|
||||
}} />
|
||||
<ProjectWall limit={99} />
|
||||
</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-4); }
|
||||
.blurb { margin-top: var(--space-3); margin-bottom: var(--space-5); font-size: 15px; color: var(--ink-2); line-height: 1.7; max-width: 640px; }
|
||||
</style>
|
||||
Reference in new issue
Block a user