Files
website/.vitepress/theme/nav-anchor.ts
T
Dai0814 39209ef591
mirror-to-gitee / mirror (push) Successful in 3s
fix(portal): 导航栏锚点项点击后正确高亮
问题:点「产品功能 / 核心能力 / 界面预览 / 官方文档」跳转后图标不变蓝。
根因:VitePress 的 isActive() 只比较 path,不看 hash。nav 里
「首页 /」「产品功能 /#features」「核心能力 /#core」的路径部分都是 `/`,
默认逻辑要么把首页和当前锚点同时点亮,要么只亮首页。

改动
- 新增 .vitepress/theme/nav-anchor.ts 接管首页内锚点项的激活判定:
  用 data-anchor-active 属性标记,规则为
  在首页且有 hash → 只有 hash 匹配项亮;在首页无 hash → 只有「首页」亮;
  不在首页 → 清标记交回 VitePress 默认逻辑
- 样式侧用 html.portal-nav-home 限定压制默认 active 的作用域,
  只影响首页,不影响 /projects/ 与 /about 的正常高亮
- 导航「项目」加 activeMatch: '^/projects/',
  使 /projects/iotlcm/ 等详情页也保持高亮
- MutationObserver 监听导航 DOM 变化(SPA 切换、主题切换会重建)

踩的坑
- Layout 是函数式组件,onMounted 在其中无效(Vue 报"no active component
  instance"),改用 nextTick 调度
- 初版用 hashOf(a) 判空来识别「首页」,但 /projects/ 与 /about 也没有 hash,
  被误判成首页项一起点亮;改为判断 href 解析后是否为 '/'
- 抑制规则最初没加作用域,导致非首页页面的正常 active 也被压成黑字

验证(8 个场景逐个实测)
首页顶部→首页 / 四个锚点→各自高亮 / 项目页→项目 /
关于页→关于 / iotLCM 详情→项目;构建 13 页 274 引用 0 失效
2026-11-04 15:23:37 +08:00

98 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 导航栏锚点高亮
*
* 问题:VitePress 的 isActive() 只比较 path,不看 hash。
* nav 里「首页 /」「产品功能 /#features」「核心能力 /#core」路径部分都是 `/`,
* 于是停在 #features 时,首页和产品功能会同时被标蓝。
*
* 解决:接管首页内锚点项的激活判定,用 data-anchor-active 标记,
* 样式侧压掉 VitePress 默认的 .active。
* - 在首页且有 hash:只有 hash 匹配的那项亮,首页项不亮
* - 在首页且无 hash:只有「首页」亮
* - 不在首页:不干预,清掉标记交回默认逻辑
*/
const SEL = '.VPNavBarMenuLink'
function hashOf(a: HTMLAnchorElement): string {
const href = a.getAttribute('href') ?? ''
const i = href.indexOf('#')
return i === -1 ? '' : href.slice(i)
}
function apply(): void {
const links = Array.from(document.querySelectorAll<HTMLAnchorElement>(SEL))
if (!links.length) return
const path = location.pathname
const onHome = path === '/' || path === '' || /^\/index\.html?$/.test(path)
// 标记首页状态:样式侧据此压掉 VitePress 对「项目 / 关于」的前缀匹配高亮
document.documentElement.classList.toggle('portal-nav-home', onHome)
if (!onHome) {
links.forEach((a) => a.removeAttribute('data-anchor-active'))
return
}
// 兼容 VitePress 的 cleanUrls:路径可能是 / 或 /index.html
const h = location.hash
links.forEach((a) => {
const href = a.getAttribute('href') ?? ''
const itemHash = hashOf(a)
if (itemHash) {
// 锚点项:只有 hash 完全匹配时才亮
a.toggleAttribute('data-anchor-active', h === itemHash)
return
}
// 没有 hash 的项:只有「指向首页本身」的才算首页项
// (/projects/ 与 /about 也没有 hash,不能一并当作首页)
const bare = href.split('#')[0].replace(/\/index\.html?$/, '/')
const isHomeItem = bare === '/' || bare === ''
if (isHomeItem) {
a.toggleAttribute('data-anchor-active', h === '')
} else {
a.removeAttribute('data-anchor-active')
}
})
}
let raf = 0
function schedule(): void {
if (raf) return
raf = requestAnimationFrame(() => {
raf = 0
apply()
})
}
/** 首屏导航是异步渲染的,用 MutationObserver 等它出现再判定 */
export function setupNavAnchorHighlight(): void {
if (typeof window === 'undefined') return
const run = () => {
apply()
// 导航可能是分块渲染的,多补两次
requestAnimationFrame(apply)
setTimeout(apply, 80)
setTimeout(apply, 300)
}
run()
window.addEventListener('hashchange', schedule)
window.addEventListener('popstate', schedule)
window.addEventListener('load', schedule)
// 导航栏内容变化(SPA 切换、暗色切换重建)时重新判定
const nav = document.querySelector('.VPNavBar') ?? document.body
const mo = new MutationObserver(() => schedule())
mo.observe(nav, { childList: true, subtree: true })
}
export { apply as applyNavAnchorHighlight }