feat(router): 支持?page=深链接&错误路由回退
- 导航/子菜单 href 统一为 ?page=<id>[#<slug>],支持复制/新开直达 - 启动时解析 ?page= 并同步导航高亮;子菜单跳转可组合 hash 定位分类 - 输入无效 pageId 时自动纠正 URL 到首页(replaceState,避免“内容回退但地址栏仍错误”) - 构建生成 dist/404.html:将 /<id>(或 /<repo>/<id>)回跳到 /?page=<id> 并保留 hash
This commit is contained in:
114
src/generator.js
114
src/generator.js
@@ -1721,6 +1721,116 @@ function copyStaticFiles(config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 生成 GitHub Pages 的 404 回跳页:将 /<id> 形式的路径深链接转换为 /?page=<id>
|
||||||
|
function generate404Html(config) {
|
||||||
|
const siteTitle =
|
||||||
|
config && config.site && typeof config.site.title === 'string' ? config.site.title : 'MeNav';
|
||||||
|
const safeTitle = escapeHtml(siteTitle);
|
||||||
|
|
||||||
|
return `<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="robots" content="noindex" />
|
||||||
|
<title>${safeTitle} - 页面未找到</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 40px 16px;
|
||||||
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue',
|
||||||
|
Arial, 'Noto Sans', 'Liberation Sans', sans-serif;
|
||||||
|
color: #111;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 8px 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #2563eb;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
|
||||||
|
'Courier New', monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var l = window.location;
|
||||||
|
var pathname = (l && l.pathname) || '';
|
||||||
|
var hash = (l && l.hash) || '';
|
||||||
|
var search = (l && l.search) || '';
|
||||||
|
|
||||||
|
// 清理首尾 /,并分割路径段
|
||||||
|
var segments = pathname.replace(/^\\/+|\\/+$/g, '').split('/').filter(Boolean);
|
||||||
|
|
||||||
|
// 仅处理两种常见形态:
|
||||||
|
// - 用户站点:/<id>
|
||||||
|
// - 仓库站点:/<repo>/<id>
|
||||||
|
var base = '/';
|
||||||
|
var pageId = '';
|
||||||
|
|
||||||
|
if (segments.length === 1) {
|
||||||
|
pageId = segments[0];
|
||||||
|
} else if (segments.length === 2) {
|
||||||
|
base = '/' + segments[0] + '/';
|
||||||
|
pageId = segments[1];
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤明显不是“页面 id”的情况(静态资源/404 本身)
|
||||||
|
if (!pageId || pageId === '404.html' || pageId.indexOf('.') !== -1) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
pageId = decodeURIComponent(pageId);
|
||||||
|
} catch (e) {
|
||||||
|
// 保持原值
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将原 search 附加到目标 URL(去掉可能存在的 page 参数,避免冲突)
|
||||||
|
var extra = '';
|
||||||
|
if (search && search.length > 1) {
|
||||||
|
try {
|
||||||
|
var params = new URLSearchParams(search);
|
||||||
|
params.delete('page');
|
||||||
|
var rest = params.toString();
|
||||||
|
if (rest) extra = '&' + rest;
|
||||||
|
} catch (e) {
|
||||||
|
// URLSearchParams 不可用时直接忽略
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var target = base + '?page=' + encodeURIComponent(pageId) + extra + hash;
|
||||||
|
l.replace(target);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>页面未找到</h1>
|
||||||
|
<p>若你访问的是“页面路径深链接”,系统将自动回跳到 <code>?page=</code> 形式的可用地址。</p>
|
||||||
|
<p><a href="./">返回首页</a></p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
// 主函数
|
// 主函数
|
||||||
function main() {
|
function main() {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
@@ -1740,6 +1850,9 @@ function main() {
|
|||||||
// 生成HTML
|
// 生成HTML
|
||||||
fs.writeFileSync('dist/index.html', htmlContent);
|
fs.writeFileSync('dist/index.html', htmlContent);
|
||||||
|
|
||||||
|
// GitHub Pages 静态路由回退:用于支持 /<id> 形式的路径深链接
|
||||||
|
fs.writeFileSync('dist/404.html', generate404Html(config));
|
||||||
|
|
||||||
// 复制静态文件
|
// 复制静态文件
|
||||||
copyStaticFiles(config);
|
copyStaticFiles(config);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -1756,6 +1869,7 @@ if (require.main === module) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
loadConfig,
|
loadConfig,
|
||||||
generateHTML,
|
generateHTML,
|
||||||
|
generate404Html,
|
||||||
copyStaticFiles,
|
copyStaticFiles,
|
||||||
generateNavigation,
|
generateNavigation,
|
||||||
generateCategories,
|
generateCategories,
|
||||||
|
|||||||
280
src/script.js
280
src/script.js
@@ -1886,6 +1886,172 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const submenuItems = document.querySelectorAll('.submenu-item');
|
const submenuItems = document.querySelectorAll('.submenu-item');
|
||||||
pages = document.querySelectorAll('.page');
|
pages = document.querySelectorAll('.page');
|
||||||
|
|
||||||
|
// 方案 A:用 ?page=<id> 作为页面深链接(兼容 GitHub Pages 静态托管)
|
||||||
|
const normalizeText = (value) =>
|
||||||
|
String(value === null || value === undefined ? '' : value).trim();
|
||||||
|
|
||||||
|
const isValidPageId = (pageId) => {
|
||||||
|
const id = normalizeText(pageId);
|
||||||
|
if (!id) return false;
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
return Boolean(el && el.classList && el.classList.contains('page'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRawPageIdFromUrl = () => {
|
||||||
|
try {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
return normalizeText(url.searchParams.get('page'));
|
||||||
|
} catch (error) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPageIdFromUrl = () => {
|
||||||
|
try {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
const pageId = normalizeText(url.searchParams.get('page'));
|
||||||
|
return isValidPageId(pageId) ? pageId : '';
|
||||||
|
} catch (error) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setUrlState = (next, options = {}) => {
|
||||||
|
const { replace = true } = options;
|
||||||
|
try {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
|
||||||
|
if (next && typeof next.pageId === 'string') {
|
||||||
|
const pageId = normalizeText(next.pageId);
|
||||||
|
if (pageId) {
|
||||||
|
url.searchParams.set('page', pageId);
|
||||||
|
} else {
|
||||||
|
url.searchParams.delete('page');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (next && Object.prototype.hasOwnProperty.call(next, 'hash')) {
|
||||||
|
const hash = normalizeText(next.hash);
|
||||||
|
url.hash = hash ? `#${hash}` : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextUrl = `${url.pathname}${url.search}${url.hash}`;
|
||||||
|
const fn = replace ? history.replaceState : history.pushState;
|
||||||
|
fn.call(history, null, '', nextUrl);
|
||||||
|
} catch (error) {
|
||||||
|
// 忽略 URL/History API 异常,避免影响主流程
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setActiveNavByPageId = (pageId) => {
|
||||||
|
const id = normalizeText(pageId);
|
||||||
|
let activeItem = null;
|
||||||
|
|
||||||
|
navItems.forEach((nav) => {
|
||||||
|
const isActive = nav.getAttribute('data-page') === id;
|
||||||
|
nav.classList.toggle('active', isActive);
|
||||||
|
if (isActive) activeItem = nav;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 同步子菜单展开状态:只展开当前激活项
|
||||||
|
navItemWrappers.forEach((wrapper) => {
|
||||||
|
const nav = wrapper.querySelector('.nav-item');
|
||||||
|
if (!nav) return;
|
||||||
|
const hasSubmenu = Boolean(wrapper.querySelector('.submenu'));
|
||||||
|
const shouldExpand = hasSubmenu && nav === activeItem;
|
||||||
|
wrapper.classList.toggle('expanded', shouldExpand);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const escapeSelector = (value) => {
|
||||||
|
if (value === null || value === undefined) return '';
|
||||||
|
const text = String(value);
|
||||||
|
if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(text);
|
||||||
|
// 回退:尽量避免打断选择器(不追求完全覆盖所有边界字符)
|
||||||
|
return text.replace(/[^a-zA-Z0-9_\u00A0-\uFFFF-]/g, '\\$&');
|
||||||
|
};
|
||||||
|
|
||||||
|
const escapeAttrValue = (value) => {
|
||||||
|
if (value === null || value === undefined) return '';
|
||||||
|
return String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHashFromUrl = () => {
|
||||||
|
const rawHash = window.location.hash ? String(window.location.hash).slice(1) : '';
|
||||||
|
if (!rawHash) return '';
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(rawHash).trim();
|
||||||
|
} catch (error) {
|
||||||
|
return rawHash.trim();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollToCategoryInPage = (pageId, options = {}) => {
|
||||||
|
const id = normalizeText(pageId);
|
||||||
|
if (!id) return false;
|
||||||
|
|
||||||
|
const targetPage = document.getElementById(id);
|
||||||
|
if (!targetPage) return false;
|
||||||
|
|
||||||
|
const categoryId = normalizeText(options.categoryId);
|
||||||
|
const categoryName = normalizeText(options.categoryName);
|
||||||
|
|
||||||
|
let targetCategory = null;
|
||||||
|
|
||||||
|
// 优先使用 slug/data-id 精准定位(解决重复命名始终命中第一个的问题)
|
||||||
|
if (categoryId) {
|
||||||
|
const escapedId = escapeSelector(categoryId);
|
||||||
|
targetCategory =
|
||||||
|
targetPage.querySelector(`#${escapedId}`) ||
|
||||||
|
targetPage.querySelector(
|
||||||
|
`[data-type="category"][data-id="${escapeAttrValue(categoryId)}"]`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回退:旧逻辑按文本包含匹配(兼容旧页面/旧数据)
|
||||||
|
if (!targetCategory && categoryName) {
|
||||||
|
targetCategory = Array.from(targetPage.querySelectorAll('.category h2')).find((heading) =>
|
||||||
|
heading.textContent.trim().includes(categoryName)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetCategory) return false;
|
||||||
|
|
||||||
|
// 优化的滚动实现:滚动到使目标分类位于视口1/4处(更靠近顶部位置)
|
||||||
|
try {
|
||||||
|
// 直接获取所需元素和属性,减少重复查询
|
||||||
|
const contentElement = document.querySelector('.content');
|
||||||
|
|
||||||
|
if (contentElement && contentElement.scrollHeight > contentElement.clientHeight) {
|
||||||
|
// 获取目标元素相对于内容区域的位置
|
||||||
|
const rect = targetCategory.getBoundingClientRect();
|
||||||
|
const containerRect = contentElement.getBoundingClientRect();
|
||||||
|
|
||||||
|
// 计算目标应该在视口中的位置(视口高度的1/4处)
|
||||||
|
const desiredPosition = containerRect.height / 4;
|
||||||
|
|
||||||
|
// 计算需要滚动的位置
|
||||||
|
const scrollPosition =
|
||||||
|
contentElement.scrollTop + rect.top - containerRect.top - desiredPosition;
|
||||||
|
|
||||||
|
// 执行滚动
|
||||||
|
contentElement.scrollTo({
|
||||||
|
top: scrollPosition,
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 回退到基本滚动方式
|
||||||
|
targetCategory.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during scroll:', error);
|
||||||
|
// 回退到基本滚动方式
|
||||||
|
targetCategory.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
// 初始化主题
|
// 初始化主题
|
||||||
initTheme();
|
initTheme();
|
||||||
|
|
||||||
@@ -1907,8 +2073,32 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 立即执行初始化,不再使用requestAnimationFrame延迟
|
// 立即执行初始化,不再使用requestAnimationFrame延迟
|
||||||
// 显示首页
|
// 支持 ?page=<id> 直接打开对应页面;无效时回退到首页
|
||||||
showPage(homePageId);
|
const rawPageIdFromUrl = getRawPageIdFromUrl();
|
||||||
|
const validatedPageIdFromUrl = getPageIdFromUrl();
|
||||||
|
const initialPageId = validatedPageIdFromUrl || (isValidPageId(homePageId) ? homePageId : 'home');
|
||||||
|
|
||||||
|
setActiveNavByPageId(initialPageId);
|
||||||
|
showPage(initialPageId);
|
||||||
|
|
||||||
|
// 当输入了不存在的 page id 时,自动纠正 URL,避免“内容回退但地址栏仍错误”
|
||||||
|
if (rawPageIdFromUrl && !validatedPageIdFromUrl) {
|
||||||
|
setUrlState({ pageId: initialPageId }, { replace: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始深链接:支持 ?page=<id>#<categorySlug>
|
||||||
|
const initialHash = getHashFromUrl();
|
||||||
|
if (initialHash) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const found = scrollToCategoryInPage(initialPageId, {
|
||||||
|
categoryId: initialHash,
|
||||||
|
categoryName: initialHash,
|
||||||
|
});
|
||||||
|
|
||||||
|
// hash 存在但未命中时,不做强制修正,避免误伤其他用途的 hash
|
||||||
|
if (!found) return;
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
// 添加载入动画
|
// 添加载入动画
|
||||||
categories.forEach((category, index) => {
|
categories.forEach((category, index) => {
|
||||||
@@ -1964,7 +2154,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
const pageId = item.getAttribute('data-page');
|
const pageId = item.getAttribute('data-page');
|
||||||
if (pageId) {
|
if (pageId) {
|
||||||
|
const prevPageId = currentPageId;
|
||||||
showPage(pageId);
|
showPage(pageId);
|
||||||
|
// 切换页面时同步 URL(清空旧 hash,避免跨页残留)
|
||||||
|
if (normalizeText(prevPageId) !== normalizeText(pageId)) {
|
||||||
|
setUrlState({ pageId, hash: '' }, { replace: true });
|
||||||
|
}
|
||||||
|
|
||||||
// 在移动端视图下点击导航项后自动收起侧边栏
|
// 在移动端视图下点击导航项后自动收起侧边栏
|
||||||
if (isMobile() && isSidebarOpen && !hasSubmenu) {
|
if (isMobile() && isSidebarOpen && !hasSubmenu) {
|
||||||
@@ -1984,19 +2179,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const categoryName = item.getAttribute('data-category');
|
const categoryName = item.getAttribute('data-category');
|
||||||
const categoryId = item.getAttribute('data-category-id');
|
const categoryId = item.getAttribute('data-category-id');
|
||||||
|
|
||||||
const escapeSelector = (value) => {
|
|
||||||
if (value === null || value === undefined) return '';
|
|
||||||
const text = String(value);
|
|
||||||
if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(text);
|
|
||||||
// 回退:尽量避免打断选择器(不追求完全覆盖所有边界字符)
|
|
||||||
return text.replace(/[^a-zA-Z0-9_\u00A0-\uFFFF-]/g, '\\$&');
|
|
||||||
};
|
|
||||||
|
|
||||||
const escapeAttrValue = (value) => {
|
|
||||||
if (value === null || value === undefined) return '';
|
|
||||||
return String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
||||||
};
|
|
||||||
|
|
||||||
if (pageId) {
|
if (pageId) {
|
||||||
// 清除所有子菜单项的激活状态
|
// 清除所有子菜单项的激活状态
|
||||||
submenuItems.forEach((subItem) => {
|
submenuItems.forEach((subItem) => {
|
||||||
@@ -2013,74 +2195,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// 显示对应页面
|
// 显示对应页面
|
||||||
showPage(pageId);
|
showPage(pageId);
|
||||||
|
// 先同步 page 参数并清空旧 hash,避免跨页残留;后续若找到分类再写入新的 hash
|
||||||
|
setUrlState({ pageId, hash: '' }, { replace: true });
|
||||||
|
|
||||||
// 等待页面切换完成后滚动到对应分类
|
// 等待页面切换完成后滚动到对应分类
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// 查找目标分类元素
|
const found = scrollToCategoryInPage(pageId, { categoryId, categoryName });
|
||||||
const targetPage = document.getElementById(pageId);
|
if (!found) return;
|
||||||
if (targetPage) {
|
|
||||||
let targetCategory = null;
|
|
||||||
|
|
||||||
// 优先使用 slug/data-id 精准定位(解决重复命名始终命中第一个的问题)
|
|
||||||
if (categoryId) {
|
|
||||||
const escapedId = escapeSelector(categoryId);
|
|
||||||
targetCategory =
|
|
||||||
targetPage.querySelector(`#${escapedId}`) ||
|
|
||||||
targetPage.querySelector(
|
|
||||||
`[data-type="category"][data-id="${escapeAttrValue(categoryId)}"]`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 回退:旧逻辑按文本包含匹配(兼容旧页面/旧数据)
|
|
||||||
if (!targetCategory && categoryName) {
|
|
||||||
targetCategory = Array.from(targetPage.querySelectorAll('.category h2')).find(
|
|
||||||
(heading) => heading.textContent.trim().includes(categoryName)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetCategory) {
|
|
||||||
// 由于对子菜单 click 做了 preventDefault,这里手动同步 hash(不触发浏览器默认跳转)
|
// 由于对子菜单 click 做了 preventDefault,这里手动同步 hash(不触发浏览器默认跳转)
|
||||||
const nextHash = categoryId || categoryName;
|
const nextHash = normalizeText(categoryId) || normalizeText(categoryName);
|
||||||
if (nextHash) {
|
if (nextHash) {
|
||||||
try {
|
setUrlState({ pageId, hash: nextHash }, { replace: true });
|
||||||
history.replaceState(null, '', `#${nextHash}`);
|
|
||||||
} catch (error) {
|
|
||||||
// 忽略 history API 失败,避免影响滚动体验
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 优化的滚动实现:滚动到使目标分类位于视口1/4处(更靠近顶部位置)
|
|
||||||
try {
|
|
||||||
// 直接获取所需元素和属性,减少重复查询
|
|
||||||
const contentElement = document.querySelector('.content');
|
|
||||||
|
|
||||||
if (contentElement && contentElement.scrollHeight > contentElement.clientHeight) {
|
|
||||||
// 获取目标元素相对于内容区域的位置
|
|
||||||
const rect = targetCategory.getBoundingClientRect();
|
|
||||||
const containerRect = contentElement.getBoundingClientRect();
|
|
||||||
|
|
||||||
// 计算目标应该在视口中的位置(视口高度的1/4处)
|
|
||||||
const desiredPosition = containerRect.height / 4;
|
|
||||||
|
|
||||||
// 计算需要滚动的位置
|
|
||||||
const scrollPosition =
|
|
||||||
contentElement.scrollTop + rect.top - containerRect.top - desiredPosition;
|
|
||||||
|
|
||||||
// 执行滚动
|
|
||||||
contentElement.scrollTo({
|
|
||||||
top: scrollPosition,
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 回退到基本滚动方式
|
|
||||||
targetCategory.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error during scroll:', error);
|
|
||||||
// 回退到基本滚动方式
|
|
||||||
targetCategory.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, 25); // 延迟时间
|
}, 25); // 延迟时间
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<div data-container="nav-items">
|
<div data-container="nav-items">
|
||||||
{{#each this}}
|
{{#each this}}
|
||||||
<div class="nav-item-wrapper">
|
<div class="nav-item-wrapper">
|
||||||
<a href="#" class="nav-item{{#if isActive}} active{{/if}}{{#if active}} active{{/if}}" data-page="{{id}}" data-type="nav-item" data-id="{{id}}" data-name="{{name}}" data-icon="{{icon}}">
|
<a href="?page={{id}}" class="nav-item{{#if isActive}} active{{/if}}{{#if active}} active{{/if}}" data-page="{{id}}" data-type="nav-item" data-id="{{id}}" data-name="{{name}}" data-icon="{{icon}}">
|
||||||
<div class="icon-container">
|
<div class="icon-container">
|
||||||
<i class="{{icon}}"></i>
|
<i class="{{icon}}"></i>
|
||||||
</div>
|
</div>
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
{{#if submenu}}
|
{{#if submenu}}
|
||||||
<div class="submenu">
|
<div class="submenu">
|
||||||
{{#each submenu}}
|
{{#each submenu}}
|
||||||
<a href="#{{#if slug}}{{slug}}{{else}}{{name}}{{/if}}" class="submenu-item" data-page="{{../id}}" data-category="{{name}}" data-category-id="{{#if slug}}{{slug}}{{else}}{{name}}{{/if}}">
|
<a href="?page={{../id}}#{{#if slug}}{{slug}}{{else}}{{name}}{{/if}}" class="submenu-item" data-page="{{../id}}" data-category="{{name}}" data-category-id="{{#if slug}}{{slug}}{{else}}{{name}}{{/if}}">
|
||||||
<i class="{{icon}}"></i>
|
<i class="{{icon}}"></i>
|
||||||
<span>{{name}}</span>
|
<span>{{name}}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
16
test/404-fallback.node-test.js
Normal file
16
test/404-fallback.node-test.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const { generate404Html } = require('../src/generator.js');
|
||||||
|
|
||||||
|
test('P1-5:404.html 回跳应将 /<id> 转为 ?page=<id>(并支持仓库前缀)', () => {
|
||||||
|
const html = generate404Html({ site: { title: 'Test Site' } });
|
||||||
|
|
||||||
|
assert.ok(typeof html === 'string' && html.length > 0);
|
||||||
|
assert.ok(html.includes('?page='), '应包含 ?page= 形态');
|
||||||
|
assert.ok(html.includes('encodeURIComponent(pageId)'), '应对 pageId 做 URL 编码');
|
||||||
|
assert.ok(html.includes('segments.length === 1'), '应支持用户站点 /<id>');
|
||||||
|
assert.ok(html.includes('segments.length === 2'), '应支持仓库站点 /<repo>/<id>');
|
||||||
|
assert.ok(html.includes('l.replace(target)'), '应使用 location.replace 执行回跳');
|
||||||
|
});
|
||||||
|
|
||||||
@@ -37,7 +37,9 @@ test('P1-2:子菜单锚点应使用分类 slug(href + data-category-id)',
|
|||||||
const html = generateHTML(config);
|
const html = generateHTML(config);
|
||||||
|
|
||||||
assert.ok(html.includes('class="submenu-item"'), '应输出子菜单项');
|
assert.ok(html.includes('class="submenu-item"'), '应输出子菜单项');
|
||||||
assert.ok(html.includes('href="#重复-分类"'), '子菜单 href 应指向 slug');
|
assert.ok(html.includes('href="?page=home#重复-分类"'), '子菜单 href 应支持 ?page=<id>#<slug>');
|
||||||
assert.ok(html.includes('data-category-id="重复-分类"'), '子菜单应携带 data-category-id');
|
assert.ok(html.includes('data-category-id="重复-分类"'), '子菜单应携带 data-category-id');
|
||||||
|
assert.ok(html.includes('class="nav-item'), '应输出导航项');
|
||||||
|
assert.ok(html.includes('href="?page=home"'), '导航项 href 应支持 ?page=<id> 深链接');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user