1. 创建一个“Hello World”视图

第一步是使用一个简单的组件创建一个 JavaScript 实现。

  1. static/src 中创建 gallery_view.jsgallery_controller.jsgallery_controller.xml 文件。

  2. gallery_controller.js 中实现一个简单的“Hello World”组件。

  3. gallery_view.js 中,导入控制器,创建一个视图对象,并将其注册到视图注册表中,名称为 gallery

    Example

    以下是如何定义视图对象的示例:

    import { registry } from "@web/core/registry";
    import { MyController } from "./my_controller";
    
    export const myView = {
          type: "my_view",
          display_name: "MyView",
          icon: "oi oi-view-list",
          multiRecord: true,
          Controller: MyController,
    };
    
    registry.category("views").add("my_controller", myView);
    
  4. contacts.action_contacts 操作中将 gallery 添加为一种视图类型。

  5. 确保切换到画廊视图时可以看到您的“Hello World”组件。

../../../_images/view_button.png ../../../_images/new_view.png

2. 使用布局组件

到目前为止,我们的画廊视图看起来不像标准视图。让我们使用 Layout 组件,使其具有与其他视图相同的标准功能。

  1. 导入 Layout 组件并将其添加到 GalleryControllercomponents 中。

  2. 更新模板以使用 Layout。它需要一个 display 属性,可以在 props.display 中找到。

../../../_images/layout1.png

3. 解析架构

目前,我们的画廊视图功能不多。让我们从读取视图架构中包含的信息开始。

解析架构的过程通常通过特定于每个视图的 ArchParser 完成。它继承自通用的 XMLParser 类。

Example

以下是一个 ArchParser 的示例:

export class MyCustomArchParser {
    parse(xmlDoc) {
       const myAttribute = xmlDoc.getAttribute("my_attribute")
       return {
           myAttribute,
       }
    }
}
  1. 在单独的文件中创建 ArchParser 类。

  2. 使用它读取 image_field 信息。

  3. 更新 gallery 视图代码,将其添加到控制器接收到的属性中。

注解

这样做可能有些过度设计,因为我们基本上只需要从架构中读取一个属性,但这是 Odoo 其他视图普遍采用的设计,因为它可以让我们从控制器中提取一些预处理逻辑。

4. 加载一些数据

现在让我们从服务器获取一些真实的数据。为此,我们必须使用 ORM 服务中的 webSearchRead 方法。

Example

以下是一个使用 webSearchRead 从模型中获取记录的示例:

const { length, records } = this.orm.webSearchRead(this.resModel, domain, {
   specification: {
        [this.fieldToFetch]: {},
        [this.secondFieldToFetch]: {},
    },
    context: {
        bin_size: true,
    }
})
  1. GalleryController 中添加一个 loadImages(domain) {...} 方法。该方法应通过 ORM 服务执行 webSearchRead 调用以获取与域条件匹配的记录,并使用通过属性传递的 imageField

  2. 如果调用上下文中未包含 bin_size,您将接收到以 base64 编码的图像字段。请确保在上下文中添加 bin_size 以接收图像字段的大小。我们稍后会显示图像。

  3. 修改 setup 代码,在 onWillStartonWillUpdateProps 钩子中调用该方法。

  4. 修改模板,以在 Layout 组件的默认插槽中显示每个图像的 ID 和大小。

注解

加载数据的代码将在下一个练习中移动到适当的模型中。

../../../_images/gallery_data.png

5. 解决并发问题

目前,我们的代码无法保证并发安全性。如果两次更改域条件,将触发两次 loadImages(domain)。因此,这两个请求可能会因不同因素而返回不同的时间。如果第一个请求的响应在第二个请求之后到达,将导致状态不一致。

Odoo 的 KeepLast 原语解决了这个问题,它管理任务列表,并仅保持最后一个任务处于活动状态。

  1. @web/core/utils/concurrency 导入 KeepLast

  2. 在模型中实例化一个 KeepLast 对象。

  3. webSearchRead 调用添加到 KeepLast 中,以确保只有最后一次调用被解析。

6. 重组代码

实际视图的组织更加规范。在这个例子中可能有些大材小用,但目的是学习如何在 Odoo 中构建代码结构。此外,这也有助于更好地适应需求的变化。

  1. 将所有模型代码移动到其自己的 GalleryModel 类中。

  2. 将所有渲染代码移动到 GalleryRenderer 组件中。

  3. GalleryController 中导入 GalleryModelGalleryRenderer 以使其正常工作。

7. 使视图可扩展

为了扩展视图,可以导入画廊视图对象并根据需要进行修改。问题是目前无法定义自定义模型或渲染器,因为它们在控制器中是硬编码的。

  1. 在画廊视图文件中导入 GalleryModelGalleryRenderer

  2. 在画廊视图对象中添加 ModelRenderer 键,并将其分配给 GalleryModelGalleryRenderer。将 ModelRenderer 作为属性传递给控制器。

  3. 移除控制器中的硬编码导入,并从属性中获取它们。

  4. 使用 t-component 来实现动态子组件。

注解

这就是某人现在可以通过修改渲染器来扩展画廊视图的方式:

import { registry } from '@web/core/registry';
import { galleryView } from '@awesome_gallery/gallery_view';
import { GalleryRenderer } from '@awesome_gallery/gallery_renderer';

export class MyExtendedGalleryRenderer extends GalleryRenderer {
   static template = "my_module.MyExtendedGalleryRenderer";
   setup() {
      super.setup();
      console.log("my gallery renderer extension");
   }
}

registry.category("views").add("my_gallery", {
   ...galleryView,
   Renderer: MyExtendedGalleryRenderer,
});

8. 显示图像

更新渲染器,以便在字段设置时以美观的方式显示图像。如果 image_field 为空,则显示一个空白框。

小技巧

有一个控制器可以从记录中检索图像。您可以使用以下代码片段构建链接:

import { url } from "@web/core/utils/urls";
const url = url("/web/image", {
   model: resModel,
   id: image_id,
   field: imageField,
});
../../../_images/tshirt_images.png

9. 点击时切换到表单视图

更新渲染器以响应图像点击,并切换到表单视图。您可以使用操作服务中的 switchView 函数。

10. 添加可选工具提示

在鼠标悬停时显示一些额外信息是非常有用的。

  1. 更新代码以允许在架构中添加一个可选的附加属性:

    <gallery image_field="some_field" tooltip_field="some_other_field"/>
    
  2. 在鼠标悬停时,显示工具提示字段的内容。如果字段是字符字段、数字字段或多对一字段,则应该可以正常工作。要为 HTML 元素添加工具提示,可以将字符串放入元素的 data-tooltip 属性中。

  3. 更新客户画廊视图的架构,将客户添加为工具提示字段。

../../../_images/image_tooltip.png

11. 添加分页

让我们在控制面板上添加一个分页器,并像普通 Odoo 视图一样管理所有分页功能。

../../../_images/pagination.png

12. 验证视图

到目前为止,我们已经创建了一个漂亮且实用的视图。但在实际使用中,用户可能会错误地编码 Gallery 视图的 arch:目前它只是一个非结构化的 XML 片段。

让我们添加一些验证!在 Odoo 中,XML 文档可以通过 RN 文件(Relax NG 文件)进行描述,然后进行验证。

  1. 添加一个描述当前语法的 RNG 文件:

    • 一个必填属性 image_field

    • 一个可选属性:tooltip_field

  2. 添加一些代码以确保所有视图都根据此 RNG 文件进行验证。

  3. 在此过程中,让我们确保 image_fieldtooltip_field 是当前模型中的字段。

由于验证 RNG 文件并不简单,这里提供一个代码片段来帮助完成:

# -*- coding: utf-8 -*-
import logging
import os

from lxml import etree

from odoo.loglevels import ustr
from odoo.tools import misc, view_validation

_logger = logging.getLogger(__name__)

_viewname_validator = None

@view_validation.validate('viewname')
def schema_viewname(arch, **kwargs):
      """ Check the gallery view against its schema

      :type arch: etree._Element
      """
      global _viewname_validator

      if _viewname_validator is None:
         with misc.file_open(os.path.join('modulename', 'rng', 'viewname.rng')) as f:
            _viewname_validator = etree.RelaxNG(etree.parse(f))

      if _viewname_validator.validate(arch):
         return True

      for error in _viewname_validator.error_log:
         _logger.error(ustr(error))
      return False

13. 上传图像

我们的画廊视图目前不允许用户上传图像。让我们来实现这一功能。

  1. 通过使用 FileUploader 组件,在每个图像上添加一个按钮。

  2. FileUploader 组件接受 onUploaded 属性,该属性在用户上传图像时被调用。确保调用 ORM 服务中的 webSave 方法以上传新图像。

  3. 您可能注意到图像已上传,但浏览器未重新渲染它。这是因为图像链接没有改变,因此浏览器不会重新获取它们。将记录中的 write_date 包含到图像 URL 中。

  4. 确保点击上传按钮时不会触发切换视图(switchView)。

../../../_images/upload_image.png

14. 高级工具提示模板

目前我们只能指定一个工具提示字段。但如果希望允许为其编写特定模板呢?

Example

这是一个画廊架构视图的示例,应该在完成此练习后可以正常工作。

<record id="contacts_gallery_view" model="ir.ui.view">
   <field name="name">awesome_gallery.orders.gallery</field>
   <field name="model">res.partner</field>
   <field name="arch" type="xml">
      <gallery image_field="image_1920" tooltip_field="name">
         <field name="email"/> <!-- Specify to the model that email should be fetched -->
         <field name="name"/>  <!-- Specify to the model that name should be fetched -->
         <tooltip-template> <!-- Specify the owl template for the tooltip -->
            <p class="m-0">name: <field name="name"/></p> <!-- field is compiled into a t-esc-->
            <p class="m-0">e-mail: <field name="email"/></p>
         </tooltip-template>
      </gallery>
   </field>
</record>
  1. awesome_gallery/views/views.xml 中的 res.partner 画廊架构视图替换为上述示例中的架构。如果未通过 RNG 验证,请不用担心。

  2. 修改画廊的 RNG 验证器以接受新的架构结构。

    小技巧

    您可以使用此 RNG 片段来验证 tooltip-template 标签。

    <rng:define name="tooltip-template">
       <rng:element name="tooltip-template">
             <rng:zeroOrMore>
                <rng:text/>
                <rng:ref name="any"/>
             </rng:zeroOrMore>
       </rng:element>
    </rng:define>
    
    <rng:define name="any">
       <rng:element>
             <rng:anyName/>
             <rng:zeroOrMore>
                <rng:choice>
                   <rng:attribute>
                         <rng:anyName/>
                   </rng:attribute>
                   <rng:text/>
                   <rng:ref name="any"/>
                </rng:choice>
             </rng:zeroOrMore>
       </rng:element>
    </rng:define>
    
  3. 架构解析器应解析字段和工具提示模板。从 @web/core/utils/xml 导入 visitXML 并使用它来解析字段名称和工具提示模板。

  4. 确保模型在规范中包含解析后的字段名称时调用 webSearchRead

  5. 渲染器(或为其创建的任何子组件)应接收解析后的工具提示模板。操作此模板,将 <field> 元素替换为 <t t-esc="x"> 元素。

    小技巧

    模板是一个 Element 对象,因此可以像操作 HTML 元素一样对其进行操作。

  6. 通过 @odoo/owl 中的 xml 函数将模板注册到 Owl。

  7. 使用 @web/core/tooltip/tooltip_hook 中的 useTooltip 钩子显示工具提示。该钩子接受 Owl 模板和模板所需的变量作为参数。

../../../_images/advanced_tooltip.png