mirror of
https://github.com/afoim/fuwari.git
synced 2026-01-31 09:03:18 +08:00
refactor: 移除标签相关功能及优化统计代码
- 删除标签相关的组件、页面和工具函数 - 简化文章卡片和元数据组件,移除标签显示 - 优化Umami统计代码,提取公共函数 - 删除未使用的组件和布局
This commit is contained in:
@@ -44,4 +44,48 @@
|
|||||||
localStorage.removeItem(cacheKey);
|
localStorage.removeItem(cacheKey);
|
||||||
delete global.__umamiSharePromise;
|
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);
|
})(window);
|
||||||
@@ -5,22 +5,10 @@ import { getPostUrlBySlug } from "../utils/url-utils";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
keyword?: string;
|
keyword?: string;
|
||||||
tags?: string[];
|
|
||||||
}
|
}
|
||||||
const { tags } = Astro.props;
|
|
||||||
|
|
||||||
let posts = await getSortedPosts();
|
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 groups: { year: number; posts: typeof posts }[] = (() => {
|
||||||
const groupedPosts = posts.reduce(
|
const groupedPosts = posts.reduce(
|
||||||
(grouped: { [year: number]: typeof posts }, post) => {
|
(grouped: { [year: number]: typeof posts }, post) => {
|
||||||
@@ -50,10 +38,6 @@ function formatDate(date: Date) {
|
|||||||
const day = date.getDate().toString().padStart(2, "0");
|
const day = date.getDate().toString().padStart(2, "0");
|
||||||
return `${month}-${day}`;
|
return `${month}-${day}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTag(tag: string[]) {
|
|
||||||
return tag.map((t) => `#${t}`).join(" ");
|
|
||||||
}
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="card-base px-8 py-6">
|
<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
|
<div class="hidden md:block md:w-[15%] text-left text-sm transition
|
||||||
whitespace-nowrap overflow-ellipsis overflow-hidden
|
whitespace-nowrap overflow-ellipsis overflow-hidden
|
||||||
text-30"
|
text-30"
|
||||||
>{formatTag(post.data.tags)}</div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
---
|
|
||||||
|
|
||||||
---
|
|
||||||
@@ -15,7 +15,6 @@ interface Props {
|
|||||||
url: string;
|
url: string;
|
||||||
published: Date;
|
published: Date;
|
||||||
updated?: Date;
|
updated?: Date;
|
||||||
tags: string[];
|
|
||||||
image: string;
|
image: string;
|
||||||
description: string;
|
description: string;
|
||||||
draft: boolean;
|
draft: boolean;
|
||||||
@@ -27,7 +26,6 @@ const {
|
|||||||
url,
|
url,
|
||||||
published,
|
published,
|
||||||
updated,
|
updated,
|
||||||
tags,
|
|
||||||
image,
|
image,
|
||||||
description,
|
description,
|
||||||
style,
|
style,
|
||||||
@@ -57,7 +55,7 @@ const { remarkPluginFrontmatter } = await entry.render();
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- metadata -->
|
<!-- 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 -->
|
<!-- description -->
|
||||||
<div class:list={["transition text-75 mb-3.5 pr-4 line-clamp-1 md:line-clamp-2"]}>
|
<div class:list={["transition text-75 mb-3.5 pr-4 line-clamp-1 md:line-clamp-2"]}>
|
||||||
|
|||||||
@@ -9,16 +9,12 @@ interface Props {
|
|||||||
class: string;
|
class: string;
|
||||||
published: Date;
|
published: Date;
|
||||||
updated?: Date;
|
updated?: Date;
|
||||||
tags: string[];
|
|
||||||
hideTagsForMobile?: boolean;
|
|
||||||
hideUpdateDate?: boolean;
|
hideUpdateDate?: boolean;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
}
|
}
|
||||||
const {
|
const {
|
||||||
published,
|
published,
|
||||||
updated,
|
updated,
|
||||||
tags,
|
|
||||||
hideTagsForMobile = false,
|
|
||||||
hideUpdateDate = false,
|
hideUpdateDate = false,
|
||||||
slug,
|
slug,
|
||||||
} = Astro.props;
|
} = Astro.props;
|
||||||
@@ -46,25 +42,6 @@ const className = Astro.props.class;
|
|||||||
</div>
|
</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 -->
|
<!-- page views & visitors -->
|
||||||
{slug && (
|
{slug && (
|
||||||
<>
|
<>
|
||||||
@@ -88,55 +65,27 @@ const className = Astro.props.class;
|
|||||||
<script define:vars={{ slug, umamiConfig }}>
|
<script define:vars={{ slug, umamiConfig }}>
|
||||||
|
|
||||||
// 获取访问量统计
|
// 获取访问量统计
|
||||||
async function fetchPageViews(isRetry = false) {
|
async function fetchPageViews() {
|
||||||
if (!umamiConfig.enable) {
|
if (!umamiConfig.enable) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 调用全局工具获取 Umami 分享数据
|
const statsData = await fetchUmamiStats(umamiConfig.baseUrl, umamiConfig.shareId, {
|
||||||
const { websiteId, token } = await getUmamiShareData(umamiConfig.baseUrl, umamiConfig.shareId);
|
timezone: umamiConfig.timezone,
|
||||||
|
path: `eq./posts/${slug}/`
|
||||||
// 第二步:获取统计数据
|
|
||||||
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
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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 pageViews = statsData.pageviews || 0;
|
||||||
const visits = statsData.visitors || 0;
|
const visits = statsData.visitors || 0;
|
||||||
|
|
||||||
const viewsElement = document.getElementById(`page-views-${slug}`);
|
const viewsElement = document.getElementById(`page-views-${slug}`);
|
||||||
const visitorsElement = document.getElementById(`page-visitors-${slug}`);
|
const visitorsElement = document.getElementById(`page-visitors-${slug}`);
|
||||||
if (viewsElement) {
|
if (viewsElement) viewsElement.textContent = pageViews;
|
||||||
viewsElement.textContent = pageViews;
|
if (visitorsElement) visitorsElement.textContent = visits;
|
||||||
}
|
|
||||||
if (visitorsElement) {
|
|
||||||
visitorsElement.textContent = visits;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching page views:', error);
|
console.error('Error fetching page views:', error);
|
||||||
const viewsElement = document.getElementById(`page-views-${slug}`);
|
const viewsElement = document.getElementById(`page-views-${slug}`);
|
||||||
const visitorsElement = document.getElementById(`page-visitors-${slug}`);
|
const visitorsElement = document.getElementById(`page-visitors-${slug}`);
|
||||||
if (viewsElement) {
|
if (viewsElement) viewsElement.textContent = '-';
|
||||||
viewsElement.textContent = '-';
|
if (visitorsElement) visitorsElement.textContent = '-';
|
||||||
}
|
|
||||||
if (visitorsElement) {
|
|
||||||
visitorsElement.textContent = '-';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ const interval = 50;
|
|||||||
<PostCard
|
<PostCard
|
||||||
entry={entry}
|
entry={entry}
|
||||||
title={entry.data.title}
|
title={entry.data.title}
|
||||||
tags={entry.data.tags}
|
|
||||||
published={entry.data.published}
|
published={entry.data.published}
|
||||||
updated={entry.data.updated}
|
updated={entry.data.updated}
|
||||||
url={getPostUrlBySlug(entry.slug)}
|
url={getPostUrlBySlug(entry.slug)}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
|
|
||||||
|
|
||||||
@@ -70,46 +70,19 @@ const config = profileConfig;
|
|||||||
<script define:vars={{ umamiConfig}}>
|
<script define:vars={{ umamiConfig}}>
|
||||||
// 获取全站访问量统计
|
// 获取全站访问量统计
|
||||||
async function loadSiteStats() {
|
async function loadSiteStats() {
|
||||||
if (!umamiConfig.enable) {
|
if (!umamiConfig.enable) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 调用全局工具获取 Umami 分享数据
|
const statsData = await fetchUmamiStats(umamiConfig.baseUrl, umamiConfig.shareId, {
|
||||||
const { websiteId, token } = await getUmamiShareData(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 pageviews = statsData.pageviews || 0;
|
||||||
const visitors = statsData.visitors || 0;
|
const visitors = statsData.visitors || 0;
|
||||||
|
|
||||||
const viewsElement = document.getElementById('site-views');
|
const viewsElement = document.getElementById('site-views');
|
||||||
const visitorsElement = document.getElementById('site-visitors');
|
const visitorsElement = document.getElementById('site-visitors');
|
||||||
if (viewsElement) {
|
if (viewsElement) viewsElement.textContent = pageviews;
|
||||||
viewsElement.textContent = pageviews;
|
if (visitorsElement) visitorsElement.textContent = visitors;
|
||||||
}
|
|
||||||
if (visitorsElement) {
|
|
||||||
visitorsElement.textContent = visitors;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取全站统计失败:', error);
|
console.error('获取全站统计失败:', error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
import type { MarkdownHeading } from "astro";
|
import type { MarkdownHeading } from "astro";
|
||||||
|
|
||||||
import Profile from "./Profile.astro";
|
import Profile from "./Profile.astro";
|
||||||
import Tag from "./Tags.astro";
|
|
||||||
import DomainSwitcher from "./DomainSwitcher.astro";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: string;
|
class?: string;
|
||||||
@@ -17,7 +15,6 @@ const className = Astro.props.class;
|
|||||||
<Profile></Profile>
|
<Profile></Profile>
|
||||||
</div>
|
</div>
|
||||||
<div id="sidebar-sticky" class="transition-all duration-700 flex flex-col w-full gap-4 top-4 sticky top-4">
|
<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">
|
<div class="overflow-hidden flex justify-center">
|
||||||
<a href="https://secbit.ai/" target="_blank" rel="noopener noreferrer">
|
<a href="https://secbit.ai/" target="_blank" rel="noopener noreferrer">
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -31,10 +31,9 @@ const jsonLd = {
|
|||||||
"@type": "BlogPosting",
|
"@type": "BlogPosting",
|
||||||
headline: entry.data.title,
|
headline: entry.data.title,
|
||||||
description: entry.data.description || entry.data.title,
|
description: entry.data.description || entry.data.title,
|
||||||
keywords: entry.data.tags,
|
author: {
|
||||||
author: {
|
"@type": "Person",
|
||||||
"@type": "Person",
|
name: profileConfig.name,
|
||||||
name: profileConfig.name,
|
|
||||||
url: Astro.site,
|
url: Astro.site,
|
||||||
},
|
},
|
||||||
datePublished: formatDateToYYYYMMDD(entry.data.published),
|
datePublished: formatDateToYYYYMMDD(entry.data.published),
|
||||||
|
|||||||
@@ -28,28 +28,3 @@ export async function getSortedPosts() {
|
|||||||
return sorted;
|
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] }));
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user