移除不必要的组件和代码,完善dom的监听和清理,更换文章目录

This commit is contained in:
lsy 2025-04-22 01:27:38 +08:00
parent 864d134acd
commit 8ec3fe5df0
15 changed files with 2245 additions and 715 deletions

78
package-lock.json generated
View File

@ -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"
}

View 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>

View File

@ -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>
);
}

View File

@ -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>

View File

@ -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} />
))}

View File

@ -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(),
}),
});

View File

@ -2,6 +2,7 @@
title: "常用软件"
date: 2023-04-28T20:56:00Z
tags: []
summary : "常用的应用合集"
---
### Windows 应用

View File

@ -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'
}
});
};

View File

@ -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 // 添加文章内容
};
})

View File

@ -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>

View File

@ -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

View File

@ -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">

View File

@ -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}

View File

@ -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;