- 前端
- UI组件
【免费下载链接】formily
📱🚀 🧩 Cross Device & High Performance Normal Form/Dynamic(JSON Schema) Form/Form Builder -- Support React/React Native/Vue 2/Vue 3
导读
本文围绕@formily/next中的日期选择器组件DatePicker展开,系统讲解其在 Formily 表单体系中的三种接入方式(Markup Schema、JSON Schema、Pure JSX)、五种内置形态(普通日期、周选择、月选择、年选择、范围选择)以及与FormItem装饰器、只读态PreviewText的配合。读完本文,你将掌握在 Formily 项目中快速落地各种日期选择场景的完整套路,并理解组件底层基于 moment 的「值格式自动转换」机制,从而避免表单值与展示格式不一致的常见坑。
组件定位:基于 Fusion Next 的 Formily 化封装
packages/next是 Formily 面向阿里 Fusion Design(@alifd/next)体系的高阶组件包。其中的 DatePicker 源码 并非从零实现一个日期控件,而是通过@formily/react提供的connect能力将 Fusion 的DatePicker桥接到 Formily 的字段模型中:
export const DatePicker: ComposedDatePicker = connect( NextDatePicker, mapProps(mapDateFormat(), mapSize, mapStatus), mapReadPretty(PreviewText.DatePicker) )核心套路可拆解为三层:
connect:把受控组件与 Formily 的Field绑定,实现 value/onChange 的自动接管;mapProps:把 Formily 字段状态(尺寸、校验状态)与组件的 props 做映射,并注入日期格式转换逻辑;mapReadPretty:为组件挂载只读态渲染,即字段处于pattern === 'readPretty'时自动退化为PreviewText。
DatePicker同时以复合组件形式挂载了四个子组件(见 index.tsx#L80-L102):
| 子组件 | 对应 Fusion 组件 | 默认提交格式 |
|---|---|---|
DatePicker.WeekPicker | WeekPicker | YYYY-wo |
DatePicker.MonthPicker | MonthPicker | YYYY-MM |
DatePicker.YearPicker | YearPicker | YYYY |
DatePicker.RangePicker | RangePicker | YYYY-MM-DD(起止两个值) |
从源码结构看,这些子组件都复用了同一套mapDateFormat与只读映射逻辑,因此在使用方式上与主组件完全一致,只是格式与交互不同。
三种接入方式完整示例
方式一:Markup Schema(JSX 描述式)
Markup Schema 是 Formily 最推荐的写法,用 JSX 组件即 Schema 节点,类型安全且可读性强:
import React from 'react' import { DatePicker, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { DatePicker, FormItem, }, }) const form = createForm() export default () => ( <FormProvider form={form}> <SchemaField> <SchemaField.String name="date" title="normal date" x-decorator="FormItem" x-component="DatePicker" /> <SchemaField.String name="week" title="Week Selection" x-decorator="FormItem" x-component="DatePicker.WeekPicker" /> <SchemaField.String name="month" title="Month Selection" x-decorator="FormItem" x-component="DatePicker.MonthPicker" /> <SchemaField.String name="year" title="Year selection" x-decorator="FormItem" x-component="DatePicker.YearPicker" /> <SchemaField.String name="[startDate,endDate]" title="Date Range" x-decorator="FormItem" x-component="DatePicker.RangePicker" x-component-props={{ showTime: true, }} /> <SchemaField.String name="range_month" title="Month Range Selection" x-decorator="FormItem" x-component="DatePicker.RangePicker" x-component-props={{ type: 'month', }} /> <SchemaField.String name="range_year" title="Year range selection" x-decorator="FormItem" x-component="DatePicker.RangePicker" x-component-props={{ type: 'year', }} /> </SchemaField> <FormButtonGroup> <Submit onSubmit={console.log}>Submit</Submit> </FormButtonGroup> </FormProvider> )要点说明:
- 字段统一使用
SchemaField.String,因为表单最终提交的值是字符串; x-decorator="FormItem"负责布局、标签与校验反馈展示;x-component="DatePicker.xxx"用点语法引用复合子组件;- 范围选择(RangePicker)的字段名写成
[startDate,endDate],这是 Formily 内置的「范围内置字段」约定,表单值将自动聚合成{ startDate, endDate }对象结构; x-component-props中的showTime、type等会原样透传给底层组件,type支持'month' | 'year' | 'week',用于让 RangePicker 呈现月区间、年区间。
方式二:JSON Schema(纯数据描述式)
当 Schema 需要由后端下发、或需要动态拼装时,使用 JSON Schema 写法,两种写法表达的字段结构完全等价:
import React from 'react' import { DatePicker, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { DatePicker, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { date: { title: 'Normal date', 'x-decorator': 'FormItem', 'x-component': 'DatePicker', type: 'string', }, week: { title: 'Week Selection', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.WeekPicker', type: 'string', }, month: { title: 'Month Selection', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.MonthPicker', type: 'string', }, year: { title: 'Year selection', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.YearPicker', type: 'string', }, '[startDate,endDate]': { title: 'Date range', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', 'x-component-props': { showTime: true, }, type: 'string', }, range_month: { title: 'Month Range Selection', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', 'x-component-props': { type: 'month', }, type: 'string', }, range_year: { name: 'range_year', title: 'Year range selection', 'x-decorator': 'FormItem', 'x-component': 'DatePicker.RangePicker', 'x-component-props': { type: 'year', }, type: 'string', }, }, } export default () => ( <FormProvider form={form}> <SchemaField schema={schema} /> <FormButtonGroup> <Submit onSubmit={console.log}>Submit</Submit> </FormButtonGroup> </FormProvider> )与 Markup Schema 的差异仅在书写形态:JSON Schema 以schema对象驱动<SchemaField schema={schema} />,而 Markup Schema 以 JSX 子节点驱动。二者在渲染层完全等价,可随时互相转换。
方式三:Pure JSX(命令式编程)
不依赖 Schema 时,可以直接用@formily/react的Field组件手写字段:
import React from 'react' import { DatePicker, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( <FormProvider form={form}> <Field name="date" title="date selection" decorator={[FormItem]} component={[DatePicker]} /> <Field name="week" title="Week Selection" decorator={[FormItem]} component={[DatePicker.WeekPicker]} /> <Field name="quarter" title="Financial Year Selection" decorator={[FormItem]} component={[DatePicker.MonthPicker]} /> <Field name="year" title="Year selection" decorator={[FormItem]} component={[DatePicker.YearPicker]} /> <Field name="[startDate,endDate]" title="Date range selection" decorator={[FormItem]} component={[DatePicker.RangePicker]} /> <Field name="range_month" title="Month Range Selection" decorator={[FormItem]} component={[ DatePicker.RangePicker, { type: 'month', }, ]} /> <Field name="range_year" title="Year range selection" decorator={[FormItem]} component={[ DatePicker.RangePicker, { type: 'year', }, ]} /> <FormButtonGroup> <Submit onSubmit={console.log}>Submit</Submit> </FormButtonGroup> </FormProvider> )Pure JSX 的关键差异在于 props 的传递方式:decorator={[FormItem]}等价于x-decorator,component={[DatePicker]}等价于x-component;而组件参数(如type: 'month')需要写成component={[DatePicker.RangePicker, { type: 'month' }]}的二元数组形式。此模式适合逻辑简单、无需 Schema 动态化的场景。
三种写法的选型建议
| 维度 | Markup Schema | JSON Schema | Pure JSX |
|---|---|---|---|
| 类型提示 | 强(JSX 即 Schema) | 弱(纯对象) | 中 |
| 动态化能力 | 中 | 强(可后端下发) | 弱 |
| 代码可读性 | 高 | 中 | 高 |
| 典型场景 | 常规表单开发 | 配置化 / 低代码 | 简单局部字段 |
三者最终都会落到 Formily 的Field模型上,交互行为、校验与提交逻辑完全一致,可按项目形态自由切换。
源码级原理解析:Moment 值格式自动转换
默认格式映射
DatePicker的格式转换由mapDateFormat完成(见 date-picker/index.tsx#L35-L72),其默认格式规则为:
| 场景 | 默认展示/提交格式 |
|---|---|
| 普通日期 | YYYY-MM-DD |
type='month'/ MonthPicker | YYYY-MM |
type='year'/ YearPicker | YYYY |
type='week'/ WeekPicker | YYYY-wo |
开启showTime | 追加HH:mm:ss(可用showTime.format覆盖) |
当显式传入format属性时,该属性优先生效;showTime会基于日期格式拼接时间格式,例如YYYY-MM-DD+HH:mm:ss。也就是说,表单层拿到并提交的始终是格式化后的字符串,而不是 moment 对象——这与SchemaField.String的type: 'string'声明是自洽的。
值进出场转换:momentable 与 formatMomentValue
组件通过两个工具函数完成「字符串 ⇄ moment」的双向转换,二者定义在 packages/next/src/builtins/moment.ts:
- 入场(
momentable):把表单字符串值转成 moment 实例交给底层控件渲染,且兼容数组值(范围选择的两个值会逐项转换); - 出场(
formatMomentValue):在 onChange 中把底层回传的 moment 实例按约定格式格式化回字符串;若是范围选择,会按索引分别格式化后以数组返回。
值得注意的一个细节:formatMomentValue内部对形如19:55:22的时间字符串做了特判——直接命中^(?:[01]\d|2[0-3]):[0-5]\d(:[0-5]\d)?$时,会以format作为第二参数调用 moment,从而正确处理纯时间字符串(见 moment.ts#L23-L38)。
状态与尺寸自动映射
mapStatus(mapStatus.ts)会把 Formily 字段的loading、validating、selfErrors、selfWarnings映射为 Fusion 控件需要的state属性,实现校验错误自动红框、异步校验自动 loading;mapSize(mapSize.ts)则会从 FormLayout 上下文读取尺寸,并把default归一化为 Fusion 的medium。这意味着日期选择器的外观会随FormLayout全局配置联动,无需逐个字段配置。
只读态支持:PreviewText.DatePicker
当字段进入只读场景(如详情页、提交回显)时,mapReadPretty(PreviewText.DatePicker)会让 DatePicker 自动退化为纯文本展示。对应实现见 preview-text/index.tsx#L176-L198:
const DatePicker: React.FC<React.PropsWithChildren<DatePickerProps>> = ( props ) => { const placeholder = usePlaceholder() const prefixCls = usePrefixCls('form-preview', props) const getLabels = () => { const labels = formatMomentValue(props.value, props.format, placeholder) return isArr(labels) ? labels.join('~') : labels } return <div className={cls(prefixCls, props.className)}>{getLabels()}</div> }- 单个日期值直接按
format格式化输出;范围值会以~连接(如2024-01-01~2024-01-31); - 空值默认展示占位符
N/A,可通过PreviewText.Placeholder上下文自定义; - 只读态同样走
formatMomentValue,因此展示格式与提交格式天然一致,不会出现「提交的是YYYY-MM-DD、展示却多出时间」之类的错位。
进阶:DatePicker2 与季度选择
如果项目使用 Fusion 的DatePicker2体系(moment 对象二选一之外的新版日期组件),@formily/next还提供了DatePicker2封装(date-picker2/index.tsx)。与DatePicker相比,DatePicker2额外支持QuarterPicker季度选择,默认提交格式为YYYY-\QQ(如2024-Q1),且showTime场景下默认格式直接合并为YYYY-MM-DD HH:mm:ss。其复合子组件(RangePicker/MonthPicker/YearPicker/WeekPicker/QuarterPicker)与使用方式与DatePicker完全对齐。
另外,packages/antd与packages/element分别提供了面向 Ant Design 与 Element 的等价封装,接口与本文所述保持一致——如果你在 Vue 或 AntD 生态中迁移,可以复用相同的 Schema 结构。
API 与后续延伸
DatePicker的完整属性 API 与 Fusion 官方DatePicker保持一致(即@alifd/next的 date-picker 组件属性),本文不再赘述。实际开发中如需自定义展示格式、日期禁用范围、国际化等,直接通过x-component-props透传对应属性即可,Formily 层不会拦截。
若需深入了解 Schema 驱动的底层机制,可继续阅读:
- 字段模型与联动:packages/core 文档
- 表单布局与 FormItem 装饰器:FormItem.md
- 只读态组件体系:PreviewText.md
- Vue 侧的日期组件方案:element DatePicker 指南
结合本文的三种接入方式与格式转换原理,你可以在 Formily 项目中快速、稳定地实现从「单日期」到「带时间的区间选择」的全部常见业务形态。
- 前端
- UI组件
【免费下载链接】formily
📱🚀 🧩 Cross Device & High Performance Normal Form/Dynamic(JSON Schema) Form/Form Builder -- Support React/React Native/Vue 2/Vue 3
相关推荐
从理论到实践:FluvioFX流体动力学求解器工作原理剖析
从理论到实践:FluvioFX流体动力学求解器工作原理剖析 FluvioFX是一款为Unity VFX Graph打造的流体动力学求解器,能够帮助开发者在游戏和
前端UI组件如何快速部署Home Assistant操作系统:5个简单步骤的完整指南
如何快速部署Home Assistant操作系统:5个简单步骤的完整指南 Home Assistant操作系统是一款专为智能家居控制中心优化的Linux系统,基
前端UI组件Formily 级联选择器 Cascader 实战指南:Markup Schema / JSON Schema / 纯 JSX 三种写法与异步省市区数据源
Formily 级联选择器 Cascader 实战指南:Markup Schema / JSON Schema / 纯 JSX 三种写法与异步省市区数据源 本文
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考