feat(books): 书架三案按视觉复核修订后更新预览页

长标题/作者裁切、卡片参差、4+1 孤儿位、抽书页无展开暗示等缺陷已修,
用户可直接在 /books-preview/ 看修订版。

Spec: none (预览页随选型迭代,正式改动在 spec/2026-11-02_blog-5i-fonts-polish)

Co-authored-by: GLM-5.3
This commit is contained in:
wait4xandGLM-5.3 committed 2026-11-03 11:55:33 +08:00
1 parent b8c5e1c86e
commit d119d6b555
1 file changed
+83 -19
+83 -19
View File
@@ -31,6 +31,8 @@ const firstPara = (body: string | undefined): string => {
return line.replace(/[*_>`[\]]/g, '').slice(0, 60);
};
/** 竖排书脊的高度有限:中文书名超过 ~11 字就必然溢出,用确定性截断代替裁切 */
const clip = (t: string, n: number) => (t.length > n ? `${t.slice(0, n)}…` : t);
const items = books.map((b) => ({
id: b.id,
title: b.data.title,
@@ -40,6 +42,9 @@ const items = books.map((b) => ({
mood: b.data.mood ?? 'var(--ink-4)',
year: b.data.pubDate.getFullYear(),
href: `/archives/${b.id}/`,
spineTitle: clip(b.data.title, 11),
spineAuthor: clip(b.data.bookAuthor ?? '', 4),
short: clip(b.data.title, 3),
}));
const years = [...new Set(items.map((i) => i.year))].sort((a, b) => b - a);
const withCover = items.filter((i) => i.cover).length;
@@ -74,8 +79,10 @@ const withCover = items.filter((i) => i.cover).length;
{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>}
<span class="va-spine-title">{b.spineTitle}</span>
{b.spineAuthor && <span class="va-spine-author">{b.spineAuthor}</span>}
{/* 短标题脊上半段会空出一大块,用年份竖排填补(视觉复核:空脊读起来空洞) */}
<span class="va-spine-year mono">{String(b.year).slice(2)}</span>
</a>
</li>
))}
@@ -105,7 +112,8 @@ const withCover = items.filter((i) => i.cover).length;
) : (
<span class="vb-placeholder">
<i class="vb-stripe" aria-hidden="true" />
<span class="vb-initial">{b.title.slice(0, 1)}</span>
{/* 单字无识别价值 → 换竖排书名(2–3 字) */}
<span class="vb-vertical">{b.short}</span>
</span>
)}
</span>
@@ -133,12 +141,13 @@ const withCover = items.filter((i) => i.cover).length;
<ul class="vc-list">
{items.map((b) => (
<li>
<details class="vc-book" style={`--mood:${b.mood}`}>
<details class="vc-book" style={`--mood:${b.mood}`} open={b === items[0]}>
<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>
{/* 展开暗示:没有它就只是 5 行灰字列表(视觉复核 P1) */}
<span class="vc-cue mono">封面与简介 ▾</span>
</summary>
<div class="vc-body">
{b.cover && (
@@ -207,9 +216,12 @@ const withCover = items.filter((i) => i.cover).length;
flex-wrap: wrap;
gap: 6px;
list-style: none;
padding: 0;
/* 上沿留出 hover 抬升余量(视觉复核:脊顶贴着容器上沿,位移会被裁) */
padding: 10px 0 0;
margin: 0;
align-items: flex-end;
/* 底部基线:让一排脊有"落在地上"的参照 */
border-bottom: 1px solid var(--line);
}
.va-spine {
position: relative;
@@ -218,11 +230,14 @@ const withCover = items.filter((i) => i.cover).length;
justify-content: space-between;
width: 44px;
height: 190px;
padding: 10px 6px;
padding: 10px 6px 8px;
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));
/* 最浅下限:情绪色偏淡时(如淡紫 #e6e0f5)脊会几乎消隐、墙被读断,
26% 混色 + 内描边把它拉回可见范围 */
background: color-mix(in oklab, var(--mood) 26%, var(--bg-soft));
border: 1px solid color-mix(in oklab, var(--mood) 44%, var(--line));
border-left: 5px solid var(--mood);
box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--mood) 18%, transparent);
transition: translate var(--dur) var(--ease), box-shadow var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.va-spine:hover {
@@ -243,15 +258,25 @@ const withCover = items.filter((i) => i.cover).length;
writing-mode: vertical-rl;
font-size: 10px;
color: var(--ink-4);
max-height: 48px;
max-height: 60px;
overflow: hidden;
/* 超长作者不再硬挤第二列(视觉复核:挤成第二窄列被 40px 脊宽裁掉);
Astro 侧已按 5 字截断,此处高度只兜极端字宽 */
white-space: nowrap;
}
.va-spine-year {
writing-mode: vertical-rl;
font-size: 9.5px;
color: color-mix(in oklab, var(--ink-4) 70%, transparent);
letter-spacing: 0.1em;
}
/* ══════════ B:封面网格 ══════════ */
.vb-year + .vb-year { margin-top: var(--space-5); }
.vb-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
/* 3 列:5 本书排成 3+2,避免 4+1 的孤儿位(视觉复核);卡片也更大、封面更可读 */
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
gap: var(--space-3);
list-style: none;
padding: 0;
@@ -260,6 +285,8 @@ const withCover = items.filter((i) => i.cover).length;
.vb-card {
display: flex;
flex-direction: column;
align-items: stretch;
height: 100%;
gap: var(--space-2);
padding: var(--space-2);
border: 1px solid var(--line);
@@ -295,14 +322,26 @@ const withCover = items.filter((i) => i.cover).length;
width: 12px;
background: var(--mood);
}
.vb-initial {
.vb-vertical {
writing-mode: vertical-rl;
font-family: var(--font-serif);
font-size: 40px;
font-size: 19px;
letter-spacing: 0.16em;
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-body { display: flex; flex-direction: column; gap: 1px; flex: 1; }
/* 标题定两行高:只 1 行的卡不会把作者/描述顶上移,同排基线才齐(视觉复核抓到的锯齿) */
.vb-title {
font-family: var(--font-serif);
font-size: 14.5px;
color: var(--ink-1);
line-height: 1.35;
min-height: calc(1.35em * 2);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.vb-author { font-size: 11.5px; color: var(--ink-4); }
.vb-desc {
font-size: 12.5px;
@@ -330,9 +369,32 @@ const withCover = items.filter((i) => i.cover).length;
.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-title {
font-family: var(--font-serif);
font-size: 15px;
color: var(--ink-1);
/* 定宽 + 省略:作者列起点不再随书名长短漂移(视觉复核的第二列锯齿) */
width: 11em;
flex: none;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.vc-author { font-size: 12px; color: var(--ink-4); }
.vc-date { margin-left: auto; font-size: 11.5px; color: var(--ink-4); }
.vc-author::before { content: '·'; margin-right: 6px; opacity: 0.5; }
.vc-cue {
margin-left: auto;
font-size: 11.5px;
color: var(--ink-4);
white-space: nowrap;
transition: color var(--dur) var(--ease);
}
.vc-spine:hover .vc-cue { color: var(--accent); }
details[open] > .vc-spine .vc-cue { color: var(--ink-3); }
details[open] > .vc-spine .vc-cue::after { content: ''; }
/* 色条改成贯通行高 80% 的窄脊,不再像表单色标 */
.vc-bar { width: 8px !important; margin: 4px 0; }
.vc-body {
display: flex;
gap: var(--space-3);
@@ -347,7 +409,9 @@ const withCover = items.filter((i) => i.cover).length;
.vc-link { font-size: 13px; color: var(--accent); }
@media (max-width: 560px) {
.va-spine { width: 38px; height: 160px; }
/* 移动端保持书脊比例(1:4 左右):压到 120px 会退化成矮胖色块 */
.va-spine { width: 40px; height: 172px; }
.va-wall { padding-top: 8px; }
.vb-grid { grid-template-columns: repeat(auto-fill, minmax(126px, 1fr)); gap: var(--space-2); }
.vc-body { flex-direction: column; }
}