newechoes/src/pages/articles/index.astro

1893 lines
94 KiB
Plaintext
Raw Normal View History

2025-03-03 21:16:16 +08:00
---
import { getCollection } from 'astro:content';
import type { CollectionEntry } from 'astro:content';
2025-03-10 13:56:56 +08:00
import { contentStructure } from '../../content.config';
import Layout from "@/components/Layout.astro";
import Breadcrumb from '@/components/Breadcrumb.astro';
2025-03-09 14:37:44 +08:00
// 启用静态预渲染
export const prerender = true;
// 获取查询参数
const searchParams = Astro.url.searchParams;
const path = Astro.props.path || '';
// 视图类型判断逻辑:
// 1. 如果props中有明确指定pageType则使用props的值
// 2. 否则:
// a. 访问 /articles/ (带尾斜杠) 或其他目录路径 - 使用网格视图
// b. 访问 /articles (不带尾斜杠) - 使用筛选视图
// c. 如果有path属性表示是目录浏览也使用网格视图
const currentUrl = Astro.url.pathname;
const isRootWithSlash = currentUrl === '/articles/';
const isGridView = isRootWithSlash || (currentUrl.startsWith('/articles/') && currentUrl !== '/articles');
const pageType = Astro.props.pageType || (isGridView || path ? 'grid' : 'filter');
2025-03-09 14:37:44 +08:00
const pathSegments = path ? path.split('/') : [];
2025-03-03 21:16:16 +08:00
// 获取所有文章,并按日期排序
const articles: CollectionEntry<'articles'>[] = await getCollection('articles');
const sortedArticles = articles.sort(
(a, b) => b.data.date.getTime() - a.data.date.getTime()
);
// 获取所有标签
const allTags = articles.flatMap(article => article.data.tags || []);
const tags = [...new Set(allTags)].sort();
// 获取内容结构
const { sections } = contentStructure;
// 根据路径获取当前目录
function getCurrentSection(pathSegments: string[]) {
// 过滤掉空字符串
const filteredSegments = pathSegments.filter(segment => segment.trim() !== '');
if (filteredSegments.length === 0) {
return { sections, articles: contentStructure.articles, currentPath: '' };
}
let currentSections = sections;
let currentPath = '';
// 遍历路径段,逐级查找
for (let i = 0; i < filteredSegments.length; i++) {
const segment = filteredSegments[i];
// 查找当前段对应的目录
const foundSection = currentSections.find(s => s.name === segment);
if (!foundSection) {
return { sections: [], articles: [], currentPath: '' };
}
// 更新当前路径
currentPath = currentPath ? `${currentPath}/${segment}` : segment;
// 如果是最后一个段,返回该目录的内容
if (i === filteredSegments.length - 1) {
return {
sections: foundSection.sections,
articles: foundSection.articles,
currentPath
};
}
// 否则继续向下查找
currentSections = foundSection.sections;
}
// 默认返回空
return { sections: [], articles: [], currentPath: '' };
}
// 获取当前目录内容
const { sections: currentSections, articles: currentArticles, currentPath } = getCurrentSection(pathSegments);
// 如果有标签过滤,则过滤文章
let filteredArticles = sortedArticles;
let pageTitle = currentPath ? currentPath : '文章列表';
// 使用URL搜索参数中的标签进行过滤
const tagFilter = searchParams.get('tags');
2025-03-03 21:16:16 +08:00
if (tagFilter) {
filteredArticles = sortedArticles.filter(article =>
article.data.tags && article.data.tags.includes(tagFilter)
);
pageTitle = `标签: ${tagFilter}`;
}
// 处理文章链接
function getArticleUrl(articleId: string) {
return searchParams.toString() ?
`/articles/${articleId}?${searchParams.toString()}` :
`/articles/${articleId}`;
}
2025-03-03 21:16:16 +08:00
---
<Layout>
2025-03-08 18:16:42 +08:00
<div class="bg-gray-50 dark:bg-dark-bg min-h-screen">
<main class={`mx-auto px-4 sm:px-6 lg:px-8 py-6 ${pageType === 'grid' || pageType === 'filter' ? 'max-w-7xl' : 'max-w-5xl'}`}>
2025-04-19 22:17:33 +08:00
<!-- 页面标题 -->
<h1 class="sr-only">{pageTitle}</h1>
2025-03-03 21:16:16 +08:00
<!-- 导航栏 -->
2025-03-08 18:16:42 +08:00
<div class="bg-white dark:bg-gray-800 rounded-xl mb-4 shadow-lg border border-gray-200 dark:border-gray-700">
2025-03-03 21:16:16 +08:00
<div class="px-4 py-3">
<div class="flex items-center justify-between !h-10 flex-wrap sm:flex-nowrap">
2025-03-03 21:16:16 +08:00
<Breadcrumb
pageType={pageType}
pathSegments={pathSegments}
searchParams={searchParams}
path={path}
2025-03-03 21:16:16 +08:00
/>
</div>
</div>
</div>
{pageType === 'grid' ? (
2025-03-03 21:16:16 +08:00
<>
<!-- 内容卡片网格 -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-12">
{/* 上一级目录卡片 - 仅在浏览目录时显示 */}
{!tagFilter && pathSegments.length > 0 && (
<a href={`/articles/${pathSegments.length > 1 ? pathSegments.slice(0, -1).join('/') : ''}/`}
class="group flex flex-col h-full p-5 border border-gray-200 dark:border-gray-700 rounded-xl bg-white dark:bg-gray-800 hover:shadow-xl hover:-translate-y-1 shadow-lg"
data-astro-prefetch="hover">
2025-03-03 21:16:16 +08:00
<div class="flex items-center">
<div class="w-10 h-10 flex items-center justify-center rounded-lg bg-primary-100 text-primary-600 group-hover:bg-primary-200">
2025-03-03 21:16:16 +08:00
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 17l-5-5m0 0l5-5m-5 5h12" />
</svg>
</div>
<div class="ml-3 flex-1">
<div class="font-bold text-base text-gray-800 dark:text-gray-100 group-hover:text-primary-700 dark:group-hover:text-primary-300">返回上级目录</div>
2025-03-03 21:16:16 +08:00
<div class="text-xs text-gray-500">返回上一级</div>
</div>
<div class="text-primary-500 opacity-0 group-hover:opacity-100">
2025-03-03 21:16:16 +08:00
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
</div>
</div>
</a>
)}
{/* 目录卡片 - 仅在浏览目录时显示 */}
{!tagFilter && currentSections.map(section => {
// 确保目录链接正确生成
const dirLink = currentPath ? `${currentPath}/${section.name}` : section.name;
return (
<a href={`/articles/${dirLink}/`}
class="group flex flex-col h-full p-5 border border-gray-200 dark:border-gray-700 rounded-xl bg-white dark:bg-gray-800 hover:shadow-xl hover:-translate-y-1 shadow-lg"
data-astro-prefetch="viewport">
2025-03-03 21:16:16 +08:00
<div class="flex items-center">
<div class="w-10 h-10 flex items-center justify-center rounded-lg bg-primary-100 text-primary-600 group-hover:bg-primary-200">
2025-03-03 21:16:16 +08:00
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 19a2 2 0 01-2-2V7a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1M5 19h14a2 2 0 002-2v-5a2 2 0 00-2-2H9a2 2 0 00-2 2v5a2 2 0 01-2 2z" />
</svg>
</div>
<div class="ml-3 flex-1">
<div class="font-bold text-base text-gray-800 dark:text-gray-100 group-hover:text-primary-700 dark:group-hover:text-primary-300 line-clamp-1">{section.name}</div>
2025-03-03 21:16:16 +08:00
<div class="text-xs text-gray-500 flex items-center mt-1">
{section.sections.length > 0 && (
<span class="flex items-center mr-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
</svg>
{section.sections.length} 个子目录
</span>
)}
{section.articles.length > 0 && (
<span class="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
{section.articles.length} 篇文章
</span>
)}
</div>
</div>
<div class="text-primary-500 opacity-0 group-hover:opacity-100">
2025-03-03 21:16:16 +08:00
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
</svg>
</div>
</div>
</a>
);
})}
{/* 文章卡片 - 根据是否有标签过滤显示不同内容 */}
{tagFilter ? (
// 显示标签过滤后的文章
filteredArticles.map(article => (
<div class="group flex flex-col h-full border border-gray-200 dark:border-gray-700 rounded-xl bg-white dark:bg-gray-800 hover:shadow-xl hover:-translate-y-1 shadow-lg recent-article">
<a href={getArticleUrl(article.id)}
class="p-5 block flex-grow"
data-astro-prefetch="viewport">
<div class="flex items-start">
<div class="w-10 h-10 flex-shrink-0 flex items-center justify-center rounded-lg bg-primary-100 text-primary-600 group-hover:bg-primary-200">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z" />
</svg>
2025-03-03 21:16:16 +08:00
</div>
<div class="ml-3 flex-1 min-w-0">
<h3 class="font-bold text-base text-gray-800 dark:text-gray-100 group-hover:text-primary-700 dark:group-hover:text-primary-300 line-clamp-2">{article.data.title}</h3>
{article.body && (
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1 line-clamp-2 break-words">
{article.data.summary}
</p>
)}
<div class="text-xs text-gray-500 mt-2 flex items-center justify-between">
<time datetime={article.data.date.toISOString()}>
{article.data.date.toLocaleDateString('zh-CN', {year: 'numeric', month: 'long', day: 'numeric'})}
</time>
<span class="text-primary-600 font-medium truncate ml-2">阅读全文</span>
</div>
</div>
2025-03-03 21:16:16 +08:00
</div>
</a>
{article.data.tags && article.data.tags.length > 0 && (
<div class="flex flex-wrap gap-2 mt-2 px-5 pb-5">
{article.data.tags.map(tag => (
<a href={`/articles?tags=${tag}`} class="text-xs bg-primary-50 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 py-1 px-2 rounded-full hover:bg-primary-100 dark:hover:bg-primary-900/50">
#{tag}
</a>
))}
</div>
)}
</div>
2025-03-03 21:16:16 +08:00
))
) : (
// 显示当前目录的文章
currentArticles.map(articlePath => {
2025-03-10 13:56:56 +08:00
// 获取文章ID - 不需要移除src/content前缀因为contentStructure中已经是相对路径
const articleId = articlePath;
2025-03-03 21:16:16 +08:00
2025-03-10 13:56:56 +08:00
// 尝试匹配文章
const article = articles.find(a => a.id === articleId);
2025-03-03 21:16:16 +08:00
if (!article) {
return (
2025-03-08 18:16:42 +08:00
<div class="flex flex-col h-full p-5 border border-red-200 rounded-xl bg-red-50 shadow-lg">
2025-03-03 21:16:16 +08:00
<div class="flex items-start">
<div class="w-10 h-10 flex-shrink-0 flex items-center justify-center rounded-lg bg-red-100 text-red-600">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<div class="ml-3 flex-1">
<h3 class="font-bold text-base text-red-800">文章不存在</h3>
2025-03-10 13:56:56 +08:00
<p class="text-xs text-red-600 mt-1">
<div>原始路径: {articlePath}</div>
<div>文章ID: {articleId}</div>
<div>当前目录: {currentPath}</div>
</p>
<div class="text-xs text-red-500 mt-2">
<div>可用的文章ID:</div>
<div class="line-clamp-3">{articles.map(a => a.id).join(', ')}</div>
2025-03-03 21:16:16 +08:00
</div>
</div>
</div>
</div>
);
}
return (
<div class="group flex flex-col h-full border border-gray-200 dark:border-gray-700 rounded-xl bg-white dark:bg-gray-800 hover:shadow-xl hover:-translate-y-1 shadow-lg recent-article">
<a href={getArticleUrl(article.id)}
class="p-5 block flex-grow"
data-astro-prefetch="viewport">
<div class="flex items-start">
<div class="w-10 h-10 flex-shrink-0 flex items-center justify-center rounded-lg bg-primary-100 text-primary-600 group-hover:bg-primary-200">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z" />
</svg>
2025-03-03 21:16:16 +08:00
</div>
<div class="ml-3 flex-1 min-w-0">
<h3 class="font-bold text-base text-gray-800 dark:text-gray-100 group-hover:text-primary-700 dark:group-hover:text-primary-300 line-clamp-2">{article.data.title}</h3>
{article.body && (
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1 line-clamp-2 break-words">
{article.data.summary}
</p>
)}
<div class="text-xs text-gray-500 mt-2 flex items-center justify-between">
<time datetime={article.data.date.toISOString()}>
{article.data.date.toLocaleDateString('zh-CN', {year: 'numeric', month: 'long', day: 'numeric'})}
</time>
<span class="text-primary-600 font-medium truncate ml-2">阅读全文</span>
</div>
</div>
2025-03-03 21:16:16 +08:00
</div>
</a>
{article.data.tags && article.data.tags.length > 0 && (
<div class="flex flex-wrap gap-2 mt-2 px-5 pb-5">
{article.data.tags.map(tag => (
<a href={`/articles?tags=${tag}`} class="text-xs bg-primary-50 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 py-1 px-2 rounded-full hover:bg-primary-100 dark:hover:bg-primary-900/50">
#{tag}
</a>
))}
</div>
)}
</div>
2025-03-03 21:16:16 +08:00
);
})
)}
</div>
{/* 空内容提示 */}
{((tagFilter && filteredArticles.length === 0) || (!tagFilter && currentSections.length === 0 && currentArticles.length === 0)) && (
2025-03-08 18:16:42 +08:00
<div class="text-center py-16 bg-white rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 mb-12">
2025-03-03 21:16:16 +08:00
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mx-auto text-primary-200 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
</svg>
<h3 class="text-2xl font-bold text-gray-700 mb-2">
{tagFilter ? `没有找到标签为 "${tagFilter}" 的文章` : '此目录为空'}
</h3>
<p class="text-gray-500 max-w-md mx-auto">
{tagFilter ? '请尝试其他标签或返回文章列表' : '此目录下暂无内容,请浏览其他目录或返回上一级'}
</p>
</div>
)}
</>
) : pageType === 'filter' ? (
<!-- 筛选视图 -->
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 mb-6">
<!-- 筛选控件 -->
<div class="p-4 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-center justify-between mb-3">
<h2 class="text-lg font-semibold text-gray-800 dark:text-gray-100 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-primary-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
</svg>
文章筛选
</h2>
<button id="resetAllFilters" class="text-sm text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 focus:outline-none hover:translate-x-0.5 hover:scale-105 flex items-center">
<span>重置所有筛选</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
<!-- 时间筛选 -->
<div class="filter-group">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">发布时间</label>
<div class="relative">
<select
id="dateFilter"
class="w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 py-2 px-3 rounded-lg focus:ring-primary-500 focus:border-primary-500 focus:ring-1 appearance-none"
>
<option value="all">全部时间</option>
<option value="week">本周</option>
<option value="month">本月</option>
<option value="year">今年</option>
<option value="custom">自定义范围</option>
</select>
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700 dark:text-gray-300">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</div>
</div>
<!-- 自定义日期范围选择器 -->
<div id="customDateContainer" class="mt-3 hidden">
<div class="grid grid-cols-2 gap-2">
<div>
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">开始日期</label>
<input
type="date"
id="startDate"
class="w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 py-1.5 px-2 rounded-lg focus:ring-primary-500 focus:border-primary-500 focus:ring-1"
/>
</div>
<div>
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">结束日期</label>
<input
type="date"
id="endDate"
class="w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 py-1.5 px-2 rounded-lg focus:ring-primary-500 focus:border-primary-500 focus:ring-1"
/>
</div>
</div>
<button
id="applyDateFilter"
class="w-full mt-2 bg-primary-600 hover:bg-primary-700 text-white text-sm py-1.5 px-3 rounded-lg"
>
应用日期筛选
</button>
</div>
</div>
<!-- 排序方式 -->
<div class="filter-group">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">排序方式</label>
<div class="relative">
<select
id="sortOption"
class="w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 py-2 px-3 rounded-lg focus:ring-primary-500 focus:border-primary-500 focus:ring-1 appearance-none"
>
<option value="newest">最新发布</option>
<option value="oldest">最早发布</option>
<option value="title_asc">标题 A-Z</option>
<option value="title_desc">标题 Z-A</option>
</select>
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700 dark:text-gray-300">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</div>
</div>
</div>
<!-- 标签筛选器 -->
<div class="filter-group">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">文章标签</label>
<div class="relative">
<button
id="tagSelectorButton"
class="w-full text-left flex justify-between items-center bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 py-2 px-3 rounded-lg focus:ring-primary-500 focus:border-primary-500 focus:ring-1 focus:outline-none"
>
<span id="tagSelectorText" class="truncate">选择标签</span>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
<div
id="tagDropdown"
class="hidden absolute z-20 mt-1 w-full bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg max-h-60 overflow-y-auto"
>
<div class="p-2">
<div class="sticky top-0 bg-white dark:bg-gray-800 pb-2 mb-1 border-b border-gray-200 dark:border-gray-700">
<div class="relative">
<input
type="text"
id="tagSearchInput"
placeholder="搜索标签..."
class="w-full py-1.5 pl-8 pr-2 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500"
/>
<svg class="w-4 h-4 absolute left-2 top-1/2 transform -translate-y-1/2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</div>
</div>
<div id="tagOptions" class="space-y-1 max-h-32 overflow-y-auto">
{tags.map(tag => (
<label class="flex items-center p-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded cursor-pointer">
<input
type="checkbox"
value={tag}
class="tag-checkbox h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 dark:border-gray-600 rounded"
/>
<span class="ml-2 text-gray-700 dark:text-gray-200 text-sm">{tag}</span>
</label>
))}
</div>
<div class="flex justify-between pt-2 mt-2 border-t border-gray-200 dark:border-gray-700 sticky bottom-0 bg-white dark:bg-gray-800">
<button
id="clearTagsButton"
class="text-xs text-gray-600 dark:text-gray-400 hover:text-primary-600 dark:hover:text-primary-400"
>
清除选择
</button>
<button
id="applyTagsButton"
class="px-3 py-1 text-xs bg-primary-600 hover:bg-primary-700 text-white rounded"
>
应用
</button>
</div>
</div>
</div>
</div>
<div id="selectedTagsContainer" class="flex flex-wrap gap-1 mt-2 min-h-6">
<!-- 这里会动态显示已选标签 -->
</div>
</div>
</div>
</div>
2025-03-03 21:16:16 +08:00
<!-- 选中的筛选条件展示 -->
<div id="activeFiltersContainer" class="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
<div class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">已选筛选条件:</div>
<div id="selectedFilters" class="flex flex-wrap gap-2">
<div class="text-sm text-gray-500 dark:text-gray-400 italic" id="noFiltersMessage">无筛选条件,显示全部文章</div>
<!-- 这里将由JavaScript动态填充 -->
</div>
</div>
<!-- 筛选结果统计 -->
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-800/50 flex flex-wrap justify-between items-center">
<div class="text-sm text-gray-600 dark:text-gray-400">
显示 <span id="filteredCount" class="font-medium text-primary-600 dark:text-primary-400">0</span> 篇文章(共 {sortedArticles.length} 篇)
</div>
<div class="flex items-center gap-3">
<!-- 每页显示数量选择器 -->
<div class="flex items-center">
<label class="mr-2 text-sm text-gray-600 dark:text-gray-400">每页显示:</label>
<select
id="pageSizeOption"
class="bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 py-1 px-2 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 focus:ring-1 appearance-none"
>
<option value="12">12 篇</option>
<option value="24">24 篇</option>
<option value="36">36 篇</option>
<option value="48">48 篇</option>
</select>
</div>
</div>
2025-03-03 21:16:16 +08:00
</div>
</div>
<!-- 文章列表 -->
<div id="filteredArticles" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
{sortedArticles.map(article => (
<div class="group flex flex-col h-full border border-gray-200 dark:border-gray-700 rounded-xl bg-white dark:bg-gray-800 hover:shadow-xl hover:-translate-y-1 shadow-lg recent-article article-card"
data-article-date={article.data.date.toISOString()}
data-article-tags={article.data.tags ? article.data.tags.join(',') : ''}
data-article-title={article.data.title}>
<a href={getArticleUrl(article.id)}
class="p-5 block flex-grow"
data-astro-prefetch="viewport">
<div class="flex items-start">
<div class="w-10 h-10 flex-shrink-0 flex items-center justify-center rounded-lg bg-primary-100 text-primary-600 group-hover:bg-primary-200">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z" />
</svg>
2025-03-10 14:05:40 +08:00
</div>
<div class="ml-3 flex-1 min-w-0">
<h3 class="font-bold text-base text-gray-800 dark:text-gray-100 group-hover:text-primary-700 dark:group-hover:text-primary-300 line-clamp-2">{article.data.title}</h3>
{article.body && (
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1 line-clamp-2 break-words">
{article.data.summary}
</p>
)}
<div class="text-xs text-gray-500 mt-2 flex items-center justify-between">
<time datetime={article.data.date.toISOString()}>
2025-03-10 14:05:40 +08:00
{article.data.date.toLocaleDateString('zh-CN', {year: 'numeric', month: 'long', day: 'numeric'})}
</time>
<span class="text-primary-600 font-medium truncate ml-2">阅读全文</span>
</div>
</div>
</div>
</a>
{article.data.tags && article.data.tags.length > 0 && (
<div class="flex flex-wrap gap-2 mt-2 px-5 pb-5">
{article.data.tags.map(tag => (
<a href={`/articles/filtered?tags=${tag}`} class="text-xs bg-primary-50 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 py-1 px-2 rounded-full hover:bg-primary-100 dark:hover:bg-primary-900/50">
#{tag}
</a>
))}
</div>
)}
</div>
))}
</div>
<!-- 分页控件 -->
<div id="pagination" class="flex justify-center mt-8 mb-12">
<div class="inline-flex items-center justify-center bg-white dark:bg-gray-800 rounded-lg shadow-md border border-gray-200 dark:border-gray-700">
<button
id="prevPage"
class="text-gray-600 dark:text-gray-300 hover:text-primary-600 dark:hover:text-primary-400 px-4 py-2 border-r border-gray-200 dark:border-gray-700 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
disabled
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
</button>
<div class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300">
第 <span id="currentPage">1</span> 页,共 <span id="totalPages">1</span> 页
</div>
<button
id="nextPage"
class="text-gray-600 dark:text-gray-300 hover:text-primary-600 dark:hover:text-primary-400 px-4 py-2 border-l border-gray-200 dark:border-gray-700 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
<!-- 无结果提示 -->
<div id="noResultsMessage" class="hidden text-center py-16 bg-white rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 mb-12">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mx-auto text-primary-200 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
</svg>
<h3 class="text-2xl font-bold text-gray-700 mb-2">没有找到符合条件的文章</h3>
<p class="text-gray-500 max-w-md mx-auto">请尝试调整筛选条件或者清除筛选条件</p>
</div>
<!-- 筛选功能的客户端脚本 -->
<script is:inline>
// 使用"进入→绑定→退出完全清理"模式类似Header.astro的实现
(function () {
// 确保脚本只在文章页面执行
const isArticlePage = window.location.pathname.startsWith('/articles');
// 如果不是文章页面,直接返回
if (!isArticlePage) {
return;
}
// 脚本实例ID用于跟踪当前实例
const SCRIPT_INSTANCE_ID = Date.now() + '-' + Math.random().toString(36).substring(2, 9);
// 标记脚本是否已销毁
let isDestroyed = false;
// 统一管理所有事件监听器
const listeners = [];
// 添加事件监听器并记录,方便后续统一清理
function addListener(element, eventType, handler, options) {
if (!element) return null;
element.addEventListener(eventType, handler, options);
listeners.push({ element, eventType, handler });
return handler;
}
2025-03-10 14:05:40 +08:00
// 清理函数 - 移除所有事件监听器
function cleanup() {
if (isDestroyed) {
return;
}
// 标记为已销毁
isDestroyed = true;
// 移除所有监听器
listeners.forEach(({ element, eventType, handler }) => {
try {
element.removeEventListener(eventType, handler);
} catch (err) {
}
});
// 清空数组
listeners.length = 0;
// 清除其他引用和状态
// 移除页面转换监听器
document.removeEventListener("astro:after-swap", onPageTransition);
document.removeEventListener("astro:page-load", onPageTransition);
document.removeEventListener("swup:contentReplaced", onPageTransition);
// 移除页面可见性监听器
document.removeEventListener('visibilitychange', onPageVisibilityChange);
// 清除定时检查
if (pageCheckInterval) {
clearInterval(pageCheckInterval);
}
// 从全局范围中移除自身引用
if (window.__articlesFilterInstances) {
window.__articlesFilterInstances = window.__articlesFilterInstances.filter(id => id !== SCRIPT_INSTANCE_ID);
}
}
// 跟踪页面离开,确保完全清理
function onPageVisibilityChange() {
// 只在页面隐藏且URL变化时才清理
if (document.visibilityState === 'hidden' && !window.location.pathname.startsWith('/articles') && !isDestroyed) {
cleanup();
}
}
// 检查是否离开了文章页面
function hasLeftArticlesPage() {
return !window.location.pathname.startsWith('/articles');
}
// 定期检查页面状态 - 只在500ms检查一次减少性能影响
const pageCheckInterval = setInterval(() => {
if (hasLeftArticlesPage() && !isDestroyed) {
cleanup();
}
}, 1000); // 降低检查频率到1秒
// 添加到全局实例跟踪
if (!window.__articlesFilterInstances) {
window.__articlesFilterInstances = [];
}
window.__articlesFilterInstances.push(SCRIPT_INSTANCE_ID);
// 添加页面可见性变化监听
document.addEventListener('visibilitychange', onPageVisibilityChange);
2025-03-10 14:05:40 +08:00
// 优化DOM更新以减少重排
function batchDOMUpdates(callback) {
// 使用requestAnimationFrame来批量处理DOM更新
// 这样可以避免强制重排
window.requestAnimationFrame(() => {
if (!isDestroyed) callback();
});
}
2025-03-10 14:05:40 +08:00
// 初始化筛选功能的主函数
function setupArticlesFilter() {
// 筛选控件元素
const dateFilter = document.getElementById('dateFilter');
const sortOption = document.getElementById('sortOption');
const customDateContainer = document.getElementById('customDateContainer');
const startDateInput = document.getElementById('startDate');
const endDateInput = document.getElementById('endDate');
const applyDateFilterBtn = document.getElementById('applyDateFilter');
const resetAllFiltersBtn = document.getElementById('resetAllFilters');
// 视图相关元素
const filteredArticles = document.getElementById('filteredArticles');
const noResultsMessage = document.getElementById('noResultsMessage');
const selectedFilters = document.getElementById('selectedFilters');
const noFiltersMessage = document.getElementById('noFiltersMessage');
// 标签选择器相关元素
const tagSelectorButton = document.getElementById('tagSelectorButton');
const tagSelectorText = document.getElementById('tagSelectorText');
const tagDropdown = document.getElementById('tagDropdown');
const tagSearchInput = document.getElementById('tagSearchInput');
const tagCheckboxes = document.querySelectorAll('.tag-checkbox');
const clearTagsButton = document.getElementById('clearTagsButton');
const applyTagsButton = document.getElementById('applyTagsButton');
const selectedTagsContainer = document.getElementById('selectedTagsContainer');
const tagOptions = document.getElementById('tagOptions');
// 分页相关元素
const paginationElement = document.getElementById('pagination');
const prevPageButton = document.getElementById('prevPage');
const nextPageButton = document.getElementById('nextPage');
const currentPageElement = document.getElementById('currentPage');
const totalPagesElement = document.getElementById('totalPages');
// 检查必要的元素是否存在,如果不存在,可能不是筛选页面
if (!filteredArticles || !selectedFilters) {
return;
}
// 从URL中获取筛选参数
const urlParams = new URLSearchParams(window.location.search);
// 当前活跃的筛选条件
const activeFilters = {
date: urlParams.get('date') || 'all',
tags: urlParams.get('tags')?.split(',') || [],
sort: urlParams.get('sort') || 'newest',
pageSize: parseInt(urlParams.get('limit') || '12'),
currentPage: parseInt(urlParams.get('page') || '1'),
startDate: urlParams.get('startDate') || (startDateInput ? startDateInput.value : ''),
endDate: urlParams.get('endDate') || (endDateInput ? endDateInput.value : '')
};
// 视图相关元素
const pageSizeOption = document.getElementById('pageSizeOption');
// 储存所有文章数据(全局缓存)
let allArticles = [];
let isArticlesLoaded = false;
// 从API获取所有文章数据并在客户端进行筛选
async function fetchFilteredArticles() {
try {
// 第一次加载或缓存为空时,获取所有文章数据
if (!isArticlesLoaded || allArticles.length === 0) {
const response = await fetch('/api/articles');
if (!response.ok) {
throw new Error(`获取文章失败: ${response.statusText}`);
}
const apiData = await response.json();
if (!apiData.success) {
throw new Error('API返回错误');
}
// 缓存所有文章数据
allArticles = apiData.articles || [];
isArticlesLoaded = true;
}
// ----- 以下是客户端筛选逻辑 -----
let filteredArticles = [...allArticles];
// 标签筛选 - 支持多标签
if (activeFilters.tags && activeFilters.tags.length > 0) {
filteredArticles = filteredArticles.filter(article =>
article.tags && Array.isArray(article.tags) &&
activeFilters.tags.some(tag => article.tags.includes(tag))
);
}
// 日期筛选
if (activeFilters.date !== 'all') {
const now = new Date();
if (activeFilters.date === 'custom') {
// 自定义日期范围筛选
if (activeFilters.startDate || activeFilters.endDate) {
filteredArticles = filteredArticles.filter(article => {
if (!article.date) return false;
const articleDate = new Date(article.date);
// 检查开始日期
if (activeFilters.startDate) {
const start = new Date(activeFilters.startDate);
// 文章日期必须大于等于开始日期
if (articleDate < start) return false;
}
// 检查结束日期
if (activeFilters.endDate) {
const end = new Date(activeFilters.endDate);
// 设置为当天最后一毫秒,以便包含整个结束日期
end.setHours(23, 59, 59, 999);
// 文章日期必须小于等于结束日期
if (articleDate > end) return false;
}
return true;
});
}
} else {
switch (activeFilters.date) {
case 'today':
// 今天发布的文章
filteredArticles = filteredArticles.filter(article => {
if (!article.date) return false;
const articleDate = new Date(article.date);
return articleDate.toDateString() === now.toDateString();
});
break;
case 'week':
// 本周发布的文章
const weekStart = new Date(now);
weekStart.setDate(now.getDate() - now.getDay());
weekStart.setHours(0, 0, 0, 0);
filteredArticles = filteredArticles.filter(article => {
if (!article.date) return false;
const articleDate = new Date(article.date);
return articleDate >= weekStart;
});
break;
case 'month':
// 本月发布的文章
filteredArticles = filteredArticles.filter(article => {
if (!article.date) return false;
const articleDate = new Date(article.date);
return (
articleDate.getMonth() === now.getMonth() &&
articleDate.getFullYear() === now.getFullYear()
);
});
break;
case 'year':
// 今年发布的文章
filteredArticles = filteredArticles.filter(article => {
if (!article.date) return false;
const articleDate = new Date(article.date);
return articleDate.getFullYear() === now.getFullYear();
});
break;
}
}
}
// 按日期或标题排序
filteredArticles = filteredArticles.sort((a, b) => {
if (!a.date || !b.date) return 0;
const dateA = new Date(a.date).getTime();
const dateB = new Date(b.date).getTime();
switch (activeFilters.sort) {
case 'oldest':
return dateA - dateB;
case 'title_asc':
return (a.title || '').localeCompare(b.title || '', 'zh-CN');
case 'title_desc':
return (b.title || '').localeCompare(a.title || '', 'zh-CN');
case 'newest':
default:
return dateB - dateA;
}
});
// 计算总页数
const totalItems = filteredArticles.length;
const totalPages = Math.ceil(totalItems / activeFilters.pageSize) || 1;
// 确保当前页码有效
const validatedPage = Math.max(1, Math.min(activeFilters.currentPage, totalPages));
if (validatedPage !== activeFilters.currentPage) {
activeFilters.currentPage = validatedPage;
}
// 执行分页
const startIndex = (activeFilters.currentPage - 1) * activeFilters.pageSize;
const endIndex = startIndex + activeFilters.pageSize;
const paginatedArticles = filteredArticles.slice(startIndex, endIndex);
// 返回分页和筛选后的结果
return {
articles: paginatedArticles,
total: totalItems,
page: activeFilters.currentPage,
limit: activeFilters.pageSize,
totalPages: totalPages,
filters: {
tags: activeFilters.tags,
date: activeFilters.date,
sort: activeFilters.sort,
startDate: activeFilters.startDate,
endDate: activeFilters.endDate
}
};
} catch (error) {
return {
articles: [],
total: 0,
page: activeFilters.currentPage,
totalPages: 0,
filters: {
tags: activeFilters.tags,
date: activeFilters.date,
sort: activeFilters.sort
}
};
}
}
// 创建文章卡片元素
function createArticleCard(article) {
const card = document.createElement('div');
card.className = 'group flex flex-col h-full border border-gray-200 dark:border-gray-700 rounded-xl bg-white dark:bg-gray-800 hover:shadow-xl hover:-translate-y-1 shadow-lg recent-article article-card';
// 安全地设置数据属性
if (article.date) {
card.dataset.articleDate = typeof article.date === 'string' ? article.date : article.date.toISOString();
}
if (article.tags && Array.isArray(article.tags)) {
card.dataset.articleTags = article.tags.join(',');
} else {
card.dataset.articleTags = '';
}
card.dataset.articleTitle = article.title || '';
// 格式化日期
let dateFormatted = '';
let dateIsoString = '';
if (article.date) {
try {
const dateObj = new Date(article.date);
dateFormatted = dateObj.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
dateIsoString = dateObj.toISOString();
} catch (e) {
console.error('日期格式化错误:', e);
}
}
2025-03-10 14:05:40 +08:00
// 获取当前URL参数并添加到文章链接
const currentParams = new URLSearchParams(window.location.search);
let articleUrl = article.url || '#';
// 如果文章URL有效且不是锚点添加当前筛选参数
if (articleUrl && articleUrl !== '#') {
// 检查URL是否已包含查询参数
const hasParams = articleUrl.includes('?');
articleUrl = articleUrl + (hasParams ? '&' : '?') + currentParams.toString();
}
try {
// 构建卡片内容HTML
let cardHtml = `
<a href="${articleUrl}" class="p-5 block flex-grow" data-astro-prefetch="viewport">
<div class="flex items-start">
<div class="w-10 h-10 flex-shrink-0 flex items-center justify-center rounded-lg bg-primary-100 text-primary-600 group-hover:bg-primary-200">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z" />
</svg>
</div>
<div class="ml-3 flex-1 min-w-0">
<h3 class="font-bold text-base text-gray-800 dark:text-gray-100 group-hover:text-primary-700 dark:group-hover:text-primary-300 line-clamp-2">${article.title || '无标题'}</h3>
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1 line-clamp-2 break-words">
${article.summary || ''}
</p>
<div class="text-xs text-gray-500 mt-2 flex items-center justify-between">
${dateFormatted ? `
<time datetime="${dateIsoString}">
${dateFormatted}
</time>
` : '<span>无日期</span>'}
<span class="text-primary-600 font-medium truncate ml-2">阅读全文</span>
2025-03-10 14:05:40 +08:00
</div>
</div>
2025-03-10 14:05:40 +08:00
</div>
</a>
`;
if (article.tags && Array.isArray(article.tags) && article.tags.length > 0) {
const tagsHtml = article.tags.map(tag => {
// 为每个标签创建链接,保留当前的筛选参数
const tagUrl = "/articles?tags=" + tag;
return `<a href="${tagUrl}" class="text-xs bg-primary-50 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 py-1 px-2 rounded-full hover:bg-primary-100 dark:hover:bg-primary-900/50">
#${tag}
</a>`;
}).join('');
cardHtml += `
<div class="flex flex-wrap gap-2 mt-2 px-5 pb-5">
${tagsHtml}
2025-03-10 14:05:40 +08:00
</div>
`;
}
card.innerHTML = cardHtml;
} catch (error) {
console.error('创建文章卡片错误:', error);
card.innerHTML = `
<div class="p-5">
<p class="text-red-500">加载文章出错</p>
</div>
`;
}
return card;
}
// 应用所有筛选条件
async function applyFilters() {
if (isDestroyed) return;
// 验证DOM元素存在
const filteredArticles = document.getElementById('filteredArticles');
const filteredCountElement = document.getElementById('filteredCount');
const selectedFilters = document.getElementById('selectedFilters');
if (!filteredArticles) {
return;
}
if (!selectedFilters) {
return;
}
// 显示加载状态
if (filteredArticles) {
filteredArticles.innerHTML = `
<div class="col-span-3 flex items-center justify-center py-12">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
<span class="ml-3 text-gray-600 dark:text-gray-400">加载中...</span>
</div>
`;
}
// 从API获取筛选后的文章
const data = await fetchFilteredArticles();
// 完整打印API返回数据查看是否包含正确的页码
// 确保使用API返回的页码更新本地状态
if (data.page) {
activeFilters.currentPage = data.page;
}
// 更新筛选结果计数
if (filteredCountElement) {
filteredCountElement.textContent = data.total.toString();
}
// 更新URL参数
updateUrlParams();
// 更新选中的筛选标签展示
updateSelectedFilters();
// 更新文章列表
if (filteredArticles) {
// 清空文章容器
filteredArticles.innerHTML = '';
if (data.articles.length === 0) {
// 显示无结果消息
if (noResultsMessage) {
noResultsMessage.classList.remove('hidden');
filteredArticles.classList.add('hidden');
if (paginationElement) paginationElement.classList.add('hidden');
}
} else {
// 隐藏无结果消息
if (noResultsMessage) {
noResultsMessage.classList.add('hidden');
filteredArticles.classList.remove('hidden');
}
// 创建文档片段提高性能
const fragment = document.createDocumentFragment();
// 为每篇文章创建卡片
data.articles.forEach(article => {
const card = createArticleCard(article);
fragment.appendChild(card);
});
// 将片段一次性添加到容器中
filteredArticles.appendChild(fragment);
// 处理文章内的标签链接添加当前筛选参数不包括tags参数
setTimeout(() => {
try {
const currentParams = new URLSearchParams(window.location.search);
currentParams.delete('tags'); // 删除当前的tags参数避免冲突
// 获取所有标签链接
const tagLinks = filteredArticles.querySelectorAll('a[href^="/articles?tags="]');
tagLinks.forEach((link, index) => {
const href = link.getAttribute('href');
const tagParam = new URLSearchParams(href.substring(href.indexOf('?')));
const tag = tagParam.get('tags');
if (tag) {
// 创建新的URL参数合并当前参数和标签参数
const newParams = new URLSearchParams(currentParams);
newParams.set('tags', tag);
// 更新链接
const newHref = `/articles?${newParams.toString()}`;
link.setAttribute('href', newHref);
}
});
} catch (err) {
console.error('处理标签链接时出错:', err);
}
}, 0);
}
}
// 更新分页信息
if (currentPageElement) {
currentPageElement.textContent = data.page.toString();
}
if (totalPagesElement) {
totalPagesElement.textContent = data.totalPages.toString();
}
// 更新分页按钮状态
if (prevPageButton) {
prevPageButton.disabled = data.page <= 1;
}
if (nextPageButton) {
nextPageButton.disabled = data.page >= data.totalPages;
}
// 显示/隐藏分页控件
if (paginationElement) {
if (data.total <= activeFilters.pageSize) {
paginationElement.classList.add('hidden');
} else {
paginationElement.classList.remove('hidden');
}
}
// 隐藏或显示无筛选条件提示
if (noFiltersMessage) {
const hasActiveFilters = activeFilters.date !== 'all' || activeFilters.tags.length > 0 || activeFilters.sort !== 'newest' || activeFilters.currentPage > 1;
noFiltersMessage.classList.toggle('hidden', hasActiveFilters);
}
}
// 更新选中的筛选条件展示
function updateSelectedFilters() {
if (isDestroyed) return;
const selectedFilters = document.getElementById('selectedFilters');
if (!selectedFilters) {
return;
}
selectedFilters.innerHTML = '';
// 日期筛选标签
if (activeFilters.date !== 'all') {
let dateLabel = '';
if (activeFilters.date === 'custom') {
const formatDate = (dateStr) => {
if (!dateStr) return '';
const date = new Date(dateStr);
return date.toLocaleDateString('zh-CN');
};
if (activeFilters.startDate && activeFilters.endDate) {
dateLabel = `${formatDate(activeFilters.startDate)} 至 ${formatDate(activeFilters.endDate)}`;
} else if (activeFilters.startDate) {
dateLabel = `${formatDate(activeFilters.startDate)} 之后`;
} else if (activeFilters.endDate) {
dateLabel = `${formatDate(activeFilters.endDate)} 之前`;
} else {
dateLabel = '自定义';
}
} else {
dateLabel = {
'week': '本周',
'month': '本月',
'year': '今年'
}[activeFilters.date] || '';
}
addFilterTag('日期: ' + dateLabel, () => {
activeFilters.date = 'all';
activeFilters.startDate = '';
activeFilters.endDate = '';
if (dateFilter) dateFilter.value = 'all';
if (startDateInput) startDateInput.value = '';
if (endDateInput) endDateInput.value = '';
toggleCustomDateContainer();
activeFilters.currentPage = 1;
applyFilters();
});
}
// 标签筛选 - 多选
if (activeFilters.tags.length > 0) {
addFilterTag(`标签: ${activeFilters.tags.length} 个已选`, () => {
clearAllTags();
activeFilters.currentPage = 1;
applyFilters();
});
}
// 排序方式
if (activeFilters.sort !== 'newest') {
const sortLabels = {
'newest': '最新',
'oldest': '最早',
'title_asc': '标题 A-Z',
'title_desc': '标题 Z-A'
};
const sortLabel = sortLabels[activeFilters.sort] || activeFilters.sort;
addFilterTag('排序: ' + sortLabel, () => {
activeFilters.sort = 'newest';
if (sortOption) sortOption.value = 'newest';
applyFilters();
});
}
// 每页显示数量
if (activeFilters.pageSize !== 12) {
addFilterTag(`每页: ${activeFilters.pageSize} 篇`, () => {
activeFilters.pageSize = 12;
if (pageSizeOption) pageSizeOption.value = '12';
activeFilters.currentPage = 1;
applyFilters();
});
}
// 当前页码 (不是第一页时显示)
if (activeFilters.currentPage > 1) {
addFilterTag(`页码: 第 ${activeFilters.currentPage} 页`, () => {
activeFilters.currentPage = 1;
applyFilters();
});
}
}
// 切换标签下拉菜单显示状态
function toggleTagDropdown() {
if (tagDropdown) {
tagDropdown.classList.toggle('hidden');
// 如果显示下拉菜单,聚焦搜索框
if (!tagDropdown.classList.contains('hidden') && tagSearchInput) {
tagSearchInput.focus();
}
}
}
// 关闭标签下拉菜单
function closeTagDropdown() {
if (tagDropdown) {
tagDropdown.classList.add('hidden');
}
}
// 更新标签按钮文本
function updateTagSelectorText() {
if (tagSelectorText) {
if (activeFilters.tags.length > 0) {
tagSelectorText.textContent = `已选 ${activeFilters.tags.length} 个标签`;
tagSelectorText.classList.add('text-primary-600', 'dark:text-primary-400');
} else {
tagSelectorText.textContent = '选择标签';
tagSelectorText.classList.remove('text-primary-600', 'dark:text-primary-400');
}
}
}
// 更新选中标签的显示
function updateSelectedTagsDisplay() {
if (!selectedTagsContainer) return;
// 使用批量DOM更新
batchDOMUpdates(() => {
// 清空已有标签
selectedTagsContainer.innerHTML = '';
// 如果没有选中标签,隐藏容器
if (activeFilters.tags.length === 0) {
return;
}
// 为每个选中的标签创建一个标签元素
const fragment = document.createDocumentFragment();
activeFilters.tags.forEach(tag => {
const tagElement = document.createElement('div');
tagElement.className = 'inline-flex items-center px-2 py-1 rounded-full text-xs bg-primary-50 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400';
const tagText = document.createElement('span');
tagText.textContent = tag;
const removeButton = document.createElement('button');
removeButton.className = 'ml-1 text-primary-400 hover:text-primary-600 focus:outline-none';
removeButton.innerHTML = `
<svg class="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
`;
// 点击移除按钮时移除对应标签
addListener(removeButton, 'click', () => {
activeFilters.tags = activeFilters.tags.filter(t => t !== tag);
// 更新复选框状态
updateCheckboxesFromActiveTags();
// 更新显示
updateSelectedTagsDisplay();
updateTagSelectorText();
// 应用筛选
activeFilters.currentPage = 1;
applyFilters();
});
tagElement.appendChild(tagText);
tagElement.appendChild(removeButton);
fragment.appendChild(tagElement);
});
// 一次性添加所有元素
selectedTagsContainer.appendChild(fragment);
});
}
// 根据搜索关键词过滤标签选项
function filterTagOptions() {
if (!tagSearchInput || !tagOptions) return;
const searchTerm = tagSearchInput.value.toLowerCase().trim();
const tagLabels = tagOptions.querySelectorAll('label');
let hasVisibleOptions = false;
tagLabels.forEach(label => {
const tagName = label.querySelector('span')?.textContent?.toLowerCase() || '';
if (searchTerm === '' || tagName.includes(searchTerm)) {
label.style.display = '';
hasVisibleOptions = true;
} else {
label.style.display = 'none';
}
});
// 如果没有匹配项,显示提示信息
const noResultsEl = tagOptions.querySelector('.no-tag-results');
if (searchTerm && !hasVisibleOptions) {
if (!noResultsEl) {
const noResults = document.createElement('div');
noResults.className = 'no-tag-results text-center py-2 text-sm text-gray-500 dark:text-gray-400';
noResults.textContent = '没有找到匹配的标签';
tagOptions.appendChild(noResults);
}
} else if (noResultsEl) {
noResultsEl.remove();
}
}
// 从当前选中的复选框更新活跃标签
function updateActiveTagsFromCheckboxes() {
const selectedTags = [];
tagCheckboxes.forEach(checkbox => {
if (checkbox.checked) {
selectedTags.push(checkbox.value);
}
});
activeFilters.tags = selectedTags;
}
// 从活跃标签更新复选框选中状态
function updateCheckboxesFromActiveTags() {
tagCheckboxes.forEach(checkbox => {
checkbox.checked = activeFilters.tags.includes(checkbox.value);
});
}
// 清除所有选中标签
function clearAllTags() {
activeFilters.tags = [];
updateCheckboxesFromActiveTags();
updateSelectedTagsDisplay();
updateTagSelectorText();
}
// 切换自定义日期选择器显示
function toggleCustomDateContainer() {
if (dateFilter && dateFilter.value === 'custom') {
customDateContainer?.classList.remove('hidden');
} else {
customDateContainer?.classList.add('hidden');
}
}
// 更新URL参数
function updateUrlParams() {
const params = new URLSearchParams();
// 添加当前筛选条件
if (activeFilters.date !== 'all') {
params.set('date', activeFilters.date);
// 如果是自定义日期,添加日期范围
if (activeFilters.date === 'custom') {
if (activeFilters.startDate) {
params.set('startDate', activeFilters.startDate);
}
if (activeFilters.endDate) {
params.set('endDate', activeFilters.endDate);
}
}
}
// 添加标签筛选
if (activeFilters.tags.length > 0) {
params.set('tags', activeFilters.tags.join(','));
}
// 添加排序方式
if (activeFilters.sort !== 'newest') {
params.set('sort', activeFilters.sort);
}
// 添加分页信息
if (activeFilters.currentPage !== 1) {
params.set('page', activeFilters.currentPage.toString());
}
// 添加每页显示数量
if (activeFilters.pageSize !== 12) {
params.set('limit', activeFilters.pageSize.toString());
}
// 构建新的URL
const newUrl = window.location.pathname + (params.toString() ? '?' + params.toString() : '');
// 使用history API更新URL不刷新页面
window.history.pushState({}, '', newUrl);
}
// 添加筛选标签
function addFilterTag(text, onRemove) {
if (!selectedFilters) return;
const tag = document.createElement('div');
tag.className = 'inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-primary-50 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400';
const tagText = document.createElement('span');
tagText.textContent = text;
tag.appendChild(tagText);
if (onRemove) {
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-1.5 flex-shrink-0 focus:outline-none text-primary-400 hover:text-primary-600';
removeBtn.innerHTML = `
<svg class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
`;
addListener(removeBtn, 'click', onRemove);
tag.appendChild(removeBtn);
}
selectedFilters.appendChild(tag);
}
// 重置所有筛选条件
function resetAllFilters() {
activeFilters.date = 'all';
activeFilters.tags = [];
activeFilters.sort = 'newest';
activeFilters.currentPage = 1;
activeFilters.startDate = '';
activeFilters.endDate = '';
// 更新UI
if (dateFilter) dateFilter.value = 'all';
if (sortOption) sortOption.value = 'newest';
if (startDateInput) startDateInput.value = '';
if (endDateInput) endDateInput.value = '';
updateCheckboxesFromActiveTags();
updateSelectedTagsDisplay();
updateTagSelectorText();
toggleCustomDateContainer();
applyFilters();
}
// 初始化标签选择器
function initTagSelector() {
// 标签选择器点击事件
if (tagSelectorButton) {
addListener(tagSelectorButton, 'click', event => {
event.stopPropagation();
toggleTagDropdown();
});
}
// 点击页面其他位置关闭标签下拉菜单
addListener(document, 'click', event => {
if (tagDropdown && !tagDropdown.contains(event.target) &&
tagSelectorButton && !tagSelectorButton.contains(event.target)) {
closeTagDropdown();
}
});
// 标签搜索框输入事件
if (tagSearchInput) {
addListener(tagSearchInput, 'input', () => {
filterTagOptions();
});
// 防止点击搜索框关闭下拉菜单
addListener(tagSearchInput, 'click', event => {
event.stopPropagation();
});
}
// 每个标签复选框的变更事件
tagCheckboxes.forEach((checkbox, index) => {
addListener(checkbox, 'change', () => {
updateActiveTagsFromCheckboxes();
updateSelectedTagsDisplay();
updateTagSelectorText();
});
});
// 清除标签按钮
if (clearTagsButton) {
addListener(clearTagsButton, 'click', event => {
event.stopPropagation();
clearAllTags();
});
}
// 应用标签筛选按钮
if (applyTagsButton) {
addListener(applyTagsButton, 'click', event => {
event.stopPropagation();
closeTagDropdown();
activeFilters.currentPage = 1;
applyFilters();
});
}
}
// 初始化筛选器的值
if (dateFilter) {
dateFilter.value = activeFilters.date;
toggleCustomDateContainer(); // 确保自定义日期选择器的显示状态正确
}
if (sortOption) {
sortOption.value = activeFilters.sort;
}
if (pageSizeOption) {
pageSizeOption.value = activeFilters.pageSize.toString();
}
// 设置默认日期
if (startDateInput && activeFilters.startDate) {
startDateInput.value = activeFilters.startDate;
} else if (startDateInput && !startDateInput.value) {
const today = new Date();
startDateInput.valueAsDate = today; // 设置为今天
}
if (endDateInput && activeFilters.endDate) {
endDateInput.value = activeFilters.endDate;
} else if (endDateInput && !endDateInput.value) {
const today = new Date();
endDateInput.valueAsDate = today; // 设置为今天
}
// 根据URL参数更新复选框状态
updateCheckboxesFromActiveTags();
updateSelectedTagsDisplay();
updateTagSelectorText();
// 初始化标签选择器
initTagSelector();
// 绑定事件处理程序
// 重置所有筛选按钮
if (resetAllFiltersBtn) {
addListener(resetAllFiltersBtn, 'click', resetAllFilters);
}
// 日期筛选
if (dateFilter) {
addListener(dateFilter, 'change', () => {
activeFilters.date = dateFilter.value;
toggleCustomDateContainer();
// 如果不是自定义时间,立即应用筛选
if (activeFilters.date !== 'custom') {
activeFilters.startDate = '';
activeFilters.endDate = '';
activeFilters.currentPage = 1; // 重置为第一页
applyFilters();
}
});
}
// 应用自定义日期筛选
if (applyDateFilterBtn) {
addListener(applyDateFilterBtn, 'click', () => {
if (startDateInput) {
activeFilters.startDate = startDateInput.value;
}
if (endDateInput) {
activeFilters.endDate = endDateInput.value;
}
activeFilters.currentPage = 1; // 重置为第一页
applyFilters();
});
}
if (sortOption) {
addListener(sortOption, 'change', () => {
activeFilters.sort = sortOption.value;
applyFilters(); // 排序不重置页码
});
}
// 每页显示数量
if (pageSizeOption) {
pageSizeOption.value = activeFilters.pageSize.toString();
addListener(pageSizeOption, 'change', () => {
activeFilters.pageSize = parseInt(pageSizeOption.value);
activeFilters.currentPage = 1; // 重置为第一页
applyFilters();
});
}
// 分页按钮事件
if (prevPageButton) {
addListener(prevPageButton, 'click', () => {
if (activeFilters.currentPage > 1) {
activeFilters.currentPage--;
applyFilters();
}
});
}
if (nextPageButton) {
addListener(nextPageButton, 'click', () => {
const totalPages = parseInt(totalPagesElement?.textContent || '1');
if (activeFilters.currentPage < totalPages) {
activeFilters.currentPage++;
applyFilters();
}
});
}
// 初始应用筛选条件
applyFilters();
}
// 注册清理函数
function registerCleanup() {
// 一次性清理事件
const cleanupEvents = [
"astro:before-preparation",
"astro:before-swap",
"swup:willReplaceContent",
"beforeunload"
];
// 为每个事件类型注册一次性清理
cleanupEvents.forEach(eventType => {
const target = eventType === "beforeunload" ? window : document;
target.addEventListener(eventType, () => {
cleanup();
}, { once: true });
});
}
// 在页面加载时初始化
function init() {
// 设置功能
setupArticlesFilter();
// 注册清理函数
registerCleanup();
// 处理初始文章卡片中的链接
setTimeout(() => {
// 处理文章链接添加当前的URL参数
const currentParams = new URLSearchParams(window.location.search);
// 处理文章链接
const articleLinks = document.querySelectorAll('.article-card a[href]:not([href^="/articles?"])');
articleLinks.forEach(link => {
const href = link.getAttribute('href');
if (href && href !== '#' && !href.startsWith('#')) {
// 检查URL是否已包含查询参数
const hasParams = href.includes('?');
// 添加当前参数到文章链接
const newUrl = href + (hasParams ? '&' : '?') + currentParams.toString();
link.setAttribute('href', newUrl);
}
});
// 处理标签链接
const tagLinks = document.querySelectorAll('a[href^="/articles?tags="]');
if (tagLinks.length > 0) {
const paramsWithoutTags = new URLSearchParams(currentParams);
paramsWithoutTags.delete('tags'); // 删除当前的tags参数
tagLinks.forEach(link => {
const href = link.getAttribute('href');
const tagParam = new URLSearchParams(href.substring(href.indexOf('?')));
const tag = tagParam.get('tags');
if (tag) {
// 创建新的URL参数
const newParams = new URLSearchParams(paramsWithoutTags);
newParams.set('tags', tag);
// 更新链接
link.setAttribute('href', `/articles?${newParams.toString()}`);
}
});
}
}, 0);
}
// 页面转换时的处理函数
function onPageTransition() {
// 如果离开了文章页面,执行清理
if (hasLeftArticlesPage()) {
cleanup();
return;
}
// 如果仍在文章页面但脚本已销毁,重新初始化
if (isDestroyed) {
isDestroyed = false;
init();
return;
}
// 如果仍在文章页面且脚本未销毁,可能是路由切换,重新初始化
init();
}
// 页面转换时检查
document.addEventListener("astro:after-swap", onPageTransition);
document.addEventListener("astro:page-load", onPageTransition);
document.addEventListener("swup:contentReplaced", onPageTransition);
// 初始化
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init, {
once: true,
});
} else {
// 使用setTimeout确保处于事件队列末尾避免可能的事件冲突
setTimeout(init, 0);
}
// 为window添加卸载处理
window.addEventListener('unload', () => {
cleanup();
}, { once: true });
})();
</script>
) : (
// 默认视图(参数错误时显示)
<div class="text-center py-8">
<h2 class="text-2xl font-bold text-gray-700 mb-4">无效的视图模式</h2>
<p class="text-gray-500 mb-4">您请求的视图模式"{pageType}"不存在,请选择其他视图。</p>
<div class="flex justify-center gap-4">
<a href={`/articles/${path}${tagFilter ? `/tag/${tagFilter}` : ''}/grid`}
class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700">
网格视图
</a>
<a href={`/articles/${path}${tagFilter ? `/tag/${tagFilter}` : ''}/filtered`}
class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700">
筛选视图
</a>
2025-03-10 14:05:40 +08:00
</div>
</div>
)
}
</Layout>