优化swup视图,高亮菜单,代码块名称
This commit is contained in:
parent
fc7653011b
commit
2c71dcdbd9
@ -8,7 +8,6 @@ import rehypeExternalLinks from "rehype-external-links";
|
|||||||
import sitemap from "@astrojs/sitemap";
|
import sitemap from "@astrojs/sitemap";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import swup from "@swup/astro"
|
|
||||||
import { SITE_URL } from "./src/consts";
|
import { SITE_URL } from "./src/consts";
|
||||||
import compressor from "astro-compressor";
|
import compressor from "astro-compressor";
|
||||||
import vercel from "@astrojs/vercel";
|
import vercel from "@astrojs/vercel";
|
||||||
@ -54,10 +53,6 @@ export default defineConfig({
|
|||||||
integrations: [
|
integrations: [
|
||||||
// 使用Astro官方的MDX支持
|
// 使用Astro官方的MDX支持
|
||||||
mdx(),
|
mdx(),
|
||||||
swup({
|
|
||||||
cache: true,
|
|
||||||
preload: true,
|
|
||||||
}),
|
|
||||||
react(),
|
react(),
|
||||||
// 使用文章索引生成器
|
// 使用文章索引生成器
|
||||||
articleIndexerIntegration(),
|
articleIndexerIntegration(),
|
||||||
|
5847
package-lock.json
generated
5847
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -18,8 +18,10 @@
|
|||||||
"@expressive-code/plugin-collapsible-sections": "^0.41.2",
|
"@expressive-code/plugin-collapsible-sections": "^0.41.2",
|
||||||
"@expressive-code/plugin-line-numbers": "^0.41.2",
|
"@expressive-code/plugin-line-numbers": "^0.41.2",
|
||||||
"@mermaid-js/mermaid-cli": "^11.4.2",
|
"@mermaid-js/mermaid-cli": "^11.4.2",
|
||||||
"@swup/astro": "^1.6.0",
|
|
||||||
"@swup/fragment-plugin": "^1.1.1",
|
"@swup/fragment-plugin": "^1.1.1",
|
||||||
|
"@swup/head-plugin": "^2.3.1",
|
||||||
|
"@swup/preload-plugin": "^3.2.11",
|
||||||
|
"@swup/progress-plugin": "^3.2.0",
|
||||||
"@tailwindcss/vite": "^4.1.4",
|
"@tailwindcss/vite": "^4.1.4",
|
||||||
"@types/react": "^19.1.2",
|
"@types/react": "^19.1.2",
|
||||||
"@types/react-dom": "^19.1.2",
|
"@types/react-dom": "^19.1.2",
|
||||||
|
@ -195,10 +195,11 @@ const breadcrumbs: Breadcrumb[] = pathSegments
|
|||||||
// 单独保存清理事件的监听器引用
|
// 单独保存清理事件的监听器引用
|
||||||
const cleanupListeners = [];
|
const cleanupListeners = [];
|
||||||
|
|
||||||
|
|
||||||
// 添加事件监听器并记录,方便后续统一清理
|
// 添加事件监听器并记录,方便后续统一清理
|
||||||
function addListener(element, eventType, handler, options) {
|
function addListener(element, eventType, handler, options) {
|
||||||
if (!element) {
|
if (!element) {
|
||||||
console.warn(`[面包屑尝试为不存在的元素添加事件`);
|
console.warn(`[面包屑]尝试为不存在的元素添加事件`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,7 +215,7 @@ const breadcrumbs: Breadcrumb[] = pathSegments
|
|||||||
try {
|
try {
|
||||||
element.removeEventListener(eventType, handler, options);
|
element.removeEventListener(eventType, handler, options);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[面包屑移除事件监听器出错:`, err);
|
console.error(`[面包屑]移除事件监听器出错:`, err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -226,7 +227,7 @@ const breadcrumbs: Breadcrumb[] = pathSegments
|
|||||||
try {
|
try {
|
||||||
element.removeEventListener(eventType, handler, options);
|
element.removeEventListener(eventType, handler, options);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[面包屑移除清理监听器出错:`, err);
|
console.error(`[面包屑]移除清理监听器出错:`, err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -234,6 +235,386 @@ const breadcrumbs: Breadcrumb[] = pathSegments
|
|||||||
cleanupListeners.length = 0;
|
cleanupListeners.length = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取当前URL路径(与导航栏保持一致)
|
||||||
|
function getCurrentPath() {
|
||||||
|
const path = window.location.pathname;
|
||||||
|
return path === '/' ? '/' : path.endsWith('/') ? path.slice(0, -1) : path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析URL参数
|
||||||
|
function getUrlSearchParams() {
|
||||||
|
return new URLSearchParams(window.location.search);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取页面类型
|
||||||
|
function getPageType(path) {
|
||||||
|
if (path === '/filtered' || path.startsWith('/filtered')) {
|
||||||
|
return 'filter';
|
||||||
|
} else if (path.includes('/articles/') && !path.endsWith('/articles/')) {
|
||||||
|
// 检查是否是文章详情页
|
||||||
|
const segments = path.split('/').filter(s => s);
|
||||||
|
// 如果路径中包含.html或.md,则认为是文章详情页
|
||||||
|
if (segments.length > 1 && (segments[segments.length - 1].includes('.html') || segments[segments.length - 1].includes('.md'))) {
|
||||||
|
return 'article';
|
||||||
|
}
|
||||||
|
return 'grid'; // 默认为网格视图
|
||||||
|
} else {
|
||||||
|
return 'grid'; // 默认为网格视图
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取路径段
|
||||||
|
function getPathSegments(path) {
|
||||||
|
// 如果路径为空或根路径,直接返回空数组
|
||||||
|
if (!path || path === '/') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标准化路径:移除结尾的斜杠并确保开头有斜杠
|
||||||
|
let normalizedPath = path;
|
||||||
|
if (normalizedPath.endsWith('/')) {
|
||||||
|
normalizedPath = normalizedPath.slice(0, -1);
|
||||||
|
}
|
||||||
|
if (!normalizedPath.startsWith('/')) {
|
||||||
|
normalizedPath = '/' + normalizedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拆分路径为段落
|
||||||
|
const segments = normalizedPath.split('/').filter(s => s);
|
||||||
|
|
||||||
|
// 确定基础路径部分 - 在本应用中是 "articles"
|
||||||
|
const basePathSegment = 'articles';
|
||||||
|
|
||||||
|
// 移除基础路径部分(如果存在于路径的第一段)
|
||||||
|
const pathWithoutBase = segments[0] === basePathSegment
|
||||||
|
? segments.slice(1)
|
||||||
|
: segments;
|
||||||
|
|
||||||
|
// 移除尾部的文件名(如果存在)
|
||||||
|
let result = [...pathWithoutBase];
|
||||||
|
const lastSegment = result[result.length - 1];
|
||||||
|
if (lastSegment && (lastSegment.includes('.html') || lastSegment.includes('.md'))) {
|
||||||
|
result.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对每个段进行解码
|
||||||
|
return result.map(segment => decodeURIComponent(segment));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文章标题(对于文章页面)
|
||||||
|
function getArticleTitle(path) {
|
||||||
|
// 从路径中提取文件名
|
||||||
|
const segments = path.split('/');
|
||||||
|
const fileName = segments[segments.length - 1];
|
||||||
|
|
||||||
|
if (fileName && (fileName.includes('.html') || fileName.includes('.md'))) {
|
||||||
|
// 移除扩展名,将连字符替换为空格,首字母大写
|
||||||
|
let title = fileName.replace(/\.(html|md)$/, '')
|
||||||
|
.replace(/-/g, ' ')
|
||||||
|
.replace(/\b\w/g, c => c.toUpperCase());
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动态更新面包屑
|
||||||
|
function updateBreadcrumbs() {
|
||||||
|
// 获取当前路径和相关信息
|
||||||
|
const currentPath = getCurrentPath();
|
||||||
|
const pageType = getPageType(currentPath);
|
||||||
|
const searchParams = getUrlSearchParams();
|
||||||
|
const pathSegments = getPathSegments(currentPath);
|
||||||
|
const articleTitle = pageType === 'article' ? getArticleTitle(currentPath) : '';
|
||||||
|
|
||||||
|
// 获取面包屑容器
|
||||||
|
const breadcrumbContainer = document.querySelector('.flex.items-center.text-sm.overflow-hidden');
|
||||||
|
if (!breadcrumbContainer) {
|
||||||
|
console.warn('[面包屑]找不到面包屑容器,无法更新');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成路径面包屑HTML
|
||||||
|
let breadcrumbsHtml = `
|
||||||
|
<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>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 网格视图或文章详情中的目录路径
|
||||||
|
if (pageType === 'grid' || (pageType === 'article' && pathSegments.length > 0)) {
|
||||||
|
breadcrumbsHtml += `<div class="flex items-center overflow-hidden">
|
||||||
|
<span class="mx-2 text-secondary-300 dark:text-secondary-600 flex-shrink-0">/</span>`;
|
||||||
|
|
||||||
|
// 移动端使用智能截断
|
||||||
|
breadcrumbsHtml += `<div class="flex md:hidden items-center">`;
|
||||||
|
|
||||||
|
if (pathSegments.length > 2) {
|
||||||
|
// 第一个路径段
|
||||||
|
const firstSegment = pathSegments[0];
|
||||||
|
const firstPath = encodeURIComponent(pathSegments.slice(0, 1).join('/'));
|
||||||
|
|
||||||
|
breadcrumbsHtml += `
|
||||||
|
<a
|
||||||
|
href="/articles/${firstPath}/"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
${firstSegment}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<span class="mx-2 text-secondary-300 dark:text-secondary-600 flex-shrink-0">...</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 最后一个路径段
|
||||||
|
if (pathSegments.length > 1) {
|
||||||
|
const lastSegment = pathSegments[pathSegments.length - 1];
|
||||||
|
const lastPath = pathSegments.map(encodeURIComponent).join('/');
|
||||||
|
|
||||||
|
breadcrumbsHtml += `
|
||||||
|
<a
|
||||||
|
href="/articles/${lastPath}/"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
${lastSegment}
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果段落不多,则全部显示
|
||||||
|
breadcrumbsHtml += pathSegments.map((segment, index) => {
|
||||||
|
const segmentPath = pathSegments.slice(0, index + 1).map(encodeURIComponent).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/${segmentPath}/"
|
||||||
|
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]"
|
||||||
|
>
|
||||||
|
${segment}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
breadcrumbsHtml += `</div>`;
|
||||||
|
|
||||||
|
// 桌面端显示全部路径段
|
||||||
|
breadcrumbsHtml += `<div class="hidden md:flex items-center flex-wrap">`;
|
||||||
|
|
||||||
|
breadcrumbsHtml += pathSegments.map((segment, index) => {
|
||||||
|
const segmentPath = pathSegments.slice(0, index + 1).map(encodeURIComponent).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/${segmentPath}/"
|
||||||
|
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]"
|
||||||
|
>
|
||||||
|
${segment}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
breadcrumbsHtml += `</div></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 筛选视图中的搜索参数展示
|
||||||
|
if (pageType === 'filter' && searchParams.toString()) {
|
||||||
|
breadcrumbsHtml += `
|
||||||
|
<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]">
|
||||||
|
筛选
|
||||||
|
${searchParams.toString() ? '<span class="ml-1">- 搜索结果</span>' : ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文章标题 - 仅在文章详情页显示
|
||||||
|
if (pageType === 'article' && articleTitle) {
|
||||||
|
breadcrumbsHtml += `
|
||||||
|
<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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新面包屑容器内容
|
||||||
|
breadcrumbContainer.innerHTML = breadcrumbsHtml;
|
||||||
|
|
||||||
|
// 更新视图切换按钮
|
||||||
|
updateViewSwitchButtons(pageType, currentPath, searchParams);
|
||||||
|
|
||||||
|
// 更新返回按钮(对于文章页面)
|
||||||
|
if (pageType === 'article') {
|
||||||
|
updateBackButton(currentPath, pathSegments.join('/'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新视图切换按钮
|
||||||
|
function updateViewSwitchButtons(pageType, currentPath, searchParams) {
|
||||||
|
// 获取视图切换按钮容器
|
||||||
|
const viewSwitchContainer = document.querySelector('.flex.items-center.gap-px.flex-shrink-0.ml-auto');
|
||||||
|
if (!viewSwitchContainer || !(pageType === 'filter' || pageType === 'grid')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchParamsStr = searchParams.toString() ? `?${searchParams.toString()}` : '';
|
||||||
|
const pathStr = currentPath.includes('/articles/') ? currentPath.replace('/articles/', '') : '';
|
||||||
|
|
||||||
|
// 生成视图切换按钮HTML
|
||||||
|
const switchButtonsHtml = `
|
||||||
|
<a href="/filtered${searchParamsStr}"
|
||||||
|
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="${pathStr ? `/articles/${pathStr}/` : `/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>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 更新视图切换按钮容器内容
|
||||||
|
viewSwitchContainer.innerHTML = switchButtonsHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新返回按钮
|
||||||
|
function updateBackButton(currentPath, pathWithoutFile) {
|
||||||
|
// 获取视图切换按钮容器的父元素
|
||||||
|
const parentContainer = document.querySelector('.flex.items-center.justify-between.w-full.flex-wrap.sm\\:flex-nowrap');
|
||||||
|
if (!parentContainer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已有返回按钮容器
|
||||||
|
let backButtonContainer = document.querySelector('.flex.items-center.shrink-0.ml-auto');
|
||||||
|
|
||||||
|
// 如果没有返回按钮容器,创建一个
|
||||||
|
if (!backButtonContainer) {
|
||||||
|
backButtonContainer = document.createElement('div');
|
||||||
|
backButtonContainer.className = 'flex items-center shrink-0 ml-auto';
|
||||||
|
parentContainer.appendChild(backButtonContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保路径使用编码后的形式用于URL
|
||||||
|
const encodedPath = pathWithoutFile.split('/').map(encodeURIComponent).join('/');
|
||||||
|
|
||||||
|
// 生成返回按钮HTML
|
||||||
|
const backButtonHtml = `
|
||||||
|
<a
|
||||||
|
href="/articles/${encodedPath}/"
|
||||||
|
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/${encodedPath}/"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 更新返回按钮容器内容
|
||||||
|
backButtonContainer.innerHTML = backButtonHtml;
|
||||||
|
|
||||||
|
// 设置返回按钮功能
|
||||||
|
setupBackButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加路径变化检测和自动更新
|
||||||
|
function setupPathChangeDetection() {
|
||||||
|
let lastPathChecked = getCurrentPath();
|
||||||
|
|
||||||
|
// 统一的路径变化处理函数
|
||||||
|
function handlePathChange() {
|
||||||
|
const currentPath = getCurrentPath();
|
||||||
|
if (currentPath !== lastPathChecked) {
|
||||||
|
// 更新面包屑
|
||||||
|
updateBreadcrumbs();
|
||||||
|
|
||||||
|
// 更新记录的路径
|
||||||
|
lastPathChecked = currentPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听hashchange事件 - 当URL的hash部分改变时触发
|
||||||
|
addListener(window, 'hashchange', () => {
|
||||||
|
handlePathChange();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 为所有导航链接添加点击拦截
|
||||||
|
addListener(document, 'click', (e) => {
|
||||||
|
// 检查点击的是否为站内导航链接
|
||||||
|
const link = e.target.closest('a');
|
||||||
|
if (link && link.host === window.location.host && !e.ctrlKey && !e.metaKey) {
|
||||||
|
// 延迟检查以确保导航已完成
|
||||||
|
setTimeout(handlePathChange, 50);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听history API的方法
|
||||||
|
const originalPushState = window.history.pushState;
|
||||||
|
const originalReplaceState = window.history.replaceState;
|
||||||
|
|
||||||
|
// 重写pushState
|
||||||
|
window.history.pushState = function() {
|
||||||
|
originalPushState.apply(this, arguments);
|
||||||
|
handlePathChange();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重写replaceState
|
||||||
|
window.history.replaceState = function() {
|
||||||
|
originalReplaceState.apply(this, arguments);
|
||||||
|
handlePathChange();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加到清理列表
|
||||||
|
addListener(window, 'beforeunload', () => {
|
||||||
|
// 恢复原始history方法
|
||||||
|
window.history.pushState = originalPushState;
|
||||||
|
window.history.replaceState = originalReplaceState;
|
||||||
|
}, { once: true });
|
||||||
|
|
||||||
|
// 监听popstate事件
|
||||||
|
addListener(window, 'popstate', () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
handlePathChange();
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 注册清理事件,并保存引用
|
// 注册清理事件,并保存引用
|
||||||
function registerCleanupEvents() {
|
function registerCleanupEvents() {
|
||||||
// 创建一次性事件处理函数
|
// 创建一次性事件处理函数
|
||||||
@ -275,25 +656,33 @@ const breadcrumbs: Breadcrumb[] = pathSegments
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const clickHandler = (e) => {
|
// 获取当前URL信息
|
||||||
e.preventDefault();
|
const url = new URL(window.location.href);
|
||||||
|
const searchParams = url.search;
|
||||||
const url = new URL(window.location.href);
|
|
||||||
const searchParams = url.search;
|
|
||||||
|
|
||||||
// 检查URL中是否有查询参数
|
|
||||||
if (searchParams) {
|
|
||||||
// 有查询参数,返回筛选页面
|
|
||||||
window.location.href = `/filtered${searchParams}`;
|
|
||||||
} else {
|
|
||||||
// 没有查询参数,返回默认路径
|
|
||||||
const defaultPath = backButton.getAttribute('data-path') || '';
|
|
||||||
window.location.href = defaultPath;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 添加点击事件监听
|
// 根据是否有查询参数确定返回目标
|
||||||
addListener(backButton, 'click', clickHandler);
|
let targetHref;
|
||||||
|
if (searchParams) {
|
||||||
|
// 有查询参数,返回筛选页面
|
||||||
|
targetHref = `/filtered${searchParams}`;
|
||||||
|
} else {
|
||||||
|
// 没有查询参数,返回默认路径
|
||||||
|
targetHref = backButton.getAttribute('data-path') || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改返回按钮属性,使其成为swup可识别的链接
|
||||||
|
backButton.setAttribute('href', targetHref);
|
||||||
|
|
||||||
|
// 如果支持swup,确保swup能处理此链接
|
||||||
|
if (typeof window.swup !== 'undefined') {
|
||||||
|
// 移除可能阻止swup处理的属性
|
||||||
|
backButton.removeAttribute('data-no-swup');
|
||||||
|
|
||||||
|
// 确保链接有正确的prefetch属性
|
||||||
|
if (!backButton.hasAttribute('data-astro-prefetch')) {
|
||||||
|
backButton.setAttribute('data-astro-prefetch', 'hover');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 主初始化函数
|
// 主初始化函数
|
||||||
@ -301,21 +690,22 @@ const breadcrumbs: Breadcrumb[] = pathSegments
|
|||||||
// 注册清理事件
|
// 注册清理事件
|
||||||
registerCleanupEvents();
|
registerCleanupEvents();
|
||||||
|
|
||||||
|
// 设置路径变化检测
|
||||||
|
setupPathChangeDetection();
|
||||||
|
|
||||||
|
// 执行初始更新
|
||||||
|
updateBreadcrumbs();
|
||||||
|
|
||||||
// 设置返回按钮
|
// 设置返回按钮
|
||||||
setupBackButton();
|
setupBackButton();
|
||||||
|
|
||||||
// 注册页面加载后的处理函数 - 仅当使用View Transitions或Swup时
|
// 注册页面加载后的处理函数 - 仅当使用View Transitions或Swup时
|
||||||
if (typeof document.startViewTransition !== 'undefined' || typeof window.swup !== 'undefined') {
|
if (typeof document.startViewTransition !== 'undefined' || typeof window.swup !== 'undefined') {
|
||||||
// 仅监听一个事件,保持最小侵入性
|
// 监听页面加载事件
|
||||||
const pageLoadHandler = () => {
|
const pageLoadHandler = () => {
|
||||||
// 检查是否在文章页面
|
// 重新执行一次更新
|
||||||
const backButton = document.querySelector('.back-button');
|
updateBreadcrumbs();
|
||||||
if (backButton) {
|
setupBackButton();
|
||||||
// 先销毁已有的所有处理函数
|
|
||||||
selfDestruct();
|
|
||||||
// 重新初始化
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (typeof document.startViewTransition !== 'undefined') {
|
if (typeof document.startViewTransition !== 'undefined') {
|
||||||
|
@ -343,6 +343,11 @@ const navSelectorClassName = "mr-4";
|
|||||||
|
|
||||||
// 单独保存清理事件的监听器引用
|
// 单独保存清理事件的监听器引用
|
||||||
const cleanupListeners = [];
|
const cleanupListeners = [];
|
||||||
|
// 获取当前URL路径
|
||||||
|
function getCurrentPath() {
|
||||||
|
const path = window.location.pathname;
|
||||||
|
return path === '/' ? '/' : path.endsWith('/') ? path.slice(0, -1) : path;
|
||||||
|
}
|
||||||
|
|
||||||
// 添加事件监听器并记录,方便后续统一清理
|
// 添加事件监听器并记录,方便后续统一清理
|
||||||
function addListener(element, eventType, handler, options) {
|
function addListener(element, eventType, handler, options) {
|
||||||
@ -387,7 +392,6 @@ const navSelectorClassName = "mr-4";
|
|||||||
|
|
||||||
// 注册清理事件,并保存引用
|
// 注册清理事件,并保存引用
|
||||||
function registerCleanupEvents() {
|
function registerCleanupEvents() {
|
||||||
|
|
||||||
// 创建一次性事件处理函数
|
// 创建一次性事件处理函数
|
||||||
const beforeSwapHandler = () => {
|
const beforeSwapHandler = () => {
|
||||||
selfDestruct();
|
selfDestruct();
|
||||||
@ -629,6 +633,113 @@ const navSelectorClassName = "mr-4";
|
|||||||
// 注册清理事件
|
// 注册清理事件
|
||||||
registerCleanupEvents();
|
registerCleanupEvents();
|
||||||
|
|
||||||
|
// 更新高亮背景 - 提升到全局作用域
|
||||||
|
function updateHighlights(immediate = false) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const highlightPositions = calculateHighlightPositions({
|
||||||
|
navSelector,
|
||||||
|
immediate
|
||||||
|
});
|
||||||
|
|
||||||
|
if (highlightPositions) {
|
||||||
|
highlightPositions.applyPositions(immediate);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化当前页面的激活状态 - 提升到全局作用域
|
||||||
|
function initActiveState() {
|
||||||
|
// 获取当前路径
|
||||||
|
const currentPath = getCurrentPath();
|
||||||
|
|
||||||
|
// 先清除所有导航项的激活状态
|
||||||
|
const allNavItems = document.querySelectorAll('.nav-item, .nav-subitem');
|
||||||
|
allNavItems.forEach(item => {
|
||||||
|
item.classList.remove('active', 'font-semibold', 'text-primary-700', 'dark:text-primary-300');
|
||||||
|
item.classList.add('text-gray-600', 'dark:text-gray-300', 'hover:text-primary-600', 'dark:hover:text-primary-400');
|
||||||
|
});
|
||||||
|
|
||||||
|
const allNavGroups = document.querySelectorAll('.nav-group');
|
||||||
|
allNavGroups.forEach(group => {
|
||||||
|
group.classList.remove('active');
|
||||||
|
const toggle = group.querySelector('.nav-group-toggle');
|
||||||
|
if (toggle) {
|
||||||
|
toggle.classList.remove('menu-up', 'font-semibold', 'text-primary-700', 'dark:text-primary-300');
|
||||||
|
toggle.classList.add('text-gray-600', 'dark:text-gray-300', 'hover:text-primary-600', 'dark:hover:text-primary-400');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 标记变量,用于跟踪是否找到匹配的导航项
|
||||||
|
let foundMatch = false;
|
||||||
|
|
||||||
|
// 先检查子菜单项
|
||||||
|
const subItems = document.querySelectorAll('.nav-subitem');
|
||||||
|
for (const subItem of subItems) {
|
||||||
|
const href = subItem.getAttribute('href');
|
||||||
|
if (!href) continue;
|
||||||
|
|
||||||
|
// 检查是否匹配当前路径(完全匹配或前缀匹配,但排除根路径的前缀匹配)
|
||||||
|
if (href === currentPath || (currentPath.startsWith(href) && href !== '/')) {
|
||||||
|
// 找到匹配项,设置为active
|
||||||
|
subItem.classList.add('active', 'font-semibold', 'text-primary-700', 'dark:text-primary-300');
|
||||||
|
subItem.classList.remove('text-gray-600', 'dark:text-gray-300', 'hover:text-primary-600', 'dark:hover:text-primary-400');
|
||||||
|
|
||||||
|
// 激活父组
|
||||||
|
const parentId = subItem.dataset.parentId;
|
||||||
|
if (parentId) {
|
||||||
|
const parentGroup = document.querySelector(`.nav-group[data-group-id="${parentId}"]`);
|
||||||
|
if (parentGroup) {
|
||||||
|
parentGroup.classList.add('active');
|
||||||
|
|
||||||
|
// 设置组切换按钮样式
|
||||||
|
const toggle = parentGroup.querySelector('.nav-group-toggle');
|
||||||
|
if (toggle) {
|
||||||
|
toggle.classList.add('menu-up', 'font-semibold', 'text-primary-700', 'dark:text-primary-300');
|
||||||
|
toggle.classList.remove('text-gray-600', 'dark:text-gray-300', 'hover:text-primary-600', 'dark:hover:text-primary-400');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保子菜单可见
|
||||||
|
const items = parentGroup.querySelector('.nav-group-items');
|
||||||
|
if (items) {
|
||||||
|
items.classList.remove('hidden');
|
||||||
|
items.classList.add('menu-visible');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foundMatch = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没找到匹配的子菜单项,检查主菜单项
|
||||||
|
if (!foundMatch) {
|
||||||
|
const items = document.querySelectorAll('.nav-item');
|
||||||
|
for (const item of items) {
|
||||||
|
const href = item.getAttribute('href');
|
||||||
|
if (!href) continue;
|
||||||
|
|
||||||
|
// 检查是否匹配当前路径(完全匹配或前缀匹配,但排除根路径的前缀匹配)
|
||||||
|
if (href === currentPath || (currentPath.startsWith(href) && href !== '/')) {
|
||||||
|
// 找到匹配项,设置为active
|
||||||
|
item.classList.add('active', 'font-semibold', 'text-primary-700', 'dark:text-primary-300');
|
||||||
|
item.classList.remove('text-gray-600', 'dark:text-gray-300', 'hover:text-primary-600', 'dark:hover:text-primary-400');
|
||||||
|
|
||||||
|
foundMatch = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果仍然没找到匹配项,记录警告
|
||||||
|
if (!foundMatch) {
|
||||||
|
console.warn(`导航: 找不到与当前路径匹配的导航项: ${currentPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算正确高亮位置
|
||||||
|
updateHighlights(true);
|
||||||
|
}
|
||||||
|
|
||||||
// DOM加载完成后执行初始化
|
// DOM加载完成后执行初始化
|
||||||
function initNavigation() {
|
function initNavigation() {
|
||||||
// 检查DOM是否已加载
|
// 检查DOM是否已加载
|
||||||
@ -641,13 +752,64 @@ const navSelectorClassName = "mr-4";
|
|||||||
|
|
||||||
// 主要设置函数
|
// 主要设置函数
|
||||||
function setupNavigation() {
|
function setupNavigation() {
|
||||||
|
|
||||||
// 设置桌面导航
|
// 设置桌面导航
|
||||||
setupNavSelector();
|
setupNavSelector();
|
||||||
|
|
||||||
// 设置移动端导航
|
// 设置移动端导航
|
||||||
setupMobileNav();
|
setupMobileNav();
|
||||||
|
|
||||||
|
// 记录最后一次路径值
|
||||||
|
let lastPathLogged = getCurrentPath();
|
||||||
|
|
||||||
|
// 统一的路径变化处理函数
|
||||||
|
function handlePathChange() {
|
||||||
|
const currentPath = getCurrentPath();
|
||||||
|
if (currentPath !== lastPathLogged) {
|
||||||
|
// 主动调用初始化和更新高亮
|
||||||
|
initActiveState();
|
||||||
|
updateHighlights(true);
|
||||||
|
|
||||||
|
lastPathLogged = currentPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听hashchange事件 - 当URL的hash部分改变时触发
|
||||||
|
addListener(window, 'hashchange', () => {
|
||||||
|
handlePathChange();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 为所有导航链接添加点击拦截
|
||||||
|
addListener(document, 'click', (e) => {
|
||||||
|
// 检查点击的是否为站内导航链接
|
||||||
|
const link = e.target.closest('a');
|
||||||
|
if (link && link.host === window.location.host && !e.ctrlKey && !e.metaKey) {
|
||||||
|
// 延迟检查以确保导航已完成
|
||||||
|
setTimeout(handlePathChange, 50);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听history API的方法
|
||||||
|
const originalPushState = window.history.pushState;
|
||||||
|
const originalReplaceState = window.history.replaceState;
|
||||||
|
|
||||||
|
// 重写pushState
|
||||||
|
window.history.pushState = function() {
|
||||||
|
originalPushState.apply(this, arguments);
|
||||||
|
handlePathChange();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重写replaceState
|
||||||
|
window.history.replaceState = function() {
|
||||||
|
originalReplaceState.apply(this, arguments);
|
||||||
|
handlePathChange();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加到清理列表
|
||||||
|
addListener(window, 'beforeunload', () => {
|
||||||
|
// 恢复原始history方法
|
||||||
|
window.history.pushState = originalPushState;
|
||||||
|
window.history.replaceState = originalReplaceState;
|
||||||
|
}, { once: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化导航选择器
|
// 初始化导航选择器
|
||||||
@ -666,8 +828,6 @@ const navSelectorClassName = "mr-4";
|
|||||||
console.warn(`导航脚本未找到高亮元素,导航功能可能不完整`);
|
console.warn(`导航脚本未找到高亮元素,导航功能可能不完整`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取过渡动画持续时间
|
|
||||||
const transitionDuration = parseInt(navSelector.dataset.duration || 300);
|
|
||||||
const activeClass = "font-medium";
|
const activeClass = "font-medium";
|
||||||
|
|
||||||
// 当前活动项的状态 - 从DOM中提取当前激活状态
|
// 当前活动项的状态 - 从DOM中提取当前激活状态
|
||||||
@ -730,64 +890,7 @@ const navSelectorClassName = "mr-4";
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 兼容swup的导航方法
|
|
||||||
function navigateTo(href) {
|
|
||||||
// 如果使用swup
|
|
||||||
const hasSwup = typeof window.swup !== 'undefined';
|
|
||||||
|
|
||||||
if (hasSwup) {
|
|
||||||
try {
|
|
||||||
// 正确使用swup的API
|
|
||||||
window.swup.navigate(href);
|
|
||||||
return;
|
|
||||||
} catch (err) {
|
|
||||||
// 尝试使用swup的替代方法
|
|
||||||
try {
|
|
||||||
window.swup.loadPage({
|
|
||||||
url: href
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
} catch (err2) {
|
|
||||||
console.warn(`导航脚本Swup导航失败:`, err2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否有匹配的链接,使用其点击事件可能会触发注册在链接上的swup事件
|
|
||||||
const existingLink = document.querySelector(`a[href="${href}"]`);
|
|
||||||
if (existingLink) {
|
|
||||||
existingLink.click();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 以下是在swup不可用或出错时的回退方案
|
|
||||||
|
|
||||||
// 如果使用Astro的View Transitions
|
|
||||||
if (document.startViewTransition) {
|
|
||||||
document.startViewTransition(() => {
|
|
||||||
window.location.href = href;
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 最后的回退:普通导航
|
|
||||||
window.location.href = href;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新高亮背景
|
|
||||||
function updateHighlights(immediate = false) {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const highlightPositions = calculateHighlightPositions({
|
|
||||||
navSelector,
|
|
||||||
immediate
|
|
||||||
});
|
|
||||||
|
|
||||||
if (highlightPositions) {
|
|
||||||
highlightPositions.applyPositions(immediate);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置激活的一级菜单项
|
// 设置激活的一级菜单项
|
||||||
function setActiveItem(itemId) {
|
function setActiveItem(itemId) {
|
||||||
// 清除所有菜单状态
|
// 清除所有菜单状态
|
||||||
@ -1036,12 +1139,11 @@ const navSelectorClassName = "mr-4";
|
|||||||
// 设置激活状态
|
// 设置激活状态
|
||||||
activeGroupId = groupId;
|
activeGroupId = groupId;
|
||||||
|
|
||||||
// 如果没有选中的二级菜单项,则选中第一个并导航
|
// 如果没有选中的二级菜单项,则选中第一个并模拟点击
|
||||||
if (!activeSubItemId || !activeSubItemId.startsWith(groupId)) {
|
if (!activeSubItemId || !activeSubItemId.startsWith(groupId)) {
|
||||||
const firstSubItem = targetGroup.querySelector('.nav-subitem');
|
const firstSubItem = targetGroup.querySelector('.nav-subitem');
|
||||||
if (firstSubItem) {
|
if (firstSubItem) {
|
||||||
const subItemId = firstSubItem.dataset.subitemId;
|
const subItemId = firstSubItem.dataset.subitemId;
|
||||||
const href = firstSubItem.getAttribute('href');
|
|
||||||
|
|
||||||
// 清除所有二级菜单项的高亮
|
// 清除所有二级菜单项的高亮
|
||||||
navSubItems.forEach(item => {
|
navSubItems.forEach(item => {
|
||||||
@ -1058,10 +1160,10 @@ const navSelectorClassName = "mr-4";
|
|||||||
// 设置高亮
|
// 设置高亮
|
||||||
activeSubItemId = subItemId;
|
activeSubItemId = subItemId;
|
||||||
|
|
||||||
// 导航到第一个子项
|
// 直接模拟点击第一个子菜单项
|
||||||
if (href) {
|
setTimeout(() => {
|
||||||
navigateTo(href);
|
firstSubItem.click();
|
||||||
}
|
}, 10);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 已有选中的二级菜单项,找到并重新设置高亮
|
// 已有选中的二级菜单项,找到并重新设置高亮
|
||||||
@ -1086,38 +1188,6 @@ const navSelectorClassName = "mr-4";
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化当前页面的激活状态
|
|
||||||
function initActiveState() {
|
|
||||||
// 高亮背景已经在服务器端渲染时预设,现在需确保应用正确的文字颜色
|
|
||||||
|
|
||||||
// 查找当前活动项
|
|
||||||
const activeItemElement = document.querySelector('.nav-item.active');
|
|
||||||
const activeSubItemElement = document.querySelector('.nav-subitem.active');
|
|
||||||
const activeGroupElement = document.querySelector('.nav-group.active');
|
|
||||||
|
|
||||||
// 应用激活文字样式
|
|
||||||
if (activeItemElement) {
|
|
||||||
activeItemElement.classList.add('font-semibold', 'text-primary-700', 'dark:text-primary-300');
|
|
||||||
activeItemElement.classList.remove('text-gray-600', 'dark:text-gray-300', 'hover:text-primary-600', 'dark:hover:text-primary-400');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeSubItemElement) {
|
|
||||||
activeSubItemElement.classList.add('font-semibold', 'text-primary-700', 'dark:text-primary-300');
|
|
||||||
activeSubItemElement.classList.remove('text-gray-600', 'dark:text-gray-300', 'hover:text-primary-600', 'dark:hover:text-primary-400');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeGroupElement) {
|
|
||||||
const toggle = activeGroupElement.querySelector('.nav-group-toggle');
|
|
||||||
if (toggle) {
|
|
||||||
toggle.classList.add('font-semibold', 'text-primary-700', 'dark:text-primary-300');
|
|
||||||
toggle.classList.remove('text-gray-600', 'dark:text-gray-300', 'hover:text-primary-600', 'dark:hover:text-primary-400');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算正确高亮位置
|
|
||||||
updateHighlights(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 注册事件监听
|
// 注册事件监听
|
||||||
|
|
||||||
// 普通菜单项点击
|
// 普通菜单项点击
|
||||||
@ -1126,15 +1196,11 @@ const navSelectorClassName = "mr-4";
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const itemId = item.dataset.itemId;
|
const itemId = item.dataset.itemId;
|
||||||
const href = item.getAttribute('href');
|
|
||||||
|
|
||||||
// 设置高亮
|
// 设置高亮
|
||||||
setActiveItem(itemId);
|
setActiveItem(itemId);
|
||||||
|
|
||||||
// 导航到目标页面
|
|
||||||
if (href) {
|
|
||||||
navigateTo(href);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1157,15 +1223,11 @@ const navSelectorClassName = "mr-4";
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const subItemId = item.dataset.subitemId;
|
const subItemId = item.dataset.subitemId;
|
||||||
const href = item.getAttribute('href');
|
|
||||||
|
|
||||||
// 设置高亮
|
// 设置高亮
|
||||||
setActiveSubItem(subItemId);
|
setActiveSubItem(subItemId);
|
||||||
|
|
||||||
// 导航到目标页面
|
|
||||||
if (href) {
|
|
||||||
navigateTo(href);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1177,12 +1239,9 @@ const navSelectorClassName = "mr-4";
|
|||||||
if (hasSwup) {
|
if (hasSwup) {
|
||||||
// 页面内容替换后重新初始化
|
// 页面内容替换后重新初始化
|
||||||
addListener(document, 'swup:contentReplaced', () => {
|
addListener(document, 'swup:contentReplaced', () => {
|
||||||
setTimeout(initActiveState, 50);
|
setTimeout(() => {
|
||||||
});
|
initActiveState();
|
||||||
|
}, 50);
|
||||||
// 视图转换开始
|
|
||||||
addListener(document, 'swup:animationInStart', () => {
|
|
||||||
// 这里可以添加动画开始时的处理逻辑
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 视图转换结束
|
// 视图转换结束
|
||||||
@ -1194,23 +1253,33 @@ const navSelectorClassName = "mr-4";
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加 astro:after-swap 事件监听
|
||||||
|
addListener(document, 'astro:after-swap', () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
initActiveState();
|
||||||
|
updateHighlights(true);
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
|
||||||
// 添加Astro View Transitions事件监听
|
// 添加Astro View Transitions事件监听
|
||||||
addListener(document, 'astro:page-load', () => {
|
addListener(document, 'astro:page-load', () => {
|
||||||
setTimeout(initActiveState, 50);
|
setTimeout(initActiveState, 50);
|
||||||
updateHighlights(true);
|
updateHighlights(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 添加popstate事件监听
|
||||||
|
addListener(window, 'popstate', () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
initActiveState();
|
||||||
|
updateHighlights(true);
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// 窗口大小变化时重新计算高亮位置
|
// 窗口大小变化时重新计算高亮位置
|
||||||
addListener(window, 'resize', () => {
|
addListener(window, 'resize', () => {
|
||||||
updateHighlights(true);
|
updateHighlights(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 在document上添加自定义方法,方便外部调用,但使用弱引用避免内存泄漏
|
|
||||||
if (!document.resetNavigation) {
|
|
||||||
document.resetNavigation = function() {
|
|
||||||
initActiveState();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化移动端菜单和搜索功能
|
// 初始化移动端菜单和搜索功能
|
||||||
@ -1460,12 +1529,6 @@ const navSelectorClassName = "mr-4";
|
|||||||
setupMobileSubmenuToggles();
|
setupMobileSubmenuToggles();
|
||||||
updateMobileMenuHighlight();
|
updateMobileMenuHighlight();
|
||||||
|
|
||||||
// 在document上添加自定义方法,方便外部调用(可选)
|
|
||||||
document.updateMobileMenuHighlight = updateMobileMenuHighlight;
|
|
||||||
document.closeMobileMenu = closeMobileMenu;
|
|
||||||
document.closeMobileSearch = closeMobileSearch;
|
|
||||||
document.toggleMobileSubmenu = toggleSubmenu;
|
|
||||||
|
|
||||||
return updateMobileMenuHighlight;
|
return updateMobileMenuHighlight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,51 +92,99 @@ const {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 主题切换脚本 -->
|
<!-- 主题切换脚本 - 升级为自销毁模式 -->
|
||||||
<script is:inline>
|
<script is:inline>
|
||||||
// 立即执行主题初始化,采用"无闪烁"加载方式
|
// 立即执行主题初始化,采用"无闪烁"加载方式
|
||||||
(function () {
|
(function () {
|
||||||
// 存储事件监听器,便于清理
|
// 集中管理所有事件监听器
|
||||||
const listeners = [];
|
const allListeners = [];
|
||||||
|
|
||||||
|
// 单独保存清理事件的监听器引用
|
||||||
|
const cleanupListeners = [];
|
||||||
|
|
||||||
|
// 定时器引用
|
||||||
|
let themeUpdateTimeout = null;
|
||||||
|
|
||||||
// 添加事件监听器并记录,方便后续统一清理
|
// 添加事件监听器并记录,方便后续统一清理
|
||||||
function addListener(element, eventType, handler, options) {
|
function addListener(element, eventType, handler, options) {
|
||||||
if (!element) return null;
|
if (!element) {
|
||||||
|
console.error(`尝试为不存在的元素添加事件:`, eventType);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
element.addEventListener(eventType, handler, options);
|
element.addEventListener(eventType, handler, options);
|
||||||
listeners.push({ element, eventType, handler, options });
|
allListeners.push({ element, eventType, handler, options });
|
||||||
return handler;
|
return handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理函数 - 移除所有事件监听器
|
// 统一的清理函数,执行完整清理并自销毁
|
||||||
function cleanup() {
|
function selfDestruct() {
|
||||||
listeners.forEach(({ element, eventType, handler, options }) => {
|
// 1. 清理所有计时器
|
||||||
|
if (themeUpdateTimeout) {
|
||||||
|
clearTimeout(themeUpdateTimeout);
|
||||||
|
themeUpdateTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 移除普通事件监听器
|
||||||
|
allListeners.forEach(({ element, eventType, handler, options }) => {
|
||||||
try {
|
try {
|
||||||
element.removeEventListener(eventType, handler, options);
|
element.removeEventListener(eventType, handler, options);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// 忽略错误
|
console.error(`主题初始化移除事件监听器出错:`, err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 清空数组
|
// 清空监听器数组
|
||||||
listeners.length = 0;
|
allListeners.length = 0;
|
||||||
|
|
||||||
|
// 3. 最后移除清理事件监听器自身
|
||||||
|
cleanupListeners.forEach(({ element, eventType, handler, options }) => {
|
||||||
|
try {
|
||||||
|
element.removeEventListener(eventType, handler, options);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`主题初始化移除清理监听器出错:`, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 清空清理监听器数组
|
||||||
|
cleanupListeners.length = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注册清理函数 - 确保在页面转换前清理事件
|
// 注册清理事件,并保存引用
|
||||||
function registerCleanup() {
|
function registerCleanupEvents() {
|
||||||
const cleanupEvents = [
|
// 创建一次性事件处理函数
|
||||||
"astro:before-preparation",
|
const beforeSwapHandler = () => {
|
||||||
"astro:before-swap",
|
selfDestruct();
|
||||||
"swup:willReplaceContent"
|
};
|
||||||
];
|
|
||||||
|
const beforeUnloadHandler = () => {
|
||||||
// 为每个事件注册一次性清理函数
|
selfDestruct();
|
||||||
cleanupEvents.forEach((eventName) => {
|
};
|
||||||
document.addEventListener(eventName, cleanup, { once: true });
|
|
||||||
});
|
// 添加清理事件监听器并保存引用
|
||||||
|
document.addEventListener("astro:before-swap", beforeSwapHandler, { once: true });
|
||||||
// 页面卸载时清理
|
window.addEventListener("beforeunload", beforeUnloadHandler, { once: true });
|
||||||
window.addEventListener("beforeunload", cleanup, { once: true });
|
|
||||||
|
// Astro特有的页面准备事件
|
||||||
|
document.addEventListener("astro:before-preparation", beforeSwapHandler, { once: true });
|
||||||
|
|
||||||
|
// SPA框架可能使用的事件
|
||||||
|
if (typeof document.addEventListener === 'function') {
|
||||||
|
document.addEventListener("swup:willReplaceContent", beforeSwapHandler, { once: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存清理事件引用,用于完全销毁
|
||||||
|
cleanupListeners.push(
|
||||||
|
{ element: document, eventType: "astro:before-swap", handler: beforeSwapHandler, options: { once: true } },
|
||||||
|
{ element: window, eventType: "beforeunload", handler: beforeUnloadHandler, options: { once: true } },
|
||||||
|
{ element: document, eventType: "astro:before-preparation", handler: beforeSwapHandler, options: { once: true } }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (typeof document.addEventListener === 'function') {
|
||||||
|
cleanupListeners.push(
|
||||||
|
{ element: document, eventType: "swup:willReplaceContent", handler: beforeSwapHandler, options: { once: true } }
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -181,29 +229,55 @@ const {
|
|||||||
addListener(mediaQuery, "change", handleMediaChange);
|
addListener(mediaQuery, "change", handleMediaChange);
|
||||||
|
|
||||||
// 注册清理函数
|
// 注册清理函数
|
||||||
registerCleanup();
|
registerCleanupEvents();
|
||||||
|
|
||||||
// 监听页面转换事件,确保在页面转换后重新初始化
|
// 监听页面转换事件,确保在页面转换后重新初始化
|
||||||
function onPageTransition() {
|
function onPageTransition() {
|
||||||
// 重新初始化主题
|
// 防止重复执行,使用防抖
|
||||||
if (storedTheme) {
|
if (themeUpdateTimeout) {
|
||||||
document.documentElement.dataset.theme = storedTheme;
|
clearTimeout(themeUpdateTimeout);
|
||||||
} else {
|
|
||||||
const systemTheme = getSystemTheme();
|
|
||||||
document.documentElement.dataset.theme = systemTheme;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 使用微小延迟确保DOM完全就绪
|
||||||
|
themeUpdateTimeout = setTimeout(() => {
|
||||||
|
try {
|
||||||
|
// 重新初始化主题
|
||||||
|
const storedTheme = localStorage.getItem("theme");
|
||||||
|
if (storedTheme) {
|
||||||
|
document.documentElement.dataset.theme = storedTheme;
|
||||||
|
} else {
|
||||||
|
const systemTheme = getSystemTheme();
|
||||||
|
document.documentElement.dataset.theme = systemTheme;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("页面转换后主题更新出错:", err);
|
||||||
|
} finally {
|
||||||
|
themeUpdateTimeout = null;
|
||||||
|
}
|
||||||
|
}, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置页面转换事件监听
|
// 设置页面转换事件监听
|
||||||
document.addEventListener("astro:page-load", onPageTransition);
|
addListener(document, "astro:page-load", onPageTransition);
|
||||||
document.addEventListener("astro:after-swap", onPageTransition);
|
addListener(document, "astro:after-swap", onPageTransition);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error("主题初始化失败:", error);
|
||||||
// 出错时应用默认浅色主题,确保页面正常显示
|
// 出错时应用默认浅色主题,确保页面正常显示
|
||||||
document.documentElement.dataset.theme = "light";
|
document.documentElement.dataset.theme = "light";
|
||||||
|
// 即使出错也尝试注册清理事件
|
||||||
|
try {
|
||||||
|
registerCleanupEvents();
|
||||||
|
} catch (e) {
|
||||||
|
// 忽略清理注册错误
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import '../scripts/swup-init.js';
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body
|
<body
|
||||||
class="m-0 w-full h-full bg-gray-50 dark:bg-dark-bg flex flex-col min-h-screen"
|
class="m-0 w-full h-full bg-gray-50 dark:bg-dark-bg flex flex-col min-h-screen"
|
||||||
@ -219,37 +293,5 @@ const {
|
|||||||
psbIcpUrl={PSB_ICP_URL}
|
psbIcpUrl={PSB_ICP_URL}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 预获取脚本 -->
|
|
||||||
<script is:inline>
|
|
||||||
// 在DOM加载完成后执行
|
|
||||||
document.addEventListener("astro:page-load", () => {
|
|
||||||
// 获取所有视口预获取链接
|
|
||||||
const viewportLinks = document.querySelectorAll(
|
|
||||||
'[data-astro-prefetch="viewport"]',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (viewportLinks.length > 0) {
|
|
||||||
// 创建一个交叉观察器
|
|
||||||
const observer = new IntersectionObserver((entries) => {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
const link = entry.target;
|
|
||||||
// 进入视口时,添加data-astro-prefetch="true"属性触发预获取
|
|
||||||
if (link.getAttribute("data-astro-prefetch") === "viewport") {
|
|
||||||
link.setAttribute("data-astro-prefetch", "true");
|
|
||||||
}
|
|
||||||
// 一旦预获取,就不再观察这个链接
|
|
||||||
observer.unobserve(link);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 观察所有视口预获取链接
|
|
||||||
viewportLinks.forEach((link) => {
|
|
||||||
observer.observe(link);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -291,9 +291,9 @@ const tableOfContents = generateTableOfContents(headings);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 文章头部 -->
|
<!-- 文章头部 - 包含面包屑,保持不变 -->
|
||||||
<header class="mb-8">
|
<header class="mb-8">
|
||||||
<!-- 导航区域 -->
|
<!-- 导航区域/面包屑 - 不参与视图切换 -->
|
||||||
<div
|
<div
|
||||||
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"
|
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"
|
||||||
>
|
>
|
||||||
@ -312,6 +312,11 @@ const tableOfContents = generateTableOfContents(headings);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 文章过期提醒 - 放入article-content容器内 -->
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 文章内容区域 - 只有这部分参与视图切换 -->
|
||||||
|
<div id="article-content">
|
||||||
<!-- 文章过期提醒 -->
|
<!-- 文章过期提醒 -->
|
||||||
{
|
{
|
||||||
(() => {
|
(() => {
|
||||||
@ -426,14 +431,103 @@ const tableOfContents = generateTableOfContents(headings);
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- 文章内容 -->
|
<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-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"
|
||||||
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>
|
|
||||||
|
<!-- 相关文章 -->
|
||||||
|
{
|
||||||
|
relatedArticles.length > 0 && (
|
||||||
|
<aside 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"
|
||||||
|
? "同类文章"
|
||||||
|
: "推荐阅读"}
|
||||||
|
</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
{relatedArticles.map((relatedArticle: ArticleEntry) => (
|
||||||
|
<a
|
||||||
|
href={getArticleUrl(relatedArticle.id)}
|
||||||
|
class="article-card"
|
||||||
|
data-astro-prefetch="viewport"
|
||||||
|
>
|
||||||
|
<div class="article-card-content">
|
||||||
|
<div class="article-card-icon">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-5 w-5"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="article-card-body">
|
||||||
|
<h3 class="article-card-title">
|
||||||
|
{relatedArticle.data.title}
|
||||||
|
</h3>
|
||||||
|
<p class="article-card-date">
|
||||||
|
{relatedArticle.data.date.toLocaleDateString("zh-CN")}
|
||||||
|
</p>
|
||||||
|
{relatedArticle.data.summary && (
|
||||||
|
<p class="article-card-summary">
|
||||||
|
{relatedArticle.data.summary}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div class="article-card-footer">
|
||||||
|
<time
|
||||||
|
datetime={relatedArticle.data.date.toISOString()}
|
||||||
|
class="article-card-date"
|
||||||
|
>
|
||||||
|
{relatedArticle.data.date.toLocaleDateString("zh-CN", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
})}
|
||||||
|
</time>
|
||||||
|
<span class="article-card-read-more">阅读全文</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- 返回顶部按钮 -->
|
||||||
|
<button
|
||||||
|
id="back-to-top"
|
||||||
|
class="fixed bottom-8 right-8 w-12 h-12 rounded-full bg-primary-500 dark:bg-primary-600 text-white shadow-md flex items-center justify-center opacity-0 invisible translate-y-5 hover:bg-primary-600 dark:hover:bg-primary-700"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-6 w-6"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M5 10l7-7m0 0l7 7m-7-7v18"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 目录 -->
|
<!-- 目录 -->
|
||||||
<section
|
<section
|
||||||
@ -471,96 +565,6 @@ const tableOfContents = generateTableOfContents(headings);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 相关文章 -->
|
|
||||||
{
|
|
||||||
relatedArticles.length > 0 && (
|
|
||||||
<aside 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"
|
|
||||||
? "同类文章"
|
|
||||||
: "推荐阅读"}
|
|
||||||
</h2>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
||||||
{relatedArticles.map((relatedArticle: ArticleEntry) => (
|
|
||||||
<a
|
|
||||||
href={getArticleUrl(relatedArticle.id)}
|
|
||||||
class="article-card"
|
|
||||||
data-astro-prefetch="viewport"
|
|
||||||
>
|
|
||||||
<div class="article-card-content">
|
|
||||||
<div class="article-card-icon">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-5 w-5"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="article-card-body">
|
|
||||||
<h3 class="article-card-title">
|
|
||||||
{relatedArticle.data.title}
|
|
||||||
</h3>
|
|
||||||
<p class="article-card-date">
|
|
||||||
{relatedArticle.data.date.toLocaleDateString("zh-CN")}
|
|
||||||
</p>
|
|
||||||
{relatedArticle.data.summary && (
|
|
||||||
<p class="article-card-summary">
|
|
||||||
{relatedArticle.data.summary}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<div class="article-card-footer">
|
|
||||||
<time
|
|
||||||
datetime={relatedArticle.data.date.toISOString()}
|
|
||||||
class="article-card-date"
|
|
||||||
>
|
|
||||||
{relatedArticle.data.date.toLocaleDateString("zh-CN", {
|
|
||||||
year: "numeric",
|
|
||||||
month: "long",
|
|
||||||
day: "numeric",
|
|
||||||
})}
|
|
||||||
</time>
|
|
||||||
<span class="article-card-read-more">阅读全文</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- 返回顶部按钮 -->
|
|
||||||
<button
|
|
||||||
id="back-to-top"
|
|
||||||
class="fixed bottom-8 right-8 w-12 h-12 rounded-full bg-primary-500 dark:bg-primary-600 text-white shadow-md flex items-center justify-center opacity-0 invisible translate-y-5 hover:bg-primary-600 dark:hover:bg-primary-700"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-6 w-6"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M5 10l7-7m0 0l7 7m-7-7v18"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- 文章页面脚本 -->
|
<!-- 文章页面脚本 -->
|
||||||
<script is:inline>
|
<script is:inline>
|
||||||
|
@ -74,7 +74,7 @@ const pageTitle = currentPath ? currentPath : '文章列表';
|
|||||||
<slot name="head" slot="head" />
|
<slot name="head" slot="head" />
|
||||||
|
|
||||||
<div class="py-6 w-full">
|
<div class="py-6 w-full">
|
||||||
<!-- 导航栏 -->
|
<!-- 导航栏/面包屑 - 保持不变 -->
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-xl mb-4 shadow-lg border border-gray-200 dark:border-gray-700">
|
<div class="bg-white dark:bg-gray-800 rounded-xl mb-4 shadow-lg border border-gray-200 dark:border-gray-700">
|
||||||
<div class="px-4 py-3">
|
<div class="px-4 py-3">
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
@ -85,149 +85,151 @@ const pageTitle = currentPath ? currentPath : '文章列表';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 内容卡片网格 -->
|
<!-- 内容区域 - 只有这部分参与视图切换 -->
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-12">
|
<div id="article-content">
|
||||||
{/* 上一级目录卡片 - 仅在浏览目录时显示 */}
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-12">
|
||||||
{pathSegments.length > 0 && (
|
{/* 上一级目录卡片 - 仅在浏览目录时显示 */}
|
||||||
<a href={`/articles/${pathSegments.length > 1 ? pathSegments.slice(0, -1).join('/') : ''}/`}
|
{pathSegments.length > 0 && (
|
||||||
class="group flex flex-col h-full p-5 border border-gray-200 dark:border-gray-700 rounded-xl bg-white dark:bg-gray-800 hover:shadow-xl hover:-translate-y-1 shadow-lg"
|
<a href={`/articles/${pathSegments.length > 1 ? pathSegments.slice(0, -1).join('/') : ''}`}
|
||||||
data-astro-prefetch="hover">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="w-10 h-10 flex items-center justify-center rounded-lg bg-primary-100 text-primary-600 group-hover:bg-primary-200">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 17l-5-5m0 0l5-5m-5 5h12" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="ml-3 flex-1">
|
|
||||||
<div class="font-bold text-base text-gray-800 dark:text-gray-100 group-hover:text-primary-700 dark:group-hover:text-primary-300">返回上级目录</div>
|
|
||||||
<div class="text-xs text-gray-500">返回上一级</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-primary-500 opacity-0 group-hover:opacity-100">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 目录卡片 */}
|
|
||||||
{currentSections.map(section => {
|
|
||||||
// 确保目录链接正确生成
|
|
||||||
const dirLink = currentPath ? `${currentPath}/${section.name}` : section.name;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<a href={`/articles/${dirLink}/`}
|
|
||||||
class="group flex flex-col h-full p-5 border border-gray-200 dark:border-gray-700 rounded-xl bg-white dark:bg-gray-800 hover:shadow-xl hover:-translate-y-1 shadow-lg"
|
class="group flex flex-col h-full p-5 border border-gray-200 dark:border-gray-700 rounded-xl bg-white dark:bg-gray-800 hover:shadow-xl hover:-translate-y-1 shadow-lg"
|
||||||
data-astro-prefetch="viewport">
|
data-astro-prefetch="hover">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="w-10 h-10 flex items-center justify-center rounded-lg bg-primary-100 text-primary-600 group-hover:bg-primary-200">
|
<div class="w-10 h-10 flex items-center justify-center rounded-lg bg-primary-100 text-primary-600 group-hover:bg-primary-200">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 19a2 2 0 01-2-2V7a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1M5 19h14a2 2 0 002-2v-5a2 2 0 00-2-2H9a2 2 0 00-2 2v5a2 2 0 01-2 2z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 17l-5-5m0 0l5-5m-5 5h12" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-3 flex-1">
|
<div class="ml-3 flex-1">
|
||||||
<div class="font-bold text-base text-gray-800 dark:text-gray-100 group-hover:text-primary-700 dark:group-hover:text-primary-300 line-clamp-1">{section.name}</div>
|
<div class="font-bold text-base text-gray-800 dark:text-gray-100 group-hover:text-primary-700 dark:group-hover:text-primary-300">返回上级目录</div>
|
||||||
<div class="text-xs text-gray-500 flex items-center mt-1">
|
<div class="text-xs text-gray-500">返回上一级</div>
|
||||||
{section.sections.length > 0 && (
|
|
||||||
<span class="flex items-center mr-3">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
|
||||||
</svg>
|
|
||||||
{section.sections.length} 个子目录
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{section.articles.length > 0 && (
|
|
||||||
<span class="flex items-center">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
||||||
</svg>
|
|
||||||
{section.articles.length} 篇文章
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="text-primary-500 opacity-0 group-hover:opacity-100">
|
<div class="text-primary-500 opacity-0 group-hover:opacity-100">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
);
|
)}
|
||||||
})}
|
|
||||||
|
|
||||||
{/* 文章卡片 */}
|
|
||||||
{currentArticles.map(articlePath => {
|
|
||||||
// 获取文章ID - 不需要移除src/content前缀,因为contentStructure中已经是相对路径
|
|
||||||
const articleId = articlePath;
|
|
||||||
|
|
||||||
// 尝试匹配文章
|
{/* 目录卡片 */}
|
||||||
const article = articles.find(a => a.id === articleId);
|
{currentSections.map(section => {
|
||||||
|
// 确保目录链接正确生成
|
||||||
if (!article) {
|
const dirLink = currentPath ? `${currentPath}/${section.name}` : section.name;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col h-full p-5 border border-red-200 rounded-xl bg-red-50 shadow-lg">
|
<a href={`/articles/${dirLink}/`}
|
||||||
<div class="flex items-start">
|
class="group flex flex-col h-full p-5 border border-gray-200 dark:border-gray-700 rounded-xl bg-white dark:bg-gray-800 hover:shadow-xl hover:-translate-y-1 shadow-lg"
|
||||||
<div class="w-10 h-10 flex-shrink-0 flex items-center justify-center rounded-lg bg-red-100 text-red-600">
|
data-astro-prefetch="viewport">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-10 h-10 flex items-center justify-center rounded-lg bg-primary-100 text-primary-600 group-hover:bg-primary-200">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 19a2 2 0 01-2-2V7a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1M5 19h14a2 2 0 002-2v-5a2 2 0 00-2-2H9a2 2 0 00-2 2v5a2 2 0 01-2 2z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-3 flex-1">
|
<div class="ml-3 flex-1">
|
||||||
<h3 class="font-bold text-base text-red-800">文章不存在</h3>
|
<div class="font-bold text-base text-gray-800 dark:text-gray-100 group-hover:text-primary-700 dark:group-hover:text-primary-300 line-clamp-1">{section.name}</div>
|
||||||
<p class="text-xs text-red-600 mt-1">
|
<div class="text-xs text-gray-500 flex items-center mt-1">
|
||||||
<div>原始路径: {articlePath}</div>
|
{section.sections.length > 0 && (
|
||||||
<div>文章ID: {articleId}</div>
|
<span class="flex items-center mr-3">
|
||||||
<div>当前目录: {currentPath}</div>
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
</p>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||||
</div>
|
</svg>
|
||||||
</div>
|
{section.sections.length} 个子目录
|
||||||
</div>
|
</span>
|
||||||
);
|
)}
|
||||||
}
|
{section.articles.length > 0 && (
|
||||||
|
<span class="flex items-center">
|
||||||
return (
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<div class="article-card">
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
<a href={`/articles/${article.id}`}
|
</svg>
|
||||||
class="article-card-link"
|
{section.articles.length} 篇文章
|
||||||
data-astro-prefetch="viewport">
|
</span>
|
||||||
<div class="article-card-content">
|
)}
|
||||||
<div class="article-card-icon">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="article-card-body">
|
|
||||||
<h3 class="article-card-title">{article.data.title}</h3>
|
|
||||||
{article.body && (
|
|
||||||
<p class="article-card-summary">
|
|
||||||
{article.data.summary}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<div class="article-card-footer">
|
|
||||||
<time datetime={article.data.date.toISOString()} class="article-card-date">
|
|
||||||
{article.data.date.toLocaleDateString('zh-CN', {year: 'numeric', month: 'long', day: 'numeric'})}
|
|
||||||
</time>
|
|
||||||
<span class="article-card-read-more">阅读全文</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="text-primary-500 opacity-0 group-hover:opacity-100">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
|
||||||
</div>
|
{/* 文章卡片 */}
|
||||||
|
{currentArticles.map(articlePath => {
|
||||||
{/* 空内容提示 */}
|
// 获取文章ID - 不需要移除src/content前缀,因为contentStructure中已经是相对路径
|
||||||
{(currentSections.length === 0 && currentArticles.length === 0) && (
|
const articleId = articlePath;
|
||||||
<div class="text-center py-16 bg-white rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 mb-12">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mx-auto text-primary-200 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
// 尝试匹配文章
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
const article = articles.find(a => a.id === articleId);
|
||||||
</svg>
|
|
||||||
<h3 class="text-2xl font-bold text-gray-700 mb-2">此目录为空</h3>
|
if (!article) {
|
||||||
<p class="text-gray-500 max-w-md mx-auto">此目录下暂无内容,请浏览其他目录或返回上一级</p>
|
return (
|
||||||
|
<div class="flex flex-col h-full p-5 border border-red-200 rounded-xl bg-red-50 shadow-lg">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<div class="w-10 h-10 flex-shrink-0 flex items-center justify-center rounded-lg bg-red-100 text-red-600">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 flex-1">
|
||||||
|
<h3 class="font-bold text-base text-red-800">文章不存在</h3>
|
||||||
|
<p class="text-xs text-red-600 mt-1">
|
||||||
|
<div>原始路径: {articlePath}</div>
|
||||||
|
<div>文章ID: {articleId}</div>
|
||||||
|
<div>当前目录: {currentPath}</div>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="article-card">
|
||||||
|
<a href={`/articles/${article.id}`}
|
||||||
|
class="article-card-link"
|
||||||
|
data-astro-prefetch="viewport">
|
||||||
|
<div class="article-card-content">
|
||||||
|
<div class="article-card-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="article-card-body">
|
||||||
|
<h3 class="article-card-title">{article.data.title}</h3>
|
||||||
|
{article.body && (
|
||||||
|
<p class="article-card-summary">
|
||||||
|
{article.data.summary}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div class="article-card-footer">
|
||||||
|
<time datetime={article.data.date.toISOString()} class="article-card-date">
|
||||||
|
{article.data.date.toLocaleDateString('zh-CN', {year: 'numeric', month: 'long', day: 'numeric'})}
|
||||||
|
</time>
|
||||||
|
<span class="article-card-read-more">阅读全文</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
{/* 空内容提示 */}
|
||||||
|
{(currentSections.length === 0 && currentArticles.length === 0) && (
|
||||||
|
<div class="text-center py-16 bg-white rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 mb-12">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mx-auto text-primary-200 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
||||||
|
</svg>
|
||||||
|
<h3 class="text-2xl font-bold text-gray-700 mb-2">此目录为空</h3>
|
||||||
|
<p class="text-gray-500 max-w-md mx-auto">此目录下暂无内容,请浏览其他目录或返回上一级</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
@ -9,7 +9,7 @@ const searchParams = Astro.url.searchParams;
|
|||||||
|
|
||||||
<Layout title="文章筛选">
|
<Layout title="文章筛选">
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<!-- 导航栏 -->
|
<!-- 导航栏/面包屑 - 不参与视图切换 -->
|
||||||
<div
|
<div
|
||||||
class="bg-white dark:bg-gray-800 rounded-xl mb-4 shadow-lg border border-gray-200 dark:border-gray-700"
|
class="bg-white dark:bg-gray-800 rounded-xl mb-4 shadow-lg border border-gray-200 dark:border-gray-700"
|
||||||
>
|
>
|
||||||
@ -21,11 +21,13 @@ const searchParams = Astro.url.searchParams;
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 使用ArticleFilter组件 -->
|
<!-- 内容区域 - 只有这部分参与视图切换 -->
|
||||||
<ArticleFilter
|
<div id="article-content">
|
||||||
searchParams={searchParams}
|
<ArticleFilter
|
||||||
client:load
|
searchParams={searchParams}
|
||||||
/>
|
client:load
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
|
@ -22,10 +22,16 @@ export function rehypeCodeBlocks() {
|
|||||||
className => typeof className === 'string' && className.startsWith('language-')
|
className => typeof className === 'string' && className.startsWith('language-')
|
||||||
);
|
);
|
||||||
|
|
||||||
// 提取语言标识
|
// 从父节点获取 Shiki 设置的语言标识(dataLanguage 属性)
|
||||||
|
let shikiLanguage = '';
|
||||||
|
if (node.properties && node.properties.dataLanguage) {
|
||||||
|
shikiLanguage = node.properties.dataLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取语言标识 - 优先使用 language 类,其次使用 Shiki 语言标识
|
||||||
const language = languageClass
|
const language = languageClass
|
||||||
? languageClass.split('-')[1].toUpperCase()
|
? languageClass.split('-')[1].toUpperCase()
|
||||||
: 'TEXT';
|
: (shikiLanguage ? shikiLanguage.toUpperCase() : 'TEXT');
|
||||||
|
|
||||||
// 跳过处理 mermaid 图表
|
// 跳过处理 mermaid 图表
|
||||||
if (language === 'MERMAID') {
|
if (language === 'MERMAID') {
|
||||||
|
325
src/scripts/swup-init.js
Normal file
325
src/scripts/swup-init.js
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
// 统一初始化Swup和所有插件
|
||||||
|
import Swup from 'swup';
|
||||||
|
import SwupFragmentPlugin from '@swup/fragment-plugin';
|
||||||
|
// 添加Head插件解决CSS丢失问题
|
||||||
|
import SwupHeadPlugin from '@swup/head-plugin';
|
||||||
|
// 添加预加载插件 - 优化导航体验
|
||||||
|
import SwupPreloadPlugin from '@swup/preload-plugin';
|
||||||
|
|
||||||
|
// 检查是否是文章相关页面
|
||||||
|
function isArticlePage() {
|
||||||
|
const path = window.location.pathname;
|
||||||
|
return path.includes('/articles') || path.includes('/filtered');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为元素应用动画样式
|
||||||
|
function applyAnimationStyles(element, className, duration = 300) {
|
||||||
|
if (!element) return;
|
||||||
|
|
||||||
|
// 添加动画类
|
||||||
|
element.classList.add(className);
|
||||||
|
|
||||||
|
// 设置过渡属性
|
||||||
|
element.style.transition = 'opacity 0.3s ease';
|
||||||
|
element.style.animationDuration = '0.3s';
|
||||||
|
element.style.opacity = '1';
|
||||||
|
|
||||||
|
// 添加data-swup属性标记
|
||||||
|
element.setAttribute('data-swup-transition', 'true');
|
||||||
|
element.setAttribute('data-swup-animation-duration', duration.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置元素淡入/淡出效果
|
||||||
|
function setElementOpacity(element, opacity) {
|
||||||
|
if (!element) return;
|
||||||
|
element.style.opacity = opacity.toString();
|
||||||
|
if (opacity === 0) {
|
||||||
|
element.style.transition = 'opacity 0.3s ease';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接应用样式到元素上
|
||||||
|
function applyStylesDirectly() {
|
||||||
|
// 应用到主容器 - 只在非文章页面
|
||||||
|
const mainElement = document.querySelector('main');
|
||||||
|
if (mainElement) {
|
||||||
|
mainElement.classList.add('transition-fade');
|
||||||
|
|
||||||
|
// 只有在非文章页面时,才为main添加必要的动画样式
|
||||||
|
if (!isArticlePage()) {
|
||||||
|
applyAnimationStyles(mainElement, 'transition-fade');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用到文章内容 - 只在文章页面
|
||||||
|
const articleContent = document.querySelector('#article-content');
|
||||||
|
if (articleContent) {
|
||||||
|
applyAnimationStyles(articleContent, 'swup-transition-article');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前页面的活跃元素(用于动画)
|
||||||
|
function getActiveElement() {
|
||||||
|
if (isArticlePage()) {
|
||||||
|
return document.querySelector('#article-content');
|
||||||
|
} else {
|
||||||
|
return document.querySelector('main');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在DOM加载完成后初始化
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// 直接应用样式
|
||||||
|
applyStylesDirectly();
|
||||||
|
|
||||||
|
// 创建Swup实例
|
||||||
|
const swup = new Swup({
|
||||||
|
// Swup的基本配置
|
||||||
|
animationSelector: '[class*="transition-"], #article-content, .swup-transition-article, main',
|
||||||
|
cache: true,
|
||||||
|
containers: ['main'],
|
||||||
|
animationScope: 'html', // 确保动画状态类添加到html元素
|
||||||
|
linkSelector: 'a[href^="/"]:not([data-no-swup]), a[href^="' + window.location.origin + '"]:not([data-no-swup])',
|
||||||
|
skipPopStateHandling: (event) => {
|
||||||
|
return event.state && event.state.source === 'swup';
|
||||||
|
},
|
||||||
|
plugins: [] // 手动添加插件以控制顺序
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加预加载插件 - 代替原有的预加载功能
|
||||||
|
const preloadPlugin = new SwupPreloadPlugin({
|
||||||
|
// 最多同时预加载5个链接
|
||||||
|
throttle: 5,
|
||||||
|
// 开启鼠标悬停预加载
|
||||||
|
preloadHoveredLinks: true,
|
||||||
|
// 开启视口内链接预加载,自定义配置
|
||||||
|
preloadVisibleLinks: {
|
||||||
|
// 链接可见面积达到30%时预加载
|
||||||
|
threshold: 0.3,
|
||||||
|
// 链接可见500毫秒后开始预加载
|
||||||
|
delay: 500,
|
||||||
|
// 在哪些容器内寻找链接
|
||||||
|
containers: ['body'],
|
||||||
|
// 忽略带有data-no-preload属性的链接
|
||||||
|
ignore: (el) => el.hasAttribute('data-no-preload')
|
||||||
|
},
|
||||||
|
// 预加载初始页面,以便"后退"导航更快
|
||||||
|
preloadInitialPage: true
|
||||||
|
});
|
||||||
|
swup.use(preloadPlugin);
|
||||||
|
|
||||||
|
// 创建并注册Head插件,用于解决CSS丢失问题
|
||||||
|
const headPlugin = new SwupHeadPlugin();
|
||||||
|
swup.use(headPlugin);
|
||||||
|
|
||||||
|
// 创建Fragment插件 - 简化规则避免匹配问题
|
||||||
|
const fragmentPlugin = new SwupFragmentPlugin({
|
||||||
|
debug: false, // 关闭调试模式
|
||||||
|
// 简化规则,确保基本匹配
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
// 文章页面之间的导航
|
||||||
|
name: 'article-pages',
|
||||||
|
from: '/articles', // 简化匹配规则
|
||||||
|
to: '/articles',
|
||||||
|
containers: ['#article-content']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// 从文章到筛选页面
|
||||||
|
name: 'article-to-filter',
|
||||||
|
from: '/articles',
|
||||||
|
to: '/filtered',
|
||||||
|
containers: ['#article-content']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// 从筛选到文章页面
|
||||||
|
name: 'filter-to-article',
|
||||||
|
from: '/filtered',
|
||||||
|
to: '/articles',
|
||||||
|
containers: ['#article-content']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// 筛选页面内部导航
|
||||||
|
name: 'filter-pages',
|
||||||
|
from: '/filtered',
|
||||||
|
to: '/filtered',
|
||||||
|
containers: ['#article-content']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加Fragment插件到Swup
|
||||||
|
swup.use(fragmentPlugin);
|
||||||
|
|
||||||
|
// 初始化后手动扫描并预加载带有data-swup-preload属性的链接
|
||||||
|
setTimeout(() => {
|
||||||
|
swup.preloadLinks();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// 强制应用动画样式到特定元素
|
||||||
|
function setupTransition() {
|
||||||
|
// 直接应用样式 - 会根据页面类型自动选择正确的元素
|
||||||
|
applyStylesDirectly();
|
||||||
|
|
||||||
|
// 确保初始状态正确
|
||||||
|
setTimeout(() => {
|
||||||
|
// 获取并设置当前活跃元素的不透明度
|
||||||
|
const activeElement = getActiveElement();
|
||||||
|
if (activeElement) {
|
||||||
|
activeElement.style.opacity = '1';
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化时设置
|
||||||
|
setupTransition();
|
||||||
|
|
||||||
|
// 在页面内容加载后重新应用样式
|
||||||
|
swup.hooks.on('content:replace', () => {
|
||||||
|
// 重新设置过渡样式
|
||||||
|
setTimeout(() => {
|
||||||
|
setupTransition();
|
||||||
|
}, 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听动画开始和结束
|
||||||
|
swup.hooks.on('animation:out:start', () => {
|
||||||
|
// 获取并淡出当前活跃元素
|
||||||
|
const activeElement = getActiveElement();
|
||||||
|
setElementOpacity(activeElement, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
swup.hooks.on('animation:in:start', () => {
|
||||||
|
// 等待短暂延迟后恢复可见度
|
||||||
|
setTimeout(() => {
|
||||||
|
// 获取并淡入当前活跃元素
|
||||||
|
const activeElement = getActiveElement();
|
||||||
|
setElementOpacity(activeElement, 1);
|
||||||
|
}, 50); // 短暂延迟确保可以看到效果
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加手动强制动画事件
|
||||||
|
document.addEventListener('swup:willReplaceContent', () => {
|
||||||
|
// 获取并淡出当前活跃元素
|
||||||
|
const activeElement = getActiveElement();
|
||||||
|
setElementOpacity(activeElement, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 在页面内容替换后强制应用动画
|
||||||
|
document.addEventListener('swup:contentReplaced', () => {
|
||||||
|
// 获取活跃元素
|
||||||
|
const activeElement = getActiveElement();
|
||||||
|
if (!activeElement) return;
|
||||||
|
|
||||||
|
// 先设置透明
|
||||||
|
setElementOpacity(activeElement, 0);
|
||||||
|
|
||||||
|
// 重新应用适当的类和属性
|
||||||
|
if (isArticlePage() && activeElement.id === 'article-content') {
|
||||||
|
applyAnimationStyles(activeElement, 'swup-transition-article');
|
||||||
|
} else if (!isArticlePage() && activeElement.tagName.toLowerCase() === 'main') {
|
||||||
|
applyAnimationStyles(activeElement, 'transition-fade');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 延迟后淡入
|
||||||
|
setTimeout(() => {
|
||||||
|
setElementOpacity(activeElement, 1);
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听URL变化以更新动画行为
|
||||||
|
swup.hooks.on('visit:start', (visit) => {
|
||||||
|
// 检查目标URL是否为文章相关页面
|
||||||
|
const isTargetArticlePage = visit.to.url.includes('/articles') || visit.to.url.includes('/filtered');
|
||||||
|
const isCurrentArticlePage = isArticlePage();
|
||||||
|
|
||||||
|
// 如果当前是文章页面,但目标不是,恢复main动画
|
||||||
|
if (isCurrentArticlePage && !isTargetArticlePage) {
|
||||||
|
const mainElement = document.querySelector('main');
|
||||||
|
if (mainElement) {
|
||||||
|
setElementOpacity(mainElement, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 如果当前不是文章页面,但目标是,准备article-content动画
|
||||||
|
else if (!isCurrentArticlePage && isTargetArticlePage) {
|
||||||
|
const mainElement = document.querySelector('main');
|
||||||
|
if (mainElement) {
|
||||||
|
// 移除main的动画效果
|
||||||
|
mainElement.style.transition = '';
|
||||||
|
mainElement.style.opacity = '1';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fragment导航后手动更新面包屑
|
||||||
|
function updateBreadcrumb(url) {
|
||||||
|
// 1. 获取新页面的HTML以提取面包屑
|
||||||
|
fetch(url)
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(html => {
|
||||||
|
// 创建一个临时的DOM解析新页面
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const newDoc = parser.parseFromString(html, 'text/html');
|
||||||
|
|
||||||
|
// 获取新页面的面包屑容器 - 使用更精确的选择器
|
||||||
|
const newBreadcrumbContainer = newDoc.querySelector('.bg-white.dark\\:bg-gray-800.rounded-xl.mb-4, .bg-white.dark\\:bg-gray-800.rounded-xl.p-4');
|
||||||
|
|
||||||
|
// 获取当前页面的面包屑容器
|
||||||
|
const currentBreadcrumbContainer = document.querySelector('.bg-white.dark\\:bg-gray-800.rounded-xl.mb-4, .bg-white.dark\\:bg-gray-800.rounded-xl.p-4');
|
||||||
|
|
||||||
|
if (newBreadcrumbContainer && currentBreadcrumbContainer) {
|
||||||
|
// 更新面包屑内容
|
||||||
|
currentBreadcrumbContainer.innerHTML = newBreadcrumbContainer.innerHTML;
|
||||||
|
|
||||||
|
// 重新初始化面包屑相关脚本
|
||||||
|
const breadcrumbScript = currentBreadcrumbContainer.querySelector('script');
|
||||||
|
if (breadcrumbScript) {
|
||||||
|
const newScript = document.createElement('script');
|
||||||
|
newScript.textContent = breadcrumbScript.textContent;
|
||||||
|
breadcrumbScript.parentNode.replaceChild(newScript, breadcrumbScript);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
// 出错时静默处理
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在每次页面转换结束后更新面包屑
|
||||||
|
swup.hooks.on('visit:end', (visit) => {
|
||||||
|
// 所有导航都更新面包屑
|
||||||
|
updateBreadcrumb(visit.to.url);
|
||||||
|
|
||||||
|
// 确保在页面加载完成后元素有正确样式
|
||||||
|
setTimeout(() => {
|
||||||
|
setupTransition();
|
||||||
|
|
||||||
|
// 加载完成后重新扫描预加载链接
|
||||||
|
setTimeout(() => {
|
||||||
|
swup.preloadLinks();
|
||||||
|
}, 500);
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听Fragment插件是否成功应用
|
||||||
|
document.addEventListener('swup:fragmentReplaced', () => {
|
||||||
|
// 确保新内容有正确的过渡样式
|
||||||
|
setTimeout(() => {
|
||||||
|
setupTransition();
|
||||||
|
}, 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 在页面卸载和Astro视图转换时清理资源
|
||||||
|
const cleanup = () => {
|
||||||
|
if (swup) {
|
||||||
|
swup.unuse(fragmentPlugin);
|
||||||
|
swup.unuse(headPlugin);
|
||||||
|
swup.unuse(preloadPlugin);
|
||||||
|
swup.destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 注册清理事件
|
||||||
|
window.addEventListener('beforeunload', cleanup, { once: true });
|
||||||
|
document.addEventListener('astro:before-swap', cleanup, { once: true });
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user