src/layout/components/Navbar.vue
@@ -5,6 +5,28 @@
      <span class="label">LIMS实验室管理系统</span>
    </div>
    <div class="right-menu">
      <div class="menu-search">
        <el-select
          v-model="selectedMenu"
          placeholder="搜索菜单"
          filterable
          clearable
          :fit-input-width="false"
          :filter-method="filterMenus"
          @change="handleMenuSelect"
          @clear="resetMenuFilter"
        >
          <template #prefix>
            <lims-icon name="el-icon-search" />
          </template>
          <el-option
            v-for="item in filteredMenus"
            :key="item.path"
            :label="item.breadcrumb"
            :value="item.path"
          />
        </el-select>
      </div>
      <div class="avatar-wrapper">
        <el-avatar shape="square" :size="30">{{ avatarText }}</el-avatar>
        <span class="userName">{{ userStore.nickName }}</span>
@@ -31,11 +53,61 @@
import Hamburger from '@/components/Hamburger'
import useAppStore from '@/store/modules/app'
import useUserStore from '@/store/modules/user'
import usePermissionStore from '@/store/modules/permission'
import { getNormalPath } from '@/utils/ruoyi'
const appStore = useAppStore()
const userStore = useUserStore()
const permissionStore = usePermissionStore()
const router = useRouter()
const avatarText = computed(() => (userStore.nickName || '').substring(0, 1))
const selectedMenu = ref(undefined)
const filteredMenus = ref([])
// 侧边栏菜单树拍平成可搜索列表(标题 + 完整路径面包屑)
const menuList = computed(() => {
  const list = []
  const walk = (routes, basePath, parents) => {
    ;(routes || []).forEach(route => {
      if (!route || route.hidden || typeof route.path !== 'string') return
      const fullPath = getNormalPath(`${basePath}/${route.path}`)
      const title = route.meta && route.meta.title
      const children = Array.isArray(route.children) ? route.children : []
      if (children.length) {
        walk(children, fullPath, title ? parents.concat(title) : parents)
      } else if (title && fullPath) {
        list.push({ title, path: fullPath, breadcrumb: parents.concat(title).join(' / ') })
      }
    })
  }
  walk(permissionStore.sidebarRouters, '', [])
  return list
})
function filterMenus(query) {
  const q = (query || '').trim().toLowerCase()
  filteredMenus.value = q
    ? menuList.value.filter(item =>
        item.title.toLowerCase().includes(q) || item.breadcrumb.toLowerCase().includes(q)
      )
    : menuList.value
}
function resetMenuFilter() {
  filteredMenus.value = menuList.value
}
function handleMenuSelect(path) {
  if (path) {
    router.push(path).catch(() => {})
  }
  selectedMenu.value = undefined
  resetMenuFilter()
}
watch(menuList, resetMenuFilter, { immediate: true })
function toggleSideBar() {
  appStore.toggleSideBar()
@@ -123,6 +195,18 @@
      outline: none;
    }
    .menu-search {
      margin-right: 20px;
      :deep(.el-select) {
        width: 210px;
      }
      :deep(.el-select__wrapper) {
        border-radius: 16px;
      }
    }
    .avatar-wrapper {
      display: flex;
      align-items: center;