前面介绍的表单控件和查询控件,都是原子性的,实现本身的功能便可。
而这里要介绍的是管理后台里面的各个组件之间的状态关系。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服务,因此放在另外一个仓库里面了。