feat(books): 书架定稿方案 C——抽书阅读 + 多本置顶默认展开
用户拍板选 C,置顶标记改为向上箭头图标(不写文字),并确认支持多本置顶。
- 抽书列表:情绪色脊 + 定宽标题列 + 展开 chevron,details 原生展开看封面与简介
- 置顶:所有 pinned 书排最前且默认展开,标记换成 ↑+横线 图标
- 删除一次性预览路由 /books-preview/ 与其 lint 白名单
验证: 构建 exit 0;content-lint 53 页 0/0/0;Playwright 实测展开/箭头旋转/
作者列对齐/390 无溢出;两本临置顶实测均排前且同时展开
Spec: none (先行上线,完整 5i 包仍在 spec/2026-11-02_blog-5i-fonts-polish)
Co-authored-by: GLM-5.3
This commit is contained in:
1 parent
d6e08a29f9
commit
d096d45c58
5 files changed
+208
-679
No files matched your search
+17
-2
@@ -30,7 +30,8 @@
|
|||||||
|
|
||||||
语义边界(刻意如此,别当 bug 报):
|
语义边界(刻意如此,别当 bug 报):
|
||||||
- **只影响列表顺序**:`pubDate` 不变、URL 不变、RSS 里的时间序列不变,订阅者不会看到时间错乱
|
- **只影响列表顺序**:`pubDate` 不变、URL 不变、RSS 里的时间序列不变,订阅者不会看到时间错乱
|
||||||
- 多篇同时置顶时,置顶组内部仍按日期倒序
|
- **支持多篇/多本同时置顶**:置顶组内部仍按日期倒序;书架页(/books)里每个置顶的书评都会排在前面并**默认展开**
|
||||||
|
- 书架页用「↑ + 横线」的回到顶部图标表示置顶(不写文字),与右侧展开箭头形状不同,不会混读
|
||||||
- 关掉开关立即恢复时间序,没有"冷却期"
|
- 关掉开关立即恢复时间序,没有"冷却期"
|
||||||
|
|
||||||
## 瞬间(5h 起与文章同级)
|
## 瞬间(5h 起与文章同级)
|
||||||
@@ -39,12 +40,26 @@
|
|||||||
|
|
||||||
- markdown:`**加粗**`、`*斜体*`、列表、`> 引用`、`` `行内代码` ``
|
- markdown:`**加粗**`、`*斜体*`、列表、`> 引用`、`` `行内代码` ``
|
||||||
- 排版类 HTML:`<span class="c-blue font-kai">彩色 + 楷体</span>`(可用类清单见写作指南「文章排版类」一节,与文章完全同一份白名单)
|
- 排版类 HTML:`<span class="c-blue font-kai">彩色 + 楷体</span>`(可用类清单见写作指南「文章排版类」一节,与文章完全同一份白名单)
|
||||||
- 仍守外壳灰阶纪律:彩色是**强调**不是装饰,一页 ≤3 处
|
- 彩色**不再限次**(原「≤3 处/页」已取消);仍守外壳灰阶:导航/页脚/按钮/背景不染色
|
||||||
|
|
||||||
**每条瞬间自带评论区**:时间线卡片下方有「评论」折叠条,读者点开才加载评论(不点开不为它付任何加载成本)。中文页与英文页共用同一条评论流,语言切换不会丢评论。
|
**每条瞬间自带评论区**:时间线卡片下方有「评论」折叠条,读者点开才加载评论(不点开不为它付任何加载成本)。中文页与英文页共用同一条评论流,语言切换不会丢评论。
|
||||||
|
|
||||||
**每条的浏览 / 评论数字是只读的**:读者点开评论区不会虚增浏览数(只有整页访问才计一次)。
|
**每条的浏览 / 评论数字是只读的**:读者点开评论区不会虚增浏览数(只有整页访问才计一次)。
|
||||||
|
|
||||||
|
## 照片说明文字(灯箱右侧)支持排版(5i 新增)
|
||||||
|
|
||||||
|
照片的「说明」(caption)现在支持 markdown 与排版类,与文章正文同一套:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
caption: 落日把海与城一齐烫成金箔,**这一帧等了二十分钟**
|
||||||
|
```
|
||||||
|
|
||||||
|
```html
|
||||||
|
<span class="c-blue">蓝色</span>、<span class="font-kai">楷体</span> 这些类都能用。
|
||||||
|
```
|
||||||
|
|
||||||
|
灯箱是深底,颜色类会自动取柔和对暗底的那一档,不会刺眼。EXIF 那几行是相机数据,不做排版。
|
||||||
|
|
||||||
## 照片上传流
|
## 照片上传流
|
||||||
|
|
||||||
1. 图库照片 → New Entry:填标题(**必填**,空标题会让构建炸——实测坑)、选分组(street/landscape)、可写 alt
|
1. 图库照片 → New Entry:填标题(**必填**,空标题会让构建炸——实测坑)、选分组(street/landscape)、可写 alt
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ if (MODE === 'skip') {
|
|||||||
const SKIP_PREFIX = ['/admin/', '/photos-originals/']; // admin=CMS 路由;photos-originals=部署期 rsync 叠加层(不在 dist,另有部署链保障)
|
const SKIP_PREFIX = ['/admin/', '/photos-originals/']; // admin=CMS 路由;photos-originals=部署期 rsync 叠加层(不在 dist,另有部署链保障)
|
||||||
const SKIP_FILE = new Set(['/404.html']); // Astro 404 页不参与配对
|
const SKIP_FILE = new Set(['/404.html']); // Astro 404 页不参与配对
|
||||||
|
|
||||||
|
|
||||||
function walk(dir, out = []) {
|
function walk(dir, out = []) {
|
||||||
for (const e of readdirSync(dir)) {
|
for (const e of readdirSync(dir)) {
|
||||||
const p = join(dir, e);
|
const p = join(dir, e);
|
||||||
@@ -48,8 +49,8 @@ const fails = [];
|
|||||||
const warns = [];
|
const warns = [];
|
||||||
|
|
||||||
for (const f of files) {
|
for (const f of files) {
|
||||||
const html = readFileSync(f, 'utf8');
|
|
||||||
const rel = f.slice(DIST.length) || '/';
|
const rel = f.slice(DIST.length) || '/';
|
||||||
|
const html = readFileSync(f, 'utf8');
|
||||||
|
|
||||||
// 站内 <a href>:仅根相对路径;#锚点/mailto/tel/外部/data: 全跳过
|
// 站内 <a href>:仅根相对路径;#锚点/mailto/tel/外部/data: 全跳过
|
||||||
for (const m of html.matchAll(/<a\s[^>]*href="([^"]*)"/g)) {
|
for (const m of html.matchAll(/<a\s[^>]*href="([^"]*)"/g)) {
|
||||||
|
|||||||
@@ -1,74 +1,128 @@
|
|||||||
---
|
---
|
||||||
// 书评索引画廊(/books):色脊排版卡 × 年份分组规整网格
|
/**
|
||||||
// 预研方案 B:色脊天然安全(mood 深浅跨亮暗对比度风险回避),零 JS
|
* 书评索引(/books):方案 C「抽书阅读」(用户 2026-11-03 拍板定稿)。
|
||||||
|
*
|
||||||
|
* 为什么是这一版:五个方案里它是唯一在「书少、封面少」时也不露怯的——
|
||||||
|
* 一排放得下、长书名不裁、没有 4+1 孤儿位;点一行抽出封面与简介,靠原生
|
||||||
|
* details 零 JS。
|
||||||
|
*
|
||||||
|
* 置顶契约(与文章同一套,见 src/lib/post-order.ts):
|
||||||
|
* - frontmatter `pinned: true` → 排在最前(置顶优先 → 日期倒序)
|
||||||
|
* - **支持多本置顶**:所有 pinned 书都默认展开,不限于一本
|
||||||
|
* - 置顶标记用「↑ + 横线」的回到顶部图标,不写文字(用户要求);
|
||||||
|
* 展开/收起是另一个形状的 chevron,避免两个箭头混读
|
||||||
|
*/
|
||||||
import { getCollection } from 'astro:content';
|
import { getCollection } from 'astro:content';
|
||||||
|
import { Image } from 'astro:assets';
|
||||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||||
import { t } from '../../i18n/ui';
|
import { t } from '../../i18n/ui';
|
||||||
import type { Locale } from '../../lib/locale';
|
import type { Locale } from '../../lib/locale';
|
||||||
import { sortPosts } from '../../lib/post-order';
|
import { sortPosts } from '../../lib/post-order';
|
||||||
import { Image } from 'astro:assets';
|
|
||||||
|
|
||||||
interface Props { locale?: Locale }
|
interface Props {
|
||||||
|
locale?: Locale;
|
||||||
|
}
|
||||||
const { locale = 'zh' } = Astro.props;
|
const { locale = 'zh' } = Astro.props;
|
||||||
|
|
||||||
const books = sortPosts(
|
const books = sortPosts(
|
||||||
await getCollection('posts', ({ data }) => !data.draft && data.category === 'reading' && !data.tags.includes('hidden')),
|
await getCollection(
|
||||||
|
'posts',
|
||||||
|
({ data }) => !data.draft && data.category === 'reading' && !data.tags.includes('hidden'),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 回退契约(schema 注释声称而此前未实现——视觉复核抓到的缺口):
|
/**
|
||||||
// description 缺省取正文第一个非空段落(去 markdown 记号)前 60 字
|
* description 回退契约:缺省取正文第一个非空段落(去 markdown 记号)前 60 字。
|
||||||
|
* 与历史实现保持一致——书架卡不能出现空白简介。
|
||||||
|
*/
|
||||||
const firstPara = (body: string | undefined): string => {
|
const firstPara = (body: string | undefined): string => {
|
||||||
const line = (body ?? '').split(/\n\s*\n/).map((l) => l.trim()).find((l) => l && !l.startsWith('#') && !l.startsWith('---')) ?? '';
|
const line =
|
||||||
return line.replace(/[*_>`\[\]]/g, '').slice(0, 60);
|
(body ?? '')
|
||||||
|
.split(/\n\s*\n/)
|
||||||
|
.map((l) => l.trim())
|
||||||
|
.find((l) => l && !l.startsWith('#') && !l.startsWith('---')) ?? '';
|
||||||
|
return line.replace(/[*_>`[\]]/g, '').slice(0, 60);
|
||||||
};
|
};
|
||||||
const years = [...new Set(books.map((b) => b.data.pubDate.getFullYear()))].sort((a, b) => b - a);
|
|
||||||
|
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)',
|
||||||
|
pinned: b.data.pinned === true,
|
||||||
|
href: locale === 'en' ? `/en/archives/${b.id}/` : `/archives/${b.id}/`,
|
||||||
|
}));
|
||||||
|
const pinnedCount = items.filter((i) => i.pinned).length;
|
||||||
const L = {
|
const L = {
|
||||||
shelf: locale === 'zh' ? '书架' : 'Books',
|
shelf: t('books_title', locale),
|
||||||
count: (n: number) => (locale === 'zh' ? `${n} 本` : `${n} books`),
|
count: locale === 'zh' ? `${items.length} 本` : `${items.length} books`,
|
||||||
|
note:
|
||||||
|
locale === 'zh'
|
||||||
|
? '点一行展开封面与简介 · 置顶的书默认展开'
|
||||||
|
: 'Tap a row for the cover and blurb · pinned books start open',
|
||||||
|
read: locale === 'zh' ? '读这篇书评 →' : 'Read the review →',
|
||||||
|
pinned: t('pinned_label', locale),
|
||||||
|
expand: locale === 'zh' ? '展开封面与简介' : 'Expand cover and blurb',
|
||||||
};
|
};
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout track="reading" title={`${L.shelf} · wait4x`} description={locale === 'zh' ? '读过的书与一句策展' : 'Books read, one line each'}>
|
<BaseLayout
|
||||||
<h1 class="page-title">{L.shelf}<span class="count mono">{L.count(books.length)}</span></h1>
|
track="reading"
|
||||||
{years.map((year, yi) => (
|
title={`${L.shelf} · wait4x`}
|
||||||
<details class="year-group" open={yi === 0}>
|
description={locale === 'zh' ? '读过的书与一句策展' : 'Books read, one line each'}
|
||||||
<summary class="y-summary">
|
>
|
||||||
<span class="y-label mono">{year}</span>
|
<h1 class="page-title">
|
||||||
<span class="y-count mono">· {books.filter((b) => b.data.pubDate.getFullYear() === year).length} {locale === 'zh' ? '本' : 'books'}</span>
|
{L.shelf}<span class="count mono">{L.count}</span>
|
||||||
</summary>
|
{pinnedCount > 0 && <span class="pinned-count mono">{pinnedCount} {L.pinned}</span>}
|
||||||
<ul class="shelf">
|
</h1>
|
||||||
{books.filter((b) => b.data.pubDate.getFullYear() === year).map((b) => (
|
<p class="page-note">{L.note}</p>
|
||||||
<li>
|
|
||||||
<a
|
<ul class="books">
|
||||||
class={`book ${locale === 'en' ? 'en' : ''}`}
|
{items.map((b) => (
|
||||||
href={locale === 'en' ? `/en/archives/${b.id}/` : `/archives/${b.id}/`}
|
<li>
|
||||||
style={`--mood:${b.data.mood ?? 'var(--ink-4)'}`}
|
<details class="book" style={`--mood:${b.mood}`} open={b.pinned}>
|
||||||
>
|
<summary class="row" aria-label={`${b.title}${b.author ? ' · ' + b.author : ''} — ${L.expand}`}>
|
||||||
{b.data.cover ? (
|
<span class="bar" aria-hidden="true"></span>
|
||||||
|
<span class="title">{b.title}</span>
|
||||||
|
{b.pinned && (
|
||||||
|
<span class="pin" title={L.pinned} role="img" aria-label={L.pinned}>
|
||||||
|
{/* 回到顶部图标(↑ + 横线):不写文字,形状与右侧 chevron 区分 */}
|
||||||
|
<svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true">
|
||||||
|
<path d="M8 12.6V4.2M4.4 7.6 8 4l3.6 3.6" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M3.6 13.6h8.8" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{b.author && <span class="author">{b.author}</span>}
|
||||||
|
<span class="chev" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0 16 16" width="14" height="14">
|
||||||
|
<path d="M4 6.4 8 10.4l4-4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</summary>
|
||||||
|
<div class="body">
|
||||||
|
{b.cover && (
|
||||||
|
<span class="cover">
|
||||||
<Image
|
<Image
|
||||||
src={b.data.cover}
|
src={b.cover}
|
||||||
alt={`${b.data.title} 封面`}
|
alt={`${b.title}${locale === 'zh' ? ' 封面' : ' cover'}`}
|
||||||
class="bk-cover"
|
widths={[120, 240]}
|
||||||
widths={[96, 192]}
|
sizes="120px"
|
||||||
sizes="96px"
|
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
) : (
|
</span>
|
||||||
<i class="spine" aria-hidden="true"></i>
|
)}
|
||||||
)}
|
<div class="info">
|
||||||
<div class="bk-body">
|
{b.desc && <p>{b.desc}</p>}
|
||||||
<strong class="bk-title">{b.data.title}</strong>
|
<a class="link" href={b.href}>{L.read}</a>
|
||||||
{b.data.bookAuthor && <span class="bk-author">{b.data.bookAuthor}</span>}
|
</div>
|
||||||
{(b.data.description || firstPara(b.body)) && (
|
</div>
|
||||||
<span class="bk-desc">{b.data.description ?? firstPara(b.body)}</span>
|
</details>
|
||||||
)}
|
</li>
|
||||||
</div>
|
))}
|
||||||
</a>
|
</ul>
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</details>
|
|
||||||
))}
|
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -78,97 +132,101 @@ const L = {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
|
margin-top: var(--space-5);
|
||||||
}
|
}
|
||||||
.count { font-size: 13px; color: var(--ink-3); }
|
.count { font-size: 13px; color: var(--ink-3); }
|
||||||
.year-group { margin-top: var(--space-4); border-top: 1px solid var(--line); }
|
.pinned-count { font-size: 11.5px; color: var(--ink-4); }
|
||||||
.y-summary {
|
.page-note { font-size: 13px; color: var(--ink-4); margin-block: var(--space-1) var(--space-4); }
|
||||||
|
|
||||||
|
.books { list-style: none; padding: 0; margin: 0; display: grid; gap: 6px; }
|
||||||
|
.book {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: border-color var(--dur) var(--ease);
|
||||||
|
}
|
||||||
|
.book[open] { border-color: color-mix(in oklab, var(--mood) 42%, var(--line)); }
|
||||||
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: center;
|
||||||
gap: 0.4rem;
|
gap: var(--space-3);
|
||||||
padding: var(--space-2) 0;
|
padding: 10px var(--space-3);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
transition: background var(--dur) var(--ease);
|
||||||
}
|
}
|
||||||
.y-summary::-webkit-details-marker { display: none; }
|
.row::-webkit-details-marker { display: none; }
|
||||||
.y-summary::before {
|
.row:hover { background: var(--bg-soft); }
|
||||||
content: '▸';
|
/* 情绪色脊:贯通行的窄条(不是表单色标) */
|
||||||
font-size: 11px;
|
.bar {
|
||||||
color: var(--ink-4);
|
width: 8px;
|
||||||
transition: transform var(--dur) var(--ease);
|
|
||||||
transform-origin: 50% 50%;
|
|
||||||
display: inline-block;
|
|
||||||
width: 1em;
|
|
||||||
}
|
|
||||||
.year-group[open] > .y-summary::before { transform: rotate(90deg); }
|
|
||||||
.y-summary:hover .y-label { color: var(--ink-2); }
|
|
||||||
.y-count { font-size: 12px; color: var(--ink-4); }
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.y-summary::before { transition: none; }
|
|
||||||
}
|
|
||||||
.y-label { font-size: 16px; color: var(--ink-3); margin-bottom: 0; transition: color var(--dur) var(--ease); }
|
|
||||||
.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); }
|
|
||||||
/* 书封(content/posts/assets/books/<slug>.jpg):无图时回退色脊卡设计 */
|
|
||||||
.bk-cover {
|
|
||||||
width: 96px;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
flex: none;
|
margin: 2px 0;
|
||||||
border-right: 1px solid var(--paper-line);
|
border-radius: 4px;
|
||||||
}
|
|
||||||
.spine {
|
|
||||||
flex: none;
|
|
||||||
width: 10px;
|
|
||||||
background: var(--mood);
|
background: var(--mood);
|
||||||
border-radius: 10px 0 0 10px;
|
flex: none;
|
||||||
}
|
}
|
||||||
.bk-body {
|
.title {
|
||||||
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-family: var(--font-kai);
|
||||||
font-size: 17px;
|
font-size: 15.5px;
|
||||||
color: var(--paper-ink);
|
color: var(--paper-ink);
|
||||||
line-height: 1.35;
|
line-height: 1.4;
|
||||||
}
|
/* 定宽列:作者起点不随书名长短漂移 */
|
||||||
.en .bk-title { font-family: var(--font-serif); }
|
width: 11em;
|
||||||
.bk-author {
|
flex: none;
|
||||||
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;
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.pin {
|
||||||
|
display: inline-flex;
|
||||||
|
color: color-mix(in oklab, var(--mood) 62%, var(--ink-3));
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
.author { font-size: 12.5px; color: var(--ink-3); }
|
||||||
|
.author::before { content: '·'; margin-right: 6px; opacity: 0.5; }
|
||||||
|
.chev {
|
||||||
|
margin-left: auto;
|
||||||
|
display: inline-flex;
|
||||||
|
color: var(--ink-4);
|
||||||
|
transition: rotate var(--dur) var(--ease), color var(--dur) var(--ease);
|
||||||
|
}
|
||||||
|
.row:hover .chev { color: var(--ink-2); }
|
||||||
|
/* 展开时 chevron 转成向上,和左侧「回到顶部」图标形状不同,不会混读 */
|
||||||
|
details[open] > .row .chev { rotate: 180deg; }
|
||||||
|
|
||||||
|
.body {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: var(--space-3);
|
||||||
|
border-top: 1px dashed var(--line);
|
||||||
|
background: var(--bg-soft);
|
||||||
|
}
|
||||||
|
.cover { flex: none; width: 96px; border-radius: 4px; overflow: hidden; }
|
||||||
|
.cover :global(img) { width: 100%; height: auto; display: block; }
|
||||||
|
.info { min-width: 0; }
|
||||||
|
.info p {
|
||||||
|
font-size: 13.5px;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: var(--ink-2);
|
||||||
|
margin: 0 0 var(--space-2);
|
||||||
|
max-width: var(--measure);
|
||||||
|
}
|
||||||
|
.link {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-color: color-mix(in oklab, var(--accent) 35%, transparent);
|
||||||
|
text-underline-offset: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 620px) {
|
||||||
|
.title { width: auto; flex: 1 1 auto; min-width: 0; }
|
||||||
|
.author { display: none; }
|
||||||
|
.body { flex-direction: column; }
|
||||||
|
.cover { width: 88px; }
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.book, .row, .chev { transition: none; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -113,6 +113,7 @@ const dict = {
|
|||||||
// ===== F 车道:访问量小字(Artalk pv,组件内独立计数) =====
|
// ===== F 车道:访问量小字(Artalk pv,组件内独立计数) =====
|
||||||
pv_viewed: { zh: '人看过', en: 'views' },
|
pv_viewed: { zh: '人看过', en: 'views' },
|
||||||
pinned_label: { zh: '置顶', en: 'Pinned' },
|
pinned_label: { zh: '置顶', en: 'Pinned' },
|
||||||
|
books_title: { zh: '书架', en: 'Books' },
|
||||||
comment_count: { zh: '条评论', en: 'comments' },
|
comment_count: { zh: '条评论', en: 'comments' },
|
||||||
moment_comments: { zh: '评论', en: 'Comments' },
|
moment_comments: { zh: '评论', en: 'Comments' },
|
||||||
lb_hint: { zh: '滚轮缩放 · 双击放大 · Esc 关闭', en: 'Wheel to zoom · Double-click · Esc' },
|
lb_hint: { zh: '滚轮缩放 · 双击放大 · Esc 关闭', en: 'Wheel to zoom · Double-click · Esc' },
|
||||||
|
|||||||
@@ -1,546 +0,0 @@
|
|||||||
---
|
|
||||||
/**
|
|
||||||
* 书架改版方案预览(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);
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 竖排书脊的高度有限:中文书名超过 ~11 字就必然溢出,用确定性截断代替裁切 */
|
|
||||||
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) => ({
|
|
||||||
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}/`,
|
|
||||||
pinned: b.id === pinnedId,
|
|
||||||
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;
|
|
||||||
---
|
|
||||||
|
|
||||||
<BaseLayout track="reading" title="书架方案预览 · wait4x" description="三个书架改版方案的并陈预览">
|
|
||||||
<!-- 临时预览路由:用户选定方案后连同本文件一起删除 -->
|
|
||||||
<h1 class="pv-title">书架改版:三个方案</h1>
|
|
||||||
<p class="pv-note">
|
|
||||||
共 {items.length} 本书,其中 {withCover} 本有封面图。三个方案用同一份数据渲染,
|
|
||||||
<strong>看完告诉我选哪个</strong>(回「A」「B」「C」或「D」即可,也可以说「B 但标题字大一点」这类微调)。
|
|
||||||
{pinnedIsDemo && (
|
|
||||||
<>
|
|
||||||
<br />
|
|
||||||
<span class="pv-demo">D 与 C 的「置顶默认展开」用<strong>最新一本</strong>做演示——真实使用时由该书的
|
|
||||||
`pinned: true` 决定(后台书评编辑页有开关)。</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</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>
|
|
||||||
<a href="#vd" class="pv-chip">D · 封面 + 抽书(推荐)</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.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>
|
|
||||||
))}
|
|
||||||
</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" />
|
|
||||||
{/* 单字无识别价值 → 换竖排书名(2–3 字) */}
|
|
||||||
<span class="vb-vertical">{b.short}</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}`} open={b.pinned}>
|
|
||||||
<summary class="vc-spine">
|
|
||||||
<span class="vc-bar" aria-hidden="true" />
|
|
||||||
<span class="vc-title">{b.title}</span>
|
|
||||||
{b.pinned && <span class="pin" title="置顶">置顶</span>}
|
|
||||||
{b.author && <span class="vc-author">{b.author}</span>}
|
|
||||||
{/* 展开暗示:没有它就只是 5 行灰字列表(视觉复核 P1) */}
|
|
||||||
<span class="vc-cue mono">封面与简介 ▾</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>
|
|
||||||
<!-- ══════════ 方案 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>
|
|
||||||
|
|
||||||
<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;
|
|
||||||
/* 上沿留出 hover 抬升余量(视觉复核:脊顶贴着容器上沿,位移会被裁) */
|
|
||||||
padding: 10px 0 0;
|
|
||||||
margin: 0;
|
|
||||||
align-items: flex-end;
|
|
||||||
/* 底部基线:让一排脊有"落在地上"的参照 */
|
|
||||||
border-bottom: 1px solid var(--line);
|
|
||||||
}
|
|
||||||
.va-spine {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
width: 44px;
|
|
||||||
height: 190px;
|
|
||||||
padding: 10px 6px 8px;
|
|
||||||
border-radius: 3px 6px 6px 3px;
|
|
||||||
/* 最浅下限:情绪色偏淡时(如淡紫 #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 {
|
|
||||||
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: 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;
|
|
||||||
/* 3 列:5 本书排成 3+2,避免 4+1 的孤儿位(视觉复核);卡片也更大、封面更可读 */
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
|
|
||||||
gap: var(--space-3);
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.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);
|
|
||||||
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-vertical {
|
|
||||||
writing-mode: vertical-rl;
|
|
||||||
font-family: var(--font-serif);
|
|
||||||
font-size: 19px;
|
|
||||||
letter-spacing: 0.16em;
|
|
||||||
color: color-mix(in oklab, var(--mood) 62%, var(--ink-2));
|
|
||||||
}
|
|
||||||
.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;
|
|
||||||
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);
|
|
||||||
/* 定宽 + 省略:作者列起点不再随书名长短漂移(视觉复核的第二列锯齿) */
|
|
||||||
width: 11em;
|
|
||||||
flex: none;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.vc-author { font-size: 12px; 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);
|
|
||||||
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); }
|
|
||||||
|
|
||||||
/* ══════════ 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) {
|
|
||||||
/* 移动端保持书脊比例(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; }
|
|
||||||
}
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.va-spine, .vb-card, .vc-spine { transition: none; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
Reference in new issue
Block a user