likes
comments
collection
share

基于Vue3做一套适合自己的状态管理(八)列表页面需要的局部状态 & n级子模块

作者站长头像
站长
· 阅读数 36

计划章节

  1. 基类:实现辅助功能
  2. 继承:充血实体类
  3. 继承:OptionApi 风格的状态
  4. Model:正确的打开方式
  5. 组合:setup 风格,更灵活
  6. 注册状态的方法、以及局部状态和全局状态
  7. 实践:当前登录用户的状态
  8. 实践:列表页面需要的状态

列表页面需要什么样的状态?

做一个稍微有一点点复杂的需求。

需求分析

一般的列表页面包含以下元素:

  • 数据列表:一般是 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<IListWebInfoICompany>>(flag,
    () => {
      // 页面信息
      const meta = reactive({
        title: '列表测试', // 模块标题
        deep: 0, // 递归层数
        moduleId: 100 // 模块ID
      })
      // 记录集合,reactive
      const dataList = ListICompany>([])
      // 查询条件
      const query = reactive({
        findValue: {}, // 查询条件的精简形式
        findArray: [], // 查询条件的对象形式
      })
      // 分页相关的信息
      const pagerInfo = reactive({
        pagerSize: 5,
        count: 20, // 总数
        pagerIndex: 1 // 当前页号
      })
      // 用户在列表里选择的的记录
      const selection = reactive({
        dataId: '',
        row: ModelICompany>(() => {
          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)
  

父组件的代码部分,主要负责加载需要的子组件,加载创建状态的函数,然后创建状态即可。

基于Vue3做一套适合自己的状态管理(八)列表页面需要的局部状态 & n级子模块

分页组件

做一个分页用的组件,获取父组件创建的状态,然后取出来分页信息(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"> &nbsp; 名称:</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。

如果属性名称对应不上的话,也是可以有提示的。

基于Vue3做一套适合自己的状态管理(八)列表页面需要的局部状态 & n级子模块

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,里面使用列表组件。

  • 打开多个子模块的效果:

基于Vue3做一套适合自己的状态管理(八)列表页面需要的局部状态 & n级子模块

小结

在父组件创建一个状态,加载各种子组件,子组件共享父组件共享的状态,各负其职。

这样各个组件通过共享的状态,就可以联动起来了。

这段代码反反复复改了好几回,对于泛型的应用方法有了更深入的了解,不敢保证是最优的,只是目前的能力基本只能优化到这个样子了。估计以后还可以继续优化。

源码

gitee.com/naturefw-co…

在线演示

naturefw-code.gitee.io/nf-rollup-s…

转载自:https://juejin.cn/post/7239267216804839479
评论
请登录