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);
|
||||
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);
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
|
||||
---
|
||||
@@ -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"]}>
|
||||
|
||||
@@ -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 = '-';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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}}>
|
||||
// 获取全站访问量统计
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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",
|
||||
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),
|
||||
|
||||
@@ -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] }));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user