后台管理系统之通用功能

561 阅读5分钟

后台管理系统有很多通过功能,比如国际化、换肤、页面检索、功能引导等。这些功能在开发中经常直接复制别人的代码,对于其中的原理和代码实现并不是很清楚。本文从 原理上 及 实现上 对齐进行详细的梳理,做到了然于胸。

国际化

国际化原理

先来看一个需求:

我们有一个变量 msg ,但是这个 msg 有且只能有两个值:

  1. hello world
  2. 你好世界

要求:根据需要切换 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>

实现流程:

  1. 通过一个变量来 控制 语言环境
  2. 所有语言环境下的数据源要 预先 定义好
  3. 通过一个方法来获取 当前语言 下 指定属性 的值
  4. 该值即为国际化下展示值

基于 vue-i18n V9 的国际化实现方案

在 vue 的项目中,我们不需要手写这么复杂的一些基础代码,可以直接使用 vue-i18n 进行实现(注意:vue3 下需要使用 V 9.x 的 i18n

vue-i18n 的使用可以分为四个部分:

  1. 创建 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
    }
  }
}
  1. 创建 locale 语言变量 在store中创建
state: () => ({
    language: getItem(LANG) || 'zh'
})
mutations: {
    // 设置国际化
    setLanguage(state, lang) {
      setItem(LANG, lang)
      state.language = lang
    },
}
  1. 初始化 i18n 实例
import { createI18n } from 'vue-i18n'

const i18n = createI18n({
  // 使用 Composition API 模式,则需要将其设置为false
  legacy: false,
  // 全局注入 $t 函数
  globalInjection: true,
  locale,
  messages
})
export default i18n
  1. 注册 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>
  1. 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都会有对应的国际化方案,参照文档即可。

国际化缓存与监听语言变化

缓存

希望在刷新页面后,当前的国际化选择可以被保留**,所以想要实现这个功能,那么就需要进行国际化的缓存处理。此处的缓存,我们依然通过两个方面进行:

  1. vuex 缓存
  2. 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 中,主要提供了两个方法:

  1. Document.exitFullscreen():该方法用于请求从全屏模式切换到窗口模式。

  2. Element.requestFullscreen():该方法用于请求浏览器(user agent)将特定元素(甚至延伸到它的后代元素)置为全屏模式。

比如我们可以通过 document.getElementById('app').requestFullscreen() 在获取 id=app 的 DOM 之后,把该区域置为全屏

但是该方法存在一定的小问题,比如:appmain 区域背景颜色为黑色,所以通常情况下我们不会直接使用该 API 来去实现全屏效果,而是会使用它的包装库 screenfull

headerSearch

headerSearch 是复杂后台系统中非常常见的一个功能,它可以:在指定搜索框中对当前应用中所有页面进行检索,以 select 的形式展示出被检索的页面,以达到快速进入的目的。因此,整个 headerSearch 其实可以分为三个核心的功能点:

  1. 根据指定内容对所有页面进行检索
  2. 以 select 形式展示检索出的页面
  3. 通过检索页面可快速进入对应页面

对照着三个核心功能点和原理,实现步骤如下:

  1. 创建 headerSearch 组件,用作样式展示和用户输入内容获取
  2. 获取所有的页面数据,用作被检索的数据源
  3. 根据用户输入内容在数据源中进行 模糊搜索
  4. 把搜索到的内容以 select 进行展示
  5. 监听 select 的 change 事件,完成对应跳转

检索数据源

对于我们当前的业务而言,我们希望被检索的页面其实就是左侧菜单中的页面,那么我们检索数据源即为:左侧菜单对应的数据源

let searchPool = computed(() => {
  const filterRoutes = filterRouters(router.getRoutes())
  // 处理成fuse要求的数据结构
  return generateRoutes(filterRoutes)
})

如果我们想要进行 模糊搜索 的话,那么需要依赖一个第三方的库 fuse.js,具体使用参见文档即可。

tagsView

tagsView如下图: image.png

实现步骤:

  1. 创建 tagsView 组件:用来处理 tags 的展示
  2. 处理基于路由的动态过渡,在 AppMain 中进行:用于处理 view 的部分

这里面最重要的一点是如何处理路由的动态过渡?刚好vue-router官网已经帮我们处理好了,见文档:基于路由的动态过渡

  1. 路由动态过渡
<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);
}
  1. 在 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 进行引导页处理。