vue-element-admin

vscode 插件和配置推薦

這是一篇對 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-views.png

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-along 使用文檔地址

vuex-persistedstate:解決 vuex 刷新消失問題,周下載量 10w+

vuex-persistedstate 使用文檔地址

后續建議使用vuex-persistedstate

  • vuex-along 周下載量
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
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.組件里面的用法

更多方法請查看:http://www.lxweimin.com/p/0f13a6bec687

存入數據: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 默認會讀取其父級的 color fill: 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

相關文檔-My

相關文檔-Other

"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 中 rem 用法及 scss

個人建議:

如果拿來開發的話,建議選用vue-admin-template這套框架,這是vue-admin-element的極簡版本,它只包含了 Element UI & axios & iconfont & permission control & lint,這些搭建后臺必要的東西;如果想用vue-admin-element的東西,也是可以直接拿過來就用的,就不會有那么多的代碼沉余;

vue-admin-template官方文檔鏈接

總結:

框架功能比較豐富,社區完整,是個值得入手學習的框架;現在還是在初期使用階段,一些細節上的技術點,會在使用中持續更新;

可參考鏈接:http://www.lxweimin.com/p/d3e3b21696e4

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,481評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,241評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,697評論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,182評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,406評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,933評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,772評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,973評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評論 1 285
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,644評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,953評論 2 373