newechoes/src/pages/articles/[...id].astro

979 lines
35 KiB
Plaintext
Raw Normal View History

2025-03-03 21:16:16 +08:00
---
import { getCollection, render } from "astro:content";
import { getSpecialPath } from "@/content.config";
import Layout from "@/components/Layout.astro";
import Breadcrumb from "@/components/Breadcrumb.astro";
import { ARTICLE_EXPIRY_CONFIG } from "@/consts";
import "@/styles/articles.css";
2025-03-03 21:16:16 +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() {
const articles = await getCollection("articles");
// 为每篇文章生成路由参数
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([
article.id, // 只保留原始路径
2025-03-10 17:35:21 +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-03-10 17:35:21 +08:00
// 只有当文件名与其父目录名相同时才添加特殊路径
if (fileName === dirName) {
possiblePaths.add(getSpecialPath(article.id));
}
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 },
props: {
2025-03-10 17:35:21 +08:00
article,
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-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
}
// 获取文章内容
const { article, section, originalId } = Astro.props;
// 获取搜索参数
const searchParams = new URLSearchParams(Astro.url.search);
// 如果有原始ID使用它来渲染内容
const articleToRender = originalId ? { ...article, id: originalId } : article;
2025-03-03 21:16:16 +08:00
// 渲染文章内容
const { Content, headings } = await render(articleToRender);
2025-03-03 21:16:16 +08:00
// 获取面包屑路径段
const pathSegments = section ? section.split("/") : [];
2025-03-03 21:16:16 +08:00
// 获取相关文章
const allArticles = await getCollection("articles");
// 1. 尝试通过标签匹配相关文章
let relatedArticles = allArticles
.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
// 跟踪相关文章的匹配方式: "tag", "directory", "latest"
let relatedArticlesMatchType = relatedArticles.length > 0 ? "tag" : "";
// 2. 如果标签匹配没有找到足够的相关文章,尝试根据目录结构匹配
if (relatedArticles.length < 3) {
// 获取当前文章的目录路径
const currentPath = article.id.includes("/")
? article.id.substring(0, article.id.lastIndexOf("/"))
: "";
// 如果有目录路径,查找同目录的其他文章
if (currentPath) {
// 收集同目录下的文章,但排除已经通过标签匹配的和当前文章
const dirRelatedArticles = allArticles
.filter(
(a: ArticleEntry) =>
a.id !== article.id &&
a.id.startsWith(currentPath + "/") &&
!relatedArticles.some((r: ArticleEntry) => r.id === a.id),
)
.sort((a: ArticleEntry, b: ArticleEntry) => b.data.date.getTime() - a.data.date.getTime())
.slice(0, 3 - relatedArticles.length);
if (dirRelatedArticles.length > 0) {
relatedArticles = [...relatedArticles, ...dirRelatedArticles];
relatedArticlesMatchType =
relatedArticles.length > 0 && !relatedArticlesMatchType
? "directory"
: relatedArticlesMatchType;
}
}
}
// 3. 如果仍然没有找到足够的相关文章,则选择最新的文章(排除当前文章和已选择的文章)
if (relatedArticles.length < 3) {
const latestArticles = allArticles
.filter(
(a: ArticleEntry) => a.id !== article.id && !relatedArticles.some((r: ArticleEntry) => r.id === a.id),
)
.sort((a: ArticleEntry, b: ArticleEntry) => b.data.date.getTime() - a.data.date.getTime())
.slice(0, 3 - relatedArticles.length);
if (latestArticles.length > 0) {
relatedArticles = [...relatedArticles, ...latestArticles];
relatedArticlesMatchType =
relatedArticles.length > 0 && !relatedArticlesMatchType
? "latest"
: relatedArticlesMatchType;
}
}
2025-03-08 18:16:42 +08:00
// 准备文章描述
const description =
article.data.summary ||
`${article.data.title} - 发布于 ${article.data.date.toLocaleDateString("zh-CN")}`;
// 处理特殊ID的函数
function getArticleUrl(articleId: string) {
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;
}
// 生成目录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}
skipSrTitle={false}
pageType="article"
2025-03-08 18:16:42 +08:00
>
<div class="max-w-5xl py-8">
2025-03-08 18:16:42 +08:00
<!-- 阅读进度条 -->
<div
class="fixed top-0 left-0 w-full h-1 bg-transparent z-50"
id="progress-container"
>
<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-03-08 18:16:42 +08:00
<!-- 文章头部 -->
<header class="mb-8">
<!-- 导航区域 -->
<div
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"
>
<div
class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3"
>
<div class="w-full overflow-hidden">
<Breadcrumb
pageType="article"
pathSegments={pathSegments}
searchParams={searchParams}
articleTitle={article.data.title}
path={section}
/>
</div>
2025-03-03 21:16:16 +08:00
</div>
2025-03-08 18:16:42 +08:00
</div>
<!-- 文章过期提醒 -->
{
(() => {
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;
})()
}
<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>
{article.data.date.toLocaleDateString("zh-CN")}
2025-03-08 18:16:42 +08:00
</time>
{
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
href={`/articles/${section}/`}
class="hover:text-indigo-600 break-all"
>
{section}
</a>
</span>
)
}
2025-03-08 18:16:42 +08:00
</div>
{
article.data.tags && article.data.tags.length > 0 && (
<div class="flex flex-wrap gap-2 mb-6">
{article.data.tags.map((tag: string) => (
<a
href={`/articles?tags=${tag}`}
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"
data-astro-prefetch="hover"
>
#{tag}
</a>
))}
</div>
)
}
2025-03-08 18:16:42 +08:00
</header>
<!-- 文章内容 -->
<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"
>
<div
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"
>
<div
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"
>
<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>
</section>
2025-03-08 18:16:42 +08:00
<!-- 相关文章 -->
{
relatedArticles.length > 0 && (
<aside class="mt-12 pt-8 border-t border-secondary-200 dark:border-gray-700">
<h2 class="text-2xl font-bold mb-6 text-primary-900 dark:text-primary-100">
{relatedArticlesMatchType === "tag"
? "相关文章"
: relatedArticlesMatchType === "directory"
? "同类文章"
: "推荐阅读"}
</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
{relatedArticles.map((relatedArticle: ArticleEntry) => (
<a
href={getArticleUrl(relatedArticle.id)}
class="article-card"
data-astro-prefetch="viewport"
>
<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>
</a>
))}
</div>
</aside>
)
}
2025-03-08 18:16:42 +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>
</Layout>
<!-- 文章页面脚本 -->
<script is:inline>
// 文章页面交互脚本 - 自销毁模式
(function() {
// 如果不是文章页面,立即退出,不执行任何代码
if (!document.querySelector("article")) {
return;
}
const scriptInstanceId = Date.now();
console.log(`[文章脚本:${scriptInstanceId}] 检测到文章页面,开始初始化`);
// 集中管理所有事件监听器
const allListeners = [];
// 为特殊清理任务准备的数组
const customCleanupTasks = [];
// 单独保存清理事件的监听器引用
const cleanupListeners = [];
// 添加事件监听器并记录,方便后续统一清理
function addListener(element, eventType, handler, options) {
if (!element) {
console.warn(`[文章脚本:${scriptInstanceId}] 尝试为不存在的元素添加事件:`, eventType);
return null;
}
console.log(`[文章脚本:${scriptInstanceId}] 添加事件监听器: ${eventType} 到`, element.tagName || "Window/Document");
element.addEventListener(eventType, handler, options);
allListeners.push({ element, eventType, handler, options });
return handler;
}
// 统一的清理函数,执行完整清理并自销毁
function selfDestruct() {
console.log(`[文章脚本:${scriptInstanceId}] 执行自销毁流程`);
// 1. 先移除普通事件监听器
console.log(`[文章脚本:${scriptInstanceId}] 移除常规监听器,数量:`, allListeners.length);
allListeners.forEach(({ element, eventType, handler, options }) => {
try {
console.log(`[文章脚本:${scriptInstanceId}] 移除事件监听器: ${eventType} 从`, element.tagName || "Window/Document");
element.removeEventListener(eventType, handler, options);
} catch (err) {
console.error(`[文章脚本:${scriptInstanceId}] 移除事件监听器出错:`, err);
}
});
// 清空监听器数组
allListeners.length = 0;
// 2. 执行特殊清理任务
console.log(`[文章脚本:${scriptInstanceId}] 执行特殊清理任务,数量:`, customCleanupTasks.length);
customCleanupTasks.forEach(task => {
try {
task();
} catch (err) {
console.error(`[文章脚本:${scriptInstanceId}] 执行特殊清理任务出错:`, err);
}
});
// 清空特殊任务数组
customCleanupTasks.length = 0;
// 3. 最后移除清理事件监听器自身
console.log(`[文章脚本:${scriptInstanceId}] 移除清理监听器,数量:`, cleanupListeners.length);
cleanupListeners.forEach(({ element, eventType, handler, options }) => {
try {
console.log(`[文章脚本:${scriptInstanceId}] 移除清理监听器: ${eventType} 从`, element.tagName || "Window/Document");
element.removeEventListener(eventType, handler, options);
} catch (err) {
console.error(`[文章脚本:${scriptInstanceId}] 移除清理监听器出错:`, err);
}
});
console.log(`[文章脚本:${scriptInstanceId}] 完全销毁完成`);
2025-03-03 21:16:16 +08:00
}
// 注册清理事件,并保存引用
function registerCleanupEvents() {
console.log(`[文章脚本:${scriptInstanceId}] 注册清理事件`);
// 创建一次性事件处理函数
const beforeSwapHandler = () => {
console.log(`[文章脚本:${scriptInstanceId}] astro:before-swap 触发,执行自销毁`);
selfDestruct();
};
const beforeUnloadHandler = () => {
console.log(`[文章脚本:${scriptInstanceId}] beforeunload 触发,执行自销毁`);
selfDestruct();
};
// 添加清理事件监听器并保存引用
document.addEventListener("astro:before-swap", beforeSwapHandler, { once: true });
window.addEventListener("beforeunload", beforeUnloadHandler, { once: true });
// 保存清理事件引用,用于完全销毁
cleanupListeners.push(
{ element: document, eventType: "astro:before-swap", handler: beforeSwapHandler, options: { once: true } },
{ element: window, eventType: "beforeunload", handler: beforeUnloadHandler, options: { once: true } }
);
}
// 初始化所有功能
function initializeFeatures() {
console.log(`[文章脚本:${scriptInstanceId}] 开始初始化各功能`);
// 1. 代码块复制功能
function setupCodeCopy() {
console.log(`[文章脚本:${scriptInstanceId}] 初始化代码复制功能`);
const copyButtons = document.querySelectorAll('.code-block-copy');
if (copyButtons.length === 0) {
console.log(`[文章脚本:${scriptInstanceId}] 未找到代码复制按钮`);
return;
}
console.log(`[文章脚本:${scriptInstanceId}] 找到代码复制按钮数量:`, copyButtons.length);
copyButtons.forEach(button => {
addListener(button, 'click', async () => {
try {
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');
2025-05-04 03:44:10 +08:00
button.innerHTML = `
<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>
已复制
`;
setTimeout(() => {
button.classList.remove('copied');
button.innerHTML = originalHTML;
}, 2000);
} catch (err) {
console.error(`[文章脚本:${scriptInstanceId}] 复制失败:`, err);
button.innerHTML = `
<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(() => {
button.innerHTML = `
<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);
}
});
});
}
// 2. 阅读进度条
function setupProgressBar() {
console.log(`[文章脚本:${scriptInstanceId}] 初始化阅读进度条`);
const progressBar = document.getElementById("progress-bar");
const backToTopButton = document.getElementById("back-to-top");
if (!progressBar) {
console.warn(`[文章脚本:${scriptInstanceId}] 未找到进度条元素`);
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"
);
}
}
}
addListener(window, "scroll", updateReadingProgress);
if (backToTopButton) {
addListener(backToTopButton, "click", () => {
window.scrollTo({
top: 0,
behavior: "smooth",
});
});
}
// 初始更新一次进度条
updateReadingProgress();
}
// 3. 目录交互
function setupTableOfContents() {
console.log(`[文章脚本:${scriptInstanceId}] 初始化目录交互`);
const tocContent = document.getElementById("toc-content");
const tocPanel = document.querySelector("#toc-panel");
if (!tocPanel || !tocContent) {
console.warn(`[文章脚本:${scriptInstanceId}] 未找到目录面板或内容`);
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");
}
}
addListener(window, "resize", checkTocVisibility);
checkTocVisibility();
// 处理目录链接点击跳转
const tocLinks = tocContent.querySelectorAll("a");
console.log(`[文章脚本:${scriptInstanceId}] 找到目录链接数量:`, tocLinks.length);
tocLinks.forEach(link => {
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);
}
});
});
// 监听滚动以更新当前活动的目录项
const article = document.querySelector("article");
if (!article) {
console.warn(`[文章脚本:${scriptInstanceId}] 未找到文章内容元素`);
return;
}
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;
}
}
// 高亮当前标题对应的目录项
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
}
addListener(window, "scroll", () => {
if (!ticking) {
window.requestAnimationFrame(() => {
updateActiveHeading();
ticking = false;
});
ticking = true;
}
});
updateActiveHeading();
}
// 4. Mermaid图表渲染
function setupMermaid() {
console.log(`[文章脚本:${scriptInstanceId}] 检查Mermaid图表`);
// 查找所有mermaid代码块
const mermaidBlocks = document.querySelectorAll(
'pre.language-mermaid, pre > code.language-mermaid, .mermaid'
);
if (mermaidBlocks.length === 0) {
console.log(`[文章脚本:${scriptInstanceId}] 未找到Mermaid图表`);
return;
}
console.log(`[文章脚本:${scriptInstanceId}] 找到Mermaid图表数量:`, mermaidBlocks.length);
// 动态加载mermaid库
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js';
script.onload = function() {
console.log(`[文章脚本:${scriptInstanceId}] Mermaid库加载成功`);
if (!window.mermaid) {
console.error(`[文章脚本:${scriptInstanceId}] Mermaid库加载后window.mermaid不存在`);
return;
}
// 初始化mermaid配置
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(`[文章脚本:${scriptInstanceId}] 开始渲染Mermaid图表`);
window.mermaid.run().catch(err => {
console.error(`[文章脚本:${scriptInstanceId}] Mermaid渲染出错:`, err);
});
} catch (error) {
console.error(`[文章脚本:${scriptInstanceId}] 初始化Mermaid渲染失败:`, error);
}
};
script.onerror = function() {
console.error(`[文章脚本:${scriptInstanceId}] 加载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);
// 添加Mermaid清理任务
customCleanupTasks.push(() => {
console.log(`[文章脚本:${scriptInstanceId}] 执行Mermaid特殊清理`);
// 移除脚本标签
if (script.parentNode) {
script.parentNode.removeChild(script);
}
// 清除全局mermaid对象
if (window.mermaid) {
console.log(`[文章脚本:${scriptInstanceId}] 清除window.mermaid对象`);
try {
// 尝试清理mermaid内部状态
if (typeof window.mermaid.destroy === 'function') {
window.mermaid.destroy();
}
window.mermaid = undefined;
} catch (e) {
console.error(`[文章脚本:${scriptInstanceId}] 清理mermaid对象出错:`, e);
}
}
// 移除页面上可能留下的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) {
console.log(`[文章脚本:${scriptInstanceId}] 移除Mermaid元素:`, el.id || el.className);
el.parentNode.removeChild(el);
}
});
} catch (e) {
console.error(`[文章脚本:${scriptInstanceId}] 清理Mermaid元素时出错:`, e);
}
});
}
// 启动所有功能
setupCodeCopy();
setupProgressBar();
setupTableOfContents();
setupMermaid();
console.log(`[文章脚本:${scriptInstanceId}] 所有功能初始化完成`);
}
// 执行初始化
registerCleanupEvents();
initializeFeatures();
})();
</script>