這是一篇對 vue-element-admin 的學習總結文章。
目錄結構
├── 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
# 建議不要直接使用 cnpm 安裝依賴,會有各種詭異的 bug。可以通過如下操作解決 npm 下載速度慢的問題
npm install --registry=https://registry.npm.taobao.org
# 啟動服務
npm run dev
#發布
#正式環境
npm run build:prod
#集成環境
npm run sit
如果
node-sass
安裝報錯的話,可以重試npm install node-sass
,如果還是不行的話,可以npm install --registry=https://registry.npm.taobao.org/
,再安裝;實在不行的話用cnpm install node-sass
安裝這個包感覺也沒關系,雖然文檔不建議,但是這個包用cnpm
肯定是沒問題的
node-sass報錯可參考鏈接:https://github.com/PanJiaChen/vue-element-admin/issues/24
一、src 目錄
views 和 api 兩個模塊一一對應,從而方便維護
api:請求接口文件夾
views:頁面組件文件夾
api
一個
.js
對應一個views文件夾
里面的一個模塊
例如:
api 里面的login.js
,對應的是 views 里面的login
文件夾,如果有公共模塊就單獨放置就好
用法:
1. 先在/src/api 新建xxx.js
,例如bind.js
2. 引入
import axios from "@/utils/request";
import * as qs from "qs";
// 解除管控-列表頁
export const deleteClassPlateCtrlByBatch = (params) => {
return axios.post(
`ctrlSystem/deleteClassPlateCtrlByBatch`,
qs.stringify(params)
);
};
3. 使用
3.1 引入
import { deleteStudentPlateCtrlByBatch } from "@/api/bindManage";
3.2 函數中使用
deleCtro(){
let defaultBaseInfo = this.$store.state.user.defaultBaseInfo;
let obj = {
interUser: "runLfb",
interPwd: hex_md5(1234578),
operateAccountNo: defaultBaseInfo.operateAccountNo,
belongSchoolId: defaultBaseInfo.belongSchoolId,
schoolId: queryObj.schoolId,
classId: queryObj.classId,
surfacePlateBindRequestVoList: this.classIdList,
};
let params = {
requestJson: JSON.stringify(obj)
}
console.log(obj);
deleteStudentPlateCtrlByBatch(params).then((r) => {
console.log("deleteClassPlateCtrlByBatch", r);
this.success(r);
});
}
封裝 axios
1. /src/utils/request.js
import Vue from "vue";
import axios from "axios";
import { MessageBox, Message } from "element-ui";
import store from "@/store";
import { getToken } from "@/utils/auth";
import router from "../router";
import { Loading } from "element-ui";
import Cookies from "js-cookie";
// create an axios instance
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: 10000, // request timeout
});
// //========================================================token ======================================
var loading = ""; //定義loading變量
function startLoading() {
//使用Element loading-start 方法
loading = Loading.service({
lock: true,
// text: '加載中……',
background: "rgba(0, 0, 0, 0)",
});
}
function endLoading() {
//使用Element loading-close 方法
loading.close();
}
// 刷新token的過期時間判斷
function isRefreshTokenExpired() {
const oData = store.getters.getTokenTime; // 這是在登陸時候保存的時間戳
const nDta = new Date().getTime();
const stamp = nDta - oData; // 相差的微秒數
// const seconds = parseInt((stamp % (1000 * 60 * 60)) / 1000) 錯誤的計算,秒數差永遠<3600
const seconds = parseInt(stamp / 1000);
return (
seconds >= (store.getters.getTokenUsable * 3) / 4 &&
seconds < store.getters.getTokenUsable
);
// return false
}
// 刷新token
function getRefreshToken() {
// 刷新token 注意這里用到的service
return service.post("/public/regenerationToken").then((res) => {
return Promise.resolve(res.data);
});
}
// 是否正在刷新的標志
window.isRefreshing = false;
// 存儲請求的數組
let refreshSubscribers = [];
/* 將所有的請求都push到數組中*/
function subscribeTokenRefresh(cb) {
refreshSubscribers.push(cb);
}
// 數組中的請求得到新的token之后自執行,用新的token去請求數據
function onRrefreshed(token) {
refreshSubscribers.map((cb) => cb(token));
}
// 刪除cookie
function removeCookie() {
Cookies.remove("username", { path: "/" });
Cookies.remove("password", { path: "/" });
}
// request interceptor
service.interceptors.request.use(
(config) => {
startLoading();
let url = config.url;
// 解決問題:axios不會對url中的功能性字符進行編碼,手動編碼
// get參數編碼
if (config.method === "get" && config.params) {
url += "?";
const keys = Object.keys(config.params);
for (const key of keys) {
url += `${key}=${encodeURIComponent(config.params[key])}&`;
}
url = url.substring(0, url.length - 1);
config.params = {};
}
config.url = url;
const accessToken = store.getters.getAccessToken; // 本地保存的token
const refreshToken = store.getters.getRefreshToken; // 本地保存的token
/* 判斷token是否存在*/
if (accessToken && accessToken != "undefined") {
/* 在請求頭中添加token類型、token*/
config.headers.access_token = accessToken;
config.headers.client_type = store.state.user.client_type;
// config.url = config.url + '?t=' + (new Date()).getTime().toString(); // 清楚緩存
/* 判斷token是否將要過期 */
if (
isRefreshTokenExpired() &&
config.url.indexOf("public/regenerationToken") === -1
) {
if (!window.isRefreshing) {
// /*判斷是否正在刷新*/
window.isRefreshing = true;
/* 發起刷新token的請求*/
// config.headers.Authorization = ''
getRefreshToken();
/* 把請求(token)=>{....}都push到一個數組中*/
const retry = new Promise((resolve, reject) => {
/* (token) => {...}這個函數就是回調函數*/
subscribeTokenRefresh((token) => {
// config.headers.common['Authorization'] = 'bearer ' + token;
config.headers.access_token = token;
/* 將請求掛起*/
resolve(config);
});
});
return retry;
}
return config;
} else if (config.url.search(/\/public\/regenerationToken$/) >= 0) {
config.headers.refresh_token = refreshToken;
return config;
} else {
return config;
}
} else {
return config;
}
},
(error) => {
return Promise.reject(error);
}
);
// response interceptor
service.interceptors.response.use(
/**
* 根據后端的code碼,做一些操作
*/
(response) => {
endLoading();
const res = response.data;
// 沒有身份令牌或過期
if (res.code == 11002 || res.code == 11001) {
store.commit("user/setAccessToken", null);
store.commit("user/setRefreshToken", null);
store.commit("user/setTokenTime", null);
store.commit("user/setTokenUsable", null);
localStorage.clear();
removeCookie();
Message({
message: "登錄信息失效,請重新登錄",
type: "error",
duration: 3 * 1000,
});
router.push("/login");
}
// alert(response.config.url)
// console.log(response.config.url, 'response.config.url')
if (
(response.config.url.search(/\/user\/phoneLogin$/) >= 0 && res.flag) ||
(response.config.url.search(/\/user\/registerByVerificationCode$/) >= 0 &&
res.flag)
) {
store.commit("user/setAccessToken", response.headers.access_token);
store.commit("user/setRefreshToken", response.headers.refresh_token);
store.commit("user/setTokenTime", new Date().getTime());
store.commit("user/setTokenUsable", response.headers.token_usable);
} else if (
response.config.url.search(/\/public\/regenerationToken$/) >= 0
) {
if (res.code == "0") {
store.commit("user/setAccessToken", response.headers.access_token);
store.commit("user/setRefreshToken", response.headers.refresh_token);
store.commit("user/setTokenTime", new Date().getTime());
store.commit("user/setTokenUsable", response.headers.token_usable);
onRrefreshed(response.headers.access_token);
window.isRefreshing = false;
refreshSubscribers = [];
} else {
/* 清除本地保存的*/
store.commit("user/setAccessToken", null);
store.commit("user/setRefreshToken", null);
store.commit("user/setTokenTime", null);
store.commit("user/setTokenUsable", null);
localStorage.clear();
removeCookie();
Message({
message: "登錄信息失效,請重新登錄",
type: "error",
duration: 3 * 1000,
});
router.push("/login");
}
}
return Promise.resolve(res);
},
(error) => {
// Vue.prototype.$log4b.error("響應錯誤"+error.config.url+"錯誤信息"+JSON.stringify(error) )
console.log("err", error); // for debug
if (error && error.response) {
switch (error.response.status) {
case 400:
error.message = "請求錯誤(400)";
break;
case 401:
return history.push("/login");
break;
case 403:
error.message = "拒絕訪問(403)";
break;
case 404:
error.message = "請求出錯(404)";
break;
case 408:
error.message = "請求超時(408)";
break;
case 500:
error.message = "服務器錯誤(500)";
break;
case 501:
error.message = "服務未實現(501)";
break;
case 502:
error.message = "網絡錯誤(502)";
break;
case 503:
error.message = "服務不可用(503)";
break;
case 504:
error.message = "網絡超時(504)";
break;
case 505:
error.message = "HTTP版本不受支持(505)";
break;
default:
error.message = `連接出錯(${error.response.status})!`;
}
Message({
message: error.message,
type: "error",
duration: 3 * 1000,
});
}
return Promise.reject(error);
}
);
export default service;
components
components 放置的都是全局公用的一些組件,簡單來說就是多個頁面能用到的,不只是你當前頁面,如上傳組件;一些頁面級的組件建議還是放在各自 views 文件下,方便管理。
store
用來寫 vuex
用法:
1. ./store/index.js
vuex-along:解決 vuex 刷新消失問題,周下載量 215
vuex-persistedstate:解決 vuex 刷新消失問題,周下載量 10w+
后續建議使用
vuex-persistedstate
- vuex-along 周下載量
-
vuex-persistedstate 周下載量
vuex-persistedstate
import Vue from "vue";
import Vuex from "vuex";
import getters from "./getters";
import createVuexAlong from "vuex-along";
Vue.use(Vuex);
// https://webpack.js.org/guides/dependency-management/#requirecontext
const modulesFiles = require.context("./modules", true, /\.js$/);
// you do not need `import app from './modules/app'`
// it will auto require all vuex module from modules file
const modules = modulesFiles.keys().reduce((modules, modulePath) => {
// set './app.js' => 'app'
const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, "$1");
const value = modulesFiles(modulePath);
modules[moduleName] = value.default;
return modules;
}, {});
const store = new Vuex.Store({
modules,
getters,
plugins: [createVuexAlong()],
});
export default store;
2.0. 在/store/modules/新建xxx.js
,例如,bind.js
2.1. bind.js 代碼:
const state = {
bindFilter: "", //篩選條件緩存
detailRouter: false, //是否是詳情頁回來的
};
const mutations = {
bindFilter_Fun: (state, data) => {
state.bindFilter = data;
localStorage.setItem("bindFilter", JSON.stringify(data)); //緩存在localStorage里面,解決vuex刷新消失問題
},
detailRouter_Fun: (state, data) => {
state.detailRouter = data;
localStorage.setItem("detailRouter", JSON.stringify(data));
},
};
const actions = {
bindFilter({ commit, state }, data) {
commit("bindFilter_Fun", data);
},
detailRouter({ commit, state }, data) {
commit("detailRouter_Fun", data);
},
};
export default {
namespaced: true,
state,
mutations,
actions,
};
3. /store/getters.js
const getters = {
// 綁定管理篩選數據
getBindFilter: (state) =>
state.bind.bindFilter || JSON.parse(localStorage.getItem("bindFilter")),
getDetailRouter: (state) =>
state.bind.detailRouter || JSON.parse(localStorage.getItem("detailRouter")),
};
export default getters;
4.組件里面的用法
存入數據:this.$store.dispatch("bind/detailRouter", false)
獲取數據:this.$store.getters.getDetailRouter
5.namespaced
vuex 中的 store 分模塊管理,需要在 store 的 index.js 中引入各個模塊,為了解決不同模塊命名沖突的問題,將不同模塊的 namespaced:true,之后在不同頁面中引入 getter、actions、mutations 時,需要加上所屬的模塊名,相反,如果 namespaced:false 就是正常使用不用加模塊名字;
icon 圖標的使用方式
把下載好的圖標放入
/src/icons/svg/
文件夾
1. 使用方式
<svg-icon icon-class="password" /> // icon-class 為 icon 的名字
2. 改變顏色
svg-icon
默認會讀取其父級的 colorfill: currentColor;
你可以改變父級的
color
或者直接改變fill
的顏色即可。
二、layout布局
這里簡單看一下layout的布局,方便以后好修改;
簡單來說就是app.vue里面包含著layout,layout又包含著TagsView,sideBar,AppMain;然后我們寫的東西都是在AppMain里面的
- app.vue
- layout
- TagsView
- sideBar
- AppMain (內容容器)
- layout
三、環境變量配置
1. 本地開發環境
.env.development---這個對應本地地址打包環境
# just a flag
ENV = 'development'
# base api
#VUE_APP_BASE_API = '/dev-api'
#代理服務器api
#VUE_APP_BASE_API = '/api' 接口地址
#不用代理服務器api 接口地址
VUE_APP_BASE_API = 'http://119.23.xxx.xxx:9001/service-soa'
# vue-cli uses the VUE_CLI_BABEL_TRANSPILE_MODULES environment variable,
# to control whether the babel-plugin-dynamic-import-node plugin is enabled.
# It only does one thing by converting all import() to require().
# This configuration can significantly increase the speed of hot updates,
# when you have a large number of pages.
# Detail: https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/babel-preset-app/index.js
#開發環境不使用路由懶加載
VUE_CLI_BABEL_TRANSPILE_MODULES = true
2. 上線正式環境
.env.production------這個對應正式環境
# just a flag
ENV = 'production'
# base api 接口地址
VUE_APP_BASE_API = 'http://119.23.xxx.xx:8080/service-soa'
3. 集成測試環境
.env.sit------這個對應正式環境
#
NODE_ENV = production
# just a flag
ENV = 'sit'
# base api 接口地址
VUE_APP_BASE_API = 'http://119.23.xxx.71:9001/service-soa'
四、 vue.config.js
"use strict";
const path = require("path");
const defaultSettings = require("./src/settings.js");
function resolve(dir) {
return path.join(__dirname, dir);
}
const name = defaultSettings.title || "vue Element Admin"; // page title
const port = process.env.port || process.env.npm_config_port || 9530; // 端口號 port
module.exports = {
publicPath: "/",
outputDir: "dist",
assetsDir: "static",
// lintOnSave: process.env.NODE_ENV === 'development',
lintOnSave: false,
productionSourceMap: false,
devServer: {
hot: true, // 熱加載
port: port,
https: false, // false關閉https,true為開啟
// open: true,
overlay: {
warnings: false,
errors: true,
},
// before: require('./mock/mock-server.js'),
proxy: {
"/api": {
target: "http://119.23.xxx.xxx:9001/service-soa",
// 在本地會創建一個虛擬服務端,然后發送請求的數據,并同時接收請求的數據,這樣服務端和服務端進行數據的交互就不會有跨域問題
changeOrigin: true,
ws: true,
pathRewrite: {
// 替換target中的請求地址,也就是說以后你在請求http://api.jisuapi.com/XXXXX這個地址的時候直接寫成/api/xxx即可
"^/api": "/",
},
},
"/qq": {
target: "https://xxx.qq.com/oauth2.0",
// 在本地會創建一個虛擬服務端,然后發送請求的數據,并同時接收請求的數據,這樣服務端和服務端進行數據的交互就不會有跨域問題
changeOrigin: true,
ws: true,
pathRewrite: {
// 替換target中的請求地址,也就是說以后你在請求http://api.jisuapi.com/XXXXX這個地址的時候直接寫成/api/xxx即可
"^/qq": "/",
},
},
"/oss": {
target: "http://xxx.xxx.aliyuncs.com",
// 在本地會創建一個虛擬服務端,然后發送請求的數據,并同時接收請求的數據,這樣服務端和服務端進行數據的交互就不會有跨域問題
changeOrigin: true,
ws: true,
pathRewrite: {
// 替換target中的請求地址,也就是說以后你在請求http://api.jisuapi.com/XXXXX這個地址的時候直接寫成/api/xxx即可
"^/oss": "/",
},
},
},
},
configureWebpack: {
// provide the app's title in webpack's name field, so that
// it can be accessed in index.html to inject the correct title.
name: name,
resolve: {
alias: {
"@": resolve("src"),
},
},
},
chainWebpack(config) {
config.plugins.delete("preload"); // TODO: need test
config.plugins.delete("prefetch"); // TODO: need test
// set svg-sprite-loader
config.module.rule("svg").exclude.add(resolve("src/icons")).end();
config.module
.rule("icons")
.test(/\.svg$/)
.include.add(resolve("src/icons"))
.end()
.use("svg-sprite-loader")
.loader("svg-sprite-loader")
.options({
symbolId: "icon-[name]",
})
.end();
// set preserveWhitespace
config.module
.rule("vue")
.use("vue-loader")
.loader("vue-loader")
.tap((options) => {
options.compilerOptions.preserveWhitespace = true;
return options;
})
.end();
config
// https://webpack.js.org/configuration/devtool/#development
.when(process.env.NODE_ENV === "development", (config) =>
config.devtool("cheap-source-map")
);
config.when(process.env.NODE_ENV !== "development", (config) => {
config
.plugin("ScriptExtHtmlWebpackPlugin")
.after("html")
.use("script-ext-html-webpack-plugin", [
{
// `runtime` must same as runtimeChunk name. default is `runtime`
inline: /runtime\..*\.js$/,
},
])
.end();
config.optimization.splitChunks({
chunks: "all",
cacheGroups: {
libs: {
name: "chunk-libs",
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: "initial", // only package third parties that are initially dependent
},
elementUI: {
name: "chunk-elementUI", // split elementUI into a single package
priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
test: /[\\/]node_modules[\\/]_?element-ui(.*)/, // in order to adapt to cnpm
},
commons: {
name: "chunk-commons",
test: resolve("src/components"), // can customize your rules
minChunks: 3, // minimum common number
priority: 5,
reuseExistingChunk: true,
},
},
});
config.optimization.runtimeChunk("single");
});
},
};
五、 package.json
{
"name": "vue-element-admin",
"version": "4.2.1",
"description": "A magical vue admin. An out-of-box UI solution for enterprise applications. Newest development stack of vue. Lots of awesome features",
"author": "Pan <panfree23@gmail.com>",
"license": "MIT",
"scripts": {
"dev": "vue-cli-service serve --open",
"sit": "vue-cli-service build --mode sit",
"prod": "vue-cli-service build --mode production",
"build:prod": "vue-cli-service build",
"build:stage": "vue-cli-service build --mode staging",
"preview": "node build/index.js --preview",
"lint": "eslint --ext .js,.vue src",
"test:unit": "jest --clearCache && vue-cli-service test:unit",
"test:ci": "npm run lint && npm run test:unit",
"svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml",
"new": "plop"
},
"lint-staged": {
"src/**/*.{js,vue}": ["eslint --fix", "git add"]
},
"keywords": [
"vue",
"admin",
"dashboard",
"element-ui",
"boilerplate",
"admin-template",
"management-system"
],
"repository": {
"type": "git",
"url": "git+https://github.com/PanJiaChen/vue-element-admin.git"
},
"bugs": {
"url": "https://github.com/PanJiaChen/vue-element-admin/issues"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.29",
"@fortawesome/free-brands-svg-icons": "^5.13.1",
"@fortawesome/free-regular-svg-icons": "^5.13.1",
"@fortawesome/free-solid-svg-icons": "^5.13.1",
"@fortawesome/vue-fontawesome": "^0.1.10",
"arr2tree": "0.0.5",
"axios": "0.18.1",
"clipboard": "2.0.4",
"codemirror": "5.45.0",
"crypto-js": "^4.0.0",
"driver.js": "0.9.5",
"dropzone": "5.5.1",
"echarts": "4.2.1",
"element-ui": "2.13.0",
"file-saver": "2.0.1",
"fingerprintjs2": "^2.1.0",
"fundebug-javascript": "^2.4.2",
"fundebug-vue": "0.0.1",
"fuse.js": "3.4.4",
"js-cookie": "2.2.0",
"jsonlint": "1.6.3",
"jszip": "3.2.1",
"kindeditor": "^4.1.10",
"moment": "^2.27.0",
"normalize.css": "7.0.0",
"nprogress": "0.2.0",
"path-to-regexp": "2.4.0",
"qrcode": "^1.4.4",
"qrcodejs2": "0.0.2",
"screenfull": "4.2.0",
"script-loader": "0.7.2",
"showdown": "1.9.0",
"sortablejs": "1.8.4",
"tui-editor": "1.3.3",
"vue": "2.6.10",
"vue-count-to": "1.0.13",
"vue-router": "3.0.2",
"vue-splitpane": "1.0.4",
"vuedraggable": "2.20.0",
"vuex": "3.1.0",
"vuex-along": "^1.2.11",
"wangeditor": "^3.1.1",
"xlsx": "0.14.1"
},
"devDependencies": {
"@babel/core": "7.0.0",
"@babel/register": "7.0.0",
"@vue/cli-plugin-babel": "3.5.3",
"@vue/cli-plugin-eslint": "^3.9.1",
"@vue/cli-plugin-unit-jest": "3.5.3",
"@vue/cli-service": "3.5.3",
"@vue/test-utils": "1.0.0-beta.29",
"autoprefixer": "^9.5.1",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "10.0.1",
"babel-jest": "23.6.0",
"chalk": "2.4.2",
"chokidar": "2.1.5",
"connect": "3.6.6",
"eslint": "5.15.3",
"eslint-plugin-vue": "5.2.2",
"html-webpack-plugin": "3.2.0",
"husky": "1.3.1",
"lint-staged": "8.1.5",
"mockjs": "1.0.1-beta3",
"node-sass": "^4.9.0",
"plop": "2.3.0",
"runjs": "^4.3.2",
"sass-loader": "^7.1.0",
"script-ext-html-webpack-plugin": "2.1.3",
"serve-static": "^1.13.2",
"svg-sprite-loader": "4.1.3",
"svgo": "1.2.0",
"vue-template-compiler": "2.6.10"
},
"engines": {
"node": ">=8.9",
"npm": ">= 3.0.0"
},
"browserslist": ["> 1%", "last 2 versions"]
}
六、權限
1. 路由權限
流程:
1.登錄頁面按鈕點擊
2.vuex 里面的 login 方法被調用
3.vuex 里面的 login 方法被調用 完畢 4.監聽路由改變 然后獲取當前登錄的用戶角色 5.獲取當前用戶信息 獲取角色組 并保存登錄狀態,返回當前角色信息 6.通過 角色 和 所有路由 匹配出對應角色擁有的路由權限 返回路由組
7 將上面獲取到的 路由權限 掛載到真實的路由上面去
路由權限涉及文件:
/src/views/login/index.vue 登錄頁面的入口文件
/src/store/modules/user.js vuex 的文件 全局方法
/src/permission.js 監聽路由改變后的 js
/src/store/mudules/permission.js 通過 角色返回 登錄角色的對應路由列表的方法
src/views/permission/components/SwitchRoles.vue 切換角色的文件 這個登錄不走 切換角色才會走
/src/router/index.js
首先路由頁面 router:
有 2 個參數
export const constantRouterMap = [] 為初始路由參數,如登錄 首頁 404 等共有頁面 不需要權限控制的路由
export const asyncRouterMap = []為動態路由 登錄成功后 在 router.beforeEach 中根據后端權限 加載不同路由 已展示不同的左側菜單
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
/* Layout */
import Layout from '@/layout'
/**
* Note: sub-menu only appear when route children.length >= 1
* Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
*
* hidden: true 如果設置為true,則不會在側邊欄中顯示該項(默認為false)
* alwaysShow: true 如果設置為true,將始終顯示根菜單
* 如果未設置alwaysShow,則當項目有多個子路徑時,
*它將變成嵌套模式,否則不顯示根菜單
* redirect: noRedirect 如果set noRedirect將不會在breadcrumb中重定向
* name:'router-name' 名稱由<keep alive>使用(必須設置!!!)
* meta : {
roles: ['admin','editor'] 控制頁面角色(可以設置多個角色)
title: 'title' 在邊欄和面包屑中顯示的名稱(推薦集)
icon: 'svg-name' 圖標顯示在側欄中
noCache: true 如果設置為true,則不會緩存該頁(默認值為false)
affix: true 如果設置為true,則標記將附加在tags視圖中
breadcrumb: false 如果設置為false,則項目將隱藏在breadcrumb中(默認值為true)
activeMenu: '/example/list' 如果設置路徑,側欄將突出顯示您設置的路徑
}
*/
/**
* constantRoutes
* 無權限的基礎路由,所有角色可訪問
*/
export const constantRoutes = [
{
path: '/login',
component: () => import('@/views/login/login-index.vue'),
hidden: true
},
{
path: '/',
component: Layout,
redirect: '/bind-management'
},
{
path: '/auth-redirect',
component: () => import('@/views/login/auth-redirect'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/error-page/404'),
hidden: true
},
{
path: '/401',
component: () => import('@/views/error-page/401'),
hidden: true
},
{
path: '/notice',
component: Layout,
hidden: true,
children: [{
path: 'index',
component: () => import('@/views/noticeManagement/noticeList'),
name: 'Notice',
meta: {
title: '消息通知',
icon: 'guide',
noCache: true
}
}]
}
]
/**
* asyncRoutes
* 有權限,權限為admin可以訪問
*/
export const asyncRoutes = [
{
path: '/icon',
component: Layout,
children: [{
path: 'index',
component: () => import('@/views/icons/index'),
name: 'Icons',
meta: {
title: 'Icons',
icon: 'icon',
noCache: true,
roles: ['noPremission']
}
}]
},
const strategyManagementRouter = {
path: '/strategy-management',
component: Layout,
redirect: '/strategy-management/index',
meta: {
title: '策略管理',
icon: 'ctrl_icon_strategy',
roles: ['admin']
},
children: [
{
path: '/strategy-management/index',
component: () => import('@/views/strategyManagement/index'),
name: 'strategyManagement',
alwaysShow: true,
meta: {
title: '策略管理',
icon: 'ctrl_icon_strategy',
roles: ['admin']
}
}
]
},
{
path: '/bind-management',
component: Layout,
redirect: '/bind-management/index',
meta: {
title: '綁定管理',
icon: 'ctrl_icon_bindings',
roles: ['admin'],
noCache: false
},
children: [
{
path: '/bind-management/index',
component: () => import('@/views/bindManagement/index'),
name: 'bindManagement',
alwaysShow: true,
meta: {
title: '綁定管理',
icon: 'ctrl_icon_bindings',
roles: ['admin'],
noCache: false
}
},
{
path: '/bind-management/detail',
component: () => import('@/views/bindManagement/bindDetail'),
name: 'bindDetail',
hidden: true,
meta: {
title: '班級詳情',
icon: 'ctrl_icon_bindings',
roles: ['admin'],
activeMenu: '/bind-management/index',
noCache: false
}
}
]
},
// 404 page must be placed at the end !!!
{
path: '*',
redirect: '/404',
hidden: true
}
]
const createRouter = () => new Router({
scrollBehavior: () => ({
y: 0
}),
routes: constantRoutes
})
const router = createRouter()
//重新設置路由
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher // reset router
}
export default router
/src/permission.js
import router from "./router";
import store from "./store";
import { Message } from "element-ui";
import NProgress from "nprogress"; // progress bar
import "nprogress/nprogress.css"; // progress bar style
import getPageTitle from "@/utils/get-page-title";
NProgress.configure({
showSpinner: false,
}); // NProgress Configuration
const whiteList = ["/login", "/auth-redirect", "/dashboard"]; // no redirect whitelist
let flag = 0;
router.beforeEach(async (to, from, next) => {
// 路由加載進度條
NProgress.start();
// 設置頁面title
document.title = getPageTitle(to.meta.title);
// 確定是否登錄
const hasToken = store.getters.getAccessToken;
if (hasToken) {
if (to.path === "/login") {
next({
path: "/",
});
NProgress.done();
} else {
try {
const hasAddRoutes =
store.getters.addRoutes && store.getters.addRoutes.length > 0;
if (flag === 0 || !hasAddRoutes) {
const permissionRoutes = await store.dispatch(
"user/queryFuncByRoles"
); //觸發權限函數,查詢路由、按鈕權限
const buttonCode = permissionRoutes.buttonCode;
localStorage.setItem("buttonCode", JSON.stringify(buttonCode)); //保存權限按鈕到本地
const accessRoutes = await store.dispatch(
"permission/generateRoutes",
permissionRoutes.sysFuncViewList
); //獲取動態路由數組
if (!accessRoutes.length) {
await store.dispatch("user/resetToken");
Message.error("該賬戶無可訪問權限");
NProgress.done();
next(`/login?redirect=${to.path}`);
return;
}
console.log("accessRoutes", accessRoutes);
router.addRoutes(accessRoutes);
flag++;
next({ ...to, replace: true });
} else {
next();
}
} catch (error) {
// 刪除token,跳轉到登錄頁
await store.dispatch("user/resetToken");
Message.error({
message: error || "出現錯誤,請稍后再試",
});
next(`/login?redirect=${to.path}`);
NProgress.done();
}
}
} else {
// 未登錄去whiteList里面的路由可以去,去別的則跳轉登錄頁
if (whiteList.indexOf(to.path) !== -1) {
next();
} else {
//沒權限的重定向到首頁
next(`/login`);
NProgress.done();
}
}
});
router.afterEach(() => {
NProgress.done();
});
/src/store/mudules/permission.js
/*
* @Author: your name
* @Date: 2020-10-27 17:49:08
* @LastEditTime: 2020-11-18 16:09:40
* @LastEditors: Please set LastEditors
* @Description: In User Settings Edit
* @FilePath: \Git\plate-control-admin\src\store\modules\permission.js
*/
import { asyncRoutes, constantRoutes } from "@/router";
/**
* 通過meta.roles判斷是否與當前用戶權限匹配
* 判斷傳進來的路由(route)里面的meta.roles是否滿足'admin'條件,滿足返回true,相反false
* @param roles 權限數組 ['admin']
* @param route 路由數組
*/
function hasPermission(roles, route) {
if (route.meta && route.meta.roles) {
return roles.some((role) => route.meta.roles.includes(role));
} else {
return true;
}
}
/**
* 遞歸過濾異步路由表,返回符合用戶角色權限的路由表
* @param routes asyncRoutes
* @param roles
*/
export function filterAsyncRoutes(routes, roles) {
const res = [];
routes.forEach((route) => {
const tmp = { ...route };
if (hasPermission(roles, tmp)) {
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, roles);
}
res.push(tmp);
}
});
console.log("roles", res);
return res;
}
export function getResultRouters(treeData, arr) {
treeData.forEach((element) => {
arr.forEach((ele) => {
if (element.path == ele.funcUrl) {
element.meta.roles = ["admin"];
}
});
if (element.children && element.children.length > 0) {
getResultRouters(element.children, arr);
}
});
return treeData;
}
const state = {
routes: [],
addRoutes: [],
};
const mutations = {
SET_ROUTES: (state, routes) => {
//保存動態路由時 將靜態路由和動態路由合并
state.addRoutes = routes;
state.routes = constantRoutes.concat(routes);
},
};
const actions = {
generateRoutes({ commit }, roles) {
return new Promise((resolve) => {
let resetRouters, accessedRoutes;
resetRouters = getResultRouters(asyncRoutes, roles);
if (!resetRouters.length) {
resolve(resetRouters);
return;
}
accessedRoutes = filterAsyncRoutes(resetRouters, ["admin"]);
if (!accessedRoutes.length) {
commit("SET_ROUTES", []);
} else {
commit("SET_ROUTES", accessedRoutes);
}
resolve(accessedRoutes);
});
},
};
export default {
namespaced: true,
state,
mutations,
actions,
};
2. 按鈕級別權限控制
2.1 思路:
頁面展示需要鑒權的所有按鈕,需要先鑒權菜單權限的顯示與隱藏。
勾選每個角色或者用戶所能看的權限保存在數據庫。該權限數據是一個權限字段的數組。
全局自定義指令(directive)控制按鈕權限數據的方法,登入時獲取后端傳來的按鈕權限數組。
在每個按鈕中調用該指令,并傳入該操作的權限字段和后端保存的權限字段進行匹配,能匹配則該操作按鈕可顯示
我們公司這一塊是不用根據菜單權限,來判斷按鈕權限,只需要根據后端返回的權限字段的數組判斷就好了,然后這一塊我們公司也是做的指令封裝,代碼如下
2.2 使用方法
-
在
/src/directive/
新建/btnPermission/btnPermission.js
/src/directive/btnPermission/btnPermission.js
export const hasPermission = {
install(Vue) {
Vue.directive("hasPermission", {
bind(el, binding, vnode) {
const permissionsNameList = JSON.parse(
localStorage.getItem("buttonCode")
); //按鈕數組列表
const permissions = Object.keys(permissionsNameList); //返回一個由一個給定對象的自身可枚舉屬性組成的數組,對象的key
console.log(permissions, "permissions");
const value = binding.value;
let flag = true;
for (const v of value) {
//遍歷傳進來的數組
if (!permissions.includes(v)) {
//判斷后端給的數組,是否包含傳進來的這個字段,包含則顯示,不包含則隱藏
flag = false;
}
}
if (!flag) {
if (!el.parentNode) {
el.style.display = "none";
} else {
el.parentNode.removeChild(el);
}
}
},
});
},
};
- 在
/src/main.js
引入
// 引入權限按鈕文件
import { hasPermission } from "../src/directive/btnPermission/btnPermission.js"; // 按鈕權限指令
Vue.use(hasPermission); // 按鈕權限指令
- 使用方法
<el-button
class="inquireButton"
v-hasPermission="['platectrl_b_policy_search']"
@click="inquire"
>查詢</el-button
>
<el-button
class="addNewButton"
v-hasPermission="['platectrl_b_policy_add']"
@click="addNew"
>新增</el-button
>
七、媒體查詢移動、PC 兼容
雖然 element 框架有一些自適應的處理,但是還是有一些需要調整,所以我就自己寫了一套媒體查詢,哪里需要做一些樣式處理,只需要在對應的屏幕寬度下面修改就好,這里我的 rem 計算方法是
px/10/2
或者直接根據媒體查詢調整;
1. 用法:
- 先在
/src/styles/
里面新建media.scss
- 引入到
/src/index.scss/
里面
直接在index.scss引入就好
@import './variables.scss';
@import './mixin.scss';
@import './transition.scss';
@import './element-ui.scss';
@import './sidebar.scss';
@import './btn.scss';
@import "./media.scss"; /*媒體查詢的css*/
- 代碼如下
/* -----------mobile----------- */
@media screen and (max-width: 480px) {
/* 登錄自適應 */
.login-right {
min-width: 20rem;
overflow: auto;
}
.bg-container {
width: 100%;
justify-content: center;
}
.login-left {
display: none;
}
.login-left-title_phone {
margin-bottom: 1rem;
font-size: 1.4rem;
display: block;
}
/* 登錄自適應 end*/
/* 彈窗自適應 dialog */
.Dialog-box {
max-height: 60%;
.el-dialog {
width: 80% !important;
}
.el-form-item--medium .el-form-item__label {
width: 106px !important;
}
.el-form-item--medium .el-form-item__content {
margin-left: 72px !important;
}
}
// 按鈕位置
.el-form-item-btns {
float: left !important;
}
.inquireButton-father {
// float: none !important;
// width: 22% !important;
}
}
/* -----------ipad small----------- */
@media screen and (min-device-width: 481px) and (max-device-width: 768px) {
/* 登錄自適應 */
.login-right {
min-width: 20rem;
overflow: auto;
}
.bg-container {
width: 100%;
justify-content: center;
}
.login-left {
display: none;
}
.login-left-title_phone {
margin-bottom: 1rem;
font-size: 1.4rem;
display: block;
}
/* 彈窗自適應 dialog */
.Dialog-box {
max-height: 60%;
.el-dialog {
width: 80% !important;
}
.el-form-item--medium .el-form-item__label {
width: 106px !important;
}
.el-form-item--medium .el-form-item__content {
margin-left: 72px !important;
}
}
// 按鈕位置
.el-form-item-btns {
float: left !important;
}
.inquireButton-father {
// float: none !important;
width: 22% !important;
}
}
/* ----------- iPad big----------- */
@media screen and (min-device-width: 768px) and (max-device-width: 1024px) {
/* 登錄自適應 */
.user-input {
background-color: #fff;
padding: 0 2%;
margin: 0;
height: 9rem;
}
/* 彈窗自適應 dialog */
.Dialog-box {
max-height: 60%;
.el-dialog {
width: 80% !important;
}
.el-form-item--medium .el-form-item__label {
width: 106px !important;
}
.el-form-item--medium .el-form-item__content {
margin-left: 72px !important;
}
}
// 按鈕位置
.el-form-item-btns {
float: left !important;
}
.inquireButton-father {
// float: none !important;
width: 22% !important;
}
}
/* ----------- iPad Pro 屏幕小的筆記本----------- */
/* Portrait and Landscape */
@media only screen and (min-device-width: 1025px) and (max-device-width: 1366px) and (-webkit-min-device-pixel-ratio: 1.5) {
/* 登錄自適應 */
.login-right {
min-width: 27rem;
overflow: auto;
}
}
/* Portrait */
@media only screen and (min-device-width: 1024px) and (max-device-width: 1366px) and (orientation: portrait) and (-webkit-min-device-pixel-ratio: 1.5) {
/* 登錄自適應 */
.login-right {
min-width: 27rem;
overflow: auto;
}
}
/* Landscape */
@media only screen and (min-device-width: 1024px) and (max-device-width: 1366px) and (orientation: landscape) and (-webkit-min-device-pixel-ratio: 1.5) {
/* 登錄自適應 */
.login-right {
min-width: 27rem;
overflow: auto;
}
}
個人建議:
如果拿來開發的話,建議選用
vue-admin-template
這套框架,這是vue-admin-element
的極簡版本,它只包含了 Element UI & axios & iconfont & permission control & lint,這些搭建后臺必要的東西;如果想用vue-admin-element
的東西,也是可以直接拿過來就用的,就不會有那么多的代碼沉余;
總結:
框架功能比較豐富,社區完整,是個值得入手學習的框架;現在還是在初期使用階段,一些細節上的技術點,會在使用中持續更新;