SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(四):引入 vuex 进行状态管理、引入 vue-i18n 进行国际化管理
(1) 相关博文地址:
SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(一):搭建基本环境:https://www.cnblogs.com/l-y-h/p/12930895.html SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(二):引入 element-ui 定义基本页面显示:https://www.cnblogs.com/l-y-h/p/12935300.html SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(三):引入 js-cookie、axios、mock 封装请求处理以及返回结果:https://www.cnblogs.com/l-y-h/p/12955001.html
(2)代码地址:
https://github.com/lyh-man/admin-vue-template.git
一、引入 vuex 进行状态管理
1、简介
vuex指的是一种状态管理模式,集中式管理所有组件的状态(管理数据)。
【vuex 官方文档:】 https://vuex.vuejs.org/zh/guide/ 【vuex 参考地址:】 https://www.cnblogs.com/l-y-h/p/11666653.html
使用场景分析:
之前 Home.vue 页面中,Header 部分有个折叠按钮,点击之后,可以折叠与展开 Aside 组件,这之间就设计到数据在组件间的共享。
之前使用 this.$emit 触发父组件的方法,然后通过 props 属性传递数据来实现的。
上面一种方式,虽然可以实现,但是组件太多的话,数据传递起来会很复杂。
使用 vuex 后,数据统一管理,当数据发生变化时,其所有引用的地方均会修改。
2、安装、模块化使用 vuex
(1)安装
项目构建时,已经安装过了。
可以使用 npm 手动安装,然后在 vuex 中全局引入。
【npm 安装:】 npm install vuex
如下图:项目已经构建好了,在 store 文件夹下进行相关编写即可。
(2)引入 vuex
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export default new Vuex.Store({ state: { }, mutations: { }, actions: { }, modules: { } })
(3)根据功能拆分成各个模块
所有的状态管理写在一个 js 里,不方便维护。
可以根据不同的功能,抽取去不同的模块 js 进行处理。
比如:登录模块可以定义 user.js 去处理、公共模块可以定义 common.js 去处理。
如下:定义一个 user.js,其中 state 管理一个 userName 数据,当登录成功时,保存该数据,并显示在主页面的右上角。
export default { // 开启命名空间(防止各模块间命名冲突),访问时需要使用 模块名 + 方法名 namespaced: true, // 管理数据(状态) state: { // 用于保存用户名 userName: 'Admin' }, // 更改 state(同步) mutations: { updateName(state, data) { if (data) { state.userName = data } else { state.userName = 'Admin' } } }, // 异步触发 mutations actions: { updateName({commit, state}, data) { commit("updateName", data) } } }
(4)将定义好的模块,在 index.js 中引入。
import Vue from 'vue' import Vuex from 'vuex' import user from './module/user.js' Vue.use(Vuex) export default new Vuex.Store({ modules: { user } })
(5)使用
在组件中,可以引用 state、action 。
其中:
使用 $store.state 可以引用 state 中的数据,也可使用 mapState 替代 $store.state。
使用 $store.dispatch 可以引用 action 中的方法,也可使用 mapActions 替代·$store.dispatch。
如下:
在 Header.vue 页面中,使用 mapState 引入 userName,并显示。
在 computed 中 引入 state 定义的属性(可以使用 数组 或者 对象的形式)。
由于使用了 模块进行封装,所以在引入时,第一个参数需要指定 模块名。
import {mapState} from 'vuex' export default { computed: { // ...mapState('user', {userName: 'userName'}), ...mapState('user', ['userName']) } }
在 Login.vue 中引入 action 方法,用于修改 用户名。
import {mapActions} from 'vuex' export default { data() { return { dataForm: { userName: '', password: '' } }, methods: { ...mapActions('user', ['updateName']) } } }
简单测试一下:
输入用户名,登录后,右上角显示 自定义用户名。
不输入用户名,登录后,右上角显示 Admin(默认)。
二、引入 vue-i18n 进行国际化管理
1、简介
一般项目都要求多语言显示,比如: 中文、英文 快速切换。
使用 vue-i18n 插件可以快捷、方便的进行这种操作。
【官方文档:】 https://kazupon.github.io/vue-i18n/zh/introduction.html
2、安装、使用
(1)npm 安装
npm install vue-i18n
(2)引入 vue-i18n
在 src 下新建一个 i18n 目录,并创建一个 index.js,用于引入 vue-i18n。
并引入 各语言文件。
import Vue from 'vue' // 引入 VueI18n import VueI18n from 'vue-i18n' // 全局挂载 VueI18n Vue.use(VueI18n) // 创建 i18n 实例,并引入语言文件(可以是 js 文件、也可以为 json 文件) const i18n = new VueI18n({ // locale 为语言标识,通过切换locale的值来实现语言切换( this.$i18n.locale ) locale: 'zh', messages: { 'zh': require('@/i18n/languages/zh.json'), 'en': require('@/i18n/languages/en.json') } }) export default i18n
(3)定义各语言文件
此处以 zh.json、en.json 为例(也可以定义成 js 文件)。
【zh.json】 { "login": { "title": "管理员登录", "userName": "用户名", "password": "密码", "language": "语言选择", "signIn": "登录", "userNameNotNull": "用户名不能为空", "passwordNotNull": "密码不能为空", "signInSuccess": "登录成功" }, "language": { "setting": "设置", "languageSettings": "语言设置: ", "zh": "中文", "en": "英语" }, "header": { "foldAside": "折叠侧边栏", "unFoldAside": "展开侧边栏", "setUp": "设置", "help": "帮助", "blogAddress": "博客地址", "codeAddress": "代码地址", "userSetUp": "用户设置", "updatePassword": "修改密码", "logOut": "退出" }, "aside": { "adminCenter": "后台管理中心", "admin": "后台", "homePage": "首页" } } 【en.json】 { "login": { "title": "Administrator Login", "userName": "UserName", "password": "Password", "language": "Language", "signIn": "SignIn", "userNameNotNull": "UserName Not Null", "passwordNotNull": "Password Not Null", "signInSuccess": "SignIn Success" }, "language": { "setting": "Settings", "languageSettings": "Language Settings: ", "zh": "Chinese", "en": "English" }, "header": { "foldAside": "Fold Aside", "unFoldAside": "Un Fold Aside", "setUp": "SetUp", "help": "Help", "blogAddress": "Blog Address", "codeAddress": "Code Address", "userSetUp": "User SetUp", "updatePassword": "Update Password", "logOut": "LogOut" }, "aside": { "adminCenter": "Admin Center", "admin": "AC", "homePage": "Home Page" } }
(4)在 main.js 中全局引入 定义好的 i18n。
import i18n from '@/i18n/index.js' new Vue({ router, store, i18n, render: h => h(App) }).$mount('#app')
(5)根据 zh.json、en.json 定义好的数据,将各个组件中的中文替换掉。
使用 $t("") 去替换值。形式:{{$t("")}} 或者 this.$t("")。
如下例:
替换 Login.vue 中的数据(留个坑)。
(6)填坑
上面我留了个坑,在 data 中使用 this.$t(""),修改语言后,不会变化。
可以将其写在 computed 属性中。
如下例,在 Header.vue 中,使用一个 language 对象进行国际化管理。
computed: { // 定义国际化显示 language() { return { foldAside: this.$t("header.foldAside"), unFoldAside: this.$t("header.unFoldAside"), setUp: this.$t("header.setUp"), help: this.$t("header.help"), blogAddress: this.$t("header.blogAddress"), codeAddress: this.$t("header.codeAddress"), userSetUp: this.$t("header.userSetUp"), updatePassword: this.$t("header.updatePassword"), logOut: this.$t("header.logOut") } } }
替换后的 Header.vue 如下,其余页面修改类似修改即可。
<template> <div class="header"> <!-- 是否展开侧边栏 --> <div class="header-title" @click="foldOrOpen"> <a class="el-icon-s-fold" v-if="foldAside" :title="language.foldAside" /> <a class="el-icon-s-unfold" v-else :title="language.unFoldAside" /> </div> <!-- 设置、文档、用户设置等 --> <div class="header-menu"> <el-menu mode="horizontal" class="header-menu-submenu"> <!-- 设置 --> <el-menu-item :title="language.setUp" index="1" @click="showSetup"> <i class="el-icon-setting"></i>{{language.setUp}} </el-menu-item> <!-- 帮助文档 --> <el-submenu :title="language.help" index="2"> <template slot="title"> <i class="el-icon-info"></i>{{language.help}} </template> <el-menu-item index="2-1"> <a href="https://www.cnblogs.com/l-y-h/" target="_blank" class="header-submenu-a">{{language.blogAddress}}</a> </el-menu-item> <el-menu-item index="2-2"> <a href="https://github.com/lyh-man/admin-vue-template.git" target="_blank" class="header-submenu-a">{{language.codeAddress}}</a> </el-menu-item> </el-submenu> <!-- 用户设置 --> <el-submenu :title="language.userSetUp" index="3"> <template slot="title"> <span class="header-span"> <img src="~@/assets/avatar.gif" :alt="userName"> {{ userName }} </span> </template> <el-menu-item index="3-1" @click="showPasswordBox"> <i class="el-icon-edit"></i>{{language.updatePassword}} </el-menu-item> <el-menu-item index="3-2" @click="logout"> <i class="el-icon-close"></i>{{language.logOut}} </el-menu-item> </el-submenu> </el-menu> </div> <!-- 密码修改框 --> <UpdatePassword v-if="updatePasswordVisible" ref="updatePassowrd"></UpdatePassword> <!-- 设置框 --> <Setup v-if="setUpVisible" ref="setUp"></Setup> </div> </template> <script> import UpdatePassword from '@/views/home/UpdatePassword.vue' import Setup from '@/views/home/Setup.vue' import {mapState} from 'vuex' export default { name: 'Header', data() { return { // 是否展开侧边栏 foldAside: true, // 默认用户名 // userName: 'admin', // 是否展开密码框 updatePasswordVisible: false, // 是否展开设置框 setUpVisible: false } }, computed: { // ...mapState('user', {userName: 'userName'}), ...mapState('user', ['userName']), // 定义国际化显示 language() { return { foldAside: this.$t("header.foldAside"), unFoldAside: this.$t("header.unFoldAside"), setUp: this.$t("header.setUp"), help: this.$t("header.help"), blogAddress: this.$t("header.blogAddress"), codeAddress: this.$t("header.codeAddress"), userSetUp: this.$t("header.userSetUp"), updatePassword: this.$t("header.updatePassword"), logOut: this.$t("header.logOut") } } }, components: { // 引入密码框组件 UpdatePassword, // 引入设置框组件 Setup }, methods: { // 展开设置框 showSetup() { this.setUpVisible = true; this.$nextTick(() => { this.$refs.setUp.init() }) }, // 展开密码修改框 showPasswordBox() { this.updatePasswordVisible = true // this.$nextTick 表示数据渲染后,执行密码框初始化 this.$nextTick(() => { this.$refs.updatePassowrd.init() }) }, // 展开、折叠侧边栏 foldOrOpen() { this.foldAside = !this.foldAside // this.$emit 用于触发父组件的方法,并传递参数值 this.$emit("foldOrOpenAside", this.foldAside) }, // 退出登录,回到登录界面 logout() { // TODO:退出逻辑待完成 // alert("退出逻辑未完成"); this.$router.push({ name: "Login" }) } } } </script> <style> .header { padding: 0 10px; display: flex; height: 50px; line-height: 50px; } .header-title { height: 50px; width: 50px; float: left; font-size: 50px; cursor: pointer; } .header-menu { height: 50px; width: 100%; flex: 1; line-height: 50px; font-size: 30px; } .header-menu-submenu { float: right; } .header-submenu-a { text-decoration: none; color: #4CC4B8; font-weight: bold; font-size: 16px; } .header-submenu-a:hover { background-color: #2C3E50; } .el-menu--horizontal>.el-menu-item, .el-menu--horizontal>.el-submenu .el-submenu__title { height: 50px !important; line-height: 50px !important; } .el-menu--collapse .el-menu .el-submenu, .el-menu--popup { min-width: auto !important; } .header-span img { width: 40px; height: 40px; line-height: 40px; margin: 5px 10px 10px 10px; } .header-span { font-size: 20px; } </style>
替换后的 Aside.vue 如下:
<template> <div> <!-- 系统 Logo --> <el-aside class="header-logo" :width="asideWidth"> <div @click="$router.push({ name: 'Home' })"> <a v-if="foldAside">{{language.adminCenter}}</a> <a v-else>{{language.admin}}</a> </div> </el-aside> <el-aside class="aside" :width="asideWidth" :class='"icon-size-" + iconSize'> <el-scrollbar style="height: 100%; width: 100%;"> <!-- default-active 表示当前选中的菜单,默认为 home。 collapse 表示是否折叠菜单,仅 mode 为 vertical(默认)可用。 collapseTransition 表示是否开启折叠动画,默认为 true。 background-color 表示背景颜色。 text-color 表示字体颜色。 --> <el-menu :default-active="menuActiveName || 'home'" :collapse="!foldAside" :collapseTransition="false" background-color="#263238" text-color="#8a979e"> <el-menu-item index="home" @click="$router.push({ name: 'Home' })"> <i class="el-icon-s-home"></i> <span slot="title">{{language.homePage}}</span> </el-menu-item> <el-submenu index="demo"> <template slot="title"> <i class="el-icon-star-off"></i> <span>demo</span> </template> <el-menu-item index="demo-echarts" @click="$router.push({ name: 'Echarts' })"> <i class="el-icon-s-data"></i> <span slot="title">echarts</span> </el-menu-item> <el-menu-item index="demo-ueditor" @click="$router.push({ name: 'Ueditor' })"> <i class="el-icon-document"></i> <span slot="title">ueditor</span> </el-menu-item> </el-submenu> </el-menu> </el-scrollbar> </el-aside> </div> </template> <script> export default { name: 'Aside', props: ['foldAside'], data() { return { // 保存当前选中的菜单 menuActiveName: 'home', // 保存当前侧边栏的宽度 asideWidth: '200px', // 用于拼接当前图标的 class 样式 iconSize: 'true' } }, computed: { // 国际化 language() { return { adminCenter: this.$t("aside.adminCenter"), admin: this.$t("aside.admin"), homePage: this.$t("aside.homePage") } } }, watch: { // 监视是否折叠侧边栏,折叠则宽度为 64px。 foldAside(val) { this.asideWidth = val ? '200px' : '64px' this.iconSize = val } } } </script> <style> .aside { margin-bottom: 0; height: 100%; max-height: calc(100% - 50px); width: 100%; max-width: 200px; background-color: #263238; text-align: left; right: 0; } .header-logo { background-color: #17b3a3; text-align: center; height: 50px; line-height: 50px; width: 200px; font-size: 24px; color: #fff; font-weight: bold; margin-bottom: 0; cursor: pointer; } .el-submenu .el-menu-item { max-width: 200px !important; } .el-scrollbar__wrap { overflow-x: hidden !important; } .icon-size-false I { font-size: 30px !important; } .icon-size-true I { font-size: 18px !important; } </style>
3、在主界面导航栏添加一个设置页面 Setup.vue
(1)简介
用于定义系统设置,比如设置语言。
(2)定义页面内容:
用于之前引入了 vuex,可以使用 vuex 保存 language 的状态。
添加一个 common.js 模块,用于保存公共的状态。
export default { // 开启命名空间(防止各模块间命名冲突),访问时需要使用 模块名 + 方法名 namespaced: true, // 管理数据(状态) state: { // 用于保存语言设置(国际化),默认为中文 language: 'zh' }, // 更改 state(同步) mutations: { updateLanguage(state, data) { state.language = data } }, // 异步触发 mutations actions: { updateLanguage({commit, state}, data) { commit("updateLanguage", data) } } }
将定义好的模块在 vuex 的入口文件 index.js 中引入。
引入 vuex,通过 ...mapState 以及 ...mapActions 引入。
{{$t("")}} 进行国际化处理。
<template> <el-dialog :title="setUp.setting" :visible.sync="visible" :append-to-body="true"> <el-row :gutter="20"> <el-col :span="7" :offset="2"> {{setUp.languageSettings}} </el-col> <el-col :span="4"> <el-radio v-model="language" label="zh">{{setUp.zh}}</el-radio> </el-col> <el-col :span="4"> <el-radio v-model="language" label="en">{{setUp.en}}</el-radio> </el-col> </el-row> </el-dialog> </template> <script> import {mapState, mapActions} from 'vuex' export default { name: 'setUp', data() { return { visible: false, language: 'en' } }, computed: { ...mapState('common', {lang: 'language'}), setUp() { return { setting: this.$t("language.setting"), languageSettings: this.$t("language.languageSettings"), zh: this.$t("language.zh"), en: this.$t("language.en") } } }, methods: { ...mapActions('common', ['updateLanguage']), // 初始化 init() { this.visible = true this.language = this.lang this.$i18n.locale = this.lang } }, watch: { language(val) { this.updateLanguage(val) this.$i18n.locale = val } } } </script> <style> </style>
(3)在 Header.vue 中引入,引入方式类似于 UpdatePassword.vue
大概步骤:
Step1:import 导入 vue 组件。
Step2:在 components 中声明组件。
Step3:添加开关属性,并指定方法触发组件。
(4)页面显示如下:
4、在登录界面添加一个语言选择框
上面在主界面的导航栏上添加了一个语言设置选项,但是登录界面没有语言设置选项,看着有点别扭,给登录界面加上一个语言选择框,可以将数据传向后台,控制后台的国际化。
(1)添加一个语言选择框
<el-form-item> <el-select v-model="dataForm.language" :placeholder="language" class="login-select"> <el-option :label="zh" value="zh"></el-option> <el-option :label="en" value="en"></el-option> </el-select> </el-form-item> 【样式显示有些问题,稍作调整】 .el-scrollbar__wrap { overflow-x: scroll !important; } .login-select { left: -120px; width: 120px; }
(2)同样引入 vuex 对语言进行管理(写法类似于 Setup.vue)。
Step1:
通过 mapActions 引入 common 模块中的方法:,并定义方法进行操作。
import { mapActions } from 'vuex' ...mapActions('common', {updateLang: "updateLanguage"}) updateLanguage() { this.$i18n.locale = this.dataForm.language this.updateLang(this.dataForm.language) }
Step2:
定义触发方法,通过 el-select 标签的 change 事件可以触发。
<el-form-item> <el-select v-model="dataForm.language" :placeholder="language" class="login-select" @change="updateLanguage"> <el-option :label="zh" value="zh"></el-option> <el-option :label="en" value="en"></el-option> </el-select> </el-form-item>
完整的 Login.vue:
<template> <div class="login-wrapper"> <div class="login-content"> <div class="login-main"> <h2 class="login-main-title">{{language.title}}</h2> <el-form :model="dataForm" :rules="dataRule" ref="dataForm" @keyup.enter.native="dataFormSubmit()" status-icon> <el-form-item prop="userName"> <el-input v-model="dataForm.userName" :placeholder="language.userName"></el-input> </el-form-item> <el-form-item prop="password"> <el-input v-model="dataForm.password" type="password" :placeholder="language.password"></el-input> </el-form-item> <el-form-item> <el-select v-model="dataForm.language" :placeholder="language.language" class="login-select" @change="updateLanguage"> <el-option :label="language.zh" value="zh"></el-option> <el-option :label="language.en" value="en"></el-option> </el-select> </el-form-item> <el-form-item> <el-button class="login-btn-submit" type="primary" @click="dataFormSubmit()">{{language.signIn}}</el-button> </el-form-item> </el-form> </div> </div> </div> </template> <script> import { mapState,mapActions } from 'vuex' export default { data() { return { dataForm: { userName: '', password: '', language: 'zh' }, dataRule: { userName: [{ required: true, message: this.$t("login.userNameNotNull"), trigger: 'blur' }], password: [{ required: true, message: this.$t("login.passwordNotNull"), trigger: 'blur' }] } } }, computed: { // 国际化 language() { return { title: this.$t("login.title"), userName: this.$t("login.userName"), password: this.$t("login.password"), language: this.$t("login.language"), zh: this.$t("language.zh"), en: this.$t("language.en"), signIn: this.$t("login.signIn") } } }, methods: { ...mapActions('user', ['updateName']), ...mapActions('common', {updateLang: "updateLanguage"}), // 提交表单 dataFormSubmit() { // TODO:登录代码逻辑待完善 // alert("登录代码逻辑未完善") this.$http({ url: '/auth/token', method: 'get' }).then(response => { this.$message({ message: this.$t("login.signInSuccess"), type: 'success' }) this.updateName(this.dataForm.userName) console.log(response) this.$router.push({ name: 'Home' }) }) }, updateLanguage() { this.$i18n.locale = this.dataForm.language this.updateLang(this.dataForm.language) } }, created() { // 页面创建时,获取当前系统语言,并显示在下拉框中 this.dataForm.language = this.$i18n.locale } } </script> <style> .login-wrapper { position: absolute; top: 0; right: 0; bottom: 0; left: 0; overflow: hidden; background-color: rgba(38, 50, 56, .6); background: url(~@/assets/login_bg.jpg) no-repeat; background-size: 100% 100%; } .login-content { position: absolute; top: 0; right: 0; bottom: 0; left: 0; margin: auto; height: 350px; width: 400px; background-color: #112234; opacity: .8; } .login-main { color: beige; padding: 20px 20px 10px 20px; } .el-scrollbar__wrap { overflow-x: scroll !important; } .login-select { left: -120px; width: 120px; } </style>
(3)页面显示