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>
本文暂时没有评论,来添加一个吧(●'◡'●)