笔者在工作中遇到一个比较偏门的问题,需要在不同的应用中运用一套不同的css,对于平常的list/tree、from视图我们可以在xml加style,对于菜单我专门写了这个博客找到比较固定的锚点,
对于依照这个锚点进行css的隔离本次暂不做讨论,后续可能会出一篇专门的博客
1. 引言
企业资源规划(ERP)系统的用户界面(UI)设计直接关系到用户的操作效率与体验。Odoo 作为全球领先的开源 ERP 平台,其前端架构在 Odoo 18 中已高度成熟,完全基于 Odoo Web Library (Owl) 框架运行。
本报告旨在深入探讨 Odoo 18 前端架构的核心机制,特别是针对 Web Client 导航栏(Navbar) 的实现原理。报告将解决一个具体的业务需求:如何在保持其他模块标准样式的同时,仅在"销售(Sales)"应用下实现 Navbar 的差异化样式定制。
基于最新的源码审计(Source Code Audit),本报告将确凿地展示如何利用 Owl 组件系统、Menu 服务数据结构以及 XML 模板继承机制来实现这一目标。
2. Odoo 18 前端架构基础:Owl 与 Web Client
要理解如何定制 Navbar,首先必须理解承载它的"土壤"------Odoo 18 的前端运行环境。
2.1 Owl 框架的核心设计哲学
Owl (Odoo Web Library) 是 Odoo 自研的 UI 框架,核心特性包括:
- 声明式 UI (Declarative UI): 开发者描述状态(State)与 UI 的映射关系,而非手动操作 DOM。
- 组件化 (Component-Based): Navbar 是一个拥有生命周期、状态和子组件的 Owl 类(Class)。
- 响应式系统 (Reactivity): 当组件依赖的数据(如
currentApp)发生变化时,Owl 自动触发重新渲染。
2.2 Web Client 的宏观架构
在 Odoo 18 中,WebClient 是根组件。根据 webclient/navbar/navbar.js 源码,其结构如下:
javascript
// webclient/navbar/navbar.js
export class NavBar extends Component {
static template = "web.NavBar";
// ...
setup() {
this.menuService = useService("menu"); // 核心:依赖注入菜单服务
// ...
}
}
2.3 关键服务:MenuService
MenuService 是导航系统的核心。它负责维护当前用户的菜单树和活动应用状态。我们的定制方案完全依赖于该服务提供的数据准确性。
3. 源码级证据:数据源确认
在进行开发之前,我们对 Odoo 18 的核心源码进行了审计,以确认**外部标识符(XML ID)**在前端的可用性。这是确保方案稳定性的关键。
3.1 菜单数据的构建 (menu_helpers.js)
通过审查 addons/web/static/src/webclient/menus/menu_helpers.js,我们发现了前端菜单数据对象的构建逻辑:
javascript
// menu_helpers.js - computeAppsAndMenuItems 函数
const item = {
// ...
id: menuItem.id, // 数据库 ID (不稳定)
xmlid: menuItem.xmlid, // 外部标识符 (稳定,关键证据!) 👈
actionID: menuItem.actionID,
// ...
};
结论: Odoo 18 的前端明确保留了 xmlid 字段。这意味着我们不需要后端扩展,也不需要通过图标路径进行模糊猜测,直接使用 xmlid 是最安全、最标准的做法。
3.2 导航栏模板结构 (navbar.xml)
审查 addons/web/static/src/webclient/navbar/navbar.xml,我们锁定了修改目标:
xml
<!-- navbar.xml -->
<t t-name="web.NavBar">
<header class="o_navbar" t-ref="root">
<!-- 我们的目标:给这个 nav 标签增加动态 class -->
<nav class="o_main_navbar d-print-none" ... >
<!-- ... -->
</nav>
</header>
</t>
4. 实现方案:基于 Owl Patch 的精准组件增强
鉴于源码分析的结果,我们采用 Owl Patch 方案。此方案利用 Odoo 的 patch 工具函数扩展 NavBar 组件,将"销售应用模式"的状态注入到 HTML 类名中。
4.1 方案核心逻辑
- JS Patch: 扩展
NavBar类,添加一个 getter,判断当前应用的xmlid是否为sale.sale_menu_root。 - XML Patch: 利用 XPath 找到
<nav>标签,绑定上述 getter 返回的 class。 - SCSS: 针对该 class 编写特定样式。
4.2 代码实现详解
第一步:JS Patch (navbar_patch.js)
我们利用 patch 函数扩展原型。注意,原生 NavBar 组件(navbar.js 第 80 行)已经定义了 get currentApp(),我们可以直接复用。
javascript
/** @odoo-module */
import { NavBar } from "@web/webclient/navbar/navbar";
import { patch } from "@web/core/utils/patch";
patch(NavBar.prototype, {
/**
* 计算属性:根据当前应用返回特定的 CSS 类名
* 销售应用的 XML ID 固定为: sale.sale_menu_root
*/
get currentAppModeClass() {
// 直接调用原生的 currentApp getter
const app = this.currentApp;
// 严谨判断:app 是否存在,且其 xmlid 是否匹配
if (app && app.xmlid === 'sale.sale_menu_root') {
return 'o_navbar_sales_app_mode';
}
return '';
}
});
第二步:XML Patch (navbar_patch.xml)
我们需要将 CSS 类注入到 navbar.xml 中定义的 <nav class="o_main_navbar ..."> 元素上。
xml
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="theme_sales_navbar.NavBar" t-inherit="web.NavBar" t-inherit-mode="extension">
<!-- 精准定位到含有 o_main_navbar 类的 nav 标签 -->
<xpath expr="//nav[hasclass('o_main_navbar')]" position="attributes">
<attribute name="t-attf-class">
{{ currentAppModeClass }}
</attribute>
</xpath>
</t>
</templates>
第三步:SCSS 样式定制 (navbar_sales.scss)
现在,只有在销售应用下,导航栏才会拥有 o_navbar_sales_app_mode 类。
scss
/* 销售应用专属导航栏样式 */
.o_main_navbar.o_navbar_sales_app_mode {
/* 强制覆盖背景色为销售红/紫色 */
background-color: #875A7B !important;
border-bottom: 2px solid #a26d93;
/* 修改品牌文字颜色 */
.o_menu_brand {
color: #ffffff !important;
font-weight: bold;
}
/* 修改菜单项 hover 效果 */
.o_nav_entry:hover, .o_menu_brand:hover {
background-color: rgba(0, 0, 0, 0.1) !important;
}
}
5. 为什么这是最佳实践?
- 零闪烁 (No Flash of Unstyled Content): Owl 的虚拟 DOM 机制保证了 CSS 类是在渲染前计算好的。当浏览器绘制第一帧时,样式已经生效,用户体验极其流畅。
- 数据源可靠: 我们直接使用了
menu_helpers.js确认存在的xmlid字段,避免了使用图标路径(web_icon)进行猜测的脆弱性。 - 状态驱动: 不需要编写复杂的事件监听器去监听点击事件。只要
menuService的状态改变,Navbar 会自动重算currentAppModeClass,离开销售应用时,样式自动复原,不会污染其他模块。
6. 模块结构与部署
6.1 目录结构
text
theme_sales_navbar/
├── __init__.py
├── __manifest__.py
└── static/
└── src/
├── js/
│ └── navbar_patch.js
├── xml/
│ └── navbar_patch.xml
└── scss/
└── navbar_sales.scss
6.2 Manifest 配置
确保资源文件被加载到 web.assets_backend 包中。
python
{
'name': 'Sales Navbar Theme',
'version': '18.0.1.0.0',
'category': 'Hidden',
'depends': ['web', 'sale'], # 依赖 sale 模块确保 xmlid 存在
'assets': {
'web.assets_backend': [
'theme_sales_navbar/static/src/scss/navbar_sales.scss',
'theme_sales_navbar/static/src/js/navbar_patch.js',
'theme_sales_navbar/static/src/xml/navbar_patch.xml',
],
},
'installable': True,
}
7. 结论
通过对 Odoo 18 源码的深入剖析,我们证实了前端 currentApp 对象中包含稳定的 xmlid 属性。基于此发现,我们构建了一个无需后端代码介入、完全基于前端 Owl Patch 的轻量级解决方案。
该方案利用了 NavBar.prototype 的扩展能力和 QWeb 模板的动态属性绑定,精准地在"销售"应用激活时注入特定 CSS 类。这不仅满足了差异化视觉定制的需求,也展示了 Odoo 18 现代前端架构的灵活性与健壮性。对于需要根据应用上下文改变 UI 表现的场景,这是一种标准且高效的设计模式。