前面介紹的表單控件和查詢控件,都是原子性的,實現本身的功能便可。
而這裏要介紹的是管理後臺裏面的各個組件之間的狀態關係。html
頁面結構爲啥須要狀態?由於組件劃分的很是原子化(細膩),因此形成了不少的組件,那麼組件之間就須要一種「通信方式」,這個就是狀態了。不只僅是傳遞數據,還能夠實現事件總線。vue
通常的後臺管理大致是這樣的結構:react
具體項目裏頁面結構會有一些變化,可是整體結構不會有太大的改變。git
作出來的效果大致是這樣的:vue-router
動態菜單
根據用戶權限加載須要的菜單。json
動態 tab
點擊一下左面的菜單,建立一個新的tab,而後加載對應的組件,通常是列表頁面(組件),也能夠是其餘頁面(組件)。後端
查詢
各類查詢條件那是必備的,總不能沒有查詢功能吧,查詢控件須要提供查詢條件。api
操做按鈕組
裏面能夠有常見的添加、修改、刪除、查看按鈕,也能夠有自定義的其餘按鈕。能夠「彈窗」也能夠直接調用後端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 // 子組件得到狀態 } }
由於使用的是局部的狀態,並非全局狀態,因此在須要使用的時候,首先須要在父組件裏面註冊一下。看起來彷佛沒有全局狀態簡單,可是能夠更好的實現複用,更輕鬆的區分數據,兄弟組件的狀態不會混淆。
由於或者狀態必須在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() } }
這段代碼,實際上是放在狀態管理類裏面的,拿出來單獨介紹一下,避免混淆。
altKey
是否按下了 alt 鍵。有些快捷鍵能夠是組合方式,原本想用 ctrl 鍵的,可是發如今網頁裏面 ctrl 開頭的快捷鍵實在太多,搶不過,因此只好 用 alt。
alt + a 至關於按 添加按鈕
alt + s 至關於按 修改按鈕
alt + d 至關於按 刪除按鈕
你以爲 a 表明 add,d 表明 delete嗎?
其實不是的,a、s、d 的鍵位能夠對應操做按鈕裏面前三個按鈕。就醬。
https://gitee.com/naturefw/nf-vite2-element
https://naturefw.gitee.io/nf-vue-cdn/elecontrol/
nf-vite2-element 的倉庫沒來得及開通pager服務,因此放在另外一個倉庫裏面了。