开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第8天,点击查看活动详情
前言
后台系统中的headerSearch功能,指的是可以对当前系统的某些页面进行搜索,以下拉菜单的形式展示,并且点击能够直接进入。今天来记录这个功能如何实现。
实现
基本样式
实现的前端样式如下
点击图片后变为下图样式
需要用到 element-plus select 远程搜索
下拉菜单中的选项根据输入框中的值,搜索出与其相关的选项
由于下拉菜单中的数据需要额外处理,我们先固定其为数字一到五
搜索框及下拉菜单代码如下
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用于去重
去重的数据为下
接下来声明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)用于去除带有' : '的数据,如下
在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)
}