网站信息卡

This commit is contained in:
2026-06-19 08:55:14 +08:00 Unverified
parent 3d5eb000f1
commit 2d4eeb362c
9 changed files with 329 additions and 5 deletions
+5 -1
View File
@@ -1,5 +1,6 @@
---
import { getCategoryHref, type BlogPost } from '@/lib/posts';
import SiteInfoCard from '@/components/SiteInfoCard.astro';
import { siteConfig } from '../../site.config.mjs';
type TocItem = {
@@ -17,9 +18,10 @@ interface Props {
post: BlogPost;
toc: TocItem[];
relatedPosts: BlogPost[];
posts: BlogPost[];
}
const { post, toc, relatedPosts } = Astro.props;
const { post, toc, relatedPosts, posts } = Astro.props;
const avatar = siteConfig.author.avatar;
const topDepth = toc.length > 0 ? Math.min(...toc.map((item) => item.depth)) : 1;
const tocSections = toc.reduce<TocSection[]>((sections, item) => {
@@ -107,5 +109,7 @@ const tocSections = toc.reduce<TocSection[]>((sections, item) => {
<p class="sidebar-empty">这个分类下暂时没有其他文章。</p>
)}
</section>
<SiteInfoCard posts={posts} />
</div>
</aside>
+3
View File
@@ -1,5 +1,6 @@
---
import type { BlogPost } from '@/lib/posts';
import SiteInfoCard from '@/components/SiteInfoCard.astro';
import { siteConfig } from '../../site.config.mjs';
interface Props {
@@ -111,6 +112,8 @@ const recentPosts = posts.slice(0, 5);
))}
</div>
</section>
<SiteInfoCard posts={posts} />
</div>
</aside>
+88
View File
@@ -0,0 +1,88 @@
---
import type { BlogPost } from '@/lib/posts';
import { siteConfig } from '../../site.config.mjs';
interface Props {
posts: BlogPost[];
}
const { posts } = Astro.props;
const latestPost = posts[0];
const totalWordCount = posts.reduce((total, post) => {
const plainText = post.body
.replace(/```[\s\S]*?```/g, '')
.replace(/<[^>]+>/g, '')
.replace(/[#>*_`~\-\[\]\(\)]/g, '')
.replace(/\s+/g, '');
return total + plainText.length;
}, 0);
const formatStatNumber = (value: number) => new Intl.NumberFormat('en-US').format(value);
const siteStats = [
{ label: '文章数目', value: formatStatNumber(posts.length) },
{ label: '本站总字数', value: formatStatNumber(totalWordCount) },
{ label: '本站访客数', value: '--', key: 'visitors' },
{ label: '本站访问量', value: '--', key: 'pageviews' },
{ label: '最后更新时间', value: latestPost?.dateText ?? '--' },
];
const statsEndpoint = siteConfig.analytics.umami.statsEndpoint;
---
<section class="sidebar-card site-info-card" data-site-info-card data-stats-endpoint={statsEndpoint}>
<h2><i class="fa-solid fa-chart-line" aria-hidden="true"></i> 网站信息</h2>
<dl class="site-info-list">
{siteStats.map((item) => (
<div>
<dt>{item.label}</dt>
<dd data-stat-key={item.key}>{item.value}</dd>
</div>
))}
</dl>
</section>
<script is:inline>
(() => {
const cacheDuration = 10 * 60 * 1000;
const formatter = new Intl.NumberFormat('en-US');
const cards = [...document.querySelectorAll('[data-site-info-card]')];
const endpoint = cards.find((card) => card.dataset.statsEndpoint)?.dataset.statsEndpoint;
if (!endpoint) return;
const cacheKey = `site-info-stats:${endpoint}`;
const updateCards = (stats) => {
const visitors = Number(stats?.visitors);
const pageviews = Number(stats?.pageviews);
cards.forEach((card) => {
const visitorsEl = card.querySelector('[data-stat-key="visitors"]');
const pageviewsEl = card.querySelector('[data-stat-key="pageviews"]');
if (visitorsEl && Number.isFinite(visitors)) visitorsEl.textContent = formatter.format(visitors);
if (pageviewsEl && Number.isFinite(pageviews)) pageviewsEl.textContent = formatter.format(pageviews);
});
};
try {
const cached = JSON.parse(localStorage.getItem(cacheKey) || 'null');
if (cached?.time && Date.now() - cached.time < cacheDuration) {
updateCards(cached.data);
return;
}
} catch {
localStorage.removeItem(cacheKey);
}
fetch(endpoint, { headers: { Accept: 'application/json' } })
.then((response) => {
if (!response.ok) throw new Error(`Stats request failed: ${response.status}`);
return response.json();
})
.then((data) => {
localStorage.setItem(cacheKey, JSON.stringify({ time: Date.now(), data }));
updateCards(data);
})
.catch(() => {
});
})();
</script>
+1
View File
@@ -21,6 +21,7 @@ const publicConfigJson = JSON.stringify(publicSiteConfig).replace(/</g, '\\u003c
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content={description} />
<link rel="icon" href={siteConfig.assets.icon} />
<script defer src={siteConfig.analytics.umami.script} data-website-id={siteConfig.analytics.umami.websiteId}></script>
<script is:inline set:html={`window.SITE_CONFIG = ${publicConfigJson};`}></script>
<script is:inline>
(() => {
+1 -1
View File
@@ -127,7 +127,7 @@ const relatedPosts = allPosts
<PostCopyrightCard post={post} />
{post.comments && <TwikooComments envId={siteConfig.comments.envId} region={siteConfig.comments.region} path={post.href} title={post.title} />}
</article>
<ArticleSidebar post={post} toc={toc} relatedPosts={relatedPosts} />
<ArticleSidebar post={post} toc={toc} relatedPosts={relatedPosts} posts={allPosts} />
</section>
</BaseLayout>
+55
View File
@@ -686,6 +686,56 @@ a {
line-height: 1.9;
}
.site-info-card h2 i {
width: 18px;
color: #3eb8be;
font-size: 0.92rem;
}
.site-info-list {
display: grid;
margin: 0;
}
.site-info-list div {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
border-top: 1px dashed rgba(104, 111, 120, 0.2);
padding: 9px 0;
}
.site-info-list div:first-child {
border-top: 0;
padding-top: 0;
}
.site-info-list div:last-child {
padding-bottom: 0;
}
.site-info-list dt,
.site-info-list dd {
margin: 0;
}
.site-info-list dt {
color: #68717a;
font-size: 0.92rem;
font-weight: 700;
}
.site-info-list dd {
min-width: 0;
color: #1aa8f6;
font-size: 0.98rem;
font-weight: 800;
line-height: 1.3;
text-align: right;
overflow-wrap: anywhere;
}
#welcome-info,
#welcome-ip-location-info {
margin: 0.65em 0;
@@ -2885,6 +2935,7 @@ meting-js {
.notice-card p,
#welcome-info,
#welcome-ip-location-info,
.site-info-list dt,
.toc-link-child,
.same-category-list a,
.profile-links,
@@ -2893,6 +2944,10 @@ meting-js {
color: var(--muted);
}
:root[data-theme='dark'] .site-info-list div {
border-top-color: rgba(174, 205, 197, 0.16);
}
:root[data-theme='dark'] :where(
.meta-list a,
.article-tag-list a,