后台管理系统有很多通过功能,比如国际化、换肤、页面检索、功能引导等。这些功能在开发中经常直接复制别人的代码,对于其中的原理和代码实现并不是很清楚。本文从 原理上 及 实现上 对齐进行详细的梳理,做到了然于胸。
国际化
国际化原理
先来看一个需求:
我们有一个变量
msg
,但是这个msg
有且只能有两个值:
- hello world
- 你好世界
要求:根据需要切换
msg
的值
这就是国际化
的需求,可以通过以下代码来实现这个需求:
<script>
// 1. 定义 msg 值的数据源
const messages = {
en: {
msg: 'hello world'
},
zh: {
msg: '你好世界'
}
}
// 2. 定义切换变量
let locale = 'en'
// 3. 定义赋值函数
function t(key) {
return messages[locale][key]
}
// 4. 为 msg 赋值
let msg = t('msg')
console.log(msg);
// 修改 locale, 重新执行 t 方法,获取不同语言环境下的值
</script>
实现流程:
- 通过一个变量来 控制 语言环境
- 所有语言环境下的数据源要 预先 定义好
- 通过一个方法来获取 当前语言 下 指定属性 的值
- 该值即为国际化下展示值
基于 vue-i18n V9 的国际化实现方案
在 vue
的项目中,我们不需要手写这么复杂的一些基础代码,可以直接使用 vue-i18n 进行实现(注意:vue3
下需要使用 V 9.x
的 i18n
)
vue-i18n 的使用可以分为四个部分:
- 创建
messages
数据源
// 英文 en.js
export default {
login: {
title: 'User Login',
loginBtn: 'Login',
}
...
}
// 中文 zh.js
export default {
login: {
title: '用户登录',
loginBtn: '登录'
}
...
}
import mZhLocale from './lang/zh'
import mEnLocale from './lang/en'
const messages = {
en: {
msg: {
...mEnLocale
}
},
zh: {
msg: {
...mZhLocale
}
}
}
- 创建
locale
语言变量 在store中创建
state: () => ({
language: getItem(LANG) || 'zh'
})
mutations: {
// 设置国际化
setLanguage(state, lang) {
setItem(LANG, lang)
state.language = lang
},
}
- 初始化
i18n
实例
import { createI18n } from 'vue-i18n'
const i18n = createI18n({
// 使用 Composition API 模式,则需要将其设置为false
legacy: false,
// 全局注入 $t 函数
globalInjection: true,
locale,
messages
})
export default i18n
- 注册
i18n
实例 在main.js
中导入
// i18n (导入放到 APP.vue 导入之前,因为后面我们会在 app.vue 中使用国际化内容)
import i18n from '@/i18n'
import App from './App.vue'
app.use(i18n)
那么就会在全局注入 $t 函数,直接使用:
<div class="tips" v-html="$t('msg.login.desc')"></div>
- vue-i18n在不同文件中的t函数的使用
// 1. 在模板template中的使用,直接使用$t函数
<div class="tips" v-html="$t('msg.login.desc')"></div>
// 2. 在<script setup>中的使用
import { useI18n } from 'vue-i18n'
i18n.t('msg.login.usernameRule')
// 3. 在js文件中的使用
import i18n from '@/i18n'
i18n.global.t('msg.login.passwordRule')
如果使用了第三方UI库,那么也需要对其进行处理,一般像ant-design-vue都会有对应的国际化方案,参照文档即可。
国际化缓存与监听语言变化
缓存
希望在刷新页面后,当前的国际化选择可以被保留**,所以想要实现这个功能,那么就需要进行国际化的缓存处理。此处的缓存,我们依然通过两个方面进行:
vuex
缓存LocalStorage
缓存
// getItem(LANG)从localStorage中取
state: () => ({
language: getItem(LANG) || 'zh',
})
function getLanguage() {
return store && store.getters && store.getters.language
}
// 实例化的时候从store中取
const i18n = createI18n({
locale: getLanguage(),
})
监听语言变化
当监听到语言的变化后,需要重新获取接口,接口里面的内容需要是英文的。
因为监听的逻辑都是一样的,所以单独提取出来
import { watch } from 'vue'
export function watchSwitchLang(...cbs) {
watch(
() => store.getters.language,
() => {
cbs.forEach((cb) => cb(store.getters.language))
}
)
}
同时需要在请求接口里面配置Accept-Language
,这样后台就会返回对应的英文数据。
// 配置接口国际化
config.headers['Accept-Language'] = store.getters.language
动态换肤
想要实现动态换肤的一个前置条件就是:颜色值不可以写死!。
换肤原理
在 scss
中,我们可以通过 $变量名:变量值
的方式定义 css 变量
,然后通过该 css 变量
来去指定某一块 DOM
对应的颜色。
如果此时改变了该 css 变量
的值,那么所对应的 DOM
颜色也会同步发生变化,所谓的 动态换肤 就可以实现了,这个就是实现 动态换肤 的原理。
代码实现
一般我们选用第三方组件库的color-picker组件来进行颜色的选择,选择之后调用store
里面的setMainColor
方法,并把主题色缓存到localstorage里面。
store/module/theme.js
import { getItem, setItem } from '@/utils/storage'
import { MAIN_COLOR, DEFAULT_COLOR } from '@/constant'
// css变量
import variables from '@/styles/variables.scss'
export default {
namespaced: true,
state: () => ({
// 缓存默认的颜色值
mainColor: getItem(MAIN_COLOR) || DEFAULT_COLOR,
variables: variables
}),
mutations: {
// 设置主题色
setMainColor(state, newColor) {
state.mainColor = newColor
state.variables.menuBg = newColor
setItem(MAIN_COLOR, newColor)
}
}
}
store/getters.js
const getters = {
cssVar: (state) => ({
...state.theme.variables
}),
mainColor: (state) => state.theme.mainColor
}
最后在需要进行动态改变颜色的组件中,引入$store.getters.cssVar
对应的变量,比如面包屑:
const store = useStore()
const linkHoverColor = ref(store.getters.cssVar.menuBg)
// 样式里面引用linkHoverColor
.redirect:hover {
// 将来需要进行主题替换,所以这里不去写死样式
color: v-bind(linkHoverColor);
}
如果引入了第三方组件库,一般都会有动态设置主题的配置文档。
注意:这里自定义的颜色,并不是在原有variables的基础上替换,而是重新生成一个cssVar的颜色对象,在模板中使用这个cssVar。如果我直接使用scss自定义的颜色变量,又要是响应式,又要是全局共享,所以只能设置在store中,因为你在selectColor直接设置颜色变量后,在menu组件中是无法共享的。
screenfull全屏
对于 screenfull
而言,浏览器本身已经提供了对用的 API
,点击这里即可查看,这个 API
中,主要提供了两个方法:
-
Document.exitFullscreen()
:该方法用于请求从全屏模式切换到窗口模式。 -
Element.requestFullscreen()
:该方法用于请求浏览器(user agent)将特定元素(甚至延伸到它的后代元素)置为全屏模式。
比如我们可以通过 document.getElementById('app').requestFullscreen()
在获取 id=app
的 DOM
之后,把该区域置为全屏
但是该方法存在一定的小问题,比如:appmain
区域背景颜色为黑色,所以通常情况下我们不会直接使用该 API
来去实现全屏效果,而是会使用它的包装库 screenfull。
headerSearch
headerSearch
是复杂后台系统中非常常见的一个功能,它可以:在指定搜索框中对当前应用中所有页面进行检索,以 select
的形式展示出被检索的页面,以达到快速进入的目的。因此,整个 headerSearch
其实可以分为三个核心的功能点:
- 根据指定内容对所有页面进行检索
- 以
select
形式展示检索出的页面 - 通过检索页面可快速进入对应页面
对照着三个核心功能点和原理,实现步骤如下:
- 创建
headerSearch
组件,用作样式展示和用户输入内容获取 - 获取所有的页面数据,用作被检索的数据源
- 根据用户输入内容在数据源中进行 模糊搜索
- 把搜索到的内容以
select
进行展示 - 监听
select
的change
事件,完成对应跳转
检索数据源
对于我们当前的业务而言,我们希望被检索的页面其实就是左侧菜单中的页面,那么我们检索数据源即为:左侧菜单对应的数据源
let searchPool = computed(() => {
const filterRoutes = filterRouters(router.getRoutes())
// 处理成fuse要求的数据结构
return generateRoutes(filterRoutes)
})
如果我们想要进行 模糊搜索 的话,那么需要依赖一个第三方的库 fuse.js,具体使用参见文档即可。
tagsView
tagsView如下图:
实现步骤:
- 创建
tagsView
组件:用来处理tags
的展示 - 处理基于路由的动态过渡,在
AppMain
中进行:用于处理view
的部分
这里面最重要的一点是如何处理路由的动态过渡
?刚好vue-router官网已经帮我们处理好了,见文档:基于路由的动态过渡
- 路由动态过渡
<div class="app-main">
<router-view v-slot="{ Component, route }">
<transition name="fade-transform" mode="out-in">
<keep-alive>
<component :is="Component" :key="route.path" />
</keep-alive>
</transition>
</router-view>
</div>
.fade-transform-leave-active,
.fade-transform-enter-active {
transition: all 0.5s;
}
// 新页面进入的动画,也就是opacity: 0;translateX(-30px)变为opacity: 1,translateX(0px)
.fade-transform-enter-from {
opacity: 0;
transform: translateX(-30px);
}
// 老页面离开的动画,也就是老页面从opacity: 1,translateX(0px)变为opacity: 0,translateX(30px)
.fade-transform-leave-to {
opacity: 0;
transform: translateX(30px);
}
- 在
appmain
中监听路由的变化
watch(
route,
(to, from) => {
// isTags用来判断那些路由是没必要放在tagsView里面的,比如login页面,错误页面
if (!isTags(to.path)) return
const { fullPath, meta, name, params, path, query } = to
store.commit('app/addTagsViewList', { fullPath, meta, name...})
},
{
immediate: true
}
)
guide引导页
引导页是软件中经常见到的一个功能,无论是在后台项目还是前台或者是移动端项目中。通常情况下引导页是通过 聚焦 的方式,高亮一块视图,然后通过文字解释的形式来告知用户该功能的作用。
对于引导页来说,市面上有很多现成的轮子,所以不需要手动的去进行以上内容的处理,这里可以直接使用 driver.js 进行引导页处理。