CRUD 组件系统
概述
系统提供了一套完整的 CRUD(增删改查) 组件封装,基于 Vue 3 Composition API 设计,开箱即用。通过简单的配置即可实现数据表格的增删改查、分页、排序、导出等常用功能,大幅提升开发效率。
核心特性:
- 🚀 开箱即用 - 一次配置,自动实现增删改查全流程
- 🎯 组件化设计 - 头部操作、行操作、搜索等功能组件化
- 📦 功能完整 - 支持分页、排序、批量删除、数据导出等
- 🔄 响应式 - 基于 Vue 3 响应式系统,数据自动同步
- 🎨 灵活扩展 - 提供丰富的插槽,支持自定义功能扩展
- ⚡ 性能优化 - 内置防抖、加载状态、错误处理等优化
核心 API
useCrud - CRUD 组合式 API
配置文件: src/components/CRUD/useCrud.js
这是 CRUD 系统的核心 Hook,提供了完整的增删改查逻辑封装。
参数说明:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
crudMethod | Object | 是 | - | CRUD 方法对象,包含 list、add、edit、del、download 方法 |
defaultForm | Function | 是 | - | 返回默认表单数据的函数 |
searchInfo | Object | 否 | - | 搜索条件响应式对象 |
pageSizeOptions | Array | 否 | [1, 10, 20, 30, 50, 100] | 分页大小选项 |
sortProp | String | 否 | 'id' | 默认排序字段 |
sortOrder | String | 否 | 'descending' | 默认排序方式 |
refreshOnInit | Boolean | 否 | true | 是否在初始化时自动刷新数据 |
返回值说明:
| 属性/方法 | 类型 | 说明 |
|---|---|---|
data | Ref | 表格数据数组 |
loading | Ref | 加载状态 |
total | Ref | 数据总数 |
dialogVisible | Ref | 对话框显示状态 |
isEdit | Ref | 是否为编辑模式 |
form | Ref | 表单数据 |
pagination | ComputedRef | 分页配置对象 |
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 参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
perms | Object | 是 | 权限配置对象,包含 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 参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
perms | Object | 是 | {} | 权限配置对象 |
row | Object | 是 | - | 当前行数据 |
disabledEdit | Boolean | 否 | false | 是否禁用编辑按钮 |
disabledDle | Boolean | 否 | false | 是否禁用删除按钮 |
val | String | 否 | '' | 删除确认提示中显示的值 |
插槽说明:
| 插槽名 | 说明 |
|---|---|
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 功能页面:

功能清单:
- ✅ 搜索表单与条件筛选
- ✅ 新增、批量删除、导出按钮
- ✅ 搜索显示/隐藏切换
- ✅ 数据列表刷新
- ✅ 表格多选与排序
- ✅ 行级编辑与删除操作
- ✅ 分页与每页条数切换
- ✅ 新增/编辑对话框
高级用法
自定义表单验证
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", // 导出权限
};
