1. 创建一个“你好,世界”视图

第一步是创建一个带有简单组件的 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. 确保在切换到画廊视图时可以看到你的“你好,世界”组件。

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

2. 使用 Layout 组件

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

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

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

../../../_images/layout.png

3. 解析 arch

目前,我们的画廊视图还做不了太多事情。让我们先来读取视图的 arch 中包含的信息。

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

Example

这是一个 ArchParser 可能的示例:

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

  2. 用来读取 image_field 的信息。

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

注解

可能这样做有点过度了,因为我们基本上只需要从 arch 中读取一个属性,但这种设计被其他所有 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 调用,以获取与 domain 对应的记录,并使用通过 props 接收到的 imageField

  2. 如果你在调用的上下文中没有包含 bin_size,你将收到以 base64 编码的图片字段。请确保在上下文中添加 bin_size,以便获取图片字段的大小。我们稍后会显示这张图片。

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

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

注解

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

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

5. 解决并发问题

目前,我们的代码并不具备并发安全性。如果有人两次修改领域(domain),将会触发两次 loadImages(domain)。因此会有两个请求,它们可能在不同时间到达,具体取决于不同的因素。如果在接收到第二个请求的响应之后才接收到第一个请求的响应,就会导致状态不一致。

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

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

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

  3. KeepLast 中添加 webSearchRead 调用,以便仅解析最后一次调用。

6. 重新组织代码

实际视图的组织性要强一些。在本例中这可能有些过度设计,但目的是学习如何在 Odoo 中组织代码。此外,这种方式在需求变化时也更容易扩展。

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

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

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

7. 使视图可扩展

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

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

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

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

  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. 将代码更新为允许在 arch 中添加一个可选属性:

    <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. 验证视图

到目前为止,我们已经有了一个美观且有用的视图。但在实际生活中,用户可能会在错误地编码其画廊视图的 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 包含到图片网址中。

  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 验证器修改为接受新的 arch 结构。

    小技巧

    你可以使用此 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_hookuseTooltip 钩子来显示提示信息。此钩子接收 Owl 模板和模板所需的变量作为参数。

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