专业的编程技术博客社区

网站首页 > 博客文章 正文

Vue3表格 Hooks 的封装与使用(vue中hook)

baijin 2024-09-02 10:56:08 博客文章 6 ℃ 0 评论

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

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

useSelection 的封装

// 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 集成了与新增/修改表单弹窗的一些联动操作,这些联动操作不是必须的,也就是你可以只使用关于表格相关的功能。

// 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(新增/编辑保存之后)。

生命周期的类型如下

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

使用示例

// 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>

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表