D2Admin基本使用

来源:https://www.cnblogs.com/izbw/archive/2019/06/24/11077815.html
-Advertisement-
Play Games

[toc] d2 admin基本使用 官方演示: "https://d2admin.fairyever.com/ /index" 官方文檔: "https://doc.d2admin.fairyever.com/zh/" 1 安裝 1.1 全局安裝 d2 admin 1.2 創建項目 或者 之後選擇 ...


目錄

d2-admin基本使用

官方演示:https://d2admin.fairyever.com/#/index
官方文檔:https://doc.d2admin.fairyever.com/zh/

1 安裝

1.1 全局安裝 d2-admin

npm install -g @d2-admin/d2-admin-cli

1.2 創建項目

d2 create 項目名稱

或者

d2 c 項目名稱

之後選擇 簡化版

1.3 啟動項目

進入項目文件夾

npm i
npm run serve

1.4 註意事項

  • d2-container是 D2Admin 構建頁面最重要的組件,在模板頁面中記得要用該標簽包裹,該標簽針對不樣式的頁面內置不同的類型,詳見官方文檔

2.1 修改 title

// .env.development

# 開發環境

# 頁面 title 首碼
VUE_APP_TITLE=ZAdmin

修改完成後重啟項目即生效

首頁LOGL:
替換 .\public\image\theme\d2\logo\all.png

網頁 ico 小圖標:
替換 .\public\icon.ico

3 圖表庫

3.1 常用的圖表庫

  • 百度圖表庫:echarts (此框架不方便)
  • 餓了麽圖表庫:v-charts (用這個)

v-charts 官方文檔:https://v-charts.js.org/#/

3.2 安裝v-charts

npm i v-charts echarts -S

3.3 導入項目

// main.js
import Vue from 'vue'
import VCharts from 'v-charts'
import App from './App.vue'

Vue.use(VCharts)

new Vue({
  el: '#app',
  render: h => h(App)
})

3.4 簡單舉例

以折線圖為例,其他類型詳見官方文檔。

為了美觀,將其包含在elementUI的Card組件中。

<template>
  <el-card>
    <ve-line :data="chartData"></ve-line>
  </el-card>
</template>

<script>
  export default {
    data: function () {
      return {
        chartData: {
          columns: ['日期', '訪問用戶', '下單用戶', '下單率'],
          rows: [
            { '日期': '1/1', '訪問用戶': 1393, '下單用戶': 1093, '下單率': 0.32 },
            { '日期': '1/2', '訪問用戶': 3530, '下單用戶': 3230, '下單率': 0.26 },
            { '日期': '1/3', '訪問用戶': 2923, '下單用戶': 2623, '下單率': 0.76 },
            { '日期': '1/4', '訪問用戶': 1723, '下單用戶': 1423, '下單率': 0.49 },
            { '日期': '1/5', '訪問用戶': 3792, '下單用戶': 3492, '下單率': 0.323 },
            { '日期': '1/6', '訪問用戶': 4593, '下單用戶': 4293, '下單率': 0.78 }
          ]
        }
      }
    }
  }
</script>

3.5 註意事項

  • 將多個圖表分別放置在tab標簽頁時,切換標簽頁後下個圖表可能會等待很久才會出現,是因為收到 elementUI 中Tabs標簽頁的 lazy 屬性的影響(初始化時有個渲染的過程),如果沒有開啟延遲渲染,會只渲染第一個標簽頁內容,因此需要將 lazy 設置為 true


從第二 tbgs 標簽頁起,將lazy 屬性設置為 true

<el-tabs v-model="activeName" @tab-click="handleClick">
    <el-tab-pane label="用戶管理" name="first">
        <ve-line :data="chartData"></ve-line>      
    </el-tab-pane>
    <el-tab-pane :lazy="true" label="配置管理" name="second">
        <ve-histogram :data="chartDataHis"></ve-histogram>      
    </el-tab-pane>
    <el-tab-pane :lazy="true" label="角色管理" name="third">角色管理</el-tab-pane>
    <el-tab-pane :lazy="true" label="定時任務補償" name="fourth">定時任務補償</el-tab-pane>
</el-tabs>

4 CURD插件(表格)

D2 CURD是一個基於Vue.js 和 Element UI的表格組件,封裝了常用的表格操作,使用該組件可以快速在頁面上處理表格數據。
詳見官方文檔

4.1 安裝

npm i element-ui @d2-projects/d2-crud -S

4.2 導入項目

import Vue from 'vue'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import D2Crud from '@d2-projects/d2-crud'

Vue.use(ElementUI)
Vue.use(D2Crud)

new Vue({
  el: '#app',
  render: h => h(App)
})

4.3 圖表使用

此處為帶有新增和分頁功能的表格,但CURD2.x中分頁功能的數據需要從後臺獲取,因此這裡只簡單演示了表格的樣式。

columns: 表格的列屬性
data: 表格的數據
add-title: 彈出新增模態框的標題
pagination: 開啟分頁功能
loading: 載入數據時會有載入中的樣式
slot="header": 表格的頭部信息,自定義樣式(如:標題,按鈕,輸入框等)可以顯示在表格的頂部(2.x新增,更好的替代了1.x中的 title 屬性)
BusinessTable1List: 後臺數據API,獲取後臺數據,這裡只是頁面展示,採用固定的數據,未使用API介面。

<template>
<d2-container>
  <el-card>
    <d2-crud
      ref="d2Crud"
      :columns="columns"
      :data="data"
      add-title="添加數據"
      :add-template="addTemplate"
      :form-options="formOptions"
      :pagination="pagination"
      :loading="loading"
      @pagination-current-change="paginationCurrentChange"
      @dialog-open="handleDialogOpen"
      @row-add="handleRowAdd"
      @dialog-cancel="handleDialogCancel">
      <el-button slot="header" style="margin-bottom: 5px" @click="addRow"><i class="fa fa-plus" aria-hidden="true"></i> 新增</el-button>
      <el-button slot="header" style="margin-bottom: 5px" @click="addRowWithNewTemplate">使用自定義模板新增</el-button>
      <el-input slot="header" style="margin-bottom: 5px" placeholder="商品名稱" suffix-icon="el-icon-search"> </el-input>
      <el-input slot="header" style="margin-bottom: 5px" placeholder="最低價格" suffix-icon="el-icon-caret-bottom"> </el-input>
      <el-input slot="header" style="margin-bottom: 5px" placeholder="最高價格" suffix-icon="el-icon-caret-top"> </el-input>
      <el-button slot="header" style="margin-bottom: 5px"><i class="el-icon-search"></i> 搜索</el-button>
    </d2-crud>
  </el-card>
</d2-container>
</template>

<script>
// import { BusinessTable1List } from '@api/demo.business.table.1'
export default {
  data () {
    return {
      columns: [
        {
          title: '日期',
          key: 'date'
        },
        {
          title: '姓名',
          key: 'name'
        },
        {
          title: '地址',
          key: 'address'
        }
      ],
      data: [
          {
            date: '2016-05-02',
            name: '王小虎',
            address: '上海市普陀區金沙江路 1518 弄'
          },
          {
            date: '2016-05-04',
            name: '王小虎',
            address: '上海市普陀區金沙江路 1517 弄'
          },
          {
            date: '2016-05-01',
            name: '王小虎',
            address: '上海市普陀區金沙江路 1519 弄'
          },
          {
            date: '2016-05-03',
            name: '王小虎',
            address: '上海市普陀區金沙江路 1516 弄'
          }
      ],
      addTemplate: {
        date: {
          title: '日期',
          value: '2016-05-05'
        },
        name: {
          title: '姓名',
          value: '王小虎'
        },
        address: {
          title: '地址',
          value: '上海市普陀區金沙江路 1520 弄'
        }
      },
      formOptions: {
        labelWidth: '80px',
        labelPosition: 'left',
        saveLoading: false
      },
      loading: false,
      pagination: {
        currentPage: 1,
        pageSize: 5,
        total: 100
      }
    }
  },
  mounted () {
    this.fetchData()
  },
  methods: {
    handleDialogOpen ({ mode }) {
      this.$message({
        message: '打開模態框,模式為:' + mode,
        type: 'success'
      })
    },
    // 普通的新增
    addRow () {
      this.$refs.d2Crud.showDialog({
        mode: 'add'
      })
    },
    // 傳入自定義模板的新增
    addRowWithNewTemplate () {
      this.$refs.d2Crud.showDialog({
        mode: 'add',
        template: {
          name: {
            title: '姓名',
            value: ''
          },
          value1: {
            title: '新屬性1',
            value: ''
          },
          value2: {
            title: '新屬性2',
            value: ''
          }
        }
      })
    },
    handleRowAdd (row, done) {
      this.formOptions.saveLoading = true
      setTimeout(() => {
        console.log(row)
        this.$message({
          message: '保存成功',
          type: 'success'
        });

        // done可以傳入一個對象來修改提交的某個欄位
        done({
          address: '我是通過done事件傳入的數據!'
        })
        this.formOptions.saveLoading = false
      }, 300)
    },
    handleDialogCancel (done) {
      this.$message({
        message: '取消保存',
        type: 'warning'
      });
      done()
    },
    paginationCurrentChange (currentPage) {
      this.pagination.currentPage = currentPage
      this.fetchData()
    },
    fetchData () {
      this.loading = true
      BusinessTable1List({
        ...this.pagination
      }).then(res => {
        this.data = res.list
        this.pagination.total = res.page.total
        this.loading = false
      }).catch(err => {
        console.log('err', err)
        this.loading = false
      })
    }
  }
}
</script>

<style scoped>
  .el-input {
    width: 200px;
    margin-right: 10px;
  }
</style>

4.4 註意事項

  • 課程中使用CURD組件為1.x,最新版本為2.x,變化較大,尤其表現在分頁和添加功能上,如在1.x中分頁是完全基於前端,而2.x版本的CURD是基於後端的,其需要後端提供數據介面,對返回的數據進行分頁展現。詳見官方文檔。關於如何從定義後臺API並使用mockjs模擬生成後臺數據,請看下一章

5 定義數據API

  • mock (模擬後端,設計API)
  • api (封裝axios調用api,對接後端和vuex)
  • view (使用vuex處理數據)

兩個概念:

  • 模塊化
  • 命名空間

5.1 第一步,使用mockjs創建數據

註意:創建調用API的URL時,需帶上首碼 /api/xxx

// /mock/api/product.js
const Mock = require('mockjs')
const Qs = require('qs')
const Random = Mock.Random


const titleList = ['男士上衣', 'T恤', '襯衫', '牛仔褲', '皮衣', '短裙', '女士襯衫', '長裙', '羽絨服', '秋褲', '軍大衣'];

const getProductList = function (params) {
  // 獲取全部商品列表或者分頁商品列表
  let products = []
  for (let i = 0; i < 100; i++) {
    let product = {
      id: i + 1,
      title: titleList[Math.floor(Math.random()*titleList.length)],
      price: Random.float(10, 100).toFixed(2),
      stock: Random.integer(10, 100),
      saleCount: Random.integer(0, 90),
      isSale: Random.integer(0, 1),
      createTime: Random.datetime(),
      imgUrl: Random.dataImage('60x60', 'ZAdmin-' + (i + 1)),
      showEditButton: true,
      showRemoveButton: true
    }
    products.push(product)
  }

  let total = products.length;
  if (params.body && products.length) {
    let pageInfo = JSON.parse(Qs.parse(params).body);
    let start = (pageInfo.currentPage - 1) * pageInfo.pageSize;
    let end = pageInfo.currentPage * pageInfo.pageSize;
    products = products.slice(start, end);
    console.log(`start: ${start}  end: ${end}  products: ${products}  total: ${total}`);
  }

  return { products, total }
}

// 通過post請求,使用參數的token判斷是否登錄,並通過參數判斷獲取全部評論列表或者獲取分頁評論列表
const getProductComments = function (params) {
  let data = JSON.parse(Qs.parse(params).body);
  console.log('api-comments-params: ', data);
  if (!data.token) {
    return {
      status: 401,
      msg: 'token錯誤,需要登錄',
      data: {}
    }
  }

  let comments = []
  for (let i = 0; i < 120; i++) {
    let comment = {
      id: i + 1,
      name: Random.cname(),
      itemScore: Random.integer(1, 5),
      serviceScore: Random.integer(1, 5),
      content: Random.ctitle(10, 50),
      createTime: Random.datetime(),
      showEditButton: true,
      showRemoveButton: true
    }
    comments.push(comment)
  }

  let total = comments.length;

  if (data.page) {
    let pageInfo = data.page;
    let start = (pageInfo.currentPage - 1) * pageInfo.pageSize;
    let end = pageInfo.currentPage * pageInfo.pageSize;
    comments = comments.slice(start, end);
    console.log(`currentPage: ${pageInfo.currentPage} start: ${start}  end: ${end}  comments: ${comments}  total: ${total}`);
  }

  return {
    status: 0,
    msg: '成功',
    data: comments,
    total: total
  }
}


// 創建API的URL,讓vue通過URL獲取數據
Mock.mock('/api/products', 'get', getProductList); // 獲取所有商品
Mock.mock('/api/products', 'post', getProductList); // 獲取分頁商品數據
Mock.mock('/api/productComments', 'post', getProductComments)

5.2 第二步,創建API介面,共VUE項目調用

註意:
引用 plugin/axios 中的 request 方法發起請求
發起請求的URL,不需要帶 /api/ 首碼

// /src/api/pruduct.js
import request from '@/plugin/axios'

export function getProducts (data) {
  return request({
    url: '/products',
    method: 'get',
    data
  })
}

export function getPageProducts (data) {
  return request({
    url: '/products',
    method: 'post',
    data
  })
}

export function getProductComments (data) {
  return request({
    url: '/productComments',
    method: 'post',
    data
  })
}

5.3 第三步,在 VUEX 中創建API,調用第二步中創建API介面,對數據進行操作

註意:

  • 在 /src/store/modules/ 下創建操作對象的模塊,如 product,其包括了 modules 文件夾和 index.js 文件,index.js 文件複製 \src\store\modules\d2admin\index.js即可
  • 在modules文件夾下創建 product.js 文件用於調用第二步中創建的 API 介面,並對收到的數據進行操作,文件結構與 VUE 項目中store.js相同。
  • 在 product.js 文件中聲明 namespaced: true,加了命名空間操作,將該文件直接加在了product文件夾下,被調用時,中間省去了一層 modules,為 product/product
  • 調用 API 時,需要先導入提前定義好的 API
  • 修改 \src\store\index.js 文件,將該模塊導出,使 view 能夠調用
// \src\store\modules\product\modules\product.js
import { getProducts, getPageProducts, getProductComments } from '@/api/product'


export default {
  namespaced: true,
  actions: {
    getProducts ({ commit }, payload) {
      commit('getProducts', payload)
    },

    getPageProducts ({ commit }, payload) {
      commit('getPageProducts', payload)
    },

    getComments ({ commit }, payload) {
      commit('getComments', payload)
    }
  },

  mutations: {
    getProducts (state, payload) {
      state.loading = true;
      getProducts(payload).then(res => {
        console.log('getProducts:  ', res);
        state.loading = false;
        state.products = res.products;
      }).catch( err => {
        console.log('err', err)
        state.loading = false
      })
    },

    getComments (state, payload) {
      // 獲取全部評價信息或者分頁評價信息
      state.loading = true;
      console.log(payload);
      getProductComments(payload).then(res => {
        console.log('getComments:  ', res);
        if (res.status == 0) {
          state.loading = false;
          state.total = res.total;
          if (payload.page) {
            state.pageComments = res.data;
          } else {
            state.comments = res.data;
          }
        }
      }).catch( err => {
        console.log('err', err)
        state.loading = false
      })
    },

    getPageProducts (state, payload) {
      console.log('page_params: ', payload);
      state.loading = true;
      getPageProducts(payload).then(res => {
        console.log('res:  ', res);
        state.loading = false;
        state.total = res.total;
        state.pageProducts = res.products;
      }).catch( err => {
        console.log('err', err)
        state.loading = false
      })
    }
  },

  state: {
    products: [],
    pageProducts: [],
    comments: [],
    pageComments: [],
    loading: false,
    total: 0
  },

  getters: {
    products: (state) => { return state.products },
    pageProducts: (state) => { return state.pageProducts },
    comments: (state) => { return state.comments },
    pageComments: (state) => { return state.pageComments },
    loading: (state) => { return state.loading },
    total: (state) => { return state.total }
  }
}
// \src\store\index.js

import Vue from 'vue'
import Vuex from 'vuex'

import d2admin from './modules/d2admin'
import product from './modules/product'

Vue.use(Vuex)

export default new Vuex.Store({
  modules: {
    d2admin,
    product
  }
})

5.4 第四步,在 view 中調用 VUEX 中的模塊中的方法實現具體功能。

註意:

  • 調用vuex中的方法以及從vuex中取數據時,都需加上命名空間 product/product
  • 使用mockjs 生成的圖片為base64 格式,需要新建組件轉換圖片進行顯示,view使用時需導入該組件
  • D2-admin框架自帶很多工具集,其中 src/libs/util.js 中cookies.get('token') 可以獲取當前用戶的token
  • CURD2.x 分頁功能放棄純前端分頁方法,採用後端傳入分頁好的數據(切片處理)進行分頁

自定義組件,用於顯示圖片:

// \src\views\product\all\MyImage.vue

<template>
    <div>
        <img :src="value" alt="商品圖片">
    </div>
</template>

<script>
export default {
    props: {
        value: '' // 必須叫value
    }
}
</script>

基於 CURD2.x 的本地增刪改查 以及 分頁功能實現

<template>
<d2-container>
  <template slot="header">
    <div class="flex-header">
      <div class="header-title">
        商品列表
      </div>
      <div>
        <el-input v-model="searchWords" placeholder="商品名稱" suffix-icon="el-icon-search"></el-input>
        <el-input v-model="searchMinPrice" placeholder="最低價格" suffix-icon="el-icon-caret-bottom"> </el-input>
        <el-input v-model="searchMaxPrice" placeholder="最高價格" suffix-icon="el-icon-caret-top"> </el-input>
        <el-button @click="onSearch"><i class="el-icon-search"></i> 搜索</el-button>
        <el-button type="primary" @click="addRow"><i class="fa fa-plus" aria-hidden="true"></i> 添加商品</el-button>
      </div>
    </div>
  </template>
  <el-card>
    <d2-crud
      ref="d2Crud"
      :columns="columns"
      :data="filterProducts.length ? filterProducts : products"
      add-title="添加商品"
      :add-template="addTemplate"
      :rowHandle="rowHandle"
      edit-title="編輯商品信息"
      :edit-template="editTemplate"
      :form-options="formOptions"
      :loading="loading"
      :pagination="pagination"
      @pagination-current-change="paginationCurrentChange"
      @row-add="handleRowAdd"
      @row-edit="handleRowEdit"
      @row-remove="handleRowRemove"
      @dialog-cancel="handleDialogCancel">
    </d2-crud>
  </el-card>
  <template slot="footer">ZAdmin Created By <a href="">@ZBW</a> for D2-admin</template>
</d2-container>
</template>

<script>
// 導入自定義用於顯示圖片的組件
import MyImage from "./MyImage"

export default {
  data () {
    return {
      searchWords: '',
      searchMinPrice: '',
      searchMaxPrice: '',
      filterProducts: [],
      columns: [
        {
          title: 'ID',
          key: 'id',
          width: '40'
        },
        {
          title: '名稱',
          key: 'title'
        },
        {
          title: '價格',
          key: 'price',
          width: '80'
        },
        {
          title: '庫存',
          key: 'stock',
          width: '80'
        },
        {
          title: '銷量',
          key: 'saleCount',
          width: '80'
        },
        {
          title: '是否上架',
          key: 'isSale',
          component: {
            name: 'el-select',
            options: [
              {
                value: 0,
                label: '否'
              },
              {
                value: 1,
                label: '是'
              }
            ]
          }
        },
        {
          title: '圖片',
          key: 'imgUrl',
          width: '120',
          component: {
            name: MyImage
          }
        },
        {
          title: '創建時間',
          key: 'createTime'
        }
      ],
      addTemplate: {
        createTime: {
          title: '創建日期',
          value: '2019-06-01',
          component: {
            name: 'el-date-picker',
            span: 12
          }
        },
        isSale: {
          title: '是否上架',
          value: 0,
          component: {
            name: 'el-select',
            options: [
              {
                value: 0,
                label: '否'
              },
              {
                value: 1,
                label: '是'
              }
            ],
            span: 12
          }
        },
        title: {
          title: '名稱',
          value: '',
          span: 24
        },
        price: {
          title: '價格',
          value: '',
          span: 24
        }
      },
      rowHandle: {
        columnHeader: '操作',
        edit: {
          icon: 'el-icon-edit',
          text: '編輯',
          size: 'mini',
          show (index, row) {
            if (row.showEditButton) {
              return true
            }
            return false
          },
          disabled (index, row) {
            if (row.forbidEdit) {
              return true
            }
            return false
          }
        },
        remove: {
          icon: 'el-icon-delete',
          size: 'mini',
          text: '刪除',
          fixed: 'right',
          confirm: true,
          show (index, row) {
            if (row.showRemoveButton) {
              return true
            }
            return false
          }
        }
      },
      editTemplate: {
        createTime: {
          title: '創建日期',
          component: {
            name: 'el-date-picker',
            span: 12
          }
        },
        isSale: {
          title: '是否上架',
          component: {
            name: 'el-select',
            options: [
              {
                value: 0,
                label: '否'
              },
              {
                value: 1,
                label: '是'
              }
            ],
            span: 12
          }
        },
        title: {
          title: '名稱',
          span: 24
        },
        price: {
          title: '價格',
          span: 24
        },
        forbidEdit: {
          title: '禁用按鈕',
          value: false,
          component: {
            show: false
          }
        },
        showEditButton: {
          title: '顯示按鈕',
          value: true,
          component: {
            show: false
          }
        }
      },
      formOptions: {
        labelWidth: '80px',
        labelPosition: 'left',
        saveLoading: false
      },
    }
  },
  created() {
    // 調用vuex中方法時,需要加上命名空間 product/product
    this.fetchData();
    this.$store.commit('product/product/getProducts');  // 請求全部商品列表

  },
  computed: {
    all_products() {
      // 全部商品列表,用於搜索
      return this.$store.getters['product/product/products'];
    },
    products() {
      // 當前分頁的商品列表
      // 取 vuex 中數據時,需要加上命名空間 product/product
      return this.$store.getters['product/product/pageProducts']
    },
    loading() {
      return this.$store.getters['product/product/loading']
    },
    pagination() {
      return {
        currentPage: 1,
        pageSize: 5,
        background: true,
        total: this.$store.getters['product/product/total']
      }
    }
  },
  
  
  methods: {
    onSearch() {
      this.filterProducts = this.all_products.filter(p => {
        if (this.searchWords){
          return p.price >= parseFloat(this.searchMinPrice) && p.price <= parseFloat(this.searchMaxPrice) && p.title.includes(this.searchWords);
        } else {
          return p.price >= parseFloat(this.searchMinPrice) && p.price <= parseFloat(this.searchMaxPrice);
        }
      })
      console.log('filterProducts: ', this.filterProducts);
    },
    addRow () {
      // 點擊新增後,以“添加”模式打開模態框
      this.$refs.d2Crud.showDialog({
        mode: 'add'
      })
    },
    handleRowAdd (row, done) {
      // 點擊確認添加後觸發的事件,可以將數據傳遞到後臺,保存至資料庫中
      this.formOptions.saveLoading = true
      setTimeout(() => {
        // row 是表單提交的內容
        console.log(row)
        this.$message({
          message: '保存成功',
          type: 'success'
        });

        // done可以傳入一個對象來修改提交的某個欄位
        done({
          price: '你雖然提交了 但是我能在這修改你顯示在頁面的內容!'
        })
        this.formOptions.saveLoading = false
      }, 300)
    },
    handleRowEdit ({ index, row }, done) {
      // 點擊確認修改後觸發的事件,可以將數據傳遞到後臺,保存至資料庫中
      //  index 是當前編輯行的索引, row 是當前編輯行的數據, done 用於控制編輯成功,可以在 done() 之前加入自己的邏輯代碼 
      this.formOptions.saveLoading = true
      setTimeout(() => {
        console.log(index)
        console.log(row)
        this.$message({
          message: '編輯成功',
          type: 'success'
        })

        // done可以傳入一個對象來修改提交的某個欄位
        // done()可以傳入包含表單欄位的對象來覆蓋提交的數據,done(false) 可以取消編輯
        done({
          price: '你雖然在後臺修改了價格,但是我能在這控制你在前臺顯示的內容'
        })
        this.formOptions.saveLoading = false
      }, 300)
    },
    handleRowRemove ({ index, row }, done) {
      // 與編輯類似
      setTimeout(() => {
        console.log(index)
        console.log(row)
        this.$message({
          message: '刪除成功',
          type: 'success'
        })
        done()
      }, 300)
    },
    handleDialogCancel (done) {
      // 關閉模態框執行的事件,並可以自定義執行done函數
      this.$message({
        message: '取消保存',
        type: 'warning'
      });
      done()
    },
    paginationCurrentChange (currentPage) {
      // 分頁頁碼發生改變觸發的事件
      this.pagination.currentPage = currentPage
      this.fetchData()
    },
    fetchData () {
      // 點擊分頁按鈕後,動態請求該頁所需的數據
      this.$store.commit('product/product/getPageProducts', {pageSize: this.pagination.pageSize, currentPage: this.pagination.currentPage})
    }
  }
}
</script>

<style scoped>
  .flex-header {
    display: flex;
    justify-content: space-between;
    align-items:center
  }

  .header-title {
    min-width: 4rem;
  }
  .flex-header .el-input {
    width: 200px;
    margin-right: 10px;
  }
</style>

經測試,各功能全部正常。

6 許可權控制

用戶許可權:

  • 使用token進行驗證
  • 使用sessionStorage保存用戶信息(包含許可權列表)
    • 登錄時保存
    • 退出時刪除
    • 結合路由meta信息進行判斷
  • meta:註意使用...meta,原來有bug,需要解析賦值(最新版中,該BUG已修複)

控制許可權的幾種方式:

  • 控制查看表格,以及表格的操作按鈕
  • 無許可權訪問某頁面或者頁面部分內容

6.1 第一步,在 mock 中添加用戶信息,以及不同的用戶對應的不同的許可權,並保存至本地的sessionStorage中。

  • 在 \src\mock\api\sys.login.js 中定義用戶信息,以及不同用戶角色(role)對應不同頁面(路由)的許可權信息
  • 定義獲取用戶信息介面,其中包括該用戶對用角色的許可權信息
// src\mock\api\sys.login.js

import Mock from 'mockjs'

const userDB = [
  { username: 'admin', password: 'admin', uuid: 'admin-uuid', name: '管理員', role: 'admin' },
  { username: 'editor', password: 'editor', uuid: 'editor-uuid', name: '編輯', role: 'editor' },
  { username: 'user1', password: 'user1', uuid: 'user1-uuid', name: '用戶1', role: 'user' }
]

// 為不同的用戶角色(role)劃分不同的許可權

const permissions = {
  admin: [
    {
      path: '/index',
      operations: ['modify', 'delete', 'add']
    },
    {
      path: '/product',
      operations: ['modify', 'delete', 'add']
    },
    {
      path: '/order',
      operations: ['modify', 'delete', 'add']
    },
    {
      path: '/permission',
      operations: ['modify', 'delete', 'add']
    },
    {
      path: '/users',
      operations: ['modify', 'delete', 'add']
    },
    {
      path: '/menu',
      operations: ['modify', 'delete', 'add']
    },
    {
      path: '/stuff',
      operations: ['modify', 'delete', 'add']
    },
    {
      path: '/data-charts',
      operations: ['modify', 'delete', 'add']
    },
    {
      path: '/log'
    }
  ],
  editor: [{
    path: '/index',
    operations: ['modify', 'add']
    },
    {
      path: '/product',
      operations: ['modify', 'add']
    },
    {
      path: '/permission',
      operations: ['modify']
    },
    {
      path: '/order',
      operations: ['modify', 'add']
    }
  ],
  user: [{
    path: '/index',
    operations: ['add']
    },
    {
      path: '/product',
      operations: ['add']
    },
    {
      path: '/order',
      operations: ['add']
    }
  ]
}

Mock.mock('/api/userInfo', 'post', ({ body }) => {
  const bodyObj = JSON.parse(body)
  if (!bodyObj.token) {
    return {
      status: 401,
      msg: 'token錯誤,需要登錄',
      data: {}
    }
  }
  const user = userDB.find(e => e.uuid === bodyObj.uuid)
  if (user) {
    return {
      status: 0,
      msg: '成功',
      data: {
        username: user.username,
        name: user.name,
        uuid: user.uuid,
        role: user.role,
        permissions: permissions[user.role]
      }
    }
  } else {
    return {
      status: 401,
      msg: '用戶名或密碼錯誤',
      data: {}
    }
  }
})

// 判斷是否登錄
export default [
  {
    path: '/api/login',
    method: 'post',
    handle ({ body }) {
      const user = userDB.find(e => e.username === body.username && e.password === body.password)
      if (user) {
        return {
          code: 0,
          msg: '登錄成功',
          data: {
            ...user,
            token: '8dfhassad0asdjwoeiruty'
          }
        }
      } else {
        return {
          code: 401,
          msg: '用戶名或密碼錯誤',
          data: {}
        }
      }
    }
  }
]

6.2 第二步,在 src/API 中定義調用 mock 中 API 的介面,取到用戶信息

// src\api\sys.login.js

import request from '@/plugin/axios'

export function AccountLogin (data) {
  return request({
    url: '/login',
    method: 'post',
    data
  })
}

export function getUserInfo (data) {
  return request({
    url: '/userinfo',
    method: 'post',
    data
  })
}

6.3 第三步,在 router/index.js 中添加鉤子函數。

  • 獲取跳轉路由的 meta 信息,判斷該路由是否需要驗證。
  • 若需驗證,則從sessionStroage中獲取用戶的許可權列表,如果該用戶的許可權列表中沒有此路由信息,則跳轉至401頁面提示無權訪問此頁面,如果許可權列表中有此路由信息,則將該用戶對此路由的許可權信息(如:增刪改查)添加至路由的 meta 信息中,並跳轉頁面。
  • 若不需要驗證,直接跳轉。
// \src\router\index.js

import Vue from 'vue'
import VueRouter from 'vue-router'

// 進度條
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'

import store from '@/store/index'

import util from '@/libs/util.js'

// 路由數據
import routes from './routes'

Vue.use(VueRouter)

// 導出路由 在 main.js 里使用
const router = new VueRouter({
  routes
})

/**
 * 路由攔截
 * 許可權驗證
 */
router.beforeEach((to, from, next) => {
  // 進度條
  NProgress.start()
  // 關閉搜索面板
  store.commit('d2admin/search/set', false)
  // 獲取路由的meta信息,判斷當前頁面是否需要驗證(登錄驗證,許可權驗證等等)
  // 驗證當前路由所有的匹配中是否需要有登錄驗證的
  if (to.matched.some(r => r.meta.auth)) {
    // 這裡暫時將cookie里是否存有token作為驗證是否登錄的條件
    // 請根據自身業務需要修改
    const token = util.cookies.get('token') // 獲取cookie中的token
    if (token && token !== 'undefined') {
      // 用戶已登錄,從sessionStorage中獲取用戶許可權信息
      let userInfo = JSON.parse(sessionStorage.getItem('userInfo'));
      console.log(token, userInfo);
      if (userInfo) {
        let permissions = userInfo.permissions;
        console.log('router permissions:  ', permissions);
        let allow = false;
        permissions.forEach(p => {
          let item = router.match(p.path)  // 找到許可權列表中匹配改條路由對應的許可權信息
          if (item) {
            // 匹配到路由後,將許可權信息添加在路由的meta中
            item.meta.operations = p.operations
          }

          // 完全匹配或者首碼匹配
          console.log('path:  ', to.path, p.path);
          if (p.path === to.path || to.path.startsWith(p.path)) {
            allow = true;
          }
        })

        // 根據allow判斷是否可以跳轉到下一個頁面
        if (allow) {
          next()
        } else {
          next({ name: '401' })
        }
      } else {
        next()
      }
    } else {
      // 沒有登錄的時候跳轉到登錄界面
      // 攜帶上登陸成功之後需要跳轉的頁面完整路徑
      next({
        name: 'login',
        query: {
          redirect: to.fullPath
        }
      })
      // https://github.com/d2-projects/d2-admin/issues/138
      NProgress.done()
    }
  } else {
    // 不需要身份校驗 直接通過
    next()
  }
})

router.afterEach(to => {
  // 進度條
  NProgress.done()
  // 多頁控制 打開新的頁面
  store.dispatch('d2admin/page/open', to)
  // 更改標題
  util.title(to.meta.title)
})

export default router

6.4 第四步,在 store 中根據許可權處理數據

  • (用戶登錄的API)登錄成功後把 userInfo 存至本地的 sessionStorage 中,註銷登錄後將用戶信息從sessionStorage 中刪除
  • 通過取到 router 對象( this.$route.meta.[operations] )的 meta 中的許可權信息(如:增刪改查),得出具體某個頁面的操作許可權信息,針對不同用戶許可權做出不同的操作。

註意:

  • $router: 表示全局的 router 對象
  • $route: 表示當前頁面的路由對象
// src\store\modules\d2admin\modules\account.js

import { Message, MessageBox } from 'element-ui'
import util from '@/libs/util.js'
import router from '@/router'
import { AccountLogin, getUserInfo } from '@api/sys.login'

export default {
  namespaced: true,
  actions: {
    /**
     * @description 登錄
     * @param {Object} param context
     * @param {Object} param username {String} 用戶賬號
     * @param {Object} param password {String} 密碼
     * @param {Object} param route {Object} 登錄成功後定向的路由對象 任何 vue-router 支持的格式
     */
    login ({ dispatch }, {
      username = '',
      password = ''
    } = {}) {
      return new Promise((resolve, reject) => {
        // 開始請求登錄介面
        AccountLogin({
          username,
          password
        })
          .then(async res => {
            // 設置 cookie 一定要存 uuid 和 token 兩個 cookie
            // 整個系統依賴這兩個數據進行校驗和存儲
            // uuid 是用戶身份唯一標識 用戶註冊的時候確定 並且不可改變 不可重覆
            // token 代表用戶當前登錄狀態 建議在網路請求中攜帶 token
            // 如有必要 token 需要定時更新,預設保存一天
            util.cookies.set('uuid', res.uuid)
            util.cookies.set('token', res.token)

            // 登錄成功後,載入用戶信息和許可權信息
            getUserInfo({ uuid: res.uuid, token: res.token }).then(res => {
              console.log('get user info:  ', res);
              if (res.status == 401) {
                return;
              } else {
                let userInfo = res.data;
                sessionStorage.setItem('userInfo', JSON.stringify(userInfo));
              }
            })

            // 設置 vuex 用戶信息
            await dispatch('d2admin/user/set', {
              name: res.name
            }, { root: true })
            // 用戶登錄後從持久化數據載入一系列的設置
            await dispatch('load')
            // 結束
            resolve()
          })
          .catch(err => {
            console.log('err: ', err)
            reject(err)
          })
      })
    },
    /**
     * @description 註銷用戶並返回登錄頁面
     * @param {Object} param context
     * @param {Object} param confirm {Boolean} 是否需要確認
     */
    logout ({ commit, dispatch }, { confirm = false } = {}) {
      /**
       * @description 註銷
       */
      async function logout () {
        // 刪除cookie
        util.cookies.remove('token')
        util.cookies.remove('uuid')

        // 退出後將sessionStorage中的用戶信息刪除
        sessionStorage.removeItem('userInfo')

        // 清空 vuex 用戶信息
        await dispatch('d2admin/user/set', {}, { root: true })
        // 跳轉路由
        router.push({
          name: 'login'
        })
      }
      // 判斷是否需要確認
      if (confirm) {
        commit('d2admin/gray/set', true, { root: true })
        MessageBox.confirm('註銷當前賬戶嗎?  打開的標簽頁和用戶設置將會被保存。', '確認操作', {
          confirmButtonText: '確定註銷',
          cancelButtonText: '放棄',
          type: 'warning'
        })
          .then(() => {
            commit('d2admin/gray/set', false, { root: true })
            logout()
          })
          .catch(() => {
            commit('d2admin/gray/set', false, { root: true })
            Message({
              message: '放棄註銷用戶'
            })
          })
      } else {
        logout()
      }
    },
    /**
     * @description 用戶登錄後從持久化數據載入一系列的設置
     * @param {Object} state vuex state
     */
    load ({ dispatch }) {
      return new Promise(async resolve => {
        // DB -> store 載入用戶名
        await dispatch('d2admin/user/load', null, { root: true })
        // DB -> store 載入主題
        await dispatch('d2admin/theme/load', null, { root: true })
        // DB -> store 載入頁面過渡效果設置
        await dispatch('d2admin/transition/load', null, { root: true })
        // DB -> store 持久化數據載入上次退出時的多頁列表
        await dispatch('d2admin/page/openedLoad', null, { root: true })
        // DB -> store 持久化數據載入側邊欄摺疊狀態
        await dispatch('d2admin/menu/asideCollapseLoad', null, { root: true })
        // DB -> store 持久化數據載入全局尺寸
        await dispatch('d2admin/size/load', null, { root: true })
        // end
        resolve()
      })
    }
  }
}

6.5 第五步,渲染頁面後,即可看到當前用戶具體有哪些操作許可權

  • 針對錶格操作,可以根據 meta 信息中存儲的操作許可權來判斷當前用戶所具有的許可權,最後對其許可權之外的按鈕等進行隱藏或者disable。
// src\views\permission\index.vue
<template>
  <d2-container>
    <template slot="header">頁面許可權驗證</template>
    <el-card>
    <d2-crud
      ref="d2Crud"
      :columns="columns"
      :data="data"
      add-title="我的新增"
      edit-title="我的修改"
      :add-template="addTemplate"
      :edit-template="editTemplate"
      :rowHandle="rowHandle"
      :form-options="formOptions"
      @row-add="handleRowAdd"
      @row-edit="handleRowEdit"
      @row-remove="handleRowRemove"
      @dialog-cancel="handleDialogCancel">
    <el-button slot="header" type="primary" style="margin-bottom: 5px;" @click="addRow">新增</el-button>
    </d2-crud>
    </el-card>
    <template slot="footer">ZAdmin Created By <a href="">@ZBW</a> for D2-admin</template>
  </d2-container>
</template>

<script>
export default {
  data () {
    return {
      columns: [
        {
          title: 'ID',
          key: 'id',
          width: 50
        },
        {
          title: '日期',
          key: 'date',
          width: 150
        },
        {
          title: '姓名',
          key: 'name'
        },
        {
          title: '地址',
          key: 'address'
        },
        {
          title: '會員',
          key: 'role',
          width: 100
        }
      ],
      data: [
        {
          id: 1,
          date: '2016-05-02',
          name: '王小虎',
          address: '上海市普陀區金沙江路 1518 弄',
          role: '普通會員',
          showEditButton: this.$route.meta.operations.includes('modify'),
          showRemoveButton: this.$route.meta.operations.includes('delete')
        },
        {
          id: 2,
          date: '2016-05-04',
          name: '王小虎',
          address: '上海市普陀區金沙江路 1517 弄',
          role: '普通會員',
          showEditButton: this.$route.meta.operations.includes('modify'),
          showRemoveButton: this.$route.meta.operations.includes('delete')
        },
        {
          id: 3,
          date: '2016-05-01',
          name: '王小虎',
          address: '上海市普陀區金沙江路 1519 弄',
          role: '普通會員',
          showEditButton: this.$route.meta.operations.includes('modify'),
          showRemoveButton: this.$route.meta.operations.includes('delete')
        },
        {
          id: 4,
          date: '2016-05-03',
          name: '王小虎',
          address: '上海市普陀區金沙江路 1516 弄',
          role: '普通會員',
          showEditButton: this.$route.meta.operations.includes('modify'),
          showRemoveButton: this.$route.meta.operations.includes('delete')
        },
        {
          id: 5,
          date: '2016-05-02',
          name: '王小虎',
          address: '上海市普陀區金沙江路 1518 弄',
          role: '普通會員',
          showEditButton: this.$route.meta.operations.includes('modify'),
          showRemoveButton: this.$route.meta.operations.includes('delete')
        }
      ],
      rowHandle: {
        columnHeader: '編輯表格',
        edit: {
          icon: 'el-icon-edit',
          text: '編輯',
          size: 'small',
          show (index, row) {
            if (row.showEditButton) {
              return true
            }
            return false
          },
          disabled (index, row) {
            if (row.forbidEdit) {
              return true
            }
            return false
          }
        },
        remove: {
          icon: 'el-icon-delete',
          size: 'small',
          fixed: 'right',
          confirm: true,
          show (index, row) {
            if (row.showRemoveButton) {
              return true
            }
            return false
          },
          disabled (index, row) {
            if (row.forbidRemove) {
              return true
            }
            return false
          }
        }
      },
      addTemplate: {
        date: {
          title: '日期',
          value: '2018-11-11',
          component: {
            name: 'el-date-picker',
            span: 12
          }
        },
        role: {
          title: '會員',
          value: '普通會員',
          component: {
            name: 'el-select',
            options: [
              {
                value: '普通會員',
                label: '普通會員'
              },
              {
                value: '金卡會員',
                label: '金卡會員'
              }
            ],
            span: 12
          }
        },
        name: {
          title: '姓名',
          value: ''
        },
        address: {
          title: '地址',
          value: ''
        }
      },
      editTemplate: {
        date: {
          title: '日期',
          value: '2018-11-11',
          component: {
            name: 'el-date-picker',
            span: 12
          }
        },
        role: {
          title: '會員',
          value: '普通會員',
          component: {
            name: 'el-select',
            options: [
              {
                value: '普通會員',
                label: '普通會員'
              },
              {
                value: '金卡會員',
                label: '金卡會員'
              }
            ],
            span: 12
          }
        },
        name: {
          title: '姓名',
          value: ''
        },
        address: {
          title: '地址',
          value: ''
        },
        forbidEdit: {
          title: '禁用按鈕',
          value: false,
          component: {
            show: false
          }
        },
        showEditButton: {
          title: '顯示按鈕',
          value: true,
          component: {
            show: false
          }
        }
      },
      formOptions: {
        labelWidth: '80px',
        labelPosition: 'left',
        saveLoading: false
      }
    }
  },
  methods: {
    addRow () {
      this.$refs.d2Crud.showDialog({
        mode: 'add'
      })
    },
    handleRowAdd (row, done) {
      this.formOptions.saveLoading = true
      setTimeout(() => {
        console.log(row)
        this.$message({
          message: '添加成功',
          type: 'success'
        });
        done({
          showEditButton: this.$route.meta.operations.includes('modify'),
          showRemoveButton: this.$route.meta.operations.includes('delete')
        })
        this.formOptions.saveLoading = false
      }, 300);
    },
    handleRowEdit ({index, row}, done) {
      this.formOptions.saveLoading = true
      setTimeout(() => {
        console.log(index)
        console.log(row)
        this.$message({
          message: '編輯成功',
          type: 'success'
        })
        done()
        this.formOptions.saveLoading = false
      }, 300)
    },
    handleDialogCancel (done) {
      this.$message({
        message: '操作已取消',
        type: 'warning'
      });
      done()
    },
    handleRowRemove ({index, row}, done) {
      setTimeout(() => {
        console.log(index)
        console.log(row)
        this.$message({
          message: '刪除成功',
          type: 'success'
        })
        done()
      }, 300)
    }
  }
}
</script>

<style scoped>

</style>

admin:

editer:

user:


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • [2019.06.24 學習筆記1] 1.定義客戶端腳本(如javascript); 2.包含腳本文件 3.引入外部腳本 html文件 javascript文件 ...
  • 瀏覽器 DNS緩存 瀏覽器DNS緩存的時間跟DNS伺服器返回的TTL值無關。 註:TTL(Time-To-Live),就是一條功能變數名稱解析記錄在DNS伺服器中的存留時間。 瀏覽器在獲取網站功能變數名稱的實際IP地址後會對其IP進行緩存,減少網路請求的損耗。每種瀏覽器都有一個固定的DNS緩存時間,其中Chrome ...
  • [TOC] 1. 起源 JavaScript誕生於 1995 年 最開始只是用來做一個簡單的輸入驗證器 最初名字為 LiveScript,為搭上Java當時火熱的順風車,臨時把名字改為JavaScript 2. JavaScript實現 一個完整的JavaScript包含以下三個部分 核心: ECM ...
  • 一個函數 如果輸入參數包含函數 或 返回值是函數,就稱為高階函數。 這篇文章介紹高階函數的一個子集:輸入 fn,輸出 fn'。 ...
  • 示例代碼托管在: "http://www.github.com/dashnowords/blogs" 博客園地址: "《大史住在大前端》原創博文目錄" 華為雲社區地址: "【你要的前端打怪升級指南】" [TOC] 一.概述 模塊相關的邏輯較為複雜,不僅包含JavaScript層的實現,也包括C++編 ...
  • 提前在E:\nodejs文件夾下建立node_gobal和node_cache 並配置環境變數NODE_PATH:E:\nodejs\node_global\node_modules 改變用戶變數中的C:\Users\11872\AppData\Roaming\npm為 E:\nodejs\node ...
  • 1 2 性別 3 4 5 12 13 ... ...
  • 二進位和十進位相互轉換、位運算 記錄下在codewar上做的一個題目和收穫 128.32.10.1 == 10000000.00100000.00001010.00000001 Because the above IP address has 32 bits, we can represent it ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...