feat(books): 书架改版三方案预览页(临时路由)
用户要求「书架页多做出几个方案让我直接查看网页供我选择」,故先并陈三案:
A 竖排色脊墙(书脊 writing-mode 竖排、情绪色即是视觉线索)
B 封面网格(封面为主角,无封面用色脊占位 + 首字,悬停浮起并显出简介)
C 抽书阅读(一排书脊,点开原生 details 抽书,显示封面/简介/入口)
同一份数据渲染三案(posts category=reading),页顶 sticky 锚点跳转。
用户选定后:把选中方案落进 BooksPage.astro 并删除本临时路由。
验证: 构建 exit 0;dist/books-preview 27KB;Playwright 实测三案均渲染
(脊 5 / 卡 5 / 抽书行 5)、1280 与 390 均零横向溢出、破图 0
Spec: 2026-11-02_blog-5i-fonts-polish (T7 预览)
Co-authored-by: GLM-5.3
This commit is contained in:
1 parent
6293fe25d5
commit
907f64472a
1 file changed
+357
@@ -0,0 +1,357 @@
|
||||
---
|
||||
/**
|
||||
* 书架改版方案预览(Phase 5i T7):一页并陈三个方案,供用户直接点选。
|
||||
*
|
||||
* 为什么做成预览页而不是直接改 /books:书架是纯审美决策,改错要回滚一轮;
|
||||
* 一页三案 + 页顶锚点,用户在手机或桌面点开就能比,选定后我再把选中的方案
|
||||
* 落到 BooksPage.astro,然后删掉本预览路由(含 sitemap 排除与 noindex)。
|
||||
*
|
||||
* 三方案共同的数据契约与 BooksPage 一致:posts 里 category=reading 的条目,
|
||||
* 封面走 Astro image()(cover 缺省时用 mood 色脊兜底),description 缺省取正文首段。
|
||||
*/
|
||||
import { getCollection } from 'astro:content';
|
||||
import { Image } from 'astro:assets';
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import { sortPosts } from '../lib/post-order';
|
||||
|
||||
const books = sortPosts(
|
||||
await getCollection(
|
||||
'posts',
|
||||
({ data }) => !data.draft && data.category === 'reading' && !data.tags.includes('hidden'),
|
||||
),
|
||||
);
|
||||
|
||||
/** description 缺省回退正文首段(去 markdown 记号、截 60 字),与 BooksPage 同契约 */
|
||||
const firstPara = (body: string | undefined): string => {
|
||||
const line =
|
||||
(body ?? '')
|
||||
.split(/\n\s*\n/)
|
||||
.map((l) => l.trim())
|
||||
.find((l) => l && !l.startsWith('#') && !l.startsWith('---')) ?? '';
|
||||
return line.replace(/[*_>`[\]]/g, '').slice(0, 60);
|
||||
};
|
||||
|
||||
const items = books.map((b) => ({
|
||||
id: b.id,
|
||||
title: b.data.title,
|
||||
author: b.data.bookAuthor ?? '',
|
||||
desc: b.data.description ?? firstPara(b.body),
|
||||
cover: b.data.cover,
|
||||
mood: b.data.mood ?? 'var(--ink-4)',
|
||||
year: b.data.pubDate.getFullYear(),
|
||||
href: `/archives/${b.id}/`,
|
||||
}));
|
||||
const years = [...new Set(items.map((i) => i.year))].sort((a, b) => b - a);
|
||||
const withCover = items.filter((i) => i.cover).length;
|
||||
---
|
||||
|
||||
<BaseLayout track="reading" title="书架方案预览 · wait4x" description="三个书架改版方案的并陈预览">
|
||||
<!-- 临时预览路由:用户选定方案后连同本文件一起删除 -->
|
||||
<h1 class="pv-title">书架改版:三个方案</h1>
|
||||
<p class="pv-note">
|
||||
共 {items.length} 本书,其中 {withCover} 本有封面图。三个方案用同一份数据渲染,
|
||||
<strong>看完告诉我选哪个</strong>(回「A」「B」或「C」即可,也可以说「B 但标题字大一点」这类微调)。
|
||||
</p>
|
||||
|
||||
<nav class="pv-switch" aria-label="方案跳转">
|
||||
<a href="#va" class="pv-chip">A · 竖排色脊墙</a>
|
||||
<a href="#vb" class="pv-chip">B · 封面网格</a>
|
||||
<a href="#vc" class="pv-chip">C · 抽书阅读</a>
|
||||
</nav>
|
||||
|
||||
<!-- ══════════ 方案 A:竖排色脊墙 ══════════ -->
|
||||
<section id="va" class="pv-sec">
|
||||
<header class="pv-head">
|
||||
<span class="pv-tag mono">A</span>
|
||||
<h2>竖排色脊墙</h2>
|
||||
<p class="pv-pros">像真书架:书脊竖排、颜色即情绪。<b>优点</b>最像"书架";<b>代价</b>无封面的书看不出内容,标题长会挤。</p>
|
||||
</header>
|
||||
|
||||
{years.map((year) => (
|
||||
<div class="va-year">
|
||||
<p class="va-year-label mono">{year} · {items.filter((i) => i.year === year).length} 本</p>
|
||||
<ul class="va-wall">
|
||||
{items.filter((i) => i.year === year).map((b) => (
|
||||
<li>
|
||||
<a class="va-spine" href={b.href} style={`--mood:${b.mood}`} title={`${b.title}${b.author ? ' · ' + b.author : ''}`}>
|
||||
<span class="va-spine-title">{b.title}</span>
|
||||
{b.author && <span class="va-spine-author">{b.author}</span>}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<!-- ══════════ 方案 B:封面网格 ══════════ -->
|
||||
<section id="vb" class="pv-sec">
|
||||
<header class="pv-head">
|
||||
<span class="pv-tag mono">B</span>
|
||||
<h2>封面网格</h2>
|
||||
<p class="pv-pros">以封面为主角,悬停浮起并显出简介。<b>优点</b>视觉最抓人、信息最全;<b>兜底</b>无封面的书用色脊占位,不出现空洞卡。</p>
|
||||
</header>
|
||||
|
||||
{years.map((year) => (
|
||||
<div class="vb-year">
|
||||
<p class="vb-year-label mono">{year} · {items.filter((i) => i.year === year).length} 本</p>
|
||||
<ul class="vb-grid">
|
||||
{items.filter((i) => i.year === year).map((b) => (
|
||||
<li>
|
||||
<a class="vb-card" href={b.href} style={`--mood:${b.mood}`}>
|
||||
<span class="vb-art">
|
||||
{b.cover ? (
|
||||
<Image src={b.cover} alt={`${b.title} 封面`} widths={[220, 440]} sizes="(min-width: 900px) 200px, 45vw" loading="lazy" />
|
||||
) : (
|
||||
<span class="vb-placeholder">
|
||||
<i class="vb-stripe" aria-hidden="true" />
|
||||
<span class="vb-initial">{b.title.slice(0, 1)}</span>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span class="vb-body">
|
||||
<strong class="vb-title">{b.title}</strong>
|
||||
{b.author && <span class="vb-author">{b.author}</span>}
|
||||
<span class="vb-desc">{b.desc}</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<!-- ══════════ 方案 C:抽书阅读 ══════════ -->
|
||||
<section id="vc" class="pv-sec">
|
||||
<header class="pv-head">
|
||||
<span class="pv-tag mono">C</span>
|
||||
<h2>抽书阅读</h2>
|
||||
<p class="pv-pros">一排书脊,点哪本抽哪本,展开看简介与入口。<b>优点</b>交互最有"抽书"感、竖向省空间;<b>代价</b>要点一下才看到内容(零 JS,原生 details)。</p>
|
||||
</header>
|
||||
|
||||
<ul class="vc-list">
|
||||
{items.map((b) => (
|
||||
<li>
|
||||
<details class="vc-book" style={`--mood:${b.mood}`}>
|
||||
<summary class="vc-spine">
|
||||
<span class="vc-bar" aria-hidden="true" />
|
||||
<span class="vc-title">{b.title}</span>
|
||||
{b.author && <span class="vc-author">{b.author}</span>}
|
||||
<span class="vc-date mono">{b.year}</span>
|
||||
</summary>
|
||||
<div class="vc-body">
|
||||
{b.cover && (
|
||||
<span class="vc-cover">
|
||||
<Image src={b.cover} alt={`${b.title} 封面`} widths={[120, 240]} sizes="120px" loading="lazy" />
|
||||
</span>
|
||||
)}
|
||||
<div class="vc-info">
|
||||
<p>{b.desc}</p>
|
||||
<a class="vc-link" href={b.href}>读这篇书评 →</a>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
/* ── 预览页外壳(三个方案各自独立命名空间,互不串味) ── */
|
||||
.pv-title { font-size: var(--text-h1); color: var(--ink-1); margin-top: var(--space-5); }
|
||||
.pv-note { font-size: 14px; color: var(--ink-3); line-height: 1.7; max-width: var(--measure); margin-block: var(--space-2) var(--space-4); }
|
||||
.pv-switch {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2) 0;
|
||||
background: color-mix(in oklab, var(--bg) 92%, transparent);
|
||||
backdrop-filter: blur(6px);
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.pv-chip {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12.5px;
|
||||
color: var(--ink-2);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
padding: 4px 12px;
|
||||
transition: border-color var(--dur) var(--ease), color var(--dur) var(--ease);
|
||||
}
|
||||
.pv-chip:hover { border-color: var(--accent); color: var(--ink-1); }
|
||||
.pv-sec { margin-top: var(--space-6); scroll-margin-top: 56px; }
|
||||
.pv-head { border-top: 1px solid var(--line); padding-top: var(--space-3); }
|
||||
.pv-tag {
|
||||
display: inline-block;
|
||||
font-size: 11px;
|
||||
color: var(--ink-4);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 4px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
.pv-head h2 { font-size: 20px; color: var(--ink-1); margin: var(--space-1) 0 var(--space-1); }
|
||||
.pv-pros { font-size: 13px; color: var(--ink-3); line-height: 1.7; max-width: var(--measure); margin: 0 0 var(--space-4); }
|
||||
.pv-pros b { color: var(--ink-2); font-weight: 600; }
|
||||
|
||||
/* ══════════ A:竖排色脊(书脊真实感) ══════════ */
|
||||
.va-year + .va-year { margin-top: var(--space-4); }
|
||||
.va-year-label,
|
||||
.vb-year-label { font-size: 12px; color: var(--ink-4); letter-spacing: 0.06em; margin: 0 0 var(--space-2); }
|
||||
.va-wall {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.va-spine {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
width: 44px;
|
||||
height: 190px;
|
||||
padding: 10px 6px;
|
||||
border-radius: 3px 6px 6px 3px;
|
||||
background: color-mix(in oklab, var(--mood) 16%, var(--bg-soft));
|
||||
border: 1px solid color-mix(in oklab, var(--mood) 34%, var(--line));
|
||||
border-left: 5px solid var(--mood);
|
||||
transition: translate var(--dur) var(--ease), box-shadow var(--dur) var(--ease), border-color var(--dur) var(--ease);
|
||||
}
|
||||
.va-spine:hover {
|
||||
translate: 0 -6px;
|
||||
box-shadow: 0 10px 22px rgb(0 0 0 / 0.12);
|
||||
}
|
||||
.va-spine-title {
|
||||
writing-mode: vertical-rl;
|
||||
font-family: var(--font-serif);
|
||||
font-size: 13px;
|
||||
line-height: 1.15;
|
||||
color: var(--ink-1);
|
||||
max-height: 150px;
|
||||
overflow: hidden;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.va-spine-author {
|
||||
writing-mode: vertical-rl;
|
||||
font-size: 10px;
|
||||
color: var(--ink-4);
|
||||
max-height: 48px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ══════════ B:封面网格 ══════════ */
|
||||
.vb-year + .vb-year { margin-top: var(--space-5); }
|
||||
.vb-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: var(--space-3);
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.vb-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-img);
|
||||
transition: translate var(--dur) var(--ease), box-shadow var(--dur) var(--ease), border-color var(--dur) var(--ease);
|
||||
}
|
||||
.vb-card:hover {
|
||||
translate: 0 -3px;
|
||||
border-color: color-mix(in oklab, var(--mood) 45%, var(--line));
|
||||
box-shadow: 0 10px 24px rgb(0 0 0 / 0.10);
|
||||
}
|
||||
.vb-art {
|
||||
display: block;
|
||||
aspect-ratio: 3 / 4;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: color-mix(in oklab, var(--mood) 10%, var(--bg-soft));
|
||||
}
|
||||
.vb-art :global(img) { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
.vb-placeholder {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.vb-stripe {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 12px;
|
||||
background: var(--mood);
|
||||
}
|
||||
.vb-initial {
|
||||
font-family: var(--font-serif);
|
||||
font-size: 40px;
|
||||
color: color-mix(in oklab, var(--mood) 62%, var(--ink-2));
|
||||
opacity: 0.85;
|
||||
}
|
||||
.vb-body { display: flex; flex-direction: column; gap: 1px; }
|
||||
.vb-title { font-family: var(--font-serif); font-size: 14.5px; color: var(--ink-1); line-height: 1.35; }
|
||||
.vb-author { font-size: 11.5px; color: var(--ink-4); }
|
||||
.vb-desc {
|
||||
font-size: 12.5px;
|
||||
color: var(--ink-3);
|
||||
line-height: 1.55;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* ══════════ C:抽书阅读 ══════════ */
|
||||
.vc-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 6px; }
|
||||
.vc-book { border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
|
||||
.vc-spine {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
padding: 10px var(--space-3);
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
transition: background var(--dur) var(--ease);
|
||||
}
|
||||
.vc-spine::-webkit-details-marker { display: none; }
|
||||
.vc-spine:hover { background: var(--bg-soft); }
|
||||
.vc-bar { width: 6px; align-self: stretch; border-radius: 3px; background: var(--mood); flex: none; }
|
||||
.vc-title { font-family: var(--font-serif); font-size: 15px; color: var(--ink-1); }
|
||||
.vc-author { font-size: 12px; color: var(--ink-4); }
|
||||
.vc-date { margin-left: auto; font-size: 11.5px; color: var(--ink-4); }
|
||||
.vc-body {
|
||||
display: flex;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3);
|
||||
border-top: 1px dashed var(--line);
|
||||
background: var(--bg-soft);
|
||||
}
|
||||
.vc-cover { flex: none; width: 88px; border-radius: 4px; overflow: hidden; }
|
||||
.vc-cover :global(img) { width: 100%; height: auto; display: block; }
|
||||
.vc-info { min-width: 0; }
|
||||
.vc-info p { font-size: 13.5px; line-height: 1.7; color: var(--ink-2); margin: 0 0 var(--space-2); }
|
||||
.vc-link { font-size: 13px; color: var(--accent); }
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.va-spine { width: 38px; height: 160px; }
|
||||
.vb-grid { grid-template-columns: repeat(auto-fill, minmax(126px, 1fr)); gap: var(--space-2); }
|
||||
.vc-body { flex-direction: column; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.va-spine, .vb-card, .vc-spine { transition: none; }
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user