refactor: 移除标签相关功能及优化统计代码

- 删除标签相关的组件、页面和工具函数
- 简化文章卡片和元数据组件,移除标签显示
- 优化Umami统计代码,提取公共函数
- 删除未使用的组件和布局
This commit is contained in:
二叉树树
2025-12-15 11:17:30 +08:00
parent 98a0e93feb
commit ac2bcf7521
16 changed files with 64 additions and 497 deletions

View File

@@ -44,4 +44,48 @@
localStorage.removeItem(cacheKey);
delete global.__umamiSharePromise;
};
/**
* 获取 Umami 统计数据
* 自动处理 token 获取和过期重试
* @param {string} baseUrl
* @param {string} shareId
* @param {object} queryParams
* @returns {Promise<any>}
*/
global.fetchUmamiStats = async function (baseUrl, shareId, queryParams) {
async function doFetch(isRetry = false) {
const { websiteId, token } = await global.getUmamiShareData(baseUrl, shareId);
const currentTimestamp = Date.now();
const params = new URLSearchParams({
startAt: 0,
endAt: currentTimestamp,
unit: 'hour',
timezone: queryParams.timezone || 'Asia/Shanghai',
compare: false,
...queryParams
});
const statsUrl = `${baseUrl}/api/websites/${websiteId}/stats?${params.toString()}`;
const res = await fetch(statsUrl, {
headers: {
'x-umami-share-token': token
}
});
if (!res.ok) {
if (res.status === 401 && !isRetry) {
global.clearUmamiShareCache();
return doFetch(true);
}
throw new Error('获取统计数据失败');
}
return await res.json();
}
return doFetch();
};
})(window);

View File

@@ -5,22 +5,10 @@ import { getPostUrlBySlug } from "../utils/url-utils";
interface Props {
keyword?: string;
tags?: string[];
}
const { tags } = Astro.props;
let posts = await getSortedPosts();
if (Array.isArray(tags) && tags.length > 0) {
posts = posts.filter(
(post) =>
Array.isArray(post.data.tags) &&
post.data.tags.some((tag) => tags.includes(tag)),
);
}
const groups: { year: number; posts: typeof posts }[] = (() => {
const groupedPosts = posts.reduce(
(grouped: { [year: number]: typeof posts }, post) => {
@@ -50,10 +38,6 @@ function formatDate(date: Date) {
const day = date.getDate().toString().padStart(2, "0");
return `${month}-${day}`;
}
function formatTag(tag: string[]) {
return tag.map((t) => `#${t}`).join(" ");
}
---
<div class="card-base px-8 py-6">
@@ -99,7 +83,7 @@ function formatTag(tag: string[]) {
<div class="hidden md:block md:w-[15%] text-left text-sm transition
whitespace-nowrap overflow-ellipsis overflow-hidden
text-30"
>{formatTag(post.data.tags)}</div>
></div>
</div>
</a>
))}

View File

@@ -1,3 +0,0 @@
---
---

View File

@@ -15,7 +15,6 @@ interface Props {
url: string;
published: Date;
updated?: Date;
tags: string[];
image: string;
description: string;
draft: boolean;
@@ -27,7 +26,6 @@ const {
url,
published,
updated,
tags,
image,
description,
style,
@@ -57,7 +55,7 @@ const { remarkPluginFrontmatter } = await entry.render();
</a>
<!-- metadata -->
<PostMetadata published={published} updated={updated} tags={tags} hideTagsForMobile={true} hideUpdateDate={true} slug={entry.slug} class="mb-2 md:mb-4"></PostMetadata>
<PostMetadata published={published} updated={updated} hideUpdateDate={true} slug={entry.slug} class="mb-2 md:mb-4"></PostMetadata>
<!-- description -->
<div class:list={["transition text-75 mb-3.5 pr-4 line-clamp-1 md:line-clamp-2"]}>

View File

@@ -9,16 +9,12 @@ interface Props {
class: string;
published: Date;
updated?: Date;
tags: string[];
hideTagsForMobile?: boolean;
hideUpdateDate?: boolean;
slug?: string;
}
const {
published,
updated,
tags,
hideTagsForMobile = false,
hideUpdateDate = false,
slug,
} = Astro.props;
@@ -46,25 +42,6 @@ const className = Astro.props.class;
</div>
)}
<!-- tags -->
<div class:list={["items-center", {"flex": !hideTagsForMobile, "hidden md:flex": hideTagsForMobile}]}>
<div class="meta-icon"
>
<Icon name="material-symbols:tag-rounded" class="text-xl"></Icon>
</div>
<div class="flex flex-row flex-nowrap items-center">
{(tags && tags.length > 0) && tags.map((tag, i) => (
<div class:list={[{"hidden": i == 0}, "mx-1.5 text-[var(--meta-divider)] text-sm"]}>/</div>
<a href={url(`/archive/tag/${tag}/`)} aria-label=`View all posts with the ${tag} tag`
class="link-lg transition text-50 text-sm font-medium
hover:text-[var(--primary)] dark:hover:text-[var(--primary)] whitespace-nowrap">
{tag}
</a>
))}
{!(tags && tags.length > 0) && <div class="transition text-50 text-sm font-medium">无标签</div>}
</div>
</div>
<!-- page views & visitors -->
{slug && (
<>
@@ -88,55 +65,27 @@ const className = Astro.props.class;
<script define:vars={{ slug, umamiConfig }}>
// 获取访问量统计
async function fetchPageViews(isRetry = false) {
if (!umamiConfig.enable) {
return;
}
async function fetchPageViews() {
if (!umamiConfig.enable) return;
try {
// 调用全局工具获取 Umami 分享数据
const { websiteId, token } = await getUmamiShareData(umamiConfig.baseUrl, umamiConfig.shareId);
// 第二步:获取统计数据
const currentTimestamp = Date.now();
const statsUrl = `${umamiConfig.baseUrl}/api/websites/${websiteId}/stats?startAt=0&endAt=${currentTimestamp}&unit=hour&timezone=${encodeURIComponent(umamiConfig.timezone)}&path=eq.%2Fposts%2F${slug}%2F&compare=false`;
const statsResponse = await fetch(statsUrl, {
headers: {
'x-umami-share-token': token
}
const statsData = await fetchUmamiStats(umamiConfig.baseUrl, umamiConfig.shareId, {
timezone: umamiConfig.timezone,
path: `eq./posts/${slug}/`
});
if (!statsResponse.ok) {
if (statsResponse.status === 401 && !isRetry) {
clearUmamiShareCache();
return fetchPageViews(true);
}
throw new Error('获取统计数据失败');
}
const statsData = await statsResponse.json();
const pageViews = statsData.pageviews || 0;
const visits = statsData.visitors || 0;
const viewsElement = document.getElementById(`page-views-${slug}`);
const visitorsElement = document.getElementById(`page-visitors-${slug}`);
if (viewsElement) {
viewsElement.textContent = pageViews;
}
if (visitorsElement) {
visitorsElement.textContent = visits;
}
if (viewsElement) viewsElement.textContent = pageViews;
if (visitorsElement) visitorsElement.textContent = visits;
} catch (error) {
console.error('Error fetching page views:', error);
const viewsElement = document.getElementById(`page-views-${slug}`);
const visitorsElement = document.getElementById(`page-visitors-${slug}`);
if (viewsElement) {
viewsElement.textContent = '-';
}
if (visitorsElement) {
visitorsElement.textContent = '-';
}
if (viewsElement) viewsElement.textContent = '-';
if (visitorsElement) visitorsElement.textContent = '-';
}
}

View File

@@ -15,7 +15,6 @@ const interval = 50;
<PostCard
entry={entry}
title={entry.data.title}
tags={entry.data.tags}
published={entry.data.published}
updated={entry.data.updated}
url={getPostUrlBySlug(entry.slug)}

View File

@@ -1,43 +0,0 @@
---
interface Props {
badge?: string;
url?: string;
label?: string;
}
const { badge, url, label } = Astro.props;
---
<a href={url} aria-label={label}>
<button
class:list={`
w-full
h-10
rounded-lg
bg-none
hover:bg-[var(--btn-plain-bg-hover)]
active:bg-[var(--btn-plain-bg-active)]
transition-all
pl-2
hover:pl-3
text-neutral-700
hover:text-[var(--primary)]
dark:text-neutral-300
dark:hover:text-[var(--primary)]
`
}
>
<div class="flex items-center justify-between relative mr-2">
<div class="overflow-hidden text-left whitespace-nowrap overflow-ellipsis ">
<slot></slot>
</div>
{ badge !== undefined && badge !== null && badge !== '' &&
<div class="transition px-2 h-7 ml-4 min-w-[2rem] rounded-lg text-sm font-bold
text-[var(--btn-content)] dark:text-[var(--deep-text)]
bg-[var(--btn-regular-bg)] dark:bg-[var(--primary)]
flex items-center justify-center">
{ badge }
</div>
}
</div>
</button>
</a>

View File

@@ -1,13 +0,0 @@
---
interface Props {
size?: string;
dot?: boolean;
href?: string;
label?: string;
}
const { dot, href, label }: Props = Astro.props;
---
<a href={href} aria-label={label} class="btn-regular h-8 text-sm px-3 rounded-lg">
{dot && <div class="h-1 w-1 bg-[var(--btn-content)] dark:bg-[var(--card-bg)] transition rounded-md mr-2"></div>}
<slot></slot>
</a>

View File

@@ -1,173 +0,0 @@
---
import WidgetLayout from "./WidgetLayout.astro";
import { siteConfig } from "@/config";
interface Props {
class?: string;
style?: string;
}
const className = Astro.props.class;
const style = Astro.props.style;
const officialSites = siteConfig.officialSites || [];
---
<WidgetLayout name="线路切换" id="domain-switcher" class={className} style={style}>
<div class="flex flex-col gap-2">
{officialSites.map((site) => {
const urlStr = typeof site === 'string' ? site : site.url;
const alias = typeof site === 'object' ? site.alias : null;
let hostname = urlStr;
try {
hostname = new URL(urlStr).hostname;
} catch (e) {
// fallback to original string if not a valid URL
}
const displayName = alias || hostname;
return (
<a
href={urlStr}
data-url={urlStr}
class="domain-link btn-regular !justify-between px-3 py-2 rounded-lg text-sm group"
data-domain={hostname}
>
<span class="truncate">{displayName}</span>
<div class="flex items-center gap-2">
<span class="latency-text text-xs font-bold opacity-0 transition-opacity"></span>
<span class="status-indicator w-2 h-2 rounded-full bg-[var(--btn-content)]/30 dark:bg-white/30"></span>
</div>
</a>
);
})}
</div>
</WidgetLayout>
<script>
function initDomainSwitcher() {
const currentDomain = window.location.hostname;
const links = document.querySelectorAll('.domain-link');
links.forEach(link => {
const domain = link.getAttribute('data-domain');
const href = link.getAttribute('href');
// Reset classes first
link.className = 'domain-link btn-regular !justify-between px-3 py-2 rounded-lg text-sm group';
const indicator = link.querySelector('.status-indicator');
if (indicator) {
indicator.className = 'status-indicator w-2 h-2 rounded-full bg-[var(--btn-content)]/30 dark:bg-white/30';
}
// Highlight current domain
if (domain === currentDomain) {
// Remove btn-regular to avoid style conflicts and apply active styles
link.classList.remove('btn-regular');
link.classList.add(
'flex', 'items-center', 'justify-between', // Restore layout styles from btn-regular/base
'bg-[var(--primary)]',
'text-white',
'dark:text-black/70',
'cursor-default'
);
if (indicator) {
indicator.classList.remove('bg-[var(--btn-content)]/30', 'dark:bg-white/30');
indicator.classList.add('bg-white', 'dark:bg-black/70');
}
link.removeAttribute('href');
} else if (href) {
// Update link to preserve current path
try {
const url = new URL(href);
url.pathname = window.location.pathname;
url.search = window.location.search;
url.hash = window.location.hash;
link.setAttribute('href', url.toString());
} catch (e) {
console.error('Invalid URL:', href);
}
}
});
}
async function testLatency() {
const links = document.querySelectorAll('.domain-link');
for (const link of links) {
const url = link.getAttribute('data-url');
const latencyText = link.querySelector('.latency-text');
const indicator = link.querySelector('.status-indicator');
if (!url || !latencyText) continue;
// Loading state
latencyText.textContent = '...';
latencyText.classList.remove('opacity-0', 'text-green-500', 'text-yellow-500', 'text-red-500');
latencyText.classList.add('opacity-50');
const start = performance.now();
let latency = -1;
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
await fetch(url, {
method: 'HEAD',
mode: 'no-cors',
cache: 'no-store',
signal: controller.signal
});
clearTimeout(timeoutId);
const end = performance.now();
latency = Math.round(end - start);
} catch (e) {
// Error or timeout
}
latencyText.classList.remove('opacity-50');
const isActive = link.classList.contains('bg-[var(--primary)]');
if (latency >= 0) {
latencyText.textContent = `${latency}ms`;
if (latency < 200) {
if (!isActive) {
latencyText.classList.add('text-green-600', 'dark:text-green-400');
if (indicator) {
indicator.classList.remove('bg-[var(--btn-content)]/30', 'dark:bg-white/30');
indicator.classList.add('bg-green-500');
}
}
} else if (latency < 500) {
if (!isActive) {
latencyText.classList.add('text-yellow-600', 'dark:text-yellow-400');
if (indicator) {
indicator.classList.remove('bg-[var(--btn-content)]/30', 'dark:bg-white/30');
indicator.classList.add('bg-yellow-500');
}
}
} else {
latencyText.classList.add('text-red-600', 'dark:text-red-400');
if (indicator && !isActive) {
indicator.classList.remove('bg-[var(--btn-content)]/30', 'dark:bg-white/30');
indicator.classList.add('bg-red-500');
}
}
} else {
latencyText.textContent = '-';
latencyText.classList.add('text-red-600', 'dark:text-red-400');
}
}
}
initDomainSwitcher();
testLatency();
document.addEventListener('astro:page-load', () => {
initDomainSwitcher();
testLatency();
});
</script>

View File

@@ -70,46 +70,19 @@ const config = profileConfig;
<script define:vars={{ umamiConfig}}>
// 获取全站访问量统计
async function loadSiteStats() {
if (!umamiConfig.enable) {
return;
}
if (!umamiConfig.enable) return;
try {
// 调用全局工具获取 Umami 分享数据
const { websiteId, token } = await getUmamiShareData(umamiConfig.baseUrl, umamiConfig.shareId);
const statsData = await fetchUmamiStats(umamiConfig.baseUrl, umamiConfig.shareId, {
timezone: umamiConfig.timezone
});
// 第二步获取全站统计数据不指定url参数获取全站数据
const currentTimestamp = Date.now();
const statsUrl = `${umamiConfig.baseUrl}/api/websites/${websiteId}/stats?startAt=0&endAt=${currentTimestamp}&unit=hour&timezone=${encodeURIComponent(umamiConfig.timezone)}&compare=false`;
const statsResponse = await fetch(statsUrl, {
headers: {
'x-umami-share-token': token
}
});
if (statsResponse.status === 401) {
// token 失效,清理缓存后重新获取一次
clearUmamiShareCache();
return await loadSiteStats();
}
if (!statsResponse.ok) {
throw new Error('获取统计数据失败');
}
const statsData = await statsResponse.json();
const pageviews = statsData.pageviews || 0;
const visitors = statsData.visitors || 0;
const viewsElement = document.getElementById('site-views');
const visitorsElement = document.getElementById('site-visitors');
if (viewsElement) {
viewsElement.textContent = pageviews;
}
if (visitorsElement) {
visitorsElement.textContent = visitors;
}
if (viewsElement) viewsElement.textContent = pageviews;
if (visitorsElement) visitorsElement.textContent = visitors;
} catch (error) {
console.error('获取全站统计失败:', error);
}

View File

@@ -2,8 +2,6 @@
import type { MarkdownHeading } from "astro";
import Profile from "./Profile.astro";
import Tag from "./Tags.astro";
import DomainSwitcher from "./DomainSwitcher.astro";
interface Props {
class?: string;
@@ -17,7 +15,6 @@ const className = Astro.props.class;
<Profile></Profile>
</div>
<div id="sidebar-sticky" class="transition-all duration-700 flex flex-col w-full gap-4 top-4 sticky top-4">
<Tag class="onload-animation" style="animation-delay: 200ms"></Tag>
<!-- 赞助标 -->
<div class="overflow-hidden flex justify-center">
<a href="https://secbit.ai/" target="_blank" rel="noopener noreferrer">

View File

@@ -1,30 +0,0 @@
---
import { getTagList } from "../../utils/content-utils";
import { url } from "../../utils/url-utils";
import ButtonTag from "../control/ButtonTag.astro";
import WidgetLayout from "./WidgetLayout.astro";
const tags = await getTagList();
const COLLAPSED_HEIGHT = "7.5rem";
const isCollapsed = tags.length >= 20;
interface Props {
class?: string;
style?: string;
}
const className = Astro.props.class;
const style = Astro.props.style;
---
<WidgetLayout name="标签" id="tags" isCollapsed={isCollapsed} collapsedHeight={COLLAPSED_HEIGHT} class={className} style={style}>
<div class="flex gap-2 flex-wrap">
{tags.map(t => (
<ButtonTag href={url(`/archive/tag/${t.name}/`)} label={`View all posts with the ${t.name} tag`}>
{t.name}
</ButtonTag>
))}
</div>
</WidgetLayout>

View File

@@ -1,58 +0,0 @@
---
import { Icon } from "astro-icon/components";
interface Props {
id: string;
name?: string;
isCollapsed?: boolean;
collapsedHeight?: string;
class?: string;
style?: string;
}
const { id, name, isCollapsed, collapsedHeight, style } = Astro.props;
const className = Astro.props.class;
---
<widget-layout data-id={id} data-is-collapsed={String(isCollapsed)} class={"pb-4 card-base border border-black/10 dark:border-white/10 " + className} style={style}>
<div class="font-bold transition text-lg text-neutral-900 dark:text-neutral-100 relative ml-8 mt-4 mb-2
before:w-1 before:h-4 before:rounded-md before:bg-[var(--primary)]
before:absolute before:left-[-16px] before:top-[5.5px]">{name}</div>
<div id={id} class:list={["collapse-wrapper px-4 overflow-hidden", {"collapsed": isCollapsed}]}>
<slot></slot>
</div>
{isCollapsed && <div class="expand-btn px-4 -mb-2">
<button class="btn-plain rounded-lg w-full h-9">
<div class="text-[var(--primary)] flex items-center justify-center gap-2 -translate-x-2">
<Icon name="material-symbols:more-horiz" class="text-[1.75rem]"></Icon> 更多
</div>
</button>
</div>}
</widget-layout>
<style define:vars={{ collapsedHeight }}>
.collapsed {
height: var(--collapsedHeight);
}
</style>
<script>
class WidgetLayout extends HTMLElement {
constructor() {
super();
if (this.dataset.isCollapsed !== "true")
return;
const id = this.dataset.id;
const btn = this.querySelector('.expand-btn');
const wrapper = this.querySelector(`#${id}`)
btn!.addEventListener('click', () => {
wrapper!.classList.remove('collapsed');
btn!.classList.add('hidden');
})
}
}
if (!customElements.get("widget-layout")) {
customElements.define("widget-layout", WidgetLayout);
}
</script>

View File

@@ -1,31 +0,0 @@
---
import ArchivePanel from "@components/ArchivePanel.astro";
import MainGridLayout from "@layouts/MainGridLayout.astro";
import { getSortedPosts } from "@utils/content-utils";
export async function getStaticPaths() {
const posts = await getSortedPosts();
// タグを集めるための Set の型を指定
const allTags = posts.reduce<Set<string>>((acc, post) => {
// biome-ignore lint/complexity/noForEach: <explanation>
post.data.tags.forEach((tag) => acc.add(tag));
return acc;
}, new Set());
const allTagsArray = Array.from(allTags);
return allTagsArray.map((tag) => ({
params: {
tag: tag,
},
}));
}
const tag = Astro.params.tag as string;
---
<MainGridLayout title={`${tag} - 标签`} description={`归档 - ${tag}`}>
<ArchivePanel tags={[tag]}></ArchivePanel>
</MainGridLayout>

View File

@@ -31,10 +31,9 @@ const jsonLd = {
"@type": "BlogPosting",
headline: entry.data.title,
description: entry.data.description || entry.data.title,
keywords: entry.data.tags,
author: {
"@type": "Person",
name: profileConfig.name,
author: {
"@type": "Person",
name: profileConfig.name,
url: Astro.site,
},
datePublished: formatDateToYYYYMMDD(entry.data.published),

View File

@@ -28,28 +28,3 @@ export async function getSortedPosts() {
return sorted;
}
export type Tag = {
name: string;
count: number;
};
export async function getTagList(): Promise<Tag[]> {
const allBlogPosts = await getCollection<"posts">("posts", ({ data }) => {
return import.meta.env.PROD ? data.draft !== true : true;
});
const countMap: { [key: string]: number } = {};
allBlogPosts.map((post: { data: { tags: string[] } }) => {
post.data.tags.map((tag: string) => {
if (!countMap[tag]) countMap[tag] = 0;
countMap[tag]++;
});
});
// sort tags
const keys: string[] = Object.keys(countMap).sort((a, b) => {
return a.toLowerCase().localeCompare(b.toLowerCase());
});
return keys.map((key) => ({ name: key, count: countMap[key] }));
}