integrations icon indicating copy to clipboard operation
integrations copied to clipboard

可能的bug: vitepress-plugin-breadcrumbs 的动态路由响应及url子路径支持

Open MWTJC opened this issue 9 months ago • 5 comments

如题,目前发现vitepress-plugin-breadcrumbs似乎不会跟随页面的切换而自动更新,以及在vitepress处于子路径(如主页url类似vitepress.site/blog/)下时,vitepress-plugin-breadcrumbs似乎并没有配置选项能够应对这种情况; 目前是否有计划添加这两点特性?

MWTJC avatar May 01 '25 09:05 MWTJC

你好,谢谢你的反馈,会添加第二点特性,但是第一点我还不太明白,能否更详细描述一下想要什么样的自动更新。

目前它的行为是这样的:

Image

LemonNekoGH avatar May 01 '25 09:05 LemonNekoGH

明白了,应该是我这边的配置有问题,我这边的vitepress(自行东拼西凑出来的)的面包屑确实不会像图中一样跟随实际情况自行更新,我找下我自己的问题

MWTJC avatar May 01 '25 10:05 MWTJC

这是我这边用ai生成的在我这边能正常运行的面包屑(链接可用性检查有问题),希望能帮忙:

<template>
  <div class="breadcrumb">
    <span v-for="(item, index) in breadcrumbs" :key="index">
      <a v-if="item.link" :href="item.link">{{ item.text }}</a>
      <span v-else>{{ item.text }}</span>
      <span v-if="index < breadcrumbs.length - 1" class="separator"> / </span>
    </span>
  </div>
</template>

<script setup>
import { computed } from 'vue'
import { useRoute, useData } from 'vitepress'

const route = useRoute()
const { site, theme, page } = useData()

// 获取所有可用路径
const availablePaths = computed(() => {
  const paths = new Set()

  // 从侧边栏收集所有有效链接
  const collectLinks = (items) => {
    items.forEach(item => {
      if (item.link) paths.add(item.link)
      if (item.items) collectLinks(item.items)
    })
  }

  if (Array.isArray(theme.value.sidebar)) {
    collectLinks(theme.value.sidebar)
  } else {
    Object.values(theme.value.sidebar || {}).forEach(section => {
      if (Array.isArray(section)) {
        collectLinks(section)
      }
    })
  }

  return paths
})

const breadcrumbs = computed(() => {
  // 获取当前路径,确保不重复base
  const path = route.path
  const basePath = site.value.base || '/'

  // 移除base前缀(如果存在)以避免重复
  const relativePath = path.startsWith(basePath) && basePath !== '/'
      ? path.slice(basePath.length - 1)
      : path

  // 首页始终是第一个面包屑
  const items = [
    {
      text: '首页',
      link: basePath
    }
  ]

  // 如果不是首页,添加其他面包屑
  if (relativePath !== '/') {
    // 分割路径,创建面包屑层次
    const segments = relativePath.split('/').filter(Boolean)
    let currentPath = ''

    segments.forEach((segment, index) => {
      // 对URL编码的部分进行解码,正确显示中文
      const decodedSegment = decodeURIComponent(segment)
      currentPath += `/${segment}`

      // 格式化段名称作为默认文本
      let text = decodedSegment.charAt(0).toUpperCase() + decodedSegment.slice(1).replace(/-/g, ' ')

      // 尝试从侧边栏找到更好的标题
      const sidebarItem = findSidebarItem(theme.value.sidebar, currentPath)
      if (sidebarItem) {
        text = sidebarItem.text
      }

      // 对于最后一段,使用页面标题
      if (index === segments.length - 1) {
        text = page.value.title || text
      }

      // 检查路径是否可访问
      const fullPath = basePath + currentPath.slice(1)
      const isPathAvailable = isValidPath(fullPath)

      items.push({
        text,
        // 如果是最后一项或路径不可访问,则不提供链接
        link: index === segments.length - 1 ? null :
            isPathAvailable ? fullPath : findFirstValidChildPath(currentPath)
      })
    })
  }

  return items
})

// 检查路径是否有效
function isValidPath(path) {
  return availablePaths.value.has(path)
}

// 查找指定路径下的第一个有效子页面
function findFirstValidChildPath(parentPath) {
  const validPaths = Array.from(availablePaths.value)
      .filter(path => path.startsWith(parentPath + '/'))
      .sort((a, b) => a.length - b.length)

  return validPaths[0] || null
}

// 辅助函数:在侧边栏中查找项
function findSidebarItem(sidebar, path) {
  if (!sidebar) return null

  // 处理不同形式的侧边栏配置
  const flattenSidebar = (items) => {
    let result = []
    items.forEach(item => {
      if (item.link) result.push(item)
      if (item.items) result = result.concat(flattenSidebar(item.items))
    })
    return result
  }

  let items = []
  if (Array.isArray(sidebar)) {
    items = flattenSidebar(sidebar)
  } else {
    // 处理对象形式的侧边栏
    Object.values(sidebar).forEach(section => {
      if (Array.isArray(section)) {
        items = items.concat(flattenSidebar(section))
      }
    })
  }

  return items.find(item => {
    // 移除可能的base路径进行比较
    const itemPath = item.link
    const basePath = site.value.base || '/'
    const normalizedItemPath = itemPath.startsWith(basePath) && basePath !== '/'
        ? itemPath.slice(basePath.length - 1)
        : itemPath

    return normalizedItemPath === path
  })
}
</script>

<style scoped>
.breadcrumb {
  padding: 0.5rem 0;
  margin-bottom: 1rem;
  font-size: 0.9rem;
  color: var(--vp-c-text-2);
}
.breadcrumb a {
  color: var(--vp-c-brand);
  text-decoration: none;
}
.breadcrumb a:hover {
  text-decoration: underline;
}
.separator {
  margin: 0 0.5rem;
}
</style>

食用方法:

export default {
    extends: DefaultTheme,
    Layout: () => {
        return h(DefaultTheme.Layout, null, {
            'doc-before': () => h(Breadcrumb),
    })
},
enhanceApp({ app, router }) {
        app.component('Breadcrumb', Breadcrumb)
    },

至于vitepress-plugin-breadcrumbs在我这边不正常的情况,后续我会尝试排查

MWTJC avatar May 01 '25 11:05 MWTJC

辛苦了,非常感谢!

LemonNekoGH avatar May 15 '25 01:05 LemonNekoGH

额我这试图尝试从头搭一个新的vitepress来试下:

pnpm add vitepress
pnpm vitepress init

┌  Welcome to VitePress!
│
◇  Where should VitePress initialize the config?
│  ./docs
│
◇  Site title:
│  My Awesome Project
│
◇  Site description:
│  A VitePress Site
│
◇  Theme:
│  Default Theme + Customization
│
◇  Use TypeScript for config and theme files?
│  Yes
│
◇  Add VitePress npm scripts to package.json?
│  Yes
│
└  Done! Now run pnpm run docs:dev and start writing.

Tips:
- Since you've chosen to customize the theme, you should also explicitly install vue as a dev dependency.

pnpm add -D vue
pnpm add @nolebase/vitepress-plugin-breadcrumbs

添加了对应的测试目录与手动目录配置
然后我把这个文件复制到项目根目录并去除了"paths"板块,
最后我参照这里进行了修改,

pnpm run docs:dev

最后发现不知为何breadcrumbs他不会自动更新,只会在手动刷新页面时才更新,编译(build)后再运行(preview)也是同样的现象,

Image

以上我操作的结果已在此repo复现,其中7313ab8是未经任何修改的vitepress

MWTJC avatar Jun 03 '25 13:06 MWTJC