移除不必要的组件和代码,完善dom的监听和清理,更换文章目录
This commit is contained in:
parent
864d134acd
commit
8ec3fe5df0
78
package-lock.json
generated
78
package-lock.json
generated
@ -3622,60 +3622,60 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@shikijs/core": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/@shikijs/core/-/core-3.2.1.tgz",
|
||||
"integrity": "sha512-FhsdxMWYu/C11sFisEp7FMGBtX/OSSbnXZDMBhGuUDBNTdsoZlMSgQv5f90rwvzWAdWIW6VobD+G3IrazxA6dQ==",
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/@shikijs/core/-/core-3.2.2.tgz",
|
||||
"integrity": "sha512-yvlSKVMLjddAGBa2Yu+vUZxuu3sClOWW1AG+UtJkvejYuGM5BVL35s6Ijiwb75O9QdEx6IkMxinHZSi8ZyrBaA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "3.2.1",
|
||||
"@shikijs/types": "3.2.2",
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
"@types/hast": "^3.0.4",
|
||||
"hast-util-to-html": "^9.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/engine-javascript": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/@shikijs/engine-javascript/-/engine-javascript-3.2.1.tgz",
|
||||
"integrity": "sha512-eMdcUzN3FMQYxOmRf2rmU8frikzoSHbQDFH2hIuXsrMO+IBOCI9BeeRkCiBkcLDHeRKbOCtYMJK3D6U32ooU9Q==",
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/@shikijs/engine-javascript/-/engine-javascript-3.2.2.tgz",
|
||||
"integrity": "sha512-tlDKfhWpF4jKLUyVAnmL+ggIC+0VyteNsUpBzh1iwWLZu4i+PelIRr0TNur6pRRo5UZIv3ss/PLMuwahg9S2hg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "3.2.1",
|
||||
"@shikijs/types": "3.2.2",
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
"oniguruma-to-es": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/engine-oniguruma": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/@shikijs/engine-oniguruma/-/engine-oniguruma-3.2.1.tgz",
|
||||
"integrity": "sha512-wZZAkayEn6qu2+YjenEoFqj0OyQI64EWsNR6/71d1EkG4sxEOFooowKivsWPpaWNBu3sxAG+zPz5kzBL/SsreQ==",
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/@shikijs/engine-oniguruma/-/engine-oniguruma-3.2.2.tgz",
|
||||
"integrity": "sha512-vyXRnWVCSvokwbaUD/8uPn6Gqsf5Hv7XwcW4AgiU4Z2qwy19sdr6VGzMdheKKN58tJOOe5MIKiNb901bgcUXYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "3.2.1",
|
||||
"@shikijs/types": "3.2.2",
|
||||
"@shikijs/vscode-textmate": "^10.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/langs": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/@shikijs/langs/-/langs-3.2.1.tgz",
|
||||
"integrity": "sha512-If0iDHYRSGbihiA8+7uRsgb1er1Yj11pwpX1c6HLYnizDsKAw5iaT3JXj5ZpaimXSWky/IhxTm7C6nkiYVym+A==",
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/@shikijs/langs/-/langs-3.2.2.tgz",
|
||||
"integrity": "sha512-NY0Urg2dV9ETt3JIOWoMPuoDNwte3geLZ4M1nrPHbkDS8dWMpKcEwlqiEIGqtwZNmt5gKyWpR26ln2Bg2ecPgw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "3.2.1"
|
||||
"@shikijs/types": "3.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/themes": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/@shikijs/themes/-/themes-3.2.1.tgz",
|
||||
"integrity": "sha512-k5DKJUT8IldBvAm8WcrDT5+7GA7se6lLksR+2E3SvyqGTyFMzU2F9Gb7rmD+t+Pga1MKrYFxDIeyWjMZWM6uBQ==",
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/@shikijs/themes/-/themes-3.2.2.tgz",
|
||||
"integrity": "sha512-Zuq4lgAxVKkb0FFdhHSdDkALuRpsj1so1JdihjKNQfgM78EHxV2JhO10qPsMrm01FkE3mDRTdF68wfmsqjt6HA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "3.2.1"
|
||||
"@shikijs/types": "3.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/types": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/@shikijs/types/-/types-3.2.1.tgz",
|
||||
"integrity": "sha512-/NTWAk4KE2M8uac0RhOsIhYQf4pdU0OywQuYDGIGAJ6Mjunxl2cGiuLkvu4HLCMn+OTTLRWkjZITp+aYJv60yA==",
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/@shikijs/types/-/types-3.2.2.tgz",
|
||||
"integrity": "sha512-a5TiHk7EH5Lso8sHcLHbVNNhWKP0Wi3yVnXnu73g86n3WoDgEra7n3KszyeCGuyoagspQ2fzvy4cpSc8pKhb0A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
@ -11021,19 +11021,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/oniguruma-parser": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmmirror.com/oniguruma-parser/-/oniguruma-parser-0.5.4.tgz",
|
||||
"integrity": "sha512-yNxcQ8sKvURiTwP0mV6bLQCYE7NKfKRRWunhbZnXgxSmB1OXa1lHrN3o4DZd+0Si0kU5blidK7BcROO8qv5TZA==",
|
||||
"version": "0.11.2",
|
||||
"resolved": "https://registry.npmmirror.com/oniguruma-parser/-/oniguruma-parser-0.11.2.tgz",
|
||||
"integrity": "sha512-F7Ld4oDZJCI5/wCZ8AOffQbqjSzIRpKH7I/iuSs1SkhZeCj0wS6PMZ4W6VA16TWHrAo0Y9bBKEJOe7tvwcTXnw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/oniguruma-to-es": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/oniguruma-to-es/-/oniguruma-to-es-4.1.0.tgz",
|
||||
"integrity": "sha512-SNwG909cSLo4vPyyPbU/VJkEc9WOXqu2ycBlfd1UCXLqk1IijcQktSBb2yRQ2UFPsDhpkaf+C1dtT3PkLK/yWA==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/oniguruma-to-es/-/oniguruma-to-es-4.2.0.tgz",
|
||||
"integrity": "sha512-MDPs6KSOLS0tKQ7joqg44dRIRZUyotfTy0r+7oEEs6VwWWP0+E2PPDYWMFN0aqOjRyWHBYq7RfKw9GQk2S2z5g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex-xs": "^1.0.0",
|
||||
"oniguruma-parser": "^0.5.4",
|
||||
"oniguruma-parser": "^0.11.0",
|
||||
"regex": "^6.0.1",
|
||||
"regex-recursion": "^6.0.2"
|
||||
}
|
||||
@ -13406,17 +13406,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/shiki": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/shiki/-/shiki-3.2.1.tgz",
|
||||
"integrity": "sha512-VML/2o1/KGYkEf/stJJ+s9Ypn7jUKQPomGLGYso4JJFMFxVDyPNsjsI3MB3KLjlMOeH44gyaPdXC6rik2WXvUQ==",
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/shiki/-/shiki-3.2.2.tgz",
|
||||
"integrity": "sha512-0qWBkM2t/0NXPRcVgtLhtHv6Ak3Q5yI4K/ggMqcgLRKm4+pCs3namgZlhlat/7u2CuqNtlShNs9lENOG6n7UaQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/core": "3.2.1",
|
||||
"@shikijs/engine-javascript": "3.2.1",
|
||||
"@shikijs/engine-oniguruma": "3.2.1",
|
||||
"@shikijs/langs": "3.2.1",
|
||||
"@shikijs/themes": "3.2.1",
|
||||
"@shikijs/types": "3.2.1",
|
||||
"@shikijs/core": "3.2.2",
|
||||
"@shikijs/engine-javascript": "3.2.2",
|
||||
"@shikijs/engine-oniguruma": "3.2.2",
|
||||
"@shikijs/langs": "3.2.2",
|
||||
"@shikijs/themes": "3.2.2",
|
||||
"@shikijs/types": "3.2.2",
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
"@types/hast": "^3.0.4"
|
||||
}
|
||||
|
399
src/components/Breadcrumb.astro
Normal file
399
src/components/Breadcrumb.astro
Normal file
@ -0,0 +1,399 @@
|
||||
---
|
||||
interface Breadcrumb {
|
||||
name: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
pageType: 'filter' | 'grid' | 'article';
|
||||
pathSegments?: string[]; // 路径段数组
|
||||
searchParams?: URLSearchParams; // 搜索参数
|
||||
articleTitle?: string; // 文章标题(仅在文章详情页使用)
|
||||
path?: string; // 当前路径
|
||||
}
|
||||
|
||||
const {
|
||||
pageType,
|
||||
pathSegments = [],
|
||||
searchParams = new URLSearchParams(),
|
||||
articleTitle = '',
|
||||
path = ''
|
||||
} = Astro.props;
|
||||
|
||||
// 计算面包屑
|
||||
const breadcrumbs: Breadcrumb[] = pathSegments
|
||||
.filter(segment => segment.trim() !== '')
|
||||
.map((segment, index, array) => {
|
||||
const path = array.slice(0, index + 1).join('/');
|
||||
return { name: segment, path };
|
||||
});
|
||||
---
|
||||
|
||||
<div class="flex items-center justify-between w-full flex-wrap sm:flex-nowrap">
|
||||
<div class="flex items-center text-sm overflow-hidden">
|
||||
<!-- 文章列表链接 - 根据当前页面类型决定链接 -->
|
||||
<a href={'/articles/'} class="text-secondary-600 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400 flex items-center flex-shrink-0">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
文章
|
||||
</a>
|
||||
|
||||
<!-- 网格视图或文章详情中的目录路径 -->
|
||||
{(pageType === 'grid' || (pageType === 'article' && breadcrumbs.length > 0)) && (
|
||||
<div class="flex items-center overflow-hidden">
|
||||
<span class="mx-2 text-secondary-300 dark:text-secondary-600 flex-shrink-0">/</span>
|
||||
|
||||
<!-- 移动端使用智能截断 -->
|
||||
<div class="flex md:hidden items-center">
|
||||
{breadcrumbs.length > 2 ? (
|
||||
<>
|
||||
<!-- 第一个路径段 -->
|
||||
<a
|
||||
href={`/articles/${breadcrumbs[0].path}/`}
|
||||
class="text-secondary-600 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400 truncate max-w-[80px] sm:max-w-[100px] flex-shrink-0"
|
||||
>
|
||||
{breadcrumbs[0].name}
|
||||
</a>
|
||||
|
||||
<!-- 省略号 -->
|
||||
<span class="mx-2 text-secondary-300 dark:text-secondary-600 flex-shrink-0">...</span>
|
||||
|
||||
<!-- 最后一个路径段 -->
|
||||
{breadcrumbs.length > 1 && (
|
||||
<a
|
||||
href={`/articles/${breadcrumbs[breadcrumbs.length - 1].path}/`}
|
||||
class="text-secondary-600 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400 truncate max-w-[80px] sm:max-w-[120px] flex-shrink-0"
|
||||
>
|
||||
{breadcrumbs[breadcrumbs.length - 1].name}
|
||||
</a>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
breadcrumbs.map((crumb: Breadcrumb, index: number) => {
|
||||
const crumbPath = breadcrumbs.slice(0, index + 1).map((b: Breadcrumb) => b.name).join('/');
|
||||
return (
|
||||
<span class="flex items-center flex-shrink-0">
|
||||
{index > 0 && <span class="mx-2 text-secondary-300 dark:text-secondary-600">/</span>}
|
||||
<a
|
||||
href={`/articles/${crumbPath}/`}
|
||||
class="text-secondary-600 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400 truncate max-w-[100px] sm:max-w-[150px]"
|
||||
>
|
||||
{crumb.name}
|
||||
</a>
|
||||
</span>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
<!-- 桌面端显示全部路径段 -->
|
||||
<div class="hidden md:flex items-center flex-wrap">
|
||||
{breadcrumbs.map((crumb: Breadcrumb, index: number) => {
|
||||
const crumbPath = breadcrumbs.slice(0, index + 1).map((b: Breadcrumb) => b.name).join('/');
|
||||
return (
|
||||
<span class="flex items-center flex-shrink-0">
|
||||
{index > 0 && <span class="mx-2 text-secondary-300 dark:text-secondary-600">/</span>}
|
||||
<a
|
||||
href={`/articles/${crumbPath}/`}
|
||||
class="text-secondary-600 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400 truncate max-w-[200px] lg:max-w-[250px] xl:max-w-[300px]"
|
||||
>
|
||||
{crumb.name}
|
||||
</a>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<!-- 筛选视图中的搜索参数展示 -->
|
||||
{pageType === 'filter' && searchParams.toString() && (
|
||||
<div class="flex items-center overflow-hidden">
|
||||
<span class="mx-2 text-secondary-300 dark:text-secondary-600 flex-shrink-0">/</span>
|
||||
<span class="text-secondary-600 dark:text-secondary-400 truncate max-w-[120px] sm:max-w-[180px] md:max-w-[250px]">
|
||||
搜索结果
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<!-- 文章标题 - 仅在文章详情页显示 -->
|
||||
{pageType === 'article' && articleTitle && (
|
||||
<>
|
||||
<span class="mx-2 text-secondary-300 dark:text-secondary-600 flex-shrink-0">/</span>
|
||||
<span class="text-secondary-600 dark:text-secondary-400 truncate max-w-[120px] sm:max-w-[180px] md:max-w-[250px]">{articleTitle}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<!-- 视图切换按钮 - 仅在文章列表页面显示 -->
|
||||
{(pageType === 'filter' || pageType === 'grid') && (
|
||||
<div class="flex items-center gap-px flex-shrink-0 ml-auto">
|
||||
<a href={`/articles${searchParams.toString() ? `?${searchParams.toString()}` : ''}`}
|
||||
class={`px-3 py-1.5 flex items-center gap-1 ${
|
||||
pageType === 'filter'
|
||||
? 'text-primary-600 dark:text-primary-400 font-medium'
|
||||
: 'text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400'
|
||||
}`}
|
||||
data-astro-prefetch="hover">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" 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>
|
||||
<span class="hidden sm:inline text-xs">筛选</span>
|
||||
</a>
|
||||
<a href={path ? `/articles/${path}/` : `/articles/`}
|
||||
class={`px-3 py-1.5 flex items-center gap-1 ${
|
||||
pageType === 'grid'
|
||||
? 'text-primary-600 dark:text-primary-400 font-medium'
|
||||
: 'text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400'
|
||||
}`}
|
||||
data-astro-prefetch="hover">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
|
||||
</svg>
|
||||
<span class="hidden sm:inline text-xs">网格</span>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<!-- 文章详情页的返回按钮 -->
|
||||
{pageType === 'article' && (
|
||||
<div class="flex items-center shrink-0 ml-auto">
|
||||
<a
|
||||
href={`/articles/${path}/`}
|
||||
class="text-secondary-500 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400 flex items-center text-sm back-button"
|
||||
data-astro-prefetch="hover"
|
||||
data-path={`/articles/${path}/`}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 mr-1"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 19l-7-7m0 0l7-7m-7 7h18"
|
||||
></path>
|
||||
</svg>
|
||||
返回文章列表
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<script is:inline>
|
||||
// 返回按钮点击事件处理
|
||||
(function() {
|
||||
// 页面导航计数器
|
||||
let pageNavigationCount = 0;
|
||||
|
||||
// 存储事件监听器,便于统一清理
|
||||
const listeners = [];
|
||||
|
||||
// 清理按钮事件监听器
|
||||
function cleanupButtonListeners() {
|
||||
// 查找所有返回按钮
|
||||
const buttons = document.querySelectorAll('.back-button');
|
||||
|
||||
buttons.forEach(button => {
|
||||
// 移除所有可能的事件
|
||||
if (button._clickHandler) {
|
||||
button.removeEventListener('click', button._clickHandler);
|
||||
delete button._clickHandler;
|
||||
}
|
||||
|
||||
// 清除其他可能的事件
|
||||
const otherClickHandlers = button.__backButtonClickHandlers || [];
|
||||
otherClickHandlers.forEach(handler => {
|
||||
try {
|
||||
button.removeEventListener('click', handler);
|
||||
} catch (e) {
|
||||
// 忽略错误
|
||||
}
|
||||
});
|
||||
|
||||
// 重置处理函数数组
|
||||
button.__backButtonClickHandlers = [];
|
||||
});
|
||||
}
|
||||
|
||||
// 添加事件监听器并记录,方便后续统一清理
|
||||
function addListener(element, eventType, handler, options) {
|
||||
if (!element) return null;
|
||||
|
||||
// 确保先移除可能已存在的同类型事件处理函数
|
||||
if (eventType === 'click' && element.classList.contains('back-button')) {
|
||||
if (element._clickHandler) {
|
||||
element.removeEventListener('click', element._clickHandler);
|
||||
}
|
||||
element._clickHandler = handler;
|
||||
|
||||
// 保存到数组中以便清理
|
||||
if (!element.__backButtonClickHandlers) {
|
||||
element.__backButtonClickHandlers = [];
|
||||
}
|
||||
element.__backButtonClickHandlers.push(handler);
|
||||
}
|
||||
|
||||
element.addEventListener(eventType, handler, options);
|
||||
listeners.push({ element, eventType, handler, options });
|
||||
return handler;
|
||||
}
|
||||
|
||||
// 清理函数 - 移除所有事件监听器
|
||||
function cleanup() {
|
||||
// 先直接从按钮清理事件
|
||||
cleanupButtonListeners();
|
||||
|
||||
// 移除所有监听器
|
||||
listeners.forEach(({ element, eventType, handler, options }) => {
|
||||
try {
|
||||
element.removeEventListener(eventType, handler, options);
|
||||
} catch (err) {
|
||||
// 忽略错误
|
||||
}
|
||||
});
|
||||
|
||||
// 清空数组
|
||||
listeners.length = 0;
|
||||
}
|
||||
|
||||
// 设置返回按钮事件
|
||||
function setupBackButton() {
|
||||
// 确保当前没有活动的返回按钮事件
|
||||
cleanup();
|
||||
|
||||
const backButton = document.querySelector('.back-button');
|
||||
|
||||
if (!backButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
backButton.style.pointerEvents = 'auto';
|
||||
} catch (e) {
|
||||
// 忽略样式错误
|
||||
}
|
||||
|
||||
const clickHandler = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const url = new URL(window.location.href);
|
||||
const searchParams = url.search;
|
||||
|
||||
// 检查URL中是否有查询参数
|
||||
if (searchParams) {
|
||||
// 有查询参数,返回筛选页面
|
||||
window.location.href = `/articles${searchParams}`;
|
||||
} else {
|
||||
// 没有查询参数,返回默认路径
|
||||
const defaultPath = backButton.getAttribute('data-path') || '';
|
||||
window.location.href = defaultPath;
|
||||
}
|
||||
};
|
||||
|
||||
// 添加点击事件监听
|
||||
addListener(backButton, 'click', clickHandler);
|
||||
}
|
||||
|
||||
// 注册清理函数 - 确保在每次页面转换前清理事件
|
||||
function registerCleanup() {
|
||||
const cleanupEvents = [
|
||||
'astro:before-preparation',
|
||||
'astro:before-swap',
|
||||
'astro:beforeload',
|
||||
'swup:willReplaceContent'
|
||||
];
|
||||
|
||||
// 为每个事件注册一次性清理函数
|
||||
cleanupEvents.forEach(eventName => {
|
||||
const handler = () => {
|
||||
cleanup();
|
||||
};
|
||||
|
||||
document.addEventListener(eventName, handler, { once: true });
|
||||
});
|
||||
|
||||
// 页面卸载时清理
|
||||
window.addEventListener('beforeunload', () => {
|
||||
cleanup();
|
||||
}, { once: true });
|
||||
}
|
||||
|
||||
// 初始化函数
|
||||
function init() {
|
||||
pageNavigationCount++;
|
||||
setupBackButton();
|
||||
registerCleanup();
|
||||
}
|
||||
|
||||
// 监听页面转换事件
|
||||
function setupPageTransitionEvents() {
|
||||
// 确保事件处理程序唯一性的函数
|
||||
function setupUniqueEvent(eventName, callback) {
|
||||
const eventKey = `__back_button_event_${eventName.replace(/:/g, '_')}`;
|
||||
|
||||
// 移除可能存在的旧处理函数
|
||||
if (window[eventKey]) {
|
||||
document.removeEventListener(eventName, window[eventKey]);
|
||||
}
|
||||
|
||||
// 保存新处理函数并注册
|
||||
window[eventKey] = callback;
|
||||
document.addEventListener(eventName, window[eventKey]);
|
||||
}
|
||||
|
||||
// 页面转换后事件
|
||||
const pageTransitionEvents = [
|
||||
{ name: 'astro:after-swap', delay: 10 },
|
||||
{ name: 'astro:page-load', delay: 10 },
|
||||
{ name: 'swup:contentReplaced', delay: 10 }
|
||||
];
|
||||
|
||||
// 设置每个页面转换事件
|
||||
pageTransitionEvents.forEach(({ name, delay }) => {
|
||||
setupUniqueEvent(name, () => {
|
||||
cleanupButtonListeners(); // 立即清理按钮上的事件
|
||||
|
||||
// 延迟初始化,确保DOM完全更新
|
||||
setTimeout(() => {
|
||||
cleanupButtonListeners(); // 再次清理,确保没有遗漏
|
||||
init();
|
||||
}, delay);
|
||||
});
|
||||
});
|
||||
|
||||
// 特别处理 swup:pageView 事件
|
||||
setupUniqueEvent('swup:pageView', () => {
|
||||
// 对于偶数次页面跳转,特别确保事件被正确重新绑定
|
||||
if (pageNavigationCount % 2 === 0) {
|
||||
setTimeout(() => {
|
||||
const buttons = document.querySelectorAll('.back-button');
|
||||
if (buttons.length > 0) {
|
||||
cleanupButtonListeners();
|
||||
setupBackButton();
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 设置页面转换事件监听
|
||||
setupPageTransitionEvents();
|
||||
|
||||
// 在页面加载后初始化
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
init();
|
||||
}, { once: true });
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
init();
|
||||
}, 0);
|
||||
}
|
||||
})();
|
||||
</script>
|
@ -1,70 +0,0 @@
|
||||
interface Breadcrumb {
|
||||
name: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface BreadcrumbProps {
|
||||
pageType: 'articles' | 'article' | 'timeline'; // 页面类型
|
||||
pathSegments?: string[]; // 路径段数组
|
||||
tagFilter?: string; // 标签过滤器
|
||||
articleTitle?: string; // 文章标题(仅在文章详情页使用)
|
||||
}
|
||||
|
||||
export function Breadcrumb({
|
||||
pageType,
|
||||
pathSegments = [],
|
||||
tagFilter = '',
|
||||
articleTitle = ''
|
||||
}: BreadcrumbProps) {
|
||||
// 将路径段转换为面包屑对象
|
||||
const breadcrumbs: Breadcrumb[] = pathSegments
|
||||
.filter(segment => segment.trim() !== '')
|
||||
.map((segment, index, array) => {
|
||||
const path = array.slice(0, index + 1).join('/');
|
||||
return { name: segment, path };
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex items-center text-sm">
|
||||
{/* 文章列表链接 */}
|
||||
<a href="/articles" className="text-secondary-600 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400 flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clipRule="evenodd" />
|
||||
</svg>
|
||||
文章
|
||||
</a>
|
||||
|
||||
{/* 标签过滤 */}
|
||||
{tagFilter && (
|
||||
<>
|
||||
<span className="mx-2 text-secondary-300 dark:text-secondary-600">/</span>
|
||||
<span className="text-secondary-600 dark:text-secondary-400 flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M17.707 9.293a1 1 0 010 1.414l-7 7a1 1 0 01-1.414 0l-7-7A.997.997 0 012 10V5a3 3 0 013-3h5c.256 0 .512.098.707.293l7 7zM5 6a1 1 0 100-2 1 1 0 000 2z" clipRule="evenodd" />
|
||||
</svg>
|
||||
{tagFilter}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 目录路径 */}
|
||||
{!tagFilter && breadcrumbs.map((crumb: Breadcrumb, index: number) => {
|
||||
const crumbPath = breadcrumbs.slice(0, index + 1).map((b: Breadcrumb) => b.name).join('/');
|
||||
return (
|
||||
<span key={`crumb-${index}`}>
|
||||
<span className="mx-2 text-secondary-300 dark:text-secondary-600">/</span>
|
||||
<a href={`/articles?path=${encodeURIComponent(crumbPath)}`} className="text-secondary-600 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400">{crumb.name}</a>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 文章标题 */}
|
||||
{pageType === 'article' && articleTitle && (
|
||||
<>
|
||||
<span className="mx-2 text-secondary-300 dark:text-secondary-600">/</span>
|
||||
<span className="text-secondary-600 dark:text-secondary-400 truncate max-w-[150px] sm:max-w-[300px]">{articleTitle}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -291,7 +291,7 @@ const normalizedPath =
|
||||
</header>
|
||||
|
||||
<script>
|
||||
// Header组件逻辑 - 使用"进入→绑定→退出完全清理"模式
|
||||
// Header组件逻辑
|
||||
(function () {
|
||||
// 存储所有事件监听器,便于统一清理
|
||||
const listeners: Array<{
|
||||
@ -330,7 +330,6 @@ const normalizedPath =
|
||||
|
||||
// 清理函数 - 移除所有事件监听器
|
||||
function cleanup(): void {
|
||||
// 移除所有监听器
|
||||
listeners.forEach(({ element, eventType, handler }) => {
|
||||
try {
|
||||
element.removeEventListener(eventType, handler);
|
||||
@ -338,16 +337,11 @@ const normalizedPath =
|
||||
console.error(`移除Header事件监听器出错:`, err);
|
||||
}
|
||||
});
|
||||
|
||||
// 清空数组
|
||||
listeners.length = 0;
|
||||
}
|
||||
|
||||
// Header和导航高亮逻辑
|
||||
function initHeader(): void {
|
||||
const header = document.getElementById("header-bg");
|
||||
const scrollThreshold = 50;
|
||||
|
||||
// 获取桌面端导航链接(排除移动端菜单中的链接)
|
||||
const navLinks = document.querySelectorAll(".hidden.md\\:flex a[href]");
|
||||
|
||||
@ -366,8 +360,6 @@ const normalizedPath =
|
||||
return true;
|
||||
}
|
||||
|
||||
// 可以添加其他特殊情况的处理逻辑
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -427,22 +419,9 @@ const normalizedPath =
|
||||
});
|
||||
}
|
||||
|
||||
// 处理滚动更新背景
|
||||
function updateHeaderBackground(): void {
|
||||
if (window.scrollY > scrollThreshold) {
|
||||
header?.classList.add("scrolled");
|
||||
} else {
|
||||
header?.classList.remove("scrolled");
|
||||
}
|
||||
}
|
||||
|
||||
// 初始检查
|
||||
updateHeaderBackground();
|
||||
updateNavHighlight();
|
||||
|
||||
// 添加滚动事件监听
|
||||
addListener(window, "scroll", updateHeaderBackground);
|
||||
|
||||
// 监听路由变化
|
||||
addListener(document, "astro:page-load", updateNavHighlight);
|
||||
addListener(document, "astro:after-swap", updateNavHighlight);
|
||||
@ -479,7 +458,7 @@ const normalizedPath =
|
||||
}
|
||||
|
||||
if (mobileMenuButton && mobileMenu && menuOpenIcon && menuCloseIcon) {
|
||||
// 移动端菜单按钮点击事件 - 使用捕获模式确保事件优先处理
|
||||
// 移动端菜单按钮点击事件
|
||||
(mobileMenuButton as HTMLElement).style.pointerEvents = "auto";
|
||||
|
||||
addListener(
|
||||
@ -492,24 +471,18 @@ const normalizedPath =
|
||||
const expanded =
|
||||
mobileMenuButton.getAttribute("aria-expanded") === "true";
|
||||
|
||||
// 切换菜单状态
|
||||
mobileMenuButton.setAttribute(
|
||||
"aria-expanded",
|
||||
(!expanded).toString(),
|
||||
);
|
||||
|
||||
if (expanded) {
|
||||
// 直接隐藏菜单,不使用过渡效果
|
||||
mobileMenu.classList.add("hidden");
|
||||
} else {
|
||||
// 打开菜单前先关闭搜索面板
|
||||
closeMobileSearch();
|
||||
|
||||
// 直接显示菜单,不使用过渡效果
|
||||
mobileMenu.classList.remove("hidden");
|
||||
}
|
||||
|
||||
// 切换图标
|
||||
menuOpenIcon.classList.toggle("hidden");
|
||||
menuCloseIcon.classList.toggle("hidden");
|
||||
},
|
||||
@ -526,7 +499,6 @@ const normalizedPath =
|
||||
link,
|
||||
"click",
|
||||
(e) => {
|
||||
// 不要阻止默认行为,因为需要跳转
|
||||
e.stopPropagation();
|
||||
closeMobileMenu();
|
||||
},
|
||||
@ -537,7 +509,6 @@ const normalizedPath =
|
||||
|
||||
// 移动端搜索按钮
|
||||
if (mobileSearchButton && mobileSearchPanel) {
|
||||
// 搜索按钮点击事件 - 使用捕获模式确保事件优先处理
|
||||
(mobileSearchButton as HTMLElement).style.pointerEvents = "auto";
|
||||
|
||||
addListener(
|
||||
@ -547,18 +518,13 @@ const normalizedPath =
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// 检查搜索面板是否已经打开
|
||||
const isSearchVisible =
|
||||
!mobileSearchPanel.classList.contains("hidden");
|
||||
|
||||
if (isSearchVisible) {
|
||||
// 如果搜索面板已打开,则关闭它
|
||||
closeMobileSearch();
|
||||
} else {
|
||||
// 打开搜索面板前先关闭菜单
|
||||
closeMobileMenu();
|
||||
|
||||
// 打开搜索面板
|
||||
mobileSearchPanel.classList.remove("hidden");
|
||||
if (mobileSearch) mobileSearch.focus();
|
||||
}
|
||||
@ -593,13 +559,11 @@ const normalizedPath =
|
||||
"click",
|
||||
(e: Event) => {
|
||||
const target = e.target as HTMLElement;
|
||||
// 如果点击的不是主题切换按钮本身,则手动触发主题切换
|
||||
if (
|
||||
target.id !== "theme-toggle-button" &&
|
||||
!target.closest("#theme-toggle-button")
|
||||
) {
|
||||
e.stopPropagation();
|
||||
// 获取容器内的主题切换按钮并模拟点击
|
||||
const toggleButton = themeToggleContainer.querySelector(
|
||||
"#theme-toggle-button",
|
||||
);
|
||||
@ -637,7 +601,6 @@ const normalizedPath =
|
||||
const mobileResults = document.getElementById("mobile-search-results");
|
||||
const mobileList = document.getElementById("mobile-search-list");
|
||||
const mobileMessage = document.getElementById("mobile-search-message");
|
||||
const mobileSearchPanel = document.getElementById("mobile-search-panel");
|
||||
|
||||
// 获取文章数据
|
||||
async function fetchArticles(): Promise<void> {
|
||||
@ -659,7 +622,6 @@ const normalizedPath =
|
||||
function highlightText(text: string, query: string): string {
|
||||
if (!text || !query.trim()) return text;
|
||||
|
||||
// 转义正则表达式中的特殊字符
|
||||
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const regex = new RegExp(`(${escapedQuery})`, "gi");
|
||||
|
||||
@ -761,15 +723,12 @@ const normalizedPath =
|
||||
const endPos = Math.min(article.content.length, matchIndex + 100);
|
||||
// 提取片段
|
||||
let snippet = article.content.substring(startPos, endPos);
|
||||
// 如果不是从文章开头开始,添加省略号
|
||||
if (startPos > 0) {
|
||||
snippet = "..." + snippet;
|
||||
}
|
||||
// 如果不是到文章结尾,添加省略号
|
||||
if (endPos < article.content.length) {
|
||||
snippet = snippet + "...";
|
||||
}
|
||||
// 高亮匹配的文本
|
||||
snippet = highlightText(snippet, query);
|
||||
contentMatch = `<p class="text-xs text-gray-500 dark:text-gray-400 mt-1 line-clamp-2">${snippet}</p>`;
|
||||
}
|
||||
@ -900,13 +859,11 @@ const normalizedPath =
|
||||
|
||||
// 关闭所有搜索面板的函数
|
||||
function closeSearchPanels(): void {
|
||||
// 关闭桌面端搜索结果
|
||||
const desktopSearchResults = document.getElementById("desktop-search-results");
|
||||
if (desktopSearchResults) {
|
||||
desktopSearchResults.classList.add("hidden");
|
||||
}
|
||||
|
||||
// 关闭移动端搜索面板
|
||||
const mobileSearchPanel = document.getElementById("mobile-search-panel");
|
||||
if (mobileSearchPanel) {
|
||||
mobileSearchPanel.classList.add("hidden");
|
||||
@ -915,13 +872,10 @@ const normalizedPath =
|
||||
|
||||
// 为搜索结果链接添加点击事件
|
||||
function addSearchResultsClickListeners(): void {
|
||||
// 获取所有搜索结果链接
|
||||
const searchResultLinks = document.querySelectorAll('.search-result-link');
|
||||
|
||||
// 为每个链接添加点击事件
|
||||
searchResultLinks.forEach(link => {
|
||||
addListener(link, 'click', () => {
|
||||
// 点击搜索结果后关闭所有搜索面板
|
||||
closeSearchPanels();
|
||||
});
|
||||
});
|
||||
@ -929,27 +883,19 @@ const normalizedPath =
|
||||
|
||||
// 注册清理函数
|
||||
function registerCleanup(): void {
|
||||
// Astro 事件
|
||||
document.addEventListener("astro:before-preparation", cleanup, {
|
||||
once: true,
|
||||
});
|
||||
document.addEventListener("astro:before-swap", cleanup, { once: true });
|
||||
|
||||
// Swup 事件
|
||||
document.addEventListener("swup:willReplaceContent", cleanup, {
|
||||
once: true,
|
||||
});
|
||||
|
||||
// 页面卸载
|
||||
window.addEventListener("beforeunload", cleanup, { once: true });
|
||||
}
|
||||
|
||||
// 初始化全部功能
|
||||
function setupHeader(): void {
|
||||
// 先清理之前的事件监听器
|
||||
cleanup();
|
||||
|
||||
// 初始化各个组件
|
||||
initHeader();
|
||||
initSearch();
|
||||
registerCleanup();
|
||||
@ -957,7 +903,6 @@ const normalizedPath =
|
||||
// 在页面路由变化时关闭搜索面板
|
||||
addListener(document, 'astro:page-load', closeSearchPanels);
|
||||
addListener(document, 'astro:after-swap', closeSearchPanels);
|
||||
addListener(document, 'swup:contentReplaced', closeSearchPanels);
|
||||
}
|
||||
|
||||
// 在页面加载时初始化
|
||||
@ -966,15 +911,11 @@ const normalizedPath =
|
||||
once: true,
|
||||
});
|
||||
} else {
|
||||
// 使用setTimeout确保处于事件队列末尾,避免可能的事件冲突
|
||||
setTimeout(setupHeader, 0);
|
||||
}
|
||||
|
||||
// 在页面转换后重新初始化
|
||||
document.addEventListener("astro:after-swap", setupHeader);
|
||||
document.addEventListener("astro:page-load", setupHeader);
|
||||
|
||||
// Swup页面内容替换后重新初始化
|
||||
document.addEventListener("swup:contentReplaced", setupHeader);
|
||||
})();
|
||||
</script>
|
||||
|
@ -9,16 +9,14 @@ interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
date?: Date;
|
||||
author?: string;
|
||||
tags?: string[];
|
||||
image?: string;
|
||||
}
|
||||
|
||||
// 获取完整的 URL
|
||||
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
|
||||
|
||||
// 从props中获取页面特定信息
|
||||
const { title = SITE_NAME, description = SITE_DESCRIPTION, date, author, tags, image } = Astro.props;
|
||||
const { title = SITE_NAME, description = SITE_DESCRIPTION, date, tags } = Astro.props;
|
||||
---
|
||||
<!doctype html>
|
||||
<html lang="zh-CN" class="m-0 w-full h-full">
|
||||
@ -39,18 +37,15 @@ const { title = SITE_NAME, description = SITE_DESCRIPTION, date, author, tags, i
|
||||
<meta property="og:url" content={canonicalURL} />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description || `${SITE_NAME} - 个人博客`} />
|
||||
{image && <meta property="og:image" content={new URL(image, Astro.site)} />}
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:url" content={canonicalURL} />
|
||||
<meta property="twitter:title" content={title} />
|
||||
<meta property="twitter:description" content={description || `${SITE_NAME} - 个人博客`} />
|
||||
{image && <meta property="twitter:image" content={new URL(image, Astro.site)} />}
|
||||
|
||||
<!-- 文章特定元数据 -->
|
||||
{date && <meta property="article:published_time" content={date.toISOString()} />}
|
||||
{author && <meta name="author" content={author} />}
|
||||
{tags && tags.map(tag => (
|
||||
<meta property="article:tag" content={tag} />
|
||||
))}
|
||||
|
@ -135,11 +135,7 @@ const articles = defineCollection({
|
||||
date: z.date(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
summary: z.string().optional(),
|
||||
image: z.string().optional(),
|
||||
author: z.string().optional(),
|
||||
draft: z.boolean().optional().default(false),
|
||||
section: z.string().optional(),
|
||||
weight: z.number().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
title: "常用软件"
|
||||
date: 2023-04-28T20:56:00Z
|
||||
tags: []
|
||||
summary : "常用的应用合集"
|
||||
---
|
||||
|
||||
### Windows 应用
|
||||
|
@ -2,69 +2,62 @@ import type { APIRoute } from 'astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { getSpecialPath } from '../../content.config';
|
||||
|
||||
// 从文章内容中提取摘要的函数
|
||||
function extractSummary(content: string, length = 150) {
|
||||
// 移除 Markdown 标记
|
||||
const plainText = content
|
||||
.replace(/---[\s\S]*?---/, '') // 移除 frontmatter
|
||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // 将链接转换为纯文本
|
||||
.replace(/[#*`~>]/g, '') // 移除特殊字符
|
||||
.replace(/\n+/g, ' ') // 将换行转换为空格
|
||||
.trim();
|
||||
|
||||
return plainText.length > length
|
||||
? plainText.slice(0, length).trim() + '...'
|
||||
: plainText;
|
||||
}
|
||||
|
||||
// 处理特殊ID的函数
|
||||
function getArticleUrl(articleId: string) {
|
||||
return `/articles/${getSpecialPath(articleId)}`;
|
||||
}
|
||||
|
||||
export const GET: APIRoute = async ({ request }) => {
|
||||
// 获取查询参数
|
||||
const url = new URL(request.url);
|
||||
const page = parseInt(url.searchParams.get('page') || '1');
|
||||
const limit = parseInt(url.searchParams.get('limit') || '10');
|
||||
const tag = url.searchParams.get('tag') || '';
|
||||
const path = url.searchParams.get('path') || '';
|
||||
try {
|
||||
// 获取所有文章
|
||||
const articles = await getCollection('articles');
|
||||
// 格式化文章数据
|
||||
const formattedArticles = articles.map(article => ({
|
||||
id: article.id,
|
||||
title: article.data.title,
|
||||
date: article.data.date,
|
||||
tags: article.data.tags || [],
|
||||
summary: article.data.summary || (article.body ? extractSummary(article.body) : ''),
|
||||
url: getArticleUrl(article.id) // 使用特殊ID处理函数
|
||||
}));
|
||||
|
||||
// 获取所有文章
|
||||
const articles = await getCollection('articles');
|
||||
|
||||
// 根据条件过滤文章
|
||||
let filteredArticles = articles;
|
||||
|
||||
// 如果有标签过滤
|
||||
if (tag) {
|
||||
filteredArticles = filteredArticles.filter(article =>
|
||||
article.data.tags && article.data.tags.includes(tag)
|
||||
);
|
||||
}
|
||||
|
||||
// 如果有路径过滤,直接使用文章ID来判断
|
||||
if (path) {
|
||||
const normalizedPath = path.toLowerCase();
|
||||
filteredArticles = filteredArticles.filter(article => {
|
||||
return article.id.toLowerCase().includes(normalizedPath);
|
||||
return new Response(JSON.stringify({
|
||||
articles: formattedArticles,
|
||||
total: formattedArticles.length,
|
||||
success: true
|
||||
}), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
// 添加缓存头,缓存1小时
|
||||
'Cache-Control': 'public, max-age=3600'
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({
|
||||
error: '获取文章数据失败',
|
||||
success: false,
|
||||
articles: [],
|
||||
total: 0
|
||||
}), {
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 按日期排序(最新的在前面)
|
||||
const sortedArticles = filteredArticles.sort(
|
||||
(a, b) => b.data.date.getTime() - a.data.date.getTime()
|
||||
);
|
||||
|
||||
// 计算分页
|
||||
const startIndex = (page - 1) * limit;
|
||||
const endIndex = startIndex + limit;
|
||||
const paginatedArticles = sortedArticles.slice(startIndex, endIndex);
|
||||
|
||||
// 格式化文章数据
|
||||
const formattedArticles = paginatedArticles.map(article => ({
|
||||
id: article.id,
|
||||
title: article.data.title,
|
||||
date: article.data.date,
|
||||
tags: article.data.tags || [],
|
||||
summary: article.data.summary || '',
|
||||
url: getArticleUrl(article.id) // 使用特殊ID处理函数
|
||||
}));
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
articles: formattedArticles,
|
||||
total: sortedArticles.length,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(sortedArticles.length / limit)
|
||||
}), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
};
|
@ -26,7 +26,6 @@ export async function GET() {
|
||||
date: article.data.date,
|
||||
summary: article.data.summary || '',
|
||||
tags: article.data.tags || [],
|
||||
image: article.data.image || '',
|
||||
content: contentText // 添加文章内容
|
||||
};
|
||||
})
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { getCollection, render } from "astro:content";
|
||||
import { getSpecialPath } from "@/content.config";
|
||||
import Layout from "@/components/Layout.astro";
|
||||
import { Breadcrumb } from "@/components/Breadcrumb.tsx";
|
||||
import Breadcrumb from "@/components/Breadcrumb.astro";
|
||||
import { ARTICLE_EXPIRY_CONFIG } from "@/consts";
|
||||
|
||||
// 添加这一行,告诉Astro预渲染这个页面
|
||||
@ -10,7 +10,6 @@ export const prerender = true;
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const articles = await getCollection("articles");
|
||||
const views = ["grid", "timeline"];
|
||||
|
||||
// 为每篇文章生成路由参数
|
||||
const paths = [];
|
||||
@ -34,7 +33,6 @@ export async function getStaticPaths() {
|
||||
|
||||
// 为每个可能的路径生成路由
|
||||
for (const path of possiblePaths) {
|
||||
// 添加基本路由
|
||||
paths.push({
|
||||
params: { id: path },
|
||||
props: {
|
||||
@ -43,24 +41,8 @@ export async function getStaticPaths() {
|
||||
? article.id.split("/").slice(0, -1).join("/")
|
||||
: "",
|
||||
originalId: path !== article.id ? article.id : undefined,
|
||||
view: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
// 为每个视图添加路由
|
||||
for (const view of views) {
|
||||
paths.push({
|
||||
params: { id: `${path}/${view}` },
|
||||
props: {
|
||||
article,
|
||||
section: article.id.includes("/")
|
||||
? article.id.split("/").slice(0, -1).join("/")
|
||||
: "",
|
||||
originalId: path !== article.id ? article.id : undefined,
|
||||
view,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,7 +50,10 @@ export async function getStaticPaths() {
|
||||
}
|
||||
|
||||
// 获取文章内容
|
||||
const { article, section, originalId, view } = Astro.props;
|
||||
const { article, section, originalId } = Astro.props;
|
||||
|
||||
// 获取搜索参数
|
||||
const searchParams = new URLSearchParams(Astro.url.search);
|
||||
|
||||
// 如果有原始ID,使用它来渲染内容
|
||||
const articleToRender = originalId ? { ...article, id: originalId } : article;
|
||||
@ -76,8 +61,8 @@ const articleToRender = originalId ? { ...article, id: originalId } : article;
|
||||
// 渲染文章内容
|
||||
const { Content } = await render(articleToRender);
|
||||
|
||||
// 获取面包屑导航
|
||||
const breadcrumbs = section ? section.split("/") : [];
|
||||
// 获取面包屑路径段
|
||||
const pathSegments = section ? section.split("/") : [];
|
||||
|
||||
// 获取相关文章
|
||||
const allArticles = await getCollection("articles");
|
||||
@ -151,17 +136,16 @@ const description =
|
||||
|
||||
// 处理特殊ID的函数
|
||||
function getArticleUrl(articleId: string) {
|
||||
return `/articles/${getSpecialPath(articleId)}${view ? `/${view}` : ""}`;
|
||||
return `/articles/${getSpecialPath(articleId)}${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
<Layout
|
||||
title={article.data.title}
|
||||
description={description}
|
||||
date={article.data.date}
|
||||
author={article.data.author}
|
||||
tags={article.data.tags}
|
||||
image={article.data.image}
|
||||
>
|
||||
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- 阅读进度条 -->
|
||||
@ -180,44 +164,18 @@ function getArticleUrl(articleId: string) {
|
||||
<header class="mb-8">
|
||||
<!-- 导航区域 -->
|
||||
<div
|
||||
class="bg-white dark:bg-dark-card rounded-xl p-4 mb-6 shadow-lg border border-gray-200 dark:border-gray-700"
|
||||
class="bg-white dark:bg-gray-800 rounded-xl p-4 mb-6 shadow-lg border border-gray-200 dark:border-gray-700 relative z-10"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3"
|
||||
>
|
||||
<div class="overflow-x-auto">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||
<div class="w-full overflow-hidden">
|
||||
<Breadcrumb
|
||||
pageType="article"
|
||||
pathSegments={breadcrumbs}
|
||||
pathSegments={pathSegments}
|
||||
searchParams={searchParams}
|
||||
articleTitle={article.data.title}
|
||||
client:load
|
||||
path={section}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center shrink-0">
|
||||
{/* 返回按钮 */}
|
||||
<a
|
||||
href={`/articles${view ? `/${view}` : ""}`}
|
||||
class="text-secondary-500 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400 flex items-center text-sm"
|
||||
data-astro-prefetch="hover"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 mr-1"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 19l-7-7m0 0l7-7m-7 7h18"
|
||||
></path>
|
||||
</svg>
|
||||
返回文章列表
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -267,7 +225,7 @@ function getArticleUrl(articleId: string) {
|
||||
/>
|
||||
</svg>
|
||||
<a
|
||||
href={`/articles?path=${encodeURIComponent(section)}`}
|
||||
href={`/articles/${section}/`}
|
||||
class="hover:text-indigo-600 break-all"
|
||||
>
|
||||
{section}
|
||||
@ -282,7 +240,7 @@ function getArticleUrl(articleId: string) {
|
||||
<div class="flex flex-wrap gap-2 mb-6">
|
||||
{article.data.tags.map((tag) => (
|
||||
<a
|
||||
href={`/articles?tag=${tag}`}
|
||||
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 hover:bg-primary-100 dark:hover:bg-primary-800/30"
|
||||
data-astro-prefetch="hover"
|
||||
>
|
||||
@ -341,7 +299,7 @@ function getArticleUrl(articleId: string) {
|
||||
|
||||
<!-- 文章内容 -->
|
||||
<article
|
||||
class="prose prose-lg dark:prose-invert prose-primary prose-table:rounded-lg prose-table:border-separate prose-table:border-2 prose-thead:bg-primary-50 dark:prose-thead:bg-dark-surface prose-ul:list-disc prose-ol:list-decimal prose-li:my-1 prose-blockquote:border-l-4 prose-blockquote:border-primary-500 prose-blockquote:bg-gray-100 prose-blockquote:dark:bg-dark-surface prose-a:text-primary-600 prose-a:dark:text-primary-400 prose-a:no-underline prose-a:border-b prose-a:border-primary-300 prose-a:hover:border-primary-600 max-w-none mb-12"
|
||||
class="prose prose-lg dark:prose-invert prose-primary prose-table:rounded-lg prose-table:border-separate prose-table:border-2 prose-thead:bg-primary-50 dark:prose-thead:bg-gray-800 prose-ul:list-disc prose-ol:list-decimal prose-li:my-1 prose-blockquote:border-l-4 prose-blockquote:border-primary-500 prose-blockquote:bg-gray-100 prose-blockquote:dark:bg-gray-800 prose-a:text-primary-600 prose-a:dark:text-primary-400 prose-a:no-underline prose-a:border-b prose-a:border-primary-300 prose-a:hover:border-primary-600 max-w-none mb-12"
|
||||
>
|
||||
<Content />
|
||||
</article>
|
||||
@ -351,10 +309,10 @@ function getArticleUrl(articleId: string) {
|
||||
class="hidden 2xl:block fixed right-[calc(50%-48rem)] top-20 w-64 z-30"
|
||||
>
|
||||
<div
|
||||
class="bg-white dark:bg-dark-card rounded-lg shadow-lg p-4 max-h-[calc(100vh-8rem)] overflow-y-auto border border-gray-200 dark:border-gray-700"
|
||||
class="bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 flex flex-col"
|
||||
>
|
||||
<div
|
||||
class="border-b border-secondary-100 dark:border-dark-border pb-2 mb-3 sticky top-0 bg-white dark:bg-dark-card"
|
||||
class="border-b border-secondary-100 dark:border-gray-700 p-4 pb-2 sticky top-0 bg-white dark:bg-gray-800 z-10"
|
||||
>
|
||||
<h3 class="font-bold text-primary-700 dark:text-primary-400">
|
||||
文章目录
|
||||
@ -362,7 +320,7 @@ function getArticleUrl(articleId: string) {
|
||||
</div>
|
||||
<div
|
||||
id="toc-content"
|
||||
class="text-sm"
|
||||
class="text-sm p-4 pt-0 overflow-y-auto max-h-[calc(100vh-8rem-42px)]"
|
||||
>
|
||||
<!-- 目录内容将通过JavaScript动态生成 -->
|
||||
</div>
|
||||
@ -373,7 +331,7 @@ function getArticleUrl(articleId: string) {
|
||||
<!-- 相关文章 -->
|
||||
{
|
||||
relatedArticles.length > 0 && (
|
||||
<div class="mt-12 pt-8 border-t border-secondary-200 dark:border-dark-border">
|
||||
<div class="mt-12 pt-8 border-t border-secondary-200 dark:border-gray-700">
|
||||
<h2 class="text-2xl font-bold mb-6 text-primary-900 dark:text-primary-100">
|
||||
{relatedArticlesMatchType === "tag" ? "相关文章" :
|
||||
relatedArticlesMatchType === "directory" ? "同类文章" : "推荐阅读"}
|
||||
@ -382,7 +340,7 @@ function getArticleUrl(articleId: string) {
|
||||
{relatedArticles.map((relatedArticle) => (
|
||||
<a
|
||||
href={getArticleUrl(relatedArticle.id)}
|
||||
class="block p-5 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-dark-card hover:shadow-xl hover:-translate-y-1 shadow-lg"
|
||||
class="block p-5 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 hover:shadow-xl hover:-translate-y-1 shadow-lg"
|
||||
data-astro-prefetch="viewport"
|
||||
>
|
||||
<h3 class="font-bold text-lg mb-2 line-clamp-2 text-gray-800 dark:text-gray-200 hover:text-primary-700 dark:hover:text-primary-400">
|
||||
@ -637,7 +595,7 @@ function getArticleUrl(articleId: string) {
|
||||
const language = languageMatch ? languageMatch[1] : "text";
|
||||
|
||||
const header = document.createElement("div");
|
||||
header.className = "code-header flex justify-between items-center text-xs px-4 py-2 bg-secondary-800 dark:bg-dark-card text-secondary-300 dark:text-secondary-400 rounded-t-lg";
|
||||
header.className = "code-header flex justify-between items-center text-xs px-4 py-2 bg-secondary-800 dark:bg-gray-900 text-secondary-300 dark:text-secondary-400 rounded-t-lg";
|
||||
|
||||
const languageLabel = document.createElement("span");
|
||||
languageLabel.className = "code-language font-mono";
|
||||
@ -687,16 +645,6 @@ function getArticleUrl(articleId: string) {
|
||||
});
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
listeners.forEach(({ element, eventType, handler }) => {
|
||||
try {
|
||||
element.removeEventListener(eventType, handler);
|
||||
} catch (err) {}
|
||||
});
|
||||
|
||||
listeners.length = 0;
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (!document.querySelector("article")) return;
|
||||
|
||||
@ -718,6 +666,16 @@ function getArticleUrl(articleId: string) {
|
||||
window.addEventListener("beforeunload", cleanup, { once: true });
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
listeners.forEach(({ element, eventType, handler }) => {
|
||||
try {
|
||||
element.removeEventListener(eventType, handler);
|
||||
} catch (err) {}
|
||||
});
|
||||
|
||||
listeners.length = 0;
|
||||
}
|
||||
|
||||
registerCleanup();
|
||||
})();
|
||||
</script>
|
||||
|
@ -1,106 +1,65 @@
|
||||
---
|
||||
import ArticlesPage, { getStaticPaths as getOriginalPaths } from './index.astro';
|
||||
import ArticlesPage from './index.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
// 启用静态预渲染
|
||||
export const prerender = true;
|
||||
|
||||
// 重新导出 getStaticPaths,处理所有路径模式
|
||||
// 获取目录结构
|
||||
export async function getStaticPaths() {
|
||||
const paths = await getOriginalPaths();
|
||||
const allPaths = paths.map(({ props }) => {
|
||||
const results = [];
|
||||
const articles = await getCollection('articles');
|
||||
|
||||
// 1. 如果有标签,添加标签路径
|
||||
if (props.tag) {
|
||||
// 标签主页
|
||||
results.push({
|
||||
params: { path: `tag/${props.tag}` },
|
||||
props: { ...props }
|
||||
});
|
||||
// 标签视图页
|
||||
results.push({
|
||||
params: { path: `tag/${props.tag}/grid` },
|
||||
props: { ...props, view: 'grid' }
|
||||
});
|
||||
results.push({
|
||||
params: { path: `tag/${props.tag}/timeline` },
|
||||
props: { ...props, view: 'timeline' }
|
||||
});
|
||||
// 从文章ID中提取所有目录路径
|
||||
const directories = new Set<string>();
|
||||
|
||||
articles.forEach(article => {
|
||||
if (article.id.includes('/')) {
|
||||
// 获取所有层级的目录
|
||||
const parts = article.id.split('/');
|
||||
let currentPath = '';
|
||||
|
||||
// 逐级构建目录路径
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
currentPath = currentPath ? `${currentPath}/${parts[i]}` : parts[i];
|
||||
directories.add(currentPath);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 2. 如果有路径,添加目录路径
|
||||
if (props.path) {
|
||||
// 目录主页
|
||||
results.push({
|
||||
params: { path: props.path },
|
||||
props: { ...props }
|
||||
});
|
||||
// 目录视图页
|
||||
results.push({
|
||||
params: { path: `${props.path}/grid` },
|
||||
props: { ...props, view: 'grid' }
|
||||
});
|
||||
results.push({
|
||||
params: { path: `${props.path}/timeline` },
|
||||
props: { ...props, view: 'timeline' }
|
||||
});
|
||||
}
|
||||
// 准备路径数组
|
||||
const paths = [];
|
||||
|
||||
return results;
|
||||
}).flat();
|
||||
// 为每个目录创建一个路由
|
||||
for (const path of directories) {
|
||||
paths.push({
|
||||
params: {
|
||||
// 对于 [...path] 参数,Astro 需要接收单个字符串
|
||||
path: path
|
||||
},
|
||||
props: {
|
||||
path,
|
||||
pageType: 'grid'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 添加顶级视图路径
|
||||
allPaths.push(
|
||||
{
|
||||
params: { path: 'grid' },
|
||||
props: { path: '', tag: '', view: 'grid' }
|
||||
// 添加根路径 (即 /articles/)
|
||||
paths.push({
|
||||
params: {
|
||||
path: undefined
|
||||
},
|
||||
{
|
||||
params: { path: 'timeline' },
|
||||
props: { path: '', tag: '', view: 'timeline' }
|
||||
props: {
|
||||
path: '',
|
||||
pageType: 'grid'
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return allPaths;
|
||||
return paths;
|
||||
}
|
||||
|
||||
// 使用主页面组件
|
||||
const { props } = Astro;
|
||||
|
||||
// 解析路径参数
|
||||
const pathParts = (Astro.params.path as string | undefined)?.split('/') || [];
|
||||
|
||||
// 初始化变量
|
||||
let path = '';
|
||||
let tag = '';
|
||||
let view = 'grid';
|
||||
|
||||
if (pathParts[0] === 'tag' && pathParts.length >= 2) {
|
||||
// 标签路径处理
|
||||
tag = pathParts[1];
|
||||
view = pathParts[2] || 'grid';
|
||||
} else {
|
||||
// 处理普通路径和视图
|
||||
const lastPart = pathParts[pathParts.length - 1];
|
||||
|
||||
if (['grid', 'timeline'].includes(lastPart)) {
|
||||
// 如果最后一部分是视图类型,则移除它并设置视图
|
||||
view = lastPart;
|
||||
pathParts.pop();
|
||||
}
|
||||
|
||||
// 剩余的部分都作为路径
|
||||
path = pathParts.join('/');
|
||||
}
|
||||
|
||||
// 合并属性
|
||||
const mergedProps = {
|
||||
...props,
|
||||
path,
|
||||
tag,
|
||||
view
|
||||
};
|
||||
const { path, pageType = 'grid' } = Astro.props;
|
||||
|
||||
---
|
||||
|
||||
<ArticlesPage {...mergedProps} />
|
||||
<ArticlesPage pageType={pageType} path={path} />
|
File diff suppressed because it is too large
Load Diff
@ -7,6 +7,7 @@ import { VISITED_PLACES } from '@/consts';
|
||||
|
||||
<Layout title="其他">
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<h1 class="text-4xl font-bold mb-8 text-center">其他内容</h1>
|
||||
<section class="mb-16">
|
||||
<h2 class="text-3xl font-semibold text-center mb-6">距离退休还有</h2>
|
||||
<div class="max-w-3xl mx-auto bg-white dark:bg-gray-800 rounded-xl shadow-lg p-8 hover:shadow-xl">
|
||||
|
@ -6,6 +6,7 @@ import { GitPlatform } from '@/components/GitProjectCollection';
|
||||
|
||||
<Layout title="项目 | echoes">
|
||||
<main class="container mx-auto px-4 py-8">
|
||||
<h1 class="text-4xl font-bold mb-8 text-center">项目</h1>
|
||||
<div class="space-y-12">
|
||||
<GitProjectCollection
|
||||
platform={GitPlatform.GITEA}
|
||||
|
@ -12,6 +12,26 @@
|
||||
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
/* 专门为卡片元素添加过渡属性,修复悬停问题 */
|
||||
.recent-article,
|
||||
[class*="hover:-translate-y"] {
|
||||
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease, transform 0.3s ease;
|
||||
/* 添加transform-style属性以优化渲染 */
|
||||
transform-style: preserve-3d;
|
||||
/* 添加will-change提示浏览器将使用GPU加速 */
|
||||
will-change: transform;
|
||||
/* 确保初始状态是稳定的 */
|
||||
transform: translateY(0);
|
||||
/* 防止鼠标在卡片内移动时触发重新计算 */
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
/* 使用更具体的选择器优先级来控制悬停行为 */
|
||||
.recent-article:hover,
|
||||
[class*="hover:-translate-y"]:hover {
|
||||
transform: translateY(-0.25rem) !important;
|
||||
}
|
||||
|
||||
@theme {
|
||||
/* 主色调 - 使用更现代的蓝紫色 */
|
||||
--color-primary-50: #f5f7ff;
|
||||
|
Loading…
Reference in New Issue
Block a user