天天看點

vue3,背景管理清單頁面各元件之間的狀态關系技術棧前情回顧頁面結構視訊示範設計狀态使用“輕量級狀态管理”定義狀态:元件裡面使用輕量級狀态的方法清單的管理類檔案結構清單狀态的使用快捷鍵開源線上示範

技術棧

  • vite2
  • vue 3.0.5
  • vue-router 4.0.6
  • vue-data-state 0.1.1
  • element-plus 1.0.2-beta.39

前情回顧

  • 表單控件
  • 查詢控件
  • 輕量級狀态管理

前面介紹的表單控件和查詢控件,都是原子性的,實作自己的功能即可。

而這裡要介紹的是管理背景裡面的各個元件之間的狀态關系。

為啥需要狀态?因為元件劃分的非常原子化(細膩),是以造成了很多的元件,那麼元件之間就需要一種“通訊方式”,這個就是狀态了。不僅僅是傳遞資料,還可以實作事件總線。

頁面結構

一般的背景管理大體是這樣的結構:

具體項目裡頁面結構會有一些變化,但是總體結構不會有太大的改變。

做出來的效果大體是這樣的:

  • 動态菜單

    根據使用者權限加載需要的菜單。

  • 動态 tab

    點選一下左面的菜單,建立一個新的tab,然後加載對應的元件,一般是清單頁面(元件),也可以是其他頁面(元件)。

  • 查詢

    各種查詢條件那是必備的,總不能沒有查詢功能吧,查詢控件需要提供查詢條件。

  • 操作按鈕組

    裡面可以有常見的添加、修改、删除、檢視按鈕,也可以有自定義的其他按鈕。可以“彈窗”也可以直接調用後端API。

  • 清單

    顯示客戶需要的資料,看起來簡單,但是要和查詢、翻頁、添加、修改、删除等功能配合。

  • 分頁

    這是和清單最接近的一個需求,因為資料有可能很大,不能一次性都顯示出來,那麼就需要分頁處理,是以分頁控件和清單控件就是天然CP。

  • 表單(添加、修改)

    資料送出之後,為了便于确認資料添加成功,是不是需要通知清單去更新資料呢?總不能填完資料,清單一點變化都沒有吧。

  • 删除

    資料删掉了,不管是實體删除還是邏輯删除,清單裡面都不需要再顯示出來了。

    也就是說删除後要通知清單更新資料。

總之,各個元件直接需要統籌一下狀态關系。

視訊示範

我們來看一下實際效果。

【放視訊】

設計狀态

我們整理一下需求,用腦圖表達出來:

使用“輕量級狀态管理”定義狀态:

/store-ds/index.js

import VuexDataState from 'vue-data-state'

export default VuexDataState.createStore({
  global: { // 全局狀态
    userOnline: {
      name: 'jyk' //
    }
  },
  local: { // 局部狀态
    dataListState () { // 擷取清單資料的狀态 dataPagerState
      return {
        query: {}, // 查詢條件
        pager: { // 分頁參數
          pageTotal: 100, // 0:需要統計總數;其他:不需要統計總數
          pageSize: 5, // 一頁記錄數
          pageIndex: 1, // 第幾頁的資料,從 1  開始
          orderBy: { id: false } // 排序字段
        },
        choice: { // 清單裡面選擇的記錄
          dataId: '', // 單選,便于修改和删除
          dataIds: [], // 多選,便于批量删除
          row: {}, // 選擇的記錄資料,僅限于清單裡面的。
          rows: [] // 選擇的記錄資料,僅限于清單裡面的。
        },
        hotkey: () => {}, // 處理快捷鍵的事件,用于操作按鈕
        reloadFirstPager: () => {}, // 重新加載第一頁,統計總數(添加後)
        reloadCurrentPager: () => {}, // 重新加載目前頁,不統計總數(修改後)
        reloadPager: () => {} // 重新加載目前頁,統計總數(删除後)
      }
    } 
  },
  init (state) {
  }
})
           

這裡沒有使用 Vuex,因為我覺得 Vuex 有點臃腫,還是自己做的清爽。

另外,狀态裡面除了資料之外,還可以有方法(事件總線)。

元件裡面使用輕量級狀态的方法

// 引入狀态
import VueDS from 'vue-data-state'

// 通路狀态
const { reg, get } = VueDS.useStore()
// 父元件注冊清單的狀态
const state = reg.dataListState()

// 子元件裡面擷取父元件注冊的狀态
const dataListState = get.dataListState()

           

先引入狀态,然後在父元件注冊(也就是注入)狀态,然後在子元件就可以擷取狀态。

函數名就是 /store-ds/index.js 裡面定義的名稱。

然後我們還可以仿照 MVC 的 Controllar ,做一個控制類,當然也可以叫做管理類。

叫什麼不是重點,重點是實作了什麼功能。

清單的管理類

我們可以為清單的狀态寫一個狀态的管理類。

這個類是在單獨的 js 檔案裡面,并不需要像 Vuex 那樣去設定 action 或者 module。

/control/data-list.js

import { watch, reactive } from 'vue'
// 狀态
import VueDS from 'vue-data-state'

// 仿後端API
import service from '../api/dataList-service.js'

/**
 * * 資料清單的通用管理類
 * * 注冊清單的狀态
 * * 關聯擷取資料的方式
 * * 設定快捷鍵
 * @param {string} modeluId 子產品ID
 * @returns 清單狀态管理類
 */
export default function dataListControl (modeluId) {
  // 顯示資料清單的數組
  const dataList = reactive([])
  // 模拟後端API
  const { loadDataList } = service()

  // 通路狀态
  const { reg, get } = VueDS.useStore()
  // 子元件裡面擷取父元件注冊的狀态
  const dataListState = get.dataListState()

  // 資料加載中
  let isLoading = false

  /**
   * 父元件注冊狀态
   * @returns 注冊清單狀态
   */
  const regDataListState = () => {
    // 注冊清單的狀态,用于分頁、查詢、添加、修改、删除等
    const state = reg.dataListState()

    //  重新加載第一頁,統計總數(添加、查詢後)
    state.reloadFirstPager = () => {
      isLoading = true
      state.pager.pageIndex = 1 // 顯示第一頁
   
      // 擷取資料
      loadDataList(modeluId, state.pager, state.query, true).then((data) => {
        state.pager.pageTotal = data.count
        dataList.length = 0
        dataList.push(...data.list)
        isLoading = false
      })
    }
    // 先執行一下,擷取初始資料
    state.reloadFirstPager()

    // 重新加載目前頁,不統計總數(修改後)
    state.reloadCurrentPager = () => {
      // 擷取資料
      loadDataList(modeluId, state.pager, state.query).then((data) => {
        dataList.length = 0
        dataList.push(...data)
      })
    }

    // 重新加載目前頁,統計總數(删除後)
    state.reloadPager = () => {
      // 擷取資料
      loadDataList(modeluId, state.pager, state.query, true).then((data) => {
        state.pager.pageTotal = data.count
        dataList.length = 0
        dataList.push(...data.list)
      })
    }

    // 監聽,用于翻頁控件的翻頁。翻頁,擷取指定頁号的資料
    watch(() => state.pager.pageIndex, () => {
      // 避免重複加載
      if (isLoading) {
        // 不擷取資料
        return
      }
      // 擷取資料
      loadDataList(modeluId, state.pager, state.query).then((data) => {
        dataList.length = 0
        dataList.push(...data)
      })
    })

    return state
  }
 
  return {
    setHotkey, // 設定快捷鍵,(後面介紹)
    regDataListState, // 父元件注冊狀态
    dataList, // 父元件獲得清單
    dataListState // 子元件獲得狀态
  }
}
           

管理類的功能:

  1. 父元件注冊狀态
  2. 子元件擷取狀态
  3. 定義清單資料的容器
  4. 各種監聽
  5. 事件總線

父元件注冊狀态

因為使用的是局部的狀态,并不是全局狀态,是以在需要使用的時候,首先需要在父元件裡面注冊一下。看起來似乎沒有全局狀态簡單,但是可以更好的實作複用,更輕松的區分資料,兄弟元件的狀态不會混淆。

子元件擷取狀态

因為或者狀态必須在vue的直接函數内才行,是以才需要先把狀态擷取出來,而不能等到觸發事件了再擷取。

定義清單資料的容器

清單資料并沒有在狀态裡面定義,而是在管理類裡面定義的,因為主要清單元件才需要這個清單資料,其他的元件并不關心清單資料。

監聽:

  • 監聽頁号的變化,依據目前的查詢條件擷取新的記錄,用于翻頁,不用重新統計總數。

事件:

  • 統計總數并且翻到第一頁,用于查詢條件變化,添加新記錄。
  • 重新擷取目前頁号的清單資料,用于修改資料後的更新。
  • 重新擷取目前頁号的清單資料,并且統計總記錄數,用于删除資料後的更新。

是否重新統計總數

可能你會發現上面擷取資料裡面有一個明顯的差別,那就是是否需要統計總數。

在資料量非常大的情況下,如果每次翻頁都重新統計總數,那麼會嚴重影響性能!

其實仔細考慮一下,一些情況是不用重新統計總數的,比如翻頁、修改後的更新等,這些操作都不會影響總記錄數(不考慮并發操作),那麼我們也就不必每次都重新統計。

檔案結構

基礎功能搭建好了之後,剩下的就簡單了,建立元件設定模闆、控件、元件和使用狀态即可。

總體結構如下:

清單狀态的使用

基礎工作做好之後我們來看看,在各個元件裡面是如何使用狀态的。

查詢

首先看看查詢,使用者設定查詢條件後,查詢控件把查詢條件記入狀态裡面。

然後調用狀态管理裡的 reloadFirstPager ,擷取清單資料。

查詢控件支援防抖功能。

<template>
  <!--查詢-->
  <nf-el-find
    v-model="listState.query"
    v-bind="findProps"
    @my-change="myChange"
  />
</template>
           

直接使用查詢控件,模闆内容是不是很簡單了?

import { reactive } from 'vue'
// 加載json
import loadJson from './control/loadjson.js'
// 狀态
import VueDS from 'vue-data-state'

  // 元件
  import nfElFind from '/ctrl/nf-el-find/el-find-div.vue'

  // 屬性:子產品ID、查詢條件
  const props = defineProps({
    moduleId:  [Number, String]
  })

  // 設定 查詢的 meta
  const findProps = reactive({reload: true})
  loadJson(props.moduleId, 'find', findProps)

  // 通路狀态
  const { get } = VueDS.useStore()
  // 擷取狀态
  const listState = get.dataListState()
  // 使用者設定查詢條件後觸發
  const myChange = (query) => {
    // 擷取第一頁的資料,并且重新統計總數
    listState.reloadFirstPager()
  }
           

分頁

分頁就很簡單了,查詢條件由查詢控件搞定,是以這裡隻需要按照 el-pagination 的要求,把分頁狀态設定給 el-pagination 的屬性即可。

<template>
  <!--分頁-->
  <el-pagination
    background
    layout="prev, pager, next"
    v-model:currentPage="pager.pageIndex"
    :page-size="pager.pageSize"
    :total="pager.pageTotal">
  </el-pagination>
</template>
           

直接把狀态作為屬性值。

// 狀态
import VueDS from 'vue-data-state'

// 通路狀态
const { get } = VueDS.useStore()
// 擷取分頁資訊
const pager = get.dataListState().pager
           

直接擷取分頁狀态設定 el-pagination 的屬性即可。

翻頁的時候 el-pagination 會自動修改 pager.pageIndex 的值,而狀态管理裡面會監聽其變化,然後擷取對應的清單資料。

添加、修改

添加完成之後,總記錄數會增加,是以需要重新統計總記錄數,然後翻到第一頁。

而修改之後,一般總記錄數并不會變化,是以隻需要重新擷取目前頁号的資料即可。

<template>
  <div>
    <!--表單-->
    <el-form
      ref="formControl"
      v-model="model"
      :partModel="partModel"
      v-bind="formProps"
    >
    </el-form>
    <span class="dialog-footer">
      <el-button @click="">取 消</el-button>
      <el-button type="primary" @click="mysubmit">确 定</el-button>
    </span>
  </div>
</template>

           

使用表單控件和兩個按鈕。

import { computed, reactive, watch } from 'vue'
import { ElMessage } from 'element-plus'
// 加載json
import loadJson from './control/loadjson.js'

// 狀态
import VueDS from 'vue-data-state'

// 仿後端API
import service from './api/data-service.js'
 
  // 表單元件
  import elForm from '/ctrl/nf-el-form/el-form-div.vue'

  // 通路狀态
  const { get } = VueDS.useStore()

  // 定義屬性
  const props = defineProps({
    moduleId:  [Number, String], // 子產品ID
    formMetaId:  [Number, String], // 表單的ID
    dataId: Number, // 修改或者顯示的記錄的ID
    type: String // 類型:添加、修改、檢視
  })

  // 子產品ID + 表單ID = 自己的标志
  const modFormId = computed(() => props.moduleId + props.formMetaId)

  // 子元件裡面擷取狀态
  const dataListState = get.dataListState(modFormId.value)
  
  // 表單控件的 model
  const model = reactive({})
  
  // 表單控件需要的屬性
  const formProps = reactive({reload:false})
  // 加載需要的 json
  loadJson(props.moduleId, 'form_' + props.formMetaId,  formProps)
  
  // 仿後端API
  const { getData, addData, updateData } = service(modFormId.value)

  // 監聽記錄ID的變化,加載資料便于修改
  watch(() => props.dataId, (id) => {
    if (props.type !== 'add') {
      // 加載資料
      getData( id ).then((data) => {
        Object.assign(model, data[0])
        formProps.reload = !formProps.reload
      })
    }
  },
  {immediate: true})

  // 送出資料
  const mysubmit = () => {
    // 判斷是添加還是修改
    if (props.type === 'add'){
      // 添加資料
      addData(model).then(() => {
        ElMessage({
          type: 'success',
          message: '添加資料成功!'
        })
        // 重新加載第一頁的資料
        dataListState.reloadFirstPager()
      })
    } else if (props.type === 'update') {
      // 修改資料
      updateData(model, props.dataId).then(() => {
        ElMessage({
          type: 'success',
          message: '修改資料成功!'
        })
        // 重新加載目前頁号的資料
        dataListState.reloadCurrentPager()
      })
    }
  }
           

代碼稍微多了一些,基本上就是在合适的時機調用狀态裡的重新加載資料的事件。

删除

删除之後也會影響總記錄數,是以需要重新統計,然後重新整理目前頁号的清單資料。

删除的代碼寫在了操作按鈕的元件裡面,對應删除按鈕觸發的事件:

case 'delete':
        dialogInfo.show = false
        // 删除
        ElMessageBox.confirm('此操作将删除該記錄, 是否繼續?', '溫馨提示', {
          confirmButtonText: '删除',
          cancelButtonText: '後悔了',
          type: 'warning'
        }).then(() => {
          // 後端API
          const { deleteData } = service(props.moduleId + meta.formMetaId)
          deleteData(dataListState.choice.dataId).then(() => {
            ElMessage({
              type: 'success',
              message: '删除成功!'
            })
            dataListState.reloadPager() // 重新整理清單資料
          })
        }).catch(() => {
          ElMessage({
            type: 'info',
            message: '已經取消了。'
          })
        })
        break
           

删除成功之後,調用狀态的 dataListState.reloadPager() 重新整理清單頁面。

快捷鍵

我是喜歡用快捷鍵實作一些操作的,比如翻頁、添加等操作。

用滑鼠去找到“上一頁”、“下一頁”或者需要的頁号,這個太麻煩。

如果通過鍵盤操作就能翻頁,是不是可以更友善一些呢?

比如 w、a、s、d,分别表示上一頁、下一頁、首頁、末頁;數字鍵就是要翻到的頁号。

是不是有一種打遊戲的感覺?

實作方式也比較簡單,一開始打算用 Vue 的鍵盤事件,但是發現似乎不太好用,于是改用監聽document 的鍵盤事件。

/**
   * 清單頁面的快捷鍵
   */
  const setHotkey = (dataListState) => {
    // 設定分頁、操作按鈕等快捷鍵
    // 計時器做一個防抖
    let timeout
    let tmpIndex = 0 // 頁号
    document.onkeydown = (e) => {
      if (!(e.target instanceof HTMLBodyElement)) return // 表單觸發,退出
      if (e.altKey) {
        // alt + 的快捷鍵,調用操作按鈕的事件
        dataListState.hotkey(e.key)
      } else {
        // 翻頁
        const maxPager = parseInt(dataListState.pager.pageTotal / dataListState.pager.pageSize) + 1
        switch (e.key) {
          case 'ArrowLeft': // 左箭頭 上一頁
          case 'PageUp':
          case 'a':
            dataListState.pager.pageIndex -= 1
            if (dataListState.pager.pageIndex <= 0) {
              dataListState.pager.pageIndex = 1
            }
            break
          case 'ArrowRight': // 右箭頭 下一頁
          case 'PageDown':
          case 'd':
            dataListState.pager.pageIndex += 1
            if (dataListState.pager.pageIndex >= maxPager) {
              dataListState.pager.pageIndex = maxPager
            }
            break
          case 'ArrowUp': // 上箭頭
          case 'Home': // 首頁
          case 'w':
            dataListState.pager.pageIndex = 1
            break
          case 'ArrowDown': // 下箭頭
          case 'End': // 末頁
          case 's':
            dataListState.pager.pageIndex = maxPager
            break
          default:
            // 判斷是不是數字
            if (!isNaN(parseInt(e.key))) {
              // 做一個防抖
              tmpIndex = tmpIndex * 10 + parseInt(e.key)
              clearTimeout(timeout) // 清掉上一次的計時
              timeout = setTimeout(() => {
                // 修改 modelValue 屬性
                if (tmpIndex === 0) {
                  dataListState.pager.pageIndex = 10
                } else {
                  if (tmpIndex >= maxPager) {
                    tmpIndex = maxPager
                  }
                  dataListState.pager.pageIndex = tmpIndex
                }
                tmpIndex = 0
              }, 500)
            }
        }
      }
      e.stopPropagation()
    }
  }
           

這段代碼,其實是放在狀态管理類裡面的,拿出來單獨介紹一下,避免混淆。

  • document.onkeydown

    監聽鍵盤按下的事件,這個 e 并不是原生的 e,而是Vue封裝之後的 e。

    首先要判斷一下事件來源,如果是 input 等觸發的需要跳過,以免影響正常的資料輸入。

    然後是判斷按了哪個按鍵,根據需求調用對應的函數。

  • altKey

    是否按下了 alt 鍵。有些快捷鍵可以是組合方式,本來想用 ctrl 鍵的,但是發現在網頁裡面 ctrl 開頭的快捷鍵實在太多,搶不過,是以隻好 用 alt。

  • alt + a 相當于按 添加按鈕
  • alt + s 相當于按 修改按鈕
  • alt + d 相當于按 删除按鈕

你覺得 a 代表 add,d 代表 delete嗎?

其實不是的,a、s、d 的鍵位可以對應操作按鈕裡面前三個按鈕。就醬。

  • 數字翻頁的防抖

    如果不做防抖的話,隻能實作 1-9 的頁号翻頁,如果做了防抖的話,基本可以做到三位數頁号的翻頁。是以手欠做了個防抖。

開源

https://gitee.com/naturefw/nf-vite2-element

線上示範

https://naturefw.gitee.io/nf-vue-cdn/elecontrol/

nf-vite2-element 的倉庫沒來得及開通pager服務,是以放在另一個倉庫裡面了。