第 2 章:创建画廊视图¶
让我们看看如何从零开始创建一个新视图。从某种程度上来说,这并不难,但目前还没有很好的资源来指导如何实现。需要注意的是,大多数情况下应该通过自定义现有视图或使用客户端操作来解决问题。
在本次练习中,我们假设要创建一个 gallery
(画廊)视图,该视图允许我们用图像字段表示一组记录。
这个问题当然可以通过看板视图解决,但这意味着无法在同一操作中同时拥有普通的看板视图和画廊视图。
让我们创建一个画廊视图。每个画廊视图将由其架构中的 image_field
属性定义:
<gallery image_field="some_field"/>
为了完成本章的任务,您需要安装 awesome_gallery 插件。该插件包含添加新视图所需的服务器文件。
目标

本章每个练习的解决方案托管在 官方 Odoo 教程仓库 中。
1. 创建一个“Hello World”视图¶
第一步是使用一个简单的组件创建一个 JavaScript 实现。
在
static/src
中创建gallery_view.js
、gallery_controller.js
和gallery_controller.xml
文件。在
gallery_controller.js
中实现一个简单的“Hello World”组件。在
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);
在
contacts.action_contacts
操作中将gallery
添加为一种视图类型。确保切换到画廊视图时可以看到您的“Hello World”组件。


2. 使用布局组件¶
到目前为止,我们的画廊视图看起来不像标准视图。让我们使用 Layout
组件,使其具有与其他视图相同的标准功能。
导入
Layout
组件并将其添加到GalleryController
的components
中。更新模板以使用
Layout
。它需要一个display
属性,可以在props.display
中找到。

3. 解析架构¶
目前,我们的画廊视图功能不多。让我们从读取视图架构中包含的信息开始。
解析架构的过程通常通过特定于每个视图的 ArchParser
完成。它继承自通用的 XMLParser
类。
Example
以下是一个 ArchParser 的示例:
export class MyCustomArchParser {
parse(xmlDoc) {
const myAttribute = xmlDoc.getAttribute("my_attribute")
return {
myAttribute,
}
}
}
在单独的文件中创建
ArchParser
类。使用它读取
image_field
信息。更新
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,
}
})
在
GalleryController
中添加一个loadImages(domain) {...}
方法。该方法应通过 ORM 服务执行webSearchRead
调用以获取与域条件匹配的记录,并使用通过属性传递的imageField
。如果调用上下文中未包含
bin_size
,您将接收到以 base64 编码的图像字段。请确保在上下文中添加bin_size
以接收图像字段的大小。我们稍后会显示图像。修改
setup
代码,在onWillStart
和onWillUpdateProps
钩子中调用该方法。修改模板,以在
Layout
组件的默认插槽中显示每个图像的 ID 和大小。
注解
加载数据的代码将在下一个练习中移动到适当的模型中。

5. 解决并发问题¶
目前,我们的代码无法保证并发安全性。如果两次更改域条件,将触发两次 loadImages(domain)
。因此,这两个请求可能会因不同因素而返回不同的时间。如果第一个请求的响应在第二个请求之后到达,将导致状态不一致。
Odoo 的 KeepLast
原语解决了这个问题,它管理任务列表,并仅保持最后一个任务处于活动状态。
从
@web/core/utils/concurrency
导入KeepLast
。在模型中实例化一个
KeepLast
对象。将
webSearchRead
调用添加到KeepLast
中,以确保只有最后一次调用被解析。
6. 重组代码¶
实际视图的组织更加规范。在这个例子中可能有些大材小用,但目的是学习如何在 Odoo 中构建代码结构。此外,这也有助于更好地适应需求的变化。
将所有模型代码移动到其自己的
GalleryModel
类中。将所有渲染代码移动到
GalleryRenderer
组件中。在
GalleryController
中导入GalleryModel
和GalleryRenderer
以使其正常工作。
7. 使视图可扩展¶
为了扩展视图,可以导入画廊视图对象并根据需要进行修改。问题是目前无法定义自定义模型或渲染器,因为它们在控制器中是硬编码的。
在画廊视图文件中导入
GalleryModel
和GalleryRenderer
。在画廊视图对象中添加
Model
和Renderer
键,并将其分配给GalleryModel
和GalleryRenderer
。将Model
和Renderer
作为属性传递给控制器。移除控制器中的硬编码导入,并从属性中获取它们。
使用 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,
});

9. 点击时切换到表单视图¶
更新渲染器以响应图像点击,并切换到表单视图。您可以使用操作服务中的 switchView
函数。
10. 添加可选工具提示¶
在鼠标悬停时显示一些额外信息是非常有用的。
更新代码以允许在架构中添加一个可选的附加属性:
<gallery image_field="some_field" tooltip_field="some_other_field"/>
在鼠标悬停时,显示工具提示字段的内容。如果字段是字符字段、数字字段或多对一字段,则应该可以正常工作。要为 HTML 元素添加工具提示,可以将字符串放入元素的
data-tooltip
属性中。更新客户画廊视图的架构,将客户添加为工具提示字段。

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

12. 验证视图¶
到目前为止,我们已经创建了一个漂亮且实用的视图。但在实际使用中,用户可能会错误地编码 Gallery 视图的 arch
:目前它只是一个非结构化的 XML 片段。
让我们添加一些验证!在 Odoo 中,XML 文档可以通过 RN 文件(Relax NG 文件)进行描述,然后进行验证。
添加一个描述当前语法的 RNG 文件:
一个必填属性
image_field
。一个可选属性:
tooltip_field
。
添加一些代码以确保所有视图都根据此 RNG 文件进行验证。
在此过程中,让我们确保
image_field
和tooltip_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. 上传图像¶
我们的画廊视图目前不允许用户上传图像。让我们来实现这一功能。
通过使用
FileUploader
组件,在每个图像上添加一个按钮。FileUploader
组件接受onUploaded
属性,该属性在用户上传图像时被调用。确保调用 ORM 服务中的webSave
方法以上传新图像。您可能注意到图像已上传,但浏览器未重新渲染它。这是因为图像链接没有改变,因此浏览器不会重新获取它们。将记录中的
write_date
包含到图像 URL 中。确保点击上传按钮时不会触发切换视图(switchView)。

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>
将
awesome_gallery/views/views.xml
中的res.partner
画廊架构视图替换为上述示例中的架构。如果未通过 RNG 验证,请不用担心。修改画廊的 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>
架构解析器应解析字段和工具提示模板。从
@web/core/utils/xml
导入visitXML
并使用它来解析字段名称和工具提示模板。确保模型在规范中包含解析后的字段名称时调用
webSearchRead
。渲染器(或为其创建的任何子组件)应接收解析后的工具提示模板。操作此模板,将
<field>
元素替换为<t t-esc="x">
元素。小技巧
模板是一个
Element
对象,因此可以像操作 HTML 元素一样对其进行操作。通过
@odoo/owl
中的xml
函数将模板注册到 Owl。使用
@web/core/tooltip/tooltip_hook
中的useTooltip
钩子显示工具提示。该钩子接受 Owl 模板和模板所需的变量作为参数。
