vue3 + ts + element-plus 使用hooks封装表格CRUD方法

872 阅读5分钟

表格是日常开发中最常用的组件之一,传统的封装方式大都是通过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>