基于Vue3做一套适合自己的状态管理(八)列表页面需要的局部状态 & n级子模块
计划章节
- 基类:实现辅助功能
- 继承:充血实体类
- 继承:OptionApi 风格的状态
- Model:正确的打开方式
- 组合:setup 风格,更灵活
- 注册状态的方法、以及局部状态和全局状态
- 实践:当前登录用户的状态
- 实践:列表页面需要的状态
列表页面需要什么样的状态?
做一个稍微有一点点复杂的需求。
需求分析
一般的列表页面包含以下元素:
- 数据列表:一般是 table 形式
- 分页:一般由分页组件实现
- 查询:其实可以做一个查询控件
- 获取数据的函数:向后端申请数据
- 操作按钮:添加、修改、删除、详细信息等按钮
- 表单:实现添加、修改等功能
- 列表可以嵌套:打开子模块
基本需求暂时就是这些。
设计接口
我们来设计一个列表的状态,最好兼容一下通用性能。
我们先来定义一组接口:
- /type.ts
// 演示用
export interface ICompany {
id: string,
name: string,
address: string,
telephone: string
}
/**
* 列表里选择的记录
*/
export interface IGridSelection<T extends object> {
dataId: string | number, // 单选ID number 、string
row: BaseObject<T> & T, // 单选的数据对象 {}
dataIds: BaseArray<string | number>, // 多选ID []
rows: BaseArray<T> // 多选的数据对象 []
}
/**
* 查询条件
*/
export interface IQuery {
findValue: any, // 查询条件的精简形式
findArray: Array<object>, // 查询条件的对象形式
}
/**
* 分页信息
*/
export interface IPagerInfo {
pagerSize: number,
count: number, // 总数
pagerIndex: number // 当前页号
}
/**
* 列表页面用的状态
*/
export interface IListWebInfo<T extends object> {
meta: {
title: string, // 模块标题
deep: number, // 递归层数
moduleId: string // 模块ID
},
dataList: BaseArray<T>, // 数据列表
selection: IGridSelection<T>, // 选择
query: IQuery, // 查询条件
pagerInfo: IPagerInfo, // 分页信息
// 加载数据
loadData: (isReset: boolean) => void
}
- ICompany 用于演示的数据类型,简单定义几个属性。
- meta 存放页面需要的一些信息,比如模块ID、模块名称、递归的深度等,以后有用
- dataList 可以读写的数据列表,使用 BaseArray<T> 类型。
- selection
用户选择了哪个数据,或者哪些数据,可以用于修改和删除。
- dataId:选择的单条记录的ID
- row:选择的单条记录的信息,来自于列表,BaseObject 类型。
- dataIds:选择了多条记录的ID集合,一般用于批量删除。BaseArray<string | number>
- rows:选择的多条记录的集合。BaseArray 类型。
- pagerInfo
存放分页信息,给分页组件使用,也是获取数据的依据。
- pagerSize: number,
- count: number, // 总数
- pagerIndex: number // 当前页号
- query
存放查询条件,获取数据的查询依据
- findValue:来自于查询控件的数据结构,紧凑型
- findArray:标准的查询条件,数组。
- loadData 加载数据的函数,其实里面有一个回调函数。这个函数是便于内部做联动的。
IGridSelection 采用了泛型的接口,这样可以支持更多的类型。
创建状态
// 泛型约束
type Res = {
allCount: number,
list: Array<ICompany>
}
/**
* 注册局部状态,数据列表用
* @param service 获取数据的回调函数
* @returns
*/
export function regListState(
service: (query: IQuery, pagerInfo: IPagerInfo) => Promise<Res>
) {
// 使用 setup风格,定义列表用的状态
const state = defineStore<IListWebInfo<ICompany>>(flag,
() => {
// 页面信息
const meta = reactive({
title: '列表测试', // 模块标题
deep: 0, // 递归层数
moduleId: 100 // 模块ID
})
// 记录集合,reactive
const dataList = List<ICompany>([])
// 查询条件
const query = reactive({
findValue: {}, // 查询条件的精简形式
findArray: [], // 查询条件的对象形式
})
// 分页相关的信息
const pagerInfo = reactive({
pagerSize: 5,
count: 20, // 总数
pagerIndex: 1 // 当前页号
})
// 用户在列表里选择的的记录
const selection = reactive({
dataId: '',
row: Model<ICompany>(() => {
return {
id: '',
name: '',
address: '',
telephone: ''
}
}),
dataIds: List([]),
rows: List([])
})
/**
* 加载数据的函数,
* @param isReset true:设置总数,页号设置为1;false:仅翻页
*/
async function loadData (isReset = false) {
// 获取数据
const { allCount, list } = await service(query, pagerInfo)
if (isReset) {
pagerInfo.pagerIndex = 1
}
pagerInfo.count = allCount
dataList.$state = list
}
// 监听页号,实现翻页功能
watch(() => pagerInfo.pagerIndex, () => {
loadData()
})
// 监听查询条件,实现查询功能。
watch(query.findValue, () => {
loadData(true)
})
// 返回一个状态,如果需要只读,可以使用 readonly
return {
loadData, // 其实是内部使用的。
meta,
dataList,
selection,
pagerInfo, // 分页信息
query // 查询条件
}
}
)
// 初始化
state.loadData(true)
// 返回 数据和状态
return state
}
创建状态的思路:定义相关的对象存放需要的数据,然后用 watch 建立关联关系。
这里采用了 setup 风格的状态,因为可以更灵活的进行组合。
如果分成多个状态的话,那么在父组件里面需要创建多个状态,这样代码写起来就比较麻烦,不如定义一次,把需要的状态都创建出来。
对于数组类型,我们使用了 List 语法糖,这样赋值的时候可以方便一点。
- 监听分页 当页号发生变化时,重新加载数据。
- 监听查询条件 当查询条件发生变化时,页号置于 1 ,然后重新加载数据。
- 加载数据的函数 判断是否需要重置页号,然后用回调函数加载数据,然后设置分页信息,和数据源。
父组件(列表页面)
在父组件里面创建状态,加载需要的子组件,比如表单组件、查询组件、列表组件、分页组件、子模块组件等。
- template
<!--修改表单-->
<sub-form></sub-form>
<!--子模块-->
<sub-module></sub-module>
<hr>
<!--查询-->
<sub-find></sub-find>
<hr>
<!--列表-->
<sub-grid></sub-grid>
<hr>
<!--分页-->
<sub-comp-pager></sub-comp-pager>
把具体的代码都分到相应的子组件里面,这样列表组件的代码就会非常整齐易读,也容易更换子组件。
// 列表页面的状态
import { regListState } from './_state-data-list'
// 引入接口
import type { IQuery, IPagerInfo } from '../../types/type'
// 获取数据,模拟一下
const getData = async (query: IQuery, pagerInfo: IPagerInfo) => {
const re = {}
// 从后端获取数据,略
return re
}
// 注册列表页面的状态
const state = regListState(getData)
父组件的代码部分,主要负责加载需要的子组件,加载创建状态的函数,然后创建状态即可。
分页组件
做一个分页用的组件,获取父组件创建的状态,然后取出来分页信息(pagerInfo),交给 template 里面的 el-gagination组件。
// 获取状态的函数
import { getListState } from './_state-data-list'
// 获取父组件的状态
const { pagerInfo } = getListState()
使用UI库提供的 el-pagination 组件实现分页的功能,我们就不用关心细节了,只需要设置需要的属性即可,比如设置v-model:currentPage
实现翻页的互动。
<el-pagination
v-model:currentPage="pagerInfo.pagerIndex"
:total="pagerInfo.count">
</el-pagination>
分页功能单独做一个组件的优点:
- 可以减轻列表页面的代码,列表页面不用关心分页的细节。
- 可以灵活更换分页组件,分页的部分可以各种换,不会影响列表页面的代码。
- 列表页面可以选择不同的分页方案。
查询组件
查询组件和分页组件基本类似,也是先获取父组件创建的状态,然后取出查询条件(query)。然后根据用户输入的查询信息设置query即可。
<el-row>
<el-col :span="1"> 名称:</el-col>
<el-col :span="4">
<el-input v-model="findValue.name" placeholder="名称"></el-input>
</el-col>
其他查询条件 略
</el-row>
做一个简单的表单,示意一下查询功能。
import { getListState } from './_state-data-list'
// 获取父组件的状态
const { query } = getListState()
// 记录查询条件
const findValue = reactive({
name: '',
address: '',
telephone: ''
})
watch(findValue, () => {
query.findArray.length = 0
for (const key in findValue) {
const value = findValue[key]
if (value) {
query.findArray.push({key, value})
}
}
})
监听查询条件的变化,然后遍历集合,如果有查询条件,则存入 query.findArray 里面。这样可以把查询信息共享出去了。
这里只是一个简单的示例,没有考虑其他情况,比如防抖等需求。
修改的表单
修改功能的表单组件,需要获取用户选择的记录,然后绑定到表单里面,做好修改的准备。
const { selection } = getListState()
const dialogVisible = ref(false)
// 创建一个 model
const company = Model(() => {
return {
id: '',
name: '',
address: '',
telephone: ''
}
})
// 打开“窗口”准备修改
const show = () => {
dialogVisible.value = true
if (selection.dataId) {
company.$state = selection.row
}
}
- 定义一个 Model 用于绑定表单
- 做一个事件,显示弹窗,获取用户选择的记录,设置个 model。
如果属性名称对应不上的话,也是可以有提示的。
template 里面可以用 el-dialog 做一个弹窗,然后里面用 el-form 做一个表单,代码比较简单,这里就不贴了。
子模块组件
这里可以采用递归的方式加载子模块,因为都是列表需求,除了数据(表、字段)之外,基本都可以是一样的。
这里又做了一个子模块的组件,是因为一般情况下子模块会以“弹窗”的方式出现,所以再做一个组件,避免代码过多带来混乱。
<el-button @click="dialogVisible=true">打开子模块</el-button>
<el-dialog v-model="dialogVisible" >
<!--递归加载子模块,可以无限递归-->
<sub-comp></sub-comp>
</el-dialog>
外面一个 el-dialog,里面使用列表组件。
- 打开多个子模块的效果:
小结
在父组件创建一个状态,加载各种子组件,子组件共享父组件共享的状态,各负其职。
这样各个组件通过共享的状态,就可以联动起来了。
这段代码反反复复改了好几回,对于泛型的应用方法有了更深入的了解,不敢保证是最优的,只是目前的能力基本只能优化到这个样子了。估计以后还可以继续优化。
源码
在线演示
转载自:https://juejin.cn/post/7239267216804839479