评论区 侧边栏

This commit is contained in:
2026-06-18 16:03:38 +08:00 Unverified
parent ebaf187d89
commit ce4a201508
6 changed files with 368 additions and 7 deletions
+47 -3
View File
@@ -7,6 +7,11 @@ type TocItem = {
id: string; id: string;
}; };
type TocSection = {
heading: TocItem;
children: TocItem[];
};
interface Props { interface Props {
post: BlogPost; post: BlogPost;
toc: TocItem[]; toc: TocItem[];
@@ -15,6 +20,16 @@ interface Props {
const { post, toc, relatedPosts } = Astro.props; const { post, toc, relatedPosts } = Astro.props;
const avatar = 'https://free.picui.cn/free/2025/08/10/689845496a283.png'; 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="文章侧边栏"> <aside class="article-sidebar" aria-label="文章侧边栏">
@@ -27,6 +42,21 @@ const avatar = 'https://free.picui.cn/free/2025/08/10/689845496a283.png';
</div> </div>
</section> </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 && ( {toc.length > 0 && (
<section class="sidebar-card toc-card"> <section class="sidebar-card toc-card">
<h2> <h2>
@@ -34,9 +64,23 @@ const avatar = 'https://free.picui.cn/free/2025/08/10/689845496a283.png';
文章目录 文章目录
<em>{toc.length}</em> <em>{toc.length}</em>
</h2> </h2>
<nav class="toc-list" aria-label="文章目录"> <nav class="toc-list toc-tree" aria-label="文章目录" data-toc-tree>
{toc.map((item) => ( {tocSections.map((section, index) => (
<a class={`toc-depth-${item.depth}`} href={`#${item.id}`}>{item.text}</a> <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> </nav>
</section> </section>
+72
View File
@@ -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>
+11
View File
@@ -19,6 +19,7 @@ const currentYear = new Date().getFullYear();
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content={description} /> <meta name="description" content={description} />
<link rel="icon" href="/images/Bi.ico" /> <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> <title>{pageTitle}</title>
</head> </head>
<body> <body>
@@ -31,6 +32,13 @@ const currentYear = new Date().getFullYear();
<a href="/tags/">标签</a> <a href="/tags/">标签</a>
<a href="/about/">关于</a> <a href="/about/">关于</a>
<a href="/link/">友链</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> </div>
</nav> </nav>
</header> </header>
@@ -92,6 +100,9 @@ const currentYear = new Date().getFullYear();
</section> </section>
</div> </div>
</footer> </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> <script is:inline>
(() => { (() => {
const runtime = document.getElementById('site-runtime'); const runtime = document.getElementById('site-runtime');
+2
View File
@@ -13,6 +13,7 @@ export type BlogPost = MarkdownModule & {
tags: string[]; tags: string[];
categories: string[]; categories: string[];
summary: string; summary: string;
comments: boolean;
cover?: string; cover?: string;
href: string; href: string;
}; };
@@ -80,6 +81,7 @@ export function getAllPosts(): BlogPost[] {
tags: asArray(module.frontmatter.tags), tags: asArray(module.frontmatter.tags),
categories: asArray(module.frontmatter.categories), categories: asArray(module.frontmatter.categories),
summary: makeSummary(module), summary: makeSummary(module),
comments: module.frontmatter.comments !== false,
cover: typeof module.frontmatter.cover === 'string' ? module.frontmatter.cover : undefined, cover: typeof module.frontmatter.cover === 'string' ? module.frontmatter.cover : undefined,
href: `/posts/${slug}/`, href: `/posts/${slug}/`,
}; };
+48
View File
@@ -1,6 +1,7 @@
--- ---
import ArticleSidebar from '@/components/ArticleSidebar.astro'; import ArticleSidebar from '@/components/ArticleSidebar.astro';
import PostCopyrightCard from '@/components/PostCopyrightCard.astro'; import PostCopyrightCard from '@/components/PostCopyrightCard.astro';
import TwikooComments from '@/components/TwikooComments.astro';
import BaseLayout from '@/layouts/BaseLayout.astro'; import BaseLayout from '@/layouts/BaseLayout.astro';
import { getAllPosts } from '@/lib/posts'; import { getAllPosts } from '@/lib/posts';
@@ -73,6 +74,7 @@ const relatedPosts = allPosts
.filter((item) => item.slug !== post.slug) .filter((item) => item.slug !== post.slug)
.filter((item) => item.categories.some((category) => post.categories.includes(category))) .filter((item) => item.categories.some((category) => post.categories.includes(category)))
.slice(0, 8); .slice(0, 8);
const twikooEnvId = 'https://comment.biss.click';
--- ---
<BaseLayout title={post.title} description={post.summary}> <BaseLayout title={post.title} description={post.summary}>
@@ -122,6 +124,7 @@ const relatedPosts = allPosts
<Content /> <Content />
</div> </div>
<PostCopyrightCard post={post} /> <PostCopyrightCard post={post} />
{post.comments && <TwikooComments envId={twikooEnvId} path={post.href} title={post.title} />}
</article> </article>
<ArticleSidebar post={post} toc={toc} relatedPosts={relatedPosts} /> <ArticleSidebar post={post} toc={toc} relatedPosts={relatedPosts} />
</section> </section>
@@ -144,6 +147,51 @@ const relatedPosts = allPosts
if (item?.id && !heading.id) heading.id = item.id; 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'); const summaryText = document.querySelector('.article-summary-text');
if (!summaryText) return; if (!summaryText) return;
+188 -4
View File
@@ -109,7 +109,8 @@ a {
white-space: nowrap; white-space: nowrap;
} }
.nav-links a { .nav-links a,
.nav-search {
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 999px; border-radius: 999px;
padding: 7px 12px; padding: 7px 12px;
@@ -121,13 +122,36 @@ a {
transform 0.2s ease; 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-links a:hover,
.nav-search:hover,
.site-footer a:hover, .site-footer a:hover,
.section-heading a:hover { .section-heading a:hover {
color: var(--accent); color: var(--accent);
} }
.nav-links a:hover { .nav-links a:hover,
.nav-search:hover {
border-color: rgba(255, 255, 255, 0.62); border-color: rgba(255, 255, 255, 0.62);
background: rgba(255, 255, 255, 0.38); background: rgba(255, 255, 255, 0.38);
box-shadow: box-shadow:
@@ -646,6 +670,7 @@ a {
} }
.toc-list, .toc-list,
.article-tag-list,
.same-category-list { .same-category-list {
display: grid; display: grid;
gap: 6px; gap: 6px;
@@ -669,6 +694,7 @@ a {
} }
.toc-list a, .toc-list a,
.article-tag-list a,
.same-category-list a { .same-category-list a {
border-radius: 8px; border-radius: 8px;
padding: 9px 11px; padding: 9px 11px;
@@ -681,18 +707,108 @@ a {
} }
.toc-list a:hover, .toc-list a:hover,
.article-tag-list a:hover,
.same-category-list a:hover { .same-category-list a:hover {
color: #1f618d; color: #1f618d;
background: rgba(102, 200, 245, 0.16); background: rgba(102, 200, 245, 0.16);
transform: translateX(2px); 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 { .toc-depth-2 {
padding-left: 22px !important; padding-left: 22px !important;
} }
.toc-depth-3 { .toc-depth-3 {
padding-left: 34px !important; padding-left: 24px !important;
font-size: 0.9rem; font-size: 0.9rem;
} }
@@ -1290,6 +1406,73 @@ time {
transform: translateY(-1px); 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 { .archive-list {
margin-top: 24px; margin-top: 24px;
display: grid; display: grid;
@@ -1589,7 +1772,8 @@ time {
padding-bottom: 2px; padding-bottom: 2px;
} }
.nav-links a { .nav-links a,
.nav-search {
flex: 0 0 auto; flex: 0 0 auto;
} }