常见的通用功能 国际化、 动态换肤、全屏显示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>