feat: 分类锚点&质量检查&依赖治理

- 分类生成唯一 slug,模板/子菜单/滚动/扩展定位统一使用 data-id
- lint 覆盖 src/scripts/test,CI 增量格式检查
- 清理冗余依赖,升级 esbuild,overrides 修复审计项
- 补充单测并更新修复清单
This commit is contained in:
rbetree
2026-01-04 20:39:42 +08:00
parent 3d9363a550
commit 48609b86de
11 changed files with 641 additions and 224 deletions

View File

@@ -413,6 +413,46 @@ function getSubmenuForNavItem(navItem, config) {
return null;
}
function makeCategorySlugBase(name) {
const raw = typeof name === 'string' ? name : String(name ?? '');
const trimmed = raw.trim();
if (!trimmed) return 'category';
// 规则:尽量可读、跨平台稳定;保留字母/数字/下划线/短横线,其它字符替换为短横线
// 注意:分类名允许中文等非 ASCII 字符Node 18+ 支持 Unicode 属性类
const normalized = trimmed
.replace(/\s+/g, '-')
.toLowerCase()
.replace(/[^\p{L}\p{N}_-]+/gu, '-')
.replace(/-+/g, '-')
.replace(/^-+|-+$/g, '');
return normalized || 'category';
}
function makeUniqueSlug(base, usedSlugs) {
const current = usedSlugs.get(base) || 0;
const next = current + 1;
usedSlugs.set(base, next);
return next === 1 ? base : `${base}-${next}`;
}
function assignCategorySlugs(categories, usedSlugs) {
if (!Array.isArray(categories)) return;
categories.forEach(category => {
if (!category || typeof category !== 'object') return;
const base = makeCategorySlugBase(category.name);
const uniqueSlug = makeUniqueSlug(base, usedSlugs);
category.slug = uniqueSlug;
if (Array.isArray(category.subcategories)) {
assignCategorySlugs(category.subcategories, usedSlugs);
}
});
}
/**
* 将 JSON 字符串安全嵌入到 <script> 中,避免出现 `</script>` 结束标签导致脚本块被提前终止。
* 说明:返回值仍是合法 JSONJSON.parse 后数据不变。
@@ -825,6 +865,16 @@ function prepareRenderData(config) {
// 首页默认页规则navigation 顺序第一项即首页
renderData.homePageId = renderData.navigation && renderData.navigation[0] ? renderData.navigation[0].id : null;
// 为每个页面的分类生成稳定锚点 slug解决重名/空格/特殊字符导致的 hash 冲突)
if (Array.isArray(renderData.navigation)) {
renderData.navigation.forEach(navItem => {
const pageConfig = renderData[navItem.id];
if (pageConfig && Array.isArray(pageConfig.categories)) {
assignCategorySlugs(pageConfig.categories, new Map());
}
});
}
// 添加序列化的配置数据,用于浏览器扩展(确保包含 homePageId 等处理结果)
renderData.configJSON = makeJsonSafeForHtmlScript(
JSON.stringify({
@@ -1382,6 +1432,11 @@ function renderPage(pageId, config) {
if (config.profile.subtitle !== undefined) data.subtitle = config.profile.subtitle;
}
// 分类锚点:为当前页面分类生成稳定 slug用于 id/hash避免重名/特殊字符冲突)
if (Array.isArray(data.categories) && data.categories.length > 0) {
assignCategorySlugs(data.categories, new Map());
}
if (config[pageId] && config[pageId].template) {
console.log(`页面 ${pageId} 使用指定模板: ${templateName}`);
}

View File

@@ -153,30 +153,42 @@ window.MeNav = {
return menavConfigCacheValue;
},
// 获取元素的唯一标识符
_getElementId: function(element) {
const type = element.getAttribute('data-type');
if (type === 'nav-item') {
return element.getAttribute('data-id');
} else if (type === 'social-link') {
return element.getAttribute('data-url');
} else {
return element.getAttribute('data-name');
}
},
// 获取元素的唯一标识符
_getElementId: function(element) {
const type = element.getAttribute('data-type');
if (type === 'nav-item') {
return element.getAttribute('data-id');
} else if (type === 'social-link') {
return element.getAttribute('data-url');
} else {
// 优先使用 data-id例如分类 slug回退 data-name兼容旧扩展/旧页面)
return element.getAttribute('data-id') || element.getAttribute('data-name');
}
},
// 根据类型和ID查找元素
_findElement: function(type, id) {
let selector;
if (type === 'nav-item') {
selector = `[data-type="${type}"][data-id="${id}"]`;
} else if (type === 'social-link') {
selector = `[data-type="${type}"][data-url="${id}"]`;
} else {
selector = `[data-type="${type}"][data-name="${id}"]`;
}
return document.querySelector(selector);
},
// 根据类型和ID查找元素
_findElement: function(type, id) {
let selector;
if (type === 'nav-item') {
selector = `[data-type="${type}"][data-id="${id}"]`;
} else if (type === 'social-link') {
selector = `[data-type="${type}"][data-url="${id}"]`;
} else if (type === 'site') {
// 站点:优先用 data-url更稳定回退 data-id/data-name
return (
document.querySelector(`[data-type="${type}"][data-url="${id}"]`) ||
document.querySelector(`[data-type="${type}"][data-id="${id}"]`) ||
document.querySelector(`[data-type="${type}"][data-name="${id}"]`)
);
} else {
// 其他:优先 data-id例如分类 slug回退 data-name兼容旧扩展/旧页面)
return (
document.querySelector(`[data-type="${type}"][data-id="${id}"]`) ||
document.querySelector(`[data-type="${type}"][data-name="${id}"]`)
);
}
return document.querySelector(selector);
},
// 更新DOM元素
updateElement: function(type, id, newData) {
@@ -1929,19 +1941,33 @@ document.addEventListener('DOMContentLoaded', () => {
});
});
// 子菜单项点击效果
submenuItems.forEach(item => {
item.addEventListener('click', (e) => {
e.preventDefault();
// 子菜单项点击效果
submenuItems.forEach(item => {
item.addEventListener('click', (e) => {
e.preventDefault();
// 获取页面ID和分类名称
const pageId = item.getAttribute('data-page');
const categoryName = item.getAttribute('data-category');
// 获取页面ID和分类名称
const pageId = item.getAttribute('data-page');
const categoryName = item.getAttribute('data-category');
const categoryId = item.getAttribute('data-category-id');
if (pageId) {
// 清除所有子菜单项的激活状态
submenuItems.forEach(subItem => {
subItem.classList.remove('active');
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) {
// 清除所有子菜单项的激活状态
submenuItems.forEach(subItem => {
subItem.classList.remove('active');
});
// 激活当前子菜单项
@@ -1955,20 +1981,45 @@ document.addEventListener('DOMContentLoaded', () => {
// 显示对应页面
showPage(pageId);
// 等待页面切换完成后滚动到对应分类
setTimeout(() => {
// 查找目标分类元素
const targetPage = document.getElementById(pageId);
if (targetPage) {
const targetCategory = Array.from(targetPage.querySelectorAll('.category h2')).find(
heading => heading.textContent.trim().includes(categoryName)
);
// 等待页面切换完成后滚动到对应分类
setTimeout(() => {
// 查找目标分类元素
const targetPage = document.getElementById(pageId);
if (targetPage) {
let targetCategory = null;
if (targetCategory) {
// 优化的滚动实现滚动到使目标分类位于视口1/4处更靠近顶部位置
try {
// 直接获取所需元素和属性,减少重复查询
const contentElement = document.querySelector('.content');
// 优先使用 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不触发浏览器默认跳转
const nextHash = categoryId || categoryName;
if (nextHash) {
try {
history.replaceState(null, '', `#${nextHash}`);
} catch (error) {
// 忽略 history API 失败,避免影响滚动体验
}
}
// 优化的滚动实现滚动到使目标分类位于视口1/4处更靠近顶部位置
try {
// 直接获取所需元素和属性,减少重复查询
const contentElement = document.querySelector('.content');
if (contentElement && contentElement.scrollHeight > contentElement.clientHeight) {
// 获取目标元素相对于内容区域的位置