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

717 lines
24 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.tsx";
import { ARTICLE_EXPIRY_CONFIG } from "@/consts";
2025-03-03 21:16:16 +08:00
// 添加这一行告诉Astro预渲染这个页面
export const prerender = true;
export async function getStaticPaths() {
const articles = await getCollection("articles");
const views = ["grid", "timeline"];
// 为每篇文章生成路由参数
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,
view: undefined,
},
2025-03-10 14:05:40 +08:00
});
2025-03-10 17:35:21 +08:00
// 为每个视图添加路由
for (const view of views) {
paths.push({
params: { id: `${path}/${view}` },
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,
view,
},
2025-03-10 17:35:21 +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-03-10 14:05:40 +08:00
const { article, section, originalId, view } = Astro.props;
// 如果有原始ID使用它来渲染内容
const articleToRender = originalId ? { ...article, id: originalId } : article;
2025-03-03 21:16:16 +08:00
// 渲染文章内容
const { Content } = await render(articleToRender);
2025-03-03 21:16:16 +08:00
// 获取面包屑导航
const breadcrumbs = section ? section.split("/") : [];
2025-03-03 21:16:16 +08:00
// 获取相关文章
const allArticles = await getCollection("articles");
2025-03-03 21:16:16 +08:00
const relatedArticles = allArticles
.filter(
(a) =>
a.id !== article.id &&
a.data.tags &&
article.data.tags &&
a.data.tags.some((tag) => article.data.tags?.includes(tag)),
)
2025-03-03 21:16:16 +08:00
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime())
.slice(0, 3);
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)}${view ? `/${view}` : ""}`;
}
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}
author={article.data.author}
tags={article.data.tags}
image={article.data.image}
>
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- 阅读进度条 -->
<div
class="fixed top-0 left-0 w-full h-1 bg-transparent z-50"
id="progress-container 9"
>
<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-dark-card rounded-xl p-4 mb-6 shadow-lg border border-gray-200 dark:border-gray-700"
>
<div
class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3"
>
<div class="overflow-x-auto">
<Breadcrumb
pageType="article"
pathSegments={breadcrumbs}
articleTitle={article.data.title}
2025-04-19 22:17:33 +08:00
client:load
/>
</div>
<div class="flex items-center shrink-0">
2025-03-03 21:16:16 +08:00
{/* 返回按钮 */}
<a
href={`/articles${view ? `/${view}` : ""}`}
class="text-secondary-500 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400 flex items-center text-sm"
>
<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="M10 19l-7-7m0 0l7-7m-7 7h18"
></path>
2025-03-03 21:16:16 +08:00
</svg>
返回文章列表
</a>
</div>
</div>
2025-03-08 18:16:42 +08:00
</div>
<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>
2025-03-08 18:16:42 +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
href={`/articles?path=${encodeURIComponent(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) => (
<a
href={`/articles?tag=${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"
>
#{tag}
</a>
))}
</div>
)
}
2025-03-08 18:16:42 +08:00
</header>
2025-03-08 18:16:42 +08:00
<!-- 文章内容区域 -->
<div class="relative">
<!-- 文章过期提醒 -->
{
(() => {
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-6">
<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-03-03 21:16:16 +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-dark-surface 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-dark-surface 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"
>
2025-03-03 21:16:16 +08:00
<Content />
</article>
2025-03-08 18:16:42 +08:00
<!-- 固定目录面板 - 脱离文档流 -->
<div
class="hidden 2xl:block fixed right-[calc(50%-48rem)] top-20 w-64 z-30"
>
<div
class="bg-white dark:bg-dark-card rounded-lg shadow-lg p-4 max-h-[calc(100vh-8rem)] overflow-y-auto border border-gray-200 dark:border-gray-700"
>
<div
class="border-b border-secondary-100 dark:border-dark-border pb-2 mb-3 sticky top-0 bg-white dark:bg-dark-card"
>
<h3 class="font-bold text-primary-700 dark:text-primary-400">
文章目录
</h3>
2025-03-08 18:16:42 +08:00
</div>
<div
id="toc-content"
class="text-sm"
>
2025-03-08 18:16:42 +08:00
<!-- 目录内容将通过JavaScript动态生成 -->
2025-03-03 21:16:16 +08:00
</div>
</div>
2025-03-08 18:16:42 +08:00
</div>
2025-03-03 21:16:16 +08:00
</div>
2025-03-08 18:16:42 +08:00
<!-- 相关文章 -->
{
relatedArticles.length > 0 && (
<div class="mt-12 pt-8 border-t border-secondary-200 dark:border-dark-border">
<h2 class="text-2xl font-bold mb-6 text-primary-900 dark:text-primary-100">
相关文章
</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
{relatedArticles.map((relatedArticle) => (
<a
href={getArticleUrl(relatedArticle.id)}
class="block p-5 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-dark-card hover:shadow-xl hover:-translate-y-1 shadow-lg"
>
<h3 class="font-bold text-lg mb-2 line-clamp-2 text-gray-800 dark:text-gray-200 hover:text-primary-700 dark:hover:text-primary-400">
{relatedArticle.data.title}
</h3>
<p class="text-sm text-secondary-600 dark:text-secondary-400 mb-2">
{relatedArticle.data.date.toLocaleDateString("zh-CN")}
</p>
{relatedArticle.data.summary && (
<p class="text-sm text-secondary-700 dark:text-secondary-300 line-clamp-3">
{relatedArticle.data.summary}
</p>
)}
</a>
))}
</div>
2025-03-08 18:16:42 +08:00
</div>
)
}
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>
// 阅读进度条
const progressBar = document.getElementById("progress-bar");
const backToTopButton = document.getElementById("back-to-top");
2025-03-03 21:16:16 +08:00
function updateReadingProgress() {
const scrollTop = window.scrollY || document.documentElement.scrollTop;
const scrollHeight =
document.documentElement.scrollHeight -
document.documentElement.clientHeight;
2025-03-03 21:16:16 +08:00
const progress = (scrollTop / scrollHeight) * 100;
2025-03-03 21:16:16 +08:00
if (progressBar) {
progressBar.style.width = `${progress}%`;
}
2025-03-03 21:16:16 +08:00
// 显示/隐藏返回顶部按钮
if (backToTopButton) {
if (scrollTop > 300) {
backToTopButton.classList.add(
"opacity-100",
"visible",
"translate-y-0",
);
backToTopButton.classList.remove(
"opacity-0",
"invisible",
"translate-y-5",
);
2025-03-03 21:16:16 +08:00
} else {
backToTopButton.classList.add(
"opacity-0",
"invisible",
"translate-y-5",
);
backToTopButton.classList.remove(
"opacity-100",
"visible",
"translate-y-0",
);
2025-03-03 21:16:16 +08:00
}
}
}
2025-03-03 21:16:16 +08:00
// 返回顶部功能
if (backToTopButton) {
backToTopButton.addEventListener("click", () => {
2025-03-03 21:16:16 +08:00
window.scrollTo({
top: 0,
behavior: "smooth",
2025-03-03 21:16:16 +08:00
});
});
}
2025-03-03 21:16:16 +08:00
// 监听滚动事件
window.addEventListener("scroll", updateReadingProgress);
2025-03-03 21:16:16 +08:00
// 初始化
updateReadingProgress();
2025-03-08 18:16:42 +08:00
// 目录功能
document.addEventListener("DOMContentLoaded", () => {
const tocContent = document.getElementById("toc-content");
const tocPanel = document.querySelector(
'[class*="2xl:block"][class*="fixed"]',
);
2025-03-08 18:16:42 +08:00
// 检查是否有足够空间显示目录
function checkTocVisibility() {
if (!tocPanel) return;
2025-03-08 18:16:42 +08:00
// 如果窗口宽度小于1536px (2xl breakpoint),隐藏目录
if (window.innerWidth < 1536) {
tocPanel.classList.add("hidden");
tocPanel.classList.remove("2xl:block");
2025-03-08 18:16:42 +08:00
} else {
tocPanel.classList.remove("hidden");
tocPanel.classList.add("2xl:block");
2025-03-08 18:16:42 +08:00
}
}
2025-03-08 18:16:42 +08:00
// 监听窗口大小变化
window.addEventListener("resize", checkTocVisibility);
2025-03-08 18:16:42 +08:00
// 初始检查
checkTocVisibility();
2025-03-08 18:16:42 +08:00
// 生成目录内容
function generateTableOfContents() {
// 获取文章中的所有标题元素
const article = document.querySelector("article");
2025-03-08 18:16:42 +08:00
if (!article || !tocContent) {
console.error("找不到文章内容或目录容器");
2025-03-08 18:16:42 +08:00
if (tocContent) {
tocContent.innerHTML =
'<p class="text-secondary-500 dark:text-secondary-400 italic">无法生成目录</p>';
2025-03-08 18:16:42 +08:00
}
return;
}
const headings = article.querySelectorAll("h1, h2, h3, h4, h5, h6");
2025-03-08 18:16:42 +08:00
if (headings.length === 0) {
tocContent.innerHTML =
'<p class="text-secondary-500 dark:text-secondary-400 italic">此文章没有目录</p>';
2025-03-08 18:16:42 +08:00
return;
}
2025-03-08 18:16:42 +08:00
// 创建目录列表
const tocList = document.createElement("ul");
tocList.className = "space-y-2";
2025-03-08 18:16:42 +08:00
// 为每个标题创建目录项
headings.forEach((heading, index) => {
// 为每个标题添加ID如果没有的话
if (!heading.id) {
heading.id = `heading-${index}`;
}
2025-03-08 18:16:42 +08:00
// 创建目录项
const listItem = document.createElement("li");
2025-03-08 18:16:42 +08:00
// 根据标题级别设置缩进
const headingLevel = parseInt(heading.tagName.substring(1));
const indent = (headingLevel - 1) * 0.75; // 每级缩进0.75rem
2025-03-08 18:16:42 +08:00
// 创建链接
const link = document.createElement("a");
2025-03-08 18:16:42 +08:00
link.href = `#${heading.id}`;
link.className = `block hover:text-primary-600 dark:hover:text-primary-400 duration-50 ${headingLevel > 2 ? "text-secondary-600 dark:text-secondary-400" : "text-secondary-800 dark:text-secondary-200 font-medium"}`;
2025-03-08 18:16:42 +08:00
link.style.paddingLeft = `${indent}rem`;
link.textContent = heading.textContent;
2025-03-08 18:16:42 +08:00
// 点击链接时滚动到目标位置
link.addEventListener("click", (e) => {
2025-03-08 18:16:42 +08:00
// 平滑滚动到目标位置
e.preventDefault();
const targetId = link.getAttribute("href")?.substring(1);
2025-03-08 18:16:42 +08:00
if (!targetId) return;
2025-03-08 18:16:42 +08:00
const targetElement = document.getElementById(targetId);
2025-03-08 18:16:42 +08:00
if (targetElement) {
// 滚动到目标位置,并添加一些偏移以避免被固定导航遮挡
const offset = 100; // 可以根据需要调整
const targetPosition =
targetElement.getBoundingClientRect().top +
window.scrollY -
offset;
2025-03-08 18:16:42 +08:00
window.scrollTo({
top: targetPosition,
behavior: "smooth",
2025-03-08 18:16:42 +08:00
});
2025-03-08 18:16:42 +08:00
// 高亮目标标题
targetElement.classList.add(
"bg-primary-50",
"dark:bg-primary-900/20",
);
2025-03-08 18:16:42 +08:00
setTimeout(() => {
targetElement.classList.remove(
"bg-primary-50",
"dark:bg-primary-900/20",
);
2025-03-08 18:16:42 +08:00
}, 2000);
}
});
2025-03-08 18:16:42 +08:00
listItem.appendChild(link);
tocList.appendChild(listItem);
});
2025-03-08 18:16:42 +08:00
// 将目录添加到面板
tocContent.innerHTML = "";
2025-03-08 18:16:42 +08:00
tocContent.appendChild(tocList);
}
2025-03-08 18:16:42 +08:00
// 页面加载时生成目录
try {
generateTableOfContents();
2025-03-08 18:16:42 +08:00
// 添加滚动监听,更新目录高亮
function updateActiveHeading() {
const article = document.querySelector("article");
2025-03-08 18:16:42 +08:00
if (!article || !tocContent) return;
const headings = Array.from(
article.querySelectorAll("h1, h2, h3, h4, h5, h6"),
);
2025-03-08 18:16:42 +08:00
if (headings.length === 0) return;
2025-03-08 18:16:42 +08:00
// 获取所有目录链接
const tocLinks = Array.from(tocContent.querySelectorAll("a"));
2025-03-08 18:16:42 +08:00
if (tocLinks.length === 0) return;
2025-03-08 18:16:42 +08:00
// 移除所有活跃状态
tocLinks.forEach((link) => {
link.classList.remove(
"text-primary-600",
"dark:text-primary-400",
"font-medium",
);
2025-03-08 18:16:42 +08:00
});
2025-03-08 18:16:42 +08:00
// 找到当前视口中最靠近顶部的标题
let currentHeading = null;
const scrollPosition = window.scrollY + 150; // 添加一些偏移量
2025-03-08 18:16:42 +08:00
for (const heading of headings) {
const headingTop =
heading.getBoundingClientRect().top + window.scrollY;
2025-03-08 18:16:42 +08:00
if (headingTop <= scrollPosition) {
currentHeading = heading;
} else {
break;
}
}
2025-03-08 18:16:42 +08:00
// 如果找到当前标题,高亮对应的目录项
if (currentHeading) {
const activeLink = tocLinks.find(
(link) => link.getAttribute("href") === `#${currentHeading.id}`,
);
2025-03-08 18:16:42 +08:00
if (activeLink) {
activeLink.classList.add(
"text-primary-600",
"dark:text-primary-400",
"font-medium",
);
2025-03-08 18:16:42 +08:00
}
}
}
2025-03-08 18:16:42 +08:00
// 监听滚动事件,使用节流函数优化性能
let ticking = false;
window.addEventListener("scroll", () => {
2025-03-08 18:16:42 +08:00
if (!ticking) {
window.requestAnimationFrame(() => {
updateActiveHeading();
ticking = false;
});
ticking = true;
}
});
2025-03-08 18:16:42 +08:00
// 初始化高亮
updateActiveHeading();
} catch (error) {
console.error("生成目录时发生错误:", error);
2025-03-08 18:16:42 +08:00
if (tocContent) {
tocContent.innerHTML =
'<p class="text-secondary-500 dark:text-secondary-400 italic">生成目录时发生错误</p>';
2025-03-08 18:16:42 +08:00
}
}
});
2025-03-03 21:16:16 +08:00
// 代码块增强功能
document.addEventListener("DOMContentLoaded", () => {
2025-03-03 21:16:16 +08:00
// 处理所有代码块
const codeBlocks = document.querySelectorAll("pre");
codeBlocks.forEach((pre) => {
2025-03-03 21:16:16 +08:00
// 获取代码语言
const code = pre.querySelector("code");
2025-03-03 21:16:16 +08:00
if (!code) return;
2025-03-03 21:16:16 +08:00
// 从类名中提取语言
const className = code.className;
const languageMatch = className.match(/language-(\w+)/);
const language = languageMatch ? languageMatch[1] : "text";
2025-03-03 21:16:16 +08:00
// 创建顶部栏
const header = document.createElement("div");
header.className =
"code-header flex justify-between items-center text-xs px-4 py-2 bg-secondary-800 dark:bg-dark-card text-secondary-300 dark:text-secondary-400 rounded-t-lg";
2025-03-03 21:16:16 +08:00
// 创建语言标签
const languageLabel = document.createElement("span");
languageLabel.className = "code-language font-mono";
2025-03-03 21:16:16 +08:00
languageLabel.textContent = language;
2025-03-03 21:16:16 +08:00
// 创建复制按钮
const copyButton = document.createElement("button");
copyButton.className =
"code-copy-button flex items-center gap-1 hover:text-white dark:hover:text-primary-400 transition-colors";
2025-03-03 21:16:16 +08:00
// 创建SVG图标和文本
const copyIcon = `<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>`;
const successIcon = `<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>`;
const errorIcon = `<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="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>`;
2025-03-03 21:16:16 +08:00
copyButton.innerHTML = `${copyIcon}<span>复制</span>`;
copyButton.setAttribute("aria-label", "复制代码");
copyButton.setAttribute("title", "复制代码到剪贴板");
2025-03-03 21:16:16 +08:00
// 添加复制功能
copyButton.addEventListener("click", (e) => {
2025-03-03 21:16:16 +08:00
e.stopPropagation();
2025-03-03 21:16:16 +08:00
// 获取代码文本
const codeText = code.textContent || "";
2025-03-03 21:16:16 +08:00
// 复制到剪贴板
navigator.clipboard
.writeText(codeText)
2025-03-03 21:16:16 +08:00
.then(() => {
// 复制成功,更改按钮文本
copyButton.innerHTML = `${successIcon}<span>已复制</span>`;
copyButton.classList.add("text-green-400");
2025-03-03 21:16:16 +08:00
// 2秒后恢复按钮文本
setTimeout(() => {
copyButton.innerHTML = `${copyIcon}<span>复制</span>`;
copyButton.classList.remove("text-green-400");
2025-03-03 21:16:16 +08:00
}, 2000);
})
.catch(() => {
// 复制失败,更改按钮文本
copyButton.innerHTML = `${errorIcon}<span>失败</span>`;
copyButton.classList.add("text-red-400");
2025-03-03 21:16:16 +08:00
// 2秒后恢复按钮文本
setTimeout(() => {
copyButton.innerHTML = `${copyIcon}<span>复制</span>`;
copyButton.classList.remove("text-red-400");
2025-03-03 21:16:16 +08:00
}, 2000);
});
});
2025-03-03 21:16:16 +08:00
// 将语言标签和复制按钮添加到顶部栏
header.appendChild(languageLabel);
header.appendChild(copyButton);
2025-03-03 21:16:16 +08:00
// 将顶部栏插入到代码块的最前面
pre.insertBefore(header, pre.firstChild);
2025-03-03 21:16:16 +08:00
// 调整代码块样式
pre.classList.add("rounded-b-lg", "mt-0");
pre.style.marginTop = "0";
2025-03-03 21:16:16 +08:00
});
});
</script>