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. gallery 添加为 contacts.action_contacts 操作中的一种视图类型。

  5. 确保在切换到图库视图时,您可以看到您的hello world组件。

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

2. 使用布局组件

到目前为止,我们的图库视图看起来不像是一个标准视图。让我们使用 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 调用,以获取与域相对应的记录 并使用在 props 中接收到的 imageField

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

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

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

注解

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

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

5. Solve the concurrency problem

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

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

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

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

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

6. Reorganize code

真正的视图会更加有组织。在这个例子中可能有些过度,但这是为了学习如何在Odoo中构建代码结构。此外,这将更好地适应不断变化的需求。

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

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

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

7. Make the view extensible

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

  1. 在 gallery 视图文件中导入 GalleryModelGalleryRenderer

  2. 向图库视图对象添加 ModelRenderer 键,并将它们分配给 GalleryModelGalleryRenderer。将 ModelRenderer 作为 props 传递给控制器。

  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. Display images

更新渲染器以便以美观的方式显示图像,如果字段已设置。如果 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. Switch to form view on click

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

10. Add an optional tooltip

鼠标悬停时提供一些额外的信息是有用的。

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

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

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

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

11. Add pagination

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

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

12. Validating views

到目前为止,我们已经拥有了一个漂亮且有用的视图。但在实际应用中,我们可能会遇到用户错误编码其图库视图的 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. Uploading an image

我们的画廊视图不允许用户上传图片。让我们来实现这个功能。

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

  2. FileUploader 组件接受 onUploaded 属性,当用户上传图片时会被调用。确保从 orm 服务调用 webSave 以上传新图片。

  3. 你可能已经注意到图片已上传,但浏览器并未重新渲染。这是因为图片链接未改变,所以浏览器不会重新获取它们。请将记录中的 write_date 包含到图片的URL中。

  4. 确保点击上传按钮不会触发 switchView。

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

14. Advanced tooltip template

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

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 gallery arch 视图替换为上例中的 arch。即使它没有通过 rng 验证,也不必担心。

  2. 修改 gallery 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_hook 中的 useTooltip 钩子来显示工具提示。此钩子将 Owl 模板和模板所需的变量作为参数。

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