新增,编辑在企业表格中的最佳实践

背景介绍

日常开发工作中,尤其是中后台系统,表格功能有增、删、改、查是很正常的,今天我们暂且不说,可能在大部分场景下需要复用同一个弹窗 (Modal),所以我想和大家分享一下我在这边做的最佳实践,先看效果图

案例

目前市面上封装的基于 json 的或者是基于 template 的,都需要将行信息 (row)通过函数进行传递,主要为了区分是新增、编辑或者是其他状态,比如以下代码(截图自 vben-admin)

解释一下:

  1. 上图代码的 BasicTable 组件是基于 AntDesignVue Table 二次封装的表格组件
  2. handleEdit 是编辑按钮的回调函数,显示在表格的最后一列,这里将 record 进行了传递
  3. handleCreate 是新增按钮的回调函数,显示在表格的工具栏 (右上方),没有传递 record

我们去看一下 handleEdithandleCreate 做了什么

解释一下:

  1. handleEdit 函数调用了 openModal 方法打开弹窗,并且将行信息 (record) 进行了传递,为了等下可以在弹窗中 (Modal) 回显, 这里我们不研究内部是怎么传递的
  2. handleCreate 函数也调用了 openModal 方法打开弹窗,区别是没有传递行信息 (record),因为没有

我们在去看 AccountModal 组件 中针对这些信息做的事情

解释一下: 额外定义了 2个变量isUpdaterowId,本质是为了区分 新增、编辑

痛点

一句话概括:为了记录是新增还是编辑 额外定义了一些变量,需要手动维护这些变量

解决思路

既然我们知道了痛点是手动维护这些变量,那么我们可以实现一个自定义hook来解决手动维护的问题,这个 hook 可以自动跟踪当前表格的行信息,返回一个响应式的数据交给外层,伪代码如下

ts 复制代码
<script setup lang="ts">
    const proTableInst = ref<ProTableInst>()
    const { row } = useTrackTableRow(proTableInst)
</script>

<template>
    当前表格行信息:{{ row }}
    <ProTable ref="proTableInst" />
</template>

可以看到,我们的这个 hook 接收一个 表格实例 作为参数,返回的是 当前行信息,先看下最终效果

看完了效果,我们一起来实现一下这个 hook

实现前的思考

我们要想一下如何去实现这个功能,有以下几个问题

  1. 如何拿到行信息
  2. 如何在用户点击按钮触发点击回调前执行获取行信息逻辑
  3. 在一些场景中,如何扩展获取到的行信息,比如 复制编辑 都在同一行中,但是 复制本质上是新增,不需要携带 id 去请求,但是又需要回填,所以要扩展行信息

如何拿到行信息

一般表格组件都有行的唯一值,拿 AntDesignVue 举例,有一个 row-key 参数,代表表格每一行的唯一值,我们可以在封装 Table 时将这个 row-key 映射到 dom 属性上,如图

我们的 行信息 (row) 是存储在表格的数据源中的,拿 AntDesignVue 举例,就是 dataSource 属性,它是一个数组,里面就是我们所有的表格信息,我们可以通过一个计算属性来拿到 rowKey 对应 行信息的 Map, 如图

解释一下

  1. traverse 函数用来遍历的,它可以进行递归,因为有可能是树形表格
  2. getRowKey 函数是用来根据用户传递的 row-key 属性来获取唯一值的,表格组件是必须要传递这个属性的

最后我们将这个 rowKeyToRowMap 作为表格的实例方法,可以通过表格实例拿到,因为我们的 useTrackTableRow 接收表格实例作为参数,这样就可以根据 row-key 拿到行信息了,如图

如何在用户点击按钮触发点击回调前执行获取行信息逻辑

我们可以给封装的 Table 组件整体包裹一个 div,这样我们就可以通过 proTableInst.value.$el 拿到当前表格实例的 根节点元素,我们可以对这个 根节点元素 实现监听,比如监听 click 事件,这样用户不管是点击 新增,还是 编辑 都可以触发相应的事件回调,如图

以上代码还有一个问题,就是我们的 trackRowInfo 函数是在用户点击 新增编辑 按钮之后通过事件冒泡执行的,我们需要用户在点击新增编辑 按钮之前执行,也就是说在事件的捕获阶段触发,我们可以添加一个配置搞定,如图

这样我们就可以完成用户点击按钮触发点击回调前执行获取行信息逻辑

如何扩展行信息

我们在一些场景中,可能需要扩展一些额外的字段来满足我们的业务场景,因为现在获取行信息的逻辑是 hook 自动完成的,我们需要暴露给用户如何扩展行信息的方法,我们假设这个方法名叫 extend,假设我们现在有 新增复制 (copy)编辑 (edit) 的业务场景,用户使用的伪代码如下

ts 复制代码
<script setup lang="ts">
   const open = ref(false)
   const proTableInst = ref<ProTableInst>()
   const { row,extend } = useTrackTableRow(proTableInst)
   
   // 表单验证成功后触发的回调事件
   function onFinish(values){
      const rawRow = toValue(row)
      if (rawRow && rawRow.type === 'edit') {
        // 编辑请求
        return reloadUpdateUser({ ...values, id: rawRow.id })
      }
      // 新增 & 复制 请求
      return reloadAddUser(values)
   }
   
   function openModal(type: 'copy' | 'edit') {
      // 我们在这里将行信息进行扩展,增加一个 `type` 类型用来标识 `复制` 或者 `编辑`
      extend({ type })
      open.value = true
   }
</script>

<template>
    <ProTable>
        <template #extra>
           <KProButton type="primary" @click="open = true">新增</KProButton>
        </template>
        
       <template #bodyCell="{ column }">
          <template v-if="column.key === 'action'">
            <ASpace>
              <KProButton compact @click="openModal('copy')">复制</KProButton>
              <KProButton compact color="warn" @click="openModal('edit')">编辑</KProButton>
            </ASpace>
          </template>
        </template>
    </ProTable>
    
    <KProModalForm
        v-model:open="open"
        :title="`${row ? '编辑' : '新增'}用户`"
        :initial-values="row"
        @finish="onFinish"
    >
        <KProInput label="用户名" name="name" required />
        <KProPassword label="密码" name="password" required />
  </KProModalForm>
</template>

解释一下:

  1. KProModalForm 可以简单看成 Modal + Form 的结合
  2. KProInput 可以简单看成 FormItem + Input 的结合
  3. KProPassword 可以简单看成 FormItem + Password 的结合
  4. 在点击 复制 按钮时在行信息中扩展了一个 type: 'copy'
  5. 在点击 编辑 按钮时在行信息中扩展了一个 type: 'edit'
  6. 这样我们在触发 onFinish 时就可以拿到 row.type 字段,然后根据字段区分是 复制 还是 编辑,从而走不同的接口请求

好了,看完了以上的伪代码,我们大概也知道 extend 方法的内部实现了,其实就是做了一个合并,我们的问题都已经描述清楚了,接下来我们正式的去完成 useTrackTableRow 这个 hook

实现 useTrackTableRow

参数设计

ts 复制代码
import type { MaybeRef } from 'vue'
import type { KProTableInst } from '../inst'

const rowAttributeKey = 'custom-row-key'

interface UseTrackTableRowOptions {
  /**
   * @description 跟踪行信息的触发时机
   * @default 'click'
   */
  trigger?: 'click' | 'hover'
  /**
   * @description 跟踪行信息的拦截器,可以返回 false 阻止跟踪,在遇到一些特殊的场景时可能会有用
   */
  guard?: (ev: MouseEvent) => boolean | void
}

const triggerToEventNameMap: Record<string, string> = {
  click: 'click',
  hover: 'mousemove',
}

export function useTrackTableRow<Row extends Record<string, any>>(
  tableInstRef: MaybeRef<KProTableInst | undefined>,
  options: UseTrackTableRowOptions = {},
) {}

解释一下:

  1. 设计了 tableInstRef 参数, 为了用户方便,可以直接传递一个 ref 类型
  2. 设计了 trigger 配置,用来覆盖更多的场景,可以是点击,或者是鼠标移入
  3. 设计了 guard 配置,主要针对一些特殊化场景,比如弹窗不是挂载到 body,而是挂载到 表格 内部,这个时候我们点击弹窗的蒙版,可能也会触发到获取行信息的函数,从而导致行信息被清空的情况

hook 实现

上面已经介绍了每一步的实现思路及伪代码,所以我们这里将这些思路具象化,完整代码如下

ts 复制代码
import type { ComponentPublicInstance, ComputedRef, MaybeRef } from 'vue'
import type { KProTableInst } from '../inst'

const rowAttributeKey = 'custom-row-key'
export const proTableCls = 'k-pro-table'
/**
 * @description 跟踪表格行信息
 */
interface UseTrackTableRowOptions {
  /**
   * @description 跟踪行信息的触发时机
   * @default 'click'
   */
  trigger?: 'click' | 'hover'
  /**
   * @description 跟踪行信息的拦截器,可以返回 false 阻止跟踪,在遇到一些特殊的场景时可能会有用
   */
  guard?: (ev: MouseEvent) => boolean | void
}
export function useTrackTableRow<Row extends Record<string, any>>(
  tableInstRef: MaybeRef<KProTableInst | undefined>,
  options: UseTrackTableRowOptions = {},
) {
  const {
    guard,
    trigger = 'click',
  } = options

  const row = ref<Row | null>(null)
  const extendInfo = ref<Partial<Row>>({})

  function trackRowInfo(ev: MouseEvent) {
    const tableInst = toValue(tableInstRef)
    const shouldTrack = guard?.(ev) ?? true
    if (!shouldTrack || !tableInst)
      return

    const rowKey = findRowKeyByElement(ev.target)
    const rowInfo = tableInst.getRowKeyToRowMap().get(rowKey as string) ?? null
    ;(row as any).value = rowInfo
  }

  function getRowKey(target: Element) {
    return target.getAttribute(rowAttributeKey)
  }

  function hasRowKey(target: Element) {
    return !!getRowKey(target)
  }

  function isRootElement(target: Element) {
    return target.classList.contains(proTableCls)
  }

  function findRowKeyByElement(target: EventTarget | null) {
    const rowKey = null
    let current = target as Element
    while (current) {
      if (isRootElement(current))
        return null

      if (hasRowKey(current))
        return getRowKey(current)

      current = current.parentElement as Element
    }
    return rowKey
  }

  useEventListener(
    computed(() => {
      return (toValue(tableInstRef) as any as ComponentPublicInstance)?.$el
    }),
    triggerToEventNameMap[trigger],
    trackRowInfo,
    { capture: true },
  )

  function extend(data: Partial<Row>) {
    extendInfo.value = data as any
  }

  return {
    row: computed(() => {
      const rawRow = toRaw(toValue(row))
      if (!rawRow)
        return null
      const rawExtendInfo = toRaw(toValue(extendInfo)) as Partial<Row>
      return { ...rawRow, ...rawExtendInfo }
    }) as ComputedRef<Row | null>,
    extend,
  }
}
相关推荐
m0_7482552640 分钟前
前端安全——敏感信息泄露
前端·安全
鑫~阳2 小时前
html + css 淘宝网实战
前端·css·html
Catherinemin2 小时前
CSS|14 z-index
前端·css
漫天转悠2 小时前
Vue3项目中引入TailwindCSS(图文详情)
vue.js
qq_589568103 小时前
Echarts+vue电商平台数据可视化——后台实现笔记
vue.js·信息可视化·echarts
2401_882727574 小时前
低代码配置式组态软件-BY组态
前端·后端·物联网·低代码·前端框架
NoneCoder4 小时前
CSS系列(36)-- Containment详解
前端·css
anyup_前端梦工厂4 小时前
初始 ShellJS:一个 Node.js 命令行工具集合
前端·javascript·node.js
5hand4 小时前
Element-ui的使用教程 基于HBuilder X
前端·javascript·vue.js·elementui
GDAL4 小时前
vue3入门教程:ref能否完全替代reactive?
前端·javascript·vue.js