第 2 章:构建仪表板

本教程的第一部分向您介绍了 Owl 的大部分想法。现在是时候全面了解 Web 客户端所使用的 Odoo JavaScript 框架了。

../../../_images/previously_learned.svg

首先,您需要一个正在运行的 Odoo 服务器和一个开发环境设置。在开始练习之前,请确保您已遵循此 tutorial introduction 中描述的所有步骤。对于本章,我们将从 awesome_dashboard 插件提供的空仪表板开始。我们将使用 Odoo JavaScript 框架逐步为其添加功能。

目标

../../../_images/overview_02.png

本章每个练习的解决方案都托管在 official Odoo tutorials repository 上。

1.新的布局

Odoo Web 客户端中的大多数屏幕都使用通用布局:顶部有一个控制面板,带有一些按钮,下面是一个主要内容区域。这是使用 @web/search/layout 中提供的 Layout component 完成的。

  1. 更新位于 awesome_dashboard/static/src/ 中的 AwesomeDashboard 组件以使用 Layout 组件。您可以使用 {controlPanel: {} } 作为 Layout 组件的 display 属性。

  2. className 属性添加到 LayoutclassName="'o_dashboard h-100'"

  3. 添加一个 dashboard.scss 文件,在其中将 .o_dashboard 的背景颜色设置为灰色(或您喜欢的颜色)

打开 http://localhost:8069/web,然后打开 Awesome Dashboard 应用程序,然后查看结果。

../../../_images/new_layout.png

理论:服务

实际上,每个组件(根组​​件除外)都可以随时销毁并用另一个组件替换(或不替换)。这意味着每个组件的内部状态不是持久的。这在很多情况下都很好,但在某些情况下我们确实希望保留一些数据。例如,每次我们显示频道时,不应重新加载所有讨论消息。

另外,我们可能需要编写一些不是组件的代码。也许可以处理所有条形码,或者管理用户配置(上下文等)。

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
}

2.添加一些按钮以便快速导航

Odoo 提供的一项重要服务是 action 服务:它可以执行 Odoo 定义的所有类型的标准操作。例如,以下是一个组件如何通过其 xml id 执行操作:

import { useService } from "@web/core/utils/hooks";
...
setup() {
      this.action = useService("action");
}
openSettings() {
      this.action.doAction("base_setup.action_general_configuration");
}
...

现在让我们向控制面板添加两个按钮:

  1. 按钮 Customers,用于打开所有客户的看板视图(此操作已存在,因此您应该使用 its xml id)。

  2. 按钮 Leads,用于通过列表和表单视图在 crm.lead 模型上打开动态操作。遵循 this use of the action service 的示例。

../../../_images/navigation_buttons.png

其他资料

Code: action service

3.添加仪表板项目

现在让我们改进我们的内容。

  1. 创建一个通用 DashboardItem 组件,以漂亮的卡片布局显示其默认插槽。它应该采用可选的 size 数字道具,默认为 1。宽度应硬编码为 (18*size)rem

  2. 将两张卡添加到仪表板。一个没有大小,另一个大小为 2。

../../../_images/dashboard_item.png

其他资料

Owl’s slot system

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});
   })
   // ...
}
  1. 更新`Dashboard`,使其使用`rpc`函数并调用统计路由`/awesome_dashboard/statistics`。

  2. 在仪表板中显示一些卡片,其中包含:

    • 本月新增订单数

    • 本月新增订单总额

    • 本月 T 恤平均订单量

    • 本月取消订单数

    • 订单从“新”到“已发送”或“已取消”的平均时间

../../../_images/statistics.png

其他资料

Code: rpc

5.缓存网络调用,创建服务

如果您打开浏览器开发工具的 Network 选项卡,您将看到每次显示客户端操作时都会调用 /awesome_dashboard/statistics 。这是因为每次安装 Dashboard 组件时都会调用 onWillStart 钩子。但在这种情况下,我们宁愿只在第一次执行此操作,因此我们实际上需要在 Dashboard 组件之外维护一些状态。这是一个很好的服务用例!

  1. 注册并导入新的 awesome_dashboard.statistics 服务。

  2. 它应该提供一个函数 loadStatistics,一旦被调用,就会执行实际的 rpc,并始终返回相同的信息。

  3. 使用 @web/core/utils/functions 中的 memoize 实用函数来缓存统计信息。

  4. Dashboard 组件中使用此服务。

  5. 检查它是否按预期工作。

6. 显示饼图

每个人都喜欢图表(!),所以让我们在仪表板中添加一个饼图。它将显示每种尺码所售 T 恤的比例:S/M/L/XL/XXL。

对于本练习,我们将使用 Chart.js。它是图形视图使用的图表库。但是,默认情况下它不会加载,因此我们需要将其添加到我们的资源包中,或者延迟加载它。延迟加载通常更好,因为我们的用户不需要每次都加载 ChartJS 代码(如果他们不需要)。

  1. 创建一个 PieChart 组件。

  2. 在其`onWillStart`方法中,加载chartjs,可以使用`loadJs <https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/ addons/web/static/src/core/assets.js#L23>`_函数加载:file:/web/static/lib/Chart/Chart.js

  3. Use the PieChart component in a DashboardItem to display a pie chart that shows the quantity for each sold t-shirts in each size (that information is available in the /statistics route). Note that you can use the size property to make it look larger.

  4. PieChart 组件需要渲染画布,并使用 chart.js 在其上绘图。

  5. 让它发挥作用!

../../../_images/pie_chart.png

7.现实生活更新

由于我们将数据加载移到缓存中,因此它永远不会更新。但是假设我们正在查看快速移动的数据,因此我们希望定期(例如每 10 分钟)重新加载新数据。

这实现起来非常简单,只需在统计服务中使用 setTimeoutsetInterval 即可。然而,这里是棘手的部分:如果仪表板当前正在显示,则应立即更新。

为此,可以使用 reactive 对象:它就像 useState 返回的代理一样,但不链接到任何组件。然后,组件可以对其执行 useState 来订阅其更改。

  1. 更新统计服务以每 10 分钟重新加载一次数据(为了测试它,请使用 10 秒!)

  2. 修改它以返回 reactive 对象。重新加载数据应该更新反应对象。

  3. 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);
  1. 将所有仪表板资产移至子文件夹 /dashboard 中,以便更轻松地添加到捆绑包中。

  2. 创建一个 awesome_dashboard.dashboard 资源包,其中包含 /dashboard 文件夹的所有内容。

  3. 修改 dashboard.js 以将其自身注册到 lazy_components 注册表,而不是 actions

  4. 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 的两个高级功能:动态组件和动态道具。

我们目前有两种项目组件:带有标题和数字的数字卡,以及带有一些标签和饼图的饼图卡。

  1. 创建并实现两个组件:NumberCardPieChartCard,以及相应的 props。

  2. 创建一个文件 dashboard_items.js,在其中定义和导出项目列表,使用 NumberCardPieChartCard 重新创建我们当前的仪表板。

  3. 在我们的 Dashboard 组件中导入该项目列表,将其添加到组件中,然后更新模板以使用 t-foreach ,如上所示。

    setup() {
       this.items = items;
    }
    

现在,我们的仪表板模板是通用的!

10. 使我们的仪表板可扩展

然而,我们的项目列表的内容仍然是硬编码的。让我们通过使用注册表来解决这个问题:

  1. 不要导出列表,而是在 awesome_dashboard 注册表中注册所有仪表板项目

  2. 导入 Dashboard 组件中 awesome_dashboard 注册表的所有项目

仪表板现在可以轻松扩展。任何其他想要向仪表板注册新项目的 Odoo 插件只需将其添加到注册表即可。

11.添加和删除仪表板项目

让我们看看如何使我们的仪表板可定制。为了简单起见,我们将把用户仪表板配置保存在本地存储中,以便它是持久的,但我们现在不必处理服务器。

仪表板配置将保存为已删除项目 ID 的列表。

  1. 在控制面板中添加一个带有齿轮图标的按钮,表明它是设置按钮。

  2. 单击该按钮应打开一个对话框。

  3. 在该对话框中,我们希望看到所有现有仪表板项目的列表,每个项目都有一个复选框。

  4. 页脚中应该有一个 Apply 按钮。单击它将构建所有未选中的项目 ID 的列表。

  5. 我们希望将该值存储在本地存储中。

  6. 并修改 Dashboard 组件以通过从配置中删除项目的 id 来过滤当前项目。

../../../_images/items_configuration.png

12. 更进一步

如果您有时间,可以尝试以下一些小改进:

  1. 确保您的申请可以是 translated <reference/translations>`(带有 `env._t)。

  2. 单击饼图的一部分应打开具有相应大小的所有订单的列表视图。

  3. 将仪表板的内容保存在服务器上的用户设置中!

  4. 使其响应式:在移动模式下,每张卡片应占据 100% 的宽度。