介绍
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" />
|