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:
1 parent
b8c5e1c86e
commit
d119d6b555
1 file changed
+83
-19
@@ -31,6 +31,8 @@ const firstPara = (body: string | undefined): string => {
|
|||||||
return line.replace(/[*_>`[\]]/g, '').slice(0, 60);
|
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) => ({
|
const items = books.map((b) => ({
|
||||||
id: b.id,
|
id: b.id,
|
||||||
title: b.data.title,
|
title: b.data.title,
|
||||||
@@ -40,6 +42,9 @@ 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}/`,
|
||||||
|
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 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;
|
||||||
@@ -74,8 +79,10 @@ const withCover = items.filter((i) => i.cover).length;
|
|||||||
{items.filter((i) => i.year === year).map((b) => (
|
{items.filter((i) => i.year === year).map((b) => (
|
||||||
<li>
|
<li>
|
||||||
<a class="va-spine" href={b.href} style={`--mood:${b.mood}`} title={`${b.title}${b.author ? ' · ' + b.author : ''}`}>
|
<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>
|
<span class="va-spine-title">{b.spineTitle}</span>
|
||||||
{b.author && <span class="va-spine-author">{b.author}</span>}
|
{b.spineAuthor && <span class="va-spine-author">{b.spineAuthor}</span>}
|
||||||
|
{/* 短标题脊上半段会空出一大块,用年份竖排填补(视觉复核:空脊读起来空洞) */}
|
||||||
|
<span class="va-spine-year mono">{String(b.year).slice(2)}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@@ -105,7 +112,8 @@ const withCover = items.filter((i) => i.cover).length;
|
|||||||
) : (
|
) : (
|
||||||
<span class="vb-placeholder">
|
<span class="vb-placeholder">
|
||||||
<i class="vb-stripe" aria-hidden="true" />
|
<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>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
@@ -133,12 +141,13 @@ 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}`}>
|
<details class="vc-book" style={`--mood:${b.mood}`} open={b === items[0]}>
|
||||||
<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.author && <span class="vc-author">{b.author}</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>
|
</summary>
|
||||||
<div class="vc-body">
|
<div class="vc-body">
|
||||||
{b.cover && (
|
{b.cover && (
|
||||||
@@ -207,9 +216,12 @@ const withCover = items.filter((i) => i.cover).length;
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
/* 上沿留出 hover 抬升余量(视觉复核:脊顶贴着容器上沿,位移会被裁) */
|
||||||
|
padding: 10px 0 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
|
/* 底部基线:让一排脊有"落在地上"的参照 */
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
.va-spine {
|
.va-spine {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -218,11 +230,14 @@ const withCover = items.filter((i) => i.cover).length;
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
width: 44px;
|
width: 44px;
|
||||||
height: 190px;
|
height: 190px;
|
||||||
padding: 10px 6px;
|
padding: 10px 6px 8px;
|
||||||
border-radius: 3px 6px 6px 3px;
|
border-radius: 3px 6px 6px 3px;
|
||||||
background: color-mix(in oklab, var(--mood) 16%, var(--bg-soft));
|
/* 最浅下限:情绪色偏淡时(如淡紫 #e6e0f5)脊会几乎消隐、墙被读断,
|
||||||
border: 1px solid color-mix(in oklab, var(--mood) 34%, var(--line));
|
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);
|
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);
|
transition: translate var(--dur) var(--ease), box-shadow var(--dur) var(--ease), border-color var(--dur) var(--ease);
|
||||||
}
|
}
|
||||||
.va-spine:hover {
|
.va-spine:hover {
|
||||||
@@ -243,15 +258,25 @@ const withCover = items.filter((i) => i.cover).length;
|
|||||||
writing-mode: vertical-rl;
|
writing-mode: vertical-rl;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: var(--ink-4);
|
color: var(--ink-4);
|
||||||
max-height: 48px;
|
max-height: 60px;
|
||||||
overflow: hidden;
|
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:封面网格 ══════════ */
|
/* ══════════ B:封面网格 ══════════ */
|
||||||
.vb-year + .vb-year { margin-top: var(--space-5); }
|
.vb-year + .vb-year { margin-top: var(--space-5); }
|
||||||
.vb-grid {
|
.vb-grid {
|
||||||
display: 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);
|
gap: var(--space-3);
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -260,6 +285,8 @@ const withCover = items.filter((i) => i.cover).length;
|
|||||||
.vb-card {
|
.vb-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
height: 100%;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
padding: var(--space-2);
|
padding: var(--space-2);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
@@ -295,14 +322,26 @@ const withCover = items.filter((i) => i.cover).length;
|
|||||||
width: 12px;
|
width: 12px;
|
||||||
background: var(--mood);
|
background: var(--mood);
|
||||||
}
|
}
|
||||||
.vb-initial {
|
.vb-vertical {
|
||||||
|
writing-mode: vertical-rl;
|
||||||
font-family: var(--font-serif);
|
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));
|
color: color-mix(in oklab, var(--mood) 62%, var(--ink-2));
|
||||||
opacity: 0.85;
|
|
||||||
}
|
}
|
||||||
.vb-body { display: flex; flex-direction: column; gap: 1px; }
|
.vb-body { display: flex; flex-direction: column; gap: 1px; flex: 1; }
|
||||||
.vb-title { font-family: var(--font-serif); font-size: 14.5px; color: var(--ink-1); line-height: 1.35; }
|
/* 标题定两行高:只 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-author { font-size: 11.5px; color: var(--ink-4); }
|
||||||
.vb-desc {
|
.vb-desc {
|
||||||
font-size: 12.5px;
|
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::-webkit-details-marker { display: none; }
|
||||||
.vc-spine:hover { background: var(--bg-soft); }
|
.vc-spine:hover { background: var(--bg-soft); }
|
||||||
.vc-bar { width: 6px; align-self: stretch; border-radius: 3px; background: var(--mood); flex: none; }
|
.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-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 {
|
.vc-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
@@ -347,7 +409,9 @@ const withCover = items.filter((i) => i.cover).length;
|
|||||||
.vc-link { font-size: 13px; color: var(--accent); }
|
.vc-link { font-size: 13px; color: var(--accent); }
|
||||||
|
|
||||||
@media (max-width: 560px) {
|
@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); }
|
.vb-grid { grid-template-columns: repeat(auto-fill, minmax(126px, 1fr)); gap: var(--space-2); }
|
||||||
.vc-body { flex-direction: column; }
|
.vc-body { flex-direction: column; }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user