网站菜单

Odoo 网站菜单为什么不是“拉几个层级就行”:层级限制、Mega Menu 与 active 判定主链路讲透

很多人把 Odoo 菜单理解成一个树形链接列表,但 website.menu 实际同时处理多网站复制、层级约束、Mega Menu 结构边界、分组可见性与当前菜单 active 判定,远比“配导航”讲究。

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

先说结论

Odoo 的网站菜单不是“数据库里存一棵树,前台照着渲染”这么简单。

/home/ubuntu/odoo-temp/addons/website/models/website_menu.pytests/test_menu.py 看,website.menu 至少承担了五层职责:

  1. 导航树结构本身
  2. 多网站场景下的默认菜单复制
  3. Mega Menu 与普通菜单的结构约束
  4. 按页面可见性和用户分组控制展示
  5. 基于当前请求 URL 判断谁是 active 菜单。

所以理解它最好的方式不是“导航配置表”,而是:

一个把网站入口、站点隔离、可见性和当前上下文一起编码进来的导航对象。

这也是为什么很多菜单问题明明看起来像前端样式问题,最后却要回到模型层去查。


一、菜单为什么会“自动复制”到每个网站,而不是只建一条记录

website.menu.create() 有一段非常关键的逻辑:

  • 如果创建菜单时没有显式 website_id
  • 又不是当前上下文强制指定某个网站;
  • 系统会把这条菜单复制到每个网站。

官方注释直接点明了用途:

  • 模块安装时新增像 /shop 这样的默认菜单;
  • 每个网站都应该各自有一份。

这意味着 Odoo 默认把菜单分成两类:

1. 站点特定菜单

显式带 website_id,只属于某个网站。

2. 模板式默认菜单

作为“每个网站都应有”的公共导航项,在创建时自动扩散。

这套机制很适合多网站产品线,因为:

  • 公共功能入口不需要手工到每个站点再配一遍;
  • 但复制出来后,各站仍然可以继续局部改名或调整顺序。

所以如果你在多网站项目里发现“为什么我只建了一次菜单,却到处都有”,别急着当成 bug,它很可能正是设计使然。


二、为什么 Odoo 不允许菜单树无限往下长

_validate_parent_menu() 明确限制了层级:

  • 菜单层级不能超过两层;
  • 带子菜单的菜单,不能再作为更深一层的子菜单;
  • Mega Menu 不能有父也不能有子。

这背后的产品判断很鲜明:

官网导航不是后台科目树,过深层级会直接破坏可用性。

很多企业官网需求方一开始都想把:

  • 一级菜单
  • 二级菜单
  • 三级菜单
  • 四级栏目

全部塞进头部导航。Odoo 在模型层就替你踩了刹车。

这其实是好事。

因为一旦层级过深,不只是前端 hover 难做,移动端折叠、焦点导航、SEO 内链权重和用户认知都会一起变差。


三、Mega Menu 为什么被当成一种“结构特权”,而不是普通菜单皮肤

源码里 is_mega_menu 不是纯前端样式开关。

当它为真时:

  • 菜单会持有 mega_menu_content
  • 没内容时系统可自动渲染默认模板;
  • 它不能有 parent,也不能有 child;
  • _compute_url() 还会直接把 URL 置成 #

这说明在 Odoo 眼里,Mega Menu 的角色不是“多一层下拉菜单”,而是:

一个导航容器。

它的主要任务不是自己跳转,而是承载一整块内容区域。

所以如果你的设计是“Mega Menu 既想点进去,又想继续挂三级结构”,那就已经偏离 Odoo 预设的交互模型了。


四、菜单可见性为什么不只看菜单自己,还要看关联页面活不活着

_compute_visible() 很值得仔细看。

对外部用户来说,菜单可见性不只取决于菜单记录本身,还会继续检查:

  • 如果绑的是 page_id,页面是不是 is_visible
  • 页面对应 view 的 visibility 是否允许当前用户访问;
  • 如果绑的是 controller_page_id,对应 controller page 是否已发布、是否可访问。

也就是说,Odoo 不希望出现这种情况:

  • 导航里还挂着一个链接;
  • 结果用户点进去才发现页面未发布或无权限。

它更倾向于在菜单层就先把不可访问入口隐藏掉。

这点对体验非常重要,因为:

  • 菜单本身就是承诺;
  • 能展示出来,就意味着“你大概率可以进去”。

五、为什么 group 可见性会自动补上设计器组

write() 里还有个细节:

  • 如果设置了 group_ids,系统会顺手把 website.group_website_designer 加进去。

这听上去很小,但很合理。

因为如果一个菜单被某些业务组限制可见,而网站设计器自己却看不到,就很难维护、预览和调试。

所以 Odoo 在这里体现的是一种后台运营友好原则:

限制前台用户不等于限制网站维护者。


六、active 菜单为什么不是简单判断“当前路径等于菜单 URL”

菜单高亮判定 _is_active() 是这份源码里最容易被低估的一段。

它处理得比常见 CMS 复杂得多:

  • 会比较当前 request URL;
  • 忽略锚点 fragment;
  • 对 query string 采用“菜单参数必须是当前请求参数子集”的策略;
  • 会做 unslug 后比较,兼容 /shop/1/shop/my-product-1
  • 如果菜单本身有子菜单,就主要看子菜单是否命中;
  • 如果菜单链接到某个页面,哪怕 unslug 后相同,也要求 path 精确一致。

这套规则背后的思路很成熟:

1. 锚点不算路径身份

因为浏览器请求本来就不会把 fragment 发给服务器。

2. 查询参数会影响页面语义

如果 query string 不匹配,不能轻易算作同一个 active 页面。

3. 父菜单更像容器

只要子菜单命中,父菜单就应该亮。

4. 页面菜单要更谨慎

绑定 page_id 时,不能仅靠 unslug 宽松判定,否则容易把不同页面错高亮。

tests/test_menu.py 甚至专门覆盖了:

  • 同路径不同 query;
  • 带 anchor;
  • 不同域名;
  • 父子菜单;
  • slug URL 场景。

这说明官方非常清楚 active 高亮如果做糙了,前台导航会一直给人“不稳”的感觉。


七、为什么默认菜单删除时,还会顺带删掉各网站副本

unlink() 还有一个多网站细节:

  • 如果删除的是默认主菜单下的子菜单;
  • 系统会把其它网站上 URL 相同的副本一起删掉。

这与 create 时的复制逻辑正好成对。

也就是说,Odoo 不是只会“帮你复制”,它还试图在删除时帮你维持模板树的一致性。

否则多网站环境下,默认菜单模板和各站副本会越来越漂移,后续很难收拾。


八、菜单问题最常见的误判是什么

误判一:导航不高亮,一定是模板 CSS 问题

很多时候其实是 _is_active() 的 URL 规则没命中。

误判二:菜单显示异常,只是前端渲染错了

不一定。可能是关联页面本身未发布、权限受限或 controller page 不可见。

误判三:多网站菜单重复是脏数据

如果那条菜单本来就是默认模板式菜单,重复可能是预期行为。

误判四:Mega Menu 只是多点 HTML

不是。它在模型层就被定义成不能再当普通可跳转树节点使用。


九、实战里怎么用 Odoo 菜单机制更稳

1. 先区分“模板导航项”和“网站专属导航项”

多网站下这一步非常重要。

2. 不要硬拗三级、四级头部菜单

如果信息量太大,宁可做专题页或 Mega Menu 内容块,也别把树越拉越深。

3. 对需要精确高亮的入口,认真设计 URL 规则

尤其是带筛选参数、slug 和多域名场景。

4. 页面权限和菜单权限一起想

用户能不能点进去,最好在菜单层就给出正确预期。

5. 把 Mega Menu 当容器,不要当普通链接

这能少掉很多交互冲突。


结语

Odoo 的网站菜单之所以值得读,不是因为它能做树,而是因为它把“导航”真正当成了网站入口治理的一部分。

它要处理:

  • 多网站复制;
  • 层级边界;
  • 内容容器型菜单;
  • 页面可见性联动;
  • 当前上下文高亮。

所以 website.menu 真正代表的不是一排链接,而是:

网站当前可进入哪些入口、谁看得见、在什么上下文下应该被视为当前所在位置。

DISCUSSION

评论区

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