irpas技术客

一篇文带你使用vue完成一个完整后台_xiaoweiwei99_vue3 后台模板

未知 3359

介绍

vue-element-admin 是一个后台前端解决方案,它基于 vue 和 element-ui实现。它使用了最新的前端技术栈,内置了 i18 国际化解决方案,动态路由,权限验证,提炼了典型的业务模型,提供了丰富的功能组件,它可以帮助你快速搭建企业级中后台产品原型。相信不管你的需求是什么,本项目都能帮助到你

vue-element-admin定位是后台集成方案,不适合当基础模板进行二次开发,项目集成了许多用不到的功能,会造成代码沉余vue-admin-template是一个后台基础模板,建议使用此模板进行二次开发electron-vue-admin是一个桌面终端,如果进行桌面终端开发可以使用此模板 功能 - 登录 / 注销 - 权限验证 - 页面权限 - 指令权限 - 权限配置 - 二步登录 - 多环境发布 - dev sit stage prod - 全局功能 - 国际化多语言 - 多种动态换肤 - 动态侧边栏(支持多级路由嵌套) - 动态面包屑 - 快捷导航(标签页) - Svg Sprite 图标 - 本地/后端 mock 数据 - Screenfull全屏 - 自适应收缩侧边栏 - 编辑器 - 富文本 - Markdown - JSON 等多格式 - Excel - 导出excel - 导入excel - 前端可视化excel - 导出zip - 表格 - 动态表格 - 拖拽表格 - 内联编辑 - 错误页面 - 401 - 404 - 組件 - 头像上传 - 返回顶部 - 拖拽Dialog - 拖拽Select - 拖拽看板 - 列表拖拽 - SplitPane - Dropzone - Sticky - CountTo - 综合实例 - 错误日志 - Dashboard - 引导页 - ECharts 图表 - Clipboard(剪贴复制) - Markdown2html 目录结构 ├── build # 构建相关 ├── mock # 项目mock 模拟数据 ├── plop-templates # 基本模板 ├── public # 静态资源 │ │── favicon.ico # favicon图标 │ └── index.html # html模板 ├── src # 源代码 │ ├── api # 所有请求 │ ├── assets # 主题 字体等静态资源 │ ├── components # 全局公用组件 │ ├── directive # 全局指令 │ ├── filters # 全局 filter │ ├── icons # 项目所有 svg icons │ ├── lang # 国际化 language │ ├── layout # 全局 layout │ ├── router # 路由 │ ├── store # 全局 store管理 │ ├── styles # 全局样式 │ ├── utils # 全局公用方法 │ ├── vendor # 公用vendor │ ├── views # views 所有页面 │ ├── App.vue # 入口页面 │ ├── main.js # 入口文件 加载组件 初始化等 │ └── permission.js # 权限管理 ├── tests # 测试 ├── .env.xxx # 环境变量配置 ├── .eslintrc.js # eslint 配置项 ├── .babelrc # babel-loader 配置 ├── .travis.yml # 自动化CI配置 ├── vue.config.js # vue-cli 配置 ├── postcss.config.js # postcss 配置 └── package.json # package.json 安装 # 克隆项目 git clone https://github.com/PanJiaChen/vue-element-admin.git # 进入项目目录 cd vue-element-admin # 安装依赖 npm install # 速度过慢可以使用下面方法进行指定下载镜像原 # 也可以使用nrm选择下载镜像原 # 建议不要用 cnpm 安装 会有各种诡异的bug 可以通过如下操作解决 npm 下载速度慢的问题 npm install --registry=https://registry.npm.taobao.org # 注意:此框架启动和平常我们自己设置不同,要使用如下方法进行启动 # 本地开发 启动项目 npm run dev

启动完成后会自动打开浏览器访问 http://localhost:9527,可以看到页面就证明你操作成功了

layout布局

在大部分页面中都是基于layout的,除:404、login等没有使用到该布局

layout整合了页面所有布局进行分块展示

整个板块被分成了三部分

layout主要编排

Src目录下 入口文件 main.js

里面有用到自定义的mock文件访问,我们要将其注释掉 src下的mock文件夹建议删掉,我们后期不会用到

App.vue

src下,除了main.js还有两个文件,permission.js 和settings.js

permission.js

permission.js 是控制页面登录权限的文件,我们可以先将其全部注释掉,后期用到在慢慢添加

settings.js

settings.js则是对于一些项目信息的配置,里面有三个属性 **title(项目名称),fixedHeader(固定头部),sidebarLogo(显示左侧菜单logo) 其中的配置我们在其他地方会用到,不要去动

API模块和请求封装模块介绍

API模块的单独请求和 request模块的封装

Axios的拦截器

axios的拦截器原理: 通过create创建一个新的axios实例

// 创建了一个新的axios实例 const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url // withCredentials: true, // send cookies when cross-domain requests timeout: 5000 // request timeout })

请求拦截器 主要处理 token的_统一注入问题_

service.interceptors.request.use( config => { if (store.getters.token) { config.headers['X-Token'] = getToken() } return config }, error => { return Promise.reject(error) } )

响应拦截器 处理 返回的数据异常 和_数据结构_问题

// 响应拦截器 service.interceptors.response.use( response => { const res = response.data // if the custom code is not 20000, it is judged as an error. // 自定义代码返回值是自己商量的,按照自己的需求来编写 if (res.code !== 20000) { Message({ message: res.message || 'Error', type: 'error', duration: 5 * 1000 }) // 自定义代码返回值是自己商量的,按照自己的需求来编写 if (res.code === 50008 || res.code === 50012 || res.code === 50014) { // to re-login MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', { confirmButtonText: 'Re-Login', cancelButtonText: 'Cancel', type: 'warning' }).then(() => { store.dispatch('user/resetToken').then(() => { location.reload() }) }) } return Promise.reject(new Error(res.message || 'Error')) } else { return res } }, error => { console.log('err' + error) // for debug Message({ message: error.message, type: 'error', duration: 5 * 1000 }) return Promise.reject(error) } )

上面是在 src/utils/request.js下的源代码 我们只需要保留:

// 导出一个axios的实例 而且这个实例要有请求拦截器 响应拦截器 import axios from 'axios' const service = axios.create() // 创建一个axios的实例 service.interceptors.request.use() // 请求拦截器 service.interceptors.response.use() // 响应拦截器 export default service // 导出axios实例 单独封装api

我们习惯将所有的api请求放到api目录下统一管理,按照模块进行划分使用

api/user.js

import request from '@/utils/request' export function login(data) { return request({ url: '/vue-admin-template/user/login', method: 'post', data }) } export function getInfo(token) { return request({ url: '/vue-admin-template/user/info', method: 'get', params: { token } }) } export function logout() { return request({ url: '/vue-admin-template/user/logout', method: 'post' }) }

我们只需保留如下代码,后期在进行添加

import request from '@/utils/request' export function login(data) { } export function getInfo(token) { } export function logout() { } 登录模块 设置固定的本地访问端口和网站名称

设置统一的本地访问端口和网站title

本地服务端口: 在vue.config.js中进行设置

vue.config.js 就是vue项目相关的编译,配置,打包,启动服务相关的配置文件,它的核心在于webpack,但是又不同于webpack,相当于改良版的webpack 我们看到上面是一个环境变量而不是实际地址,那么我们在哪设置了呢

在项目下我们会发现两个文件 development => 开发环境

production => 生产环境

当我们运行npm run dev进行开发调试的时候,此时会加载执行**.env.development**文件内容

当我们运行npm run build:prod进行生产环境打包的时候,会加载执行**.env.production**文件内容

如果想要设置开发环境的接口,直接在**.env.development**文件中写入对于变量直接赋值即可

# just a flag ENV = 'development' # base api VUE_APP_BASE_API = 'api/private/v1/'

如果想要设置生产环境的接口**.env.production**文件中写入对于变量直接赋值即可

# just a flag ENV = 'production' # base api VUE_APP_BASE_API = 'api/private/v1/' 网站名称

src/settings.js中 title 就是网站名称 配置完我们要进行重启,否则有些配置不会生效

登录页面

设置头部名称:

<!-- 放置标题图片 @是设置的别名--> <div class="title-container"> <h3 class="title">海豚电商后台管理平台</h3> </div>

设置背景图片: 可根据需求更改

/* reset element-ui css */ .login-container { background-image: url('~@/assets/common/bgc.jpg'); // 设置背景图片 background-position: center; // 将图片位置设置为充满整个屏幕 } 对应代码:

登录表单的校验

el-form表单校验的条件 用户名和密码的校验:

<el-form-item prop="username"> <span class="svg-container"> <svg-icon icon-class="user" /> </span> <el-input ref="username" v-model="loginForm.username" v-focus placeholder="Username" name="username" type="text" tabindex="1" auto-complete="on" /> </el-form-item> <el-form-item prop="password"> <span class="svg-container"> <svg-icon icon-class="password" /> </span> <el-input :key="passwordType" ref="password" v-model="loginForm.password" :type="passwordType" placeholder="Password" name="password" tabindex="2" auto-complete="on" @keyup.enter.native="handleLogin" /> <span class="show-pwd" @click="showPwd"> <svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" /> </span> </el-form-item> const validateUsername = (rule, value, callback) => { if (value.length < 5) { callback(new Error('用户名最少5位')) } else if (value.length > 12) { callback(new Error('用户名最长12位')) } else { callback() } } const validatePassword = (rule, value, callback) => { if (value.length < 5) { callback(new Error('用户名最少5位')) } else if (value.length > 16) { callback(new Error('用户名最长16位')) } else { callback() } } loginRules: { username: [{ required: true, trigger: 'blur', validator: validateUsername }], password: [ { required: true, trigger: 'blur', validator: validatePassword }, { min: 5, max: 12, trigger: 'blur', message: '密码长度应该在5-12位之间' } ] } Vue-Cli配置跨域代理

出现跨域的原因是什么呢? 因为当下流行的是前后端分离单独开发,前端项目和后端接口不在同域名之下,那前端访问后端接口就出现跨域了 那么问题就来了 如何解决呢? 我们所遇到的这种跨域是位于开发环境的,真正部署上线时的跨域是生产环境的,解决方式又不同 我们先解决开发环境,生产环境在打包上线事可以解决,后面再讲

解决开发环境的跨域问题

开发环境的跨域,也就是在vue-cli脚手架环境下开发启动服务时,我们访问接口所遇到的跨域问题,vue-cli为我们在本地开启了一个服务,可以通过这个服务帮我们代理请求,解决跨域问题 也就是vue-cli配置webpack的反向代理

在vue.config.js中进行反向代理配置

module.exports = { devServer: { proxy: { 'api/private/v1/': { target: 'http://127.0.0.1:8888', // 我们要代理的地址,当匹配到上面的'api/private/v1/'时,会将http://localhost:9528 替换成 http://127.0.0.1:8888 changeOrigin: true, // 是否跨越 需要设置此值为 true 才可以让本地服务代理我们发送请求 pathRewrite: { // 重新路由 localhost:8888/api/login => http://127.0.0.1:8888/api/login '^/api': '/api', '/hr': '' } } } } }

同时,还需要注意的是,我们同时需要注释掉 mock的加载,因为mock-server会导致代理服务的异常

// before: require('./mock/mock-server.js'), // 注释mock-server加载 封装单独的登录接口 export function login(data) { // 返回一个axios对象 => promise // 返回了一个promise对象 return request({ url: 'login', // 因为所有的接口都要跨域 表示所有的接口要带 /api method: 'post', data }) } 封装Vuex的登录Action并处理token 在Vuex中对token进行管理

上图中,组件直接和接口打交道,这并没有什么问题,但是ta用的钥匙来进行相互传递,我们需要让vuex来介入,将用户的token状态共享,更方便的读取 store/modules/user.js配置

// 状态 const state = {} // 修改状态 const mutations = {} // 执行异步 const actions = {} export default { namespaced: true, state, mutations, actions }

设置token共享状态

const state = { token: null } 操作 token

utils/auth.js 中,基础模板已经为我们提供了获取 token ,设置 token ,删除 token 的方法,可以直接使用

const TokenKey = 'haitun_token' export function getToken() { // return Cookies.get(TokenKey) return localStorage.getItem(TokenKey) } export function setToken(token) { // return Cookies.set(TokenKey, token) return localStorage.setItem(TokenKey, token) } export function removeToken() { // return Cookies.remove(TokenKey) return localStorage.removeItem(TokenKey) } 初始化token状态

store/modules/user.js

import { getToken, setToken, removeToken } from '@/utils/auth' const state = { token: getToken() // 设置token初始状态 token持久化 => 放到缓存中 } 提供修改token的mutations // 修改状态 const mutations = { // 设置token setToken(state, token) { state.token = token // 设置token 只是修改state的数据 123 =》 1234 setToken(token) // vuex和 缓存数据的同步 }, // 删除缓存 removeToken(state) { state.token = null // 删除vuex的token removeToken() // 先清除 vuex 再清除缓存 vuex和 缓存数据的同步 } } 封装登录的Action

登录action要做的事情,调用登录接口,成功后设置token到vuex,失败则返回失败

// 执行异步 const actions = { // 定义login action 也需要参数 调用action时 传递过来的参数 async login(context, data) { const result = await login(data) // 实际上是一个promise result是执行的结果 // axios默认给数据加了一层data if (result.data.success) { // 表示登录接口调用成功 也就是意味着你的用户名和密码是正确的 // 现在有用户token // actions 修改state 必须通过mutations context.commit('setToken', result.data.data) } } }

为了更好的让其他模块和组件更好的获取token数据,我们要在store/getters.js中将token值作为公共的访问属性放出

const getters = { sidebar: state => state.app.sidebar, device: state => state.app.device, token: state => state.user.token // 在根级的getters上 开发子模块的属性给别人看 给别人用 } export default getters

通过此内容,我们可以有个脑图画面了

区分axios在不同环境中的请求基础地址

前端两个主要区分环境,开发环境,生产环境

环境变量 $ process.env.NODE_ENV # 当为production时为生产环境 为development时为开发环境 我们可以在**.env.development和.env.production**定义变量,变量自动就为当前环境的值 基础模板在以上文件定义了变量VUE_APP_BASE_API,该变量可以作为axios请求的baseURL

# 开发环境的基础地址和代理对应 VUE_APP_BASE_API = '/api' --------- # 这里配置了/api,意味着需要在Nginx服务器上为该服务配置 nginx的反向代理对应/prod-api的地址 VUE_APP_BASE_API = '/prod-api'

也可以都写成一样的 方便管理

在request中设置baseUrl–基准

// 创建一个axios的实例 const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API, // 设置axios请求的基础的基础地址 timeout: 5000 // 定义5秒超时 }) 处理axios的响应拦截器

// 响应拦截器 service.interceptors.response.use(response => { // axios默认加了一层data const { success, message, data } = response.data // 要根据success的成功与否决定下面的操作 if (success) { return data } else { // 业务已经错误了 还能进then ? 不能 ! 应该进catch Message.error(message) // 提示错误消息 return Promise.reject(new Error(message)) } }, error => { Message.error(error.message) // 提示错误信息 return Promise.reject(error) // 返回执行错误 让当前的执行链跳出成功 直接进入 catch }) 登录页面调用登录action,处理异常

引入辅助函数

import { mapActions } from 'vuex' // 引入vuex的辅助函数 --------------------- methods: { ...mapActions(['user/login']) }

调用登录

this.$refs.loginForm.validate(async isOK => { if (isOK) { try { this.loading = true // 只有校验通过了 我们才去调用action await this['user/login'](this.loginForm) // 应该登录成功之后 // 登陆成功后跳转到主页 this.$router.push('/') } catch (error) { console.log(error) } finally { // 不论执行try 还是catch 都去关闭转圈 this.loading = false } } }) 解析

首先使用到了elementUI的from表单进行编写

中间在前台使用表单验证进行对用户输入的账户密码进行对比,是否符合标准,如果不符合我们定义的标准进行一个提示

我们对表单里面的输入框进行双向数据绑定使用v-model

用户输入完毕之后点击登录按钮时也要进行后台验证,当我们点击登录发送请求到后台入库查询账户密码是否正确,如不正确会弹出提示

在表单里面使用了<svg>标签引入 icon 图标

我们首先在srccomponents下创建了SvgIcon组件

我们向外暴露了两个属性

通过 computed 监控 icon 的名字和其自定义的样式,当没有指定自定义样式时候,会采用默认样式,否则会再加上自定义 class

iconName() { return `#icon-${this.iconClass}` }, svgClass() { if (this.className) { return 'svg-icon ' + this.className } else { return 'svg-icon' } }

然后进行默认样式的编写

在 srcicons 中的 index.js 中引入 svg 组件 import IconSvg from '@/components/IconSvg'

使用全局注册 icon-svg Vue.component('icon-svg', IconSvg)

这样就可以在项目中任意地方使用

为了便于集中管理图标,所有图标均放在 @/icons/svg

@代表找到src目录

require.context 有三个参数:

参数一:说明需要检索的目录参数二:是否检索子目录参数三: 匹配文件的正则表达式

在@/main.js中引入import '@/icons'这样在任意页面就可以成功使用组件了

在页面中使用就可以进行使用了

<svg-icon icon-class="password" class-name="password" /> 完整代码 <template> <div class="login-container"> <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" auto-complete="on" label-position="left" > <div class="title-container"> <h3 class="title">海豚电商后台管理平台</h3> </div> <el-form-item prop="username"> <span class="svg-container"> <svg-icon icon-class="user" /> </span> <el-input ref="username" v-model="loginForm.username" v-focus placeholder="Username" name="username" type="text" tabindex="1" auto-complete="on" /> </el-form-item> <el-form-item prop="password"> <span class="svg-container"> <svg-icon icon-class="password" /> </span> <el-input :key="passwordType" ref="password" v-model="loginForm.password" :type="passwordType" placeholder="Password" name="password" tabindex="2" auto-complete="on" @keyup.enter.native="handleLogin" /> <span class="show-pwd" @click="showPwd"> <svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" /> </span> </el-form-item> <el-button :loading="loading" type="primary" style="width: 100%; margin-bottom: 30px" @click.native.prevent="handleLogin" >立即登录</el-button > <!-- <div class="tips"> <span style="margin-right: 20px">username: admin</span> <span> password: any</span> </div> --> </el-form> </div> </template> <script> import { validUsername } from '@/utils/validate' export default { name: 'Login', data () { const validateUsername = (rule, value, callback) => { if (value.length < 5) { callback(new Error('用户名最少5位')) } else if (value.length > 12) { callback(new Error('用户名最长12位')) } else { callback() } } const validatePassword = (rule, value, callback) => { if (value.length < 5) { callback(new Error('用户名最少5位')) } else if (value.length > 16) { callback(new Error('用户名最长16位')) } else { callback() } } return { loginForm: { username: 'admin', password: '123456' }, loginRules: { username: [{ required: true, trigger: 'blur', validator: validateUsername }], password: [ { required: true, trigger: 'blur', validator: validatePassword }, { min: 5, max: 12, trigger: 'blur', message: '密码长度应该在5-12位之间' } ] }, loading: false, passwordType: 'password', redirect: undefined } }, watch: { $route: { handler: function (route) { this.redirect = route.query && route.query.redirect }, immediate: true } }, methods: { showPwd () { if (this.passwordType === 'password') { this.passwordType = '' } else { this.passwordType = 'password' } this.$nextTick(() => { this.$refs.password.focus() }) }, async handleLogin () { try { await this.$refs.loginForm.validate() this.loading = true await this.$store.dispatch('user/login', this.loginForm) // console.log('ssss') // 登陆成功后跳转到主页 this.$router.push({ path: '/' }) this.loading = false } catch (err) { this.loading = false console.log(err) return false } } } } </script> <style lang="scss"> /* 修复input 背景不协调 和光标变色 */ /* Detail see https://github.com/PanJiaChen/vue-element-admin/pull/927 */ $bg: #283443; $light_gray: #fff; $cursor: #fff; @supports (-webkit-mask: none) and (not (cater-color: $cursor)) { .login-container .el-input input { color: $cursor; } } /* reset element-ui css */ .login-container { .el-input { display: inline-block; height: 47px; width: 85%; input { background: transparent; border: 0px; -webkit-appearance: none; border-radius: 0px; padding: 12px 5px 12px 15px; color: $light_gray; height: 47px; caret-color: $cursor; &:-webkit-autofill { box-shadow: 0 0 0px 1000px $bg inset !important; -webkit-text-fill-color: $cursor !important; } } } .el-form-item { border: 1px solid rgba(255, 255, 255, 0.1); background: rgba(0, 0, 0, 0.1); border-radius: 5px; color: #454545; } } </style> <style lang="scss" scoped> $bg: #2d3a4b; $dark_gray: #889aa4; $light_gray: #eee; .login-container { min-height: 100%; width: 100%; background-color: $bg; overflow: hidden; .login-form { position: relative; width: 520px; max-width: 100%; padding: 160px 35px 0; margin: 0 auto; overflow: hidden; } .tips { font-size: 14px; color: #fff; margin-bottom: 10px; span { &:first-of-type { margin-right: 16px; } } } .svg-container { padding: 6px 5px 6px 15px; color: $dark_gray; vertical-align: middle; width: 30px; display: inline-block; } .title-container { position: relative; .title { font-size: 26px; color: $light_gray; margin: 0px auto 40px auto; text-align: center; font-weight: bold; } } .show-pwd { position: absolute; right: 10px; top: 7px; font-size: 16px; color: $dark_gray; cursor: pointer; user-select: none; } } </style> 主页模块

主页token拦截并进行处理 权限拦截的流程图

我们已经完成了登录的过程,并且存储了token,但是此时主页并没有因为token的有无而被控制访问权限

拦截处理代码

src/permission.js

import Vue from 'vue' import 'normalize.css/normalize.css' // A modern alternative to CSS resets import ElementUI from 'element-ui' import 'element-ui/lib/theme-chalk/index.css' import locale from 'element-ui/lib/locale/lang/zh-CN' // lang i18n import '@/styles/index.scss' // global css import App from './App' import store from './store' import router from './router' import i18n from '@/lang/index' import '@/icons' // icon import '@/permission' // permission control import directives from './directives' import Commponent from '@/components' import filters from './filter' import Print from 'vue-print-nb' // 引入打印 // set ElementUI lang to EN Vue.use(ElementUI, { locale }) // 如果想要中文版 element-ui,按如下方式声明 // Vue.use(ElementUI) Vue.use(Print) Vue.config.productionTip = false // 遍历注册自定义指令 for (const key in directives) { Vue.directive(key, directives[key]) } Vue.use(Commponent) // 注册自己的插件 // 注册全局的过滤器 // 遍历注册过滤器 for (const key in filters) { Vue.filter(key, filters[key]) } // 设置element为当前的语言 Vue.use(ElementUI, { i18n: (key, value) => i18n.t(key) }) new Vue({ el: '#app', router, store, i18n, render: h => h(App) }) 左侧导航

样式文件styles/siderbar.scss 设置背景图片

.scrollbar-wrapper { background: url('~@/assets/common/leftnavBg.png') no-repeat 0 100%; }

左侧logo图片src/setttings.js

module.exports = { title: '海豚电商后台管理平台', /** * @type {boolean} true | false * @description Whether fix the header */ fixedHeader: false, /** * @type {boolean} true | false * @description Whether show the logo in sidebar */ sidebarLogo: true // 显示logo }

设置头部图片结构 src/layout/components/Sidebar/Logo.vue

<div class="sidebar-logo-container" :class="{ collapse: collapse }"> <transition name="sidebarLogoFade"> <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/" > <img v-if="logo" src="@/assets/common/hai.png" class="sidebar-logo" /> <h1 v-else class="sidebar-title">{{ title }}</h1> </router-link> <router-link v-else key="expand" class="sidebar-logo-link" to="/"> <img v-if="logo" src="@/assets/common/hai.png" class="sidebar-logo" /> <h1 class="sidebar-title">{{ title }}</h1> </router-link> </transition> </div>

完整代码

<template> <div class="sidebar-logo-container" :class="{ collapse: collapse }"> <transition name="sidebarLogoFade"> <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/" > <img v-if="logo" src="@/assets/common/hai.png" class="sidebar-logo" /> <h1 v-else class="sidebar-title">{{ title }}</h1> </router-link> <router-link v-else key="expand" class="sidebar-logo-link" to="/"> <img v-if="logo" src="@/assets/common/hai.png" class="sidebar-logo" /> <h1 class="sidebar-title">{{ title }}</h1> </router-link> </transition> </div> </template> <script> export default { name: 'SidebarLogo', props: { collapse: { type: Boolean, required: true } }, data () { return { title: '海豚电商后台管理平台', logo: '@/assets/common/hai.png' } } } </script> <style lang="scss" scoped> .sidebarLogoFade-enter-active { transition: opacity 1.5s; } .sidebarLogoFade-enter, .sidebarLogoFade-leave-to { opacity: 0; } .sidebar-logo-container { position: relative; width: 100%; height: 50px; line-height: 50px; background: #2b2f3a; text-align: center; overflow: hidden; & .sidebar-logo-link { height: 100%; width: 100%; & .sidebar-logo { width: 32px; height: 32px; vertical-align: middle; margin-right: 12px; } & .sidebar-title { display: inline-block; margin: 0; color: #fff; font-weight: 600; line-height: 50px; font-size: 14px; font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif; vertical-align: middle; } } &.collapse { .sidebar-logo { margin-right: 0px; } } } </style> 头部内容的布局和样式

头部组件位置layout/components/Navbar.vue

添加公司名称时要面包屑

<!-- <breadcrumb class="breadcrumb-container" /> --> <!--面包屑--> <div class="app-breadcrumb"> 北京梦呓网络有限公司 <span class="breadBtn">v1.0.0</span> </div>

右侧头像和下拉菜单等设置

<div class="right-menu"> <!-- 语言切换插件 --> <lang class="right-menu-item lang_item" /> <!-- 全屏插件 --> <screen-full class="right-menu-item" /> <!-- 动态主题插件 --> <theme-picker class="right-menu-item" /> <el-dropdown class="avatar-container" trigger="click"> <div class="avatar-wrapper"> <img v-imgerr="defaultImg" src="https://bing.ioliu.cn/v1/rand?w=100&h=100" class="user-avatar" /> <span class="name">{{ username }}</span> <i class="el-icon-caret-bottom" /> </div> <el-dropdown-menu slot="dropdown" class="user-dropdown"> <router-link to="/"> <el-dropdown-item> 主页 </el-dropdown-item> </router-link> <a href="javascript:;"> <el-dropdown-item>邮箱</el-dropdown-item> </a> <a href="javascript:;"> <el-dropdown-item>设置</el-dropdown-item> </a> <el-dropdown-item @click.native="logout"> <span style="display: block">退出</span> </el-dropdown-item> </el-dropdown-menu> </el-dropdown> </div>

完整代码:样式+事件

<template> <div class="navbar"> <hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" /> <!-- <breadcrumb class="breadcrumb-container" /> --> <div class="app-breadcrumb"> 北京梦呓网络有限公司 <span class="breadBtn">v1.0.0</span> </div> <div class="right-menu"> <!-- 语言切换插件 --> <lang class="right-menu-item lang_item" /> <!-- 全屏插件 --> <screen-full class="right-menu-item" /> <!-- 动态主题插件 --> <theme-picker class="right-menu-item" /> <el-dropdown class="avatar-container" trigger="click"> <div class="avatar-wrapper"> <img v-imgerr="defaultImg" src="https://bing.ioliu.cn/v1/rand?w=100&h=100" class="user-avatar" /> <span class="name">{{ username }}</span> <i class="el-icon-caret-bottom" /> </div> <el-dropdown-menu slot="dropdown" class="user-dropdown"> <router-link to="/"> <el-dropdown-item> 主页 </el-dropdown-item> </router-link> <a href="javascript:;"> <el-dropdown-item>邮箱</el-dropdown-item> </a> <a href="javascript:;"> <el-dropdown-item>设置</el-dropdown-item> </a> <el-dropdown-item @click.native="logout"> <span style="display: block">退出</span> </el-dropdown-item> </el-dropdown-menu> </el-dropdown> </div> </div> </template> <script> import { mapGetters } from 'vuex' import Breadcrumb from '@/components/Breadcrumb' import Hamburger from '@/components/Hamburger' export default { components: { Breadcrumb, Hamburger }, data () { return { username: '超级管理员', defaultImg: require('@/assets/common/bigUserHeader.png') } }, created () { this.usereee() }, computed: { ...mapGetters([ 'sidebar', 'avatar' ]) }, methods: { usereee () { const res = localStorage.getItem('haitunuser') // const res = sessionStorage.getItem('user_info') const username = JSON.parse(res).username this.username = username }, toggleSideBar () { this.$store.dispatch('app/toggleSideBar') }, async logout () { await this.$store.dispatch('user/logout') this.$router.push(`/login`) } } } </script> <style lang="scss" scoped> .navbar { height: 50px; overflow: hidden; position: relative; background-image: linear-gradient(left, #3d6df8, #5b8cff); box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); .app-breadcrumb { display: inline-block; font-size: 18px; line-height: 50px; margin-left: 15px; color: #fff; cursor: text; .breadBtn { background: #84a9fe; font-size: 14px; padding: 0 10px; display: inline-block; height: 30px; line-height: 30px; border-radius: 10px; margin-left: 15px; } } .hamburger-container { line-height: 46px; height: 100%; float: left; cursor: pointer; transition: background 0.3s; -webkit-tap-highlight-color: transparent; &:hover { background: rgba(0, 0, 0, 0.025); } } .breadcrumb-container { float: left; } .right-menu { float: right; height: 100%; line-height: 50px; &:focus { outline: none; } .right-menu-item { display: inline-block; vertical-align: middle; padding: 0 8px; height: 100%; font-size: 18px; color: #5a5e66; vertical-align: text-bottom; &.hover-effect { cursor: pointer; transition: background 0.3s; &:hover { background: rgba(0, 0, 0, 0.025); } } } .avatar-container { margin-right: 30px; .avatar-wrapper { display: flex; margin-top: 5px; position: relative; .user-avatar { cursor: pointer; width: 40px; height: 40px; border-radius: 10px; vertical-align: middle; margin-bottom: 10px; } .name { color: #fff; vertical-align: middle; margin-left: 5px; } .user-dropdown { color: #fff; } .el-icon-caret-bottom { cursor: pointer; position: absolute; right: -20px; top: 25px; font-size: 12px; } } } } } .lang_item { // background-color: aqua; } </style> 储存用户信息

新增变量:src/store/modules/user.js

const getDefaultState = () => { return { token: getToken(), userInfo: {}, // 储存用户信息 } }

设置和删除用户资料 mutations

// 设置用户信息 set_userInfo (state, user) { state.userInfo = user setUSERINFO(user) } // 删除用户信息 removeUserInfo (state) { this.userInfo = {} }

建立用户名的映射 src/store/getters.js

const getters = { token: state => state.user.token, username: state => state.user.userInfo.username } export default getters

最后我们换成真实名称即可

<div class="avatar-wrapper"> <img src="@/assets/common/bigUserHeader.png" class="user-avatar" /> <span class="name">{{ username }}</span> <i class="el-icon-caret-bottom" style="color: #fff" /> </div>

这里可能会出现问题,在页面刷新拿不到数据,我们可以将其保存到本地中,然后取出

实现退出功能

退出:src/store/modules/user.js

// user logout logout (context) { // 删除token context.commit('removeToken') // 不仅仅删除了vuex中的 还删除了缓存中的 // 删除用户资料 context.commit('removeUserInfo') // 删除用户信息 },

mutation

removeToken (state) { state.token = null removeToken() removeUSERINFO() removeLocalMenus() }, removeUserInfo (state) { this.userInfo = {} },

头部菜单调用 src/layout/components/Navbar.vue

async logout () { await this.$store.dispatch('user/logout') this.$router.push(`/login`) }

完整代码:

<template> <div class="navbar"> <hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" /> <!-- <breadcrumb class="breadcrumb-container" /> --> <div class="app-breadcrumb"> 北京梦呓网络有限公司 <span class="breadBtn">v1.0.0</span> </div> <div class="right-menu"> <!-- 语言切换插件 --> <lang class="right-menu-item lang_item" /> <!-- 全屏插件 --> <screen-full class="right-menu-item" /> <!-- 动态主题插件 --> <theme-picker class="right-menu-item" /> <el-dropdown class="avatar-container" trigger="click"> <div class="avatar-wrapper"> <img v-imgerr="defaultImg" src="https://bing.ioliu.cn/v1/rand?w=100&h=100" class="user-avatar" /> <span class="name">{{ username }}</span> <i class="el-icon-caret-bottom" /> </div> <el-dropdown-menu slot="dropdown" class="user-dropdown"> <router-link to="/"> <el-dropdown-item> 主页 </el-dropdown-item> </router-link> <a href="javascript:;"> <el-dropdown-item>邮箱</el-dropdown-item> </a> <a href="javascript:;"> <el-dropdown-item>设置</el-dropdown-item> </a> <el-dropdown-item @click.native="logout"> <span style="display: block">退出</span> </el-dropdown-item> </el-dropdown-menu> </el-dropdown> </div> </div> </template> <script> import { mapGetters } from 'vuex' import Breadcrumb from '@/components/Breadcrumb' import Hamburger from '@/components/Hamburger' export default { components: { Breadcrumb, Hamburger }, data () { return { username: '超级管理员', defaultImg: require('@/assets/common/bigUserHeader.png') } }, created () { this.usereee() }, computed: { ...mapGetters([ 'sidebar', 'avatar' ]) }, methods: { usereee () { const res = localStorage.getItem('haitunuser') // const res = sessionStorage.getItem('user_info') const username = JSON.parse(res).username this.username = username }, toggleSideBar () { this.$store.dispatch('app/toggleSideBar') }, async logout () { await this.$store.dispatch('user/logout') this.$router.push(`/login`) } } } </script> <style lang="scss" scoped> .navbar { height: 50px; overflow: hidden; position: relative; background-image: linear-gradient(left, #3d6df8, #5b8cff); box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); .app-breadcrumb { display: inline-block; font-size: 18px; line-height: 50px; margin-left: 15px; color: #fff; cursor: text; .breadBtn { background: #84a9fe; font-size: 14px; padding: 0 10px; display: inline-block; height: 30px; line-height: 30px; border-radius: 10px; margin-left: 15px; } } .hamburger-container { line-height: 46px; height: 100%; float: left; cursor: pointer; transition: background 0.3s; -webkit-tap-highlight-color: transparent; &:hover { background: rgba(0, 0, 0, 0.025); } } .breadcrumb-container { float: left; } .right-menu { float: right; height: 100%; line-height: 50px; &:focus { outline: none; } .right-menu-item { display: inline-block; vertical-align: middle; padding: 0 8px; height: 100%; font-size: 18px; color: #5a5e66; vertical-align: text-bottom; &.hover-effect { cursor: pointer; transition: background 0.3s; &:hover { background: rgba(0, 0, 0, 0.025); } } } .avatar-container { margin-right: 30px; .avatar-wrapper { display: flex; margin-top: 5px; position: relative; .user-avatar { cursor: pointer; width: 40px; height: 40px; border-radius: 10px; vertical-align: middle; margin-bottom: 10px; } .name { color: #fff; vertical-align: middle; margin-left: 5px; } .user-dropdown { color: #fff; } .el-icon-caret-bottom { cursor: pointer; position: absolute; right: -20px; top: 25px; font-size: 12px; } } } } } .lang_item { // background-color: aqua; } </style> token失效介入

src/utils/auth.js

const timeKey = 'haitun-setTimeStamp' // 设置一个独一无二的key // 存储 token 的时间戳(存的是 setToken 方法执行的时间) // 获取时间戳 export function setTimeStamp () { return localStorage.setItem(timeKey, Date.now()) } // 获取 token 的过期时间 export function getTimeStamp () { return localStorage.getItem(timeKey) }

src/utils/request.js

import axios from 'axios' import { Message } from 'element-ui' import store from '@/store' import router from '../router' import { getToken, getTimeStamp, removeToken } from '@/utils/auth' // 定义 token 超时时间 const timeOut = 3600 * 24 * 3 // create an axios instance const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url timeout: 5000 // request timeout }) // request interceptor service.interceptors.request.use( // 注入token config => { // do something before request is sent if (store.getters.token) { // 判断当前 token 的时间戳是否过期 // 获取 token 设置的时间 const tokenTime = getTimeStamp() // 获取当前时间 const currenTime = Date.now() if ((currenTime - tokenTime) / 1000 > timeOut) { // 如果它为true表示 过期了 // token没用了 因为超时了 store.dispatch('user/logout') // 登出操作 // 跳转到登录页 router.push('/login') return Promise.reject(new Error('登录过期了,请重新登录')) } config.headers['Authorization'] = getToken() } return config }, error => { // do something with request error console.log(error) // for debug return Promise.reject(error) } ) // response interceptor service.interceptors.response.use( response => { const { meta: { status, msg }, data } = response.data // if the custom code is not 20000, it is judged as an error. if (status !== 200 && status !== 201) { // 处理 token 过期问题 if (status === 400 && msg === '无效的token') { removeToken() store.dispatch('user/logout') router.push('login') } Message({ message: msg || 'Error', type: 'error', duration: 5 * 1000 }) return Promise.reject(new Error(msg || 'Error')) } else { return data } }, error => { console.log('err' + error) // for debug Message({ message: error.message, type: 'error', duration: 5 * 1000 }) return Promise.reject(error) } ) export default service

在登录的时候,如果登录成功,我们就应该设置时间戳 src/store/modules

async login (context, userInfo) { const { username, password } = userInfo const res = await login({ username: username.trim(), password: password }) // 设置用户信息 const token = res.token context.commit('set_token', token) context.commit('set_userInfo', res) // 设置用户权限信息 const permission = await getMenus() const menus = filterPermission(permission) context.commit('set_menus', menus) },

token失效处理 src/utils/request.js

response => { const { meta: { status, msg }, data } = response.data // if the custom code is not 20000, it is judged as an error. if (status !== 200 && status !== 201) { // 处理 token 过期问题 if (status === 400 && msg === '无效的token') { removeToken() store.dispatch('user/logout') router.push('login') } Message({ message: msg || 'Error', type: 'error', duration: 5 * 1000 }) return Promise.reject(new Error(msg || 'Error')) } else { return data }

路由、页面、用户管理、权限管理等等需要什么页面自己开发即可,步骤相似的

多语言切换、tab页全屏 全屏插件的引用

安装全局插件screenfull

npm i screenfull

封装全屏插件src/components/ScreenFull/index.vue

<template> <!-- 放置一个图标 --> <div> <!-- 放置一个svg的图标 --> <svg-icon icon-class="fullscreen" style="color: #fff; width: 20px; height: 20px" @click="changeScreen" /> <!-- <i class="el-icon-rank" @click="changeScreen" /> --> </div> </template> <script> import ScreenFull from 'screenfull' export default { methods: { // 改变全屏 changeScreen () { if (!ScreenFull.isEnabled) { // 此时全屏不可用 this.$message.warning('此时全屏组件不可用') return } // document.documentElement.requestFullscreen() 原生js调用 // 如果可用 就可以全屏 ScreenFull.toggle() } } } </script> <style> </style>

全局注册该组件 src/components/index.js

import ScreenFull from './ScreenFull' Vue.component('ScreenFull', ScreenFull) // 注册全屏组件

放置layout/navbar.vue

<screen-full class="right-menu-item" /> ------------------------------- .right-menu-item { vertical-align: middle; } 设置动态主题

封装全屏插件 src/components/ThemePicker/index.vue

<template> <el-color-picker v-model="theme" :predefine="[ '#409EFF', '#1890ff', '#304156', '#212121', '#11a983', '#13c2c2', '#6959CD', '#f5222d', ]" class="theme-picker" popper-class="theme-picker-dropdown" /> </template> <script> const version = require('element-ui/package.json').version // element-ui version from node_modules const ORIGINAL_THEME = '#409EFF' // default color export default { data () { return { chalk: '', // content of theme-chalk css theme: '' } }, computed: { defaultTheme () { return this.$store.state.settings.theme } }, watch: { defaultTheme: { handler: function (val, oldVal) { this.theme = val }, immediate: true }, async theme (val) { const oldVal = this.chalk ? this.theme : ORIGINAL_THEME if (typeof val !== 'string') return const themeCluster = this.getThemeCluster(val.replace('#', '')) const originalCluster = this.getThemeCluster(oldVal.replace('#', '')) console.log(themeCluster, originalCluster) const $message = this.$message({ message: ' Compiling the theme', customClass: 'theme-message', type: 'success', duration: 0, iconClass: 'el-icon-loading' }) const getHandler = (variable, id) => { return () => { const originalCluster = this.getThemeCluster(ORIGINAL_THEME.replace('#', '')) const newStyle = this.updateStyle(this[variable], originalCluster, themeCluster) let styleTag = document.getElementById(id) if (!styleTag) { styleTag = document.createElement('style') styleTag.setAttribute('id', id) document.head.appendChild(styleTag) } styleTag.innerText = newStyle } } if (!this.chalk) { const url = `https://unpkg.com/element-ui@${version}/lib/theme-chalk/index.css` await this.getCSSString(url, 'chalk') } const chalkHandler = getHandler('chalk', 'chalk-style') chalkHandler() const styles = [].slice.call(document.querySelectorAll('style')) .filter(style => { const text = style.innerText return new RegExp(oldVal, 'i').test(text) && !/Chalk Variables/.test(text) }) styles.forEach(style => { const { innerText } = style if (typeof innerText !== 'string') return style.innerText = this.updateStyle(innerText, originalCluster, themeCluster) }) this.$emit('change', val) $message.close() } }, methods: { updateStyle (style, oldCluster, newCluster) { let newStyle = style oldCluster.forEach((color, index) => { newStyle = newStyle.replace(new RegExp(color, 'ig'), newCluster[index]) }) return newStyle }, getCSSString (url, variable) { return new Promise(resolve => { const xhr = new XMLHttpRequest() xhr.onreadystatechange = () => { if (xhr.readyState === 4 && xhr.status === 200) { this[variable] = xhr.responseText.replace(/@font-face{[^}]+}/, '') resolve() } } xhr.open('GET', url) xhr.send() }) }, getThemeCluster (theme) { const tintColor = (color, tint) => { let red = parseInt(color.slice(0, 2), 16) let green = parseInt(color.slice(2, 4), 16) let blue = parseInt(color.slice(4, 6), 16) if (tint === 0) { // when primary color is in its rgb space return [red, green, blue].join(',') } else { red += Math.round(tint * (255 - red)) green += Math.round(tint * (255 - green)) blue += Math.round(tint * (255 - blue)) red = red.toString(16) green = green.toString(16) blue = blue.toString(16) return `#${red}${green}${blue}` } } const shadeColor = (color, shade) => { let red = parseInt(color.slice(0, 2), 16) let green = parseInt(color.slice(2, 4), 16) let blue = parseInt(color.slice(4, 6), 16) red = Math.round((1 - shade) * red) green = Math.round((1 - shade) * green) blue = Math.round((1 - shade) * blue) red = red.toString(16) green = green.toString(16) blue = blue.toString(16) return `#${red}${green}${blue}` } const clusters = [theme] for (let i = 0; i <= 9; i++) { clusters.push(tintColor(theme, Number((i / 10).toFixed(2)))) } clusters.push(shadeColor(theme, 0.1)) return clusters } } } </script> <style> .theme-message, .theme-picker-dropdown { z-index: 99999 !important; } .theme-picker .el-color-picker__trigger { height: 26px !important; width: 26px !important; padding: 2px; } .theme-picker-dropdown .el-color-dropdown__link-btn { display: none; } .el-color-picker { height: auto !important; } </style>

全局注册该组件 src/components/index.js

import ThemePicker from './ThemePicker' Vue.component('ThemePicker', ThemePicker)

放置layout/navbar.vue

<theme-picker class="right-menu-item" /> 多语言实现

安装国际化的语言包 i18n

npm i vue-i18n

需要多语言的实例化文件 src/lang/index.js

import Vue from 'vue' // 引入Vue import VueI18n from 'vue-i18n' // 引入国际化的包 import Cookie from 'js-cookie' // 引入cookie包 import elementEN from 'element-ui/lib/locale/lang/en' // 引入饿了么的英文包 import elementZH from 'element-ui/lib/locale/lang/zh-CN' // 引入饿了么的中文包 import customZH from './zh' // 引入自定义中文包 import customEN from './en' // 引入自定义英文包 Vue.use(VueI18n) // 全局注册国际化包 export default new VueI18n({ locale: Cookie.get('language') || 'zh', // 从cookie中获取语言类型 获取不到就是中文 messages: { en: { ...elementEN, // 将饿了么的英文语言包引入 ...customEN }, zh: { ...elementZH, // 将饿了么的中文语言包引入 ...customZH } } })

main.js中对挂载 i18n的插件,并设置element为当前的语言

// 设置element为当前的语言 Vue.use(ElementUI, { i18n: (key, value) => i18n.t(key, value) }) new Vue({ el: '#app', router, store, i18n, render: h => h(App) })

引入自定义语言包 src/lang/zh.js , src/lang/en.js zh

export default { route: { Dashboard: '首页', manage: '后台管理', users: '用户管理', menus: '菜单管理', logs: '日志管理', example: '示例', table: '数据列表', // permissions: '权限管理', // employees: '员工', // employeesList: '员工管理', // employeesInfo: '个人信息', goods: '商品管理', postInfo: '岗位信息', manageSelf: '经理自助', setting: '设置', reports: '报表分析', employeesAdd: '添加员工', EditiNfo: '编辑信息', rights: '权限管理', print: '打印页面', form: '表单页', basicForm: '基础表单', stepForm: '分步表单', advancedList: '高级表单', step: '步骤', details: '详情页', BasicsDetails: '基础详情页', seniorDetails: '高级详情页', import: '导入', // 注册 register: '人资-注册', login: '人资-登录', // 审批 approvals: '审批', // 审批 salaryApproval: '工资审核', enterApproval: '入职审核', leaveApproval: '申请请假', quitApproval: '申请离职', overtimeApproval: '加班申请', securitySetting: '审批设置', // 员工 employees: '员工', employeesList: '员工列表', employeesInfo: '个人信息', employeesAdjust: '调岗', employeesLeave: '离职', employeesPrint: '打印', // 工资 salarys: '工资', salarysList: '工资列表', salarysSetting: '工资设置', salarysDetails: '工资详情', salarysHistorical: '历史归档', salarysMonthStatement: '月报表', // 社保 'social_securitys': '社保', socialSecuritys: '社保管理', socialDetail: '详情', socialHistorical: '历史归档', socialMonthStatement: '当月报表', // 组织架构 departments: '组织架构', 'departments-import': '引入', // 公司 settings: '公司设置', // 考勤 attendances: '考勤', usersApprovals: '用户审批', // saas企业 'saas-clients': '企业', 'saas-clients-details': '企业详情', // 权限 'permissions': '权限管理' // 权限管理 }, navbar: { search: '站内搜索', logOut: '退出登录', dashboard: '首页', github: '项目地址', screenfull: '全屏', theme: '换肤', lang: '多语言', error: '错误日志' }, login: { title: '人力资源管理系统', login: '登录', username: '账号', password: '密码', any: '随便填', thirdparty: '第三方登录', thirdpartyTips: '本地不能模拟,请结合自己业务进行模拟!!!' }, tagsView: { close: '关闭', closeOthers: '关闭其它', closeAll: '关闭所有', refresh: '刷新' }, table: { title: '请输入用户', search: '搜索', add: '添加', addUser: '新增用户', id: '序号', email: '邮箱', phone: '手机', name: '姓名', entryTime: '入职时间', hireForm: '聘用形式', jobNumber: '工号', department: '部门', managementForm: '管理形式', city: '工作城市', turnPositiveTime: '转正时间', permissionNew: '新增权限组', permissionUser: '权限组名称', imdsAi: '高级接口授权', avatar: '头像', introduction: '介绍', paddword: '密码', powerCode: '权限代码', powerDistriB: '权限分配', powerTitle: '权限标题', powerNav: '主导航', actions: '操作', edit: '编辑', delete: '删除', cancel: '取 消', confirm: '确 定', return: '返回', operationType: '操作类型', operationDate: '操作时间', date: '日期', submit: '提交', operator: '操作人', results: '执行结果', describe: '描述', save: '保存', signOut: '退出', reset: '重置', know: '我知道了', view: '查看' } }

en

export default { route: { dashboard: 'Dashboard', manage: 'manage', users: 'users', menus: 'menus', // permissions: 'permissions', logs: 'logs', example: 'example', table: 'table', postInfo: 'Job information', manageSelf: 'Manager self-help', setting: 'setting', reports: 'report', employeesAdd: 'add employees', EditiNfo: 'Edit information', print: 'print', form: 'form', basicForm: 'basic form', stepForm: 'step form', advancedList: 'advanced form', step: 'step', details: 'details', BasicsDetails: 'Basic details page', seniorDetails: 'Advanced details page', import: 'Import', register: 'HRM-Register', // 登录 login: 'HRM-Login', // 审批 approvals: 'Approvals', // 审批 salaryApproval: 'Salary-Approval', enterApproval: 'Enter-Approval', leaveApproval: 'Leave-Approval', quitApproval: 'Quit-Approval', overtimeApproval: 'Overtime-Approval', securitySetting: 'Security-Setting', // 员工 employees: 'Employees', employeesList: 'Employees-List', employeesInfo: 'Employees-Info', employeesAdjust: 'Employees-Adjust', employeesLeave: 'Employees-Leave', employeesPrint: 'Employees-Print', // 工资 salarys: 'salarys', salarysList: 'Salarys-List', salarysSetting: 'Salarys-Setting', salarysDetails: 'Salarys-Details', salarysHistorical: 'Salarys-Historical', salarysMonthStatement: 'Salarys-Month', // 社保 'social_securitys': 'Social', socialSecuritys: 'Social-Securitys', socialDetail: 'Social-Detail', socialHistorical: 'Social-Historical', socialMonthStatement: 'Social-Month', // 组织架构 departments: 'departments', 'departments-import': 'import', // 公司 settings: 'Company-Settings', // 考勤 attendances: 'Attendances', // 用户审批 usersApprovals: 'Users-Approvals', // 企业 'saas-clients': 'Saas-Clients', 'saas-clients-details': 'Saas-Details', 'permissions': 'permissions' // 权限管理 }, navbar: { search: 'search', logOut: 'Log Out', dashboard: 'Dashboard', github: 'Github', screenfull: 'screenfull', theme: 'theme', lang: 'i18n', error: 'error log' }, login: { title: 'itheima login', login: 'Log in', name: 'name', entryTime: 'entry time', hireForm: 'hire form', jobNumber: 'job number', department: 'department', managementForm: 'management form', city: 'city', turnPositiveTime: 'turn positive time', password: 'Password', any: 'any', thirdparty: 'Third', thirdpartyTips: 'Can not be simulated on local, so please combine you own business simulation! ! !' }, tagsView: { close: 'Close', closeOthers: 'Close Others', closeAll: 'Close All', refresh: 'refresh' }, table: { title: 'Title', search: 'Search', add: 'add', addUser: 'addUser', id: 'ID', email: 'Email', phone: 'Phone', username: 'User', permissionNew: 'permissionNew', permissionUser: 'Permission', imdsAi: 'Advanced interface authorization', avatar: 'Avatar', introduction: 'Introduction', paddword: 'paddWord', powerCode: 'Permission code', powerTitle: 'Permission title', actions: 'Actions', edit: 'Edit', delete: 'Delete', cancel: 'Cancel', confirm: 'Confirm', operationType: 'operationType', operationDate: 'operationDate', date: 'Date', operator: 'operator', results: 'results of enforcement', describe: 'Pedagogical operation', save: 'save', signOut: 'sign out', submit: 'submit', reset: 'reset', know: 'I Know', return: 'return', view: 'view' } }

index.js中同样引入该语言包

import customZH from './zh' // 引入自定义中文包 import customEN from './en' // 引入自定义英文包 Vue.use(VueI18n) // 全局注册国际化包 export default new VueI18n({ locale: Cookie.get('language') || 'zh', // 从cookie中获取语言类型 获取不到就是中文 messages: { en: { ...elementEN, // 将饿了么的英文语言包引入 ...customEN }, zh: { ...elementZH, // 将饿了么的中文语言包引入 ...customZH } } })

将左侧菜单变成多语言展示文本 layout/components/SidebarItem.vue

<item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="$t('route.'+onlyOneChild.name)" />

封装多语言组件 src/components/lang/index.vue

<template> <el-dropdown trigger="click" @command="changeLanguage"> <!-- 这里必须加一个div --> <div> <svg-icon style="color: #fff; font-size: 20px" icon-class="language" /> </div> <el-dropdown-menu slot="dropdown"> <el-dropdown-item command="zh" :disabled="'zh' === $i18n.locale" >中文</el-dropdown-item > <el-dropdown-item command="en" :disabled="'en' === $i18n.locale" >en</el-dropdown-item > </el-dropdown-menu> </el-dropdown> </template> <script> import Cookie from 'js-cookie' export default { methods: { changeLanguage (lang) { Cookie.set('language', lang) // 切换多语言 this.$i18n.locale = lang // 设置给本地的i18n插件 this.$message.success('切换多语言成功') } } } </script>

全局注册该组件 src/components/index.js

import lang from './lang' Vue.component('lang', lang) // 注册全屏组件

放置layout/navbar.vue

<lang class="right-menu-item" />


1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,会注明原创字样,如未注明都非原创,如有侵权请联系删除!;3.作者投稿可能会经我们编辑修改或补充;4.本站不提供任何储存功能只提供收集或者投稿人的网盘链接。

标签: #Vue3 #后台模板 #是一个后台前端解决方案它基于 #Vue # #elementui实现 #它使用了最新的前端技术栈内置了