Formily Next 日期选择器(DatePicker)实战指南:三种 Schema 写法与 Moment 值转换原理
2026/9/23 18:30:14 网站建设 项目流程
  • 前端
  • UI组件

【免费下载链接】formily

📱🚀 🧩 Cross Device & High Performance Normal Form/Dynamic(JSON Schema) Form/Form Builder -- Support React/React Native/Vue 2/Vue 3

项目地址:https://gitcode.com/gh_mirrors/fo/formily
点击查看免费下载

导读

本文围绕@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.WeekPickerWeekPickerYYYY-wo
DatePicker.MonthPickerMonthPickerYYYY-MM
DatePicker.YearPickerYearPickerYYYY
DatePicker.RangePickerRangePickerYYYY-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中的showTimetype等会原样透传给底层组件,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/reactField组件手写字段:

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-decoratorcomponent={[DatePicker]}等价于x-component;而组件参数(如type: 'month')需要写成component={[DatePicker.RangePicker, { type: 'month' }]}的二元数组形式。此模式适合逻辑简单、无需 Schema 动态化的场景。

三种写法的选型建议

维度Markup SchemaJSON SchemaPure JSX
类型提示强(JSX 即 Schema)弱(纯对象)
动态化能力强(可后端下发)
代码可读性
典型场景常规表单开发配置化 / 低代码简单局部字段

三者最终都会落到 Formily 的Field模型上,交互行为、校验与提交逻辑完全一致,可按项目形态自由切换。

源码级原理解析:Moment 值格式自动转换

默认格式映射

DatePicker的格式转换由mapDateFormat完成(见 date-picker/index.tsx#L35-L72),其默认格式规则为:

场景默认展示/提交格式
普通日期YYYY-MM-DD
type='month'/ MonthPickerYYYY-MM
type='year'/ YearPickerYYYY
type='week'/ WeekPickerYYYY-wo
开启showTime追加HH:mm:ss(可用showTime.format覆盖)

当显式传入format属性时,该属性优先生效;showTime会基于日期格式拼接时间格式,例如YYYY-MM-DD+HH:mm:ss。也就是说,表单层拿到并提交的始终是格式化后的字符串,而不是 moment 对象——这与SchemaField.Stringtype: '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 字段的loadingvalidatingselfErrorsselfWarnings映射为 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/antdpackages/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

项目地址:https://gitcode.com/gh_mirrors/fo/formily
点击查看免费下载

相关推荐

上一篇:终极粒子群优化实战指南:用scikit-opt轻松掌握PSO算法核心与调优技巧
下一篇:RR项目为RS2821RP+设备构建定制化系统镜像的技术实践

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询