自定义组件
当内置组件无法满足需求时,v3sf 支持开发和注册自定义组件。
概述
自定义组件的开发流程:
- 创建一个 Vue 组件,遵循标准 props 接口
- 使用
defineWidget()包装组件(可选) - 注册到适配器中
- 在 Schema 中通过
widget名称引用
组件标准 Props
所有 v3sf 组件(内置或自定义)都会接收以下标准 props:
ts
interface WidgetStandardProps {
modelValue: any // 字段当前值
disabled?: boolean // 是否禁用
readonly?: boolean // 是否只读
placeholder?: string // 占位文本
error?: string // 校验错误信息
addon?: FieldWidgetAddon // 表单上下文(也可通过 useAddon() 获取)
}自定义组件必须:
- 接收
modelValueprop - 通过
update:modelValue事件更新值
基本示例:评分组件
1. 创建组件
vue
<!-- widgets/MyRating.vue -->
<script setup lang="ts">
defineProps<{
modelValue: number
disabled?: boolean
readonly?: boolean
max?: number
}>()
const emit = defineEmits<{
'update:modelValue': [value: number]
}>()
</script>
<template>
<div class="my-rating">
<span
v-for="i in max ?? 5"
:key="i"
:class="{ active: i <= modelValue, disabled }"
@click="!disabled && !readonly && emit('update:modelValue', i)"
>
★
</span>
</div>
</template>
<style scoped>
.my-rating span {
cursor: pointer;
font-size: 24px;
color: #ccc;
}
.my-rating span.active {
color: #f5a623;
}
.my-rating span.disabled {
cursor: not-allowed;
opacity: 0.5;
}
</style>2. 注册到适配器
ts
import { defineAdapter } from '@v3sf/core'
import { vantAdapter } from '@v3sf/vant'
import MyRating from './widgets/MyRating.vue'
const customAdapter = defineAdapter({
widgets: {
...vantAdapter.widgets,
rating: { component: MyRating },
},
globalPropsMap: vantAdapter.globalPropsMap,
})3. 在 Schema 中使用
ts
const schema = {
type: 'object',
properties: {
score: {
type: 'number',
title: '评分',
widget: 'rating',
props: { max: 10 },
},
},
}使用 defineWidget()
defineWidget() 是一个类型辅助函数,用于定义组件配置。它可以指定 propsMap 来映射标准 prop 名到组件实际的 prop 名:
ts
import { defineWidget } from '@v3sf/core'
import MyColorPicker from './MyColorPicker.vue'
const colorPickerWidget = defineWidget({
component: MyColorPicker,
propsMap: {
modelValue: 'color', // 将 modelValue 映射为 color prop
disabled: 'isDisabled', // 将 disabled 映射为 isDisabled prop
},
})然后注册到适配器:
ts
const customAdapter = defineAdapter({
widgets: {
...vantAdapter.widgets,
colorPicker: colorPickerWidget,
},
globalPropsMap: vantAdapter.globalPropsMap,
})何时需要 propsMap
如果你的组件已经使用 modelValue / disabled / readonly / placeholder 等标准命名,则不需要 propsMap。只有当组件使用不同的 prop 名称时才需要映射。
使用 useAddon() 获取表单上下文
在自定义组件中,通过 useAddon() hook 可以访问丰富的表单上下文信息:
vue
<script setup lang="ts">
import { useAddon } from '@v3sf/core'
const addon = useAddon()
// addon.value 包含以下属性:
// - name: string — 字段名
// - schema: Schema — 当前字段的 Schema
// - rootSchema: Schema — 根 Schema
// - disabled?: boolean — 是否禁用
// - readonly?: boolean — 是否只读
// - placeholder?: string — 占位文本
// - className?: string — 自定义类名
// - required?: boolean — 是否必填
// - props?: object — Schema 中的 props 透传
// - setFormData(data) — 设置表单数据(可以修改其他字段)
// - getFormData() — 获取整个表单数据
// - validate() — 触发全部字段校验
// - validateFields(fields) — 校验指定字段
</script>示例:地址选择器
使用 useAddon() 实现一个能修改多个字段的地址选择器:
vue
<!-- widgets/AddressPicker.vue -->
<script setup lang="ts">
import { useAddon } from '@v3sf/core'
defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const addon = useAddon()
// 选择地址后,同时更新省份和城市字段
function onAddressSelect(province: string, city: string) {
addon.value.setFormData({
province,
city,
})
emit('update:modelValue', `${province} ${city}`)
}
</script>
<template>
<div class="address-picker">
<!-- 自定义地址选择 UI -->
<select @change="onAddressSelect('浙江', '杭州')">
<option>请选择地址</option>
<option>浙江 杭州</option>
<option>北京 北京</option>
</select>
</div>
</template>完整示例:颜色选择器
以下是一个完整的颜色选择器自定义组件示例。
组件实现
vue
<!-- widgets/ColorPicker.vue -->
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useAddon } from '@v3sf/core'
const props = defineProps<{
modelValue: string
disabled?: boolean
readonly?: boolean
colors?: string[]
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const addon = useAddon()
const presetColors = props.colors ?? [
'#ff4d4f',
'#ff7a45',
'#ffa940',
'#ffc53d',
'#73d13d',
'#36cfc9',
'#40a9ff',
'#597ef7',
'#9254de',
'#f759ab',
]
const customColor = ref(props.modelValue ?? '#000000')
watch(customColor, (val) => {
emit('update:modelValue', val)
})
function selectColor(color: string) {
if (props.disabled || props.readonly) return
customColor.value = color
emit('update:modelValue', color)
}
</script>
<template>
<div class="color-picker" :class="{ disabled }">
<div class="color-presets">
<span
v-for="color in presetColors"
:key="color"
class="color-swatch"
:class="{ selected: modelValue === color }"
:style="{ backgroundColor: color }"
@click="selectColor(color)"
/>
</div>
<input
v-if="!readonly"
type="color"
:value="modelValue"
:disabled="disabled"
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
/>
<span v-if="modelValue" class="color-value">{{ modelValue }}</span>
</div>
</template>
<style scoped>
.color-picker {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.color-picker.disabled {
opacity: 0.5;
pointer-events: none;
}
.color-presets {
display: flex;
gap: 4px;
}
.color-swatch {
width: 24px;
height: 24px;
border-radius: 4px;
cursor: pointer;
border: 2px solid transparent;
}
.color-swatch.selected {
border-color: #333;
}
.color-value {
font-family: monospace;
font-size: 14px;
color: #666;
}
</style>注册和使用
ts
import { createSchemaForm, defineAdapter } from '@v3sf/core'
import { vantAdapter } from '@v3sf/vant'
import ColorPicker from './widgets/ColorPicker.vue'
// 注册到适配器
const adapter = defineAdapter({
widgets: {
...vantAdapter.widgets,
colorPicker: { component: ColorPicker },
},
globalPropsMap: vantAdapter.globalPropsMap,
})
const SchemaForm = createSchemaForm(adapter)
// 在 Schema 中使用
const schema = {
type: 'object',
properties: {
themeColor: {
type: 'string',
title: '主题颜色',
widget: 'colorPicker',
props: {
colors: ['#1890ff', '#52c41a', '#faad14', '#f5222d', '#722ed1'],
},
},
},
}注意事项
useAddon()必须在setup中调用 — 它基于 Vue 的inject机制,只能在 SchemaForm 内部的组件中使用。更新值必须通过
emit('update:modelValue', newValue')— 不要直接修改 props。通过
propsMap解决 prop 命名冲突 — 如果组件已有valueprop 而非modelValue,使用propsMap: { modelValue: 'value' }映射。Schema 中的
props会透传给组件 — 可以在 Schema 中通过props传递额外的配置参数。