Skip to content

CRUD 组件系统

概述

系统提供了一套完整的 CRUD(增删改查) 组件封装,基于 Vue 3 Composition API 设计,开箱即用。通过简单的配置即可实现数据表格的增删改查、分页、排序、导出等常用功能,大幅提升开发效率。

核心特性:

  • 🚀 开箱即用 - 一次配置,自动实现增删改查全流程
  • 🎯 组件化设计 - 头部操作、行操作、搜索等功能组件化
  • 📦 功能完整 - 支持分页、排序、批量删除、数据导出等
  • 🔄 响应式 - 基于 Vue 3 响应式系统,数据自动同步
  • 🎨 灵活扩展 - 提供丰富的插槽,支持自定义功能扩展
  • 性能优化 - 内置防抖、加载状态、错误处理等优化

核心 API

useCrud - CRUD 组合式 API

配置文件: src/components/CRUD/useCrud.js

这是 CRUD 系统的核心 Hook,提供了完整的增删改查逻辑封装。

参数说明:

参数类型必填默认值说明
crudMethodObject-CRUD 方法对象,包含 list、add、edit、del、download 方法
defaultFormFunction-返回默认表单数据的函数
searchInfoObject-搜索条件响应式对象
pageSizeOptionsArray[1, 10, 20, 30, 50, 100]分页大小选项
sortPropString'id'默认排序字段
sortOrderString'descending'默认排序方式
refreshOnInitBooleantrue是否在初始化时自动刷新数据

返回值说明:

属性/方法类型说明
dataRef表格数据数组
loadingRef加载状态
totalRef数据总数
dialogVisibleRef对话框显示状态
isEditRef是否为编辑模式
formRef表单数据
paginationComputedRef分页配置对象
refresh()Function刷新数据列表
toAdd()Function打开新增对话框
toEdit(row)Function打开编辑对话框
save()Function保存数据
doDelete(row)Function删除单条数据
doBatchDelete()Function批量删除数据
doExport()Function导出数据
onSelectionChange()Function表格选择变化回调
onSortChange()Function排序变化回调
resetQuery()Function重置查询条件

完整代码:

javascript
import {
  reactive,
  ref,
  toRefs,
  watch,
  provide,
  computed,
  getCurrentInstance,
} from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { downloadFile } from "@/utils/downloadFile";

/**
 * CRUD操作的Vue3组合式API Hook
 * @param {Object} options - 配置选项
 * @param {Object} options.crudMethod - CRUD方法对象,包含list、add、edit、del、download方法
 * @param {Function} options.defaultForm - 默认表单数据生成函数
 * @param {Object} options.searchInfo - 搜索条件对象
 * @param {Array<number>} options.pageSizeOptions - 分页大小选项数组
 * @param {string} options.sortProp - 默认排序字段
 * @param {string} options.sortOrder - 默认排序方式
 * @param {boolean} options.refreshOnInit - 是否在初始化时刷新数据
 * @returns {Object} CRUD实例对象
 */
export function useCrud({
  crudMethod,
  defaultForm,
  searchInfo,
  pageSizeOptions = [1, 10, 20, 30, 50, 100],
  sortProp = "id",
  sortOrder = "descending",
  refreshOnInit = true,
}) {
  // 主数据与状态
  const crudState = reactive({
    data: [],
    form: defaultForm(),
    formTitle: "",
    pageIndex: 1,
    pageSize: pageSizeOptions[1],
    total: 0,
    loading: false,
    dialogVisible: false,
    isEdit: false,
    multipleSelection: [],
    sortProp: sortProp,
    sortOrder: sortOrder,
    searchToggle: true,
  });

  const query = searchInfo || ref({});

  /**
   * 获取查询参数
   * @param {boolean} isExport - 是否为导出查询
   * @returns {Object} 查询参数对象
   */
  function getQueryParams(isExport = false) {
    if (isExport) {
      return {
        ...query.value,
      };
    }
    let sort = {};
    if (crudState.sortProp && crudState.sortOrder) {
      sort = {
        sortField: crudState.sortProp,
        orderByType: crudState.sortOrder === "ascending" ? 0 : 1,
      };
    }
    return {
      pageIndex: crudState.pageIndex,
      pageSize: crudState.pageSize,
      ...query.value,
      ...sort,
    };
  }

  /**
   * 刷新数据列表
   * @returns {Promise<void>} 无返回值的Promise
   */
  const refresh = async () => {
    crudState.loading = true;
    try {
      const params = getQueryParams();
      const res = await crudMethod.list(params);
      crudState.data = res.data.content || res.data || [];
      crudState.total = res.data.totalElements || res.total || 0;
    } finally {
      crudState.loading = false;
    }
  };

  /**
   * 打开新增对话框
   * @param {Object} extra - 额外的表单数据
   * @returns {void}
   */
  const toAdd = (extra = {}) => {
    crudState.form = { ...defaultForm(), ...extra };
    crudState.formTitle = "新增";
    crudState.isEdit = false;
    crudState.dialogVisible = true;
  };

  /**
   * 打开编辑对话框
   * @param {Object} row - 要编辑的行数据
   * @returns {void}
   */
  const toEdit = (row) => {
    crudState.form = { ...row };
    crudState.formTitle = "编辑";
    crudState.isEdit = true;
    crudState.dialogVisible = true;
  };

  /**
   * 重置表单数据
   * @param {Object} data - 重置的数据,如果不传则使用默认表单数据
   * @returns {void}
   */
  const resetForm = (data) => {
    crudState.form = data ? { ...data } : defaultForm();
  };

  /**
   * 关闭对话框
   * @returns {void}
   */
  const closeDialog = () => {
    crudState.isEdit = false;
    crudState.dialogVisible = false;
    resetForm();
  };

  /**
   * 获取表单标题
   * @returns {string} 表单标题
   */
  const getFormTitle = () => {
    return `${crudState.isEdit ? "编辑" : "新增"}`;
  };

  /**
   * 保存数据(新增或编辑)
   * @param {Object} formData - 表单数据
   * @returns {Promise<void>} 无返回值的Promise
   */
  const save = async (formData) => {
    const submitData = formData || crudState.form;
    crudState.loading = true;
    try {
      if (crudState.isEdit) {
        await crudMethod.edit(submitData);
        ElMessage({
          type: "success",
          message: "编辑成功",
          showClose: true,
        });
      } else {
        const res = await crudMethod.add(submitData);
        ElMessage({
          type: "success",
          message: res.data.message || "新增成功",
          showClose: true,
        });
      }
      crudState.dialogVisible = false;
      crudState.isEdit = false;
      await refresh();
    } finally {
      crudState.loading = false;
    }
  };

  /**
   * 验证表单并保存
   * @param {Object} formRef - 表单引用
   * @param {Object} customData - 自定义数据
   * @returns {Promise<void>} 无返回值的Promise
   */
  const validateAndSave = async (formRef, customData) => {
    if (!formRef) {
      console.warn("表单引用不存在");
      return;
    }
    try {
      const valid = await formRef.validate();
      if (valid) {
        await save(customData);
      }
    } catch (error) {
      console.log("表单验证失败", error);
    }
  };

  /**
   * 删除单条记录
   * @param {Object} row - 要删除的行数据
   * @returns {Promise<void>} 无返回值的Promise
   */
  const doDelete = async (row) => {
    crudState.loading = true;
    try {
      const res = await crudMethod.del({ IdArray: [row.id] });
      ElMessage({
        type: "success",
        message: res.data.message,
      });
      await refresh();
    } finally {
      crudState.loading = false;
    }
  };

  /**
   * 批量删除记录
   * @returns {Promise<void>} 无返回值的Promise
   */
  const doBatchDelete = async () => {
    if (!crudState.multipleSelection.length) {
      ElMessage.warning("请先选择要删除的数据");
      return;
    }
    crudState.loading = true;
    try {
      ElMessageBox.confirm(
        `确认删除选中的${crudState.multipleSelection.length}条数据?`,
        "警告",
        {
          confirmButtonText: "确定",
          cancelButtonText: "取消",
          type: "warning",
        }
      ).then(async () => {
        const res = await crudMethod.del({
          IdArray: crudState.multipleSelection.map((row) => row.id),
        });
        ElMessage({
          type: "success",
          message: res.data.message,
        });
        await refresh();
      });
    } finally {
      crudState.loading = false;
    }
  };

  /**
   * 导出数据
   * @returns {Promise<void>} 无返回值的Promise
   */
  const doExport = async () => {
    crudState.loading = true;
    try {
      const res = await crudMethod.download(getQueryParams(true));
      // 1. 获取文件名
      let fileName = "download.xlsx";
      const disposition = res.headers["content-disposition"];
      if (disposition) {
        // 优先 filename*= ,其次 filename=
        let match = disposition.match(/filename\*=(?:UTF-8'')?([^;]+)/i);
        if (match) {
          // 解码 RFC 5987
          fileName = decodeURIComponent(match[1].replace(/['"]/g, ""));
        } else {
          match = disposition.match(/filename="?([^"]+)"?/);
          if (match) {
            fileName = decodeURIComponent(escape(match[1])); // 兼容中文
          }
        }
      }

      downloadFile(res.data, fileName);
    } finally {
      crudState.loading = false;
    }
  };

  /**
   * 分页页码改变事件
   * @param {number} page - 新的页码
   * @returns {Promise<void>} 无返回值的Promise
   */
  const onPageChange = async (page) => {
    crudState.pageIndex = page;
    await refresh();
  };
  /**
   * 分页大小改变事件
   * @param {number} size - 新的分页大小
   * @returns {Promise<void>} 无返回值的Promise
   */
  const onSizeChange = async (size) => {
    crudState.pageSize = size;
    crudState.pageIndex = 1;
    await refresh();
  };

  /**
   * 表格选择改变事件
   * @param {Array} val - 选中的行数据数组
   * @returns {void}
   */
  const onSelectionChange = (val) => {
    crudState.multipleSelection = val;
  };

  /**
   * 重置查询条件
   * @returns {Promise<void>} 无返回值的Promise
   */
  const resetQuery = async () => {
    if (searchInfo) {
      Object.keys(searchInfo.value).forEach((key) => {
        searchInfo.value[key] = null;
      });
    } else {
      Object.keys(query.value).forEach((key) => {
        query.value[key] = null;
      });
    }
    crudState.sortProp = "";
    crudState.sortOrder = "";
    crudState.pageIndex = 1;
    await refresh();
  };

  /**
   * 表格排序改变事件
   * @param {Object} sortInfo - 排序信息
   * @param {string} sortInfo.prop - 排序字段
   * @param {string} sortInfo.order - 排序方式
   * @returns {Promise<void>} 无返回值的Promise
   */
  const onSortChange = async ({ prop, order }) => {
    crudState.sortProp = prop;
    crudState.sortOrder = order;
    crudState.pageIndex = 1;
    await refresh();
  };

  let timer = null;
  if (searchInfo) {
    watch(
      searchInfo,
      () => {
        clearTimeout(timer);
        timer = setTimeout(async () => {
          crudState.pageIndex = 1;
          await refresh();
        }, 500); // 防抖处理
      },
      { deep: true }
    );
  }

  const instance = getCurrentInstance();
  /**
   * 查找指定名称的Vue组件实例
   * @param {string} name - 组件名称
   * @returns {Object|null} 组件实例或null
   */
  const findVM = (name) => {
    debugger;
    if (!instance || !instance.parent) {
      return null;
    }

    let parent = instance.parent;
    while (parent) {
      try {
        // 添加更严格的空值检查
        if (
          parent &&
          parent.type &&
          (parent.type.name === name ||
            (parent.props && parent.props.name === name) ||
            (parent.exposed && parent.exposed.name === name))
        ) {
          return parent;
        }
      } catch (error) {
        console.warn("查找组件时出错:", error);
      }

      // 安全地获取下一个父组件
      try {
        parent = parent.parent;
      } catch (error) {
        break;
      }
    }
    return null;
  };

  /**
   * 获取表格组件实例
   * @returns {Object|null} 表格组件实例或null
   */
  const getTable = () => {
    debugger;
    try {
      const presenterVM = findVM("presenter");
      if (presenterVM) {
        // 尝试多种方式获取表格引用
        if (presenterVM.refs && presenterVM.refs.table) {
          return presenterVM.refs.table;
        }

        // if (presenterVM.exposed && presenterVM.exposed.tableRef) {
        //   return presenterVM.exposed.tableRef
        // }

        if (presenterVM.exposed && presenterVM.exposed.table) {
          return presenterVM.exposed.table;
        }
      }

      // 备用方案:直接查找DOM
      const tableEl = document.querySelector(".el-table");
      if (tableEl && tableEl.__vueParentComponent) {
        return tableEl.__vueParentComponent.proxy;
      }
    } catch (error) {
      console.warn("获取表格失败:", error);
    }

    return null;
  };

  /**
   * 分页配置对象
   */
  const pagination = computed(() => ({
    "current-page": crudState.pageIndex,
    "page-size": crudState.pageSize,
    total: crudState.total,
    pageSizes: pageSizeOptions,
    layout: "total, sizes, prev, pager, next, jumper",
    onCurrentChange: onPageChange,
    onSizeChange: onSizeChange,
  }));

  const crudInstance = {
    ...toRefs(crudState),
    query,
    pagination,
    refresh,
    toAdd,
    toEdit,
    save,
    doDelete,
    doBatchDelete,
    doExport,
    onPageChange,
    onSizeChange,
    onSelectionChange,
    resetQuery,
    onSortChange,
    resetForm,
    closeDialog,
    getFormTitle,
    validateAndSave,
    getTable,
  };

  provide("crud", crudInstance);
  if (refreshOnInit) {
    void refresh();
  }
  return crudInstance;
}

组件详解

CrudOpts - 头部操作按钮组件

头部操作组件提供了新增、批量删除、导出、搜索切换、刷新等常用功能按钮。

配置文件: src/components/CRUD/CrudOpts.vue

Props 参数:

参数类型必填说明
permsObject权限配置对象,包含 add、del、download 等权限标识

插槽说明:

插槽名说明
left左侧按钮组前置插槽,可添加自定义按钮
right左侧按钮组后置插槽,可添加自定义按钮

组件代码:

vue
<template>
  <div class="ape-volo-btn-list">
    <div class="crud-opts">
      <!-- 左侧按钮组 -->
      <span class="crud-opts-left">
        <slot name="left" />
        <!-- 新增和删除按钮 -->
        <el-button
          v-has-perm="props.perms.add"
          type="primary"
          icon="plus"
          @click="crud.toAdd"
        >
          {{ crud.getFormTitle() }}
        </el-button>
        <el-button
          v-has-perm="props.perms.del"
          type="danger"
          icon="delete"
          :disabled="!crud.multipleSelection.value.length"
          @click="crud.doBatchDelete"
          >删除
        </el-button>
        <el-button
          v-has-perm="props.perms.download"
          type="warning"
          icon="download"
          @click="crud.doExport"
          >导出
        </el-button>
        <slot name="right" />
      </span>

      <!-- 右侧按钮组 -->
      <div class="crud-opts-right">
        <el-button-group>
          <!-- 显示隐藏查询模块 -->
          <el-tooltip
            :content="crud.searchToggle.value ? '隐藏搜索' : '显示搜索'"
            placement="top"
          >
            <el-button
              :type="crud.searchToggle.value ? 'primary' : ''"
              circle
              @click="toggleSearch"
              class="search-btn"
            >
              <el-icon>
                <Search />
              </el-icon>
            </el-button>
          </el-tooltip>
          <!-- 刷新按钮 -->
          <el-tooltip content="刷新" placement="top">
            <el-button circle @click="crud.refresh">
              <el-icon>
                <Refresh />
              </el-icon>
            </el-button>
          </el-tooltip>
        </el-button-group>
      </div>
    </div>
  </div>
</template>

使用示例:

vue
<template>
  <div class="ape-volo-table">
    <!-- 头部操作按钮组 -->
    <CrudOpts :perms="perms">
      <!-- 自定义左侧按钮 -->
      <template #left>
        <el-button type="success" icon="Upload"> 导入 </el-button>
      </template>

      <!-- 自定义右侧按钮 -->
      <template #right>
        <el-button type="info" icon="Setting"> 高级设置 </el-button>
      </template>
    </CrudOpts>

    <!-- 数据表格 -->
    <el-table
      ref="tableRef"
      :data="data"
      v-loading="loading"
      @selection-change="onSelectionChange"
      @sort-change="onSortChange"
      style="width: 100%"
      row-key="id"
    >
      <!-- 表格列配置... -->
    </el-table>
  </div>
</template>

<script setup>
import CrudOpts from "@/components/CRUD/CrudOpts.vue";
import { useCrud } from "@/components/CRUD/useCrud";

// 权限配置
const perms = {
  add: "sys:menu:add",
  edit: "sys:menu:edit",
  del: "sys:menu:del",
  download: "sys:menu:download",
};

// 初始化 CRUD
const { data, loading, onSelectionChange, onSortChange } = useCrud({
  // CRUD 配置...
});
</script>

RowOpts - 行操作按钮组件

行操作组件提供了编辑、删除等行级操作功能,支持权限控制和状态禁用。

配置文件: src/components/CRUD/RowOpts.vue

Props 参数:

参数类型必填默认值说明
permsObject{}权限配置对象
rowObject-当前行数据
disabledEditBooleanfalse是否禁用编辑按钮
disabledDleBooleanfalse是否禁用删除按钮
valString''删除确认提示中显示的值

插槽说明:

插槽名说明
left操作按钮前置插槽
right操作按钮后置插槽

组件代码:

vue
<template>
  <div>
    <slot name="left" />
    <el-button
      v-has-perm="props.perms.edit"
      :disabled="disabledEdit"
      icon="edit"
      type="primary"
      link
      @click="crud.toEdit(props.row)"
    >
      编辑
    </el-button>
    <el-popconfirm
      v-model="pop"
      placement="top"
      :title="getTitle()"
      width="200"
      confirm-button-text="确定"
      cancel-button-text="取消"
      @confirm="doDelete(props.row)"
      @cancel="doCancel"
    >
      <template #reference>
        <el-button
          v-has-perm="props.perms.del"
          :disabled="disabledDle"
          icon="delete"
          type="danger"
          link
          @click="toDelete"
        >
          删除
        </el-button>
      </template>
    </el-popconfirm>
    <slot name="right" />
  </div>
</template>

<script setup>
import { inject, ref } from "vue";

const pop = ref(false);

// 注入crud
const crud = inject("crud");
const { doDelete } = crud;

const props = defineProps({
  perms: {
    type: Object,
    default: () => {
      return {};
    },
  },
  row: {
    type: Object,
    required: true,
  },
  disabledEdit: {
    type: Boolean,
    default: false,
  },
  disabledDle: {
    type: Boolean,
    default: false,
  },
  val: {
    type: String,
    default: "",
  },
});

const getTitle = () => {
  return props.val ? `确定删除${props.val}吗?` : "确定删除本条数据吗?";
};

function doCancel() {
  pop.value = false;
}

function toDelete() {
  pop.value = true;
}
</script>

使用示例:

vue
<template>
  <el-table :data="data">
    <!-- 其他列配置... -->

    <!-- 操作列 -->
    <el-table-column
      :min-width="appStore.operateMinWith"
      label="操作"
      fixed="right"
    >
      <template #default="{ row }">
        <RowOpts
          :row="row"
          :val="row.name"
          :perms="perms"
          :disabled-edit="row.status === 0"
          :disabled-dle="row.isSystem"
        >
          <!-- 自定义左侧操作按钮 -->
          <template #left>
            <el-button
              v-has-perm="'sys:menu:view'"
              type="info"
              link
              icon="View"
              @click="handleView(row)"
            >
              查看
            </el-button>
          </template>

          <!-- 自定义右侧操作按钮 -->
          <template #right>
            <el-button
              v-has-perm="'sys:menu:reset'"
              type="warning"
              link
              icon="Refresh"
              @click="handleReset(row)"
            >
              重置
            </el-button>
          </template>
        </RowOpts>
      </template>
    </el-table-column>
  </el-table>
</template>

<script setup>
import RowOpts from "@/components/CRUD/RowOpts.vue";
import { useAppStore } from "@/pinia/modules/app";

const appStore = useAppStore();

const perms = {
  edit: "sys:menu:edit",
  del: "sys:menu:del",
};

const handleView = (row) => {
  console.log("查看", row);
};

const handleReset = (row) => {
  console.log("重置", row);
};
</script>

SearchOpts - 搜索操作组件

搜索操作组件提供了查询和重置功能按钮。

配置文件: src/components/CRUD/SearchOpts.vue

组件功能:

  • 提供查询按钮,触发 crud.refresh() 方法
  • 提供重置按钮,触发 crud.resetQuery() 方法
  • 支持自定义插槽扩展

使用示例:

vue
<template>
  <div class="search-container" v-show="searchToggle">
    <!-- 搜索表单 -->
    <el-form :model="query" inline>
      <el-form-item label="菜单名称">
        <el-input v-model="query.name" placeholder="请输入菜单名称" clearable />
      </el-form-item>

      <el-form-item label="状态">
        <el-select v-model="query.status" placeholder="请选择状态" clearable>
          <el-option label="启用" :value="1" />
          <el-option label="禁用" :value="0" />
        </el-select>
      </el-form-item>

      <!-- 搜索操作按钮 -->
      <el-form-item>
        <SearchOpts />
      </el-form-item>
    </el-form>
  </div>
</template>

<script setup>
import SearchOpts from "@/components/CRUD/SearchOpts.vue";
import { reactive, ref } from "vue";

const searchToggle = ref(true);
const query = reactive({
  name: "",
  status: null,
});
</script>

完整使用示例

步骤 1: 初始化 CRUD

在 Vue 组件中引入并初始化 useCrud,配置 CRUD 方法和默认表单数据。

vue
<script setup>
import { reactive } from "vue";
import { useCrud } from "@/components/CRUD/useCrud";
import { get, add, edit, del, download } from "@/api/system/menu";

// 搜索条件
const searchInfo = reactive({
  name: "",
  status: null,
  createTime: [],
});

// 初始化 CRUD
const {
  data, // 表格数据
  searchToggle, // 搜索框显示状态
  loading, // 加载状态
  dialogVisible, // 对话框显示状态
  form, // 表单数据
  isEdit, // 是否编辑模式
  pagination, // 分页配置
  onSelectionChange, // 选择变化回调
  onSortChange, // 排序变化回调
  toAdd, // 打开新增对话框
  toEdit, // 打开编辑对话框
  save, // 保存数据
  doDelete, // 删除单条数据
  refresh, // 刷新列表
  resetQuery, // 重置查询
} = useCrud({
  // CRUD 方法配置
  crudMethod: {
    list: get, // 查询列表
    add: add, // 新增
    edit: edit, // 编辑
    del: del, // 删除
    download: download, // 导出
  },

  // 默认表单数据
  defaultForm: () => ({
    name: "",
    path: "",
    component: "",
    sort: 999,
    enabled: true,
    icon: "",
    parentId: null,
  }),

  // 搜索条件
  searchInfo,

  // 分页配置
  pageSizeOptions: [10, 20, 50, 100],

  // 默认排序
  sortProp: "sort",
  sortOrder: "ascending",

  // 是否初始化时刷新
  refreshOnInit: true,
});
</script>

核心功能

完成上述配置后,系统自动提供以下功能:

  • ✅ 列表数据加载与刷新
  • ✅ 分页与每页条数切换
  • ✅ 排序功能
  • ✅ 新增、编辑、删除操作
  • ✅ 批量删除
  • ✅ 数据导出
  • ✅ 搜索条件自动监听与刷新

步骤 2: 构建完整页面

将 CRUD 组件组合成完整的页面结构。

vue
<template>
  <div class="app-container">
    <!-- 搜索区域 -->
    <div class="search-wrapper" v-show="searchToggle">
      <el-form :model="searchInfo" inline>
        <el-form-item label="菜单名称">
          <el-input
            v-model="searchInfo.name"
            placeholder="请输入菜单名称"
            clearable
          />
        </el-form-item>

        <el-form-item label="状态">
          <el-select v-model="searchInfo.status" placeholder="全部" clearable>
            <el-option label="启用" :value="1" />
            <el-option label="禁用" :value="0" />
          </el-select>
        </el-form-item>

        <el-form-item>
          <SearchOpts />
        </el-form-item>
      </el-form>
    </div>

    <!-- 表格区域 -->
    <div class="ape-volo-table">
      <!-- 头部操作按钮 -->
      <CrudOpts :perms="perms" />

      <!-- 数据表格 -->
      <el-table
        ref="tableRef"
        :data="data"
        v-loading="loading"
        @selection-change="onSelectionChange"
        @sort-change="onSortChange"
        row-key="id"
        border
      >
        <!-- 多选列 -->
        <el-table-column type="selection" width="55" />

        <!-- 数据列 -->
        <el-table-column prop="name" label="菜单名称" sortable="custom" />
        <el-table-column prop="path" label="路径" />
        <el-table-column prop="component" label="组件" />
        <el-table-column
          prop="sort"
          label="排序"
          sortable="custom"
          width="100"
        />

        <!-- 状态列 -->
        <el-table-column label="状态" width="100">
          <template #default="{ row }">
            <el-tag :type="row.enabled ? 'success' : 'danger'">
              {{ row.enabled ? "启用" : "禁用" }}
            </el-tag>
          </template>
        </el-table-column>

        <!-- 操作列 -->
        <el-table-column label="操作" width="200" fixed="right">
          <template #default="{ row }">
            <RowOpts :row="row" :val="row.name" :perms="perms" />
          </template>
        </el-table-column>
      </el-table>

      <!-- 分页组件 -->
      <el-pagination v-bind="pagination" />
    </div>

    <!-- 编辑对话框 -->
    <el-dialog
      v-model="dialogVisible"
      :title="isEdit ? '编辑菜单' : '新增菜单'"
      width="600px"
    >
      <el-form :model="form" label-width="100px">
        <el-form-item label="菜单名称">
          <el-input v-model="form.name" placeholder="请输入菜单名称" />
        </el-form-item>

        <el-form-item label="路径">
          <el-input v-model="form.path" placeholder="请输入路径" />
        </el-form-item>

        <el-form-item label="组件">
          <el-input v-model="form.component" placeholder="请输入组件路径" />
        </el-form-item>

        <el-form-item label="排序">
          <el-input-number v-model="form.sort" :min="0" />
        </el-form-item>

        <el-form-item label="状态">
          <el-switch v-model="form.enabled" />
        </el-form-item>
      </el-form>

      <template #footer>
        <el-button @click="dialogVisible = false">取消</el-button>
        <el-button type="primary" @click="save" :loading="loading">
          确定
        </el-button>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
import { reactive } from "vue";
import { useCrud } from "@/components/CRUD/useCrud";
import CrudOpts from "@/components/CRUD/CrudOpts.vue";
import RowOpts from "@/components/CRUD/RowOpts.vue";
import SearchOpts from "@/components/CRUD/SearchOpts.vue";
import { get, add, edit, del, download } from "@/api/system/menu";

// 权限配置
const perms = {
  add: "sys:menu:add",
  edit: "sys:menu:edit",
  del: "sys:menu:del",
  download: "sys:menu:download",
};

// 搜索条件
const searchInfo = reactive({
  name: "",
  status: null,
});

// 初始化 CRUD(完整配置见步骤 1)
const {
  data,
  searchToggle,
  loading,
  dialogVisible,
  form,
  isEdit,
  pagination,
  onSelectionChange,
  onSortChange,
  save,
} = useCrud({
  crudMethod: { list: get, add, edit, del, download },
  defaultForm: () => ({
    name: "",
    path: "",
    component: "",
    sort: 999,
    enabled: true,
  }),
  searchInfo,
});
</script>

效果展示

完成以上配置后,即可实现完整的 CRUD 功能页面:

CRUD 页面效果

功能清单:

  • ✅ 搜索表单与条件筛选
  • ✅ 新增、批量删除、导出按钮
  • ✅ 搜索显示/隐藏切换
  • ✅ 数据列表刷新
  • ✅ 表格多选与排序
  • ✅ 行级编辑与删除操作
  • ✅ 分页与每页条数切换
  • ✅ 新增/编辑对话框

高级用法

自定义表单验证

vue
<script setup>
import { ref } from "vue";
import { useCrud } from "@/components/CRUD/useCrud";

const formRef = ref(null);

const { validateAndSave } = useCrud({
  // ... 其他配置
});

// 表单验证规则
const formRules = {
  name: [{ required: true, message: "请输入菜单名称", trigger: "blur" }],
  path: [{ required: true, message: "请输入路径", trigger: "blur" }],
};

// 保存时验证表单
const handleSave = () => {
  validateAndSave(formRef.value);
};
</script>

<template>
  <el-form ref="formRef" :model="form" :rules="formRules">
    <!-- 表单项... -->
  </el-form>

  <el-button @click="handleSave">保存</el-button>
</template>

自定义查询参数

javascript
import { useCrud } from "@/components/CRUD/useCrud";

const { refresh } = useCrud({
  crudMethod: {
    list: (params) => {
      // 自定义参数处理
      const customParams = {
        ...params,
        extra: "custom-value",
      };
      return get(customParams);
    },
    // ... 其他方法
  },
  // ... 其他配置
});

监听数据变化

vue
<script setup>
import { watch } from "vue";
import { useCrud } from "@/components/CRUD/useCrud";

const { data, searchInfo } = useCrud({
  // ... 配置
});

// 监听数据变化
watch(data, (newData) => {
  console.log("数据已更新:", newData);
});

// 监听搜索条件变化(已内置防抖处理)
watch(
  searchInfo,
  (newSearch) => {
    console.log("搜索条件变化:", newSearch);
  },
  { deep: true }
);
</script>

常见问题

Q1: 如何禁用自动刷新?

设置 refreshOnInit: false 可以禁用初始化时的自动刷新。

javascript
useCrud({
  // ... 其他配置
  refreshOnInit: false,
});

Q2: 如何自定义分页配置?

javascript
useCrud({
  // ... 其他配置
  pageSizeOptions: [5, 10, 20, 50],
  // 默认每页显示 10 条(取 pageSizeOptions[1])
});

Q3: 如何处理导出文件名?

系统会自动从响应头的 Content-Disposition 中提取文件名,支持中文文件名。

Q4: 如何手动触发刷新?

vue
<script setup>
const { refresh } = useCrud({
  // ... 配置
});

// 手动刷新
const handleRefresh = async () => {
  await refresh();
  console.log("刷新完成");
};
</script>

Q5: 如何获取选中的数据?

vue
<script setup>
const { multipleSelection } = useCrud({
  // ... 配置
});

// 获取选中的数据
const getSelectedData = () => {
  console.log("选中的数据:", multipleSelection.value);
};
</script>

最佳实践

1. API 接口规范

建议 API 接口遵循以下规范:

javascript
// 查询列表
export const get = (params) => {
  return request({
    url: "/api/menu/list",
    method: "get",
    params,
  });
};

// 新增
export const add = (data) => {
  return request({
    url: "/api/menu",
    method: "post",
    data,
  });
};

// 编辑
export const edit = (data) => {
  return request({
    url: "/api/menu",
    method: "put",
    data,
  });
};

// 删除
export const del = (data) => {
  return request({
    url: "/api/menu",
    method: "delete",
    data,
  });
};

// 导出
export const download = (params) => {
  return request({
    url: "/api/menu/export",
    method: "get",
    params,
    responseType: "blob",
  });
};

2. 返回数据格式

列表查询接口返回格式:

json
{
  "data": {
    "content": [], // 数据列表
    "totalElements": 100 // 总条数
  }
}

或简化格式:

json
{
  "data": [], // 数据列表
  "total": 100 // 总条数
}

3. 权限配置建议

采用统一的权限标识命名规范:

javascript
const perms = {
  add: "sys:module:add", // 新增权限
  edit: "sys:module:edit", // 编辑权限
  del: "sys:module:del", // 删除权限
  download: "sys:module:download", // 导出权限
};

参考资源

版权所有 © 2021-2026 ApeVolo-Team