第 2 章:构建仪表盘¶
本教程的第一部分向您介绍了大多数 Owl 的概念。现在是时候全面学习 Web 客户端使用的 Odoo JavaScript 框架了。
要开始学习,您需要一个正在运行的 Odoo 服务器和开发环境设置。在进入练习之前,请确保您已遵循本 教程介绍 中描述的所有步骤。在本章中,我们将从 awesome_dashboard 插件提供的空白仪表盘开始,并逐步使用 Odoo JavaScript 框架为其添加功能。
目标
本章每个练习的解决方案托管在 官方 Odoo 教程仓库 上。
1. 新布局¶
Odoo Web 客户端中的大多数屏幕使用通用布局:顶部有一个控制面板,带有一些按钮,下方是主要内容区域。这是使用 Layout 组件 实现的,该组件在 @web/search/layout 中可用。
更新位于
awesome_dashboard/static/src/的AwesomeDashboard组件以使用Layout组件。您可以使用{controlPanel: {} }作为Layout组件的display属性。为
Layout添加一个className属性:className="'o_dashboard h-100'"添加一个
dashboard.scss文件,在其中将.o_dashboard的背景颜色设置为灰色(或您喜欢的颜色)。
打开 http://localhost:8069/web,然后打开 Awesome Dashboard 应用程序,查看结果。
理论:服务¶
实际上,每个组件(根组件除外)都可能随时被销毁并替换(或不替换)为另一个组件。这意味着每个组件的内部状态不是持久的。这在许多情况下是可以接受的,但确实存在一些我们希望保留某些数据的情况。例如,每次显示频道时都不应重新加载所有 Discuss 消息。
此外,有时我们可能需要编写一些非组件代码。例如,处理所有条形码的代码,或者管理用户配置(上下文等)的代码。
Odoo 框架定义了 服务 的概念,这是一个持久化的代码片段,用于导出状态和/或函数。每个服务可以依赖于其他服务,组件可以导入服务。
以下示例注册了一个简单的服务,该服务每 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(route, params, settings) 。一个基本的请求可能如下所示:
import { rpc } from "@web/core/network/rpc";
// ...
setup() {
onWillStart(async () => {
const result = await rpc("/my/controller", {a: 1, b: 2});
})
// ...
}
更新
Dashboard,使其使用rpc函数并调用统计路由/awesome_dashboard/statistics。在仪表盘中显示包含以下内容的几张卡片:
本月新订单数量
本月新订单总金额
本月每笔订单的平均 T 恤数量
本月取消的订单数量
订单从“新建”变为“已发送”或“已取消”的平均时间
参见
5. 缓存网络调用,创建服务¶
如果您打开浏览器开发者工具中的 Network 标签页,您会发现每次显示客户端操作时都会调用 /awesome_dashboard/statistics 。这是因为每次 Dashboard 组件挂载时都会调用 onWillStart 钩子。但在这种情况下,我们更希望只在第一次调用,因此实际上需要在 Dashboard 组件之外维护一些状态。这是一个很好的服务用例!
注册并导入一个新的
awesome_dashboard.statistics服务。它应提供一个
loadStatistics函数,该函数一旦被调用,将执行实际的 rpc,并始终返回相同的信息。使用来自
@web/core/utils/functions的 memoize 工具函数,允许缓存统计信息。在
Dashboard组件中使用此服务。检查它是否按预期工作。
6. 显示饼图¶
每个人都喜欢图表(!),所以让我们在仪表盘中添加一个饼图。它将显示每个尺寸(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. 实时更新¶
由于我们将数据加载移到了缓存中,它永远不会更新。但假设我们正在查看快速变化的数据,因此我们希望定期(例如,每 10 分钟)重新加载最新数据。
这很容易实现,只需在统计服务中使用 setTimeout 或 setInterval 。然而,棘手的部分是:如果仪表盘当前正在显示,则应立即更新。
为此,可以使用 reactive 对象:它就像 useState 返回的代理,但不链接到任何组件。然后,组件可以对其执行 useState 来订阅其更改。
更新统计服务,使其每隔 10 分钟重新加载数据(为了测试,可以改为 10 秒!)。
修改它以返回一个 响应式对象 。重新加载数据应就地更新响应式对象。
Dashboard组件现在可以通过useState使用它。
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中,以便更容易添加到资源包中。创建一个包含
/dashboard文件夹所有内容的awesome_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,以及相应的 props。创建一个文件
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. 进一步扩展¶
如果您有时间,可以尝试以下一些小改进:
确保您的应用程序可以被 翻译 (使用
env._t)。点击饼图的一个部分时,应打开具有相应尺寸的所有订单的列表视图。
将仪表盘的内容保存到服务器上的用户设置中!
使其响应式:在移动端模式下,每张卡片应占据 100% 的宽度。