feat(books): 新增 D 方案(封面网格 × 抽书),C 改置顶默认展开
用户反馈「B 和 C 都不错,C 方案可以默认展开置顶的书目」——故:
- 新增方案 D:格子仍是 B 的封面网格,但每格用原生 details 抽开成详情卡,
置顶书默认展开、封面左上角带置顶角标;零 JS
- C 的默认展开从「第一行」改为「置顶书目」(frontmatter pinned: true,与文章
置顶同一字段同一排序契约),行内显示置顶徽标
- 当前没有书被置顶,预览页用最新一本演示并明示(真实使用由后台开关决定)
验证: 构建 53 页 exit 0;Playwright 实测 C/D 各自唯一展开项 = 置顶书(等待戈多)、
D 5 格 / 1 置顶 / 3 列 / 角标 1、1280 与 390 零横向溢出
Spec: 2026-11-02_blog-5i-fonts-polish (T7 预览迭代)
Co-authored-by: GLM-5.3
This commit is contained in:
1 parent
31fb52cb06
commit
18ea5b17b8
1 file changed
+127
-2
@@ -33,6 +33,15 @@ const firstPara = (body: string | undefined): string => {
|
|||||||
|
|
||||||
/** 竖排书脊的高度有限:中文书名超过 ~11 字就必然溢出,用确定性截断代替裁切 */
|
/** 竖排书脊的高度有限:中文书名超过 ~11 字就必然溢出,用确定性截断代替裁切 */
|
||||||
const clip = (t: string, n: number) => (t.length > n ? `${t.slice(0, n)}…` : t);
|
const clip = (t: string, n: number) => (t.length > n ? `${t.slice(0, n)}…` : t);
|
||||||
|
/**
|
||||||
|
* 置顶书目(用户 2026-11-03 的要求:「C 方案可以默认展开置顶的书目」)。
|
||||||
|
* 真实来源是 frontmatter 的 `pinned: true`(与文章置顶同一字段、同一排序契约)。
|
||||||
|
* 当前还没有书被置顶,预览里用最新一本演示该效果,并在页面上明示这是演示。
|
||||||
|
*/
|
||||||
|
const pinnedBook = books.find((b) => b.data.pinned === true) ?? books[0];
|
||||||
|
const pinnedId = pinnedBook?.id ?? '';
|
||||||
|
const pinnedIsDemo = !books.some((b) => b.data.pinned === true);
|
||||||
|
|
||||||
const items = books.map((b) => ({
|
const items = books.map((b) => ({
|
||||||
id: b.id,
|
id: b.id,
|
||||||
title: b.data.title,
|
title: b.data.title,
|
||||||
@@ -42,11 +51,14 @@ const items = books.map((b) => ({
|
|||||||
mood: b.data.mood ?? 'var(--ink-4)',
|
mood: b.data.mood ?? 'var(--ink-4)',
|
||||||
year: b.data.pubDate.getFullYear(),
|
year: b.data.pubDate.getFullYear(),
|
||||||
href: `/archives/${b.id}/`,
|
href: `/archives/${b.id}/`,
|
||||||
|
pinned: b.id === pinnedId,
|
||||||
spineTitle: clip(b.data.title, 11),
|
spineTitle: clip(b.data.title, 11),
|
||||||
spineAuthor: clip(b.data.bookAuthor ?? '', 4),
|
spineAuthor: clip(b.data.bookAuthor ?? '', 4),
|
||||||
short: clip(b.data.title, 3),
|
short: clip(b.data.title, 3),
|
||||||
}));
|
}));
|
||||||
const years = [...new Set(items.map((i) => i.year))].sort((a, b) => b - a);
|
const years = [...new Set(items.map((i) => i.year))].sort((a, b) => b - a);
|
||||||
|
|
||||||
|
|
||||||
const withCover = items.filter((i) => i.cover).length;
|
const withCover = items.filter((i) => i.cover).length;
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -55,13 +67,21 @@ const withCover = items.filter((i) => i.cover).length;
|
|||||||
<h1 class="pv-title">书架改版:三个方案</h1>
|
<h1 class="pv-title">书架改版:三个方案</h1>
|
||||||
<p class="pv-note">
|
<p class="pv-note">
|
||||||
共 {items.length} 本书,其中 {withCover} 本有封面图。三个方案用同一份数据渲染,
|
共 {items.length} 本书,其中 {withCover} 本有封面图。三个方案用同一份数据渲染,
|
||||||
<strong>看完告诉我选哪个</strong>(回「A」「B」或「C」即可,也可以说「B 但标题字大一点」这类微调)。
|
<strong>看完告诉我选哪个</strong>(回「A」「B」「C」或「D」即可,也可以说「B 但标题字大一点」这类微调)。
|
||||||
|
{pinnedIsDemo && (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
<span class="pv-demo">D 与 C 的「置顶默认展开」用<strong>最新一本</strong>做演示——真实使用时由该书的
|
||||||
|
`pinned: true` 决定(后台书评编辑页有开关)。</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<nav class="pv-switch" aria-label="方案跳转">
|
<nav class="pv-switch" aria-label="方案跳转">
|
||||||
<a href="#va" class="pv-chip">A · 竖排色脊墙</a>
|
<a href="#va" class="pv-chip">A · 竖排色脊墙</a>
|
||||||
<a href="#vb" class="pv-chip">B · 封面网格</a>
|
<a href="#vb" class="pv-chip">B · 封面网格</a>
|
||||||
<a href="#vc" class="pv-chip">C · 抽书阅读</a>
|
<a href="#vc" class="pv-chip">C · 抽书阅读</a>
|
||||||
|
<a href="#vd" class="pv-chip">D · 封面 + 抽书(推荐)</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- ══════════ 方案 A:竖排色脊墙 ══════════ -->
|
<!-- ══════════ 方案 A:竖排色脊墙 ══════════ -->
|
||||||
@@ -141,10 +161,11 @@ const withCover = items.filter((i) => i.cover).length;
|
|||||||
<ul class="vc-list">
|
<ul class="vc-list">
|
||||||
{items.map((b) => (
|
{items.map((b) => (
|
||||||
<li>
|
<li>
|
||||||
<details class="vc-book" style={`--mood:${b.mood}`} open={b === items[0]}>
|
<details class="vc-book" style={`--mood:${b.mood}`} open={b.pinned}>
|
||||||
<summary class="vc-spine">
|
<summary class="vc-spine">
|
||||||
<span class="vc-bar" aria-hidden="true" />
|
<span class="vc-bar" aria-hidden="true" />
|
||||||
<span class="vc-title">{b.title}</span>
|
<span class="vc-title">{b.title}</span>
|
||||||
|
{b.pinned && <span class="pin" title="置顶">置顶</span>}
|
||||||
{b.author && <span class="vc-author">{b.author}</span>}
|
{b.author && <span class="vc-author">{b.author}</span>}
|
||||||
{/* 展开暗示:没有它就只是 5 行灰字列表(视觉复核 P1) */}
|
{/* 展开暗示:没有它就只是 5 行灰字列表(视觉复核 P1) */}
|
||||||
<span class="vc-cue mono">封面与简介 ▾</span>
|
<span class="vc-cue mono">封面与简介 ▾</span>
|
||||||
@@ -165,6 +186,53 @@ const withCover = items.filter((i) => i.cover).length;
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
<!-- ══════════ 方案 D:封面网格 + 抽书(B 的视觉 × C 的交互) ══════════ -->
|
||||||
|
<section id="vd" class="pv-sec">
|
||||||
|
<header class="pv-head">
|
||||||
|
<span class="pv-tag mono">D</span>
|
||||||
|
<h2>封面 + 抽书</h2>
|
||||||
|
<p class="pv-pros">
|
||||||
|
格子还是 B 的封面网格,但每格可以直接点开成「抽出来」的详情卡(原生 details,零 JS);
|
||||||
|
<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="vd-grid">
|
||||||
|
{items.filter((i) => i.year === year).map((b) => (
|
||||||
|
<li class:list={['vd-cell', b.pinned && 'is-pinned']}>
|
||||||
|
<details class="vd-book" style={`--mood:${b.mood}`} open={b.pinned}>
|
||||||
|
<summary class="vd-sum">
|
||||||
|
<span class="vd-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-vertical">{b.short}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{b.pinned && <span class="vd-flag mono">置顶</span>}
|
||||||
|
</span>
|
||||||
|
<span class="vd-cap">
|
||||||
|
<strong class="vb-title">{b.title}</strong>
|
||||||
|
{b.author && <span class="vb-author">{b.author}</span>}
|
||||||
|
<span class="vd-cue mono">展开 ▾</span>
|
||||||
|
</span>
|
||||||
|
</summary>
|
||||||
|
<div class="vd-body">
|
||||||
|
<p>{b.desc}</p>
|
||||||
|
<a class="vc-link" href={b.href}>读这篇书评 →</a>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -408,6 +476,63 @@ const withCover = items.filter((i) => i.cover).length;
|
|||||||
.vc-info p { font-size: 13.5px; line-height: 1.7; color: var(--ink-2); margin: 0 0 var(--space-2); }
|
.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); }
|
.vc-link { font-size: 13px; color: var(--accent); }
|
||||||
|
|
||||||
|
/* ══════════ D:封面 + 抽书 ══════════ */
|
||||||
|
.vd-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
|
||||||
|
gap: var(--space-3);
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
.vd-book {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius-img);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
|
||||||
|
}
|
||||||
|
.vd-cell.is-pinned .vd-book { border-color: color-mix(in oklab, var(--mood) 50%, var(--line)); }
|
||||||
|
.vd-book:hover { box-shadow: 0 8px 22px rgb(0 0 0 / 0.10); }
|
||||||
|
.vd-sum {
|
||||||
|
display: block;
|
||||||
|
padding: var(--space-2);
|
||||||
|
cursor: pointer;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
.vd-sum::-webkit-details-marker { display: none; }
|
||||||
|
.vd-art {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
aspect-ratio: 3 / 4;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: color-mix(in oklab, var(--mood) 10%, var(--bg-soft));
|
||||||
|
}
|
||||||
|
.vd-art :global(img) { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||||
|
.vd-flag {
|
||||||
|
position: absolute;
|
||||||
|
left: 6px;
|
||||||
|
top: 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #fff;
|
||||||
|
background: color-mix(in oklab, var(--mood) 78%, #000);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 1px 5px;
|
||||||
|
}
|
||||||
|
.vd-cap { display: flex; flex-direction: column; gap: 1px; margin-top: var(--space-2); }
|
||||||
|
.vd-cue { font-size: 11px; color: var(--ink-4); margin-top: 2px; }
|
||||||
|
details[open] > .vd-sum .vd-cue::after { content: ''; }
|
||||||
|
details[open] > .vd-sum .vd-cue { color: var(--ink-3); }
|
||||||
|
.vd-body {
|
||||||
|
border-top: 1px dashed var(--line);
|
||||||
|
padding: var(--space-2);
|
||||||
|
background: var(--bg-soft);
|
||||||
|
}
|
||||||
|
.vd-body p { font-size: 12.5px; line-height: 1.6; color: var(--ink-2); margin: 0 0 var(--space-2); }
|
||||||
|
.pv-demo { display: inline-block; margin-top: var(--space-1); font-size: 12.5px; color: var(--ink-4); line-height: 1.6; }
|
||||||
|
.pv-demo strong { color: var(--ink-3); }
|
||||||
|
|
||||||
@media (max-width: 560px) {
|
@media (max-width: 560px) {
|
||||||
/* 移动端保持书脊比例(1:4 左右):压到 120px 会退化成矮胖色块 */
|
/* 移动端保持书脊比例(1:4 左右):压到 120px 会退化成矮胖色块 */
|
||||||
.va-spine { width: 40px; height: 172px; }
|
.va-spine { width: 40px; height: 172px; }
|
||||||
|
|||||||
Reference in new issue
Block a user