移除不必要的组件和代码,完善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": {
|
"node_modules/@shikijs/core": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmmirror.com/@shikijs/core/-/core-3.2.1.tgz",
|
"resolved": "https://registry.npmmirror.com/@shikijs/core/-/core-3.2.2.tgz",
|
||||||
"integrity": "sha512-FhsdxMWYu/C11sFisEp7FMGBtX/OSSbnXZDMBhGuUDBNTdsoZlMSgQv5f90rwvzWAdWIW6VobD+G3IrazxA6dQ==",
|
"integrity": "sha512-yvlSKVMLjddAGBa2Yu+vUZxuu3sClOWW1AG+UtJkvejYuGM5BVL35s6Ijiwb75O9QdEx6IkMxinHZSi8ZyrBaA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@shikijs/types": "3.2.1",
|
"@shikijs/types": "3.2.2",
|
||||||
"@shikijs/vscode-textmate": "^10.0.2",
|
"@shikijs/vscode-textmate": "^10.0.2",
|
||||||
"@types/hast": "^3.0.4",
|
"@types/hast": "^3.0.4",
|
||||||
"hast-util-to-html": "^9.0.5"
|
"hast-util-to-html": "^9.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@shikijs/engine-javascript": {
|
"node_modules/@shikijs/engine-javascript": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmmirror.com/@shikijs/engine-javascript/-/engine-javascript-3.2.1.tgz",
|
"resolved": "https://registry.npmmirror.com/@shikijs/engine-javascript/-/engine-javascript-3.2.2.tgz",
|
||||||
"integrity": "sha512-eMdcUzN3FMQYxOmRf2rmU8frikzoSHbQDFH2hIuXsrMO+IBOCI9BeeRkCiBkcLDHeRKbOCtYMJK3D6U32ooU9Q==",
|
"integrity": "sha512-tlDKfhWpF4jKLUyVAnmL+ggIC+0VyteNsUpBzh1iwWLZu4i+PelIRr0TNur6pRRo5UZIv3ss/PLMuwahg9S2hg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@shikijs/types": "3.2.1",
|
"@shikijs/types": "3.2.2",
|
||||||
"@shikijs/vscode-textmate": "^10.0.2",
|
"@shikijs/vscode-textmate": "^10.0.2",
|
||||||
"oniguruma-to-es": "^4.1.0"
|
"oniguruma-to-es": "^4.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@shikijs/engine-oniguruma": {
|
"node_modules/@shikijs/engine-oniguruma": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmmirror.com/@shikijs/engine-oniguruma/-/engine-oniguruma-3.2.1.tgz",
|
"resolved": "https://registry.npmmirror.com/@shikijs/engine-oniguruma/-/engine-oniguruma-3.2.2.tgz",
|
||||||
"integrity": "sha512-wZZAkayEn6qu2+YjenEoFqj0OyQI64EWsNR6/71d1EkG4sxEOFooowKivsWPpaWNBu3sxAG+zPz5kzBL/SsreQ==",
|
"integrity": "sha512-vyXRnWVCSvokwbaUD/8uPn6Gqsf5Hv7XwcW4AgiU4Z2qwy19sdr6VGzMdheKKN58tJOOe5MIKiNb901bgcUXYQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@shikijs/types": "3.2.1",
|
"@shikijs/types": "3.2.2",
|
||||||
"@shikijs/vscode-textmate": "^10.0.2"
|
"@shikijs/vscode-textmate": "^10.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@shikijs/langs": {
|
"node_modules/@shikijs/langs": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmmirror.com/@shikijs/langs/-/langs-3.2.1.tgz",
|
"resolved": "https://registry.npmmirror.com/@shikijs/langs/-/langs-3.2.2.tgz",
|
||||||
"integrity": "sha512-If0iDHYRSGbihiA8+7uRsgb1er1Yj11pwpX1c6HLYnizDsKAw5iaT3JXj5ZpaimXSWky/IhxTm7C6nkiYVym+A==",
|
"integrity": "sha512-NY0Urg2dV9ETt3JIOWoMPuoDNwte3geLZ4M1nrPHbkDS8dWMpKcEwlqiEIGqtwZNmt5gKyWpR26ln2Bg2ecPgw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@shikijs/types": "3.2.1"
|
"@shikijs/types": "3.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@shikijs/themes": {
|
"node_modules/@shikijs/themes": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmmirror.com/@shikijs/themes/-/themes-3.2.1.tgz",
|
"resolved": "https://registry.npmmirror.com/@shikijs/themes/-/themes-3.2.2.tgz",
|
||||||
"integrity": "sha512-k5DKJUT8IldBvAm8WcrDT5+7GA7se6lLksR+2E3SvyqGTyFMzU2F9Gb7rmD+t+Pga1MKrYFxDIeyWjMZWM6uBQ==",
|
"integrity": "sha512-Zuq4lgAxVKkb0FFdhHSdDkALuRpsj1so1JdihjKNQfgM78EHxV2JhO10qPsMrm01FkE3mDRTdF68wfmsqjt6HA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@shikijs/types": "3.2.1"
|
"@shikijs/types": "3.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@shikijs/types": {
|
"node_modules/@shikijs/types": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmmirror.com/@shikijs/types/-/types-3.2.1.tgz",
|
"resolved": "https://registry.npmmirror.com/@shikijs/types/-/types-3.2.2.tgz",
|
||||||
"integrity": "sha512-/NTWAk4KE2M8uac0RhOsIhYQf4pdU0OywQuYDGIGAJ6Mjunxl2cGiuLkvu4HLCMn+OTTLRWkjZITp+aYJv60yA==",
|
"integrity": "sha512-a5TiHk7EH5Lso8sHcLHbVNNhWKP0Wi3yVnXnu73g86n3WoDgEra7n3KszyeCGuyoagspQ2fzvy4cpSc8pKhb0A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@shikijs/vscode-textmate": "^10.0.2",
|
"@shikijs/vscode-textmate": "^10.0.2",
|
||||||
@ -11021,19 +11021,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/oniguruma-parser": {
|
"node_modules/oniguruma-parser": {
|
||||||
"version": "0.5.4",
|
"version": "0.11.2",
|
||||||
"resolved": "https://registry.npmmirror.com/oniguruma-parser/-/oniguruma-parser-0.5.4.tgz",
|
"resolved": "https://registry.npmmirror.com/oniguruma-parser/-/oniguruma-parser-0.11.2.tgz",
|
||||||
"integrity": "sha512-yNxcQ8sKvURiTwP0mV6bLQCYE7NKfKRRWunhbZnXgxSmB1OXa1lHrN3o4DZd+0Si0kU5blidK7BcROO8qv5TZA==",
|
"integrity": "sha512-F7Ld4oDZJCI5/wCZ8AOffQbqjSzIRpKH7I/iuSs1SkhZeCj0wS6PMZ4W6VA16TWHrAo0Y9bBKEJOe7tvwcTXnw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/oniguruma-to-es": {
|
"node_modules/oniguruma-to-es": {
|
||||||
"version": "4.1.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmmirror.com/oniguruma-to-es/-/oniguruma-to-es-4.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/oniguruma-to-es/-/oniguruma-to-es-4.2.0.tgz",
|
||||||
"integrity": "sha512-SNwG909cSLo4vPyyPbU/VJkEc9WOXqu2ycBlfd1UCXLqk1IijcQktSBb2yRQ2UFPsDhpkaf+C1dtT3PkLK/yWA==",
|
"integrity": "sha512-MDPs6KSOLS0tKQ7joqg44dRIRZUyotfTy0r+7oEEs6VwWWP0+E2PPDYWMFN0aqOjRyWHBYq7RfKw9GQk2S2z5g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"emoji-regex-xs": "^1.0.0",
|
"emoji-regex-xs": "^1.0.0",
|
||||||
"oniguruma-parser": "^0.5.4",
|
"oniguruma-parser": "^0.11.0",
|
||||||
"regex": "^6.0.1",
|
"regex": "^6.0.1",
|
||||||
"regex-recursion": "^6.0.2"
|
"regex-recursion": "^6.0.2"
|
||||||
}
|
}
|
||||||
@ -13406,17 +13406,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/shiki": {
|
"node_modules/shiki": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmmirror.com/shiki/-/shiki-3.2.1.tgz",
|
"resolved": "https://registry.npmmirror.com/shiki/-/shiki-3.2.2.tgz",
|
||||||
"integrity": "sha512-VML/2o1/KGYkEf/stJJ+s9Ypn7jUKQPomGLGYso4JJFMFxVDyPNsjsI3MB3KLjlMOeH44gyaPdXC6rik2WXvUQ==",
|
"integrity": "sha512-0qWBkM2t/0NXPRcVgtLhtHv6Ak3Q5yI4K/ggMqcgLRKm4+pCs3namgZlhlat/7u2CuqNtlShNs9lENOG6n7UaQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@shikijs/core": "3.2.1",
|
"@shikijs/core": "3.2.2",
|
||||||
"@shikijs/engine-javascript": "3.2.1",
|
"@shikijs/engine-javascript": "3.2.2",
|
||||||
"@shikijs/engine-oniguruma": "3.2.1",
|
"@shikijs/engine-oniguruma": "3.2.2",
|
||||||
"@shikijs/langs": "3.2.1",
|
"@shikijs/langs": "3.2.2",
|
||||||
"@shikijs/themes": "3.2.1",
|
"@shikijs/themes": "3.2.2",
|
||||||
"@shikijs/types": "3.2.1",
|
"@shikijs/types": "3.2.2",
|
||||||
"@shikijs/vscode-textmate": "^10.0.2",
|
"@shikijs/vscode-textmate": "^10.0.2",
|
||||||
"@types/hast": "^3.0.4"
|
"@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>
|
</header>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Header组件逻辑 - 使用"进入→绑定→退出完全清理"模式
|
// Header组件逻辑
|
||||||
(function () {
|
(function () {
|
||||||
// 存储所有事件监听器,便于统一清理
|
// 存储所有事件监听器,便于统一清理
|
||||||
const listeners: Array<{
|
const listeners: Array<{
|
||||||
@ -330,7 +330,6 @@ const normalizedPath =
|
|||||||
|
|
||||||
// 清理函数 - 移除所有事件监听器
|
// 清理函数 - 移除所有事件监听器
|
||||||
function cleanup(): void {
|
function cleanup(): void {
|
||||||
// 移除所有监听器
|
|
||||||
listeners.forEach(({ element, eventType, handler }) => {
|
listeners.forEach(({ element, eventType, handler }) => {
|
||||||
try {
|
try {
|
||||||
element.removeEventListener(eventType, handler);
|
element.removeEventListener(eventType, handler);
|
||||||
@ -338,16 +337,11 @@ const normalizedPath =
|
|||||||
console.error(`移除Header事件监听器出错:`, err);
|
console.error(`移除Header事件监听器出错:`, err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 清空数组
|
|
||||||
listeners.length = 0;
|
listeners.length = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header和导航高亮逻辑
|
// Header和导航高亮逻辑
|
||||||
function initHeader(): void {
|
function initHeader(): void {
|
||||||
const header = document.getElementById("header-bg");
|
|
||||||
const scrollThreshold = 50;
|
|
||||||
|
|
||||||
// 获取桌面端导航链接(排除移动端菜单中的链接)
|
// 获取桌面端导航链接(排除移动端菜单中的链接)
|
||||||
const navLinks = document.querySelectorAll(".hidden.md\\:flex a[href]");
|
const navLinks = document.querySelectorAll(".hidden.md\\:flex a[href]");
|
||||||
|
|
||||||
@ -366,8 +360,6 @@ const normalizedPath =
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 可以添加其他特殊情况的处理逻辑
|
|
||||||
|
|
||||||
return false;
|
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();
|
updateNavHighlight();
|
||||||
|
|
||||||
// 添加滚动事件监听
|
|
||||||
addListener(window, "scroll", updateHeaderBackground);
|
|
||||||
|
|
||||||
// 监听路由变化
|
// 监听路由变化
|
||||||
addListener(document, "astro:page-load", updateNavHighlight);
|
addListener(document, "astro:page-load", updateNavHighlight);
|
||||||
addListener(document, "astro:after-swap", updateNavHighlight);
|
addListener(document, "astro:after-swap", updateNavHighlight);
|
||||||
@ -479,7 +458,7 @@ const normalizedPath =
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mobileMenuButton && mobileMenu && menuOpenIcon && menuCloseIcon) {
|
if (mobileMenuButton && mobileMenu && menuOpenIcon && menuCloseIcon) {
|
||||||
// 移动端菜单按钮点击事件 - 使用捕获模式确保事件优先处理
|
// 移动端菜单按钮点击事件
|
||||||
(mobileMenuButton as HTMLElement).style.pointerEvents = "auto";
|
(mobileMenuButton as HTMLElement).style.pointerEvents = "auto";
|
||||||
|
|
||||||
addListener(
|
addListener(
|
||||||
@ -492,24 +471,18 @@ const normalizedPath =
|
|||||||
const expanded =
|
const expanded =
|
||||||
mobileMenuButton.getAttribute("aria-expanded") === "true";
|
mobileMenuButton.getAttribute("aria-expanded") === "true";
|
||||||
|
|
||||||
// 切换菜单状态
|
|
||||||
mobileMenuButton.setAttribute(
|
mobileMenuButton.setAttribute(
|
||||||
"aria-expanded",
|
"aria-expanded",
|
||||||
(!expanded).toString(),
|
(!expanded).toString(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
// 直接隐藏菜单,不使用过渡效果
|
|
||||||
mobileMenu.classList.add("hidden");
|
mobileMenu.classList.add("hidden");
|
||||||
} else {
|
} else {
|
||||||
// 打开菜单前先关闭搜索面板
|
|
||||||
closeMobileSearch();
|
closeMobileSearch();
|
||||||
|
|
||||||
// 直接显示菜单,不使用过渡效果
|
|
||||||
mobileMenu.classList.remove("hidden");
|
mobileMenu.classList.remove("hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换图标
|
|
||||||
menuOpenIcon.classList.toggle("hidden");
|
menuOpenIcon.classList.toggle("hidden");
|
||||||
menuCloseIcon.classList.toggle("hidden");
|
menuCloseIcon.classList.toggle("hidden");
|
||||||
},
|
},
|
||||||
@ -526,7 +499,6 @@ const normalizedPath =
|
|||||||
link,
|
link,
|
||||||
"click",
|
"click",
|
||||||
(e) => {
|
(e) => {
|
||||||
// 不要阻止默认行为,因为需要跳转
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
closeMobileMenu();
|
closeMobileMenu();
|
||||||
},
|
},
|
||||||
@ -537,7 +509,6 @@ const normalizedPath =
|
|||||||
|
|
||||||
// 移动端搜索按钮
|
// 移动端搜索按钮
|
||||||
if (mobileSearchButton && mobileSearchPanel) {
|
if (mobileSearchButton && mobileSearchPanel) {
|
||||||
// 搜索按钮点击事件 - 使用捕获模式确保事件优先处理
|
|
||||||
(mobileSearchButton as HTMLElement).style.pointerEvents = "auto";
|
(mobileSearchButton as HTMLElement).style.pointerEvents = "auto";
|
||||||
|
|
||||||
addListener(
|
addListener(
|
||||||
@ -547,18 +518,13 @@ const normalizedPath =
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
// 检查搜索面板是否已经打开
|
|
||||||
const isSearchVisible =
|
const isSearchVisible =
|
||||||
!mobileSearchPanel.classList.contains("hidden");
|
!mobileSearchPanel.classList.contains("hidden");
|
||||||
|
|
||||||
if (isSearchVisible) {
|
if (isSearchVisible) {
|
||||||
// 如果搜索面板已打开,则关闭它
|
|
||||||
closeMobileSearch();
|
closeMobileSearch();
|
||||||
} else {
|
} else {
|
||||||
// 打开搜索面板前先关闭菜单
|
|
||||||
closeMobileMenu();
|
closeMobileMenu();
|
||||||
|
|
||||||
// 打开搜索面板
|
|
||||||
mobileSearchPanel.classList.remove("hidden");
|
mobileSearchPanel.classList.remove("hidden");
|
||||||
if (mobileSearch) mobileSearch.focus();
|
if (mobileSearch) mobileSearch.focus();
|
||||||
}
|
}
|
||||||
@ -593,13 +559,11 @@ const normalizedPath =
|
|||||||
"click",
|
"click",
|
||||||
(e: Event) => {
|
(e: Event) => {
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
// 如果点击的不是主题切换按钮本身,则手动触发主题切换
|
|
||||||
if (
|
if (
|
||||||
target.id !== "theme-toggle-button" &&
|
target.id !== "theme-toggle-button" &&
|
||||||
!target.closest("#theme-toggle-button")
|
!target.closest("#theme-toggle-button")
|
||||||
) {
|
) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
// 获取容器内的主题切换按钮并模拟点击
|
|
||||||
const toggleButton = themeToggleContainer.querySelector(
|
const toggleButton = themeToggleContainer.querySelector(
|
||||||
"#theme-toggle-button",
|
"#theme-toggle-button",
|
||||||
);
|
);
|
||||||
@ -637,7 +601,6 @@ const normalizedPath =
|
|||||||
const mobileResults = document.getElementById("mobile-search-results");
|
const mobileResults = document.getElementById("mobile-search-results");
|
||||||
const mobileList = document.getElementById("mobile-search-list");
|
const mobileList = document.getElementById("mobile-search-list");
|
||||||
const mobileMessage = document.getElementById("mobile-search-message");
|
const mobileMessage = document.getElementById("mobile-search-message");
|
||||||
const mobileSearchPanel = document.getElementById("mobile-search-panel");
|
|
||||||
|
|
||||||
// 获取文章数据
|
// 获取文章数据
|
||||||
async function fetchArticles(): Promise<void> {
|
async function fetchArticles(): Promise<void> {
|
||||||
@ -659,7 +622,6 @@ const normalizedPath =
|
|||||||
function highlightText(text: string, query: string): string {
|
function highlightText(text: string, query: string): string {
|
||||||
if (!text || !query.trim()) return text;
|
if (!text || !query.trim()) return text;
|
||||||
|
|
||||||
// 转义正则表达式中的特殊字符
|
|
||||||
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
const regex = new RegExp(`(${escapedQuery})`, "gi");
|
const regex = new RegExp(`(${escapedQuery})`, "gi");
|
||||||
|
|
||||||
@ -761,15 +723,12 @@ const normalizedPath =
|
|||||||
const endPos = Math.min(article.content.length, matchIndex + 100);
|
const endPos = Math.min(article.content.length, matchIndex + 100);
|
||||||
// 提取片段
|
// 提取片段
|
||||||
let snippet = article.content.substring(startPos, endPos);
|
let snippet = article.content.substring(startPos, endPos);
|
||||||
// 如果不是从文章开头开始,添加省略号
|
|
||||||
if (startPos > 0) {
|
if (startPos > 0) {
|
||||||
snippet = "..." + snippet;
|
snippet = "..." + snippet;
|
||||||
}
|
}
|
||||||
// 如果不是到文章结尾,添加省略号
|
|
||||||
if (endPos < article.content.length) {
|
if (endPos < article.content.length) {
|
||||||
snippet = snippet + "...";
|
snippet = snippet + "...";
|
||||||
}
|
}
|
||||||
// 高亮匹配的文本
|
|
||||||
snippet = highlightText(snippet, query);
|
snippet = highlightText(snippet, query);
|
||||||
contentMatch = `<p class="text-xs text-gray-500 dark:text-gray-400 mt-1 line-clamp-2">${snippet}</p>`;
|
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 {
|
function closeSearchPanels(): void {
|
||||||
// 关闭桌面端搜索结果
|
|
||||||
const desktopSearchResults = document.getElementById("desktop-search-results");
|
const desktopSearchResults = document.getElementById("desktop-search-results");
|
||||||
if (desktopSearchResults) {
|
if (desktopSearchResults) {
|
||||||
desktopSearchResults.classList.add("hidden");
|
desktopSearchResults.classList.add("hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭移动端搜索面板
|
|
||||||
const mobileSearchPanel = document.getElementById("mobile-search-panel");
|
const mobileSearchPanel = document.getElementById("mobile-search-panel");
|
||||||
if (mobileSearchPanel) {
|
if (mobileSearchPanel) {
|
||||||
mobileSearchPanel.classList.add("hidden");
|
mobileSearchPanel.classList.add("hidden");
|
||||||
@ -915,13 +872,10 @@ const normalizedPath =
|
|||||||
|
|
||||||
// 为搜索结果链接添加点击事件
|
// 为搜索结果链接添加点击事件
|
||||||
function addSearchResultsClickListeners(): void {
|
function addSearchResultsClickListeners(): void {
|
||||||
// 获取所有搜索结果链接
|
|
||||||
const searchResultLinks = document.querySelectorAll('.search-result-link');
|
const searchResultLinks = document.querySelectorAll('.search-result-link');
|
||||||
|
|
||||||
// 为每个链接添加点击事件
|
|
||||||
searchResultLinks.forEach(link => {
|
searchResultLinks.forEach(link => {
|
||||||
addListener(link, 'click', () => {
|
addListener(link, 'click', () => {
|
||||||
// 点击搜索结果后关闭所有搜索面板
|
|
||||||
closeSearchPanels();
|
closeSearchPanels();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -929,27 +883,19 @@ const normalizedPath =
|
|||||||
|
|
||||||
// 注册清理函数
|
// 注册清理函数
|
||||||
function registerCleanup(): void {
|
function registerCleanup(): void {
|
||||||
// Astro 事件
|
|
||||||
document.addEventListener("astro:before-preparation", cleanup, {
|
document.addEventListener("astro:before-preparation", cleanup, {
|
||||||
once: true,
|
once: true,
|
||||||
});
|
});
|
||||||
document.addEventListener("astro:before-swap", cleanup, { once: true });
|
document.addEventListener("astro:before-swap", cleanup, { once: true });
|
||||||
|
|
||||||
// Swup 事件
|
|
||||||
document.addEventListener("swup:willReplaceContent", cleanup, {
|
document.addEventListener("swup:willReplaceContent", cleanup, {
|
||||||
once: true,
|
once: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 页面卸载
|
|
||||||
window.addEventListener("beforeunload", cleanup, { once: true });
|
window.addEventListener("beforeunload", cleanup, { once: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化全部功能
|
// 初始化全部功能
|
||||||
function setupHeader(): void {
|
function setupHeader(): void {
|
||||||
// 先清理之前的事件监听器
|
|
||||||
cleanup();
|
cleanup();
|
||||||
|
|
||||||
// 初始化各个组件
|
|
||||||
initHeader();
|
initHeader();
|
||||||
initSearch();
|
initSearch();
|
||||||
registerCleanup();
|
registerCleanup();
|
||||||
@ -957,7 +903,6 @@ const normalizedPath =
|
|||||||
// 在页面路由变化时关闭搜索面板
|
// 在页面路由变化时关闭搜索面板
|
||||||
addListener(document, 'astro:page-load', closeSearchPanels);
|
addListener(document, 'astro:page-load', closeSearchPanels);
|
||||||
addListener(document, 'astro:after-swap', closeSearchPanels);
|
addListener(document, 'astro:after-swap', closeSearchPanels);
|
||||||
addListener(document, 'swup:contentReplaced', closeSearchPanels);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在页面加载时初始化
|
// 在页面加载时初始化
|
||||||
@ -966,15 +911,11 @@ const normalizedPath =
|
|||||||
once: true,
|
once: true,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 使用setTimeout确保处于事件队列末尾,避免可能的事件冲突
|
|
||||||
setTimeout(setupHeader, 0);
|
setTimeout(setupHeader, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在页面转换后重新初始化
|
// 在页面转换后重新初始化
|
||||||
document.addEventListener("astro:after-swap", setupHeader);
|
document.addEventListener("astro:after-swap", setupHeader);
|
||||||
document.addEventListener("astro:page-load", setupHeader);
|
document.addEventListener("astro:page-load", setupHeader);
|
||||||
|
|
||||||
// Swup页面内容替换后重新初始化
|
|
||||||
document.addEventListener("swup:contentReplaced", setupHeader);
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
@ -9,16 +9,14 @@ interface Props {
|
|||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
date?: Date;
|
date?: Date;
|
||||||
author?: string;
|
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
image?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取完整的 URL
|
// 获取完整的 URL
|
||||||
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
|
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
|
||||||
|
|
||||||
// 从props中获取页面特定信息
|
// 从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>
|
<!doctype html>
|
||||||
<html lang="zh-CN" class="m-0 w-full h-full">
|
<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:url" content={canonicalURL} />
|
||||||
<meta property="og:title" content={title} />
|
<meta property="og:title" content={title} />
|
||||||
<meta property="og:description" content={description || `${SITE_NAME} - 个人博客`} />
|
<meta property="og:description" content={description || `${SITE_NAME} - 个人博客`} />
|
||||||
{image && <meta property="og:image" content={new URL(image, Astro.site)} />}
|
|
||||||
|
|
||||||
<!-- Twitter -->
|
<!-- Twitter -->
|
||||||
<meta property="twitter:card" content="summary_large_image" />
|
<meta property="twitter:card" content="summary_large_image" />
|
||||||
<meta property="twitter:url" content={canonicalURL} />
|
<meta property="twitter:url" content={canonicalURL} />
|
||||||
<meta property="twitter:title" content={title} />
|
<meta property="twitter:title" content={title} />
|
||||||
<meta property="twitter:description" content={description || `${SITE_NAME} - 个人博客`} />
|
<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()} />}
|
{date && <meta property="article:published_time" content={date.toISOString()} />}
|
||||||
{author && <meta name="author" content={author} />}
|
|
||||||
{tags && tags.map(tag => (
|
{tags && tags.map(tag => (
|
||||||
<meta property="article:tag" content={tag} />
|
<meta property="article:tag" content={tag} />
|
||||||
))}
|
))}
|
||||||
|
@ -135,11 +135,7 @@ const articles = defineCollection({
|
|||||||
date: z.date(),
|
date: z.date(),
|
||||||
tags: z.array(z.string()).optional(),
|
tags: z.array(z.string()).optional(),
|
||||||
summary: z.string().optional(),
|
summary: z.string().optional(),
|
||||||
image: z.string().optional(),
|
|
||||||
author: z.string().optional(),
|
|
||||||
draft: z.boolean().optional().default(false),
|
draft: z.boolean().optional().default(false),
|
||||||
section: z.string().optional(),
|
|
||||||
weight: z.number().optional(),
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
title: "常用软件"
|
title: "常用软件"
|
||||||
date: 2023-04-28T20:56:00Z
|
date: 2023-04-28T20:56:00Z
|
||||||
tags: []
|
tags: []
|
||||||
|
summary : "常用的应用合集"
|
||||||
---
|
---
|
||||||
|
|
||||||
### Windows 应用
|
### Windows 应用
|
||||||
|
@ -2,69 +2,62 @@ import type { APIRoute } from 'astro';
|
|||||||
import { getCollection } from 'astro:content';
|
import { getCollection } from 'astro:content';
|
||||||
import { getSpecialPath } from '../../content.config';
|
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的函数
|
// 处理特殊ID的函数
|
||||||
function getArticleUrl(articleId: string) {
|
function getArticleUrl(articleId: string) {
|
||||||
return `/articles/${getSpecialPath(articleId)}`;
|
return `/articles/${getSpecialPath(articleId)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GET: APIRoute = async ({ request }) => {
|
export const GET: APIRoute = async ({ request }) => {
|
||||||
// 获取查询参数
|
try {
|
||||||
const url = new URL(request.url);
|
// 获取所有文章
|
||||||
const page = parseInt(url.searchParams.get('page') || '1');
|
const articles = await getCollection('articles');
|
||||||
const limit = parseInt(url.searchParams.get('limit') || '10');
|
// 格式化文章数据
|
||||||
const tag = url.searchParams.get('tag') || '';
|
const formattedArticles = articles.map(article => ({
|
||||||
const path = url.searchParams.get('path') || '';
|
id: article.id,
|
||||||
|
title: article.data.title,
|
||||||
// 获取所有文章
|
date: article.data.date,
|
||||||
const articles = await getCollection('articles');
|
tags: article.data.tags || [],
|
||||||
|
summary: article.data.summary || (article.body ? extractSummary(article.body) : ''),
|
||||||
// 根据条件过滤文章
|
url: getArticleUrl(article.id) // 使用特殊ID处理函数
|
||||||
let filteredArticles = articles;
|
}));
|
||||||
|
|
||||||
// 如果有标签过滤
|
return new Response(JSON.stringify({
|
||||||
if (tag) {
|
articles: formattedArticles,
|
||||||
filteredArticles = filteredArticles.filter(article =>
|
total: formattedArticles.length,
|
||||||
article.data.tags && article.data.tags.includes(tag)
|
success: true
|
||||||
);
|
}), {
|
||||||
}
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
// 如果有路径过滤,直接使用文章ID来判断
|
// 添加缓存头,缓存1小时
|
||||||
if (path) {
|
'Cache-Control': 'public, max-age=3600'
|
||||||
const normalizedPath = path.toLowerCase();
|
}
|
||||||
filteredArticles = filteredArticles.filter(article => {
|
});
|
||||||
return article.id.toLowerCase().includes(normalizedPath);
|
} 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,
|
date: article.data.date,
|
||||||
summary: article.data.summary || '',
|
summary: article.data.summary || '',
|
||||||
tags: article.data.tags || [],
|
tags: article.data.tags || [],
|
||||||
image: article.data.image || '',
|
|
||||||
content: contentText // 添加文章内容
|
content: contentText // 添加文章内容
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import { getCollection, render } from "astro:content";
|
import { getCollection, render } from "astro:content";
|
||||||
import { getSpecialPath } from "@/content.config";
|
import { getSpecialPath } from "@/content.config";
|
||||||
import Layout from "@/components/Layout.astro";
|
import Layout from "@/components/Layout.astro";
|
||||||
import { Breadcrumb } from "@/components/Breadcrumb.tsx";
|
import Breadcrumb from "@/components/Breadcrumb.astro";
|
||||||
import { ARTICLE_EXPIRY_CONFIG } from "@/consts";
|
import { ARTICLE_EXPIRY_CONFIG } from "@/consts";
|
||||||
|
|
||||||
// 添加这一行,告诉Astro预渲染这个页面
|
// 添加这一行,告诉Astro预渲染这个页面
|
||||||
@ -10,7 +10,6 @@ export const prerender = true;
|
|||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const articles = await getCollection("articles");
|
const articles = await getCollection("articles");
|
||||||
const views = ["grid", "timeline"];
|
|
||||||
|
|
||||||
// 为每篇文章生成路由参数
|
// 为每篇文章生成路由参数
|
||||||
const paths = [];
|
const paths = [];
|
||||||
@ -34,7 +33,6 @@ export async function getStaticPaths() {
|
|||||||
|
|
||||||
// 为每个可能的路径生成路由
|
// 为每个可能的路径生成路由
|
||||||
for (const path of possiblePaths) {
|
for (const path of possiblePaths) {
|
||||||
// 添加基本路由
|
|
||||||
paths.push({
|
paths.push({
|
||||||
params: { id: path },
|
params: { id: path },
|
||||||
props: {
|
props: {
|
||||||
@ -43,24 +41,8 @@ export async function getStaticPaths() {
|
|||||||
? article.id.split("/").slice(0, -1).join("/")
|
? article.id.split("/").slice(0, -1).join("/")
|
||||||
: "",
|
: "",
|
||||||
originalId: path !== article.id ? article.id : undefined,
|
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,使用它来渲染内容
|
// 如果有原始ID,使用它来渲染内容
|
||||||
const articleToRender = originalId ? { ...article, id: originalId } : article;
|
const articleToRender = originalId ? { ...article, id: originalId } : article;
|
||||||
@ -76,8 +61,8 @@ const articleToRender = originalId ? { ...article, id: originalId } : article;
|
|||||||
// 渲染文章内容
|
// 渲染文章内容
|
||||||
const { Content } = await render(articleToRender);
|
const { Content } = await render(articleToRender);
|
||||||
|
|
||||||
// 获取面包屑导航
|
// 获取面包屑路径段
|
||||||
const breadcrumbs = section ? section.split("/") : [];
|
const pathSegments = section ? section.split("/") : [];
|
||||||
|
|
||||||
// 获取相关文章
|
// 获取相关文章
|
||||||
const allArticles = await getCollection("articles");
|
const allArticles = await getCollection("articles");
|
||||||
@ -151,17 +136,16 @@ const description =
|
|||||||
|
|
||||||
// 处理特殊ID的函数
|
// 处理特殊ID的函数
|
||||||
function getArticleUrl(articleId: string) {
|
function getArticleUrl(articleId: string) {
|
||||||
return `/articles/${getSpecialPath(articleId)}${view ? `/${view}` : ""}`;
|
return `/articles/${getSpecialPath(articleId)}${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout
|
<Layout
|
||||||
title={article.data.title}
|
title={article.data.title}
|
||||||
description={description}
|
description={description}
|
||||||
date={article.data.date}
|
date={article.data.date}
|
||||||
author={article.data.author}
|
|
||||||
tags={article.data.tags}
|
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">
|
<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">
|
<header class="mb-8">
|
||||||
<!-- 导航区域 -->
|
<!-- 导航区域 -->
|
||||||
<div
|
<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
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||||
class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3"
|
<div class="w-full overflow-hidden">
|
||||||
>
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
pageType="article"
|
pageType="article"
|
||||||
pathSegments={breadcrumbs}
|
pathSegments={pathSegments}
|
||||||
|
searchParams={searchParams}
|
||||||
articleTitle={article.data.title}
|
articleTitle={article.data.title}
|
||||||
client:load
|
path={section}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -267,7 +225,7 @@ function getArticleUrl(articleId: string) {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<a
|
<a
|
||||||
href={`/articles?path=${encodeURIComponent(section)}`}
|
href={`/articles/${section}/`}
|
||||||
class="hover:text-indigo-600 break-all"
|
class="hover:text-indigo-600 break-all"
|
||||||
>
|
>
|
||||||
{section}
|
{section}
|
||||||
@ -282,7 +240,7 @@ function getArticleUrl(articleId: string) {
|
|||||||
<div class="flex flex-wrap gap-2 mb-6">
|
<div class="flex flex-wrap gap-2 mb-6">
|
||||||
{article.data.tags.map((tag) => (
|
{article.data.tags.map((tag) => (
|
||||||
<a
|
<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"
|
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"
|
data-astro-prefetch="hover"
|
||||||
>
|
>
|
||||||
@ -341,7 +299,7 @@ function getArticleUrl(articleId: string) {
|
|||||||
|
|
||||||
<!-- 文章内容 -->
|
<!-- 文章内容 -->
|
||||||
<article
|
<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 />
|
<Content />
|
||||||
</article>
|
</article>
|
||||||
@ -351,10 +309,10 @@ function getArticleUrl(articleId: string) {
|
|||||||
class="hidden 2xl:block fixed right-[calc(50%-48rem)] top-20 w-64 z-30"
|
class="hidden 2xl:block fixed right-[calc(50%-48rem)] top-20 w-64 z-30"
|
||||||
>
|
>
|
||||||
<div
|
<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
|
<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">
|
<h3 class="font-bold text-primary-700 dark:text-primary-400">
|
||||||
文章目录
|
文章目录
|
||||||
@ -362,7 +320,7 @@ function getArticleUrl(articleId: string) {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
id="toc-content"
|
id="toc-content"
|
||||||
class="text-sm"
|
class="text-sm p-4 pt-0 overflow-y-auto max-h-[calc(100vh-8rem-42px)]"
|
||||||
>
|
>
|
||||||
<!-- 目录内容将通过JavaScript动态生成 -->
|
<!-- 目录内容将通过JavaScript动态生成 -->
|
||||||
</div>
|
</div>
|
||||||
@ -373,7 +331,7 @@ function getArticleUrl(articleId: string) {
|
|||||||
<!-- 相关文章 -->
|
<!-- 相关文章 -->
|
||||||
{
|
{
|
||||||
relatedArticles.length > 0 && (
|
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">
|
<h2 class="text-2xl font-bold mb-6 text-primary-900 dark:text-primary-100">
|
||||||
{relatedArticlesMatchType === "tag" ? "相关文章" :
|
{relatedArticlesMatchType === "tag" ? "相关文章" :
|
||||||
relatedArticlesMatchType === "directory" ? "同类文章" : "推荐阅读"}
|
relatedArticlesMatchType === "directory" ? "同类文章" : "推荐阅读"}
|
||||||
@ -382,7 +340,7 @@ function getArticleUrl(articleId: string) {
|
|||||||
{relatedArticles.map((relatedArticle) => (
|
{relatedArticles.map((relatedArticle) => (
|
||||||
<a
|
<a
|
||||||
href={getArticleUrl(relatedArticle.id)}
|
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"
|
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">
|
<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 language = languageMatch ? languageMatch[1] : "text";
|
||||||
|
|
||||||
const header = document.createElement("div");
|
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");
|
const languageLabel = document.createElement("span");
|
||||||
languageLabel.className = "code-language font-mono";
|
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() {
|
function init() {
|
||||||
if (!document.querySelector("article")) return;
|
if (!document.querySelector("article")) return;
|
||||||
|
|
||||||
@ -718,6 +666,16 @@ function getArticleUrl(articleId: string) {
|
|||||||
window.addEventListener("beforeunload", cleanup, { once: true });
|
window.addEventListener("beforeunload", cleanup, { once: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
listeners.forEach(({ element, eventType, handler }) => {
|
||||||
|
try {
|
||||||
|
element.removeEventListener(eventType, handler);
|
||||||
|
} catch (err) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
listeners.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
registerCleanup();
|
registerCleanup();
|
||||||
})();
|
})();
|
||||||
</script>
|
</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;
|
export const prerender = true;
|
||||||
|
|
||||||
// 重新导出 getStaticPaths,处理所有路径模式
|
// 获取目录结构
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const paths = await getOriginalPaths();
|
const articles = await getCollection('articles');
|
||||||
const allPaths = paths.map(({ props }) => {
|
|
||||||
const results = [];
|
// 从文章ID中提取所有目录路径
|
||||||
|
const directories = new Set<string>();
|
||||||
// 1. 如果有标签,添加标签路径
|
|
||||||
if (props.tag) {
|
articles.forEach(article => {
|
||||||
// 标签主页
|
if (article.id.includes('/')) {
|
||||||
results.push({
|
// 获取所有层级的目录
|
||||||
params: { path: `tag/${props.tag}` },
|
const parts = article.id.split('/');
|
||||||
props: { ...props }
|
let currentPath = '';
|
||||||
});
|
|
||||||
// 标签视图页
|
// 逐级构建目录路径
|
||||||
results.push({
|
for (let i = 0; i < parts.length - 1; i++) {
|
||||||
params: { path: `tag/${props.tag}/grid` },
|
currentPath = currentPath ? `${currentPath}/${parts[i]}` : parts[i];
|
||||||
props: { ...props, view: 'grid' }
|
directories.add(currentPath);
|
||||||
});
|
}
|
||||||
results.push({
|
|
||||||
params: { path: `tag/${props.tag}/timeline` },
|
|
||||||
props: { ...props, view: 'timeline' }
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
// 2. 如果有路径,添加目录路径
|
|
||||||
if (props.path) {
|
// 准备路径数组
|
||||||
// 目录主页
|
const paths = [];
|
||||||
results.push({
|
|
||||||
params: { path: props.path },
|
// 为每个目录创建一个路由
|
||||||
props: { ...props }
|
for (const path of directories) {
|
||||||
});
|
paths.push({
|
||||||
// 目录视图页
|
params: {
|
||||||
results.push({
|
// 对于 [...path] 参数,Astro 需要接收单个字符串
|
||||||
params: { path: `${props.path}/grid` },
|
path: path
|
||||||
props: { ...props, view: 'grid' }
|
},
|
||||||
});
|
props: {
|
||||||
results.push({
|
path,
|
||||||
params: { path: `${props.path}/timeline` },
|
pageType: 'grid'
|
||||||
props: { ...props, view: 'timeline' }
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
// 添加根路径 (即 /articles/)
|
||||||
}).flat();
|
paths.push({
|
||||||
|
params: {
|
||||||
// 添加顶级视图路径
|
path: undefined
|
||||||
allPaths.push(
|
|
||||||
{
|
|
||||||
params: { path: 'grid' },
|
|
||||||
props: { path: '', tag: '', view: 'grid' }
|
|
||||||
},
|
},
|
||||||
{
|
props: {
|
||||||
params: { path: 'timeline' },
|
path: '',
|
||||||
props: { path: '', tag: '', view: 'timeline' }
|
pageType: 'grid'
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
return allPaths;
|
return paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用主页面组件
|
// 使用主页面组件
|
||||||
const { props } = Astro;
|
const { path, pageType = 'grid' } = Astro.props;
|
||||||
|
|
||||||
// 解析路径参数
|
|
||||||
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
|
|
||||||
};
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<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="其他">
|
<Layout title="其他">
|
||||||
<div class="container mx-auto px-4 py-8">
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<h1 class="text-4xl font-bold mb-8 text-center">其他内容</h1>
|
||||||
<section class="mb-16">
|
<section class="mb-16">
|
||||||
<h2 class="text-3xl font-semibold text-center mb-6">距离退休还有</h2>
|
<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">
|
<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">
|
<Layout title="项目 | echoes">
|
||||||
<main class="container mx-auto px-4 py-8">
|
<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">
|
<div class="space-y-12">
|
||||||
<GitProjectCollection
|
<GitProjectCollection
|
||||||
platform={GitPlatform.GITEA}
|
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;
|
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 {
|
@theme {
|
||||||
/* 主色调 - 使用更现代的蓝紫色 */
|
/* 主色调 - 使用更现代的蓝紫色 */
|
||||||
--color-primary-50: #f5f7ff;
|
--color-primary-50: #f5f7ff;
|
||||||
|
Loading…
Reference in New Issue
Block a user