Files
notebook/src/layouts/ProseLayout.astro
T
secret-deus 15cdabb647
notebook-ci / build-push-deploy (push) Successful in 36s
style(notebook): flatten cards and add side physics doodles
Remove panel/card borders and shadows for a flatter reading layout.
Add SideDoodles with physics formulas/curves, random angles on load.
2026-07-16 20:01:52 +08:00

217 lines
4.6 KiB
Plaintext

---
import type { ImageMetadata } from 'astro';
import { Image } from 'astro:assets';
import type { PostEntry } from '../lib/content';
import BaseLayout from './BaseLayout.astro';
import FormattedDate from '../components/FormattedDate.astro';
import DenseEntryList from '../components/DenseEntryList.astro';
import { getPublishedPosts, getTagMap } from '../lib/content';
import { tagHref } from '../lib/tags';
import { folderHref } from '../lib/tree';
interface Props {
title: string;
description?: string;
pubDate?: Date;
updatedDate?: Date;
heroImage?: ImageMetadata;
tags?: string[];
kind?: 'post' | 'note';
currentId?: string;
related?: PostEntry[];
}
const {
title,
description = '',
pubDate,
updatedDate,
heroImage,
tags = [],
currentId,
related,
} = Astro.props;
const posts = await getPublishedPosts();
const tagMap = await getTagMap();
const others = posts.filter((e) => e.id !== currentId);
const relatedResolved =
related ??
(() => {
if (!tags.length) return others.slice(0, 8);
const scored = others
.map((e) => {
const et = new Set(e.data.tags ?? []);
const score = tags.reduce((n, t) => n + (et.has(String(t)) ? 1 : 0), 0);
return { e, score };
})
.filter((x) => x.score > 0)
.sort((a, b) => b.score - a.score)
.map((x) => x.e);
return (scored.length ? scored : others).slice(0, 8);
})();
const topTags = [...tagMap.entries()]
.sort((a, b) => b[1].length - a[1].length)
.slice(0, 8);
const parentFolder =
currentId && currentId.includes('/') ? currentId.split('/').slice(0, -1).join('/') : '';
---
<BaseLayout title={title} description={description} wide>
<div class="prose-board">
<article class="article">
{
heroImage && (
<div class="hero-image">
<Image width={1020} height={510} src={heroImage} alt="" />
</div>
)
}
<header class="article-head">
<p class="kind meta">文章</p>
<h1>{title}</h1>
{
(pubDate || updatedDate) && (
<p class="meta">
{pubDate && pubDate.valueOf() > 0 && <FormattedDate date={pubDate} />}
{updatedDate && (
<>
{pubDate && pubDate.valueOf() > 0 ? ' · ' : ''}
更新于 <FormattedDate date={updatedDate} />
</>
)}
</p>
)
}
{
tags.length > 0 && (
<div class="tag-row">
{tags.map((tag) => (
<a class="tag" href={tagHref(String(tag))}>
#{tag}
</a>
))}
</div>
)
}
</header>
<div class="prose">
<slot />
</div>
</article>
<aside class="board-side">
<section class="panel">
<div class="panel-head">
<h2>本页</h2>
</div>
<ul class="bullet-list">
<li>类型:文章</li>
{
pubDate && pubDate.valueOf() > 0 && (
<li>
发布:
<FormattedDate date={pubDate} />
</li>
)
}
{
updatedDate && (
<li>
更新:
<FormattedDate date={updatedDate} />
</li>
)
}
{
parentFolder && (
<li>
目录:
<a href={folderHref(parentFolder)}>{parentFolder}</a>
</li>
)
}
<li>
返回 <a href="/posts/">文章列表</a>
</li>
</ul>
</section>
{
tags.length > 0 && (
<section class="panel">
<div class="panel-head">
<h2>标签</h2>
</div>
<div class="tag-cloud">
{tags.map((tag) => (
<a class="tag" href={tagHref(String(tag))}>
#{tag}
</a>
))}
</div>
</section>
)
}
<section class="panel">
<div class="panel-head">
<h2>相关文章</h2>
<a href="/posts/">全部 →</a>
</div>
<DenseEntryList entries={relatedResolved} empty="暂无相关内容" showKind={false} />
</section>
{
topTags.length > 0 && (
<section class="panel">
<div class="panel-head">
<h2>热门标签</h2>
<a href="/tags/">全部 →</a>
</div>
<div class="tag-cloud">
{topTags.map(([tag, items]) => (
<a class="tag" href={tagHref(tag)}>
#{tag}
<span class="count">{items.length}</span>
</a>
))}
</div>
</section>
)
}
</aside>
</div>
</BaseLayout>
<style>
.article-head {
margin-bottom: 1.4rem;
padding-bottom: 0.95rem;
border-bottom: 1px solid rgb(var(--line));
animation: fade-up var(--dur-slow) var(--ease-out) both;
}
.article-head h1 {
margin-bottom: 0.35em;
}
.kind {
margin: 0 0 0.4rem;
letter-spacing: 0.04em;
font-size: 0.8em;
}
.hero-image {
margin: 0 0 1.1rem;
}
.hero-image :global(img) {
width: 100%;
display: block;
box-shadow: none;
}
.prose :global(> *:first-child) {
margin-top: 0;
}
</style>