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

介绍

vue-element-admin 是一个后台前端解决方案,它基于 vueelement-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.jssettings.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 图标

我们首先在src\components下创建了SvgIcon组件

我们向外暴露了两个属性

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

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

然后进行默认样式的编写

在这里插入图片描述

src\icons 中的 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" />
  • 177
    点赞
  • 630
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 272
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 272
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

David凉宸

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值