表格是日常开发中最常用的组件之一,传统的封装方式大都是通过JSON化数据传入组件当中去封装配置,vue3提供了hooks写法能够大大减少代码量和提高复用性,以下是使用hooks封装的一个数据表格增删改查案例
约束CrudHooksOptions
export interface CrudHooksOptions {
// 是否在创建页面时,调用数据列表接口
createdIsNeed?: boolean
// 数据列表 Url
dataListUrl?: string
// 是否需要分页
isPage?: boolean
// 删除 Url
deleteUrl?: string
// 主键key,用于删除场景
primaryKey?: string
// 导出 Url
exportUrl?: string
// 查询条件
queryForm?: any
// 数据列表
dataList?: any[]
// 排序字段
order?: string
// 是否升序
asc?: boolean
// 当前页码
page?: number
// 每页数
limit?: number
// 总条数
total?: number
pageSizes?: any[]
// 数据列表,loading状态
dataListLoading?: boolean
// 数据列表,多选项
dataListSelections?: any[]
}
index.ts 创建 useCrud方法
import { CrudHooksOptions } from './interface'
import service from '../utils/requset'
import { onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import axios from 'axios'
import qs from 'qs'
export const useCrud = (options: CrudHooksOptions) => {
const defaultOptions: CrudHooksOptions = {
createdIsNeed: true,
dataListUrl: '',
isPage: true,
deleteUrl: '',
primaryKey: 'id',
exportUrl: '',
queryForm: {},
dataList: [],
order: '',
asc: false,
page: 1,
limit: 10,
total: 0,
pageSizes: [10, 20, 50, 100, 200],
dataListLoading: false,
dataListSelections: []
}
const mergeDefaultOptions = (options: any, props: any): CrudHooksOptions => {
for (const key in options) {
if (!Object.getOwnPropertyDescriptor(props, key)) {
props[key] = options[key]
}
}
return props
}
// 覆盖默认值
const state = mergeDefaultOptions(defaultOptions, options)
onMounted(() => {
if (state.createdIsNeed) {
query()
}
})
const query = () => {
if (!state.dataListUrl) {
return
}
state.dataListLoading = true
service
.get(state.dataListUrl, {
params: {
order: state.order,
asc: state.asc,
page: state.isPage ? state.page : null,
limit: state.isPage ? state.limit : null,
...state.queryForm
},
paramsSerializer: params => {
return qs.stringify(params)
}
})
.then((res: any) => {
state.dataList = state.isPage ? res.data.list : res.data
state.total = state.isPage ? res.data.total : 0
})
.finally(() => {
state.dataListLoading = false
})
}
const getDataList = () => {
state.page = 1
query()
}
const sizeChangeHandle = (val: number) => {
state.page = 1
state.limit = val
query()
}
const currentChangeHandle = (val: number) => {
state.page = val
query()
}
// 多选
const selectionChangeHandle = (selections: any[]) => {
state.dataListSelections = selections.map((item: any) => state.primaryKey && item[state.primaryKey])
}
// 排序
const sortChangeHandle = (data: any) => {
const { prop, order } = data
if (prop && order) {
state.order = prop
state.asc = order === 'ascending'
} else {
state.order = ''
}
query()
}
const deleteHandle = (key: number | string) => {
if (!state.deleteUrl) {
return
}
ElMessageBox.confirm('确定进行删除操作?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
service.delete(state.deleteUrl + '/' + key).then(() => {
ElMessage.success('删除成功')
query()
})
})
.catch(() => { })
}
const deleteBatchHandle = (key?: number | string) => {
let data: any[] = []
if (key) {
data = [key]
} else {
data = state.dataListSelections ? state.dataListSelections : []
if (data.length === 0) {
ElMessage.warning('请选择删除记录')
return
}
}
ElMessageBox.confirm('确定进行删除操作?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
if (state.deleteUrl) {
service.delete(state.deleteUrl, { data }).then(() => {
ElMessage.success('删除成功')
query()
})
}
})
.catch(() => { })
}
const downloadHandle = (url: string, filename?: string, method: string = 'GET'): Promise<any> => {
return axios({
responseType: 'blob',
url: url,
method: method
})
.then((res: any): any => {
// 创建a标签
const down = document.createElement('a')
// 文件名没传,则使用时间戳
down.download = filename || new Date().getTime().toString()
// 隐藏a标签
down.style.display = 'none'
// 创建下载url
let binaryData = []
binaryData.push(res.data)
down.href = URL.createObjectURL(new Blob(binaryData))
// 模拟点击下载
document.body.appendChild(down)
down.click()
// 释放URL
URL.revokeObjectURL(down.href)
// 下载完成移除
document.body.removeChild(down)
})
.catch(err => {
ElMessage.error(err.message)
})
}
return {
getDataList,
sizeChangeHandle,
currentChangeHandle,
selectionChangeHandle,
sortChangeHandle,
deleteHandle,
deleteBatchHandle,
downloadHandle
}
}
页面调用案例
<template>
<el-card>
<el-form :inline="true" :model="state.queryForm" @keyup.enter="getDataList()">
<el-form-item>
<el-input v-model="state.queryForm.username" placeholder="用户名" clearable></el-input>
</el-form-item>
<el-form-item>
<el-input v-model="state.queryForm.mobile" placeholder="手机号" clearable></el-input>
</el-form-item>
<el-form-item>
<el-button class="btn-radius" @click="getDataList()">查询</el-button>
</el-form-item>
<el-form-item>
<el-button class="btn-radius" @click="addOrUpdateHandle()">新增</el-button>
</el-form-item>
<el-form-item>
<el-button class="btn-radius" @click="deleteBatchHandle()">删除</el-button>
</el-form-item>
</el-form>
<el-table class="table" v-loading="state.dataListLoading" :data="state.dataList" border style="width: 100%"
@selection-change="selectionChangeHandle">
<el-table-column prop="username" label="用户名" header-align="center" align="center"></el-table-column>
<el-table-column prop="realName" label="姓名" header-align="center" align="center"></el-table-column>
<fast-table-column prop="gender" label="性别" dict-type="user_gender"></fast-table-column>
<el-table-column prop="mobile" label="手机号" show-overflow-tooltip header-align="center"
align="center"></el-table-column>
<el-table-column prop="email" label="邮箱" show-overflow-tooltip header-align="center"
align="center"></el-table-column>
<el-table-column prop="orgName" label="所属机构" show-overflow-tooltip header-align="center"
align="center"></el-table-column>
<fast-table-column prop="status" label="状态" dict-type="user_status"></fast-table-column>
<el-table-column prop="createTime" label="创建时间" show-overflow-tooltip header-align="center" align="center"
width="170"></el-table-column>
<el-table-column label="操作" fixed="right" header-align="center" align="center" width="150">
<template #default="scope">
<el-button type="primary" link @click="addOrUpdateHandle(scope.row.id)">修改</el-button>
<el-button type="primary" link @click="deleteBatchHandle(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination :current-page="state.page" :page-sizes="state.pageSizes" :page-size="state.limit" :total="state.total"
layout="total, sizes, prev, pager, next, jumper" @size-change="sizeChangeHandle"
@current-change="currentChangeHandle">
</el-pagination>
<!-- 弹窗, 新增 / 修改 -->
<add-or-update ref="addOrUpdateRef" @refresh-data-list="getDataList"></add-or-update>
</el-card>
</template>
<script setup lang="ts" name="SysUserIndex">
import { useCrud } from '../../../hooks'
import { reactive, ref } from 'vue'
import AddOrUpdate from './add-or-update.vue'
import { CrudHooksOptions } from '../../../hooks/interface'
import { useUserExportApi } from '../../../api/sys/user'
import { ElMessage, UploadProps } from 'element-plus'
const state: CrudHooksOptions = reactive({
dataListUrl: '请求地址',
deleteUrl: '请求地址',
queryForm: {
username: '',
mobile: '',
gender: ''
}
})
const addOrUpdateRef = ref()
const addOrUpdateHandle = (id?: number) => {
addOrUpdateRef.value.init(id)
}
const downloadExcel = () => {
useUserExportApi()
return
}
const handleSuccess: UploadProps['onSuccess'] = (res, file) => {
if (res.code !== 0) {
ElMessage.error('上传失败:' + res.msg)
return false
}
ElMessage.success({
message: '上传成功',
duration: 500,
onClose: () => {
getDataList()
}
})
}
const beforeUpload: UploadProps['beforeUpload'] = file => {
if (file.size / 1024 / 1024 / 1024 / 1024 > 1) {
ElMessage.error('文件大小不能超过100M')
return false
}
return true
}
const { getDataList, selectionChangeHandle, sizeChangeHandle, currentChangeHandle, deleteBatchHandle } = useCrud(state)
</script>
add-or-update页面
<template>
<el-dialog v-model="visible" :title="!dataForm.id ? '新增' : '修改'" :close-on-click-modal="false" draggable>
<el-form ref="dataFormRef" :model="dataForm" :rules="dataRules" label-width="120px" @keyup.enter="submitHandle()">
<el-form-item prop="username" label="用户名">
<el-input v-model="dataForm.username" placeholder="用户名"></el-input>
</el-form-item>
<el-form-item prop="realName" label="姓名">
<el-input v-model="dataForm.realName" placeholder="姓名"></el-input>
</el-form-item>
<el-form-item prop="orgId" label="所属机构">
<el-tree-select v-model="dataForm.orgId" :data="orgList" value-key="id" check-strictly
:render-after-expand="false" :props="{ label: 'name', children: 'children' }" style="width: 100%" />
</el-form-item>
<el-form-item prop="gender" label="性别">
<fast-radio-group v-model="dataForm.gender" dict-type="user_gender"></fast-radio-group>
</el-form-item>
<el-form-item prop="mobile" label="手机号">
<el-input v-model="dataForm.mobile" placeholder="手机号"></el-input>
</el-form-item>
<el-form-item prop="email" label="邮箱">
<el-input v-model="dataForm.email" placeholder="邮箱"></el-input>
</el-form-item>
<el-form-item prop="password" label="密码">
<el-input v-model="dataForm.password" type="password" placeholder="密码"></el-input>
</el-form-item>
<el-form-item prop="roleIdList" label="所属角色">
<el-select v-model="dataForm.roleIdList" multiple placeholder="所属角色" style="width: 100%">
<el-option v-for="role in roleList" :key="role.id" :label="role.name" :value="role.id"></el-option>
</el-select>
</el-form-item>
<el-form-item prop="postIdList" label="所属岗位">
<el-select v-model="dataForm.postIdList" multiple placeholder="所属岗位" style="width: 100%">
<el-option v-for="post in postList" :key="post.id" :label="post.postName" :value="post.id"></el-option>
</el-select>
</el-form-item>
<el-form-item prop="status" label="状态">
<fast-radio-group v-model="dataForm.status" dict-type="user_status"></fast-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="submitHandle()">确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { ElMessage } from 'element-plus/es'
import { useOrgListApi } from '../../../api/sys/orgs'
import { useUserApi, useUserSubmitApi } from '../../../api/sys/user'
import { usePostListApi } from '../../../api/sys/post'
import { useRoleListApi } from '../../../api/sys/role'
const emit = defineEmits(['refreshDataList'])
const visible = ref(false)
const postList = ref<any[]>([])
const roleList = ref<any[]>([])
const orgList = ref([])
const dataFormRef = ref()
const dataForm = reactive({
id: '',
username: '',
realName: '',
orgId: '',
orgName: '',
password: '',
gender: 0,
email: '',
mobile: '',
roleIdList: [] as any[],
postIdList: [] as any[],
status: 1
})
const init = (id?: number) => {
visible.value = true
dataForm.id = ''
// 重置表单数据
if (dataFormRef.value) {
dataFormRef.value.resetFields()
}
// id 存在则为修改
if (id) {
getUser(id)
}
getOrgList()
getPostList()
getRoleList()
}
// 获取岗位列表
const getPostList = () => {
return usePostListApi().then(res => {
postList.value = res.data
})
}
// 获取角色列表
const getRoleList = () => {
return useRoleListApi().then(res => {
roleList.value = res.data
})
}
// 获取机构列表
const getOrgList = () => {
return useOrgListApi().then(res => {
orgList.value = res.data
})
}
// 获取信息
const getUser = (id: number) => {
useUserApi(id).then(res => {
Object.assign(dataForm, res.data)
})
}
const dataRules = ref({
username: [{ required: true, message: '必填项不能为空', trigger: 'blur' }],
realName: [{ required: true, message: '必填项不能为空', trigger: 'blur' }],
mobile: [{ required: true, message: '必填项不能为空', trigger: 'blur' }],
orgId: [{ required: true, message: '必填项不能为空', trigger: 'blur' }]
})
// 表单提交
const submitHandle = () => {
dataFormRef.value.validate((valid: boolean) => {
if (!valid) {
return false
}
useUserSubmitApi(dataForm).then(() => {
ElMessage.success({
message: '操作成功',
duration: 500,
onClose: () => {
visible.value = false
emit('refreshDataList')
}
})
})
})
}
defineExpose({
init
})
</script>