后台系统 headerSearch 实现

374 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第8天,点击查看活动详情

前言

后台系统中的headerSearch功能,指的是可以对当前系统的某些页面进行搜索,以下拉菜单的形式展示,并且点击能够直接进入。今天来记录这个功能如何实现。

1.png

2.png

实现

基本样式

实现的前端样式如下

3.png

点击图片后变为下图样式

4.png

需要用到 element-plus select 远程搜索

5.png

下拉菜单中的选项根据输入框中的值,搜索出与其相关的选项

6.png

由于下拉菜单中的数据需要额外处理,我们先固定其为数字一到五

搜索框及下拉菜单代码如下

components/headerSearch

<template>
  <div :class="{ show: isShow }" class="header-search" @click.stop="onShowClick">
    <svg-icon
      class-name="search-icon"
      icon="search"
    />
    <el-select
      ref="headerSearchSelectRef"
      class="header-search-select"
      v-model="search"
      filterable
      default-first-option
      remote
      placeholder="Search"
      :remote-method="querySearch"
      @change="onSelectChange"
    >
      <el-option
        v-for="option in 5"
        :key="option"
        :label="option"
        :value="option"
      ></el-option>
    </el-select>
  </div>
</template>

<script setup>
import { ref } from 'vue'

// 控制 search 显示
const isShow = ref(false)

// el-select 实例
const headerSearchSelectRef = ref(null)

// 点击图片事件
const onShowClick = () => {
  // 取反 
  isShow.value = !isShow.value
  if (isShow.value) {
    // 如果打开搜索框 自动聚焦
    headerSearchSelectRef.value.focus()
  }
}

// 输入框内容
const search = ref('')

// 搜索方法
const querySearch = (query) => {
  console.log('处理下拉菜单显示的数据')
}

// 选中回调
const onSelectChange = () => {
  console.log('我选中菜单项了')
}
</script>

<style lang="scss" scoped>
.header-search {
  font-size: 0 !important;
  .search-icon {
    cursor: pointer;
    font-size: 18px;
    vertical-align: middle;
  }
  .header-search-select {
    font-size: 18px;
    transition: width 0.2s;
    width: 0;
    overflow: hidden;
    background: transparent;
    border-radius: 0;
    display: inline-block;
    vertical-align: middle;

    ::v-deep .el-input__inner {
      border-radius: 0;
      border: 0;
      padding-left: 0;
      padding-right: 0;
      box-shadow: none !important;
      border-bottom: 1px solid #d9d9d9;
      vertical-align: middle;
    }
  }
  &.show {
    .header-search-select {
      width: 210px;
      margin-left: 10px;
    }
  }
}
</style>

处理下拉数据

搜索框输入值后,需要用路由数据,对其模糊搜索,得到需要的数据

想要进行模糊搜索,可以使用第三方库fuse.js

依据fuse.js的demo

我们需要的数据格式大致为下

[    {        "path":"/my",        "title":[            "个人中心"        ]
    },
    {
        "path":"/user",
        "title":[
            "用户"
        ]
    },
    {
        "path":"/user/manage",
        "title":[
            "用户",
            "用户管理"
        ]
    },
    {
        "path":"/user/info",
        "title":[
            "用户",
            "用户信息"
        ]
    },
    {
        "path":"/article",
        "title":[
            "文章"
        ]
    },
    {
        "path":"/article/ranking",
        "title":[
            "文章",
            "文章排名"
        ]
    },
    {
        "path":"/article/create",
        "title":[
            "文章",
            "创建文章"
        ]
    }
]

安装fuse.js

npm install --save fuse.js@6.4.6

初始化fuse.js 更多配置项可在官网查看

import Fuse from 'fuse.js'
// searchPool为检索的数据源
const fuse = new Fuse(searchPool, {
    // 是否按优先级进行排序
    shouldSort: true,
    // 匹配长度超过这个值的才会被认为是匹配的
    minMatchCharLength: 1,
    // 将被搜索的键列表。 这支持嵌套路径、加权搜索、在字符串和对象数组中搜索。
    // name:搜索的键
    // weight:对应的权重
    keys: [
      {
        name: 'title',
        weight: 0.7
      },
      {
        name: 'path',
        weight: 0.3
      }
    ]
  })

接下来对路由数据处理,得到我们想要的格式数据

首先得到全部的路由数据,可以用router.getRoutes(),接下来对其进行去重操作,恰好在动态路由实现的文章中,实现了filterRouters用于去重

去重的数据为下

7.png

接下来声明generateRoutes方法用于生成所需格式的数据源

compositions/HeaderSearch/FuseData.js

import path from 'path'
import i18n from '@/i18n'
/**
 * 筛选出可供搜索的路由对象
 * @param routes 去重后的路由表
 * @param basePath 基础路径,默认为 /
 * @param prefixTitle 标题数组
 */
export const generateRoutes = (routes, basePath = '/', prefixTitle = []) => {
  let res = []
  // 遍历 routes 路由
  for (const route of routes) {
    // 创建包含 path 和 title 的 item
    const data = {
      path: path.resolve(basePath, route.path),
      title: [...prefixTitle]
    }
    // 存在 meta 时
    // 动态路由不允许被搜索
    // 匹配动态路由的正则  去除路径中带有' : '的数据
    const re = /.*\/:.*/
    if (route.meta && route.meta.title && !re.exec(route.path)) {
      // 使用 i18n 解析国际化数据,形成新的 title 
      const i18ntitle = i18n.global.t(`msg.route.${route.meta.title}`)
      data.title = [...data.title, i18ntitle]
      res.push(data)
    }

    // 存在 children 时,迭代调用
    if (route.children) {
      const tempRoutes = generateRoutes(route.children, data.path, data.title)
      if (tempRoutes.length >= 1) {
        res = [...res, ...tempRoutes]
      }
    }
  }
  return res
}

判断条件route.meta && route.meta.title && !re.exec(route.path)

meta.title用于去除非左侧菜单项

!re.exec(route.path)用于去除带有' : '的数据,如下

8.png

headerSearch中使用generateRoutes

<script setup>
import { computed, ref } from 'vue'
import { generateRoutes } from './FuseData'
import Fuse from 'fuse.js'
import { filterRouters } from '@/utils/route'
import { useRouter } from 'vue-router'

...
const router = useRouter()
// 路由表发生变化 会生成新的数据源
const searchPool = computed(() => {
  // 去重后的路由表
  const filterRoutes = filterRouters(router.getRoutes())
  // 生成数据源
  return generateRoutes(filterRoutes)
})
const fuse = new Fuse(searchPool.value, {
  ...
})
</script>

测试打印搜索结果

// 搜索方法
const querySearch = (query) => {
  console.log(query)
  console.log(fuse.search(query))
}

渲染数据源

<template>
···
  <el-option
      v-for="option in searchOptions"
      :key="option.item.path"
      :label="option.item.title.join(' > ')"
      :value="option.item"
  ></el-option>
 ···
</template>
<script setup>
...
// 搜索的结果
const searchOptions = ref([])
// 搜索方法
const querySearch = (query) => {
  console.log(query)
  if (query !== '') {
    searchOptions.value = fuse.search(query)
  } else {
    searchOptions.value = []
  }
  // console.log(fuse.search(query))
}
...
</script>

路由跳转

点击下拉菜单项后直接跳转对应路由即可

// 选中下拉菜单项方法
const onSelectChange = val => {
  router.push(val.path)
}