zoukankan      html  css  js  c++  java
  • vue3,后台管理列表页面各组件之间的状态关系

    技术栈

    • vite2
    • vue 3.0.5
    • vue-router 4.0.6
    • vue-data-state 0.1.1
    • element-plus 1.0.2-beta.39

    前情回顾

    前面介绍的表单控件和查询控件,都是原子性的,实现自己的功能即可。
    而这里要介绍的是管理后台里面的各个组件之间的状态关系。

    为啥需要状态?因为组件划分的非常原子化(细腻),所以造成了很多的组件,那么组件之间就需要一种“通讯方式”,这个就是状态了。不仅仅是传递数据,还可以实现事件总线。

    页面结构

    一般的后台管理大体是这样的结构:

    后台页面结构.png

    具体项目里页面结构会有一些变化,但是总体结构不会有太大的改变。

    做出来的效果大体是这样的:

    一种后台管理的效果

    • 动态菜单
      根据用户权限加载需要的菜单。

    • 动态 tab
      点击一下左面的菜单,创建一个新的tab,然后加载对应的组件,一般是列表页面(组件),也可以是其他页面(组件)。

    • 查询
      各种查询条件那是必备的,总不能没有查询功能吧,查询控件需要提供查询条件。

    • 操作按钮组
      里面可以有常见的添加、修改、删除、查看按钮,也可以有自定义的其他按钮。可以“弹窗”也可以直接调用后端API。

    • 列表
      显示客户需要的数据,看起来简单,但是要和查询、翻页、添加、修改、删除等功能配合。

    • 分页
      这是和列表最接近的一个需求,因为数据有可能很大,不能一次性都显示出来,那么就需要分页处理,所以分页控件和列表控件就是天然CP。

    • 表单(添加、修改)
      数据提交之后,为了便于确认数据添加成功,是不是需要通知列表去更新数据呢?总不能填完数据,列表一点变化都没有吧。

    • 删除
      数据删掉了,不管是物理删除还是逻辑删除,列表里面都不需要再显示出来了。
      也就是说删除后要通知列表更新数据。

    总之,各个组件直接需要统筹一下状态关系。

    视频演示

    我们来看一下实际效果。
    【放视频】

    设计状态

    我们整理一下需求,用脑图表达出来:

    后台管的状态.png

    使用“轻量级状态管理”定义状态:

    /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 等触发的需要跳过,以免影响正常的数据输入。
      然后是判断按了哪个按键,根据需求调用对应的函数。

     e 的样子

    • 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
    自然框架/nf-vite2-element

    在线演示

    https://naturefw.gitee.io/nf-vue-cdn/elecontrol/

    nf-vite2-element 的仓库没来得及开通pager服务,所以放在另一个仓库里面了。

  • 相关阅读:
    TCP协议的三次握手、四次挥手
    .NET Framework 3.5 安装
    grep命令总结
    线性回归
    K-Mean聚类算法
    Logistic回归
    朴素贝叶斯
    Decision Tree
    KNN
    GCC for Win32开发环境介绍
  • 原文地址:https://www.cnblogs.com/jyk/p/14981201.html
Copyright © 2011-2022 走看看