2025-03-03 21:16:16 +08:00
|
|
|
|
---
|
2025-04-19 16:19:39 +08:00
|
|
|
|
import { getCollection, render } from "astro:content";
|
|
|
|
|
import { getSpecialPath } from "@/content.config";
|
|
|
|
|
import Layout from "@/components/Layout.astro";
|
2025-04-22 01:27:38 +08:00
|
|
|
|
import Breadcrumb from "@/components/Breadcrumb.astro";
|
2025-04-19 16:19:39 +08:00
|
|
|
|
import { ARTICLE_EXPIRY_CONFIG } from "@/consts";
|
2025-05-03 19:50:03 +08:00
|
|
|
|
import "@/styles/articles.css";
|
2025-03-03 21:16:16 +08:00
|
|
|
|
|
2025-05-02 23:47:55 +08:00
|
|
|
|
// 定义文章类型
|
|
|
|
|
interface ArticleEntry {
|
|
|
|
|
id: string;
|
|
|
|
|
data: {
|
|
|
|
|
title: string;
|
|
|
|
|
date: Date;
|
|
|
|
|
tags?: string[];
|
|
|
|
|
summary?: string;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 定义标题类型
|
|
|
|
|
interface Heading {
|
|
|
|
|
depth: number;
|
|
|
|
|
slug: string;
|
|
|
|
|
text: string;
|
|
|
|
|
}
|
2025-03-03 21:16:16 +08:00
|
|
|
|
|
|
|
|
|
export async function getStaticPaths() {
|
2025-04-19 16:19:39 +08:00
|
|
|
|
const articles = await getCollection("articles");
|
|
|
|
|
|
2025-03-09 01:11:43 +08:00
|
|
|
|
// 为每篇文章生成路由参数
|
2025-03-10 14:05:40 +08:00
|
|
|
|
const paths = [];
|
2025-03-10 17:35:21 +08:00
|
|
|
|
for (const article of articles) {
|
|
|
|
|
// 获取所有可能的路径形式
|
|
|
|
|
const possiblePaths = new Set([
|
2025-04-19 16:19:39 +08:00
|
|
|
|
article.id, // 只保留原始路径
|
2025-03-10 17:35:21 +08:00
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// 如果是多级目录,检查是否需要特殊处理
|
2025-04-19 16:19:39 +08:00
|
|
|
|
if (article.id.includes("/")) {
|
|
|
|
|
const parts = article.id.split("/");
|
2025-03-10 17:35:21 +08:00
|
|
|
|
const fileName = parts[parts.length - 1];
|
|
|
|
|
const dirName = parts[parts.length - 2];
|
2025-04-19 16:19:39 +08:00
|
|
|
|
|
2025-03-10 17:35:21 +08:00
|
|
|
|
// 只有当文件名与其父目录名相同时才添加特殊路径
|
|
|
|
|
if (fileName === dirName) {
|
|
|
|
|
possiblePaths.add(getSpecialPath(article.id));
|
2025-03-09 01:11:43 +08:00
|
|
|
|
}
|
2025-03-10 17:35:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 为每个可能的路径生成路由
|
|
|
|
|
for (const path of possiblePaths) {
|
2025-03-10 14:05:40 +08:00
|
|
|
|
paths.push({
|
2025-03-10 17:35:21 +08:00
|
|
|
|
params: { id: path },
|
2025-04-19 16:19:39 +08:00
|
|
|
|
props: {
|
2025-03-10 17:35:21 +08:00
|
|
|
|
article,
|
2025-04-19 16:19:39 +08:00
|
|
|
|
section: article.id.includes("/")
|
|
|
|
|
? article.id.split("/").slice(0, -1).join("/")
|
|
|
|
|
: "",
|
2025-03-10 17:35:21 +08:00
|
|
|
|
originalId: path !== article.id ? article.id : undefined,
|
2025-04-19 16:19:39 +08:00
|
|
|
|
},
|
2025-03-10 14:05:40 +08:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-03-10 17:35:21 +08:00
|
|
|
|
|
2025-03-10 14:05:40 +08:00
|
|
|
|
return paths;
|
2025-03-03 21:16:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取文章内容
|
2025-04-22 01:27:38 +08:00
|
|
|
|
const { article, section, originalId } = Astro.props;
|
|
|
|
|
|
|
|
|
|
// 获取搜索参数
|
|
|
|
|
const searchParams = new URLSearchParams(Astro.url.search);
|
2025-03-09 01:11:43 +08:00
|
|
|
|
|
|
|
|
|
// 如果有原始ID,使用它来渲染内容
|
|
|
|
|
const articleToRender = originalId ? { ...article, id: originalId } : article;
|
2025-03-03 21:16:16 +08:00
|
|
|
|
|
|
|
|
|
// 渲染文章内容
|
2025-05-02 23:47:55 +08:00
|
|
|
|
const { Content, headings } = await render(articleToRender);
|
2025-03-03 21:16:16 +08:00
|
|
|
|
|
2025-04-22 01:27:38 +08:00
|
|
|
|
// 获取面包屑路径段
|
|
|
|
|
const pathSegments = section ? section.split("/") : [];
|
2025-03-03 21:16:16 +08:00
|
|
|
|
|
|
|
|
|
// 获取相关文章
|
2025-04-19 16:19:39 +08:00
|
|
|
|
const allArticles = await getCollection("articles");
|
2025-04-20 17:22:38 +08:00
|
|
|
|
|
|
|
|
|
// 1. 尝试通过标签匹配相关文章
|
|
|
|
|
let relatedArticles = allArticles
|
2025-05-02 23:47:55 +08:00
|
|
|
|
.filter((a: ArticleEntry) => {
|
|
|
|
|
const hasCommonTags =
|
|
|
|
|
a.id !== article.id &&
|
|
|
|
|
a.data.tags &&
|
|
|
|
|
article.data.tags &&
|
|
|
|
|
a.data.tags.length > 0 &&
|
|
|
|
|
article.data.tags.length > 0 &&
|
|
|
|
|
a.data.tags.some((tag: string) => article.data.tags?.includes(tag));
|
|
|
|
|
|
|
|
|
|
return hasCommonTags;
|
|
|
|
|
})
|
|
|
|
|
.sort((a: ArticleEntry, b: ArticleEntry) => b.data.date.getTime() - a.data.date.getTime())
|
2025-03-03 21:16:16 +08:00
|
|
|
|
.slice(0, 3);
|
2025-03-08 18:16:42 +08:00
|
|
|
|
|
2025-04-20 17:22:38 +08:00
|
|
|
|
// 跟踪相关文章的匹配方式: "tag", "directory", "latest"
|
|
|
|
|
let relatedArticlesMatchType = relatedArticles.length > 0 ? "tag" : "";
|
|
|
|
|
|
|
|
|
|
// 2. 如果标签匹配没有找到足够的相关文章,尝试根据目录结构匹配
|
|
|
|
|
if (relatedArticles.length < 3) {
|
|
|
|
|
// 获取当前文章的目录路径
|
2025-05-02 23:47:55 +08:00
|
|
|
|
const currentPath = article.id.includes("/")
|
|
|
|
|
? article.id.substring(0, article.id.lastIndexOf("/"))
|
|
|
|
|
: "";
|
|
|
|
|
|
2025-04-20 17:22:38 +08:00
|
|
|
|
// 如果有目录路径,查找同目录的其他文章
|
|
|
|
|
if (currentPath) {
|
|
|
|
|
// 收集同目录下的文章,但排除已经通过标签匹配的和当前文章
|
|
|
|
|
const dirRelatedArticles = allArticles
|
2025-05-02 23:47:55 +08:00
|
|
|
|
.filter(
|
|
|
|
|
(a: ArticleEntry) =>
|
|
|
|
|
a.id !== article.id &&
|
|
|
|
|
a.id.startsWith(currentPath + "/") &&
|
|
|
|
|
!relatedArticles.some((r: ArticleEntry) => r.id === a.id),
|
2025-04-20 17:22:38 +08:00
|
|
|
|
)
|
2025-05-02 23:47:55 +08:00
|
|
|
|
.sort((a: ArticleEntry, b: ArticleEntry) => b.data.date.getTime() - a.data.date.getTime())
|
2025-04-20 17:22:38 +08:00
|
|
|
|
.slice(0, 3 - relatedArticles.length);
|
2025-05-02 23:47:55 +08:00
|
|
|
|
|
2025-04-20 17:22:38 +08:00
|
|
|
|
if (dirRelatedArticles.length > 0) {
|
|
|
|
|
relatedArticles = [...relatedArticles, ...dirRelatedArticles];
|
2025-05-02 23:47:55 +08:00
|
|
|
|
relatedArticlesMatchType =
|
|
|
|
|
relatedArticles.length > 0 && !relatedArticlesMatchType
|
|
|
|
|
? "directory"
|
|
|
|
|
: relatedArticlesMatchType;
|
2025-04-20 17:22:38 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. 如果仍然没有找到足够的相关文章,则选择最新的文章(排除当前文章和已选择的文章)
|
|
|
|
|
if (relatedArticles.length < 3) {
|
|
|
|
|
const latestArticles = allArticles
|
2025-05-02 23:47:55 +08:00
|
|
|
|
.filter(
|
|
|
|
|
(a: ArticleEntry) => a.id !== article.id && !relatedArticles.some((r: ArticleEntry) => r.id === a.id),
|
2025-04-20 17:22:38 +08:00
|
|
|
|
)
|
2025-05-02 23:47:55 +08:00
|
|
|
|
.sort((a: ArticleEntry, b: ArticleEntry) => b.data.date.getTime() - a.data.date.getTime())
|
2025-04-20 17:22:38 +08:00
|
|
|
|
.slice(0, 3 - relatedArticles.length);
|
2025-05-02 23:47:55 +08:00
|
|
|
|
|
2025-04-20 17:22:38 +08:00
|
|
|
|
if (latestArticles.length > 0) {
|
|
|
|
|
relatedArticles = [...relatedArticles, ...latestArticles];
|
2025-05-02 23:47:55 +08:00
|
|
|
|
relatedArticlesMatchType =
|
|
|
|
|
relatedArticles.length > 0 && !relatedArticlesMatchType
|
|
|
|
|
? "latest"
|
|
|
|
|
: relatedArticlesMatchType;
|
2025-04-20 17:22:38 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-08 18:16:42 +08:00
|
|
|
|
// 准备文章描述
|
2025-04-19 16:19:39 +08:00
|
|
|
|
const description =
|
|
|
|
|
article.data.summary ||
|
|
|
|
|
`${article.data.title} - 发布于 ${article.data.date.toLocaleDateString("zh-CN")}`;
|
2025-03-09 01:11:43 +08:00
|
|
|
|
|
|
|
|
|
// 处理特殊ID的函数
|
|
|
|
|
function getArticleUrl(articleId: string) {
|
2025-05-02 23:47:55 +08:00
|
|
|
|
return `/articles/${getSpecialPath(articleId)}${searchParams.toString() ? `?${searchParams.toString()}` : ""}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 预先生成目录结构
|
|
|
|
|
function generateTableOfContents(headings: Heading[]) {
|
|
|
|
|
if (!headings || headings.length === 0) {
|
|
|
|
|
return '<p class="text-secondary-500 dark:text-secondary-400 italic">此文章没有目录</p>';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 查找最低级别的标题(数值最小)
|
|
|
|
|
const minDepth = Math.min(...headings.map(h => h.depth));
|
|
|
|
|
|
|
|
|
|
let tocHtml = '<ul class="space-y-2">';
|
|
|
|
|
|
|
|
|
|
headings.forEach((heading) => {
|
|
|
|
|
// 计算相对缩进,以最小深度为基准
|
|
|
|
|
const relativeDepth = heading.depth - minDepth;
|
|
|
|
|
const indent = relativeDepth * 0.75;
|
|
|
|
|
|
|
|
|
|
// 基于相对深度而非绝对深度决定样式
|
|
|
|
|
const isHigherLevel = relativeDepth <= 1; // 仅最高级和次高级标题使用较重的样式
|
|
|
|
|
|
|
|
|
|
tocHtml += `<li>
|
|
|
|
|
<a href="#${heading.slug}"
|
|
|
|
|
class="block hover:text-primary-600 dark:hover:text-primary-400 duration-50 ${
|
|
|
|
|
isHigherLevel
|
|
|
|
|
? "text-secondary-800 dark:text-secondary-200 font-medium"
|
|
|
|
|
: "text-secondary-600 dark:text-secondary-400"
|
|
|
|
|
}"
|
|
|
|
|
style="padding-left: ${indent}rem;">
|
|
|
|
|
${heading.text}
|
|
|
|
|
</a>
|
|
|
|
|
</li>`;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
tocHtml += '</ul>';
|
|
|
|
|
return tocHtml;
|
2025-03-09 01:11:43 +08:00
|
|
|
|
}
|
2025-04-22 01:27:38 +08:00
|
|
|
|
|
2025-05-02 23:47:55 +08:00
|
|
|
|
// 生成目录HTML
|
|
|
|
|
const tableOfContents = generateTableOfContents(headings);
|
2025-03-03 21:16:16 +08:00
|
|
|
|
---
|
|
|
|
|
|
2025-03-08 18:16:42 +08:00
|
|
|
|
<Layout
|
|
|
|
|
title={article.data.title}
|
|
|
|
|
description={description}
|
|
|
|
|
date={article.data.date}
|
|
|
|
|
tags={article.data.tags}
|
2025-05-02 23:47:55 +08:00
|
|
|
|
skipSrTitle={false}
|
|
|
|
|
pageType="article"
|
2025-03-08 18:16:42 +08:00
|
|
|
|
>
|
2025-05-02 23:47:55 +08:00
|
|
|
|
<div class="max-w-5xl py-8">
|
2025-03-08 18:16:42 +08:00
|
|
|
|
<!-- 阅读进度条 -->
|
2025-04-19 16:19:39 +08:00
|
|
|
|
<div
|
|
|
|
|
class="fixed top-0 left-0 w-full h-1 bg-transparent z-50"
|
2025-04-20 15:34:45 +08:00
|
|
|
|
id="progress-container"
|
2025-04-19 16:19:39 +08:00
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
class="h-full w-0 bg-primary-500 transition-width duration-100"
|
|
|
|
|
id="progress-bar"
|
|
|
|
|
>
|
|
|
|
|
</div>
|
2025-03-08 18:16:42 +08:00
|
|
|
|
</div>
|
2025-04-19 16:19:39 +08:00
|
|
|
|
|
2025-03-08 18:16:42 +08:00
|
|
|
|
<!-- 文章头部 -->
|
|
|
|
|
<header class="mb-8">
|
|
|
|
|
<!-- 导航区域 -->
|
2025-04-19 16:19:39 +08:00
|
|
|
|
<div
|
2025-04-22 01:27:38 +08:00
|
|
|
|
class="bg-white dark:bg-gray-800 rounded-xl p-4 mb-6 shadow-lg border border-gray-200 dark:border-gray-700 relative z-10"
|
2025-04-19 16:19:39 +08:00
|
|
|
|
>
|
2025-05-02 23:47:55 +08:00
|
|
|
|
<div
|
|
|
|
|
class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3"
|
|
|
|
|
>
|
2025-04-22 01:27:38 +08:00
|
|
|
|
<div class="w-full overflow-hidden">
|
2025-04-19 16:19:39 +08:00
|
|
|
|
<Breadcrumb
|
|
|
|
|
pageType="article"
|
2025-04-22 01:27:38 +08:00
|
|
|
|
pathSegments={pathSegments}
|
|
|
|
|
searchParams={searchParams}
|
2025-04-19 16:19:39 +08:00
|
|
|
|
articleTitle={article.data.title}
|
2025-04-22 01:27:38 +08:00
|
|
|
|
path={section}
|
2025-03-20 10:50:31 +08:00
|
|
|
|
/>
|
|
|
|
|
</div>
|
2025-03-03 21:16:16 +08:00
|
|
|
|
</div>
|
2025-03-08 18:16:42 +08:00
|
|
|
|
</div>
|
2025-04-19 16:19:39 +08:00
|
|
|
|
|
2025-05-02 23:47:55 +08:00
|
|
|
|
<!-- 文章过期提醒 -->
|
|
|
|
|
{
|
|
|
|
|
(() => {
|
|
|
|
|
const publishDate = article.data.date;
|
|
|
|
|
const currentDate = new Date();
|
|
|
|
|
const daysDiff = Math.floor(
|
|
|
|
|
(currentDate.getTime() - publishDate.getTime()) /
|
|
|
|
|
(1000 * 60 * 60 * 24),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
ARTICLE_EXPIRY_CONFIG.enabled &&
|
|
|
|
|
daysDiff > ARTICLE_EXPIRY_CONFIG.expiryDays
|
|
|
|
|
) {
|
|
|
|
|
return (
|
|
|
|
|
<div class="bg-yellow-50 border-l-4 border-yellow-400 p-4 mb-4">
|
|
|
|
|
<div class="flex">
|
|
|
|
|
<div class="flex-shrink-0">
|
|
|
|
|
<svg
|
|
|
|
|
class="h-5 w-5 text-yellow-400"
|
|
|
|
|
viewBox="0 0 20 20"
|
|
|
|
|
fill="currentColor"
|
|
|
|
|
>
|
|
|
|
|
<path
|
|
|
|
|
fill-rule="evenodd"
|
|
|
|
|
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
|
|
|
|
|
clip-rule="evenodd"
|
|
|
|
|
/>
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="ml-3">
|
|
|
|
|
<p class="text-sm text-yellow-700">
|
|
|
|
|
{ARTICLE_EXPIRY_CONFIG.warningMessage}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
})()
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-19 16:19:39 +08:00
|
|
|
|
<h1 class="text-3xl font-bold mb-4 text-gray-900 dark:text-gray-100">
|
|
|
|
|
{article.data.title}
|
|
|
|
|
</h1>
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
class="flex flex-wrap items-center gap-4 text-sm text-secondary-600 dark:text-secondary-400 mb-4"
|
|
|
|
|
>
|
|
|
|
|
<time
|
|
|
|
|
datetime={article.data.date.toISOString()}
|
|
|
|
|
class="flex items-center"
|
|
|
|
|
>
|
|
|
|
|
<svg
|
|
|
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
class="h-4 w-4 mr-1"
|
|
|
|
|
fill="none"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
>
|
|
|
|
|
<path
|
|
|
|
|
stroke-linecap="round"
|
|
|
|
|
stroke-linejoin="round"
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
|
|
|
></path>
|
2025-03-08 18:16:42 +08:00
|
|
|
|
</svg>
|
2025-04-19 16:19:39 +08:00
|
|
|
|
{article.data.date.toLocaleDateString("zh-CN")}
|
2025-03-08 18:16:42 +08:00
|
|
|
|
</time>
|
2025-04-19 16:19:39 +08:00
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
section && (
|
|
|
|
|
<span class="flex items-center">
|
|
|
|
|
<svg
|
|
|
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
class="h-4 w-4 mr-1 shrink-0"
|
|
|
|
|
fill="none"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
>
|
|
|
|
|
<path
|
|
|
|
|
stroke-linecap="round"
|
|
|
|
|
stroke-linejoin="round"
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"
|
|
|
|
|
/>
|
|
|
|
|
</svg>
|
|
|
|
|
<a
|
2025-04-22 01:27:38 +08:00
|
|
|
|
href={`/articles/${section}/`}
|
2025-04-19 16:19:39 +08:00
|
|
|
|
class="hover:text-indigo-600 break-all"
|
|
|
|
|
>
|
|
|
|
|
{section}
|
|
|
|
|
</a>
|
|
|
|
|
</span>
|
|
|
|
|
)
|
|
|
|
|
}
|
2025-03-08 18:16:42 +08:00
|
|
|
|
</div>
|
2025-04-19 16:19:39 +08:00
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
article.data.tags && article.data.tags.length > 0 && (
|
|
|
|
|
<div class="flex flex-wrap gap-2 mb-6">
|
2025-05-02 23:47:55 +08:00
|
|
|
|
{article.data.tags.map((tag: string) => (
|
2025-04-19 16:19:39 +08:00
|
|
|
|
<a
|
2025-04-22 01:27:38 +08:00
|
|
|
|
href={`/articles?tags=${tag}`}
|
2025-04-19 16:19:39 +08:00
|
|
|
|
class="text-xs bg-primary-50 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 py-1 px-2 rounded hover:bg-primary-100 dark:hover:bg-primary-800/30"
|
2025-04-20 17:22:38 +08:00
|
|
|
|
data-astro-prefetch="hover"
|
2025-04-19 16:19:39 +08:00
|
|
|
|
>
|
|
|
|
|
#{tag}
|
|
|
|
|
</a>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
2025-03-08 18:16:42 +08:00
|
|
|
|
</header>
|
2025-04-19 16:19:39 +08:00
|
|
|
|
|
2025-05-02 23:47:55 +08:00
|
|
|
|
<!-- 文章内容 -->
|
|
|
|
|
<article
|
|
|
|
|
class="prose prose-lg dark:prose-invert prose-primary prose-table:rounded-lg prose-table:border-separate prose-table:border-2 prose-thead:bg-primary-50 dark:prose-thead:bg-gray-800 prose-ul:list-disc prose-ol:list-decimal prose-li:my-1 prose-blockquote:border-l-4 prose-blockquote:border-primary-500 prose-blockquote:bg-gray-100 prose-blockquote:dark:bg-gray-800 prose-a:text-primary-600 prose-a:dark:text-primary-400 prose-a:no-underline prose-a:border-b prose-a:border-primary-300 prose-a:hover:border-primary-600 max-w-none mb-12"
|
|
|
|
|
>
|
|
|
|
|
<Content />
|
|
|
|
|
</article>
|
|
|
|
|
|
|
|
|
|
<!-- 目录 -->
|
|
|
|
|
<section
|
|
|
|
|
class="hidden 2xl:block fixed right-[calc(50%-44rem)] top-20 w-64 z-30"
|
|
|
|
|
id="toc-panel"
|
|
|
|
|
>
|
2025-04-19 16:19:39 +08:00
|
|
|
|
<div
|
2025-05-02 23:47:55 +08:00
|
|
|
|
class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 flex flex-col backdrop-blur-sm bg-opacity-95 dark:bg-opacity-95"
|
2025-04-19 16:19:39 +08:00
|
|
|
|
>
|
|
|
|
|
<div
|
2025-05-02 23:47:55 +08:00
|
|
|
|
class="border-b border-secondary-100 dark:border-gray-700 p-4 pb-3 sticky top-0 bg-white dark:bg-gray-800 bg-opacity-95 dark:bg-opacity-95 backdrop-filter backdrop-blur-sm z-10 rounded-t-xl"
|
2025-04-19 16:19:39 +08:00
|
|
|
|
>
|
2025-05-02 23:47:55 +08:00
|
|
|
|
<h3 class="font-bold text-primary-700 dark:text-primary-400 flex items-center gap-2">
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7" />
|
|
|
|
|
</svg>
|
|
|
|
|
文章目录
|
|
|
|
|
</h3>
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
id="toc-content"
|
|
|
|
|
class="text-sm p-4 pt-2 overflow-y-auto max-h-[calc(100vh-8rem-42px)] scrollbar-thin scrollbar-thumb-primary-200 dark:scrollbar-thumb-primary-800 scrollbar-track-transparent"
|
|
|
|
|
set:html={tableOfContents}
|
|
|
|
|
>
|
|
|
|
|
<!-- 目录内容在服务端生成 -->
|
2025-03-03 21:16:16 +08:00
|
|
|
|
</div>
|
2025-03-08 18:16:42 +08:00
|
|
|
|
</div>
|
2025-05-02 23:47:55 +08:00
|
|
|
|
</section>
|
2025-04-19 16:19:39 +08:00
|
|
|
|
|
2025-03-08 18:16:42 +08:00
|
|
|
|
<!-- 相关文章 -->
|
2025-04-19 16:19:39 +08:00
|
|
|
|
{
|
|
|
|
|
relatedArticles.length > 0 && (
|
2025-05-02 23:47:55 +08:00
|
|
|
|
<aside class="mt-12 pt-8 border-t border-secondary-200 dark:border-gray-700">
|
2025-04-19 16:19:39 +08:00
|
|
|
|
<h2 class="text-2xl font-bold mb-6 text-primary-900 dark:text-primary-100">
|
2025-05-02 23:47:55 +08:00
|
|
|
|
{relatedArticlesMatchType === "tag"
|
|
|
|
|
? "相关文章"
|
|
|
|
|
: relatedArticlesMatchType === "directory"
|
|
|
|
|
? "同类文章"
|
|
|
|
|
: "推荐阅读"}
|
2025-04-19 16:19:39 +08:00
|
|
|
|
</h2>
|
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
2025-05-02 23:47:55 +08:00
|
|
|
|
{relatedArticles.map((relatedArticle: ArticleEntry) => (
|
2025-04-19 16:19:39 +08:00
|
|
|
|
<a
|
|
|
|
|
href={getArticleUrl(relatedArticle.id)}
|
2025-05-02 23:47:55 +08:00
|
|
|
|
class="article-card"
|
2025-04-20 17:22:38 +08:00
|
|
|
|
data-astro-prefetch="viewport"
|
2025-04-19 16:19:39 +08:00
|
|
|
|
>
|
2025-05-02 23:47:55 +08:00
|
|
|
|
<div class="article-card-content">
|
|
|
|
|
<div class="article-card-icon">
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"></path>
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="article-card-body">
|
|
|
|
|
<h3 class="article-card-title">
|
|
|
|
|
{relatedArticle.data.title}
|
|
|
|
|
</h3>
|
|
|
|
|
<p class="article-card-date">
|
|
|
|
|
{relatedArticle.data.date.toLocaleDateString("zh-CN")}
|
|
|
|
|
</p>
|
|
|
|
|
{relatedArticle.data.summary && (
|
|
|
|
|
<p class="article-card-summary">
|
|
|
|
|
{relatedArticle.data.summary}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
<div class="article-card-footer">
|
|
|
|
|
<time datetime={relatedArticle.data.date.toISOString()} class="article-card-date">
|
|
|
|
|
{relatedArticle.data.date.toLocaleDateString("zh-CN", {
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
month: 'long',
|
|
|
|
|
day: 'numeric'
|
|
|
|
|
})}
|
|
|
|
|
</time>
|
|
|
|
|
<span class="article-card-read-more">阅读全文</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-04-19 16:19:39 +08:00
|
|
|
|
</a>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
2025-05-02 23:47:55 +08:00
|
|
|
|
</aside>
|
2025-04-19 16:19:39 +08:00
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-08 18:16:42 +08:00
|
|
|
|
<!-- 返回顶部按钮 -->
|
2025-04-19 16:19:39 +08:00
|
|
|
|
<button
|
|
|
|
|
id="back-to-top"
|
|
|
|
|
class="fixed bottom-8 right-8 w-12 h-12 rounded-full bg-primary-500 dark:bg-primary-600 text-white shadow-md flex items-center justify-center opacity-0 invisible translate-y-5 hover:bg-primary-600 dark:hover:bg-primary-700"
|
|
|
|
|
>
|
|
|
|
|
<svg
|
|
|
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
class="h-6 w-6"
|
|
|
|
|
fill="none"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
>
|
|
|
|
|
<path
|
|
|
|
|
stroke-linecap="round"
|
|
|
|
|
stroke-linejoin="round"
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
d="M5 10l7-7m0 0l7 7m-7-7v18"
|
|
|
|
|
></path>
|
2025-03-08 18:16:42 +08:00
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
2025-03-03 21:16:16 +08:00
|
|
|
|
</div>
|
2025-05-02 23:47:55 +08:00
|
|
|
|
</Layout>
|
2025-04-19 16:19:39 +08:00
|
|
|
|
|
2025-05-02 23:47:55 +08:00
|
|
|
|
<!-- 文章页面脚本 -->
|
2025-04-20 15:34:45 +08:00
|
|
|
|
<script is:inline>
|
2025-05-02 23:47:55 +08:00
|
|
|
|
// 文章页面交互脚本
|
|
|
|
|
(function () {
|
|
|
|
|
// 存储事件监听器,便于统一清理
|
|
|
|
|
const listeners = [];
|
|
|
|
|
|
|
|
|
|
// 添加事件监听器并记录,方便后续统一清理
|
|
|
|
|
function addListener(element, eventType, handler, options) {
|
|
|
|
|
if (!element) return null;
|
|
|
|
|
|
|
|
|
|
element.addEventListener(eventType, handler, options);
|
|
|
|
|
listeners.push({ element, eventType, handler, options });
|
|
|
|
|
return handler;
|
|
|
|
|
}
|
2025-04-19 16:19:39 +08:00
|
|
|
|
|
2025-05-02 23:47:55 +08:00
|
|
|
|
// 清理函数,移除所有事件监听器
|
|
|
|
|
function cleanup() {
|
|
|
|
|
listeners.forEach(({ element, eventType, handler, options }) => {
|
|
|
|
|
try {
|
|
|
|
|
element.removeEventListener(eventType, handler, options);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
// 忽略错误
|
2025-04-20 15:34:45 +08:00
|
|
|
|
}
|
2025-05-02 23:47:55 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
listeners.length = 0;
|
2025-03-03 21:16:16 +08:00
|
|
|
|
}
|
2025-04-20 15:34:45 +08:00
|
|
|
|
|
2025-05-04 03:44:10 +08:00
|
|
|
|
// 代码块复制功能
|
|
|
|
|
function setupCodeCopy() {
|
|
|
|
|
const copyButtons = document.querySelectorAll('.code-block-copy');
|
|
|
|
|
if (copyButtons.length === 0) return;
|
|
|
|
|
|
|
|
|
|
copyButtons.forEach(button => {
|
|
|
|
|
addListener(button, 'click', async () => {
|
2025-05-03 19:50:03 +08:00
|
|
|
|
try {
|
2025-05-04 03:44:10 +08:00
|
|
|
|
// 使用Base64解码获取代码文本
|
|
|
|
|
const encodedCode = button.getAttribute('data-code');
|
|
|
|
|
if (!encodedCode) return;
|
|
|
|
|
|
|
|
|
|
// 解码并复制到剪贴板
|
|
|
|
|
const code = atob(encodedCode);
|
|
|
|
|
await navigator.clipboard.writeText(code);
|
|
|
|
|
|
|
|
|
|
const originalHTML = button.innerHTML;
|
|
|
|
|
button.classList.add('copied');
|
|
|
|
|
button.innerHTML = `
|
2025-05-03 19:50:03 +08:00
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4">
|
|
|
|
|
<path d="M20 6L9 17l-5-5"></path>
|
|
|
|
|
</svg>
|
|
|
|
|
已复制
|
|
|
|
|
`;
|
2025-05-02 23:47:55 +08:00
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
2025-05-04 03:44:10 +08:00
|
|
|
|
button.classList.remove('copied');
|
|
|
|
|
button.innerHTML = originalHTML;
|
2025-05-02 23:47:55 +08:00
|
|
|
|
}, 2000);
|
2025-05-03 19:50:03 +08:00
|
|
|
|
} catch (err) {
|
2025-05-02 23:47:55 +08:00
|
|
|
|
console.error('复制失败:', err);
|
2025-05-04 03:44:10 +08:00
|
|
|
|
button.innerHTML = `
|
2025-05-03 19:50:03 +08:00
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4">
|
|
|
|
|
<circle cx="12" cy="12" r="10"></circle>
|
|
|
|
|
<line x1="12" y1="8" x2="12" y2="12"></line>
|
|
|
|
|
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
|
|
|
|
</svg>
|
|
|
|
|
失败
|
|
|
|
|
`;
|
|
|
|
|
setTimeout(() => {
|
2025-05-04 03:44:10 +08:00
|
|
|
|
button.innerHTML = `
|
2025-05-03 19:50:03 +08:00
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4">
|
|
|
|
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
|
|
|
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
|
|
|
|
</svg>
|
|
|
|
|
复制
|
|
|
|
|
`;
|
|
|
|
|
}, 2000);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
2025-04-20 15:34:45 +08:00
|
|
|
|
}
|
2025-04-19 16:19:39 +08:00
|
|
|
|
|
2025-05-02 23:47:55 +08:00
|
|
|
|
// 4. 设置阅读进度条
|
|
|
|
|
function setupProgressBar() {
|
|
|
|
|
const progressBar = document.getElementById("progress-bar");
|
|
|
|
|
const backToTopButton = document.getElementById("back-to-top");
|
2025-04-19 16:19:39 +08:00
|
|
|
|
|
2025-05-02 23:47:55 +08:00
|
|
|
|
if (!progressBar) return;
|
|
|
|
|
|
|
|
|
|
function updateReadingProgress() {
|
|
|
|
|
const scrollTop = window.scrollY || document.documentElement.scrollTop;
|
|
|
|
|
const scrollHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;
|
|
|
|
|
const progress = (scrollTop / scrollHeight) * 100;
|
|
|
|
|
|
|
|
|
|
progressBar.style.width = `${progress}%`;
|
|
|
|
|
|
|
|
|
|
if (backToTopButton) {
|
|
|
|
|
if (scrollTop > 300) {
|
|
|
|
|
backToTopButton.classList.add(
|
|
|
|
|
"opacity-100",
|
|
|
|
|
"visible",
|
|
|
|
|
"translate-y-0"
|
|
|
|
|
);
|
|
|
|
|
backToTopButton.classList.remove(
|
|
|
|
|
"opacity-0",
|
|
|
|
|
"invisible",
|
|
|
|
|
"translate-y-5"
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
backToTopButton.classList.add(
|
|
|
|
|
"opacity-0",
|
|
|
|
|
"invisible",
|
|
|
|
|
"translate-y-5"
|
|
|
|
|
);
|
|
|
|
|
backToTopButton.classList.remove(
|
|
|
|
|
"opacity-100",
|
|
|
|
|
"visible",
|
|
|
|
|
"translate-y-0"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-03-08 18:16:42 +08:00
|
|
|
|
}
|
2025-04-19 16:19:39 +08:00
|
|
|
|
|
2025-05-02 23:47:55 +08:00
|
|
|
|
addListener(window, "scroll", updateReadingProgress);
|
2025-04-20 15:34:45 +08:00
|
|
|
|
|
2025-05-02 23:47:55 +08:00
|
|
|
|
if (backToTopButton) {
|
|
|
|
|
addListener(backToTopButton, "click", () => {
|
2025-04-20 15:34:45 +08:00
|
|
|
|
window.scrollTo({
|
2025-05-02 23:47:55 +08:00
|
|
|
|
top: 0,
|
2025-04-20 15:34:45 +08:00
|
|
|
|
behavior: "smooth",
|
|
|
|
|
});
|
2025-05-02 23:47:55 +08:00
|
|
|
|
});
|
|
|
|
|
}
|
2025-04-19 16:19:39 +08:00
|
|
|
|
|
2025-05-02 23:47:55 +08:00
|
|
|
|
updateReadingProgress();
|
|
|
|
|
}
|
2025-05-03 19:50:03 +08:00
|
|
|
|
|
2025-05-02 23:47:55 +08:00
|
|
|
|
// 5. 管理目录交互
|
|
|
|
|
function setupTableOfContents() {
|
|
|
|
|
const tocContent = document.getElementById("toc-content");
|
|
|
|
|
const tocPanel = document.querySelector("#toc-panel");
|
|
|
|
|
|
|
|
|
|
if (!tocPanel || !tocContent) return;
|
|
|
|
|
|
|
|
|
|
// 检查窗口大小调整目录面板显示
|
|
|
|
|
function checkTocVisibility() {
|
|
|
|
|
if (window.innerWidth < 1536) {
|
|
|
|
|
tocPanel.classList.add("hidden");
|
|
|
|
|
tocPanel.classList.remove("2xl:block");
|
|
|
|
|
} else {
|
|
|
|
|
tocPanel.classList.remove("hidden");
|
|
|
|
|
tocPanel.classList.add("2xl:block");
|
2025-04-20 15:34:45 +08:00
|
|
|
|
}
|
2025-05-02 23:47:55 +08:00
|
|
|
|
}
|
2025-04-19 16:19:39 +08:00
|
|
|
|
|
2025-05-02 23:47:55 +08:00
|
|
|
|
addListener(window, "resize", checkTocVisibility);
|
|
|
|
|
checkTocVisibility();
|
2025-04-19 16:19:39 +08:00
|
|
|
|
|
2025-05-02 23:47:55 +08:00
|
|
|
|
// 处理目录链接点击跳转
|
|
|
|
|
const tocLinks = tocContent.querySelectorAll("a");
|
2025-04-20 15:34:45 +08:00
|
|
|
|
tocLinks.forEach(link => {
|
2025-05-02 23:47:55 +08:00
|
|
|
|
addListener(link, "click", (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const targetId = link.getAttribute("href")?.substring(1);
|
|
|
|
|
if (!targetId) return;
|
|
|
|
|
|
|
|
|
|
const targetElement = document.getElementById(targetId);
|
|
|
|
|
if (targetElement) {
|
|
|
|
|
const offset = 100;
|
|
|
|
|
const targetPosition = targetElement.getBoundingClientRect().top + window.scrollY - offset;
|
|
|
|
|
|
|
|
|
|
window.scrollTo({
|
|
|
|
|
top: targetPosition,
|
|
|
|
|
behavior: "smooth",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
targetElement.classList.add("bg-primary-50", "dark:bg-primary-900/20");
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
targetElement.classList.remove("bg-primary-50", "dark:bg-primary-900/20");
|
|
|
|
|
}, 2000);
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-04-20 15:34:45 +08:00
|
|
|
|
});
|
2025-04-19 16:19:39 +08:00
|
|
|
|
|
2025-05-02 23:47:55 +08:00
|
|
|
|
// 监听滚动以更新当前活动的目录项
|
|
|
|
|
const article = document.querySelector("article");
|
|
|
|
|
if (!article) return;
|
2025-04-19 16:19:39 +08:00
|
|
|
|
|
2025-05-02 23:47:55 +08:00
|
|
|
|
let ticking = false;
|
|
|
|
|
|
|
|
|
|
function updateActiveHeading() {
|
|
|
|
|
const headings = Array.from(article.querySelectorAll("h1, h2, h3, h4, h5, h6"));
|
|
|
|
|
const tocLinks = Array.from(tocContent.querySelectorAll("a"));
|
|
|
|
|
|
|
|
|
|
// 清除所有活动状态
|
|
|
|
|
tocLinks.forEach((link) => {
|
|
|
|
|
link.classList.remove("text-primary-600", "dark:text-primary-400", "font-medium");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 找出当前可见的标题
|
|
|
|
|
const scrollPosition = window.scrollY + 150;
|
|
|
|
|
let currentHeading = null;
|
|
|
|
|
|
|
|
|
|
for (const heading of headings) {
|
|
|
|
|
const headingTop = heading.getBoundingClientRect().top + window.scrollY;
|
|
|
|
|
if (headingTop <= scrollPosition) {
|
|
|
|
|
currentHeading = heading;
|
|
|
|
|
} else {
|
|
|
|
|
break;
|
|
|
|
|
}
|
2025-03-08 18:16:42 +08:00
|
|
|
|
}
|
2025-04-19 16:19:39 +08:00
|
|
|
|
|
2025-05-02 23:47:55 +08:00
|
|
|
|
// 高亮当前标题对应的目录项
|
|
|
|
|
if (currentHeading) {
|
|
|
|
|
const id = currentHeading.getAttribute('id');
|
|
|
|
|
if (id) {
|
|
|
|
|
const activeLink = tocLinks.find(
|
|
|
|
|
(link) => link.getAttribute("href") === `#${id}`
|
|
|
|
|
);
|
|
|
|
|
if (activeLink) {
|
|
|
|
|
// 高亮当前目录项
|
|
|
|
|
activeLink.classList.add("text-primary-600", "dark:text-primary-400", "font-medium");
|
|
|
|
|
|
|
|
|
|
// 可选: 确保当前激活的目录项在可视区域内
|
|
|
|
|
const tocContainer = tocContent.querySelector('ul');
|
|
|
|
|
if (tocContainer) {
|
|
|
|
|
const linkOffsetTop = activeLink.offsetTop;
|
|
|
|
|
const containerScrollTop = tocContainer.scrollTop;
|
|
|
|
|
const containerHeight = tocContainer.clientHeight;
|
|
|
|
|
|
|
|
|
|
// 如果当前项不在视口内,滚动目录
|
|
|
|
|
if (linkOffsetTop < containerScrollTop ||
|
|
|
|
|
linkOffsetTop > containerScrollTop + containerHeight) {
|
|
|
|
|
tocContainer.scrollTop = linkOffsetTop - containerHeight / 2;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-03-08 18:16:42 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2025-05-02 23:47:55 +08:00
|
|
|
|
|
|
|
|
|
addListener(window, "scroll", () => {
|
|
|
|
|
if (!ticking) {
|
|
|
|
|
window.requestAnimationFrame(() => {
|
|
|
|
|
updateActiveHeading();
|
|
|
|
|
ticking = false;
|
|
|
|
|
});
|
|
|
|
|
ticking = true;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
updateActiveHeading();
|
2025-03-08 18:16:42 +08:00
|
|
|
|
}
|
2025-05-02 23:47:55 +08:00
|
|
|
|
|
2025-05-03 19:50:03 +08:00
|
|
|
|
// 6. 处理Mermaid图表渲染
|
|
|
|
|
function setupMermaid() {
|
|
|
|
|
// 查找所有mermaid代码块 - 支持多种可能的类名和选择器
|
|
|
|
|
const mermaidBlocks = document.querySelectorAll(
|
|
|
|
|
'pre.language-mermaid, pre > code.language-mermaid, .mermaid'
|
|
|
|
|
);
|
2025-05-02 23:47:55 +08:00
|
|
|
|
|
2025-05-03 19:50:03 +08:00
|
|
|
|
if (mermaidBlocks.length === 0) return;
|
2025-05-02 23:47:55 +08:00
|
|
|
|
|
2025-05-03 19:50:03 +08:00
|
|
|
|
console.log('找到Mermaid代码块:', mermaidBlocks.length);
|
2025-05-02 23:47:55 +08:00
|
|
|
|
|
2025-05-03 19:50:03 +08:00
|
|
|
|
// 动态加载mermaid库
|
|
|
|
|
const script = document.createElement('script');
|
|
|
|
|
script.src = 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js';
|
2025-05-02 23:47:55 +08:00
|
|
|
|
|
2025-05-03 19:50:03 +08:00
|
|
|
|
script.onload = function() {
|
|
|
|
|
console.log('Mermaid库加载完成,开始渲染图表');
|
|
|
|
|
|
|
|
|
|
// 初始化mermaid配置 - 始终使用默认主题,通过CSS控制样式
|
|
|
|
|
window.mermaid.initialize({
|
|
|
|
|
startOnLoad: false,
|
|
|
|
|
theme: 'default',
|
|
|
|
|
securityLevel: 'loose'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 将所有mermaid代码块转换为可渲染的格式
|
|
|
|
|
mermaidBlocks.forEach((block, index) => {
|
|
|
|
|
// 获取mermaid代码
|
|
|
|
|
let code = '';
|
|
|
|
|
|
|
|
|
|
// 检查元素类型并相应处理
|
|
|
|
|
if (block.tagName === 'CODE' && block.classList.contains('language-mermaid')) {
|
|
|
|
|
// 处理 code.language-mermaid 元素
|
|
|
|
|
code = block.textContent || '';
|
|
|
|
|
const pre = block.closest('pre');
|
|
|
|
|
if (pre) {
|
|
|
|
|
// 创建新的div元素替换整个pre
|
|
|
|
|
const div = document.createElement('div');
|
|
|
|
|
div.className = 'mermaid';
|
|
|
|
|
div.id = 'mermaid-diagram-' + index;
|
|
|
|
|
div.textContent = code;
|
|
|
|
|
pre.parentNode.replaceChild(div, pre);
|
|
|
|
|
}
|
|
|
|
|
} else if (block.tagName === 'PRE' && block.classList.contains('language-mermaid')) {
|
|
|
|
|
// 处理 pre.language-mermaid 元素
|
|
|
|
|
code = block.textContent || '';
|
|
|
|
|
const div = document.createElement('div');
|
|
|
|
|
div.className = 'mermaid';
|
|
|
|
|
div.id = 'mermaid-diagram-' + index;
|
|
|
|
|
div.textContent = code;
|
|
|
|
|
block.parentNode.replaceChild(div, block);
|
|
|
|
|
} else if (block.classList.contains('mermaid') && block.tagName !== 'DIV') {
|
|
|
|
|
// 如果是其他带mermaid类的元素但不是div,转换为div
|
|
|
|
|
code = block.textContent || '';
|
|
|
|
|
const div = document.createElement('div');
|
|
|
|
|
div.className = 'mermaid';
|
|
|
|
|
div.id = 'mermaid-diagram-' + index;
|
|
|
|
|
div.textContent = code;
|
|
|
|
|
block.parentNode.replaceChild(div, block);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 初始化渲染
|
|
|
|
|
try {
|
|
|
|
|
console.log('开始渲染Mermaid图表');
|
|
|
|
|
window.mermaid.run().catch(err => {
|
|
|
|
|
console.error('Mermaid渲染出错:', err);
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('初始化Mermaid渲染失败:', error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
script.onerror = function() {
|
|
|
|
|
console.error('加载Mermaid库失败');
|
|
|
|
|
// 显示错误信息
|
|
|
|
|
mermaidBlocks.forEach(block => {
|
|
|
|
|
if (block.tagName === 'CODE') block = block.closest('pre');
|
|
|
|
|
if (block) {
|
|
|
|
|
block.innerHTML = '<div class="mermaid-error-message">无法加载Mermaid图表库</div>';
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
document.head.appendChild(script);
|
|
|
|
|
|
|
|
|
|
// 添加到清理列表,确保后续页面跳转时能删除脚本
|
|
|
|
|
listeners.push({
|
|
|
|
|
element: script,
|
|
|
|
|
eventType: 'remove',
|
|
|
|
|
handler: () => {
|
|
|
|
|
if (script.parentNode) {
|
|
|
|
|
script.parentNode.removeChild(script);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 清除全局mermaid对象
|
|
|
|
|
if (window.mermaid) {
|
|
|
|
|
window.mermaid = undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 移除页面上可能留下的mermaid相关元素
|
|
|
|
|
try {
|
|
|
|
|
// 移除所有可能的mermaid样式和元素
|
|
|
|
|
const mermaidElements = [
|
|
|
|
|
'#mermaid-style',
|
|
|
|
|
'#mermaid-cloned-styles',
|
|
|
|
|
'.mermaid-svg-reference',
|
|
|
|
|
'style[id^="mermaid-"]'
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
document.querySelectorAll(mermaidElements.join(', ')).forEach(el => {
|
|
|
|
|
if (el && el.parentNode) {
|
|
|
|
|
el.parentNode.removeChild(el);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('清理Mermaid元素时出错:', e);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
options: null
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 初始化所有功能
|
|
|
|
|
function init() {
|
|
|
|
|
if (!document.querySelector("article")) return;
|
2025-05-02 23:47:55 +08:00
|
|
|
|
|
2025-05-04 03:44:10 +08:00
|
|
|
|
setupCodeCopy(); // 只保留代码复制功能
|
2025-05-03 19:50:03 +08:00
|
|
|
|
setupProgressBar();
|
2025-05-02 23:47:55 +08:00
|
|
|
|
setupTableOfContents();
|
2025-05-03 19:50:03 +08:00
|
|
|
|
setupMermaid();
|
2025-05-02 23:47:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 注册清理函数
|
|
|
|
|
function registerCleanup() {
|
|
|
|
|
// 使用 once: true 确保事件只触发一次
|
|
|
|
|
document.addEventListener("astro:before-preparation", cleanup, { once: true });
|
|
|
|
|
document.addEventListener("astro:before-swap", cleanup, { once: true });
|
|
|
|
|
document.addEventListener("swup:willReplaceContent", cleanup, { once: true });
|
|
|
|
|
window.addEventListener("beforeunload", cleanup, { once: true });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理页面跳转事件
|
|
|
|
|
function setupPageTransitionEvents() {
|
|
|
|
|
// 页面转换后事件
|
|
|
|
|
const pageTransitionEvents = [
|
|
|
|
|
{ name: "astro:after-swap", delay: 10 },
|
|
|
|
|
{ name: "astro:page-load", delay: 10 },
|
|
|
|
|
{ name: "swup:contentReplaced", delay: 10 },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// 设置每个页面转换事件
|
|
|
|
|
pageTransitionEvents.forEach(({ name, delay }) => {
|
|
|
|
|
document.addEventListener(name, () => {
|
|
|
|
|
cleanup(); // 立即清理
|
|
|
|
|
|
|
|
|
|
// 延迟初始化,确保DOM完全更新
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
cleanup(); // 再次清理,确保没有遗漏
|
|
|
|
|
init();
|
|
|
|
|
}, delay);
|
2025-04-20 15:34:45 +08:00
|
|
|
|
});
|
2025-05-02 23:47:55 +08:00
|
|
|
|
});
|
|
|
|
|
}
|
2025-04-19 16:19:39 +08:00
|
|
|
|
|
2025-05-02 23:47:55 +08:00
|
|
|
|
// 页面加载后初始化
|
|
|
|
|
if (document.readyState === "loading") {
|
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
|
|
|
init();
|
|
|
|
|
registerCleanup();
|
|
|
|
|
setupPageTransitionEvents();
|
|
|
|
|
}, { once: true });
|
|
|
|
|
} else {
|
|
|
|
|
init();
|
|
|
|
|
registerCleanup();
|
|
|
|
|
setupPageTransitionEvents();
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
</script>
|