前端

Odoo 日期时间输入为什么不是“弹个日历完事”:DateTimeInput、rounding 与 popover 状态链路讲透

Odoo Web Client 里的日期时间控件,看起来只是一个输入框外加一个弹出式选择器。但从 `datetime_input.js`、`datetime_picker_hook.js`、`datetime_picker.js` 到 `datetime_picker_popover.js` 这条链路看,官方真正维护的是一套状态机:输入框如何托管给 service、时间粒度如何统一 round、范围选择如何稳定焦点、日期与时间又如何在提交前合并成最终值。本文把这条协议完整拆开。

前端
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

很多人看到 Odoo 的日期时间控件,第一反应会是:

  • 一个文本输入框;
  • 点开后弹出日期面板;
  • 选完关闭。

如果只把它理解成“输入框 + 日历弹层”,你会错过几个非常关键的问题:

  1. 为什么一个普通 <input> 能自动接上完整 picker 行为?
  2. 为什么分钟粒度、秒显示与时间输入是一套联动配置?
  3. 日期范围模式下,焦点日期为什么不会乱跳?
  4. 为什么确认时既要看选中的日期,又要看当前 time picker 状态?

这些问题的答案分散在 datetime_input.jsdatetime_picker_hook.jsdatetime_picker.jsdatetime_picker_popover.js 里,合起来才是完整机制。

一、DateTimeInput 自己几乎不做事,它只是把输入框交给 picker service 托管

DateTimeInput 这段源码最值得注意的不是复杂,而是克制。

组件本身只做两件事:

  • 把与输入框自身相关的 props 拆出来;
  • 调用 useDateTimePicker() 把其余配置交给服务层。

关键代码是:

  • getPickerProps = () => omit(this.props, ...Object.keys(dateTimeInputOwnProps))
  • useDateTimePicker({...})

这说明官方设计不是“每个输入组件自己实现打开/关闭/同步逻辑”,而是:

输入框只是入口,真正的交互生命周期交给 datetime_picker service 统一管理。

这种拆法很聪明,因为日期时间输入本来就很容易在不同视图里出现:表单、搜索、过滤器、弹窗、范围组件。若每个入口都自己维护弹层状态,迟早会散掉。

二、useDateTimePicker() 做的核心工作,是把原始 input ref 和 service 绑定起来

datetime_picker_hook.js 不长,但它决定了整条交互链的边界。

它先通过 useRef() 准备输入框引用,再构造 serviceParams

  • getInputs() 返回真实 DOM input;
  • 保留原始 params 的 getter;
  • 增加 useOwlHooks: true

随后调用:

useService("datetime_picker").create(serviceParams)

最后在组件销毁时执行 picker.disable()

这几步连起来,实际表达的是:

  • 输入框组件只提供 DOM 挂点;
  • picker service 负责监听、打开 popover、同步值;
  • 组件被卸载时,必须主动拆掉服务绑定,避免残留事件和悬挂弹层。

所以它不是一个“hook 帮你少写代码”的糖,而是输入框与全局 picker 服务之间的生命周期桥梁

三、rounding 不是小参数,它决定时间输入的精度契约

DateTimeInput 在调用 hook 时会传:

  • showSeconds: this.props.rounding <= 0
  • pickerProps.rounding

datetime_picker.xml 里的 TimePicker 又会使用:

  • minutesRounding="props.rounding"
  • showSeconds="props.rounding === 0"

这说明 Odoo 对时间精度的设计不是零散判断,而是一份完整契约:

  • rounding > 1:分钟按粒度跳;
  • rounding === 1:分钟不强制跳步,但不展示秒;
  • rounding === 0:开启秒级输入。

也就是说,rounding 同时影响:

  1. 时间选择 UI 怎么显示;
  2. 用户可以精确到什么粒度;
  3. onTimeChange() 写回结果时会落在哪种精度级别。

这比很多业务项目里“前端随便取整、后端再纠错”的做法稳得多。

四、onPropsUpdated() 真正维护的是 picker 的内部状态机

datetime_picker.js 里最重要的方法之一是 onPropsUpdated()

它在 props 变化时集中完成这些事情:

  • 把传入值整理成 this.values
  • 根据 minPrecision / maxPrecision 生成允许缩放层级;
  • 解析 minDate / maxDate,并在 type === "date" 时扩展到整天边界;
  • 根据当前值生成 timeValues
  • 决定是否需要重新调整焦点日期。

这一层特别关键,因为用户看到的是“一个 picker”,源码里实际上维护着多组状态:

  • 当前选中值;
  • 当前焦点月份;
  • 当前 hover 日期;
  • 当前时间输入值;
  • 当前 precision(天 / 月 / 年 / decade)。

没有这层统一重算,范围选择、外部值更新和弹层重开时都很容易乱套。

五、日期与时间不是两条平行线,最后会在 validateAndSelect() 合并

很多二开控件的问题,是把日期选择和时间输入分成两套互不相干的状态。

Odoo 没这么做。

validateAndSelect() 里很明确:

  1. 先取当前日期值;
  2. 若类型是 datetime,再把 state.timeValues[valueIndex]hour/minute/second 写回;
  3. 再检查是否落在 minDate / maxDate 范围内;
  4. 最后统一触发 onSelect()

这个顺序极其关键,因为它告诉你:

对 picker 来说,真正提交的不是“选中的日历格子”,而是“日期部分 + 当前时间部分”合并后的最终值。

所以如果你只盯着日期面板,不看 timeValues,很多边界都解释不通:

  • 为什么改时间也会触发选择校验;
  • 为什么范围模式里第二个时间可能默认比第一个晚一小时;
  • 为什么某些日期格子能点,但最终仍可能因为边界限制被拒绝。

六、焦点日期稳定的关键,是 adjustFocus()shouldAdjustFocusDate

日期范围控件最容易让用户烦躁的,是界面总自己跳月份。

Odoo 在这里做得很谨慎。

adjustFocus() 会优先选择:

  • 当前 focus index 对应的值;
  • 否则退回另一端值;
  • 再不行就退回 today()

shouldAdjustFocusDate 则控制“这次更新要不要重新对焦”。

这意味着 picker 不会因为每次状态变化都强行重置焦点。只有在真正需要的时候,才会把可视月份拉回更合理的位置。

这正是体验稳定的关键:

  • 值更新要响应;
  • 视图焦点又不能总跳。

七、popover 自身非常薄,但关闭时机被 hotkey 明确接管

datetime_picker_popover.js 看起来几乎没逻辑,只在 setup() 里做了一件事:

useHotkey("enter", () => this.props.close())

这很像小细节,但其实说明了两个设计判断:

  1. popover 不自行决定选值规则,它只是承载 picker;
  2. 关闭时机可以由统一热键系统接管,而不是写死在某个 input 事件上。

这样做的好处是,用户在键盘路径下也能完成一套可预测的提交动作,而不是只能依赖鼠标点击外部区域关闭。

八、二开最容易误解的几个点

误区 1:以为日期输入框本身负责大部分逻辑

实际上输入框只提供入口,picker service 才是主角。

误区 2:把 rounding 当视觉参数

它影响的不只是 UI,还影响可输入精度和最终值合并。

误区 3:范围模式只管两个日期,不管两个时间

源码里 timeValues 明确是按值索引维护的,范围模式下时间也是双份状态。

误区 4:外部 props 一变就该强制重置焦点月份

这样最容易造成“用户正在看 6 月,界面突然跳回今天”。

九、结论

Odoo 的日期时间输入之所以稳定,不是因为它“弹了一个好看的日历”,而是因为它把输入框、service、picker 状态和提交边界拆得非常清楚:

  • DateTimeInput 负责把入口交给 service;
  • useDateTimePicker() 负责 DOM ref 与生命周期绑定;
  • DateTimePicker 负责精度、焦点、范围、时间值和最终校验;
  • DateTimePickerPopover 只做弹层承载和关闭交互。

所以更准确的理解应该是:

Odoo 的日期时间控件,本质上是一套“输入入口 + 状态机 + 提交合并”的前端协议,而不是一个单独的日历组件。

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。