Fork me on GitHub

SchemaSearch:配置驱动搜索栏的设计与实现

本文从「为什么做」「怎么设计」「内部怎么跑」「怎么用」四个维度,完整剖析 workspace/components/common/SchemaSearch 组件,帮助你理解 配置驱动(Schema-Driven) 这一企业级前端最常用的组件封装范式。

前置阅读:项目规范 20-frontend-common-components.md 中明确,当页面存在「查询 + 工具栏 + 表格 + 分页 + 后端拉数」时,查询项可配置必须使用 SchemaSearch


一、它解决什么问题?

1.1 痛点:重复劳动

一个后台管理系统通常有几十甚至上百个列表页,每个页面顶部都有搜索栏。如果每个页面都手写一遍:

<!-- ❌ 每个页面都重复写这些 -->
<el-form inline>
  <el-form-item label="合同编号">
    <el-input v-model="query.contractNo" placeholder="请输入" />
  </el-form-item>
  <el-form-item label="状态">
    <el-select v-model="query.status" placeholder="请选择">
      <el-option label="启用" value="1" />
      <el-option label="禁用" value="0" />
    </el-select>
  </el-form-item>
  <el-form-item label="创建日期">
    <el-date-picker v-model="query.dateRange" type="daterange" />
  </el-form-item>
  <el-button @click="handleSearch">搜索</el-button>
  <el-button @click="handleReset">重置</el-button>
</el-form>

会带来三个问题:

问题 具体表现
重复代码 50 个列表页 × 5 个搜索项 = 250 段几乎一样的模板代码
行为不统一 有的页面搜索后清空了分页,有的没有;有的做了空值过滤,有的传了空字符串
维护成本高 产品想给所有搜索栏加一个"高级筛选"功能——你得改 50 个文件

1.2 解决思路

把"搜索栏长什么样"从代码逻辑中抽离,变成一份数据描述(Schema)。

页面只需声明:

const searchItems = [
  { key: 'contractNo', label: '合同编号', type: 'input' },
  { key: 'status', label: '状态', type: 'select', options: [...] },
  { key: 'createTime', label: '创建日期', type: 'daterange' },
]

组件拿到这份"说明书"后,自动完成:渲染 UI → 收集用户输入 → 转换为请求参数 → 通知页面。

这就是 Schema-Driven UI(配置驱动 UI)——企业级中后台最核心的组件封装思想。


二、核心设计思想

2.1 数据描述 UI

传统开发是命令式——你告诉框架"先放一个输入框,再放一个下拉框,给输入框绑定这个变量……"。

Schema-Driven 是声明式——你只描述"我要什么",组件自己决定"怎么做"。

┌─────────────────────────────────────────────────────┐
│                    传统写法(命令式)                   │
│                                                     │
│  页面直接操控 Element Plus 组件                        │
│  template 里写具体的 el-input / el-select            │
│  逻辑和 UI 紧耦合,改一处动全身                         │
└─────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│                  Schema 写法(声明式)                  │
│                                                     │
│  页面只提供一份 JSON 配置(Schema)                     │
│          ↓                                          │
│  SchemaSearch 组件读取配置                             │
│          ↓                                          │
│  根据 type 自动选择对应输入组件渲染                      │
│          ↓                                          │
│  用户操作后自动收集值、转换参数、通知页面                   │
└─────────────────────────────────────────────────────┘

2.2 设计模式:策略模式 + 工厂映射

组件内部的核心问题是:type: 'input' 该渲染哪个组件?type: 'select' 又该渲染哪个?

解决方案是一个映射表inputMap.ts):

// type 字符串 → Vue 组件的映射
export const INPUT_COMPONENT_MAP = {
  input:         InputText,
  select:        InputSelect,
  daterange:     InputDateRange,
  monthrange:    InputMonthRange,
  date:          InputDate,
  cascader:      InputCascader,
  inputRangeNum: InputRangeNum,
  textarea:      InputTextarea,
}

渲染时用 Vue 的 <component :is="..."> 动态组件:

<!-- SearchForm.vue 核心渲染逻辑 -->
<component
  :is="getInputComponent(item)"    // 根据 item.type 从映射表取组件
  v-model="innerModel[item.key]"   // 双向绑定
  v-bind="getBindProps(item)"      // 把 schema 属性透传给输入组件
/>

这就是策略模式的体现——同一个渲染位置,根据 type 的不同,切换不同的实现(策略)。新增一种搜索类型,只需要:

  1. 写一个新的 Input 组件
  2. 在映射表里加一行

不用改任何已有代码——这就是开放封闭原则(OCP)

2.3 架构分层:四层职责隔离

┌─────────────────────────────────────────────────────────────┐
│                        页面调用层                             │
│  只关心:传配置、监听 change 事件、拿参数刷新表格                  │
└────────────────────────────┬────────────────────────────────┘
                             │ props: items / emit: change
┌────────────────────────────▼────────────────────────────────┐
│                    index.vue(编排层)                         │
│  职责:状态管理、事件协调                                       │
│  · 管理所有搜索项的值(searchItems)                            │
│  · 协调 Popover / Drawer / Setting 的显隐                    │
│  · 调用 buildSearchParams() 收集参数                          │
│  · 向页面 emit('change', params)                             │
└──┬──────────┬──────────┬──────────┬─────────────────────────┘
   │          │          │          │
   ▼          ▼          ▼          ▼
SearchItem  SearchPopover SearchForm SearchSetting    ← UI 组件层
 (药丸标签)  (编辑气泡)   (表单渲染)   (显隐/排序设置)
                          │
                          ▼
               InputText / InputSelect / ...          ← 输入组件层
               (对 Element Plus 的薄封装)

每一层只做自己该做的事:

层级 职责 不做什么
页面 定义 schema、接收参数、刷新表格 不关心 UI 怎么渲染
编排层(index.vue) 管理状态、协调子组件、参数转换 不直接渲染输入控件
UI 组件层 渲染药丸 / 气泡 / 抽屉的交互壳 不知道具体是什么输入控件
输入组件层 包装 Element Plus 原子控件 不知道自己在哪个上下文中被使用

2.4 参数转换:Schema → Query Params

用户填完搜索条件后,组件要把"内部状态"转换成"后端接口需要的请求参数"。这一步由 buildSearchParams() 完成。

转换优先级链(这是理解参数行为的关键):

空值 → 直接跳过(不传给后端)
  ↓
有 transform? → 调用自定义转换函数(最高优先级,完全自定义)
  ↓
type === 'cascader'? → 提取末级叶子值
  ↓
paramsKey 是数组? → 拆分成多个参数(典型场景:日期范围)
  ↓
paramsKey 是字符串? → 重命名参数 key
  ↓
兜底 → 直接用 schema 的 key 作为参数名

源码补充:buildSearchParams 还会对字符串值调用 normalizeValuetrim(),避免前后空格被传给后端。

举例说明:

// 配置
{ key: 'createTime', type: 'daterange', paramsKey: ['startTime', 'endTime'] }

// 用户选了 2026-01-01 到 2026-07-25
// 内部值:{ createTime: ['2026-01-01', '2026-07-25'] }

// buildSearchParams 转换后:
// { startTime: '2026-01-01', endTime: '2026-07-25' }
//   ↑ 数组被拆开,key 被重命名,这就是 paramsKey 数组的作用

2.5 关键设计决策

决策 为什么这样做
药丸式 UI 而非传统 inline form 搜索项多时不会挤成一团,已填的条件一目了然,支持显隐/排序
Popover 内编辑而非直接 inline 避免整行搜索栏被撑高;select/cascader 这类弹出式组件在 inline 布局下体验差
高级筛选用 Drawer 搜索项超过一屏时,侧滑抽屉可以看到所有条件,一次性修改
SearchSetting 支持拖拽排序 不同用户关注的搜索条件不同,用户自行调整常用项的位置
输入组件统一暴露 focus() 方法 点击药丸弹出 Popover 后自动聚焦输入框,减少一次点击
buildSearchParams 自动过滤空值 空字符串、null、空数组都不传给后端,避免后端多余的 WHERE 条件

三、内部工作流程

3.1 初始化

页面传入 items 配置数组
        │
        ▼
index.vue 用 ref 深拷贝一份,每个 item 补上 value(取 defaultValue 或 null)
        │
        ▼
computed 计算 visibleItems:过滤掉 hidden=true 的,按 order 排序
        │
        ▼
v-for 渲染 SearchPopover + SearchItem 药丸列表

源码要点:index.vue 会监听 props.items 的深层变化,保留已有 value,只更新其他属性(如 label),避免切换 tab 时查询条件被意外清空。

3.2 用户点击药丸 → 编辑 → 确认

① 用户点击 [合同编号] 药丸
        │
        ▼
② toggle(key):设置 activeKey = 'contractNo'
        │
        ▼
③ SearchPopover 感知 visible=true,弹出气泡
   内部的 SearchForm 根据 type='input' 渲染 InputText
   自动调用 focusInput() 聚焦到输入框
        │
        ▼
④ 用户输入 "ABC-123",点击「确定」
        │
        ▼
⑤ SearchPopover emit('confirm', { key: 'contractNo', value: 'ABC-123' })
        │
        ▼
⑥ index.vue 的 handleConfirm:
   · 找到 searchItems 中 key='contractNo' 的 item
   · 更新 item.value = 'ABC-123'
   · 调用 buildSearchParams(searchItems) 收集所有参数
   · emit('change', { contractNo: 'ABC-123' })
        │
        ▼
⑦ 页面的 handleSearchChange(params):
   · query.value = params
   · pageTableRef.value.reload()

3.3 高级筛选(Drawer)

① 用户点击「高级筛选」链接
        │
        ▼
② toggleSearchDrawer():
   · 用 buildSearchParams 收集当前值到 formModel
   · 打开 BaseDrawer (filterVisible = true)
        │
        ▼
③ Drawer 中的 SearchForm 渲染所有 searchItems(含 label)
   用户一次性修改多个条件
        │
        ▼
④ 用户点击「确定」→ handleFilter():
   · 把 formModel 的值写回 searchItems
   · buildSearchParams → emit('change', params)
   · 关闭 Drawer

3.4 设置(显隐/排序)

① 用户点击 ⚙ 齿轮图标
        │
        ▼
② SearchSetting Popover 打开
   · 列出所有 searchItems
   · checkbox 控制显示/隐藏
   · 拖拽手柄控制排序(基于 vuedraggable)
        │
        ▼
③ 用户拖拽调整顺序,取消勾选某些项,点击「确定」
        │
        ▼
④ onConfirm():
   · 更新每个 item 的 hidden 属性
   · emit('update:searchItems', newItems)
   · index.vue 接收后更新 searchItems
   · visibleItems 自动重新计算 → UI 刷新

3.5 点击外部自动关闭

import { onClickOutside } from '@vueuse/core'

onClickOutside(popoverWrapperRef, (e) => {
  // 如果点击的是 Select/DatePicker/Cascader 的弹出面板,不关闭
  if (isInSelectPanel(e.target as HTMLElement)) return
  closeAll()  // 否则关闭所有 Popover
})

isInSelectPanel 会检查点击目标是否在 Element Plus 的下拉面板内(因为这些面板通过 teleport 挂载在 body 上,不在 Popover 的 DOM 树内),避免用户在选择下拉项时误关闭 Popover。

3.6 外部 values 同步(Tab 切换恢复条件)

index.vue 暴露 values prop,用于把外部保存的查询条件同步回搜索栏:

// index.vue 中的核心逻辑
watch(
  () => props.values,
  (newValues) => {
    if (!newValues) return
    searchItems.value.forEach((item) => {
      const hasKey = item.key in newValues
      const hasParamsKey = Array.isArray(item.paramsKey) && item.paramsKey.some((k) => k in newValues)
      const hasSingleParamsKey = typeof item.paramsKey === 'string' && item.paramsKey in newValues

      if (hasKey) {
        item.value = newValues[item.key]
      } else if (hasParamsKey && Array.isArray(item.paramsKey)) {
        // daterange 等 paramsKey 为数组的类型,从 query 中重建 value 数组
        item.value = item.paramsKey.map((k) => newValues[k] ?? null)
      } else if (hasSingleParamsKey) {
        item.value = newValues[item.paramsKey as string]
      } else {
        item.value = item.defaultValue ?? null
      }
    })
  },
  { deep: true }
)

这意味着你可以把后端返回的 query 对象直接回灌给 SchemaSearch,组件会自动把拆开的 startTime/endTime 重新组装成 daterange 数组。


四、配置定义详解

4.1 SearchItemSchema 完整字段

类型定义来自 workspace/components/common/SchemaSearch/type.d.ts

export type SearchItemType =
  | 'input'
  | 'select'
  | 'daterange'
  | 'date'
  | 'cascader'
  | 'inputRangeNum'
  | 'custom'
  | 'textarea'

export interface SearchOption {
  label: string
  value?: string | number
  /** 用于 el-option-group,存在时当前项作为分组标题 */
  options?: SearchOption[]
}

export interface CascaderOption {
  label: string
  value?: string | number
  children?: CascaderOption[]
}

export interface SearchItemSchema {
  // ──────── 必填 ────────
  key: string           // 唯一标识,也是默认的请求参数名
  label: string         // 显示的标签文字
  type: SearchItemType  // 输入控件类型

  // ──────── 值相关 ────────
  value?: any           // 当前值(组件内部管理,外部一般不设)
  defaultValue?: any    // 默认值(页面初始化 / 清空时回到此值)

  // ──────── 参数转换 ────────
  paramsKey?: string | string[]   // 重命名请求参数 key
  transform?: (value: any) => Record<string, any> | any  // 完全自定义转换

  // ──────── UI 控制 ────────
  placeholder?: string  // 输入提示
  order?: number        // 显示顺序(数字越小越靠前)
  hidden?: boolean      // 是否默认隐藏(可通过 ⚙ 设置打开)
  checked?: boolean     // 设置面板中是否被勾选
  clearable?: boolean   // 是否可清空,默认 true
  width?: number        // Popover 宽度

  // ──────── select 专属 ────────
  options?: SearchOption[]   // 下拉选项 [{ label, value }]
  multiple?: boolean         // 是否多选
  onVisibleChange?: (visible: boolean) => void  // 下拉显隐回调

  // ──────── daterange / date 专属 ────────
  startPlaceholder?: string   // 开始日期占位
  endPlaceholder?: string     // 结束日期占位
  valueFormat?: string        // 值格式(默认 'YYYY-MM-DD')
  maxSpanDays?: number        // 最大可选跨度天数

  // ──────── cascader 专属 ────────
  cascaderOptions?: CascaderOption[]   // 级联选项(树形)
  cascaderProps?: Record<string, any>  // 传给 el-cascader 的 props
  cascaderPopperClass?: string         // 下拉面板自定义 popper-class
  cascaderPopperOptions?: Record<string, any>  // 下拉面板 popper 配置
  collapseTags?: boolean               // 多选时折叠标签
  collapseTagsTooltip?: boolean        // 折叠标签 hover 提示

  // ──────── 自定义 ────────
  render?: (modelValue: any, onChange: (v: any) => void) => any  // type='custom' 时自定义渲染
  display?: (value: any) => string    // 自定义药丸的显示文字
  format?: (value: any) => any        // 值格式化函数

  // ──────── 高级筛选专用 ────────
  span?: number         // 抽屉中的栅格宽度
  advanced?: boolean    // 是否仅出现在高级筛选(不在主搜索栏显示)
}

4.2 type 类型使用指南

input — 文本输入

最基础的搜索项,输入关键词做模糊/精确匹配。

{ key: 'contractNo', label: '合同编号', type: 'input', placeholder: '请输入' }
// 输出:{ contractNo: '用户输入的文本' }

textarea — 多行文本

适合批量输入场景(如粘贴多个车架号),搭配 transform 做拆分。

{
  key: 'vins',
  label: '批量车架号',
  type: 'textarea',
  placeholder: '格式:车架号1,车架号2,车架号3',
  transform: (value) => ({ vinList: value.split(',').map(v => v.trim()).filter(Boolean) })
}
// 用户输入 "VIN001,VIN002,VIN003"
// 输出:{ vinList: ['VIN001', 'VIN002', 'VIN003'] }

select — 下拉选择

单选或多选下拉,需提供 options

// 单选
{
  key: 'status',
  label: '状态',
  type: 'select',
  placeholder: '请选择',
  options: [
    { label: '启用', value: '1' },
    { label: '禁用', value: '0' }
  ]
}
// 输出:{ status: '1' }

// 多选
{
  key: 'regions',
  label: '区域',
  type: 'select',
  multiple: true,
  options: regionOptions   // 可以是 ref,响应式更新
}
// 输出:{ regions: ['east', 'south'] }

动态选项(异步加载)

项目规范要求字典数据走 docsStore.getBatchLookupList + getCachedSelectOptions,示例:

import { useDocsStore } from '@/stores/docs'

const docsStore = useDocsStore()

onMounted(async () => {
  await docsStore.getBatchLookupList(['CONTRACT_STATUS'])
})

// 配置中直接引用响应式选项
const searchItems = [
  {
    key: 'status',
    label: '合同状态',
    type: 'select',
    options: docsStore.getCachedSelectOptions('CONTRACT_STATUS')
  }
]

daterange — 日期范围

日期范围是最典型的需要拆参数的场景。

// 基础用法:拆成两个参数
{
  key: 'createTime',
  label: '创建日期',
  type: 'daterange',
  paramsKey: ['startTime', 'endTime']
}
// 用户选 2026-01-01 到 2026-07-25
// 输出:{ startTime: '2026-01-01', endTime: '2026-07-25' }

// 限制最大跨度(如最多选 90 天)
{
  key: 'dateRange',
  label: '查询期间',
  type: 'daterange',
  paramsKey: ['beginDate', 'endDate'],
  maxSpanDays: 90
}

date — 单个日期

{
  key: 'evaluateDate',
  label: '评估日期',
  type: 'date',
  defaultValue: '2026-07-25'
}
// 输出:{ evaluateDate: '2026-07-25' }

cascader — 级联选择

适用于树形数据:省→市→区、部门→岗位等。

{
  key: 'area',
  label: '地区',
  type: 'cascader',
  cascaderOptions: [
    {
      label: '广东省', value: 'GD',
      children: [
        { label: '深圳市', value: 'SZ' },
        { label: '广州市', value: 'GZ' }
      ]
    }
  ],
  cascaderProps: { multiple: true, checkStrictly: true }
}
// 单选输出:{ area: 'SZ' }(默认取末级值)
// 多选输出:{ area: 'SZ,GZ' }(逗号拼接)

源码细节:buildSearchParams 对 cascader 做了特殊处理:

  • 多选 + emitPath:取每条路径的末级叶子,逗号拼接
  • 多选无 emitPath:直接取所有值,逗号拼接
  • 单选:取路径数组最后一个元素作为末级值

inputRangeNum — 数字范围

{
  key: 'amount',
  label: '金额范围',
  type: 'inputRangeNum',
  paramsKey: ['minAmount', 'maxAmount']
}
// 用户输入 1000 ~ 5000
// 输出:{ minAmount: 1000, maxAmount: 5000 }

custom — 自定义渲染

当内置类型无法满足时,使用 render 函数完全自定义:

{
  key: 'customField',
  label: '自定义字段',
  type: 'custom',
  render: (modelValue, onChange) => h(MyCustomInput, {
    modelValue,
    'onUpdate:modelValue': onChange
  })
}

4.3 参数映射对照表

配置方式 内部值 输出参数
{ key: 'name' } '张三' { name: '张三' }
{ key: 'name', paramsKey: 'customerName' } '张三' { customerName: '张三' }
{ key: 'date', paramsKey: ['start', 'end'] } ['01-01', '07-25'] { start: '01-01', end: '07-25' }
{ key: 'ids', transform: v => ({ idList: v.split(',') }) } '1,2,3' { idList: ['1','2','3'] }
值为空('' / null / [] 不输出(自动过滤)

五、使用方式示例

5.1 标准列表页(最常见)

<template>
  <PageTable ref="pageTableRef" :request="getListData" :params="query">
    <template #search>
      <SchemaSearch :items="searchItems" @change="handleSearchChange" />
    </template>

    <template #table="{ data, height, pageSize, pageIndex }">
      <SchemaTable
        :data="data"
        :height="height"
        :page-size="pageSize"
        :page-index="pageIndex"
        :columns="columns"
      />
    </template>
  </PageTable>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import SchemaSearch from '@workspace/components/common/SchemaSearch/index.vue'
import PageTable from '@workspace/components/common/PageTable.vue'
import SchemaTable from '@workspace/components/common/SchemaTable/index.vue'
import { getContractPageList } from '@workspace/api/contract/contract'

const query = ref<Record<string, any>>({})
const pageTableRef = ref<InstanceType<typeof PageTable> | null>(null)

const searchItems = [
  { key: 'contractNo', label: '合同编号', type: 'input', placeholder: '请输入' },
  { key: 'customerName', label: '客户名称', type: 'input', placeholder: '请输入' },
  {
    key: 'accountType',
    label: '合同类型',
    type: 'select',
    options: [
      { label: '乘用车', value: '1' },
      { label: '商用车', value: '2' }
    ]
  },
  {
    key: 'createTime',
    label: '创建日期',
    type: 'daterange',
    paramsKey: ['createTimeStart', 'createTimeEnd']
  }
]

// ⭐ 核心:拿到参数 → 更新 query → 手动 reload
const handleSearchChange = (params: Record<string, any>) => {
  query.value = params
  pageTableRef.value?.reload()
}

const getListData = async (params: any) => {
  const { data } = await getContractPageList(params)
  if (data?.result === '1') return data
}
</script>

注意:PageTable 不会自动深监听 params 变化,因此 handleSearchChange 中必须手动调用 pageTableRef.value?.reload()

5.2 带动态选项 + 批量搜索

import { useDocsStore } from '@/stores/docs'

const docsStore = useDocsStore()

const searchItems = [
  { key: 'contractNo', label: '合同编号', type: 'input' },
  {
    key: 'status',
    label: '合同状态',
    type: 'select',
    options: docsStore.getCachedSelectOptions('CONTRACT_STATUS')
  },
  {
    key: 'vins',
    label: '批量车架号',
    type: 'textarea',
    placeholder: '格式:车架号1,车架号2,车架号3',
    transform: (value) => {
      const vinList = value.split(',').map((v: string) => v.trim()).filter(Boolean)
      return { vinList }
    }
  }
]

5.3 带默认值 + 默认隐藏项

const searchItems = [
  {
    key: 'status',
    label: '状态',
    type: 'select',
    defaultValue: '1',         // 页面打开时默认选中"启用"
    clearable: false,          // 不允许清空(始终有值)
    options: [
      { label: '启用', value: '1' },
      { label: '禁用', value: '0' }
    ]
  },
  {
    key: 'remark',
    label: '备注',
    type: 'input',
    hidden: true,              // 默认隐藏,用户通过 ⚙ 设置可以打开
    order: 99                  // 排在最后
  }
]

5.4 Tab 切换时恢复搜索条件

<SchemaSearch
  :items="searchItems"
  :values="savedQuery"
  @change="handleSearchChange"
/>
// 切换 tab 时把之前保存的查询条件传给 values
// 组件会自动将 values 同步回内部状态,恢复搜索栏的显示
const savedQuery = ref<Record<string, any>>({})

const handleTabChange = (tab: string) => {
  savedQuery.value = tabQueryMap[tab] || {}
}

六、文件结构与职责总结

SchemaSearch/
│
├── index.vue              主组件(编排层)
│                          · 管理 searchItems 状态
│                          · 协调 Popover / Drawer / Setting
│                          · buildSearchParams → emit('change')
│
├── type.d.ts              类型定义
│                          · SearchItemSchema:配置项的完整类型
│                          · SearchOption / CascaderOption
│
├── inputMap.ts            映射表(策略模式的注册中心)
│                          · type 字符串 → Vue 输入组件
│
├── utils.ts               工具函数
│                          · buildSearchParams:schema → 请求参数
│                          · formatSearchValue:值 → 药丸显示文本
│
├── components/
│   ├── SearchItem.vue     药丸标签
│   │                      · 显示 label + 格式化的 value
│   │                      · 溢出时自动加 tooltip
│   │                      · 清除按钮
│   │
│   ├── SearchPopover.vue  编辑气泡
│   │                      · 包裹 SearchForm
│   │                      · 确定 / 取消 / 自动聚焦
│   │
│   ├── SearchForm.vue     表单渲染器
│   │                      · <component :is="..."> 动态渲染
│   │                      · 高级筛选抽屉中也复用此组件
│   │                      · 回车拦截默认提交,触发 @submit
│   │
│   └── SearchSetting.vue  设置面板
│                          · checkbox 显隐 + 拖拽排序
│
└── inputs/                输入组件(对 Element Plus 的薄封装)
    ├── InputText.vue          el-input
    ├── InputTextarea.vue      el-input (textarea)
    ├── InputSelect.vue        el-select
    ├── InputSelectFlat.vue    平铺单选/多选(Popover 内使用)
    ├── InputDate.vue          el-date-picker
    ├── InputDateRange.vue     el-date-picker (daterange)
    ├── InputMonthRange.vue    el-date-picker (monthrange)
    ├── InputRangeNum.vue      数字范围 (start ~ end)
    ├── InputCascader.vue      el-cascader
    └── InputCascaderFlat.vue  平铺级联面板(Popover 内使用)

七、设计原则总结

原则 在 SchemaSearch 中的体现
配置驱动 页面只写 JSON 配置,不写 UI 代码
策略模式 inputMap.ts 映射表 + <component :is> 动态组件
开放封闭 新增搜索类型只需加 Input 组件 + 映射表一行,不改已有代码
关注点分离 编排层 / UI 层 / 输入层 / 工具层各司其职
约定优于配置 不设 paramsKey 则默认用 key;空值自动过滤;输入组件自动聚焦
渐进增强 简单场景只需 key + label + type;复杂场景按需加 transform / display / render

这些思想不仅适用于搜索栏——项目中的 SchemaTable(配置驱动表格)也是同一套范式。掌握了 Schema-Driven 的思路,你就掌握了企业级组件封装的核心方法论。


八、接入自检清单

在新页面使用 SchemaSearch 时,建议对照以下清单检查:

posted @ 2026-08-01 09:46  极度恐慌_JG  阅读(11)  评论(0)    收藏  举报