CURD页面组件封装——搜索组件

113 阅读6分钟

前言

对于面向企业的项目而言,前端开发中最常见的场景之一便是后台管理系统,而这类系统的核心页面多为基于表格表单的 CURD(增删改查)操作页面。随着同类页面开发量的增加,封装一套通用的 CURD 功能组件变得尤为必要——这不仅能显著提升开发效率,避免重复性工作,更能为后续的统一修改与维护提供便利。

这类组件的封装建立在 CURD 页面交互标准化的基础上,若页面存在特殊的业务逻辑或交互需求,通用组件往往难以完美适配。倘若强行让组件兼容所有场景,会导致其内部逻辑复杂度激增,不可避免地引入业务逻辑耦合,既不利于组件的理解与维护,也会造成组件体积臃肿。

针对这类特殊业务需求,我们提供两种解决方案:一方面通过配置插槽建立对外扩展接口,支持自定义功能嵌入;另一方面,组件设计需遵循 “粒度最小化” 原则,支持按需组合使用 —— 即便部分组件无法适配特定场景,也不会影响整体功能的正常使用。

此外,通用组件的高效使用离不开后端的数据结构配合。建议后端统一数据返回格式与请求参数规范,否则前端需针对不同数据结构配置大量兼容参数,既增加开发成本,也提升了维护时的记忆负担。

思路

一个标准的 CURD 页面通常包含以下核心功能模块:搜索条、操作工具条、数据表格、新增 / 编辑弹窗、详情弹窗等。

本方案将采用 “分而治之” 的策略:将每个功能模块封装为独立的基础组件,再通过组件组合的方式快速搭建完整的 CURD 页面。

为了更清晰地呈现组件设计思路,后续将推出系列文章,每篇聚焦一个功能模块的封装实现细节。欢迎大家持续关注,并提出宝贵的建议与反馈。

image.png

搜索条

一个标准的搜索条通常包含若干查询表单控件以及 “搜索”“重置” 两个操作按钮。

为实现组件的通用性与灵活性,我们采用 JSON 配置化方案:通过遍历配置项数组动态生成查询表单,每个配置项需明确包含以下核心字段:后端映射字段名(prop)、表单标签(label)、表单类型(type)及组件属性配置(如占位提示、是否可清空等)。

结合业务场景分析,搜索表单的常用类型主要为三类:文本输入框(input)、下拉选择器(select)、时间选择器(time)。因此,组件内部先封装这三类基础控件,通过配置 type 参数指定显示类型;对于其他类型,可根据业务扩展需求逐步补充。

对于具备业务属性的特殊查询条件,如果非常常用,也可以封装为独立的表单组件进行复用。而无法复用的类型,则可以采用插槽机制处理—— 既保证通用组件的纯净性,又满足个性化需求。

实现

template模板

组件的核心逻辑是根据配置项数组动态渲染表单控件:通过判断每个配置项的 type 属性,渲染对应类型的表单组件;未指定 type 时,默认渲染文本输入框。

如下通过v-ifv-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 配置实现基础表单控件的动态渲染,满足绝大多数通用场景;通过插槽机制支持特殊业务控件的自定义,兼顾灵活性与扩展性。

后续将继续介绍数据表格、新增 / 编辑弹窗等组件的封装实现,敬请关注!