第 2 章:构建仪表板¶
本教程的第一部分向您介绍了 Owl 的大部分理念。现在,是时候全面了解 Odoo JavaScript 框架了,这是 Web 客户端所使用的框架。
要开始,请确保你有一个正在运行的 Odoo 服务器和一个开发环境。在进入练习之前,请确保你已经按照此 教程简介 中描述的所有步骤进行了操作。在本章中,我们将从 awesome_dashboard
插件提供的空仪表板开始,逐步为其添加功能,使用 Odoo JavaScript 框架。
目标

每个章节练习的解决方案都托管在 官方 Odoo 教程仓库 中。
1. 新的布局¶
大多数 Odoo 网页客户端屏幕使用相同的布局:顶部有一个控制面板,包含一些按钮,下方是主要的内容区域。这是通过 Layout 组件 实现的,位于 @web/search/layout
中。
将位于
awesome_dashboard/static/src/
的AwesomeDashboard
组件更新为使用Layout
组件。可以使用{controlPanel: {} }
作为Layout
组件的display
属性。向
Layout
添加className
属性:className="'o_dashboard h-100'"
在其中设置
.o_dashboard
的背景颜色为灰色(或你喜爱的颜色)的dashboard.scss
文件中添加一个。
打开 http://localhost:8069/web,然后打开 Awesome Dashboard 应用,并查看结果。

另请参见
示例:在客户端动作中的 Layout 使用 <https://github.com/odoo/odoo/blob/18.0/addons/web/static/src/webclient/actions/reports/report_action.js>`_ 以及 模板
理论:服务¶
在实际应用中,每个组件(根组件除外)可能在任何时候被销毁,并可能被另一个组件替换(也可能不被替换)。这意味着每个组件的内部状态并不是持久化的。在许多情况下这没有问题,但当然也有一些场景下我们希望保留一些数据。例如,所有 Discuss 消息不应该在每次显示渠道时都被重新加载。
此外,也有可能需要编写一些不是组件的代码。例如,处理所有条形码的代码,或者管理用户配置(上下文等)的代码。
Odoo 框架定义了“服务”(service)的概念,它是一段持久化的代码,用于导出状态和/或函数。每个服务都可以依赖其他服务,而组件可以导入服务。
以下示例注册了一个简单的服务,该服务每 5 秒显示一次通知:
import { registry } from "@web/core/registry";
const myService = {
dependencies: ["notification"],
start(env, { notification }) {
let counter = 1;
setInterval(() => {
notification.add(`Tick Tock ${counter++}`);
}, 5000);
},
};
registry.category("services").add("myService", myService);
服务可以被任何组件访问。设想我们有一个用于维护某些共享状态的服务:
import { registry } from "@web/core/registry";
const sharedStateService = {
start(env) {
let state = {};
return {
getValue(key) {
return state[key];
},
setValue(key, value) {
state[key] = value;
},
};
},
};
registry.category("services").add("shared_state", sharedStateService);
然后,任何组件都可以这样做:
import { useService } from "@web/core/utils/hooks";
setup() {
this.sharedState = useService("shared_state");
const value = this.sharedState.getValue("somekey");
// do something with value
}
3. 添加仪表板项¶
现在让我们来改进我们的内容。
创建一个通用的
DashboardItem
组件,用于在美观的卡片布局中显示其默认插槽。它应该接受一个可选的size
数字属性,其默认值为1
。宽度应硬编码为(18*size)rem
。在仪表板中添加两个卡片。一个不设置大小,另一个设置大小为 2。

另请参见
4. 调用服务器,添加一些统计数据¶
让我们通过添加一些仪表板项来改进仪表板,以显示*真实*的业务数据。awesome_dashboard
插件提供了一个 /awesome_dashboard/statistics
路线,该路线旨在返回一些有趣的信息。
要调用特定的控制器,我们需要使用 RPC 服务。它仅导出一个执行请求的函数:rpc(路线, 参数, 设置)
。一个基本的请求可能如下所示:
setup() {
this.rpc = useService("rpc");
onWillStart(async () => {
const result = await this.rpc("/my/controller", {a: 1, b: 2});
// ...
});
}
将
Dashboard
更新为使用rpc
服务。在
onWillStart
钩子中调用统计路线/awesome_dashboard/statistics
。在仪表板中显示几个包含以下内容的卡片:
本月新订单数量
本月新订单的总金额
本月每单的 T 恤平均数量
本月已取消的订单数量
从“新建”状态到“已发送”或“已取消”的平均时间

5. 缓存网络调用,创建一个服务¶
如果你打开浏览器开发者工具中的 网络 选项卡,你将看到每次显示客户端动作时都会调用 /awesome_dashboard/statistics
。这是因为 onWillStart
钩子会在每次 Dashboard
组件挂载时被调用。但在这种情况下,我们希望只在第一次调用,因此实际上需要在 Dashboard
组件之外维护一些状态。这是一个使用服务的绝佳场景!
注册并导入一个新的
awesome_dashboard.statistics
服务。它应该提供一个函数
loadStatistics
,一旦被调用,就会执行实际的 rpc,并始终返回相同的信息。使用 memoize 工具函数,该函数来自
@web/core/utils/functions
,可用于缓存统计信息。在
仪表板
组件中使用此服务。检查其是否按预期工作。
另请参见
6. 显示饼图¶
每个人都喜欢图表(!),所以让我们在仪表板中添加一个饼图。它将显示各尺寸(S/M/L/XL/XXL)的T恤销售比例。
对于这个练习,我们将使用 Chart.js。这是图表视图中使用的图表库。然而,默认情况下它不会被加载,因此我们需要将其添加到我们的资源包中,或者按需加载。按需加载通常更好,因为如果用户不需要它,他们就不必每次加载 Chart.js 的代码。
创建一个
PieChart
组件。在
onWillStart
方法中,加载 chartjs,你可以使用 loadJs 函数来加载/web/static/lib/Chart/Chart.js
。在
DashboardItem
中使用PieChart
组件来显示一个 饼图,该图表展示每种尺寸售出的 T 恤数量(该信息可在/statistics
路线中获取)。请注意,您可以使用size
属性使其看起来更大。PieChart
组件需要渲染一个画布,并使用chart.js
在其上进行绘制。让它运行起来!

另请参见
示例:懒加载一个 js 文件 <https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/ addons/web/static/src/views/graph/graph_renderer.js#L57>`_
示例:在组件中渲染图表 <https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/ addons/web/static/src/views/graph/graph_renderer.js#L618>`_
7. 实际更新¶
由于我们将数据加载移到了缓存中,它永远不会更新。但假设我们正在查看的是快速变化的数据,因此我们希望定期(例如每10分钟)重新加载最新的数据。
这实现起来相当简单,在统计服务中使用 setTimeout
或 setInterval
即可。然而,这里有一个关键点:如果当前正在显示仪表板,则应立即进行更新。
要实现这一点,可以使用一个 reactive
对象:它就像 useState
返回的代理一样,但不与任何组件相关联。然后,一个组件可以对其使用 useState
来订阅其变化。
将统计服务更新为每 10 分钟重新加载数据(为了测试,使用 10s 替代!)
将其修改为返回一个 reactive 对象。重新加载数据时,应就地更新该 reactive 对象。
仪表板
组件现在可以使用useState
另请参见
示例:在服务中使用 reactive <https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/ addons/web/static/src/core/debug/profiling/profiling_service.js#L30>`_
8. 仪表板的延迟加载¶
假设我们的仪表板变得越来越大,只对部分用户有吸引力。在这种情况下,延迟加载我们的仪表板以及所有相关资源可能是有意义的,这样我们只有在真正需要查看时才承担代码加载的成本。
一种实现方式是使用 LazyComponent`(来自 `@web/core/assets
)作为中间组件,在显示我们的组件之前加载一个资产包。
Example
example_action.js
export class ExampleComponentLoader extends Component {
static components = { LazyComponent };
static template = xml`
<LazyComponent bundle="'example_module.example_assets'" Component="'ExampleComponent'" />
`;
}
registry.category("actions").add("example_module.example_action", ExampleComponentLoader);
将所有仪表板资源移动到子文件夹
/dashboard
中,以便更方便地添加到一个包中。创建一个
awesome_dashboard.dashboard
资产包,包含/dashboard
文件夹中的所有内容。修改
dashboard.js
以将其注册到lazy_components
注册表,而不是actions
。在
src/dashboard_action.js
文件中,创建一个中间组件,该组件使用LazyComponent
并将其注册到actions
注册表中。
9. 使我们的仪表板通用化¶
到目前为止,我们已经有了一个功能良好的仪表板。但目前它在仪表板模板中是硬编码的。如果我们想要自定义仪表板呢?也许一些用户有不同的需求,希望看到其他数据。
因此,下一步是使我们的仪表板通用化:不再在模板中硬编码其内容,而是可以遍历一个仪表板项的列表。但随之而来的是许多问题:如何表示一个仪表板项,如何注册它,它应该接收哪些数据,等等。设计这样的系统有许多不同的方法,每种方法都有其权衡之处。
对于本教程,我们将假设一个仪表板项是一个具有以下结构的对象:
const item = {
id: "average_quantity",
description: "Average amount of t-shirt",
Component: StandardItem,
// size and props are optionals
size: 3,
props: (data) => ({
title: "Average amount of t-shirt by order this month",
value: data.average_quantity
}),
};
description
值将在后续练习中用于显示用户可以添加到其仪表板中的项目名称。size
数字是可选的,仅用于描述将要显示的仪表板项的大小。最后,props
函数也是可选的。如果没有提供,我们将直接将 statistics
对象作为数据传递。但如果已定义,则会用于计算组件的特定属性。
目标是用以下代码片段替换仪表板的内容:
<t t-foreach="items" t-as="item" t-key="item.id">
<DashboardItem size="item.size || 1">
<t t-set="itemProp" t-value="item.props ? item.props(statistics) : {'data': statistics}"/>
<t t-component="item.Component" t-props="itemProp" />
</DashboardItem>
</t>
请注意,上面的示例展示了 Owl 的两个高级功能:动态组件和动态属性。
我们目前有两种类型的项目组件:带有标题和数字的数字卡片,以及带有某些标签和饼图的饼图卡片。
创建并实现两个组件:
NumberCard
和PieChartCard
,并对应添加属性。在文件
dashboard_items.js
中创建一个文件,其中定义并导出一个项目列表,使用NumberCard
和PieChartCard
来重新创建我们当前的仪表板。将该物品列表导入我们的
Dashboard
组件,将其添加到组件中,并更新模板以使用如上所示的t-foreach
。setup() { this.items = items; }
现在,我们的仪表板模板是通用的!
10. 使我们的仪表板可扩展¶
然而,我们项目列表的内容仍然是硬编码的。让我们通过使用注册表来修复这个问题:
与其导出一个列表,不如将所有仪表板项注册到
awesome_dashboard
注册表中在
Dashboard
组件中导入awesome_dashboard
注册表的所有项
仪表板现在可以轻松扩展。任何其他想要向仪表板注册新条目的 Odoo 插件,只需将其添加到注册表中即可。
11. 添加和移除仪表板项¶
让我们看看如何使我们的仪表板可自定义。为了简单起见,我们将用户仪表板的配置保存在本地存储中,以便其保持持久化,但目前我们不需要处理服务器。
仪表板配置将保存为已移除项ID的列表。
在控制面板中添加一个带有齿轮图标的按钮,以表明这是一个设置按钮。
点击该按钮应打开一个对话框。
在该对话框中,我们希望看到所有现有仪表板项的列表,每个项前都有一个复选框。
在页脚应该有一个
应用
按钮。单击它将生成一个包含所有未选中项 ID 的列表。我们想将该值存储在本地存储中。
并修改
Dashboard
组件,通过移除配置中的项目 ID 来筛选当前项目。

12. 更进一步¶
以下是一些你可以尝试进行的小改进,如果你有时间的话:
确保你的应用可以被 翻译 <reference/translations>`(使用 `env._t)。
点击饼图的某个部分应打开所有具有相应尺寸的订单的列表视图。
将仪表板的内容保存到服务器上的用户设置中!
使其响应式:在移动模式下,每个卡片应占据 100% 的宽度。
另请参见
示例:使用 env._t 函数 <https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/ addons/account/static/src/components/bills_upload/bills_upload.js#L64>`_
代码:web 中的翻译代码 <https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/ addons/web/static/src/core/l10n/translation.js#L16>`_