介紹
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 flagENV = 'development'# base apiVUE_APP_BASE_API = 'api/private/v1/'
如果想要設置生產環境的接口**.env.production**文件中寫入對於變量直接賦值即可
# just a flagENV = 'production'# base apiVUE_APP_BASE_API = 'api/private/v1/'
網站名稱
src/settings.js
中
title 就是網站名稱
配置完我們要進行重啟,否則有些配置不會生效
登錄頁面
設置頭部名稱:
海豚電商後臺管理平臺
設置背景圖片:
可根據需求更改
/* reset element-ui css */.login-container { background-image: url('~@/assets/common/bgc.jpg'); // 設置背景圖片 background-position: center; // 將圖片位置設置為充滿整個屏幕}
對應代碼:
登錄表單的校驗
el-form表單校驗的條件
用戶名和密碼的校驗:
const validateUsername = (rule, value, callback) => { if (value.length 12) { callback(new Error('用戶名最長12位')) } else { callback() } } const validatePassword = (rule, value, callback) => { 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
用戶輸入完畢之後點擊登錄按鈕時也要進行後臺驗證,當我們點擊登錄發送請求到後臺入庫查詢賬戶密碼是否正確,如不正確會彈出提示
在表單裡面使用瞭標簽引入 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'
這樣在任意頁面就可以成功使用組件瞭
在頁面中使用就可以進行使用瞭
完整代碼
海豚電商後臺管理平臺 立即登錄 <!-- username: admin password: any --> import { validUsername } from '@/utils/validate'export default { name: 'Login', data () { const validateUsername = (rule, value, callback) => { if (value.length 12) { callback(new Error('用戶名最長12位')) } else { callback() } } const validatePassword = (rule, value, callback) => { 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 } } }}/* 修復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; }}$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; }}
主頁模塊
主頁token攔截並進行處理 權限攔截的流程圖
我們已經完成瞭登錄的過程,並且存儲瞭token,但是此時主頁並沒有因為token的有無而被控制訪問權限
攔截處理代碼
src/permission.js
import Vue from 'vue'import 'normalize.css/normalize.css' // A modern alternative to CSS resetsimport ElementUI from 'element-ui'import 'element-ui/lib/theme-chalk/index.css'import locale from 'element-ui/lib/locale/lang/zh-CN' // lang i18nimport '@/styles/index.scss' // global cssimport App from './App'import store from './store'import router from './router'import i18n from '@/lang/index'import '@/icons' // iconimport '@/permission' // permission controlimport directives from './directives'import Commponent from '@/components'import filters from './filter'import Print from 'vue-print-nb' // 引入打印// set ElementUI lang to ENVue.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
{{ title }}
{{ title }}
完整代碼
{{ title }}
{{ title }} export default { name: 'SidebarLogo', props: { collapse: { type: Boolean, required: true } }, data () { return { title: '海豚電商後臺管理平臺', logo: 'https://blog.csdn.net/qq_46416934/article/details/@/assets/common/hai.png' } }}.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; } }}
頭部內容的佈局和樣式
頭部組件位置layout/components/Navbar.vue
添加公司名稱時要面包屑
<!-- --> 北京夢囈網絡有限公司 v1.0.0
右側頭像和下拉菜單等設置
{{ username }} 主頁 郵箱 設置 退出
完整代碼:樣式+事件
<!-- --> 北京夢囈網絡有限公司 v1.0.0
{{ username }} 主頁 郵箱 設置 退出 import { mapGetters } from 'vuex'import Breadcrumb from '@/components/Breadcrumb'import Hamburger from '@/components/Hamburger'export default { components: { Breadcrumb, Hamburger }, data () { return { username: '超級管理員', defaultImg: require('https://blog.csdn.net/qq_46416934/article/details/@/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`) } }}.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;}
儲存用戶信息
新增變量: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
最後我們換成真實名稱即可
{{ username }}
這裡可能會出現問題,在頁面刷新拿不到數據,我們可以將其保存到本地中,然後取出
實現退出功能
退出: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`) }
完整代碼:
<!-- --> 北京夢囈網絡有限公司 v1.0.0
{{ username }} 主頁 郵箱 設置 退出 import { mapGetters } from 'vuex'import Breadcrumb from '@/components/Breadcrumb'import Hamburger from '@/components/Hamburger'export default { components: { Breadcrumb, Hamburger }, data () { return { username: '超級管理員', defaultImg: require('https://blog.csdn.net/qq_46416934/article/details/@/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`) } }}.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;}
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 instanceconst service = axios.create({ baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url timeout: 5000 // request timeout})// request interceptorservice.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 interceptorservice.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
<!-- --> import ScreenFull from 'screenfull'export default { methods: { // 改變全屏 changeScreen () { if (!ScreenFull.isEnabled) { // 此時全屏不可用 this.$message.warning('此時全屏組件不可用') return } // document.documentElement.requestFullscreen() 原生js調用 // 如果可用 就可以全屏 ScreenFull.toggle() } }}
全局註冊該組件 src/components/index.js
import ScreenFull from './ScreenFull'Vue.component('ScreenFull', ScreenFull) // 註冊全屏組件
放置layout/navbar.vue
-------------------------------.right-menu-item { vertical-align: middle;}
設置動態主題
封裝全屏插件 src/components/ThemePicker/index.vue
const version = require('element-ui/package.json').version // element-ui version from node_modulesconst ORIGINAL_THEME = '#409EFF' // default colorexport 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 } }}.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;}
全局註冊該組件 src/components/index.js
import ThemePicker from './ThemePicker'Vue.component('ThemePicker', ThemePicker)
放置layout/navbar.vue
多語言實現
安裝國際化的語言包 i18n
npm i vue-i18n
需要多語言的實例化文件 src/lang/index.js
import Vue from 'vue' // 引入Vueimport 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
封裝多語言組件 src/components/lang/index.vue
中文 en import Cookie from 'js-cookie'export default { methods: { changeLanguage (lang) { Cookie.set('language', lang) // 切換多語言 this.$i18n.locale = lang // 設置給本地的i18n插件 this.$message.success('切換多語言成功') } }}
全局註冊該組件 src/components/index.js
import lang from './lang'Vue.component('lang', lang) // 註冊全屏組件
放置layout/navbar.vue