更换到Astro #9
@@ -7,6 +7,11 @@ type TocItem = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
type TocSection = {
|
||||
heading: TocItem;
|
||||
children: TocItem[];
|
||||
};
|
||||
|
||||
interface Props {
|
||||
post: BlogPost;
|
||||
toc: TocItem[];
|
||||
@@ -15,6 +20,16 @@ interface Props {
|
||||
|
||||
const { post, toc, relatedPosts } = Astro.props;
|
||||
const avatar = 'https://free.picui.cn/free/2025/08/10/689845496a283.png';
|
||||
const topDepth = toc.length > 0 ? Math.min(...toc.map((item) => item.depth)) : 1;
|
||||
const tocSections = toc.reduce<TocSection[]>((sections, item) => {
|
||||
if (item.depth <= topDepth || sections.length === 0) {
|
||||
sections.push({ heading: item, children: [] });
|
||||
} else {
|
||||
sections[sections.length - 1].children.push(item);
|
||||
}
|
||||
|
||||
return sections;
|
||||
}, []);
|
||||
---
|
||||
|
||||
<aside class="article-sidebar" aria-label="文章侧边栏">
|
||||
@@ -27,6 +42,21 @@ const avatar = 'https://free.picui.cn/free/2025/08/10/689845496a283.png';
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{post.tags.length > 0 && (
|
||||
<section class="sidebar-card article-tags-card">
|
||||
<h2>
|
||||
<span aria-hidden="true">#</span>
|
||||
文章标签
|
||||
<em>{post.tags.length}</em>
|
||||
</h2>
|
||||
<div class="article-tag-list" aria-label="文章标签">
|
||||
{post.tags.map((tag) => (
|
||||
<a href={`/tags/${encodeURIComponent(tag)}/`}>#{tag}</a>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{toc.length > 0 && (
|
||||
<section class="sidebar-card toc-card">
|
||||
<h2>
|
||||
@@ -34,9 +64,23 @@ const avatar = 'https://free.picui.cn/free/2025/08/10/689845496a283.png';
|
||||
文章目录
|
||||
<em>{toc.length}</em>
|
||||
</h2>
|
||||
<nav class="toc-list" aria-label="文章目录">
|
||||
{toc.map((item) => (
|
||||
<a class={`toc-depth-${item.depth}`} href={`#${item.id}`}>{item.text}</a>
|
||||
<nav class="toc-list toc-tree" aria-label="文章目录" data-toc-tree>
|
||||
{tocSections.map((section, index) => (
|
||||
<div class:list={['toc-group', index === 0 && 'is-open']} data-toc-group data-heading-id={section.heading.id}>
|
||||
<a class="toc-link toc-link-main" href={`#${section.heading.id}`} data-toc-link data-toc-top>
|
||||
<span class="toc-marker" aria-hidden="true"></span>
|
||||
<span>{section.heading.text}</span>
|
||||
</a>
|
||||
{section.children.length > 0 && (
|
||||
<div class="toc-children" data-toc-children>
|
||||
{section.children.map((item) => (
|
||||
<a class={`toc-link toc-link-child toc-depth-${item.depth}`} href={`#${item.id}`} data-toc-link>
|
||||
<span>{item.text}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
interface Props {
|
||||
envId: string;
|
||||
path: string;
|
||||
title: string;
|
||||
region?: string;
|
||||
}
|
||||
|
||||
const { envId, path, title, region = '' } = Astro.props;
|
||||
---
|
||||
|
||||
<section
|
||||
id="post-comment"
|
||||
class="twikoo-comments"
|
||||
aria-label="Comments"
|
||||
data-env-id={envId}
|
||||
data-region={region}
|
||||
data-path={path}
|
||||
>
|
||||
<div class="twikoo-comments-head">
|
||||
<span class="twikoo-comments-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M21 12a8 8 0 0 1-8 8H7l-4 3v-5a8 8 0 1 1 18-6Z" />
|
||||
</svg>
|
||||
</span>
|
||||
<div>
|
||||
<h2>Comments</h2>
|
||||
<p>{title}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="twikoo-wrap" class="twikoo-wrap"></div>
|
||||
</section>
|
||||
|
||||
<script is:inline>
|
||||
(() => {
|
||||
const root = document.getElementById('post-comment');
|
||||
const wrap = document.getElementById('twikoo-wrap');
|
||||
if (!root || !wrap || root.dataset.twikooLoaded === 'true') return;
|
||||
|
||||
const envId = root.dataset.envId;
|
||||
const path = root.dataset.path || window.location.pathname;
|
||||
const region = root.dataset.region || undefined;
|
||||
if (!envId) return;
|
||||
|
||||
root.dataset.twikooLoaded = 'true';
|
||||
|
||||
const initTwikoo = () => {
|
||||
if (!window.twikoo || typeof window.twikoo.init !== 'function') return;
|
||||
|
||||
window.twikoo.init({
|
||||
el: wrap,
|
||||
envId,
|
||||
region,
|
||||
path,
|
||||
});
|
||||
};
|
||||
|
||||
if (window.twikoo) {
|
||||
initTwikoo();
|
||||
return;
|
||||
}
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/twikoo@1.7.7/dist/twikoo.min.js';
|
||||
script.defer = true;
|
||||
script.onload = initTwikoo;
|
||||
script.onerror = () => {
|
||||
root.dataset.twikooLoaded = 'false';
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
})();
|
||||
</script>
|
||||
@@ -19,6 +19,7 @@ const currentYear = new Date().getFullYear();
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content={description} />
|
||||
<link rel="icon" href="/images/Bi.ico" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdmirror.com/npm/instantsearch.css/themes/reset-min.css" />
|
||||
<title>{pageTitle}</title>
|
||||
</head>
|
||||
<body>
|
||||
@@ -31,6 +32,13 @@ const currentYear = new Date().getFullYear();
|
||||
<a href="/tags/">标签</a>
|
||||
<a href="/about/">关于</a>
|
||||
<a href="/link/">友链</a>
|
||||
<button class="nav-search search-typesense-trigger" type="button" aria-label="搜索" title="搜索">
|
||||
<svg aria-hidden="true" viewBox="0 0 24 24">
|
||||
<path d="m21 21-4.3-4.3" />
|
||||
<circle cx="11" cy="11" r="7" />
|
||||
</svg>
|
||||
<span>搜索</span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
@@ -92,6 +100,9 @@ const currentYear = new Date().getFullYear();
|
||||
</section>
|
||||
</div>
|
||||
</footer>
|
||||
<script is:inline src="https://cdn.jsdmirror.com/npm/instantsearch.js@4.56.0"></script>
|
||||
<script is:inline src="https://cdn.jsdmirror.com/npm/typesense-instantsearch-adapter@2.7.0/dist/typesense-instantsearch-adapter.min.js"></script>
|
||||
<script is:inline src="/js/search/typesense-search.js"></script>
|
||||
<script is:inline>
|
||||
(() => {
|
||||
const runtime = document.getElementById('site-runtime');
|
||||
|
||||
@@ -13,6 +13,7 @@ export type BlogPost = MarkdownModule & {
|
||||
tags: string[];
|
||||
categories: string[];
|
||||
summary: string;
|
||||
comments: boolean;
|
||||
cover?: string;
|
||||
href: string;
|
||||
};
|
||||
@@ -80,6 +81,7 @@ export function getAllPosts(): BlogPost[] {
|
||||
tags: asArray(module.frontmatter.tags),
|
||||
categories: asArray(module.frontmatter.categories),
|
||||
summary: makeSummary(module),
|
||||
comments: module.frontmatter.comments !== false,
|
||||
cover: typeof module.frontmatter.cover === 'string' ? module.frontmatter.cover : undefined,
|
||||
href: `/posts/${slug}/`,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
import ArticleSidebar from '@/components/ArticleSidebar.astro';
|
||||
import PostCopyrightCard from '@/components/PostCopyrightCard.astro';
|
||||
import TwikooComments from '@/components/TwikooComments.astro';
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import { getAllPosts } from '@/lib/posts';
|
||||
|
||||
@@ -73,6 +74,7 @@ const relatedPosts = allPosts
|
||||
.filter((item) => item.slug !== post.slug)
|
||||
.filter((item) => item.categories.some((category) => post.categories.includes(category)))
|
||||
.slice(0, 8);
|
||||
const twikooEnvId = 'https://comment.biss.click';
|
||||
---
|
||||
|
||||
<BaseLayout title={post.title} description={post.summary}>
|
||||
@@ -122,6 +124,7 @@ const relatedPosts = allPosts
|
||||
<Content />
|
||||
</div>
|
||||
<PostCopyrightCard post={post} />
|
||||
{post.comments && <TwikooComments envId={twikooEnvId} path={post.href} title={post.title} />}
|
||||
</article>
|
||||
<ArticleSidebar post={post} toc={toc} relatedPosts={relatedPosts} />
|
||||
</section>
|
||||
@@ -144,6 +147,51 @@ const relatedPosts = allPosts
|
||||
if (item?.id && !heading.id) heading.id = item.id;
|
||||
});
|
||||
|
||||
const tocTree = document.querySelector('[data-toc-tree]');
|
||||
if (tocTree && toc.length > 0) {
|
||||
const links = [...tocTree.querySelectorAll('[data-toc-link]')];
|
||||
const groups = [...tocTree.querySelectorAll('[data-toc-group]')];
|
||||
const headings = toc
|
||||
.map((item) => document.getElementById(item.id))
|
||||
.filter(Boolean);
|
||||
|
||||
const setActiveToc = (id) => {
|
||||
links.forEach((link) => {
|
||||
const isActive = link.getAttribute('href') === `#${id}`;
|
||||
link.classList.toggle('is-active', isActive);
|
||||
if (isActive) link.setAttribute('aria-current', 'location');
|
||||
else link.removeAttribute('aria-current');
|
||||
});
|
||||
|
||||
const activeLink = tocTree.querySelector(`[data-toc-link][href="#${CSS.escape(id)}"]`);
|
||||
const activeGroup = activeLink?.closest('[data-toc-group]');
|
||||
groups.forEach((group) => group.classList.toggle('is-open', group === activeGroup));
|
||||
};
|
||||
|
||||
const updateToc = () => {
|
||||
const anchorOffset = 132;
|
||||
let activeId = headings[0]?.id;
|
||||
|
||||
headings.forEach((heading) => {
|
||||
if (heading.getBoundingClientRect().top <= anchorOffset) activeId = heading.id;
|
||||
});
|
||||
|
||||
if (activeId) setActiveToc(activeId);
|
||||
};
|
||||
|
||||
tocTree.addEventListener('click', (event) => {
|
||||
const link = event.target.closest?.('[data-toc-link]');
|
||||
if (!link) return;
|
||||
|
||||
const group = link.closest('[data-toc-group]');
|
||||
groups.forEach((item) => item.classList.toggle('is-open', item === group));
|
||||
});
|
||||
|
||||
updateToc();
|
||||
window.addEventListener('scroll', updateToc, { passive: true });
|
||||
window.addEventListener('resize', updateToc);
|
||||
}
|
||||
|
||||
const summaryText = document.querySelector('.article-summary-text');
|
||||
if (!summaryText) return;
|
||||
|
||||
|
||||
+188
-4
@@ -109,7 +109,8 @@ a {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
.nav-links a,
|
||||
.nav-search {
|
||||
border: 1px solid transparent;
|
||||
border-radius: 999px;
|
||||
padding: 7px 12px;
|
||||
@@ -121,13 +122,36 @@ a {
|
||||
transform 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-search {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
line-height: inherit;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav-search svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.nav-links a:hover,
|
||||
.nav-search:hover,
|
||||
.site-footer a:hover,
|
||||
.section-heading a:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.nav-links a:hover {
|
||||
.nav-links a:hover,
|
||||
.nav-search:hover {
|
||||
border-color: rgba(255, 255, 255, 0.62);
|
||||
background: rgba(255, 255, 255, 0.38);
|
||||
box-shadow:
|
||||
@@ -646,6 +670,7 @@ a {
|
||||
}
|
||||
|
||||
.toc-list,
|
||||
.article-tag-list,
|
||||
.same-category-list {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
@@ -669,6 +694,7 @@ a {
|
||||
}
|
||||
|
||||
.toc-list a,
|
||||
.article-tag-list a,
|
||||
.same-category-list a {
|
||||
border-radius: 8px;
|
||||
padding: 9px 11px;
|
||||
@@ -681,18 +707,108 @@ a {
|
||||
}
|
||||
|
||||
.toc-list a:hover,
|
||||
.article-tag-list a:hover,
|
||||
.same-category-list a:hover {
|
||||
color: #1f618d;
|
||||
background: rgba(102, 200, 245, 0.16);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.toc-tree {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.toc-group {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.toc-link {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 9px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.toc-link-main {
|
||||
color: #3e5664 !important;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.toc-marker {
|
||||
flex: 0 0 auto;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-top: 0.52em;
|
||||
border-radius: 999px;
|
||||
background: rgba(102, 200, 245, 0.52);
|
||||
box-shadow: 0 0 0 4px rgba(102, 200, 245, 0.12);
|
||||
}
|
||||
|
||||
.toc-children {
|
||||
position: relative;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
max-height: 0;
|
||||
margin-left: 14px;
|
||||
overflow: hidden;
|
||||
border-left: 1px solid rgba(102, 200, 245, 0.24);
|
||||
padding-left: 10px;
|
||||
opacity: 0;
|
||||
transition:
|
||||
max-height 0.28s ease,
|
||||
opacity 0.22s ease,
|
||||
margin-top 0.22s ease;
|
||||
}
|
||||
|
||||
.toc-group.is-open .toc-children {
|
||||
max-height: 520px;
|
||||
margin-top: 4px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.toc-link-child {
|
||||
color: #657384 !important;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.toc-link.is-active {
|
||||
color: #0f766e !important;
|
||||
background: rgba(102, 200, 245, 0.16);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.toc-group.is-open .toc-link-main .toc-marker,
|
||||
.toc-link.is-active .toc-marker {
|
||||
background: #3eb8be;
|
||||
box-shadow: 0 0 0 4px rgba(62, 184, 190, 0.16);
|
||||
}
|
||||
|
||||
.article-tag-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.article-tag-list a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 34px;
|
||||
border: 1px solid rgba(15, 118, 110, 0.18);
|
||||
padding: 6px 11px;
|
||||
color: var(--accent);
|
||||
font-size: 0.88rem;
|
||||
font-weight: 700;
|
||||
background: rgba(236, 253, 245, 0.66);
|
||||
}
|
||||
|
||||
.toc-depth-2 {
|
||||
padding-left: 22px !important;
|
||||
}
|
||||
|
||||
.toc-depth-3 {
|
||||
padding-left: 34px !important;
|
||||
padding-left: 24px !important;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@@ -1290,6 +1406,73 @@ time {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.twikoo-comments {
|
||||
margin-top: 34px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.64);
|
||||
border-radius: 8px;
|
||||
padding: clamp(20px, 4vw, 30px);
|
||||
background:
|
||||
radial-gradient(circle at 14% 0%, rgba(255, 255, 255, 0.8), transparent 32%),
|
||||
linear-gradient(135deg, rgba(255, 253, 248, 0.8), rgba(236, 253, 245, 0.55)),
|
||||
rgba(255, 253, 248, 0.76);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.86),
|
||||
0 16px 42px rgba(58, 51, 38, 0.12);
|
||||
backdrop-filter: blur(18px) saturate(165%);
|
||||
-webkit-backdrop-filter: blur(18px) saturate(165%);
|
||||
}
|
||||
|
||||
.twikoo-comments-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.twikoo-comments-icon {
|
||||
display: grid;
|
||||
flex: 0 0 auto;
|
||||
place-items: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
background: #3eb8be;
|
||||
box-shadow: 0 12px 28px rgba(15, 118, 110, 0.16);
|
||||
}
|
||||
|
||||
.twikoo-comments-icon svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-width: 2.2;
|
||||
}
|
||||
|
||||
.twikoo-comments h2 {
|
||||
margin: 0;
|
||||
color: #27313a;
|
||||
font-size: 1.28rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.twikoo-comments p {
|
||||
margin: 4px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.twikoo-wrap {
|
||||
min-height: 160px;
|
||||
}
|
||||
|
||||
.twikoo-comments :where(.tk-comments-container, .tk-submit) {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.archive-list {
|
||||
margin-top: 24px;
|
||||
display: grid;
|
||||
@@ -1589,7 +1772,8 @@ time {
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
.nav-links a,
|
||||
.nav-search {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user