第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'"
添加一个
dashboard.scss
文件,在其中将.o_dashboard
的 background-color 设置为灰色(或您喜欢的颜色)
打开 http://localhost:8069/web,然后打开 Awesome Dashboard 应用,查看结果。
另请参阅
理论:服务¶
在实践中,每个组件(除了根组件)可能会在任何时候被销毁,并被另一个组件替换(或不被替换)。这意味着每个组件的内部状态不是持久的。这在很多情况下是没问题的,但肯定有一些情况下我们希望保留一些数据。例如,所有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. Add a dashboard item¶
让我们现在改进我们的内容。
创建一个通用的
DashboardItem
组件,以美观的卡片布局显示其默认插槽。它应接受一个可选的size
数字属性,默认值为1
。宽度应硬编码为(18*size)rem
。向仪表板添加两张卡片。一张没有大小,另一张大小为 2。
另请参阅
4. Call the server, add some statistics¶
让我们通过添加一些仪表板项目来改进仪表板,以显示 真实 的业务数据。awesome_dashboard
插件提供了一个 /awesome_dashboard/statistics
路由,旨在返回一些有趣的信息。
要调用特定的控制器,我们需要使用 rpc 服务。它只导出一个执行请求的函数:rpc(route, params, settings)
。一个基本的请求可能如下所示:
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. Cache network calls, create a service¶
如果你打开浏览器开发者工具的 Network 标签页,你会看到每次显示客户端操作时都会调用 /awesome_dashboard/statistics
。这是因为每次挂载 Dashboard
组件时都会调用 onWillStart
钩子。但在这种情况下,我们希望只在第一次时执行此操作,因此实际上需要在 Dashboard
组件之外维护一些状态。这是一个非常适合使用服务的用例!
注册并导入一个新的
awesome_dashboard.statistics
服务。它应该提供一个名为
loadStatistics
的函数,一旦调用,就执行实际的rpc,并始终返回相同的信息。使用 memoize 实用函数,来自
@web/core/utils/functions
,它允许缓存统计信息。在
Dashboard
组件中使用此服务。检查其是否按预期工作。
另请参阅
6. Display a pie chart¶
每个人都喜欢图表(!),所以让我们在我们的仪表板中添加一个饼图。它将显示每个尺码(S/M/L/XL/XXL)销售的T恤比例。
对于这个练习,我们将使用 Chart.js。它是图表视图使用的图表库。然而,默认情况下它不会被加载,所以我们需要将它添加到我们的资源包中,或者进行懒加载。懒加载通常更好,因为我们的用户如果不需要它,就不必每次都加载 chartjs 代码。
创建一个
PieChart
组件。在其
onWillStart
方法中,加载 chartjs,你可以使用 loadJs 函数来加载/web/static/lib/Chart/Chart.js
。在
DashboardItem
中使用PieChart
组件来显示一个 饼图 ,该饼图展示了每种尺寸的 T 恤销售数量(该信息可在/statistics
路由中获取)。注意,你可以使用size
属性使其看起来更大。PieChart
组件需要渲染一个画布,并使用chart.js
在上面绘制。让它工作起来!
另请参阅
7. Real life update¶
由于我们将数据加载移入了缓存,它不再更新。但假设我们正在查看快速变化的数据,因此我们希望定期(例如,每10分钟)重新加载最新数据。
这实现起来相当简单,只需在统计服务中使用 setTimeout
或 setInterval
。然而,这里有一个棘手的地方:如果仪表板当前正在显示,它应该立即更新。
为此,可以使用一个 reactive
对象:它就像 useState
返回的代理一样,但不与任何组件绑定。然后,组件可以对其执行 useState
以订阅其变化。
更新统计服务以每10分钟重新加载数据(为了测试,可以使用10秒代替!)
修改它以返回一个 reactive 对象。重新加载数据应就地更新该响应式对象。
Dashboard
组件现在可以通过useState
使用它
8. Lazy loading the dashboard¶
让我们想象一下,我们的仪表盘变得相当大,并且只对部分用户感兴趣。在这种情况下,懒加载我们的仪表盘及其所有相关资源可能是有意义的,这样我们只在真正需要查看时才承担加载代码的成本。
一种实现方式是使用 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. Making our dashboard generic¶
到目前为止,我们有一个不错的工作仪表板。但它目前是硬编码在仪表板模板中的。如果我们想自定义我们的仪表板呢?也许有些用户有不同的需求,想看到其他数据。
因此,下一步是使我们的仪表板通用化:与其在模板中硬编码其内容,不如让它遍历一个仪表板项目列表。但随之而来的是许多问题:如何表示一个仪表板项目,如何注册它,它应该接收什么数据等等。设计这样的系统有许多不同的方法,每种方法都有不同的权衡。
在本教程中,我们将说仪表板项是具有以下结构的对象:
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. Making our dashboard extensible¶
然而,我们的项目列表内容仍然是硬编码的。让我们通过使用注册表来解决这个问题:
不要导出列表,而是将所有仪表板项注册到一个
awesome_dashboard
注册表中将
Dashboard
组件中的awesome_dashboard
注册表的所有项导入
仪表板现在易于扩展。任何其他想要向仪表板注册新项目的 Odoo 插件只需将其添加到注册表中即可。
11. Add and remove dashboard items¶
让我们看看如何使我们的仪表板可定制。为了简化操作,我们将用户仪表板配置保存在本地存储中,以便其持久化,但目前我们不需要处理服务器。
仪表板配置将保存为已移除项目ID的列表。
在控制面板中添加一个带有齿轮图标的按钮,以指示这是一个设置按钮。
点击该按钮应会打开一个对话框。
在该对话框中,我们希望看到所有现有仪表板项目的列表,每个项目都带有一个复选框。
页脚处应有一个
Apply
按钮。点击它将构建一个包含所有未选中项 id 的列表。我们希望将该值存储在本地存储中。
并修改
Dashboard
组件,通过从配置中移除项目的 ID 来过滤当前项目。
12. Going further¶
以下是一些小的改进建议,如果您有时间可以尝试一下:
确保你的应用程序可以通过
env._t
进行 翻译。点击饼图的某个部分应打开具有相应尺寸的所有订单的列表视图。
将仪表板的内容保存在服务器上的用户设置中!
使其响应式:在移动模式下,每张卡片应占据 100% 的宽度。
另请参阅
需要翻译的内容是: