前言
对于面向企业的项目而言,前端开发中最常见的场景之一便是后台管理系统,而这类系统的核心页面多为基于表格表单的 CURD(增删改查)操作页面。随着同类页面开发量的增加,封装一套通用的 CURD 功能组件变得尤为必要——这不仅能显著提升开发效率,避免重复性工作,更能为后续的统一修改与维护提供便利。
这类组件的封装建立在 CURD 页面交互标准化的基础上,若页面存在特殊的业务逻辑或交互需求,通用组件往往难以完美适配。倘若强行让组件兼容所有场景,会导致其内部逻辑复杂度激增,不可避免地引入业务逻辑耦合,既不利于组件的理解与维护,也会造成组件体积臃肿。
针对这类特殊业务需求,我们提供两种解决方案:一方面通过配置插槽建立对外扩展接口,支持自定义功能嵌入;另一方面,组件设计需遵循 “粒度最小化” 原则,支持按需组合使用 —— 即便部分组件无法适配特定场景,也不会影响整体功能的正常使用。
此外,通用组件的高效使用离不开后端的数据结构配合。建议后端统一数据返回格式与请求参数规范,否则前端需针对不同数据结构配置大量兼容参数,既增加开发成本,也提升了维护时的记忆负担。
思路
一个标准的 CURD 页面通常包含以下核心功能模块:搜索条、操作工具条、数据表格、新增 / 编辑弹窗、详情弹窗等。
本方案将采用 “分而治之” 的策略:将每个功能模块封装为独立的基础组件,再通过组件组合的方式快速搭建完整的 CURD 页面。
为了更清晰地呈现组件设计思路,后续将推出系列文章,每篇聚焦一个功能模块的封装实现细节。欢迎大家持续关注,并提出宝贵的建议与反馈。
搜索条
一个标准的搜索条通常包含若干查询表单控件以及 “搜索”“重置” 两个操作按钮。
为实现组件的通用性与灵活性,我们采用 JSON 配置化方案:通过遍历配置项数组动态生成查询表单,每个配置项需明确包含以下核心字段:后端映射字段名(prop)、表单标签(label)、表单类型(type)及组件属性配置(如占位提示、是否可清空等)。
结合业务场景分析,搜索表单的常用类型主要为三类:文本输入框(input)、下拉选择器(select)、时间选择器(time)。因此,组件内部先封装这三类基础控件,通过配置 type 参数指定显示类型;对于其他类型,可根据业务扩展需求逐步补充。
对于具备业务属性的特殊查询条件,如果非常常用,也可以封装为独立的表单组件进行复用。而无法复用的类型,则可以采用插槽机制处理—— 既保证通用组件的纯净性,又满足个性化需求。
实现
template模板
组件的核心逻辑是根据配置项数组动态渲染表单控件:通过判断每个配置项的 type 属性,渲染对应类型的表单组件;未指定 type 时,默认渲染文本输入框。
如下通过v-if、v-else-if分别判断生成输入框、下拉选框、和时间选择器。
所有表单组件均基于 Element Plus 实现,其属性配置与 Element Plus 组件保持一致,确保开发者无需额外学习成本。对于需自定义的特殊控件,可通过配置 slot 字段启用插槽,由外部传入自定义内容。
「特别说明」:通过v-bind="$attrs"将外部属性绑定到<el-form>组件,使得调用者可直接传递 Element Plus 表单组件的原生属性(如校验规则、布局方式等),提升组件灵活性。
<template>
<div class="ts-search">
<el-form ref="searchFormRef" inline v-bind="$attrs" :model="data">
<!-- 对配置项columns进行遍历 -->
<el-form-item v-for="item in columns" :key="item.prop" :label="`${item.label}:`" :prop="item.prop">
<!-- 插槽 -->
<slot v-if="item.slot" :name="item.slot" />
<template v-else>
<!-- 默认为输入框类型 -->
<el-input
v-if="!item.type || item.type === 'input'"
v-model.trim="data[item.prop]"
placeholder="请输入"
:clearable="item.clearable ?? true"
/>
<!-- 下拉选 -->
<el-select
v-else-if="item.type === 'select'"
v-model="data[item.prop]"
placeholder="请选择"
:clearable="item.clearable ?? true"
:multiple="item.multiple ?? false"
collapse-tags
collapse-tags-tooltip
:filterable="item.filterable ?? true"
:allow-create="item.allowCreate ?? false"
:value-on-clear="item.valueOnClear"
>
<el-option
v-for="optItem in item.options"
:key="optItem.value"
:label="optItem.label"
:value="optItem.value"
/>
</el-select>
<!-- 时间选择器 -->
<el-date-picker
v-else-if="item.type === 'time'"
v-model="data[item.prop]"
:type="item.subType"
placeholder="请选择"
start-placeholder="开始时间"
end-placeholder="结束时间"
:clearable="item.clearable ?? true"
:editable="item.editable ?? true"
:value-format="item.format ?? dateFormatFunc(item.subType)"
:default-value="item.defaultValue"
:default-time="item.defaultTime"
:disabled-date="item.disabledDate"
/>
</template>
</el-form-item>
<div class="search-btns">
<el-button type="primary" icon="Search" @click="handleSearch">查询</el-button>
<el-button type="default" icon="Refresh" @click="handleReset">重置</el-button>
</div>
</el-form>
</div>
</template>
script逻辑
组件对外暴露两个核心属性:data(查询条件数据对象)和columns(表单配置项数组),均为必填项;对外抛出两个事件:search(查询按钮点击回调)和reset(重置按钮点击回调),支持外部自定义业务逻辑。
引入dateFormatFunc自定义工具函数,用于根据时间选择器的子类型(subType)自动匹配默认时间格式,减少配置冗余。
<script setup name="TsSearch">
import { dateFormatFunc } from '@/utils/data.js';
const props = defineProps({
data: {
type: Object,
required: true,
default: () => {}
},
columns: {
type: Array,
required: true,
default: () => []
}
});
const emit = defineEmits(['search', 'reset']);
defineOptions({
inheritAttrs: false
});
const searchFormRef = ref();
function handleSearch() {
console.log('handleSearch data: ', props.data);
emit('search');
}
function handleReset() {
searchFormRef.value.resetFields();
emit('reset');
emit('search');
}
</script>
/**
* @description: 日期格式化适配
* @param {*} type
* @return {*}
*/
export function dateFormatFunc(type) {
return (
{
date: 'YYYY-MM-DD',
daterange: 'YYYY-MM-DD',
datetime: 'YYYY-MM-DD HH:mm:ss',
datetimerange: 'YYYY-MM-DD HH:mm:ss',
year: 'YYYY',
month: 'YYYY-MM',
}[type] || 'YYYY-MM-DD'
);
}
调用
以下为组件的完整调用示例,包含基础配置项与插槽用法。
searchData为查询条件数据对象,searchColumns为json表单配置项数组。
其中“年龄”表单项配置为了slot插槽,可以在模板中进行自定义实现插入。
<template>
<ts-search :data="searchData" :columns="searchColumns" @search="handleSearch" @reset="handleReset">
<template #age>
<el-select v-model="searchData.age" placeholder="请选择">
<el-option label="20-30岁" value="20" />
<el-option label="30-40岁" value="30" />
<el-option label="40-50岁" value="40" />
<el-option label="50-60岁" value="50" />
</el-select>
</template>
</ts-search>
</template>
<script setup>
import { handleSearchData } from '@/utils/data.js';
import dayjs from 'dayjs';
const { proxy } = getCurrentInstance();
// start:查询条件相关
const searchData = reactive({
name: '222',
gender: '',
age: '',
address: '',
dateRange: []
});
const searchColumns = reactive([
{
prop: 'name',
label: '姓名'
},
{
prop: 'gender',
label: '性别',
type: 'select',
options: [
{ value: 0, label: '女' },
{ value: 1, label: '男' }
]
},
{
label: '年龄',
slot: 'age' // 使用插槽自定义
},
{
prop: 'dateRange',
label: '出生日期',
type: 'time',
subType: 'daterange',
fields: ['startTime', 'endTime'],
disabledDate: handleDisabledDate
},
{
prop: 'address',
label: '通讯地址',
type: 'input',
clearable: false
}
]);
function getTimeRange() {
const now = dayjs();
const startTime = now.startOf('month').toDate();
const endTime = now.endOf('month').toDate();
return {
startTime,
endTime
};
}
/**
* @description: 日期选择器对可选日期的限制
* @return {*}
*/
function handleDisabledDate(date) {
const { startTime, endTime } = getTimeRange();
return date.getTime() < startTime.getTime() || date.getTime() > endTime.getTime();
}
/**
* @description: 查询
* @return {*}
*/
function handleSearch() {
pageOption.pageNum = 1;
getData();
}
/**
* @description: 查询条件重置
* @return {*}
*/
function handleReset() {
// 插槽中查询条件手动重置
searchData.age = '';
}
</script>
总结
搜索条组件的核心设计思路是 “配置化 + 插槽扩展”:通过 JSON 配置实现基础表单控件的动态渲染,满足绝大多数通用场景;通过插槽机制支持特殊业务控件的自定义,兼顾灵活性与扩展性。
后续将继续介绍数据表格、新增 / 编辑弹窗等组件的封装实现,敬请关注!