第 2 章:创建画廊视图¶
让我们看看如何从头开始创建一个新的视图。在某种程度上,这并不难,但关于如何操作的真正好的资源却很少。请注意,大多数情况应该通过自定义现有视图或使用客户端动作来解决。
对于这个练习,我们假设想要创建一个 gallery
视图,这是一种可以使用图片字段来展示一组记录的视图。
问题当然可以通过看板视图来解决,但这意味着无法在同一个动作中同时使用我们正常的看板视图和画廊视图。
让我们创建一个画廊视图。每个画廊视图将在其架构中通过 image_field
属性进行定义:
<gallery image_field="some_field"/>
要完成本章中的任务,您需要安装 awesome_gallery 插件。该插件包含添加新视图所需的服务端文件。
目标

每个章节练习的解决方案托管在 官方 Odoo 教程仓库 中。
1. 创建一个“你好,世界”视图¶
第一步是创建一个带有简单组件的 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
作为视图类型之一。确保在切换到画廊视图时可以看到你的“你好,世界”组件。


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

3. 解析 arch¶
目前,我们的画廊视图还做不了太多事情。让我们先来读取视图的 arch 中包含的信息。
解析架构的过程通常使用特定于每个视图的 ArchParser
完成。它继承自通用的 XMLParser
类。
Example
这是一个 ArchParser 可能的示例:
export class MyCustomArchParser {
parse(xmlDoc) {
const myAttribute = xmlDoc.getAttribute("my_attribute")
return {
myAttribute,
}
}
}
在单独的文件中创建
ArchParser
类。用来读取
image_field
的信息。将
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,
}
})
在
GalleryController
中添加一个loadImages(domain) {...}
方法。它应通过 orm 服务执行webSearchRead
调用,以获取与 domain 对应的记录,并使用通过 props 接收到的imageField
。如果你在调用的上下文中没有包含
bin_size
,你将收到以 base64 编码的图片字段。请确保在上下文中添加bin_size
,以便获取图片字段的大小。我们稍后会显示这张图片。将
setup
代码修改为在onWillStart
和onWillUpdateProps
钩子中调用该方法。修改模板,以便在
Layout
组件的默认插槽中显示每张图片的 ID 和大小。
注解
在下一个练习中,加载数据的代码将被移动到一个合适的模型中。

5. 解决并发问题¶
目前,我们的代码并不具备并发安全性。如果有人两次修改领域(domain),将会触发两次 loadImages(domain)
。因此会有两个请求,它们可能在不同时间到达,具体取决于不同的因素。如果在接收到第二个请求的响应之后才接收到第一个请求的响应,就会导致状态不一致。
Odoo 中的 KeepLast
原语解决了这个问题,它管理一个任务列表,并仅保持最后一个任务处于激活状态。
从
@web/core/utils/concurrency
导入KeepLast
。在模型中实例化一个
KeepLast
对象。在
KeepLast
中添加webSearchRead
调用,以便仅解析最后一次调用。
6. 重新组织代码¶
实际视图的组织性要强一些。在本例中这可能有些过度设计,但目的是学习如何在 Odoo 中组织代码。此外,这种方式在需求变化时也更容易扩展。
将所有模型代码移动到其自身的
GalleryModel
类中。将所有渲染代码移动到
GalleryRenderer
组件中。在
GalleryController
中导入GalleryModel
和GalleryRenderer
以使其正常工作。
7. 使视图可扩展¶
要扩展视图,可以导入画廊视图对象以根据自己的需求进行修改。问题是目前无法定义自定义模型或渲染器,因为它们在控制器中是硬编码的。
在画廊视图文件中导入
GalleryModel
和GalleryRenderer
。在画廊视图对象中添加
Model
和Renderer
键,并将其分别赋值为GalleryModel
和GalleryRenderer
。将Model
和Renderer
作为属性传递给控制器。将控制器中的硬编码导入移除,并从 props 中获取它们。
使用 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. 添加一个可选的提示信息¶
在鼠标悬停时显示一些额外信息会很有用。
将代码更新为允许在 arch 中添加一个可选属性:
<gallery image_field="some_field" tooltip_field="some_other_field"/>
鼠标悬停时,显示工具提示字段的内容。如果该字段是字符字段、数字字段或一对多字段,应能正常工作。要在 HTML 元素上添加工具提示,可以将字符串放在元素的
data-tooltip
属性中。将客户画廊视图的架构更新,以添加客户作为提示字段。

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

12. 验证视图¶
到目前为止,我们已经有了一个美观且有用的视图。但在实际生活中,用户可能会在错误地编码其画廊视图的 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
包含到图片网址中。确保点击上传按钮不会触发 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 验证器修改为接受新的 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>
架构解析器应该解析字段和提示模板。从
@web/core/utils/xml
导入visitXML
,并使用它来解析字段名称和提示模板。确保模型通过在规范中包含解析后的字段名称来调用
webSearchRead
。渲染器(或你为其创建的任何子组件)应该接收到解析后的提示模板。操作此模板,将
<field>
元素替换为<t t-esc="x">
元素。小技巧
该模板是一个
Element
对象,因此可以像操作 HTML 元素一样对其进行操作。通过
@odoo/owl
中的xml
函数,将模板注册到 Owl。使用来自
@web/core/tooltip/tooltip_hook
的useTooltip
钩子来显示提示信息。此钩子接收 Owl 模板和模板所需的变量作为参数。

另请参见
示例:在看板中使用 useTooltip <https://github.com/odoo/odoo/blob/0e6481f359e2e4dd4f5b5147a1754bb3cca57311/addons/web/static/src/views/kanban/kanban_record.js#L189-L192>`_
示例:访问 XML 使用 <https://github.com/odoo/odoo/blob/48ef812a635f70571b395f82ffdb2969ce99da9e/addons/web/static/src/views/list/list_arch_parser.js#L19>