Vue3表格 Hooks 的封装与使用

前言

Vue3 中的 Hooks 是函数的一种写法,主要用于将组件中的某些独立功能或逻辑进行抽离和封装,以便于重用。这种写法借鉴了 React 的设计理念,使得在组件中,状态逻辑和副作用的处理更加统一和可复用。 Hooks 的函数名/文件名以 use 开头,形如: useXX。

在后台管理系统的开发中,表格组件是一个非常基础且重要的组件。为了提高代码复用性和可维护性,我们将表格的一些常用功能(如分页、查询、新增、修改、删除等)的逻辑抽离出来,封装成 Hooks。

演示案例使用的 UI 组件库为 Naive UI

演示地址:用户管理 - Unusual Admin

示例代码中首行地址是源码(Unusual-Admin)中的文件路径

useSelection 的封装

ts 复制代码
// src/components/basic/useBasicList/utils/useSelection.ts 

import { type DataTableRowKey } from 'naive-ui/es/data-table'
import { type Form } from './type'
import { type UnwrapRef } from 'vue'
import { cloneDeep } from 'lodash-es'

export const useSelection = <Row extends Form = Form>() => {
  const checkedRowKeys = ref<DataTableRowKey[]>([])
  const checkedRow = ref<Row[]>([])
  const changeCheckRow = (rowKeys: DataTableRowKey[], row: object[]) => {
    checkedRowKeys.value = rowKeys
    checkedRow.value = cloneDeep(row) as UnwrapRef<Row[]>
  }
  return {
    checkedRowKeys,
    changeCheckRow,
    checkedRow
  }
}

useBasicList 的封装

这里对该 Hooks 做一些说明:该 Hooks 集成了与新增/修改表单弹窗的一些联动操作,这些联动操作不是必须的,也就是你可以只使用关于表格相关的功能。

ts 复制代码
// src/components/basic/useBasicList/index.ts

import { dialog, message } from '@/utils/help'
import { type FormInst } from 'naive-ui'
import { usePagination } from './utils/index'
import { getData } from './utils/index'
import type { HookParams, Form } from './utils/type'
import { type RowData } from 'naive-ui/es/data-table/src/interface'
import { type UnwrapRef } from 'vue'
import { useI18n } from 'vue-i18n'
import { cloneDeep } from 'lodash-es'
import { useSelection } from './utils/useSelection'

export const useBasicList = <List extends Form = Form, QueryParams extends Form = Form>({
  name, // 名称
  url, // 查询url
  key, // rowKey
  isPagination = true, // 是否需要分页
  isInitQuery = true, // 是否初始化查询
  initForm = {} as List, // 表单初始化数据
  initQuery = {} as QueryParams, // 查询初始化数据
  doCreate, // 新建
  doDelete, // 删除
  doUpdate, // 编辑
  beforeRefresh, // 查询之前
  afterRefresh, // 查询之后
  beforeSave, // 新增/编辑保存之前
  afterSave // 新增/编辑保存之后
}: HookParams<List, QueryParams>) => {

  // 国际化
  const { t } = useI18n()

  // 操作类型
  const ACTIONS = computed(() => {
    return {
      view: t('view'),
      edit: t('edit'),
      add: t('add')
    }
  })

  const modalVisible = ref(false)
  const modalAction = ref('')
  const modalLoading = ref(false)
  const modalFormRef = ref<FormInst | null>(null)
  const modalForm = ref<List>({ ...initForm })

  const defualtQuery = ref<QueryParams>({ ...initQuery })

  const modalTitle = computed(() => ACTIONS.value[modalAction.value as keyof typeof ACTIONS.value] + ' ' + (name || ''))
  const modalShowFooter = computed(() => modalAction.value !== 'view')

  /** 表格需要勾选的话需要设置rowkey */
  const rowKey = (row: RowData) => row[key]

  /** 重置搜索 */
  const handlereset = () => {
    defualtQuery.value = {...initQuery} as UnwrapRef<QueryParams>
  }

  /** 选择行变化 */
  const { changeCheckRow, checkedRowKeys, checkedRow } = useSelection<List>()

  /** 新增 */
  const handleAdd = () => {
    modalAction.value = 'add'
    modalVisible.value = true
    modalForm.value = { ...initForm } as UnwrapRef<List>
  }

  /** 修改 */
  const handleEdit = (row?: List) => {
    let rowData = cloneDeep(row)
    if (!row && checkedRow.value) rowData = checkedRow.value[0] as List
    modalAction.value = 'edit'
    modalVisible.value = true
    modalForm.value = rowData as UnwrapRef<List>
  }

  /** 查看 */
  const handleView = (row: List) => {
    modalAction.value = 'view'
    modalVisible.value = true
    modalForm.value = cloneDeep(row) as UnwrapRef<List>
  }

  /** 保存 */
  const handleSave = () => {
    if (!['edit', 'add'].includes(modalAction.value)) {
      modalVisible.value = false
      return
    }
    modalFormRef.value?.validate(async (err: any) => {
      if (err) return
      const action = modalAction.value === 'add' ? doCreate : doUpdate
      const prompt = modalAction.value === 'add' ? t('add') : t('edit')
      try {
        modalLoading.value = true
        // 保存之前,如果返回处理后的数据则替换
        const formData = beforeSave && beforeSave(modalForm.value as List)
        const params = formData || modalForm.value as List
        action && await action(params)
        // 保存之后
        afterSave && afterSave()
        action && message.success(prompt + ' ' + t('sucess'))
        modalLoading.value = modalVisible.value = false
        listQuery()
      } catch (error) {
        modalLoading.value = false
      }
    })
  }

  /** 删除 */
  const checkIds = computed(() => {
    return checkedRow.value?.map(item => {
      return item[key]
    })
  })
  const handleDelete = (ids?: number[]) => {
    if (ids && ids.length === 0 && checkIds.value && checkIds.value.length === 0) return
    let rowKeys = ids
    if (!ids) rowKeys = checkIds.value
    const dia = dialog.warning({
      title: t('warn'),
      content: t('dureDelete'),
      positiveText: t('determine'),
      negativeText: t('cancellation'),
      onPositiveClick: async () => {
        dia.loading = true
        try {
          doDelete && await doDelete(rowKeys as number[])
          dia.loading = false
          doDelete && message.success(t('delete') + ' ' + t('sucess'))
          listQuery()
        } catch (error) {
          dia.loading = false
        }
      },
      onNegativeClick: () => {
        console.log('取消')
      }
    })
  }

  // 查询
  const loading = ref(false)
  const listData = ref<List[]>()
  const listQuery = async () => {
    loading.value = true
    try {
      let params = {
        ...defualtQuery.value
      }
      if (isPagination) {
        params = {
          page: pagination?.page || 0,
          pageSize: pagination?.pageSize || 10,
          ...defualtQuery.value
        }
      }
      // 查询前,如果返回false则不继续查询
      const queryParams = beforeRefresh && beforeRefresh(params as QueryParams)
      if (typeof queryParams === 'boolean' && !queryParams) return
      if (queryParams && typeof queryParams !== 'boolean') params = queryParams as typeof params

      const { data, total } = await getData<List[]>(url, params)

      // 查询后,如果返回处理后的数据则替换列表数据,没有则使用接口返回的数据
      const newData = afterRefresh && afterRefresh([...data])
      if (newData) {
        listData.value = newData || []
      } else {
        listData.value = data || []
      }
      if (isPagination) pagination.itemCount = total as number || 0
      loading.value = false
    } catch(e) {
      loading.value = false
    }
  }

  // 分页
  const { pagination } = usePagination(listQuery)

  // 初始化查询
  isInitQuery && listQuery()

  /** 导出 */
  const handleDownload = () => {
    console.log('handleDownload')
  }

  // 操作按钮禁用
  const btnDisabled = computed(() => {
    return {
      edit: !(checkedRowKeys.value.length === 1),
      del: !(checkedRowKeys.value.length > 0),
      download: isPagination && pagination.itemCount <= 0
    }
  })

  return {
    modalVisible,
    modalAction,
    modalTitle,
    modalLoading,
    modalShowFooter,
    handlereset,
    handleAdd,
    handleDelete,
    handleEdit,
    handleView,
    handleDownload,
    handleSave,
    modalForm,
    modalFormRef,
    defualtQuery,
    changeCheckRow,
    loading,
    listData,
    pagination,
    listQuery,
    rowKey,
    btnDisabled
  }
}

文档

options参数

参数 类型 默认值 说明
name string undefined 列表名称
key string 'id' 表格数据rowKey
url string undefined 查询数据的url
isPagination boolean true 是否分页
isInitQuery boolean true 是否初始化查询
initForm object undefined 表单初始化数据
initQuery object undefined 查询初始化数据
doCreate (form: List) => Promise<ResultData<List[]>> undefined 新建
doDelete (id: number[]) => Promise undefined 删除
doUpdate (form: List) => Promise undefined 编辑

生命周期

提供了四个生命周期,分别是 beforeRefresh(查询之前), afterRefresh(查询之后), beforeSave(新增/编辑保存之前), afterSave(新增/编辑保存之后)。

生命周期的类型如下

ts 复制代码
beforeRefresh?: (form: QueryParams) => QueryParams | boolean
afterRefresh?: (listData: List[]) => List[] | undefined
beforeSave?: (listData: List) => List | undefined
afterSave?: () => void

使用示例

ts 复制代码
// src/views/system/user.vue 

<template>
  <div>
    <BasicLayout
      v-model:columns="columns"
      :btnDisabled="btnDisabled"
      @search="listQuery"
      @reset="handlereset"
      @add="handleAdd"
      @delete="handleDelete"
      @edit="handleEdit"
      @download="handleDownload"
    >
      <template #queryBar>
        <query-item label="用户名称">
          <n-input v-model:value="defualtQuery.userName" size="small" clearable placeholder="输入用户名称,模糊搜索" />
        </query-item>
        <query-item label="手机号">
          <n-input v-model:value="defualtQuery.phone" size="small" clearable placeholder="输入手机号,模糊搜索" />
        </query-item>
        <query-item label="用户状态">
          <n-select v-model:value="defualtQuery.status" placeholder="选择用户状态" :options="dict?.status" clearable />
        </query-item>
      </template>
      <n-data-table
        :columns="columns"
        :data="listData"
        :loading="loading"
        :row-key="rowKey"
        striped
        :remote="true"
        @update:checked-row-keys="changeCheckRow"
      />
    </BasicLayout>
    <BasicModel
      v-model:visible="modalVisible"
      :title="modalTitle"
      :loading="modalLoading"
      :show-footer="modalShowFooter"
      width="600px"
      @save="handleSave"
    >
      <n-form
        ref="modalFormRef"
        label-placement="left"
        label-align="right"
        :label-width="80"
        :model="modalForm"
        :rules="formRules"
        :disabled="modalAction === 'view'"
      >
        <n-grid x-gap="12" :cols="2">
          <n-gi>
            <n-form-item label="登录账号" path="userName">
              <n-input v-model:value="modalForm.userName" clearable />
            </n-form-item>
          </n-gi>
          <n-gi>
            <n-form-item label="电话" path="phone">
              <n-input v-model:value="modalForm.phone" clearable />
            </n-form-item>
          </n-gi>
          <n-gi>
            <n-form-item label="用户姓名" path="name">
              <n-input v-model:value="modalForm.name" clearable />
            </n-form-item>
          </n-gi>
          <n-gi>
            <n-form-item label="邮箱" path="email">
              <n-input v-model:value="modalForm.email" clearable />
            </n-form-item>
          </n-gi>
          <n-gi>
            <n-form-item label="性别" path="sex">
              <n-radio-group v-model:value="modalForm.sex" name="sex">
                <n-radio v-for="item in dict?.sex" :key="item.id" :value="Number(item.value)" :label="item.label"></n-radio>
              </n-radio-group>
            </n-form-item>
          </n-gi>
          <n-gi>
            <n-form-item label="状态" path="status">
              <n-radio-group v-model:value="modalForm.status" name="status">
                <n-radio v-for="item in dict?.status" :key="item.id" :value="Number(item.value)" :label="item.label"></n-radio>
              </n-radio-group>
            </n-form-item>
          </n-gi>
          <n-gi span="2">
            <n-form-item label="用户角色" path="roles">
              <n-select v-model:value="modalForm.roles" multiple label-field="roleName" value-field="id"  filterable clearable :options="roles" />
            </n-form-item>
          </n-gi>
        </n-grid>
      </n-form>
    </BasicModel>
  </div>
</template>

<script setup lang="ts" name="User">
import { type DataTableColumn } from 'naive-ui/es/data-table'
import TableAction from '@/components/basic/tableAction.vue'
import { useBasicList } from '@/components/basic/useBasicList/index'
import { type Query, type UserList, addUser, delUser, editUser } from '@/api/user/user'
import { getUserRole } from '@/api/user/userRole'
import { type RoleList } from '@/api/user/userRole'
import { type FormRules, NSwitch } from 'naive-ui/es/components'
import { useDict } from '@/hooks/useDict'
import { checkPassword, checkEmail, checkPhone } from '@/utils/calibrationRules';

// 获取角色
const roles = ref<RoleList[]>([])
getUserRole().then(res => {
  roles.value = res.data
})

// 获取dict
const { dict, getDictLabel } =  useDict(['status', 'sex'])

// 表格
const columns = ref<Array<DataTableColumn<UserList>>>([
  {
    type: 'selection',
    disabled: (row) => {
      return row.id === 1
    }
  },
  {
    title: 'ID',
    key: 'id'
  },
  {
    title: '登录账号',
    key: 'userName'
  },
  {
    title: '用户姓名',
    key: 'name'
  },
  {
    title: '性别',
    key: 'sex',
    render(row) {
      return h('span', getDictLabel('sex', String(row.sex)))
    }
  },
  {
    title: '电话',
    key: 'phone'
  },
  {
    title: '状态',
    key: 'status',
    render(row) {
      return h(
        NSwitch,
        {
          rubberBand: false,
          value: Number(row['status']),
          loading: !!row.loading,
          checkedValue: 1,
          uncheckedValue: 0,
          disabled: row.id === 1,
          onUpdateValue: () => handleChangeStatus(row)
        }
      )
    }
  },
  {
    title: '创建日期',
    key: 'createTime'
  },
  {
    title: '操作',
    key: 'actions',
    // width: 280,
    align: 'center',
    fixed: 'right',
    render(row) {
      return [
        h(
          TableAction,
          {
            disabled: row.id === 1,
            onHandleDelete: () => handleDelete([row.id as number]),
            onHandleEdit: () => handleEdit(row),
            onHandleView: () => handleView(row)
          },
        )
      ]
    }
  }
])

// 更改用户状态
const handleChangeStatus = async (row: UserList) => {
  row.loading = true
  const params: UserList = { ...row, status: row.status === 0 ? 1 : 0 }
  await editUser(params)
  await listQuery()
  row.loading = false
}

// 表单规则
const formRules: FormRules = {
  userName: [{required: true, message: '请输入用户名', trigger: 'blur'}],
  pwd: [
    {required: true, message: '请输入密码', trigger: 'blur'},
    {validator: checkPassword, message: '密码格式不正确', trigger: 'input' }
  ],
  email: [
    {required: true, message: '请输入邮箱', trigger: 'blur'},
    {validator: checkEmail, message: '请输入正确的邮箱', trigger: 'input' }
  ],
  phone: [
    {required: true, message: '请输入手机号', trigger: 'blur'},
    {validator: checkPhone, message: '请输入正确的手机号', trigger: 'input' }
  ],
}
// 表格hooks
const {
  modalVisible,
  modalAction,
  modalShowFooter,
  modalTitle,
  modalLoading,
  handleAdd,
  handleDelete,
  handleEdit,
  handleDownload,
  handleView,
  handleSave,
  handlereset,
  defualtQuery,
  modalForm,
  modalFormRef,
  changeCheckRow,
  listQuery,
  listData,
  loading,
  rowKey,
  btnDisabled
} = useBasicList<UserList, Query>({
  name: '用户',
  url: '/user',
  key: 'id',
  isPagination: false,
  initForm: { userName: '', name: '', phone: '', email: '', sex: 0, status: 1, roles: [] },
  initQuery: { userName: undefined, phone: undefined, status: undefined },
  // 搜索前
  beforeRefresh: (query) => {
    if (query && query.title) {
      query.pid = undefined
    }
    return query
  },
  doDelete: delUser,
  doCreate: addUser,
  doUpdate: editUser
})
</script>

<style scoped>
:deep(.selected-row > .n-data-table-td) {
  background-color: #e8f4ff !important;
}
</style>

源码地址和演示地址

演示地址Unusual Admin

源码Github 或者 Gitee

相关推荐
zwjapple3 小时前
docker-compose一键部署全栈项目。springboot后端,react前端
前端·spring boot·docker
像风一样自由20205 小时前
HTML与JavaScript:构建动态交互式Web页面的基石
前端·javascript·html
aiprtem6 小时前
基于Flutter的web登录设计
前端·flutter
浪裡遊6 小时前
React Hooks全面解析:从基础到高级的实用指南
开发语言·前端·javascript·react.js·node.js·ecmascript·php
why技术6 小时前
Stack Overflow,轰然倒下!
前端·人工智能·后端
幽络源小助理6 小时前
SpringBoot基于Mysql的商业辅助决策系统设计与实现
java·vue.js·spring boot·后端·mysql·spring
GISer_Jing6 小时前
0704-0706上海,又聚上了
前端·新浪微博
止观止7 小时前
深入探索 pnpm:高效磁盘利用与灵活的包管理解决方案
前端·pnpm·前端工程化·包管理器
whale fall7 小时前
npm install安装的node_modules是什么
前端·npm·node.js
烛阴7 小时前
简单入门Python装饰器
前端·python