搜索 K
Appearance
Appearance
元数据驱动的好处是「新增模型零前端改动」,但总有字段需要专属表现:评分用星级、颜色用色板、地址用地图选点。本篇讲清楚如何在不改渲染引擎、不动内置组件的前提下,把任意字段/页面换成你自己的组件。
这套能力的核心是一个多维加权注册表:你注册一个组件并声明「它适用于哪些维度(场景/组件名/数据类型/模型/字段)」,引擎在渲染时选出最具体的那个。先读完字段渲染理解默认机制,再看本篇如何改变它。
全部能力就两个函数,从 @/widgets 导入:
import { registerFieldWidget, registerPageWidget } from '@/widgets'
// 注册自定义字段组件(局部替换某字段/模型/组件类型)
registerFieldWidget(match: Record<string, unknown>, comp: Component): void
// 注册自定义整页组件(整页替换)
registerPageWidget(match: Record<string, unknown>, comp: Component): voidmatch 是「维度约束」对象,声明你这个组件适用于哪些维度;只声明你关心的维度,不关心的不写。
字段注册表有五个维度,各有权重:
| 维度 | 权重 | 含义 |
|---|---|---|
field | 10000 | 字段名(最具体,能精确锁定一个字段) |
model | 1000 | 模型编码(锁定某个模型) |
component | 10 | 中立组件名(text/select/…) |
dataType | 5 | 数据类型(STRING/ENUM/…) |
scene | 1 | 场景(input 表单 / cell 展示) |
注册时,引擎把你声明了的维度的权重相加得到特异性(specificity)。渲染某字段时,引擎构造上下文 { scene, component, dataType, model, field },在所有「约束全部命中」的注册项里,选特异性最高的那个。
权重用「数量级」拉开是有意为之:一个 field 级注册(10000)必然压过任何 model(1000)/component(10)级注册——越精确的意图越优先。其设计原理见设计文档 · 渲染引擎。
关键规则:覆盖必须「更具体」,同特异性会输给内置项
内置组件在模块加载时就注册好了,你的注册发生在启动之后。引擎排序是稳定排序,特异性相同时先注册的(内置)排在前面、被优先选中。
所以:注册一个与内置项特异性相同的项,覆盖不会生效。要覆盖必须让特异性严格更高——多约束一个维度即可。例如想替换内置的 select 输入({scene:'input', component:'select'},特异性 11),不能再注册一个同样的,而要:
dataType 做全局覆盖:{scene:'input', component:'select', dataType:'ENUM'}(特异性 16,仍匹配所有枚举下拉);model/field 做局部覆盖(特异性更高,见下文配方)。注册表是模块级单例,注册只需在应用启动、首个页面渲染之前做一次。推荐新建一个 src/widgets/custom.ts 集中注册,并在 main.ts 挂载前 import:
// src/widgets/custom.ts —— 业务自定义组件集中注册
import { registerFieldWidget } from '@/widgets'
import RateInput from './custom/RateInput.vue'
import RateCell from './custom/RateCell.vue'
registerFieldWidget({ scene: 'input', model: 'biz.product', field: 'score' }, RateInput)
registerFieldWidget({ scene: 'cell', model: 'biz.product', field: 'score' }, RateCell)// src/main.ts —— 挂载前引入,确保注册早于渲染
import '@/widgets/custom'
// ...
app.mount('#app')当前没有内置的业务注册文件
框架导出了 registerFieldWidget/registerPageWidget 作为扩展点,但仓库里目前没有任何业务注册调用——扩展点是留给你的。按上面的约定新建 custom.ts 即可,无需改动渲染引擎。
自定义组件只要满足与内置组件相同的 props 契约即可即插即用(详见字段渲染 · 组件契约):
// 输入组件
defineProps<{ field: FieldSchema; value: any; model?: string }>()
defineEmits<{ 'update:value': [any] }>()
// 单元格组件
defineProps<{ field: FieldSchema; value: any; record?: any; model?: string }>()一个完整的自定义评分输入组件:
<!-- src/widgets/custom/RateInput.vue -->
<script setup lang="ts">
import type { FieldSchema } from '@/types/protocol'
defineProps<{ field: FieldSchema; value: any }>()
const emit = defineEmits<{ 'update:value': [any] }>()
</script>
<template>
<a-rate :value="value" :disabled="field.readonly"
@update:value="(v) => emit('update:value', v)" />
</template>下面按「从最局部到最全局」给出常用配方。match 越具体,影响面越小、优先级越高。
商品的 score 字段用星级,其余商品字段、其余模型的 score 都不受影响:
registerFieldWidget({ scene: 'input', model: 'biz.product', field: 'score' }, RateInput)
registerFieldWidget({ scene: 'cell', model: 'biz.product', field: 'score' }, RateCell)特异性 = scene(1) + model(1000) + field(10000) = 11001,碾压一切。
所有模型只要字段名叫 color 都用色板:
registerFieldWidget({ scene: 'input', field: 'color' }, ColorPickerInput)特异性 = 1 + 10000 = 10001。
后端给某字段下发 component: 'rate'(一个内置清单里没有的中立名),前端注册它:
registerFieldWidget({ scene: 'input', component: 'rate' }, RateInput)
registerFieldWidget({ scene: 'cell', component: 'rate' }, RateCell)这是最干净的扩展方式:因为 rate 不在内置清单里,不存在「同特异性输给内置」的问题。后端如何把字段标成 rate,见数据建模的字段 UI 元数据。
想让所有枚举下拉的录入换成自定义实现,给内置 {scene,component:'select'}(特异性 11)多加一维 dataType 压过它:
registerFieldWidget({ scene: 'input', component: 'select', dataType: 'ENUM' }, MySelect)特异性 = 1 + 10 + 5 = 16 > 11,且仍匹配所有枚举下拉。
为某数据类型提供统一展示,比 component 兜底更宽(注意仍要避免与内置同特异性):
registerFieldWidget({ scene: 'cell', dataType: 'DATE' }, RelativeTimeCell)| 配方 | match | 特异性 | 影响面 |
|---|---|---|---|
| 1 模型+字段 | {scene, model, field} | 11001 | 一个模型的一个字段 |
| 2 字段名 | {scene, field} | 10001 | 所有模型的同名字段 |
| 3 新组件名 | {scene, component:'rate'} | 11 | 后端标了该组件名的字段 |
| 4 类型+组件 | {scene, component, dataType} | 16 | 该组件+该类型的所有字段 |
| 5 数据类型 | {scene, dataType} | 6 | 该类型的所有字段 |
match 还支持数组(多值匹配)
某维度可传数组表示「命中其一即可」。例如 { scene: 'input', component: ['date', 'datetime'] } 同时匹配两种组件。引擎对数组维度用 includes 判断。
除了字段级,还有一个页面级注册表 pageRegistry,维度为 viewType(权重 1)+ model(权重 1000),配套 registerPageWidget / selectPageWidget:
import { registerPageWidget } from '@/widgets'
// 为商品换一个完全自定义的列表页
registerPageWidget({ viewType: 'TABLE', model: 'biz.product' }, ProductBoardView)诚实说明:整页替换当前未生效
pageRegistry 与 selectPageWidget 已实现,但没有任何视图调用 selectPageWidget——也就是说,注册了整页组件目前不会被采用。要让它生效,需要在路由视图(如 ResourceListView)入口处加一句「先问 selectPageWidget(viewType, model),命中就渲染自定义页、否则走默认渲染」的接线。这是一个已声明但未接线的扩展点,字段级覆盖(上文配方)则是完全可用的。
给字段换组件前,对照这份清单:
update:value,cell 只读)?match 的特异性是否严格高于要覆盖的内置项(同特异性会失效)?main.ts 挂载之前执行(import custom.ts)?input 与 cell 两个场景是否都要覆盖(表单换了、列表也要换吗)?component?