常见的通用功能 国际化、 动态换肤、全屏显示screenfull、 headerSearch...
# 1、国际化
通过一个变量来控制语言环境;所有语言环境下的数据源要预先定义好;通过一个方法来获取当前语言下指定属性的值;该值即为国际化展示值。
# 1.1 vue-i18n
vue-i18n实现国际化分为4个部分: 1、创建message数据源 2、创建locale语言变量 3、初始化i18n实例 4、注册i18n实例
安装vue-i18n:
npm install vue-i18n
创建i18n/index.js文件
创建message数据源:
const messages = {
en: {
msg: {
test: 'hello world'
}
},
zh: {
msg: {
test: '你好世界'
}
}
}
创建locale语言变量:
const locale= 'en'
初始化i18n实例:
import { createI18n } from 'vue-i18n'
const i18n = createI18n({
// 使用 Composition API 模式,则需要将其设置为false
legacy: false,
// 全局注入 $t 函数
globalInjection: true,
locale,
messages
})
注册i18n实例:
export default i18n
在main.js中导入
// i18n (PS:导入放到 APP.vue 导入之前,因为后面我们会在 app.vue 中使用国际化内容)
import i18n from '@/i18n'
app.use(i18n)
在 layout/components/Sidebar/index.vue 中使用 i18n
<h1 class="logo-title" v-if="$store.getters.sidebarOpened">
{{ $t('msg.test') }}
</h1>
修改 locale 的值,即可改变展示的内容
# 1.2 业务封装langSelect组件
封装 langSelect 组件用于修改 locale 导入 el-locale 语言包 创建自定义语言包
在 constant 中定义常量
// 国际化
export const LANG = 'language'
定义 store/app.js
import { LANG } from '@/constant'
import { getItem, setItem } from '@/utils/storage'
export default {
namespaced: true,
state: () => ({
...
language: getItem(LANG) || 'zh'
}),
mutations: {
...
/**
* 设置国际化
*/
setLanguage(state, lang) {
setItem(LANG, lang)
state.language = lang
}
},
actions: {}
}
创建 components/LangSelect/index
<template>
<el-dropdown
trigger="click"
class="international"
@command="handleSetLanguage"
>
<div>
<el-tooltip content="国际化" :effect="effect">
<svg-icon icon="language" />
</el-tooltip>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :disabled="language === 'zh'" command="zh">
中文
</el-dropdown-item>
<el-dropdown-item :disabled="language === 'en'" command="en">
English
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup>
import { useI18n } from 'vue-i18n'
import { defineProps, computed } from 'vue'
import { useStore } from 'vuex'
import { ElMessage } from 'element-plus'
defineProps({
effect: {
type: String,
default: 'dark',
validator: function(value) {
// 这个值必须匹配下列字符串中的一个
return ['dark', 'light'].indexOf(value) !== -1
}
}
})
const store = useStore()
const language = computed(() => store.getters.language)
// 切换语言的方法
const i18n = useI18n()
const handleSetLanguage = lang => {
i18n.locale.value = lang
store.commit('app/setLanguage', lang)
ElMessage.success('更新成功')
}
</script>
在 navbar 中导入 LangSelect
<template>
<div class="navbar">
...
<div class="right-menu">
<lang-select class="right-menu-item hover-effect" />
<!-- 头像 -->
...
</div>
</div>
</template>
<script setup>
import LangSelect from '@/components/LangSelect'
...
</script>
<style lang="scss" scoped>
.navbar {
...
.right-menu {
...
::v-deep .right-menu-item {
display: inline-block;
padding: 0 18px 0 0;
font-size: 24px;
color: #5a5e66;
vertical-align: text-bottom;
&.hover-effect {
cursor: pointer;
}
}
...
}
</style>
# 1.3 处理对应的语言包
语言包分为: 1、element-plus 语言包:用来处理 element 组件的国际化功能 2、自定义语言包:用来处理 非element 组件的国际化功能
早期版本的element-plus和最新版本的element-plus,左侧 menu 菜单无法正常显示,这是因为 element-plus 修改了 el-submenu 的组件名称 layout/components/Sidebar/SidebarItem 中,修改 el-submenu 为 el-sub-menu
在 plugins/index 中导入 element 的中文、英文语言包:
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import en from 'element-plus/lib/locale/lang/en'
注册 element 时,根据当前语言选择使用哪种语言包
import store from '@/store'
export default app => {
app.use(ElementPlus, {
locale: store.getters.language === 'en' ? en : zhCn
})
}
# 1.4 自定义语言包国际化处理
在 lang/index 中,导入语言包
import mZhLocale from './lang/zh'
import mEnLocale from './lang/en'
在 messages 中注册到语言包
const messages = {
en: {
msg: {
...mEnLocale
}
},
zh: {
msg: {
...mZhLocale
}
}
}
# 1.5 处理项目国际化内容
需要进行国际化处理的地方主要分为: 1、登录页面
<template>
<div class="login-container">
...
<div class="title-container">
<h3 class="title">{{ $t('msg.login.title') }}</h3>
<lang-select class="lang-select" effect="light"></lang-select>
</div>
...
<el-button
type="primary"
style="width: 100%; margin-bottom: 30px"
:loading="loading"
@click="handleLogin"
>{{ $t('msg.login.loginBtn') }}</el-button
>
<div class="tips" v-html="$t('msg.login.desc')"></div>
</el-form>
</div>
</template>
<script setup>
import { useI18n } from 'vue-i18n'
...
// 验证规则
const i18n = useI18n()
const loginRules = ref({
username: [
{
...
message: i18n.t('msg.login.usernameRule')
}
],
...
})
...
</script>
login/rules
import i18n from '@/i18n'
export const validatePassword = () => {
return (rule, value, callback) => {
if (value.length < 6) {
callback(new Error(i18n.global.t('msg.login.passwordRule')))
} else {
callback()
}
}
}
navbar 区域: layout/components/navbar
<template>
<div class="navbar">
...
<template #dropdown>
<el-dropdown-menu class="user-dropdown">
<router-link to="/">
<el-dropdown-item> {{ $t('msg.navBar.home') }} </el-dropdown-item>
</router-link>
<a target="_blank" href="">
<el-dropdown-item>{{ $t('msg.navBar.course') }}</el-dropdown-item>
</a>
<el-dropdown-item divided @click="logout">
{{ $t('msg.navBar.logout') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
sidebar 区域 layout/components/navbar
<template>
<div class="navbar">
...
<template #dropdown>
<el-dropdown-menu class="user-dropdown">
<router-link to="/">
<el-dropdown-item> {{ $t('msg.navBar.home') }} </el-dropdown-item>
</router-link>
<a target="_blank" href="">
<el-dropdown-item>{{ $t('msg.navBar.course') }}</el-dropdown-item>
</a>
<el-dropdown-item divided @click="logout">
{{ $t('msg.navBar.logout') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
components/LangSelect/index
<el-tooltip :content="$t('msg.navBar.lang')" :effect="effect">
const handleSetLanguage = lang => {
ElMessage.success(i18n.t('msg.toast.switchLangSuccess'))
}
面包屑区域: 分为sidebar和面包屑区域 对于 sidebar 而言,显示的文本是我们在定义路由表时的 title
<span>{{ title }}</span>
把 title 作为语言包内容的 key 进行处理 创建 utils/i18n 工具模块,用于 将 title 转化为国际化内容
import i18n from '@/i18n'
export function generateTitle(title) {
return i18n.global.t('msg.route.' + title)
}
在 layout/components/Sidebar/MenuItem.vue 中导入该方法:
<template>
...
<span>{{ generateTitle(title) }}</span>
</template>
<script setup>
import { generateTitle } from '@/utils/i18n'
...
</script>
最后修改下 sidebarHeader 的内容
<h1 class="logo-title" v-if="$store.getters.sidebarOpened">
Admin
</h1>
面包屑区域: 在 components/Breadcrumb/index
<template>
...
<!-- 不可点击项 -->
<span v-if="index === breadcrumbData.length - 1" class="no-redirect">{{
generateTitle(item.meta.title)
}}</span>
<!-- 可点击项 -->
<a v-else class="redirect" @click.prevent="onLinkClick(item)">{{
generateTitle(item.meta.title)
}}</a>
...
</template>
<script setup>
import { generateTitle } from '@/utils/i18n'
...
</script>
# 1.6 国际化缓存处理
vuex和localStorage两个方面进行(langSelect组件处理了,只需要使用缓存下来的数据即可)
在 i18n/index 中,创建 getLanguage 方法:
import store from '@/store'
/**
* 返回当前 lang
*/
function getLanguage() {
return store && store.getters && store.getters.language
}
修改 createI18n 的 locale 为 getLanguage()
const i18n = createI18n({
...
locale: getLanguage()
})
# 2、动态换肤
动态换肤的实现方式: 在 scss 中,我们可以通过 $变量名:变量值 的方式定义 css 变量,然后通过该 css 变量 来去指定某一块 DOM 对应的颜色。 那么大家可以想一下,如果我此时改变了该 css 变量 的值,那么所对应的 DOM 颜色是不是也会同步发生变化? 当大量的 DOM 都依赖于这个 css 变量 设置颜色时,我们是不是只需要改变这个 css 变量,那么所有 DOM 的颜色是不是都会发生变化, 动态换肤就可以实现了!
在 layout/components/Sidebar/SidebarMenu.vue 中, 我们实现 el-menu 的背景色时,此处将来会实现换肤功能, 所以我们不能直接写死,而需要通过一个动态的值,来进行指定。实现 动态换肤 的一个前置条件就是:色值不可以写死!
<el-menu
:default-active="activeMenu"
:collapse="!$store.getters.sidebarOpened"
:background-color="$store.getters.cssVar.menuBg"
:text-color="$store.getters.cssVar.menuText"
:active-text-color="$store.getters.cssVar.menuActiveText"
:unique-opened="true"
router
>x'z
...
</el-menu>
动态换肤关键:修改css变量;兼顾element-plus和非element-plus
# 2.1 ThemeSelect.vue组件
此组件包含:navbar 中的展示图标;选择颜色的弹出层
创建 components/ThemeSelect/index 组件
<template>
<!-- 主题图标
v-bind:https://v3.cn.vuejs.org/api/instance-properties.html#attrs -->
<el-dropdown
v-bind="$attrs"
trigger="click"
class="theme"
@command="handleSetTheme"
>
<div>
<el-tooltip :content="$t('msg.navBar.themeChange')">
<svg-icon icon="change-theme" />
</el-tooltip>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="color">
{{ $t('msg.theme.themeColorChange') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- 展示弹出层 -->
<div></div>
</template>
<script setup>
const handleSetTheme = command => {}
</script>
<style lang="scss" scoped></style>
在 layout/components/navbar 中进行引用
<div class="right-menu">
<theme-picker class="right-menu-item hover-effect"></theme-picker>
import ThemePicker from '@/components/ThemeSelect/index'
# 2.2 SelectColor.vue
此组件为颜色选择组件,使用element-plus的el-color-picker组件:完成SelectColor弹窗展示的双向数据绑定;将选中的色值进行本地缓存。 创建 components/ThemePicker/components/SelectColor.vue
<template>
<el-dialog title="提示" :model-value="modelValue" @close="closed" width="22%">
<div class="center">
<p class="title">{{ $t('msg.theme.themeColorChange') }}</p>
<el-color-picker
v-model="mColor"
:predefine="predefineColors"
></el-color-picker>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="closed">{{ $t('msg.universal.cancel') }}</el-button>
<el-button type="primary" @click="comfirm">{{
$t('msg.universal.confirm')
}}</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { defineProps, defineEmits, ref } from 'vue'
defineProps({
modelValue: {
type: Boolean,
required: true
}
})
const emits = defineEmits(['update:modelValue'])
// 预定义色值
const predefineColors = [
'#ff4500',
'#ff8c00',
'#ffd700',
'#90ee90',
'#00ced1',
'#1e90ff',
'#c71585',
'rgba(255, 69, 0, 0.68)',
'rgb(255, 120, 0)',
'hsv(51, 100, 98)',
'hsva(120, 40, 94, 0.5)',
'hsl(181, 100%, 37%)',
'hsla(209, 100%, 56%, 0.73)',
'#c7158577'
]
// 默认色值
const mColor = ref('#00ff00')
/**
* 关闭
*/
const closed = () => {
emits('update:modelValue', false)
}
/**
* 确定
* 1. 修改主题色
* 2. 保存最新的主题色
* 3. 关闭 dialog
*/
const comfirm = async () => {
// 3. 关闭 dialog
closed()
}
</script>
<style lang="scss" scoped>
.center {
text-align: center;
.title {
margin-bottom: 12px;
}
}
</style>
在 ThemePicker/index 中使用该组件
<template>
...
<!-- 展示弹出层 -->
<div>
<select-color v-model="selectColorVisible"></select-color>
</div>
</template>
<script setup>
import SelectColor from './components/SelectColor.vue'
import { ref } from 'vue'
const selectColorVisible = ref(false)
const handleSetTheme = command => {
selectColorVisible.value = true
}
</script>
在 constants/index 下新建常量值
// 主题色保存的 key
export const MAIN_COLOR = 'mainColor'
// 默认色值
export const DEFAULT_COLOR = '#409eff'
创建 store/modules/theme 模块,用来处理 主题色 相关内容
import { getItem, setItem } from '@/utils/storage'
import { MAIN_COLOR, DEFAULT_COLOR } from '@/constant'
export default {
namespaced: true,
state: () => ({
mainColor: getItem(MAIN_COLOR) || DEFAULT_COLOR
}),
mutations: {
/**
* 设置主题色
*/
setMainColor(state, newColor) {
state.mainColor = newColor
setItem(MAIN_COLOR, newColor)
}
}
}
在 store/getters 下指定快捷访问
mainColor: state => state.theme.mainColor
在 store/index 中导入 theme
...
import theme from './modules/theme.js'
export default createStore({
getters,
modules: {
...
theme
}
})
在 selectColor 中,设置初始色值 和 缓存色值
...
<script setup>
import { defineProps, defineEmits, ref } from 'vue'
import { useStore } from 'vuex'
...
const store = useStore()
// 默认色值
const mColor = ref(store.getters.mainColor)
...
/**
* 确定
* 1. 修改主题色
* 2. 保存最新的主题色
* 3. 关闭 dialog
*/
const comfirm = async () => {
// 2. 保存最新的主题色
store.commit('theme/setMainColor', mColor.value)
// 3. 关闭 dialog
closed()
}
</script>
处理 element-plus 主题变更原理
# 2.3 处理 element-plus 主题变更
实现步骤: 获取当前 element-plus 的所有样式 ;定义我们要替换之后的样式 ;在原样式中,利用正则替换新样式 ;把替换后的样式写入到 style 标签中
创建 utils/theme 工具类,写入两个方法
/**
* 写入新样式到 style
* @param {*} elNewStyle element-plus 的新样式
* @param {*} isNewStyleTag 是否生成新的 style 标签
*/
export const writeNewStyle = elNewStyle => {
}
/**
* 根据主色值,生成最新的样式表
*/
export const generateNewStyle = primaryColor => {
}
实现第一个方法 generateNewStyle:需要安装两个工具类 rgb-hex:转换RGB(A)颜色为十六进制 css-color-function:在CSS中提出的颜色函数的解析器和转换器 然后还需要写入一个 颜色转化计算器 formula.json
创建 constants/formula.json
{
"shade-1": "color(primary shade(10%))",
"light-1": "color(primary tint(10%))",
"light-2": "color(primary tint(20%))",
"light-3": "color(primary tint(30%))",
"light-4": "color(primary tint(40%))",
"light-5": "color(primary tint(50%))",
"light-6": "color(primary tint(60%))",
"light-7": "color(primary tint(70%))",
"light-8": "color(primary tint(80%))",
"light-9": "color(primary tint(90%))",
"subMenuHover": "color(primary tint(70%))",
"subMenuBg": "color(primary tint(80%))",
"menuHover": "color(primary tint(90%))",
"menuBg": "color(primary)"
}
实现 generateNewStyle 方法
import color from 'css-color-function'
import rgbHex from 'rgb-hex'
import formula from '@/constant/formula.json'
import axios from 'axios'
/**
* 根据主色值,生成最新的样式表
*/
export const generateNewStyle = async primaryColor => {
const colors = generateColors(primaryColor)
let cssText = await getOriginalStyle()
// 遍历生成的样式表,在 CSS 的原样式中进行全局替换
Object.keys(colors).forEach(key => {
cssText = cssText.replace(
new RegExp('(:|\\s+)' + key, 'g'),
'$1' + colors[key]
)
})
return cssText
}
/**
* 根据主色生成色值表
*/
export const generateColors = primary => {
if (!primary) return
const colors = {
primary
}
Object.keys(formula).forEach(key => {
const value = formula[key].replace(/primary/g, primary)
colors[key] = '#' + rgbHex(color.convert(value))
})
return colors
}
/**
* 获取当前 element-plus 的默认样式表
*/
const getOriginalStyle = async () => {
const version = require('element-plus/package.json').version
const url = `https://unpkg.com/element-plus@${version}/dist/index.css`
const { data } = await axios(url)
// 把获取到的数据筛选为原样式模板
return getStyleTemplate(data)
}
/**
* 返回 style 的 template
*/
const getStyleTemplate = data => {
// element-plus 默认色值
const colorMap = {
'#3a8ee6': 'shade-1',
'#409eff': 'primary',
'#53a8ff': 'light-1',
'#66b1ff': 'light-2',
'#79bbff': 'light-3',
'#8cc5ff': 'light-4',
'#a0cfff': 'light-5',
'#b3d8ff': 'light-6',
'#c6e2ff': 'light-7',
'#d9ecff': 'light-8',
'#ecf5ff': 'light-9'
}
// 根据默认色值为要替换的色值打上标记
Object.keys(colorMap).forEach(key => {
const value = colorMap[key]
data = data.replace(new RegExp(key, 'ig'), value)
})
return data
}
处理 writeNewStyle 方法
/**
* 写入新样式到 style
* @param {*} elNewStyle element-plus 的新样式
* @param {*} isNewStyleTag 是否生成新的 style 标签
*/
export const writeNewStyle = elNewStyle => {
const style = document.createElement('style')
style.innerText = elNewStyle
document.head.appendChild(style)
}
最后在 SelectColor.vue 中导入这两个方法:
...
<script setup>
...
import { generateNewStyle, writeNewStyle } from '@/utils/theme'
...
/**
* 确定
* 1. 修改主题色
* 2. 保存最新的主题色
* 3. 关闭 dialog
*/
const comfirm = async () => {
// 1.1 获取主题色
const newStyleText = await generateNewStyle(mColor.value)
// 1.2 写入最新主题色
writeNewStyle(newStyleText)
// 2. 保存最新的主题色
store.commit('theme/setMainColor', mColor.value)
// 3. 关闭 dialog
closed()
}
</script>
问题:刷新页面,新主题会失效(原因是因为没有写入新的 style) 解决方式: app.vue 中
<script setup>
import { useStore } from 'vuex'
import { generateNewStyle, writeNewStyle } from '@/utils/theme'
const store = useStore()
generateNewStyle(store.getters.mainColor).then(newStyleText => {
writeNewStyle(newStyleText)
})
</script>
# 2.4 自定义主题
自定义主题变更 为 menu 菜单背景色 指定 menu 菜单背景色的位置在 layout/components/sidebar/SidebarMenu.vue 中
<el-menu
:default-active="activeMenu"
:collapse="!$store.getters.sidebarOpened"
:background-color="$store.getters.cssVar.menuBg"
:text-color="$store.getters.cssVar.menuText"
:active-text-color="$store.getters.cssVar.menuActiveText"
:unique-opened="true"
router
>
此处的 背景色是通过 getters 进行指定的,该 cssVar 的 getters 为:
cssVar: state => variables,
根据当前保存的 mainColor 覆盖原有的默认色值
import variables from '@/styles/variables.scss'
import { MAIN_COLOR } from '@/constant'
import { getItem } from '@/utils/storage'
import { generateColors } from '@/utils/theme'
const getters = {
...
cssVar: state => {
return {
...variables,
...generateColors(getItem(MAIN_COLOR))
}
},
...
}
export default getters
在 layout/index 中设置 sidebar 的 backgroundColor(sidebar背景色未被替换)
<sidebar
id="guide-sidebar"
class="sidebar-container"
:style="{ backgroundColor: $store.getters.cssVar.menuBg }"
/>
因为 getters 中没有监听到 依赖值的响应变化,所以我们希望修改依赖值 在 store/modules/theme 中
...
import variables from '@/styles/variables.scss'
export default {
namespaced: true,
state: () => ({
...
variables
}),
mutations: {
/**
* 设置主题色
*/
setMainColor(state, newColor) {
...
state.variables.menuBg = newColor
...
}
}
}
在 getters 中
....
const getters = {
...
cssVar: state => {
return {
...state.theme.variables,
...generateColors(getItem(MAIN_COLOR))
}
},
...
}
export default getters
# 3、全屏显示:screenfull
# 3.1 封装screenfull组件
展示切换按钮;基于screenful实现切换功能 在navbar中引入该组件
下载依赖包:
npm i screenfull@5.1.0
创建 components/Screenfull/index
<template>
<div>
<svg-icon
:icon="isFullscreen ? 'exit-fullscreen' : 'fullscreen'"
@click="onToggle"
/>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import screenfull from 'screenfull'
// 是否全屏
const isFullscreen = ref(false)
// 监听变化
const change = () => {
isFullscreen.value = screenfull.isFullscreen
}
// 切换事件
const onToggle = () => {
screenfull.toggle()
}
// 设置侦听器
onMounted(() => {
screenfull.on('change', change)
})
// 删除侦听器
onUnmounted(() => {
screenfull.off('change', change)
})
</script>
<style lang="scss" scoped></style>
在 navbar 中引入该组件:
<screenfull class="right-menu-item hover-effect" />
import Screenfull from '@/components/Screenfull'
# 4、headerSearch
headerSearch 是复杂后台系统中非常常见的一个功能, 它可以:在指定搜索框中对当前应用中所有页面进行检索, 以 select 的形式展示出被检索的页面,以达到快速进入的目的.
headerSearch 其实可以分为三个核心的功能点: 1.根据指定内容对所有页面进行检索 2.以 select 形式展示检索出的页面 3.通过检索页面可快速进入对应页面
根据指定内容检索所有页面,把检索出的页面以 select 展示,点击对应 option 可进入
创建 headerSearch 组件,用作样式展示和用户输入内容获取 获取所有的页面数据,用作被检索的数据源 根据用户输入内容在数据源中进行 模糊搜索 把搜索到的内容以 select 进行展示 监听 select 的 change 事件,完成对应跳转
# 4.1 封装headerSearch组件
创建 components/HeaderSearch/index组件
<template>
<div :class="{ show: isShow }" class="header-search">
<svg-icon
class-name="search-icon"
icon="search"
@click.stop="onShowClick"
/>
<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
headerSearchSelectRef.value.focus()
}
// search 相关
const search = ref('')
// 搜索方法
const querySearch = () => {
console.log('querySearch')
}
// 选中回调
const onSelectChange = () => {
console.log('onSelectChange')
}
</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>
在 navbar 中导入该组件
<header-search class="right-menu-item hover-effect"></header-search>
import HeaderSearch from '@/components/HeaderSearch'
检索的页面其实就是左侧菜单中的页面,那么我们检索数据源即为:左侧菜单对应的数据源
<script setup>
import { ref, computed } from 'vue'
import { filterRouters, generateMenus } from '@/utils/route'
import { useRouter } from 'vue-router'
...
// 检索数据源
const router = useRouter()
const searchPool = computed(() => {
const filterRoutes = filterRouters(router.getRoutes())
console.log(generateMenus(filterRoutes))
return generateMenus(filterRoutes)
})
console.log(searchPool)
</script>
# 4.2 检索数据源-模糊搜索
需要依赖一个第三方的库 fuse.js
npm install --save fuse.js@6.4.6
初始化fuse
import Fuse from 'fuse.js'
/**
* 搜索库相关
*/
const fuse = new Fuse(list, {
// 是否按优先级进行排序
shouldSort: true,
// 匹配长度超过这个值的才会被认为是匹配的
minMatchCharLength: 1,
// 将被搜索的键列表。 这支持嵌套路径、加权搜索、在字符串和对象数组中搜索。
// name:搜索的键
// weight:对应的权重
keys: [
{
name: 'title',
weight: 0.7
},
{
name: 'path',
weight: 0.3
}
]
})
期望得到如下的检索数据源结构
[
{
"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":[
"文章",
"创建文章"
]
}
]
我们之前处理了的数据源并不符合我们的需要,所以我们需要对数据源进行重新处理
# 4.3 数据源重处理,生成 searchPool
创建 compositions/HeaderSearch/FuseData.js
import path from 'path'
import i18n from '@/i18n'
/**
* 筛选出可供搜索的路由对象
* @param routes 路由表
* @param basePath 基础路径,默认为 /
* @param prefixTitle
*/
export const generateRoutes = (routes, basePath = '/', prefixTitle = []) => {
// 创建 result 数据
let res = []
// 循环 routes 路由
for (const route of routes) {
// 创建包含 path 和 title 的 item
const data = {
path: path.resolve(basePath, route.path),
title: [...prefixTitle]
}
// 当前存在 meta 时,使用 i18n 解析国际化数据,组合成新的 title 内容
// 动态路由不允许被搜索
// 匹配动态路由的正则
const re = /.*\/:.*/
if (route.meta && route.meta.title && !re.exec(route.path)) {
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
}
在 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>
通过 querySearch 测试搜索结果
// 搜索方法
const querySearch = query => {
console.log(fuse.search(query))
}
# 4.4 渲染检索数据
渲染检索出的数据
<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 => {
if (query !== '') {
searchOptions.value = fuse.search(query)
} else {
searchOptions.value = []
}
}
...
</script>
完成对应跳转
// 选中回调
const onSelectChange = val => {
router.push(val.path)
}
剩余的bug: 1.在 search 打开时,点击 body 关闭 search 2.在 search 关闭时,清理 searchOptions 3.headerSearch 应该具备国际化能力
前面两个问题:
/**
* 关闭 search 的处理事件
*/
const onClose = () => {
headerSearchSelectRef.value.blur()
isShow.value = false
searchOptions.value = []
}
/**
* 监听 search 打开,处理 close 事件
*/
watch(isShow, val => {
if (val) {
document.body.addEventListener('click', onClose)
} else {
document.body.removeEventListener('click', onClose)
}
})
国际化的问题,想要处理这个问题非常简单,我们只需要:监听语言变化,重新计算数据源初始化 fuse 即可 在 utils/i18n 下,新建方法 watchSwitchLang
import { watch } from 'vue'
import store from '@/store'
/**
*
* @param {...any} cbs 所有的回调
*/
export function watchSwitchLang(...cbs) {
watch(
() => store.getters.language,
() => {
cbs.forEach(cb => cb(store.getters.language))
}
)
}
在 headerSearch 监听变化,重新赋值
<script setup>
...
import { watchSwitchLang } from '@/utils/i18n'
...
// 检索数据源
const router = useRouter()
let searchPool = computed(() => {
const filterRoutes = filterRouters(router.getRoutes())
return generateRoutes(filterRoutes)
})
/**
* 搜索库相关
*/
let fuse
const initFuse = searchPool => {
fuse = new Fuse(searchPool, {
...
}
initFuse(searchPool.value)
...
// 处理国际化
watchSwitchLang(() => {
searchPool = computed(() => {
const filterRoutes = filterRouters(router.getRoutes())
return generateRoutes(filterRoutes)
})
initFuse(searchPool.value)
})
</script>