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 的不同,切换不同的实现(策略)。新增一种搜索类型,只需要:
- 写一个新的 Input 组件
- 在映射表里加一行
不用改任何已有代码——这就是开放封闭原则(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还会对字符串值调用normalizeValue做trim(),避免前后空格被传给后端。
举例说明:
// 配置
{ 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 时,建议对照以下清单检查:


浙公网安备 33010602011771号