技术栈

  • 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服务,所以放在另一个仓库里面了。

vue3,后台管理列表页面各组件之间的状态关系的更多相关文章

  1. vuex-- Vue.的中心化状态管理方案(vue 组件之间的通信简化机制)

    vuex-- Vue.的中心化状态管理方案(vue 组件之间的通信简化机制) 如果你在使用 vue.js , 那么我想你可能会对 vue 组件之间的通信感到崩溃 .vuex就是为了解决组件通信问题的. ...

  2. Prism 文档 第三章 管理组件之间的依赖关系

                                                                          第3章:管理组件之间的依赖关系 基于Prism库的复合应用程 ...

  3. 微信小程序--页面与组件之间如何进行信息传递和函数调用

    微信小程序--页面与组件之间如何进行信息传递和函数调用 ​ 这篇文章我会以我自己开发经验从如下几个角度来讲解相关的内容 页面如何向组件传数据 组件如何向页面传数据 页面如何调用组件内的函数 组件如何调 ...

  4. vue2.0在页面中自定义组件模块,以及页面与组件之间的数据传递

    1,在初始文件index.html中加入要引入的模块,注意驼峰命名的方式(我就是没写成驼峰,报错) <!DOCTYPE html> <html> <head> &l ...

  5. tomcat配置后台管理监控页面

  6. 在后台new出页面(组件)

    Page p = new Page();            Control u = p.LoadControl("~/folderName/controlName.ascx") ...

  7. CSS 页面布局、后台管理示例

    CSS 页面布局.后台管理示例 页面布局 1.头部菜单 2.中间内容/中间左侧菜单 3.底部内容 <div class='pg-header'> <div style='width: ...

  8. go语言实战教程之 后台管理页面统计功能开发(1)

    本节内容我们将学习开发实现后台管理平台页面统计功能开发的功能接口,本章节内容将涉及到多种请求路由的方式. 功能介绍 后台管理平台不仅是功能管理平台,同时还是数据管理平台.从数据管理平台角度来说,在管理 ...

  9. 后台管理UI的选择

    最近要做一个企业的OA系统,以前一直使用EasyUI,一切都好,但感觉有点土了,想换成现在流行的Bootstrap为基础的后台UI风格,想满足的条件应该达到如下几个: 1.美观.大方.简洁 2.兼容I ...

随机推荐

  1. 在Linux中通过Top运行进程查找最高内存和CPU使用率

    按内存使用情况查找前15个进程,在批处理模式下为"top" 使用top命令查看有关当前状态,系统使用情况的更详细信息:正常运行时间,负载平均值和进程总数. 分类:Linux命令操作 ...

  2. 011.Ansible条件语句

    一 简介 在有的时候play的结果依赖于变量.fact或者是前一个任务的执行结果,或者有的时候,我们会基于上一个task执行返回的结果而决定如何执行后续的task.这个时候就需要用到条件判断. 条件语 ...

  3. Linux_配置加密的https

    一.配置https 1.安装好httpd服务后,安装mod_ssl模块 //首先查看是否安装mod_ssl [root@localhost ~]# rpm -qa | grep mod_ssl //安 ...

  4. Node.js入门(含NVM、NPM、NVM的安装)-(转载)

    Node.js的介绍 引擎 引擎的特性: JS的内核即引擎.因为引擎有以下特性: (1)转化的作用: 汽油柴油等等->动能 模板+数据--->页面 js引擎:js 代码--->机器码 ...

  5. nginx官方源安装-主配置文件详解

    HTTP相关术语 PV : Page Visit 页面独立浏览量,查看日志生成条数可以看到PV数量. PV全称Page View,中文翻译即页面浏览.其具体的度量方法是从浏览器发出一个对网络服务器的请 ...

  6. Java public 和 private 访问修饰符

    何为封装 从事面向对象编程的 Java 程序员,不可能不知道封装,它是面向对象编程的精髓,非常重要. 那什么是封装?字面意思就是把摆在外面的东西包起来. 一句话,封装就是对外隐藏内部细节. 那为何要封 ...

  7. 10.12 telnet:远程登录主机

    telnet命令 以前是用于登录远程主机,对远程主机进行管理的.但是因为telnet是采用明文传送报文的,其安全性不好,因此现在很多Linux服务器都不开放telnet服务,而是改用更安全的SSH服务 ...

  8. Processing平台之PVector求角度

    问题:在processing 平台,通过给定三个PVector向量,如何求他们之间的夹角,同时确定是在左侧还是右侧? 如图所示,在processing 平台中,PVector表示点的坐标是以原点为起点 ...

  9. TVM量化小结手册

    TVM量化小结手册 文章目录 Offical References TVM quantization roadmap INT8 quantization proposal Quantization S ...

  10. NVIDIA数据中心深度学习产品性能

    NVIDIA数据中心深度学习产品性能 在现实世界的应用程序中部署AI,需要训练网络以指定的精度融合.这是测试AI系统的最佳方法-准备将其部署在现场,因为网络随后可以提供有意义的结果(例如,对视频流正确 ...