前端

Odoo 搜索栏为什么不是“拼几个 domain 就完了”:SearchModel、facet、search panel 状态同步讲透

很多人把 Odoo 搜索栏理解成“把过滤条件拼成 domain 再发给后端”。但 web/search/search_model.js 说明,真正复杂的地方在于前端要统一管理 filter、group by、favorite、日期区间与 search panel 的状态,并保证它们能序列化、恢复和联动。

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

很多人调 Odoo 搜索功能时,脑子里只有一句话:

搜索不就是拼个 domain 吗?

这句话不能算错,但它只说中了最后一步

真正难的部分,是前端如何把这些东西同时组织起来:

  • 搜索栏里的 filter
  • group by
  • favorite
  • order by
  • 日期区间
  • search panel 左侧分类树
  • 当前上下文和全局 domain
  • 页面刷新后的状态恢复

这也是为什么 addons/web/static/src/search/search_model.js 会这么长。它不是单纯的 domain 工具,而是 Odoo 搜索 UI 的状态中枢

一、SearchModel 管的不是“一个查询条件”,而是一整套搜索会话

load(config) 就能明白,SearchModel 一上来接收的不是一个 domain,而是一整组搜索环境:

  • resModel
  • context
  • domain
  • groupBy
  • orderBy
  • searchViewArch
  • searchViewFields
  • irFilters
  • searchMenuTypes
  • display.searchPanel
  • state

这说明官方设计 SearchModel 时,目标不是“帮你算个查询表达式”,而是:

把当前这一次列表/看板/报表搜索会话完整建模出来。

所以它天然是前端状态容器,而不是后端 domain 包装器。

二、为什么 SearchModel 要自己持有 globalContext / globalDomain / globalGroupBy

load() 里,源码会先把:

  • globalContext
  • globalDomain
  • globalGroupBy
  • globalOrderBy

收进模型。

这一步非常关键。它意味着 SearchModel 从一开始就区分两层东西:

1)入口预设

来自 action、菜单、view 配置、当前上下文的默认约束。

2)用户交互后的增量状态

比如临时点亮一个筛选器、改一个 group by、切一个 favorite。

如果不分层,前端很快就会出现两个典型问题:

  • 你不知道哪些条件是入口强制给的,哪些是用户刚刚点的;
  • 一旦用户“清空筛选”,容易把本该保留的基础约束也一并清掉。

SearchModel 把全局预设先单独存住,本质上是在保护“入口语义”。

三、SearchModel 里的 query / sections / searchItems 才是 UI 真正的语言

源码里有几个很值得注意的状态:

  • query
  • searchItems
  • sections
  • searchPanelInfo

这几个名字已经暴露了设计思路:Odoo 搜索 UI 不是直接围着 domain 转,而是先围着可交互对象转。

searchItems 是什么

它更像一份“可被启用、关闭、组合的搜索项目录”。

里面的条目可能来自:

  • search view 里的 filter
  • group by 项
  • date filter
  • favorite
  • 自定义动态项

sections 是什么

它更接近 search panel 左侧的分区结构。源码里甚至专门把 section 分为 categoryfilter 两种。

这说明 search panel 不是普通附属 UI,而是 SearchModel 的内建部分。

query 是什么

它不是单纯字符串,而是当前已激活搜索条件的抽象状态集合。

换句话说,SearchModel 先维护“当前界面上到底激活了哪些搜索语义”,然后再导出 domain / context / groupBy / orderBy 给后续数据层使用。

四、为什么 favorites 在 SearchModel 里不是“另一个过滤器”

源码定义了:

  • FAVORITE_PRIVATE_GROUP = 1
  • FAVORITE_SHARED_GROUP = 2

这说明 favorite 从设计上就不是普通 filter。

普通 filter 更像“当前这次会话里临时点亮的条件”; favorite 更像“可持久化、可复用、可共享的搜索方案”。

也正因为如此,favorite 在 SearchModel 里要处理的事情更多:

  • 保存的不只是 domain
  • 还可能带 group by、排序、上下文
  • 还要区分私有和共享
  • 还要支持恢复为当前 active query

这能解释为什么很多人自己做搜索定制时,临时筛选能跑,收藏搜索却乱掉:

因为你以为自己保存的是一个 domain,实际你在保存一份完整搜索状态快照。

五、日期筛选为什么会单独复杂:它不是常量 filter,而是“运行时生成条件”

SearchModel 专门引入了:

  • constructDateDomain
  • getIntervalOptions
  • getPeriodOptions
  • DEFAULT_INTERVAL
  • referenceMoment

这已经说明日期筛选不是普通的静态 filter 节点。

它的复杂度主要来自三点:

1)同一个“本月”会随着时间变化

今天点的“本月”和下个月再恢复这个 favorite,本质上不是同一组固定日期。

所以它需要相对时间语义,而不是只存结果。

2)日期 / datetime 语义不同

时区、边界、粒度都会影响最终 domain。

3)UI 上看起来像一个简单选项,底层却需要序列化成可恢复配置

这就是 SearchModel 必须显式维护 reference moment 和 interval 逻辑的原因。

很多自定义出 bug,不是 domain 拼错,而是把动态时间条件当成静态筛选项来保存

六、SearchModel 为什么要自己加载 search view,而不是等别人把结果喂给它

load() 里如果发现需要,会调用 viewService.loadViews() 去取 search view。

这说明 SearchModel 并不是一个纯被动容器,它会主动参与“搜索语义解释”的前半段。

原因很简单:

  • filter 定义在 search arch 里
  • field 信息在 view fields 里
  • favorite 也可能依赖 search view 语义
  • search panel 是否可显示,和 view 描述也有关

所以 SearchModel 不是拿到一个 domain 后才开始工作,而是会先参与:

  1. 解析 search view
  2. 建立搜索项目录
  3. 恢复 state
  4. 再产出最终查询语义

这就是它和“普通查询参数对象”的根本区别。

七、状态可恢复,是 SearchModel 最重要也最容易低估的能力

源码里有 mapToArrayarraytoMapdeepCopy 这些工具,以及对 state 的支持。

这背后的真实目标是:

当前搜索界面必须能被序列化、恢复、复制,并在不同交互周期里维持一致。

为什么这一点重要?因为 Odoo Web Client 不是一次性页面:

  • 用户会切换视图
  • 会刷新页面
  • 会保存 favorite
  • 会从 action 返回
  • 会带着上下文重新进入同一个模型

如果搜索状态不能恢复,用户感知就会非常差。

所以 SearchModel 其实承担了很像“前端会话状态机”的角色。

八、search panel 不是边栏装饰,而是 SearchModel 的第一等公民

源码里专门有:

  • categoriesLoadId
  • filtersLoadId
  • searchPanelInfo
  • sections
  • hasValues(section)

这说明左侧 search panel 的加载、分组、计数、层级和可见性,不是临时拼在外面的。

官方是把它视为搜索模型内部的一部分。

这点很重要,因为很多二开喜欢犯一个错误:

  • 列表顶部搜索栏自己做一套状态
  • 左侧 panel 自己再做一套状态
  • 两边最后只靠 domain 勉强对齐

这样短期能用,长期就会出:

  • 选中态不同步
  • favorite 恢复不完整
  • panel 计数与顶部 query 不一致
  • 分组条件和 facet 显示打架

SearchModel 的价值,恰好就是把这些状态统一在一个地方。

九、从源码看,SearchModel 解决的是“前端搜索语义组合”问题

如果只从 ORM 角度理解搜索,就会以为搜索系统的核心在:

  • domain
  • context
  • order by
  • group by

但 SearchModel 让我们看到,前端其实还有一层更难的问题:

1)UI 项如何映射到查询语义

不是每个搜索项都等价于 domain。

  • 有些是 group by
  • 有些是 context
  • 有些是 favorite 快照
  • 有些是 search panel 分类状态

2)多个来源如何合并

  • action 默认值
  • search view 定义
  • 当前用户点击
  • 已保存 favorite
  • 左侧 panel 当前状态

3)状态如何持久化与恢复

这部分如果没有统一模型,搜索体验就会非常碎。

十、实战里怎么判断自己是不是用错了 SearchModel 思路

如果你遇到下面这些问题,通常就不是简单 domain 问题:

  • filter 点亮了,但 favorite 恢复后丢了一半状态
  • search panel 左边和顶部 facet 不同步
  • group by 改了以后收藏搜索不一致
  • 日期筛选下个月恢复时结果完全不对
  • 页面回来后顶部 UI 还在,但实际查询条件变了

这些症状大多指向一个共同问题:

你只保存了“查询结果表达式”,没有保存“搜索会话状态”。

而 SearchModel 正是官方给出的那层中介。

十一、结论

Odoo 搜索栏真正复杂的地方,不在“domain 怎么拼”,而在:

  • 搜索项如何被建模;
  • 不同类型条件如何组合;
  • search panel、favorite、date filter、group by 如何保持同一份状态语义;
  • 整个搜索会话如何被序列化、恢复和重放。

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

SearchModel 不是一个 domain 生成器,而是 Odoo 搜索 UI 的状态操作系统。

理解这一点后,你做搜索二开时就不会只盯着 domain 末端,而会先问:

  • 当前状态由谁持有?
  • 哪些条件是入口预设?
  • 哪些条件是用户激活?
  • 哪些东西需要被 favorite 一起持久化?

这才是把 Odoo 搜索功能做稳的关键。

DISCUSSION

评论区

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