mirror of
https://github.com/Ad-closeNN/blog-fuwari.git
synced 2026-05-31 01:00:04 -04:00
687 lines
35 KiB
Plaintext
687 lines
35 KiB
Plaintext
---
|
|
import { type CollectionEntry, render } from "astro:content";
|
|
import path from "node:path";
|
|
import License from "@components/misc/License.astro";
|
|
import Markdown from "@components/misc/Markdown.astro";
|
|
import I18nKey from "@i18n/i18nKey";
|
|
import { i18n } from "@i18n/translation";
|
|
import MainGridLayout from "@layouts/MainGridLayout.astro";
|
|
import { getSortedPosts } from "@utils/content-utils";
|
|
import { getDir, getPostUrlBySlug } from "@utils/url-utils";
|
|
import { Icon } from "astro-icon/components";
|
|
import { licenseConfig } from "src/config";
|
|
import ImageWrapper from "../../components/misc/ImageWrapper.astro";
|
|
import PostMetadata from "../../components/PostMeta.astro";
|
|
import { profileConfig, siteConfig } from "../../config";
|
|
import { formatDateToYYYYMMDD } from "../../utils/date-utils";
|
|
|
|
export async function getStaticPaths() {
|
|
const blogEntries = await getSortedPosts();
|
|
return blogEntries.map((entry) => ({
|
|
params: { slug: entry.id },
|
|
props: { entry },
|
|
}));
|
|
}
|
|
|
|
interface Props {
|
|
entry: CollectionEntry<"posts">;
|
|
}
|
|
|
|
const { entry }: Props = Astro.props;
|
|
const { Content, headings, remarkPluginFrontmatter } = await render(entry);
|
|
const postSlug = entry.id;
|
|
const lastUpdatedAt = entry.data.updated ?? entry.data.published;
|
|
const lastUpdatedLabel = formatDateToYYYYMMDD(lastUpdatedAt);
|
|
const lastUpdatedTimestamp = lastUpdatedAt.getTime();
|
|
|
|
const postSources = import.meta.glob("../../content/posts/**/*.{md,mdx}", {
|
|
query: "?raw",
|
|
import: "default",
|
|
eager: true,
|
|
}) as Record<string, string>;
|
|
const normalizedPostSources = Object.fromEntries(
|
|
Object.entries(postSources).map(([key, source]) => [
|
|
key.toLowerCase(),
|
|
source,
|
|
]),
|
|
);
|
|
|
|
function getPostSourceKey(entryId: string) {
|
|
const normalizedEntryPath = `../../content/posts/${entryId}`.toLowerCase();
|
|
return Object.keys(postSources).find((key) => {
|
|
const normalizedKey = key.toLowerCase();
|
|
return (
|
|
normalizedKey === normalizedEntryPath ||
|
|
normalizedKey === `${normalizedEntryPath}.md` ||
|
|
normalizedKey === `${normalizedEntryPath}.mdx`
|
|
);
|
|
});
|
|
}
|
|
|
|
function getPostSourcePath(sourceKey: string | undefined) {
|
|
return sourceKey?.replace("../../content/posts/", "src/content/posts/");
|
|
}
|
|
|
|
function getGitHubPostSourceUrl(
|
|
repo: string | undefined,
|
|
sourcePath: string | undefined,
|
|
) {
|
|
if (!repo || !sourcePath) return "";
|
|
|
|
const repoUrl = repo.includes("://") ? repo : `https://github.com/${repo}`;
|
|
const normalizedRepo = repoUrl.replace(/\.git$/, "").replace(/\/+$/, "");
|
|
const encodedSourcePath = sourcePath
|
|
.split("/")
|
|
.map(encodeURIComponent)
|
|
.join("/");
|
|
return `${normalizedRepo}/blob/main/${encodedSourcePath}?plain=1`;
|
|
}
|
|
|
|
function getGitHubRawSourceUrl(
|
|
repo: string | undefined,
|
|
sourcePath: string | undefined,
|
|
) {
|
|
if (!repo || !sourcePath) return "";
|
|
|
|
const normalizedRepo = repo.replace(/\.git$/, "").replace(/\/+$/, "");
|
|
const repoPath = normalizedRepo.includes("://")
|
|
? new URL(normalizedRepo).pathname.replace(/^\/+|\/+$/g, "")
|
|
: normalizedRepo.replace(/^\/+|\/+$/g, "");
|
|
const [owner, repository] = repoPath.split("/");
|
|
if (!owner || !repository) return "";
|
|
|
|
const encodedSourcePath = sourcePath
|
|
.split("/")
|
|
.map(encodeURIComponent)
|
|
.join("/");
|
|
return `https://raw.githubusercontent.com/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/refs/heads/main/${encodedSourcePath}`;
|
|
}
|
|
|
|
function getGiteaRawSourceUrl(
|
|
repo: string | undefined,
|
|
sourcePath: string | undefined,
|
|
giteaHost: string,
|
|
) {
|
|
if (!repo || !sourcePath) return "";
|
|
|
|
const normalizedRepo = repo.replace(/\.git$/, "").replace(/\/+$/, "");
|
|
const repoPath = normalizedRepo.includes("://")
|
|
? new URL(normalizedRepo).pathname.replace(/^\/+|\/+$/g, "")
|
|
: normalizedRepo.replace(/^\/+|\/+$/g, "");
|
|
const [owner, repository] = repoPath.split("/");
|
|
if (!owner || !repository) return "";
|
|
|
|
const encodedSourcePath = sourcePath
|
|
.split("/")
|
|
.map(encodeURIComponent)
|
|
.join("/");
|
|
return `https://${giteaHost}/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/src/branch/main/${encodedSourcePath}?display=source`;
|
|
}
|
|
|
|
function getCnbsSourceUrl(baseUrl: string, sourcePath: string | undefined) {
|
|
if (!sourcePath) return "";
|
|
|
|
const normalizedUrl = baseUrl.replace(/\.git$/, "").replace(/\/+$/, "");
|
|
const encodedSourcePath = sourcePath
|
|
.split("/")
|
|
.map(encodeURIComponent)
|
|
.join("/");
|
|
return `${normalizedUrl}/-/blob/main/${encodedSourcePath}`;
|
|
}
|
|
|
|
function parseAiSummaryValue(value: string) {
|
|
const trimmed = value.trim();
|
|
if (!trimmed) return "";
|
|
if (trimmed.startsWith('"')) {
|
|
try {
|
|
return JSON.parse(trimmed);
|
|
} catch {
|
|
return trimmed;
|
|
}
|
|
}
|
|
return trimmed;
|
|
}
|
|
|
|
function extractFrontmatterValue(frontmatter: string, key: string) {
|
|
const valueMatch = frontmatter.match(new RegExp(`^${key}:[ \\t]*(.*)$`, "m"));
|
|
if (!valueMatch) return "";
|
|
|
|
const inlineValue = valueMatch[1].trim();
|
|
if (inlineValue !== ">" && inlineValue !== "|") {
|
|
return parseAiSummaryValue(inlineValue);
|
|
}
|
|
|
|
const afterValue = frontmatter.slice(
|
|
valueMatch.index! + valueMatch[0].length,
|
|
);
|
|
const lines = afterValue.split(/\r?\n/);
|
|
const blockLines = [];
|
|
for (const line of lines) {
|
|
if (!line.startsWith(" ") && line.trim()) break;
|
|
blockLines.push(line.replace(/^ {1,2}/, ""));
|
|
}
|
|
return blockLines.join("\n").trim();
|
|
}
|
|
|
|
function getAiSummaryMeta(entryId: string) {
|
|
const source =
|
|
normalizedPostSources[`../../content/posts/${entryId}`.toLowerCase()] ??
|
|
normalizedPostSources[`../../content/posts/${entryId}.md`.toLowerCase()] ??
|
|
normalizedPostSources[`../../content/posts/${entryId}.mdx`.toLowerCase()];
|
|
const frontmatter = source?.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1];
|
|
return {
|
|
summary: frontmatter
|
|
? extractFrontmatterValue(frontmatter, "aiSummary")
|
|
: "",
|
|
model: frontmatter
|
|
? extractFrontmatterValue(frontmatter, "aiSummaryModel")
|
|
: "",
|
|
};
|
|
}
|
|
|
|
const aiSummaryMeta = getAiSummaryMeta(entry.id);
|
|
const postSourceKey = getPostSourceKey(entry.id);
|
|
const postSourcePath = getPostSourcePath(postSourceKey);
|
|
const postSourceUrl = getGitHubPostSourceUrl(
|
|
siteConfig.githubRepo,
|
|
postSourcePath,
|
|
);
|
|
const postRawSourceUrl = getGitHubRawSourceUrl(
|
|
siteConfig.githubRepo,
|
|
postSourcePath,
|
|
);
|
|
const postGiteaRawSourceUrl = getGiteaRawSourceUrl(
|
|
siteConfig.githubRepo,
|
|
postSourcePath,
|
|
"git.adclosenn.top",
|
|
);
|
|
const postCnbsSourceUrl = getCnbsSourceUrl(
|
|
"https://cnb.cool/CLN-Grated/blog-fuwari",
|
|
postSourcePath,
|
|
);
|
|
const aiPrompt = `Read from ${postRawSourceUrl} so I can ask questions about it.`;
|
|
const encodedAiPrompt = encodeURIComponent(aiPrompt);
|
|
const copyPageAiLinks = [
|
|
{
|
|
label: "Gitea",
|
|
icon: "cln-gitea",
|
|
href: postGiteaRawSourceUrl,
|
|
},
|
|
{
|
|
label: "CNB",
|
|
icon: "cnb",
|
|
href: postCnbsSourceUrl,
|
|
},
|
|
{
|
|
label: "ChatGPT",
|
|
icon: "gpt",
|
|
href: `https://chatgpt.com/?q=${encodedAiPrompt}`,
|
|
},
|
|
{
|
|
label: "Gemini",
|
|
icon: "gemini",
|
|
href: `https://gemini.google.com/app?q=${encodedAiPrompt}`,
|
|
},
|
|
{
|
|
label: "Claude",
|
|
icon: "claude",
|
|
href: `https://claude.ai/new?q=${encodedAiPrompt}`,
|
|
},
|
|
{
|
|
label: "Grok",
|
|
icon: "grok",
|
|
href: `https://grok.com/?q=${encodedAiPrompt}`,
|
|
},
|
|
];
|
|
|
|
const jsonLd = {
|
|
"@context": "https://schema.org",
|
|
"@type": "BlogPosting",
|
|
headline: entry.data.title,
|
|
description: entry.data.description || entry.data.title,
|
|
keywords: entry.data.tags,
|
|
author: {
|
|
"@type": "Person",
|
|
name: profileConfig.name,
|
|
url: Astro.site,
|
|
},
|
|
datePublished: formatDateToYYYYMMDD(entry.data.published),
|
|
inLanguage: entry.data.lang
|
|
? entry.data.lang.replace("_", "-")
|
|
: siteConfig.lang.replace("_", "-"),
|
|
// TODO include cover image here
|
|
};
|
|
|
|
// 获取头图 boolean,无需设置 true
|
|
const showcover = entry.data.showcover;
|
|
|
|
// 获取自定义的头图(Markdown 内部)
|
|
const customcover = entry.data.customcover;
|
|
const isOutdated = entry.data.outdated;
|
|
---
|
|
<MainGridLayout banner={entry.data.image} title={entry.data.title} description={entry.data.description} lang={entry.data.lang} setOGTypeArticle={true} headings={headings}>
|
|
<script is:inline slot="head" type="application/ld+json" set:html={JSON.stringify(jsonLd)}></script>
|
|
<div class="flex w-full rounded-[var(--radius-large)] overflow-hidden relative mb-4">
|
|
<div id="post-container" class:list={["card-base z-10 px-6 md:px-9 pt-6 pb-4 relative w-full ",
|
|
{}
|
|
]}>
|
|
<!-- word count and reading time -->
|
|
<div class="flex flex-col gap-3 mb-3 transition onload-animation md:flex-row md:items-start md:justify-between">
|
|
<div class="flex flex-row flex-wrap text-black/30 dark:text-white/30 gap-5 transition">
|
|
<div class="flex flex-row items-center pointer-events-none select-none cursor-default">
|
|
<div class="transition h-6 w-6 rounded-md bg-black/5 dark:bg-white/10 text-black/50 dark:text-white/50 flex items-center justify-center mr-2">
|
|
<Icon is:inline name="material-symbols:notes-rounded"></Icon>
|
|
</div>
|
|
<div class="text-sm">{remarkPluginFrontmatter.words} {" " + i18n(I18nKey.wordsCount)}</div>
|
|
</div>
|
|
<div class="flex flex-row items-center pointer-events-none select-none cursor-default">
|
|
<div class="transition h-6 w-6 rounded-md bg-black/5 dark:bg-white/10 text-black/50 dark:text-white/50 flex items-center justify-center mr-2">
|
|
<Icon is:inline name="material-symbols:schedule-outline-rounded"></Icon>
|
|
</div>
|
|
<div class="text-sm">
|
|
大约 {remarkPluginFrontmatter.minutes} {" " + i18n(remarkPluginFrontmatter.minutes === 1 ? I18nKey.minuteCount : I18nKey.minutesCount)}
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-row items-center pointer-events-none select-none cursor-default">
|
|
<div class="transition h-6 w-6 rounded-md bg-black/5 dark:bg-white/10 text-black/50 dark:text-white/50 flex items-center justify-center mr-2">
|
|
<Icon is:inline name="material-symbols:visibility-outline-rounded"></Icon>
|
|
</div>
|
|
<div class="text-sm" id="post-top-page-views">加载中...</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center justify-end gap-2 text-black/40 dark:text-white/40 md:shrink-0">
|
|
{postSourceUrl && (
|
|
<div id="copy-page-menu" class="relative inline-flex shrink-0 select-none rounded-xl border border-black/15 dark:border-white/15">
|
|
<a
|
|
href={postSourceUrl}
|
|
aria-label="在 GitHub 打开文章源文件"
|
|
title="在 GitHub 打开文章源文件"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="btn-card flex h-9 items-center gap-2 rounded-l-xl rounded-r-none px-3 font-medium"
|
|
>
|
|
<Icon is:inline name="fa6-brands:github" class="text-[1rem] text-[var(--primary)]"></Icon>
|
|
<span class="text-sm text-black/75 dark:text-white/75">在 GitHub 查看</span>
|
|
</a>
|
|
<div class="h-9 w-px bg-black/10 dark:bg-white/10"></div>
|
|
<button id="copy-page-switch" class="btn-card flex h-9 w-9 items-center justify-center rounded-l-none rounded-r-xl active:scale-95" type="button" aria-label="打开 AI 菜单" aria-haspopup="menu" aria-expanded="false">
|
|
<Icon is:inline name="material-symbols:keyboard-arrow-down-rounded" class="copy-page-arrow text-[1.2rem] text-[var(--primary)] transition"></Icon>
|
|
</button>
|
|
<div id="copy-page-panel" class="pointer-events-none fixed z-[9999] w-56 -translate-y-1 select-none rounded-2xl border border-black/10 bg-[var(--float-panel-bg)] p-2 opacity-0 shadow-xl dark:border-white/10 dark:shadow-none" role="menu" style="transition: opacity 150ms ease-out, transform 150ms ease-out">
|
|
{copyPageAiLinks.map((link) => (
|
|
<a
|
|
href={link.href}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="flex w-full items-center justify-between gap-3 rounded-xl px-3 py-2 text-sm text-black/75 transition hover:bg-[var(--btn-plain-bg-hover)] active:bg-[var(--btn-plain-bg-active)] dark:text-white/75"
|
|
role="menuitem"
|
|
>
|
|
<span class="flex items-center gap-2">
|
|
{link.icon === "gpt" && (
|
|
<svg aria-hidden="true" class="h-4 w-4 text-[var(--primary)]" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5">
|
|
<path d="M11.745 14.85L6.905 12V7c0-2.21 1.824-4 4.076-4c1.397 0 2.63.69 3.365 1.741" />
|
|
<path d="M9.6 19.18A4.1 4.1 0 0 0 13.02 21c2.25 0 4.076-1.79 4.076-4v-5L12.16 9.097" />
|
|
<path d="M9.452 13.5V7.67l4.412-2.5c1.95-1.105 4.443-.45 5.569 1.463a3.93 3.93 0 0 1 .076 3.866" />
|
|
<path d="M4.49 13.5a3.93 3.93 0 0 0 .075 3.866c1.126 1.913 3.62 2.568 5.57 1.464l4.412-2.5l.096-5.596" />
|
|
<path d="M17.096 17.63a4.09 4.09 0 0 0 3.357-1.996c1.126-1.913.458-4.36-1.492-5.464l-4.413-2.5l-5.059 2.755" />
|
|
<path d="M6.905 6.37a4.09 4.09 0 0 0-3.358 1.996c-1.126 1.914-.458 4.36 1.492 5.464l4.413 2.5l5.048-2.75" />
|
|
</g>
|
|
</svg>
|
|
)}
|
|
{link.icon === "gemini" && (
|
|
<svg aria-hidden="true" class="h-4 w-4 text-[var(--primary)]" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
<path fill="currentColor" d="M21.996 12.018a10.65 10.65 0 0 0-9.98 9.98h-.04c-.32-5.364-4.613-9.656-9.976-9.98v-.04c5.363-.32 9.656-4.613 9.98-9.976h.04c.324 5.363 4.617 9.656 9.98 9.98v.036z" />
|
|
</svg>
|
|
)}
|
|
{link.icon === "claude" && (
|
|
<svg aria-hidden="true" class="h-4 w-4 text-[var(--primary)]" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
|
<path fill="currentColor" d="m3.127 10.604l3.135-1.76l.053-.153l-.053-.085H6.11l-.525-.032l-1.791-.048l-1.554-.065l-1.505-.08l-.38-.081L0 7.832l.036-.234l.32-.214l.455.04l1.009.069l1.513.105l1.097.064l1.626.17h.259l.036-.105l-.089-.065l-.068-.064l-1.566-1.062l-1.695-1.121l-.887-.646l-.48-.327l-.243-.306l-.104-.67l.435-.48l.585.04l.15.04l.593.456l1.267.981l1.654 1.218l.242.202l.097-.068l.012-.049l-.109-.181l-.9-1.626l-.96-1.655l-.428-.686l-.113-.411a2 2 0 0 1-.068-.484l.496-.674L4.446 0l.662.089l.279.242l.411.94l.666 1.48l1.033 2.014l.302.597l.162.553l.06.17h.105v-.097l.085-1.134l.157-1.392l.154-1.792l.052-.504l.25-.605l.497-.327l.387.186l.319.456l-.045.294l-.19 1.23l-.37 1.93l-.243 1.29h.142l.161-.16l.654-.868l1.097-1.372l.484-.545l.565-.601l.363-.287h.686l.505.751l-.226.775l-.707.895l-.585.759l-.839 1.13l-.524.904l.048.072l.125-.012l1.897-.403l1.024-.186l1.223-.21l.553.258l.06.263l-.218.536l-1.307.323l-1.533.307l-2.284.54l-.028.02l.032.04l1.029.098l.44.024h1.077l2.005.15l.525.346l.315.424l-.053.323l-.807.411l-3.631-.863l-.872-.218h-.12v.073l.726.71l1.331 1.202l1.667 1.55l.084.383l-.214.302l-.226-.032l-1.464-1.101l-.565-.497l-1.28-1.077h-.084v.113l.295.432l1.557 2.34l.08.718l-.112.234l-.404.141l-.444-.08l-.911-1.28l-.94-1.44l-.759-1.291l-.093.053l-.448 4.821l-.21.246l-.484.186l-.403-.307l-.214-.496l.214-.98l.258-1.28l.21-1.016l.19-1.263l.112-.42l-.008-.028l-.092.012l-.953 1.307l-1.448 1.957l-1.146 1.227l-.274.109l-.477-.247l.045-.44l.266-.39l1.586-2.018l.956-1.25l.617-.723l-.004-.105h-.036l-4.212 2.736l-.75.096l-.324-.302l.04-.496l.154-.162l1.267-.871z" />
|
|
</svg>
|
|
)}
|
|
{link.icon === "grok" && (
|
|
<svg aria-hidden="true" class="h-4 w-4 text-[var(--primary)]" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
<path fill="currentColor" d="M4.94 4.96a9.97 9.97 0 0 1 10.835-2.182a8.7 8.7 0 0 1 2.033 1.11l-3.006 1.39C12.003 4.101 8.797 4.9 6.84 6.86c-2.564 2.565-3.146 6.954-.36 9.922l.278.284L.124 23c1.875-1.973 3.771-4.427 2.636-7.19c-1.52-3.698-.635-8.03 2.18-10.85M23.9.1c-2.264 3.174-3.184 5.389-2.197 9.64l-.007-.007c.753 3.201-.052 6.75-2.653 9.355c-3.279 3.285-8.526 4.016-12.847 1.06L9.21 18.75c2.758 1.084 5.775.607 7.943-1.564c2.169-2.17 2.655-5.332 1.566-7.963c-.207-.5-.828-.625-1.263-.304L8.59 15.472l12.7-12.77v.01z" />
|
|
</svg>
|
|
)}
|
|
{link.icon === "cln-gitea" && (
|
|
<img aria-hidden="true" class="h-4 w-4" src="https://git.adclosenn.top/assets/img/logo.svg" alt="" />
|
|
)}
|
|
{link.icon === "cnb" && (
|
|
<img aria-hidden="true" class="h-4 w-4" src="https://cnb.cool/images/favicon.svg" alt="" />
|
|
)}
|
|
<span>在 {link.label} 中打开</span>
|
|
</span>
|
|
<Icon is:inline name="material-symbols:open-in-new-rounded" class="text-[0.95rem] text-[var(--primary)]"></Icon>
|
|
</a>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
<a
|
|
href={entry.data.nextSlug ? getPostUrlBySlug(entry.data.nextSlug) : "#"}
|
|
aria-label={entry.data.nextTitle ? `上一篇:${entry.data.nextTitle}` : "没有上一篇"}
|
|
title={entry.data.nextTitle || undefined}
|
|
class:list={[
|
|
"btn-card h-9 w-9 rounded-xl active:scale-95",
|
|
{"pointer-events-none opacity-40": !entry.data.nextSlug},
|
|
]}
|
|
>
|
|
<Icon is:inline name="material-symbols:chevron-left-rounded" class="text-[1.5rem] text-[var(--primary)]"></Icon>
|
|
</a>
|
|
<a
|
|
href={entry.data.prevSlug ? getPostUrlBySlug(entry.data.prevSlug) : "#"}
|
|
aria-label={entry.data.prevTitle ? `下一篇:${entry.data.prevTitle}` : "没有下一篇"}
|
|
title={entry.data.prevTitle || undefined}
|
|
class:list={[
|
|
"btn-card h-9 w-9 rounded-xl active:scale-95",
|
|
{"pointer-events-none opacity-40": !entry.data.prevSlug},
|
|
]}
|
|
>
|
|
<Icon is:inline name="material-symbols:chevron-right-rounded" class="text-[1.5rem] text-[var(--primary)]"></Icon>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- title -->
|
|
<div class="relative onload-animation">
|
|
<div
|
|
data-pagefind-body data-pagefind-weight="10" data-pagefind-meta="title"
|
|
class="transition w-full block font-bold mb-3
|
|
text-3xl md:text-[2.25rem]/[2.75rem]
|
|
text-[var(--primary)]
|
|
underline decoration-[3px] decoration-[var(--primary)] underline-offset-[0.45rem]
|
|
md:before:w-1 before:h-5 before:rounded-md before:bg-[var(--primary)]
|
|
before:absolute before:top-[0.75rem] before:left-[-1.125rem]
|
|
">
|
|
{entry.data.title}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- description/excerpt -->
|
|
<!-- https://github.com/afoim/fuwari/commit/e34064899f0a3e7f28107f06213301f7c1167084 -->
|
|
{(entry.data.description || remarkPluginFrontmatter.excerpt) && (
|
|
<div class="onload-animation mb-4">
|
|
<div class="text-black/75 dark:text-white/75 text-sm leading-relaxed">
|
|
{entry.data.description || remarkPluginFrontmatter.excerpt}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div class:list={[
|
|
"onload-animation mb-4 rounded-xl px-4 py-3 text-sm",
|
|
isOutdated
|
|
? "border border-red-500/20 bg-red-500/8 text-red-700 dark:text-red-300"
|
|
: "border border-amber-500/20 bg-amber-500/8 text-amber-700 dark:text-amber-300",
|
|
]}>
|
|
<div class="flex items-start gap-2">
|
|
<Icon
|
|
is:inline
|
|
name={isOutdated ? "material-symbols:warning-outline-rounded" : "material-symbols:info-outline-rounded"}
|
|
class="mt-0.5 shrink-0 text-lg"
|
|
></Icon>
|
|
<div class="leading-relaxed">
|
|
{isOutdated ? (
|
|
<>本文部分内容可能已失效,请谨慎参考。</>
|
|
) : (
|
|
<>
|
|
本文最后更新于 <span class="font-medium">{lastUpdatedLabel}</span>,距今 <span class="font-medium" id="post-last-updated-distance">计算中...</span>。
|
|
随着时间推移,文中描述可能与当前实际情况有出入,请注意甄别。
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- metadata -->
|
|
<div class="onload-animation">
|
|
<PostMetadata
|
|
class="mb-5"
|
|
published={entry.data.published}
|
|
updated={entry.data.updated}
|
|
tags={entry.data.tags}
|
|
category={entry.data.category}
|
|
></PostMetadata>
|
|
{!entry.data.image && <div class="border-[var(--line-divider)] border-dashed border-b-[1px] mb-5"></div>}
|
|
</div>
|
|
|
|
{aiSummaryMeta.summary && (
|
|
<div class="mb-4 rounded-xl p-4 bg-gradient-to-r from-violet-500/10 to-indigo-500/10 dark:from-violet-500/15 dark:to-indigo-500/15 border border-violet-500/20 onload-animation">
|
|
<div class="flex items-start gap-3">
|
|
<div class="shrink-0 w-8 h-8 rounded-lg bg-violet-500/20 dark:bg-violet-500/30 flex items-center justify-center">
|
|
<Icon is:inline name="material-symbols:info-outline-rounded" class="text-violet-600 dark:text-violet-400 text-lg"></Icon>
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<div class="text-sm font-medium text-violet-700 dark:text-violet-300 mb-1 select-none pointer-events-none">
|
|
AI 摘要{aiSummaryMeta.model && <span class="text-violet-600/70 dark:text-violet-300/70"> · {aiSummaryMeta.model}</span>}
|
|
</div>
|
|
<div class="text-sm text-black/70 dark:text-white/70 leading-relaxed">{aiSummaryMeta.summary}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<!-- always show cover as long as it has one -->
|
|
<!-- 使用自制 showcover 控制器控制头图的出现 -->
|
|
{showcover && entry.data.image &&
|
|
<ImageWrapper id="post-cover" src={entry.data.image} basePath={path.join("content/posts/", getDir(entry.id))} class="mb-8 rounded-xl banner-container onload-animation" loader/>
|
|
}
|
|
|
|
<!-- 自制内部头图 customcover-->
|
|
{customcover &&
|
|
<ImageWrapper id="post-cover" src={entry.data.customcover} basePath={path.join("content/posts/", getDir(entry.id))} class="mb-8 rounded-xl banner-container onload-animation" loader/>
|
|
}
|
|
|
|
<!-- 头图调试代码
|
|
<pre>
|
|
showcover(变量): {showcover} (type: {typeof showcover})
|
|
entry.data.showcover: {entry.data.showcover} (type: {typeof entry.data.showcover})
|
|
</pre>
|
|
-->
|
|
|
|
<Markdown class="mb-6 markdown-content onload-animation">
|
|
<Content />
|
|
</Markdown>
|
|
|
|
{licenseConfig.enable && <License title={entry.data.title} slug={entry.id} pubDate={entry.data.published} class="mb-6 rounded-xl license-container onload-animation"></License>}
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col md:flex-row justify-between mb-4 gap-4 overflow-hidden w-full">
|
|
<a href={entry.data.nextSlug ? getPostUrlBySlug(entry.data.nextSlug) : "#"}
|
|
class:list={["w-full font-bold overflow-hidden active:scale-95", {"pointer-events-none": !entry.data.nextSlug}]}>
|
|
{entry.data.nextSlug && <div class="btn-card rounded-2xl w-full h-[3.75rem] max-w-full px-4 flex items-center !justify-start gap-4" >
|
|
<Icon is:inline name="material-symbols:chevron-left-rounded" class="text-[2rem] text-[var(--primary)]" />
|
|
<div class="overflow-hidden transition overflow-ellipsis whitespace-nowrap max-w-[calc(100%_-_3rem)] text-black/75 dark:text-white/75">
|
|
{entry.data.nextTitle}
|
|
</div>
|
|
</div>}
|
|
</a>
|
|
|
|
<a href={entry.data.prevSlug ? getPostUrlBySlug(entry.data.prevSlug) : "#"}
|
|
class:list={["w-full font-bold overflow-hidden active:scale-95", {"pointer-events-none": !entry.data.prevSlug}]}>
|
|
{entry.data.prevSlug && <div class="btn-card rounded-2xl w-full h-[3.75rem] max-w-full px-4 flex items-center !justify-end gap-4">
|
|
<div class="overflow-hidden transition overflow-ellipsis whitespace-nowrap max-w-[calc(100%_-_3rem)] text-black/75 dark:text-white/75">
|
|
{entry.data.prevTitle}
|
|
</div>
|
|
<Icon is:inline name="material-symbols:chevron-right-rounded" class="text-[2rem] text-[var(--primary)]" />
|
|
</div>}
|
|
</a>
|
|
</div>
|
|
|
|
<script define:vars={{ slug: postSlug, lastUpdatedTimestamp }}>
|
|
function formatUpdatedDistance(timestamp) {
|
|
const now = Date.now();
|
|
const diff = Math.max(0, now - timestamp);
|
|
const minute = 60 * 1000;
|
|
const hour = 60 * minute;
|
|
const day = 24 * hour;
|
|
const month = 30 * day;
|
|
const year = 365 * day;
|
|
|
|
if (diff < hour) {
|
|
const minutes = Math.max(1, Math.floor(diff / minute));
|
|
return `${minutes} 分钟`;
|
|
}
|
|
if (diff < day) {
|
|
return `${Math.floor(diff / hour)} 小时`;
|
|
}
|
|
if (diff < month) {
|
|
return `${Math.floor(diff / day)} 天`;
|
|
}
|
|
if (diff < year) {
|
|
return `${Math.floor(diff / month)} 个月`;
|
|
}
|
|
return `${Math.floor(diff / year)} 年`;
|
|
}
|
|
|
|
function renderUpdatedDistance() {
|
|
const distanceElement = document.getElementById('post-last-updated-distance');
|
|
if (!distanceElement) {
|
|
return;
|
|
}
|
|
distanceElement.textContent = formatUpdatedDistance(lastUpdatedTimestamp);
|
|
}
|
|
|
|
async function fetchTopPageViews() {
|
|
const displayElement = document.getElementById('post-top-page-views');
|
|
if (!displayElement || displayElement.dataset.umamiState === 'loading' || displayElement.dataset.umamiState === 'loaded') {
|
|
return;
|
|
}
|
|
|
|
displayElement.dataset.umamiState = 'loading';
|
|
|
|
try {
|
|
const umamiStore = window['__blogUmami'];
|
|
const statsData = await umamiStore?.getStats(`post:${slug}`, (websiteId) => {
|
|
const currentTimestamp = Date.now();
|
|
return `https://umami.adclosenn.top/api/websites/${websiteId}/stats?startAt=0&endAt=${currentTimestamp}&unit=hour&timezone=${encodeURIComponent('Asia/Shanghai')}&url=%2Fposts%2F${slug}%2F&compare=false`;
|
|
});
|
|
|
|
if (!statsData) {
|
|
throw new Error('统计功能未启用');
|
|
}
|
|
|
|
const pageViews = statsData.pageviews?.value || 0;
|
|
displayElement.textContent = `浏览量 ${pageViews}`;
|
|
displayElement.dataset.umamiState = 'loaded';
|
|
} catch (error) {
|
|
console.error('Error fetching top page views:', error);
|
|
displayElement.textContent = '统计不可用';
|
|
displayElement.dataset.umamiState = 'error';
|
|
}
|
|
}
|
|
|
|
function setupCopyPageMenu() {
|
|
const menu = document.getElementById('copy-page-menu');
|
|
const switchButton = document.getElementById('copy-page-switch');
|
|
let panel = document.getElementById('copy-page-panel');
|
|
const arrow = menu?.querySelector('.copy-page-arrow');
|
|
|
|
if (!menu || !switchButton || !panel) return;
|
|
|
|
// 将 panel 移到 body,彻底避免祖先 transform/overflow 等对 fixed 定位的干扰
|
|
if (panel.parentElement !== document.body) {
|
|
// 清理上一页可能残留在 body 中的旧 panel
|
|
const stale = document.body.querySelector('#copy-page-panel');
|
|
if (stale && stale !== panel) stale.remove();
|
|
document.body.appendChild(panel);
|
|
}
|
|
|
|
// 强制重置为关闭状态(只改 opacity/transform,不清位置,避免闪现)
|
|
function resetPanel() {
|
|
panel.classList.add('pointer-events-none', 'opacity-0', '-translate-y-1');
|
|
panel.classList.remove('opacity-100', 'translate-y-0');
|
|
if (arrow) arrow.classList.remove('rotate-180');
|
|
switchButton.setAttribute('aria-expanded', 'false');
|
|
}
|
|
resetPanel();
|
|
|
|
if (switchButton.dataset.copyPageReady) return;
|
|
|
|
function setMenuOpen(opening) {
|
|
if (opening) {
|
|
const menuRect = menu.getBoundingClientRect();
|
|
panel.style.top = `${menuRect.bottom + 4}px`;
|
|
panel.style.right = `${window.innerWidth - menuRect.right}px`;
|
|
}
|
|
panel.classList.toggle('pointer-events-none', !opening);
|
|
panel.classList.toggle('opacity-0', !opening);
|
|
panel.classList.toggle('-translate-y-1', !opening);
|
|
panel.classList.toggle('opacity-100', opening);
|
|
panel.classList.toggle('translate-y-0', opening);
|
|
arrow?.classList.toggle('rotate-180', opening);
|
|
switchButton.setAttribute('aria-expanded', String(opening));
|
|
}
|
|
|
|
switchButton.dataset.copyPageReady = 'true';
|
|
switchButton.addEventListener('click', () => {
|
|
setMenuOpen(panel.classList.contains('opacity-0'));
|
|
});
|
|
|
|
|
|
// 当菜单按钮滚出视口(被 navbar 遮挡或滚到下方)时自动关闭 panel
|
|
if (document.body['__copyPageObserver']) {
|
|
document.body['__copyPageObserver'].disconnect();
|
|
}
|
|
document.body['__copyPageObserver'] = new IntersectionObserver((entries) => {
|
|
if (!entries[0].isIntersecting) {
|
|
setMenuOpen(false);
|
|
}
|
|
}, { rootMargin: '-80px 0px 0px 0px', threshold: 0 });
|
|
document.body['__copyPageObserver'].observe(menu);
|
|
|
|
if (!document.body.dataset.copyPageOutsideReady) {
|
|
document.body.dataset.copyPageOutsideReady = 'true';
|
|
document.addEventListener('click', (event) => {
|
|
const currentMenu = document.getElementById('copy-page-menu');
|
|
const currentPanel = document.getElementById('copy-page-panel');
|
|
const currentSwitch = document.getElementById('copy-page-switch');
|
|
const currentArrow = currentMenu?.querySelector('.copy-page-arrow');
|
|
// 点击 menu 或 panel 内部时不关闭
|
|
if (!(event.target instanceof Node)) return;
|
|
if (currentMenu?.contains(event.target)) return;
|
|
if (currentPanel?.contains(event.target)) return;
|
|
// 关闭
|
|
currentPanel?.classList.add('pointer-events-none', 'opacity-0', '-translate-y-1');
|
|
currentPanel?.classList.remove('opacity-100', 'translate-y-0');
|
|
currentArrow?.classList.remove('rotate-180');
|
|
currentSwitch?.setAttribute('aria-expanded', 'false');
|
|
});
|
|
}
|
|
}
|
|
|
|
function initPostPage() {
|
|
renderUpdatedDistance();
|
|
fetchTopPageViews();
|
|
setupCopyPageMenu();
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', initPostPage);
|
|
} else {
|
|
initPostPage();
|
|
}
|
|
document.addEventListener('astro:page-load', initPostPage);
|
|
</script>
|
|
|
|
<!-- 评论区 -->
|
|
<script src="https://giscus.app/client.js"
|
|
data-repo="Ad-closeNN/blog-friends"
|
|
data-repo-id="R_kgDOPb5ZJw"
|
|
data-category="General"
|
|
data-category-id="DIC_kwDOPb5ZJ84CuPmR"
|
|
data-mapping="title"
|
|
data-strict="0"
|
|
data-reactions-enabled="1"
|
|
data-emit-metadata="0"
|
|
data-input-position="top"
|
|
data-theme="preferred_color_scheme"
|
|
data-lang="zh-CN"
|
|
data-loading="lazy"
|
|
crossorigin="anonymous"
|
|
async>
|
|
</script>
|
|
</MainGridLayout>
|