JavaScript 参考

本文档介绍 Odoo JavaScript 框架。从代码行数上看,这个框架并不庞大,但它相当通用,因为它本质上是一台把声明式界面描述转换成可运行应用的机器,能够与数据库中的所有模型和记录交互。甚至可以使用 web 客户端来修改 web 客户端本身的界面。

概述

JavaScript 框架的设计主要面向三个使用场景:

  • web 客户端:这是私有的 Web 应用,用户可以在这里查看和编辑业务数据。这是一个单页应用(页面永远不会重新加载,只在需要时才从服务器获取新数据)

  • 网站:这是 Odoo 的公开部分。它允许未识别的用户浏览内容、购物或以客户身份执行许多操作。这是一个经典的网站:由各种带控制器的路由以及一些使其运作的 JavaScript 组成。

  • 销售点:这是销售点(POS)的界面。它是一个专门的单页应用。

部分 javascript 代码是这三种使用场景共用的,会被打包在一起(参见下文资源章节)。本文档主要关注 Web 客户端的设计。

Web 客户端

单页应用

简而言之,webClient*(*WebClient 的实例)是整个用户界面的根组件。它的职责是协调各种子组件,并提供服务,例如 rpc、本地存储等。

在运行时,Web 客户端是一个单页应用。它不需要在用户每次执行动作时都向服务器请求完整页面。相反,它只请求所需的内容,然后替换/更新视图。此外,它还管理 url:使其与 Web 客户端状态保持同步。

这意味着当用户在使用 Odoo 时,Web 客户端类(以及动作管理器)实际上会创建和销毁许多子组件。状态高度动态,每个组件都可能在任何时候被销毁。

Web 客户端 JS 代码概览

这里,我们对 web/static/src/js 插件模块中的 Web 客户端代码做一个非常简要的概述。请注意,它有意不追求详尽。我们只涵盖最重要的文件/文件夹。

  • boot.js:这是定义模块系统的文件,必须最先加载。

  • core/:这是一组更底层的构建块。值得注意的是,它包含类系统、组件系统、并发工具以及许多其他类/函数。

  • chrome/:此文件夹中包含大多数构成用户界面的大型组件。

  • chrome/abstract_web_client.js 与 chrome/web_client.js:这些文件共同定义了 WebClient 组件,它是 Web 客户端的根组件。

  • chrome/action_manager.js:这段代码会把一个动作转换为组件(例如看板视图或表单视图)

  • chrome/search_X.js 所有这些文件定义了搜索视图(从 Web 客户端的角度看它并非视图,仅从服务器的角度看才是)

  • fields:所有主要视图的字段组件都在此定义

  • views:视图所在的位置。

文件未加载/未更新时该怎么办

文件未能正确加载的原因有很多。以下是一些可以尝试的解决办法:

  • 服务器启动后,它无法得知某个资源文件是否已被修改。因此,您只需重启服务器即可重新生成资源。

  • 检查控制台(在开发者工具中,通常按 F12 打开),确保没有明显错误

  • 尝试在文件开头(任何模块定义之前)添加一个 console.log(),这样您就可以看到文件是否被加载

  • 处于任何调试模式时,调试管理器菜单(虫子图标)中都有一个选项,可强制服务器更新其资源文件。

  • 使用 debug=assets 模式。这会绕过资源包(请注意,它实际上并没有解决问题,服务器仍会使用过期的资源包)

  • 最后,对开发者而言最方便的方式是使用 –dev=all 选项启动服务器。这会激活文件监视选项,必要时自动使资源失效。注意,在 Windows 系统上该功能效果不佳。

  • 记得刷新您的页面!

  • 或者也许是保存您的代码文件……

Note

资源文件重新生成后,需要刷新页面以重新加载正确的文件(如果无效,可能是文件被缓存了)。

加载 JavaScript 代码

大型应用通常会被拆分成需要相互连接的较小文件。某些文件可能需要使用另一个文件中定义的某些代码。在文件之间共享代码有两种方式:

  • 使用全局作用域(即 window 对象)来写入/读取某些对象或函数的引用,

  • 使用模块系统,为每个模块提供导出或导入值的方式,并确保它们按正确的顺序加载。

虽然可以在全局作用域中工作,但这存在许多问题:

很难确保在全局作用域中直接完成的工作不暴露实现细节。

  • 依赖是隐式的,导致加载顺序脆弱且不可靠。

  • 由于缺乏对执行过程的感知,无法使用各类优化(例如延迟与异步加载)。

  • 模块系统有助于解决这些问题:由于模块会指定其依赖,模块系统能够确保必要的加载顺序得到遵守;又由于模块可以精确地指定其导出,它们不太可能泄漏实现细节。

对于大多数 Odoo 代码,我们希望使用模块系统。由于 Odoo 中资源的工作方式(特别是每个已安装的 odoo 插件模块都可以修改包中所含文件列表这一事实),Odoo 必须在浏览器端解析模块。为此,Odoo 提供了一个小巧的模块系统(见 Odoo 模块系统)。

不过,Odoo 也提供对原生 javascript 模块的支持(参见 原生 Javascript 模块)。这些模块会被服务器直接转换为 odoo 模块。我们鼓励将所有 javascript 代码编写为原生模块,以获得更好的 IDE 集成。将来,Odoo 模块系统应被视为一种实现细节,而非编写 javascript 代码的主要方式。

Note

原生 javascript 模块是定义 javascript 代码的主要方式。

类系统

Odoo 开发时 ECMAScript 6 类尚未可用。在 ECMAScript 5 中,定义类的标准方式是定义一个函数,并在其原型对象上添加方法。这没有问题,但当我们想使用继承、Mixin 时就略显复杂。

基于这些原因,Odoo 决定使用自己的类系统,其灵感来自 John Resig。基础 Class 位于 web.Class,在 class.js 文件中。

Note

请注意,创建新代码时应避免使用这个自定义类系统。它将在某个时间点被弃用,然后被移除。新的类应使用标准的 ES6 类系统。

创建子类

我们来讨论类是如何创建的。主要机制是使用 extend 方法(这大致相当于 ES6 类中的 extend)。

var Class = require('web.Class');

var Animal = Class.extend({
    init: function () {
        this.x = 0;
        this.hunger = 0;
    },
    move: function () {
        this.x = this.x + 1;
        this.hunger = this.hunger + 1;
    },
    eat: function () {
        this.hunger = 0;
    },
});

在此示例中,init 函数是构造函数。它会在创建实例时被调用。创建实例是通过使用 new 关键字来完成的。

继承

继承一个已有的类很方便。这只需在父类上使用 extend 方法即可完成。当某个方法被调用时,框架会悄悄地将一个特殊方法 _super 重新绑定到当前被调用的方法上。这让我们可以在需要调用父类方法时使用 this._super。

var Animal = require('web.Animal');

var Dog = Animal.extend({
    move: function () {
        this.bark();
        this._super.apply(this, arguments);
    },
    bark: function () {
        console.log('woof');
    },
});

var dog = new Dog();
dog.move()

Mixin 混入

Odoo 的类系统不支持多重继承,但对于需要共享某些行为的场景,我们提供了 mixin 系统:extend 方法实际上可以接受任意数量的参数,并将它们全部组合到新类中。

var Animal = require('web.Animal');
var DanceMixin = {
    dance: function () {
        console.log('dancing...');
    },
};

var Hamster = Animal.extend(DanceMixin, {
    sleep: function () {
        console.log('sleeping');
    },
});

在此示例中,Hamster 类是 Animal 的子类,但它同时混入了 DanceMixin。

修补现有类

这并不常见,但我们有时需要就地修改另一个类。目标是拥有一种机制来更改某个类及其所有未来/现有的实例。这是通过使用 include 方法完成的:

var Hamster = require('web.Hamster');

Hamster.include({
    sleep: function () {
        this._super.apply(this, arguments);
        console.log('zzzz');
    },
});

这显然是一项危险的操作,应当谨慎进行。但按照 Odoo 的结构,有时需要在一个插件模块中修改另一个插件模块中定义的组件/类的行为。请注意,这会修改该类的所有实例,即使它们已经被创建。

组件

Widget 类是用户界面真正重要的构建块。用户界面中的几乎一切都由组件控制。Widget 类定义在模块 web.Widget 的 widget.js 文件中。

简而言之,Widget 类提供的功能包括:

  • 组件之间的父子关系 (PropertiesMixin)

  • 完善的生命周期管理,并带有安全特性(例如在销毁父组件时自动销毁子组件)

  • 通过 qweb 自动渲染

  • 各种实用函数,用于帮助与外部环境交互。

下面是一个基本计数器组件的示例:

var Widget = require('web.Widget');

var Counter = Widget.extend({
    template: 'some.template',
    events: {
        'click button': '_onClick',
    },
    init: function (parent, value) {
        this._super(parent);
        this.count = value;
    },
    _onClick: function () {
        this.count++;
        this.$('.val').text(this.count);
    },
});

在此示例中,假设模板 some.template (并已正确加载:模板位于某个文件中,该文件已在模块清单的 assets 中正确定义 'assets': {'web.assets_qweb': [...]},参见 assets。)如下所示:

<div t-name="some.template">
    <span class="val"><t t-esc="widget.count"/></span>
    <button>Increment</button>
</div>

该示例组件可以按以下方式使用:

// Create the instance
var counter = new Counter(this, 4);
// Render and insert into DOM
counter.appendTo(".some-div");

该示例展示了 Widget 类的若干特性,包括事件系统、模板系统,以及带初始 parent 参数的构造函数。

组件生命周期

与许多组件系统一样,组件类有着明确定义的生命周期。通常的生命周期如下:先调用 init,然后 willStart,接着进行渲染,然后 start,最后 destroy。

Widget.init(parent)

这是构造函数。init 方法用于初始化组件的基本状态。它是同步的,可以被重写,以便从组件的创建者/父级接收更多参数

参数
  • parent (Widget()) – the new widget’s parent, used to handle automatic destruction and event propagation. Can be null for the widget to have no parent.

Widget.willStart()

当组件被创建并即将追加到 DOM 时,框架会调用该方法一次。willStart 方法是一个应返回 Promise 的钩子。JS 框架会等待该 Promise 完成后再进入渲染步骤。请注意,此时组件还没有 DOM 根元素。willStart 钩子主要用于执行一些异步工作,例如从服务器获取数据

[Rendering]()

这一步由框架自动完成。框架会检查组件上是否定义了 template 键。如果定义了,就会渲染该模板,并在渲染上下文中将 widget 键绑定到该组件(见上面的示例:我们在 QWeb 模板中使用 widget.count 从组件读取值)。如果没有定义模板,则读取 tagName 键并创建相应的 DOM 元素。渲染完成后,将结果设置为组件的 $el 属性。之后,框架会自动绑定 events 和 custom_events 键中的所有事件。

Widget.start()

渲染完成后,框架会自动调用 start 方法。这适合用于执行一些专门的渲染后工作,例如配置某个库。

必须返回一个 promise 以指示其工作何时完成。

返回

promise

Widget.destroy()

这始终是组件生命周期的最后一步。当组件被销毁时,我们基本上会执行所有必要的清理操作:将组件从组件树中移除、解绑所有事件……

在该组件的父级被销毁时自动调用;若该组件没有父级,或者它被移除而父级仍存在,则必须显式调用。

请注意,willStart 和 start 方法不一定会被调用。一个组件可以被创建(init 方法会被调用),然后被销毁(destroy 方法),而从未被添加到 DOM 中。如果是这种情况,willStart 和 start 甚至不会被调用。

Widget API

Widget.tagName

在组件未定义模板时使用。默认为 div,将用作标签名来创建 DOM 元素并设为组件的 DOM 根节点。可以通过以下属性进一步自定义生成的 DOM 根节点:

Widget.id

用于在生成的 DOM 根节点上生成 id 属性。注意,这很少需要用到,如果组件可能被使用多次,这么做恐怕并不好。

Widget.className

用于在生成的 DOM 根节点上生成 class 属性。注意,它实际上可以包含多个 css 类:’some-class other-class’

Widget.attributes

属性名到属性值的映射(对象字面量)。每个键值对都会作为 DOM 属性设置在生成的 DOM 根节点上。

Widget.el

设置为组件根元素的原始 DOM 元素(仅在 start 生命周期方法之后可用)

Widget.$el

el 的 jQuery 包装器(仅在 start 生命周期方法之后可用)

Widget.template

应设置为某个 QWeb 模板 的名称。如果设置了该模板,则在组件初始化之后、启动之前渲染它。模板生成的根元素将被设置为组件的 DOM 根节点。

Widget.xmlDependencies

需要在组件渲染之前加载的 xml 文件路径列表。这不会导致加载任何已经加载过的内容。当你想惰性加载模板,或想在网站与 Web 客户端界面之间共享某个组件时,这很有用。

var EditorMenuBar = Widget.extend({
    xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
    ...
Widget.events

事件是将事件选择器(事件名和可选的 CSS 选择器,以空格分隔)映射到回调。回调可以是组件方法的名称,也可以是函数对象。无论哪种情况,this 都会被设置为该组件:

events: {
    'click p.oe_some_class a': 'some_method',
    'change input': function (e) {
        e.stopPropagation();
    }
},

选择器用于 jQuery 的事件委托,回调只会对匹配该选择器的 DOM 根节点的后代元素触发。如果省略选择器(只指定事件名),事件将直接绑定在组件的 DOM 根节点上。

注意:不建议使用内联函数,将来可能会移除该用法。

Widget.custom_events

这与 events 属性几乎相同,只是键可以是任意字符串。它们表示由某些子组件触发的业务事件。事件被触发时,会在组件树中“冒泡”(详见组件通信一节)。

Widget.isDestroyed()
返回

true if the widget is being or has been destroyed, false otherwise

Widget.$(selector)

将作为参数指定的 CSS 选择器应用于该组件的 DOM 根节点:

this.$(selector);

功能上等同于:

this.$el.find(selector);
参数
  • selector (String()) – CSS selector

返回

jQuery object

Note

该辅助方法与 Backbone.View.$ 类似

Widget.setElement(element)

将组件的 DOM 根节点重新设置为提供的元素,同时负责重设 DOM 根节点的各种别名,以及解除并重新设置委托事件。

参数
  • element (Element()) – a DOM element or jQuery object to set as the widget’s DOM root

将组件插入 DOM

Widget.appendTo(element)

渲染组件并将其作为目标的最后一个子元素插入,使用 .appendTo()

Widget.prependTo(element)

渲染组件并将其作为目标的第一个子元素插入,使用 .prependTo()

Widget.insertAfter(element)

渲染组件并将其作为目标的前一个同级元素插入,使用 .insertAfter()

Widget.insertBefore(element)

渲染组件并将其作为目标的后一个同级元素插入,使用 .insertBefore()

这些方法都接受对应 jQuery 方法所接受的参数(CSS 选择器、DOM 节点或 jQuery 对象)。它们都返回一个 promise,并负责三项任务:

  • 通过 renderElement() 渲染组件的根元素

  • 使用与其匹配的任意 jQuery 方法将组件的根元素插入 DOM

  • 启动组件,并返回启动的结果

组件准则

  • 应避免使用标识符(id 属性)。在通用应用和模块中,id 会限制组件的可复用性,并使代码更脆弱。大多数情况下,可以完全不用它们,改用类,或保留对某个 DOM 节点或 jQuery 元素的引用。

    如果确实必须使用 id (因为某个第三方库需要),则应使用 _.uniqueId() 部分地生成该 id,例如:

    this.id = _.uniqueId('my-widget-');
    
  • 应避免使用可预测/常见的 CSS 类名。像 “content” 或 “navigation” 这样的类名可能符合想要的含义/语义,但很可能会有其他开发者有同样的需求,从而造成命名冲突和非预期行为。通用类名应加上前缀,例如它们所属组件的名称(从而创建”非正式”命名空间,类似于 C 或 Objective-C)。

  • 应避免使用全局选择器。由于一个组件可能在单个页面中被多次使用(Odoo 中的示例是仪表板),查询应限定在某个组件的作用域内。诸如 $(selector) 或 document.querySelectorAll(selector) 这类不加过滤的选择通常会导致非预期或错误的行为。Odoo Web 的 Widget() 有一个提供其 DOM 根节点的属性($el),以及一个直接选择节点的快捷方式($())。

  • 更一般地说,切勿假设你的组件拥有或控制其自身 $el 之外的任何东西(因此,避免使用对父级组件的引用)

  • 除非极其简单,否则 HTML 模板/渲染应使用 QWeb。

  • 所有交互式组件(向屏幕显示信息或拦截 DOM 事件的组件)都必须继承自 Widget(),并正确实现和使用其 API 与生命周期。

  • 务必在 start 完成后才使用 $el,例如:

    var Widget = require('web.Widget');
    
    var AlmostCorrectWidget = Widget.extend({
        start: function () {
            this.$el.hasClass(....) // in theory, $el is already set, but you don't know what the parent will do with it, better call super first
            return this._super.apply(arguments);
        },
    });
    
    var IncorrectWidget = Widget.extend({
        start: function () {
            this._super.apply(arguments); // the parent promise is lost, nobody will wait for the start of this widget
            this.$el.hasClass(....)
        },
    });
    
    var CorrectWidget = Widget.extend({
        start: function () {
            var self = this;
            return this._super.apply(arguments).then(function() {
                self.$el.hasClass(....) // this works, no promise is lost and the code executes in a controlled order: first super, then our code.
            });
        },
    });
    

QWeb 模板引擎

Web 客户端使用 QWeb 模板 模板引擎来渲染组件(除非组件重写 renderElement 方法以执行其他操作)。Qweb JS 模板引擎基于 XML,与 python 实现基本兼容。

现在来解释模板是如何加载的。每当 Web 客户端启动时,都会向 /web/webclient/qweb 路由发起一次 rpc 请求。随后服务器会返回每个已安装模块数据文件中定义的所有模板列表。正确的文件列在每个模块清单文件的 web.assets_qweb 条目中。也可以通过调用同一路由并传入对应的 “bundle” 查询参数,来延迟加载其他资源包的模板。

Web 客户端会等待该模板列表加载完成后,再启动第一个组件。

对于我们的需求,这套机制已经运行得很好,但有时我们想延迟加载某个模板。例如,假设有一个很少使用的组件。在这种情况下,我们可能不想在主文件中加载它的模板,以便让 Web 客户端更轻量。此时,可以使用 Widget 的 xmlDependencies 键:

var Widget = require('web.Widget');

var Counter = Widget.extend({
    template: 'some.template',
    xmlDependencies: ['/myaddon/path/to/my/file.xml'],

    ...

});

这样一来,Counter 组件会在其 willStart 方法中加载 xmlDependencies 文件,因此执行渲染时模板已就绪。

事件系统

目前 Odoo 支持两种事件系统:一种是允许添加监听器和触发事件的简单系统;另一种是更完整的系统,还能让事件’向上冒泡’。

这两种事件系统都在 EventDispatcherMixin 中实现,位于 mixins.js 文件里。该 mixin 被包含在 Widget 类中。

基础事件系统

这套事件系统历史上最早出现。它实现了一个简单的总线模式。我们有 4 个主要方法:

  • on:用于为某个事件注册一个监听器。

  • off:用于移除事件监听器。

  • once:用于注册一个只会被调用一次的监听器。

  • trigger:触发一个事件。这会导致每个监听器都被调用。

下面是一个示例,说明该事件系统可以如何使用:

var Widget = require('web.Widget');
var Counter = require('myModule.Counter');

var MyWidget = Widget.extend({
    start: function () {
        this.counter = new Counter(this);
        this.counter.on('valuechange', this, this._onValueChange);
        var def = this.counter.appendTo(this.$el);
        return Promise.all([def, this._super.apply(this, arguments)]);
    },
    _onValueChange: function (val) {
        // do something with val
    },
});

// in Counter widget, we need to call the trigger method:

... this.trigger('valuechange', someValue);

Warning

不建议使用这一事件系统,我们计划用扩展事件系统中的 trigger_up 方法替换每个 trigger 方法

扩展事件系统

自定义事件是一套更高级的系统,模仿 DOM 事件 API。每当触发事件时,它会沿组件树’向上冒泡’,直到到达根组件或被阻止。

  • trigger_up:这个方法会创建一个小型 OdooEvent,并在组件树中派发它。请注意,它会从触发该事件的组件开始。

  • custom_events:相当于 event 字典,但用于 odoo 事件。

OdooEvent 类非常简单。它有三个公共属性:target*(触发事件的组件)、*name*(事件名称)和 *data*(负载)。它还有 2 个方法:*stopPropagation 和 is_stopped。

上面的示例可以更新为使用自定义事件系统:

var Widget = require('web.Widget');
var Counter = require('myModule.Counter');

var MyWidget = Widget.extend({
    custom_events: {
        valuechange: '_onValueChange'
    },
    start: function () {
        this.counter = new Counter(this);
        var def = this.counter.appendTo(this.$el);
        return Promise.all([def, this._super.apply(this, arguments)]);
    },
    _onValueChange: function(event) {
        // do something with event.data.val
    },
});

// in Counter widget, we need to call the trigger_up method:

... this.trigger_up('valuechange', {value: someValue});

注册表

在 Odoo 生态系统中,一个常见需求是从外部(通过安装一个应用,即另一个模块)扩展/修改基础系统的行为。例如,可能需要在某些视图中添加一种新的组件类型。在这种情况下以及许多其他情况下,通常的流程是:先创建所需的组件,再将其添加到某个注册表中(注册步骤),从而让 Web 客户端的其余部分感知到它的存在。

系统中提供了几个注册表:

字段注册表(由 web.field_registry 导出)

字段注册表包含 web 客户端所知的所有字段组件。每当视图(通常是表单或列表/看板)需要字段组件时,都会在这里查找。一个典型的用例如下:

var fieldRegistry = require('web.field_registry');

var FieldPad = ...;

fieldRegistry.add('pad', FieldPad);

请注意,每个值都应是 AbstractField 的子类。

视图注册表

该注册表包含 Web 客户端(尤其是视图管理器)已知的所有 JS 视图。该注册表中的每个值都应是 AbstractView 的子类。

动作注册表

我们在该注册表中跟踪所有客户端动作。动作管理器需要创建客户端动作时就会在此查找。在 11 版中,每个值只需是 Widget 的子类;但在 12 版中,这些值必须是 AbstractAction。

组件之间的通信

组件之间的通信方式有很多种。

从父级到子级

这是一个简单的场景。父组件可以直接调用其子组件的方法:

this.someWidget.update(someInfo);
从组件到其父级/某个祖先

在这种情况下,该组件的职责只是通知其环境某些事情发生了。由于我们不希望该组件持有对其父级的引用(这会将该组件与其父级的实现耦合起来),通常最好的做法是触发一个事件,让它通过 trigger_up 方法在组件树中向上冒泡:

this.trigger_up('open_record', { record: record, id: id});

该事件会在组件上触发,然后向上冒泡,最终被某个上游组件捕获:

var SomeAncestor = Widget.extend({
    custom_events: {
        'open_record': '_onOpenRecord',
    },
    _onOpenRecord: function (event) {
        var record = event.data.record;
        var id = event.data.id;
        // do something with the event.
    },
});
跨组件

跨组件通信可以通过总线(bus)实现。这并不是首选的通信方式,因为它的缺点是会让代码更难维护。不过它的优点是可以解耦组件。在这种情况下,只需在总线上触发和监听事件即可。例如:

  // in WidgetA
  var core = require('web.core');

  var WidgetA = Widget.extend({
      ...
      start: function () {
          core.bus.on('barcode_scanned', this, this._onBarcodeScanned);
      },
  });

  // in WidgetB
  var WidgetB = Widget.extend({
      ...
      someFunction: function (barcode) {
          core.bus.trigger('barcode_scanned', barcode);
      },
  });

In this example, we use the bus exported by *web.core*, but this is not
required. A bus could be created for a specific purpose.

服务

在 11.0 版本中,我们引入了 service 的概念。其主要思想是给子组件一种受控的方式去访问其环境,既让框架有足够的控制权,又便于测试。

服务系统围绕三个概念组织:服务、服务提供者和组件。其工作方式是组件通过 trigger_up 触发事件,这些事件向上冒泡到服务提供者,服务提供者再请求服务执行任务,然后可能返回一个答复。

服务

服务是 AbstractService 类的一个实例。它基本上只有一个名称和几个方法。它的职责是执行一些工作,通常是依赖于环境的某些事情。

例如,我们有 ajax 服务(负责执行 rpc)、localStorage (与浏览器本地存储交互)以及许多其他服务。

下面是一个简化示例,说明 ajax 服务如何实现:

var AbstractService = require('web.AbstractService');

var AjaxService = AbstractService.extend({
    name: 'ajax',
    rpc: function (...) {
        return ...;
    },
});

该服务名为 ‘ajax’,定义了一个方法 rpc。

服务提供者

要让服务正常工作,我们需要一个服务提供者来派发自定义事件。在 backend*(Web 客户端)中,这是由主 Web 客户端实例完成的。请注意,服务提供者的代码来自 *ServiceProviderMixin。

小工具

组件是发起服务请求的部分。它只需触发一个 call_service 事件(通常通过 call 辅助函数)。该事件会向上冒泡,将意图传达给系统的其余部分。

在实践中,有些函数被频繁调用,因此我们提供了一些辅助函数使其更易用。例如,_rpc 方法就是一个用于发起 rpc 的辅助函数。

var SomeWidget = Widget.extend({
    _getActivityModelViewID: function (model) {
        return this._rpc({
            model: model,
            method: 'get_activity_view_id'
        });
    },
});

Warning

如果一个组件被销毁,它会从主组件树上分离,并且不再有父级。在这种情况下,事件不会向上冒泡,这意味着相关工作不会被执行。对于一个被销毁的组件来说,这通常正是我们所期望的。

RPCs

rpc 功能由 ajax 服务提供。但大多数人可能只会与 _rpc 辅助方法交互。

在 Odoo 上开发时通常有两种使用场景:可能需要在(python)模型上调用方法(这通过控制器 call_kw 完成),或者直接调用某个控制器(在某个路由上可用)。

  • 调用 python 模型上的方法:

return this._rpc({
    model: 'some.model',
    method: 'some_method',
    args: [some, args],
});
  • 直接调用控制器:

return this._rpc({
    route: '/some/route/',
    params: { some: kwargs},
});

通知

Odoo 框架提供了一种向用户传达各类信息的标准方式:通知,显示在用户界面的右上角。

通知有两种类型:

  • notification:用于显示一些反馈,例如当用户退订某个频道时。

  • warning:用于显示一些重要/紧急的信息,通常是系统中大多数(可恢复的)错误。

此外,通知可以在不打扰用户工作流的情况下向用户提出问题。想象一下通过 VOIP 接到一个电话:可以显示一个常驻通知,带有 接受 和 拒绝 两个按钮。

通知系统

Odoo 中的通知系统由以下组件构成:

  • Notification 组件:这是一个简单的组件,用于创建并显示所需的信息

  • NotificationService:负责在每次收到请求(通过 custom_event)时创建和销毁通知的服务。注意,Web 客户端就是一个服务提供者。

  • 客户端动作 display_notification:允许从 python 触发显示通知(例如在用户点击 object 类型按钮时调用的方法中)。

  • ServiceMixin 中的一个辅助函数:displayNotification

显示通知

显示通知最常见的方式是使用来自 ServiceMixin 的方法:

  • displayNotification(options):

    使用以下 options 显示通知:

    • title:字符串,可选。它将显示在顶部作为标题。

    • subtitle:字符串,可选。它将作为副标题显示在顶部。

    • message:字符串,可选。通知的内容。如有需要,请务必通过 markup 函数对消息进行转义。

    • sticky:布尔值,可选(默认为 false)。如果为 true,通知将一直保留,直到用户将其关闭。否则,通知会在短暂延迟后自动关闭。

    • type:字符串,可选(默认为 ‘warning’)。决定通知的样式。可能的值:’info’、’success’、’warning’、’danger’、’’。

    • className:字符串,可选。这是一个会自动添加到通知中的 CSS 类名。它可能对样式有用,尽管不鼓励这样使用。

这里有两个如何使用这些方法的示例:

// note that we call _t on the text to make sure it is properly translated.
this.displayNotification({
    title: _t("Success"),
    message: _t("Your signature request has been sent.")
});
this.displayNotification({
    title: _t("Error"),
    message: _t("Filter name is required."),
    type: 'danger',
});

下面是一个 python 示例:

# note that we call _(string) on the text to make sure it is properly translated.
def show_notification(self):
    return {
        'type': 'ir.actions.client',
        'tag': 'display_notification',
        'params': {
            'title': _('Success'),
            'message': _('Your signature request has been sent.'),
            'sticky': False,
        }
    }

系统托盘(Systray)

系统托盘(Systray)是界面菜单栏的右侧部分,Web 客户端在此显示若干组件,例如消息菜单。

当菜单创建 SystrayMenu 时,它会查找所有已注册的组件,并将它们作为子组件添加到合适的位置。

目前没有针对系统托盘组件的专用 API。它们应当是简单的组件,可以像其他组件一样通过 trigger_up 方法与环境通信。

添加新的系统托盘项

没有系统托盘注册表。正确的添加方式是将组件加入类变量 SystrayMenu.items。

var SystrayMenu = require('web.SystrayMenu');

var MySystrayWidget = Widget.extend({
    ...
});

SystrayMenu.Items.push(MySystrayWidget);

排序

在将组件添加给自己之前,系统托盘菜单会按 sequence 属性对各项排序。如果原型上不存在该属性,则使用 50。因此,若要把某个系统托盘项放到右侧,可以设置一个非常大的 sequence 值(反之,较小的值则放到左侧)。

MySystrayWidget.prototype.sequence = 100;

翻译管理

部分翻译在服务端完成(基本上是由服务器渲染或处理的所有文本字符串),但静态文件中也有一些字符串需要翻译。目前的工作方式如下:

  • 每个可翻译字符串都由特殊函数 _t 标记(可在 JS 模块 web.core 中使用)

  • 服务器使用这些字符串来生成相应的 PO 文件

  • 每当 web 客户端加载时,它都会调用路由 /web/webclient/translations,该路由返回所有可翻译术语的列表

  • 运行时,每当调用函数 _t 时,它都会在此列表中查找翻译,找到则返回翻译,否则返回原始字符串。

请注意,从服务器的角度出发,翻译在文档 翻译模块 中有更详细的说明。

javascript 翻译有两个重要函数:_t 和 _lt。区别在于 _lt 是惰性求值的。

var core = require('web.core');

var _t = core._t;
var _lt = core._lt;

var SomeWidget = Widget.extend({
    exampleString: _lt('this should be translated'),
    ...
    someMethod: function () {
        var str = _t('some text');
        ...
    },
});

在此示例中,_lt 是必需的,因为模块加载时翻译尚未就绪。

请注意,翻译函数需要小心处理。传入的参数字符串不应是动态的。

会话

Web 客户端提供了一个专门的模块,其中包含与用户当前 session 相关的信息。一些重要的键包括

  • uid: 当前用户 ID(即其作为 res.users 的 ID)

  • user_name: 用户名,字符串形式

  • 用户上下文(用户 ID、语言和时区)

  • partner_id: 与当前用户关联的联系人 ID

  • db: 当前正在使用的数据库名称

向会话添加信息

加载 /web 路由时,服务器会在模板的一个 script 标签中注入一些会话信息。这些信息将从模型 ir.http 的 session_info 方法读取。因此,如果想添加特定信息,可以通过重写 session_info 方法并将其添加到字典中来实现。

from odoo import models
from odoo.http import request


class IrHttp(models.AbstractModel):
    _inherit = 'ir.http'

    def session_info(self):
        result = super(IrHttp, self).session_info()
        result['some_key'] = get_some_value_from_db()
        return result

现在,可以在 JavaScript 中通过读取会话来获取该值:

var session = require('web.session');
var myValue = session.some_key;
...

请注意,此机制旨在减少 Web 客户端就绪所需的通信量。它更适合计算开销小的数据(缓慢的 session_info 调用会拖延所有用户的 Web 客户端加载),以及初始化过程早期就需要的数据。

视图

‘view’ 一词有多种含义。本节讨论的是视图的 javascript 代码设计,而不是 arch 或其他任何内容的结构。

2017 年,Odoo 用新的架构替换了之前的视图代码。主要需求是将渲染逻辑与模型逻辑分离。

视图(泛指)现在由 4 个部分组成:View、Controller、Renderer 和 Model。这 4 个部分的 API 在 AbstractView、AbstractController、AbstractRenderer 和 AbstractModel 类中定义。

View Controller Renderer Model
  • View 是工厂。其职责是获取一组字段、arch、上下文和一些其他参数,然后构造出 Controller/Renderer/Model 三元组。

    视图的职责是以正确的信息搭好 MVC 模式的各个部分。通常,它需要处理 arch 字符串,并提取视图其他各部分所需的数据。

    请注意,视图是一个类,而非组件。一旦它的工作完成,就可以被丢弃。

  • Renderer 只有一个职责:在 DOM 元素中呈现所查看的数据。每个视图都可以以不同的方式渲染数据。此外,它还应监听相应的用户操作,并在必要时通知其父级(Controller)。

    Renderer 是 MVC 模式中的 V。

  • Model:其职责是获取并保存视图的状态。通常它以某种方式表示数据库中的一组记录。Model 是“业务数据”的所有者,是 MVC 模式中的 M。

  • Controller:其职责是协调 Renderer 和 Model。同时,它也是 Web 客户端其余部分的主要入口点。例如,当用户在搜索视图中更改某些内容时,会以相应的信息调用 Controller 的 update 方法。

    它就是 MVC 模式中的 C。

Note

视图的 JS 代码在设计上可在视图管理器/动作管理器的上下文之外使用。它们可以用于客户端动作,也可以显示在公共网站上(需要对资源做一些处理)。

字段组件

web 客户端的很大一部分体验是关于编辑和创建数据。这些工作大部分借助字段组件完成,它们了解字段类型以及值应如何显示和编辑的具体细节。

AbstractField

AbstractField 类是视图中所有组件的基类,适用于所有支持组件的视图(目前:Form、List、Kanban)。

v11 的字段组件与之前版本有很多差异。下面列出最重要的几点:

  • 组件在所有视图(确切地说是 Form/List/Kanban)之间共享,不再需要重复实现。请注意,通过在视图注册表中以视图名称作前缀,可以为某个视图提供专用版本的组件:list.many2one 会比 many2one 优先被选用。

  • 组件不再是字段值的所有者。它们只表示数据,并与视图的其他部分通信。

  • 组件不再需要能在编辑模式和只读模式之间切换。现在,当需要这种变化时,组件会被销毁并重新渲染。这不是问题,因为它们本来就不拥有自己的值

  • 字段组件可以在视图之外使用。它们的 API 略显别扭,但设计上是独立的。

装饰(Decorations)

与列表视图一样,字段组件对 decoration 有简单支持。decoration 的目标是提供了一种简单的方式,根据记录的当前状态来指定文本颜色。例如,

<field name="state" decoration-danger="amount &lt; 10000"/>

有效的装饰名称为:

  • decoration-bf

  • decoration-it

  • decoration-danger

  • decoration-info

  • decoration-muted

  • decoration-primary

  • decoration-success

  • decoration-warning

每个装饰 decoration-X 会被映射到 css 类 text-X,这是一个标准的 bootstrap css 类(text-it 和 text-bf 除外,它们由 odoo 处理,分别对应斜体和粗体)。请注意,装饰属性的值应该是一个有效的 python 表达式,它将以记录作为求值上下文进行求值。

非关系字段

我们在这里记录所有默认可用的非关系型字段,顺序不分先后。

integer (FieldInteger)

这是 integer 类型字段的默认字段类型。

  • 支持的字段类型:integer

    选项:

    • type: 设置输入类型(默认为 text,可设为 number)

    在编辑模式下,字段渲染为一个 input,其 HTML 属性 type 设为 number (这样用户可以受益于原生支持,尤其是在移动端)。在这种情况下,默认格式化被禁用以避免不兼容。

    <field name="int_value" options='{"type": "number"}'/>
    
    • step: 设置用户点击按钮时数值上下增减的步长(仅适用于 number 类型的 input,默认为 1)

    <field name="int_value" options='{"type": "number", "step": 100}'/>
    
    • format: 是否应格式化数字(默认为 true)

    默认情况下,数字会根据语言环境参数进行格式化。

    该选项将阻止字段值被格式化。

    <field name="int_value" options='{"format": false}'/>
    
float (FieldFloat)

这是 float 类型字段的默认字段类型。

  • 支持的字段类型:float

属性:

  • digits: 显示精度

<field name="factor" digits="[42,5]"/>

选项:

  • type: 设置输入类型(默认为 text,可设为 number)

在编辑模式下,字段渲染为一个 input,其 HTML 属性 type 设为 number (这样用户可以受益于原生支持,尤其是在移动端)。在这种情况下,默认格式化被禁用以避免不兼容。

<field name="int_value" options='{"type": "number"}'/>
  • step: 设置用户点击按钮时数值上下增减的步长

    (仅适用于 number 类型的输入,默认为 1)

<field name="int_value" options='{"type": "number", "step": 0.1}'/>
  • format: 是否应格式化数字(默认为 true)

默认情况下,数字会根据语言环境参数进行格式化。

该选项将阻止字段值被格式化。

<field name="int_value" options='{"format": false}'/>
float_time (FieldFloatTime)

该组件的目标是正确显示表示时间间隔(以小时为单位)的浮点值。例如,0.5 应格式化为 0:30,4.75 对应 4:45。

  • 支持的字段类型:float

float_factor (FieldFloatFactor)

该组件旨在正确显示按选项给出的因子转换后的浮点值。例如,数据库中保存的值为 0.5、因子为 3 时,组件显示的值应格式化为 1.5。

  • 支持的字段类型:float

float_toggle (FieldFloatToggle)

该组件的目标是用一个包含若干可能值范围(在选项中给出)的按钮替换输入框。每次点击都会让用户在范围内循环切换。其目的是将字段值限制在预定义的选项中。此外,该组件支持像 float_factor 组件那样的因子转换(范围值应为转换后的结果)。

  • 支持的字段类型:float

<field name="days_to_close" widget="float_toggle" options='{"factor": 2, "range": [0, 4, 8]}'/>
boolean (FieldBoolean)

这是 boolean 类型字段的默认字段类型。

  • 支持的字段类型:boolean

char (FieldChar)

这是 char 类型字段的默认字段类型。

  • 支持的字段类型:char

date (FieldDate)

这是 date 类型字段的默认字段类型。注意,它同样适用于 datetime 字段。格式化日期时使用会话时区。

  • 支持的字段类型:date, datetime

选项:

<field name="datefield" options='{"datepicker": {"daysOfWeekDisabled": [0, 6]}}'/>
datetime (FieldDateTime)

这是 datetime 类型字段的默认字段类型。

  • 支持的字段类型:date, datetime

选项:

<field name="datetimefield" options='{"datepicker": {"daysOfWeekDisabled": [0, 6]}}'/>
daterange (FieldDateRange)

该组件允许用户在单个选择器中选择开始日期和结束日期。

  • 支持的字段类型:date, datetime

选项:

  • related_start_date: 应用于结束日期字段,以获取用于在选择器中显示范围的开始日期值。

  • related_end_date: 应用于开始日期字段,以获取用于在选择器中显示范围的结束日期值。

  • picker_options: 选择器的额外设置。

<field name="start_date" widget="daterange" options='{"related_end_date": "end_date"}'/>
remaining_days (RemainingDays)

该组件可用于 date 和 datetime 字段。在只读模式下,它显示字段值与今天之间的天数差(以天为单位)。该组件仅用于信息展示:因此在编辑模式下无法修改值。

  • 支持的字段类型:date, datetime

monetary (FieldMonetary)

这是 ‘monetary’ 类型字段的默认字段类型,用于显示货币。如果选项中给出了货币字段,则使用该字段,否则回退到默认货币(在会话中)

  • 支持的字段类型:monetary, float

选项:

  • currency_field: 另一个字段名,该字段应是指向 currency 的 many2one 字段。

<field name="value" widget="monetary" options="{'currency_field': 'currency_id'}"/>
text (FieldText)

这是 text 类型字段的默认字段类型。

  • 支持的字段类型:text

handle (HandleWidget)

该字段的作用是显示为 handle (句柄),并允许通过拖放来重新排序各个记录。

Warning

必须将其指定在记录排序所依据的字段上。

Warning

同一列表中不支持多个字段使用 handle 组件。

  • 支持的字段类型:integer

email (FieldEmail)

该字段显示电子邮箱地址。使用它的主要原因是,在只读模式下,它会被渲染为带有正确 href 的锚点标签。

  • 支持的字段类型:char

phone (FieldPhone)

该字段显示电话号码。使用它的主要原因是,在只读模式下,它会被渲染为带有正确 href 的锚点标签,但仅限于某些情况:我们只希望在设备能够拨打该号码时才使其可点击。

  • 支持的字段类型:char

url (UrlWidget)

该字段显示一个 URL(在只读模式下)。使用它的主要原因是,它会被渲染为带有正确 css 类和 href 的锚点标签。

此外,锚点标签的文本可以通过 text 属性进行自定义(它不会改变 href 的值)。

<field name="foo" widget="url" text="Some URL"/>

选项:

  • website_path: (默认:false) 默认情况下,组件会强制(若尚未如此)href 值以 http:// 开头,除非将该选项设为 true,从而允许重定向到数据库自身的网站。

  • 支持的字段类型:char

domain (FieldDomain)

“Domain” 字段允许用户借助树形界面构建技术前缀域,并实时查看选中的记录。在调试模式下,还会有一个输入框,可以直接输入前缀字符域(或构建树形界面无法实现的更高级的域)。

请注意,这仅限于 ‘static’ 域(不支持动态表达式,也无法访问上下文变量)。

  • 支持的字段类型:char

link_button (LinkButton)

LinkButton 组件实际上只是显示一个 span,内容为一个图标和该文本值。该链接可以点击,并会以链接值作为 url 打开新的浏览器窗口。

  • 支持的字段类型:char

image (FieldBinaryImage)

该组件用于将二进制值表示为图片。在某些情况下,服务器返回的是 ‘bin_size’ 而不是真实图片(bin_size 是表示文件大小的字符串,如 6.5kb)。此时,组件会生成一个图片,其 source 属性对应服务器上的图片。

  • 支持的字段类型:binary

选项:

  • preview_image: 如果图像仅以 ‘bin_size’ 方式加载,则该选项可用于告知 Web 客户端:默认字段名不是当前字段的名称,而是另一个字段的名称。

  • accepted_file_extensions: 用户可以从文件输入对话框中选择的文件扩展名(cf:<input type=”file”/> 上的 accept 属性),默认值为 image/*

<field name="image" widget='image' options='{"preview_image":"image_128"}'/>
binary (FieldBinaryFile)

用于保存/下载二进制文件的通用组件。

  • 支持的字段类型:binary

选项:

  • accepted_file_extensions: 用户可以从文件输入对话框中选择的文件扩展名(cf:<input type=”file”/> 上的 accept 属性)

属性:

  • filename: 保存二进制文件会丢失其文件名,因为它只保存二进制值。文件名可以保存在另一个字段中。为此,应将属性 filename 设置为视图中存在的某个字段。

<field name="datas" filename="datas_fname"/>
priority (PriorityWidget)

该组件渲染为一组星号,允许用户点击它来选择或不选择一个值。例如,这对于将任务标记为高优先级非常有用。

请注意,这个组件也可以在 ‘readonly’ 模式下工作,这一点并不常见。

  • 支持的字段类型:selection

attachment_image (AttachmentImage)

用于 many2one 字段的图片组件。如果设置了该字段,此组件会以带有正确 src url 的图片形式渲染。该组件在编辑模式或只读模式下没有不同的行为,它仅用于查看图片。

  • 支持的字段类型:many2one

<field name="displayed_image_id" widget="attachment_image"/>
image_selection (ImageSelection)

允许用户通过点击图片来选择值。

  • 支持的字段类型:selection

选项:一个字典,将选择值映射到一个对象,该对象包含图片的 url(image_link)和预览图片(preview_link)。

请注意,这个选项并不是可选的!

<field name="external_report_layout" widget="image_selection" options="{
    'background': {
        'image_link': '/base/static/img/preview_background.png',
        'preview_link': '/base/static/pdf/preview_background.pdf'
    },
    'standard': {
        'image_link': '/base/static/img/preview_standard.png',
        'preview_link': '/base/static/pdf/preview_standard.pdf'
    }
}"/>
label_selection (LabelSelection)

该组件渲染一个简单的不可编辑标签。它只用于展示信息,而非编辑信息。

  • 支持的字段类型:selection

选项:

  • classes: 将选择值映射到 css 类的映射

<field name="state" widget="label_selection" options="{
    'classes': {'draft': 'default', 'cancel': 'default', 'none': 'danger'}
}"/>
state_selection (StateSelectionWidget)

这是一个专门化的选择组件。它假设记录有一些硬编码字段,且这些字段出现在视图中:stage_id、legend_normal、legend_blocked、legend_done。它主要用于显示和更改项目中任务的状态,并在下拉框中显示附加信息。

  • 支持的字段类型:selection

<field name="kanban_state" widget="state_selection"/>
kanban_state_selection (StateSelectionWidget)

这与 state_selection 组件完全相同

  • 支持的字段类型:selection

list.state_selection (ListStateSelectionWidget)

在列表视图中,state_selection 组件默认在图标旁边显示标签。

  • 支持的字段类型:selection

选项:

  • hide_label: 隐藏图标旁边的标签

<field name="kanban_state" widget="state_selection" options="{'hide_label': True}"/>
boolean_favorite (FavoriteWidget)

该组件根据布尔值显示为一颗空(或非空)星。请注意,在只读模式下它也可以编辑。

  • 支持的字段类型:boolean

boolean_button (FieldBooleanButton)

布尔按钮组件用于表单视图中的统计按钮。其目标是显示一个美观的按钮,展示布尔字段(例如 ‘Active’)的当前状态,并允许用户点击按钮更改该字段。

请注意,它在只读模式下也可以被编辑。

  • 支持的字段类型:boolean

选项:

  • terminology: 可以是 ‘active’、’archive’、’close’,也可以是使用 string_true、string_false、hover_true、hover_false 键的自定义映射

<field name="active" widget="boolean_button" options='{"terminology": "archive"}'/>
boolean_toggle (BooleanToggle)

显示一个切换开关来表示布尔值。它是 FieldBoolean 的子字段,主要用于呈现不同的外观。

statinfo (StatInfo)

该组件用于在*统计按钮*中展示统计信息。它基本上就是一个带数字的标签。

  • 支持的字段类型:integer, float

选项:

  • label_field: 如果给出,组件将使用 label_field 的值作为文本。

<button name="%(act_payslip_lines)d"
    icon="fa-money"
    type="action">
    <field name="payslip_count" widget="statinfo"
        string="Payslip"
        options="{'label_field': 'label_tasks'}"/>
</button>
percentpie (FieldPercentPie)

该组件用于在*统计按钮*中展示统计信息。它与 statinfo 组件类似,但信息以*饼图*(从空到满)形式表示。注意,值按百分比解释(0 到 100 之间的数字)。

  • 支持的字段类型:integer, float

<field name="replied_ratio" string="Replied" widget="percentpie"/>
progressbar (FieldProgressBar)

以进度条形式表示一个值(从 0 到某个值)

  • 支持的字段类型:integer, float

选项:

  • editable: 表示值是否可编辑的布尔值

  • current_value: 从必须出现在视图中的字段获取 current_value

  • max_value: 从必须出现在视图中的字段获取 max_value

  • edit_max_value: 表示 max_value 是否可编辑的布尔值

  • title: 进度条的标题,显示在进度条上方 –> 不做翻译,请改用参数(而非选项)”title”

<field name="absence_of_today" widget="progressbar"
    options="{'current_value': 'absence_of_today', 'max_value': 'total_employee', 'editable': false}"/>
toggle_button (FieldToggleBoolean)

该组件用于布尔字段。它通过一个按钮在绿色圆点/灰色圆点之间切换,并根据值和某些选项设置工具提示。

  • 支持的字段类型:boolean

选项:

  • active: 当布尔值为 true 时应设置的工具提示字符串

  • inactive: 当布尔值为 false 时应设置的工具提示

<field name="payslip_status" widget="toggle_button"
    options='{"active": "Reported in last payslips", "inactive": "To Report in Payslip"}'
/>
dashboard_graph (JournalDashboardGraph)

这是一个更专门化的组件,适合用来显示表示一组数据的图表。例如,它用于会计仪表盘的看板视图。

它假定该字段是一组数据的 JSON 序列化结果。

  • 支持的字段类型:char

属性

  • graph_type: 字符串,可以是 ‘line’ 或 ‘bar’

<field name="dashboard_graph_data"
    widget="dashboard_graph"
    graph_type="line"/>
ace (AceEditor)

该组件用于文本(Text)字段。它提供 Ace 编辑器来编辑 XML 和 Python。

  • 支持的字段类型:char, text

  • badge (FieldBadge)

    在 Bootstrap 徽章胶囊中显示该值。

    • 支持的字段类型:char, selection, many2one

    默认情况下,徽章带有浅灰色背景,但可以通过 decoration-X 机制进行自定义。例如,要在特定条件下显示红色徽章:

    <field name="foo" widget="badge" decoration-danger="state == 'cancel'"/>
    

关联字段

class web.relational_fields.FieldSelection()

支持的字段类型:selection

web.relational_fields.FieldSelection.placeholder

在未选择任何值时用于显示提示信息的字符串

<field name="tax_id" widget="selection" placeholder="Select a tax"/>
radio (FieldRadio)

这是 FielSelection 的子字段,但专门以单选按钮形式显示所有有效选项。

请注意,如果用在 many2one 记录上,则会执行更多 RPC 来获取相关记录的 name_get。

  • 支持的字段类型:selection, many2one

选项:

  • horizontal: 如果为 true,单选按钮将水平显示。

<field name="recommended_activity_type_id" widget="radio"
    options="{'horizontal':true}"/>
selection_badge (FieldSelectionBadge)

这是 FieldSelection 的子字段,但专门以矩形徽章形式显示所有有效选项。

  • 支持的字段类型:selection, many2one

<field name="recommended_activity_type_id" widget="selection_badge"/>
many2one (FieldMany2One)

many2one 字段的默认组件。

  • 支持的字段类型:many2one

属性:

  • can_create: 允许创建相关记录(优先于 no_create 选项)

  • can_write: 允许编辑相关记录(默认:true)

选项:

  • quick_create: 允许快速创建相关记录(默认:true)

  • no_create: 阻止创建相关记录 - 同时隐藏 ‘创建 “xxx”’ 和 “创建并编辑…” 下拉菜单项(默认:false)

  • no_quick_create: 阻止快速创建相关记录 - 隐藏 ‘创建 “xxx”’ 下拉菜单项(默认:false)

  • no_create_edit: 隐藏 “创建并编辑…” 下拉菜单项(默认:false)

  • create_name_field: 创建相关记录时,如果设置了该选项,create_name_field 的值将填入输入框的值(默认:name)

  • always_reload: 布尔值,默认为 false。如果为 true,组件将总是额外执行一次 name_get 来获取其名称值。这用于 name_get 方法被重写的情况(请尽量不要这样做)

  • no_open: 布尔值,默认为 false。如果设为 true,则点击 many2one 时(只读模式下)不会跳转到该记录

<field name="currency_id" options="{'no_create': True, 'no_open': True}"/>
list.many2one (ListFieldMany2One) 小部件

many2one 字段的默认组件(在列表视图中)。

many2one 字段针对列表视图的特化版本。主要原因是需要将 many2one 字段(在只读模式下)渲染为文本,不允许打开相关记录。

  • 支持的字段类型:many2one

many2one_barcode 组件(FieldMany2OneBarcode)

用于 many2one 字段的组件,允许从移动设备(Android/iOS)打开摄像头扫描条形码。

many2one 字段的特化版本,允许用户使用原生摄像头扫描条形码,然后使用 name_search 搜索该值。

如果设置了此组件而用户未使用移动应用,它会回退到常规的 many2one(FieldMany2One)。

  • 支持的字段类型:many2one

many2one_avatar (Many2OneAvatar) 小部件

该组件仅支持指向继承自 ‘image.mixin’ 的模型的 many2one 字段。在只读模式下,它在相关记录的 display_name 旁显示该记录的图片。注意,此时 display_name 不是可点击的链接。在编辑模式下,其行为与普通的 many2one 完全一致。

  • 支持的字段类型:many2one

many2one_avatar_user 组件(Many2OneAvatarUser)

该组件是 Many2OneAvatar 的特化版本。点击头像时,会打开与相应用户的聊天窗口。该组件只能设置在指向 ‘res.users’ 模型的 many2one 字段上。

  • 支持的字段类型:many2one (指向 ‘res.users’)

many2one_avatar_employee 组件(Many2OneAvatarEmployee)

与 Many2OneAvatarUser 相同,但用于指向 ‘hr.employee’ 的 many2one 字段。

  • 支持的字段类型:many2one (指向 ‘hr.employee’)

kanban.many2one (KanbanFieldMany2One) 小部件

many2one 字段的默认组件(在看板视图中)。我们需要在看板视图中禁用所有编辑。

  • 支持的字段类型:many2one

many2many (FieldMany2Many)

many2many 字段的默认组件。

  • 支持的字段类型:many2many

属性:

  • mode: 字符串,要显示的默认视图

  • domain: 将数据限制在特定域内

选项:

  • create_text: 允许自定义添加新记录时显示的文本

  • link: 决定是否可以将记录添加到关联关系中的域(默认:True)。

  • unlink: 用于判断记录能否从关联中被移除的域(默认:True)。

many2many_binary (FieldMany2ManyBinaryMultiFiles) 小部件

该组件帮助用户同时上传或删除一个或多个文件。

请注意,这个组件专用于 ‘ir.attachment’ 模型。

  • 支持的字段类型:many2many

选项:

  • accepted_file_extensions: 用户可以从文件输入对话框中选择的文件扩展名(cf:<input type=”file”/> 上的 accept 属性)

many2many_tags (FieldMany2ManyTags) 小部件

以标签列表的形式显示 many2many。

  • 支持的字段类型:many2many

选项:

  • create: 决定是否可以创建新标签的域(默认:True)。

<field name="category_id" widget="many2many_tags" options="{'create': [['some_other_field', '>', 24]]}"/>
  • color_field: 数值字段的字段名,该字段应出现在视图中。将根据其值选择一种颜色。

<field name="category_id" widget="many2many_tags" options="{'color_field': 'color'}"/>
  • no_edit_color: 设为 True 可移除修改标签颜色的可能性(默认:False)。

<field name="category_id" widget="many2many_tags" options="{'color_field': 'color', 'no_edit_color': True}"/>
form.many2many_tags (FormFieldMany2ManyTags) 小部件

many2many_tags 组件针对表单视图的特化版本。它包含一些额外代码,允许编辑标签的颜色。

  • 支持的字段类型:many2many

kanban.many2many_tags (KanbanFieldMany2ManyTags) 小部件

many2many_tags 组件针对看板视图的特化版本。

  • 支持的字段类型:many2many

many2many_checkboxes (FieldMany2ManyCheckBoxes) 小部件

该字段显示一个复选框列表,允许用户选择其中的一部分选项。注意,显示值的数量上限为 100,该上限不可自定义。它只是为了处理极端情况,即误将该组件设置在关联模型(comodel)数据量极大的字段上。在这些情况下,列表视图更为合适,因为它支持分页和过滤。

  • 支持的字段类型:many2many

one2many (FieldOne2Many)

one2many 字段的默认组件。

它通常在一个子列表视图或子看板视图中显示数据。

  • 支持的字段类型:one2many

选项:

  • create: 决定是否可以创建相关记录的域(默认:True)。

  • delete: 决定是否可以删除相关记录的域(默认:True)。

<field name="turtles" options="{'create': [['some_other_field', '>', 24]]}"/>
  • create_text: 用于自定义 ‘Add’ 标签/文本的字符串。

<field name="turtles" options="{\'create_text\': \'Add turtle\'}"/>
statusbar (FieldStatus)

这是一个针对表单视图的高度专门化的组件。它是许多表单顶部表示流程的栏,允许选择特定状态。

  • 支持的字段类型:selection, many2one

reference (FieldReference)

FieldReference 是 select(用于模型)与 FieldMany2One(用于其值)的组合。它允许在任意模型上选择一条记录。

  • 支持的字段类型:char, reference

    选项:

    • model_field: FieldMany2One(‘ir.model’) 的名称,其中包含可选择记录所属的模型。

      设置该选项后,FieldReference 的 select 部分将不显示。

组件

week_days (WeekDays)

该字段显示一个星期选项的复选框列表,每天对应 1 个复选框,允许用户选择其中的一部分。

<widget name="week_days"/>

客户端动作

客户端动作的思想是一个集成到 Web 客户端界面中的自定义组件,就像一个 act_window_action。当你需要的组件与现有视图或特定模型没有紧密关联时,这就很有用。例如,Discuss 应用实际上就是一个客户端动作。

客户端动作是一个在不同上下文中有不同含义的术语:

  • 从服务器的角度来看,它是模型 ir_action 的一条记录,带有一个 char 类型的 tag 字段

  • 从 Web 客户端的视角看,它是一个组件,继承自 AbstractAction 类,并应以相应的键(来自 char 字段)注册到动作注册表中

每当菜单项与某个客户端动作关联时,打开该菜单项就会从服务器获取动作定义,然后在动作注册表中以相应的键查找 Widget 定义,最后实例化组件并将其追加到 DOM 中的合适位置。

添加客户端动作

客户端动作是一种控制菜单栏下方屏幕区域的组件。必要时它可以带有控制面板。定义客户端动作可分为两步:实现一个新组件,以及将该组件注册到动作注册表中。

实现一个新的客户端动作。

这是通过创建一个组件来完成的:

var AbstractAction = require('web.AbstractAction');

var ClientAction = AbstractAction.extend({
    hasControlPanel: true,
    ...
});
注册客户端动作:

照例,我们需要让 Web 客户端了解客户端动作与实际类之间的映射关系:

var core = require('web.core');

core.action_registry.add('my-custom-action', ClientAction);

然后,要在 web 客户端中使用客户端动作,我们需要创建一个带有正确 tag 属性的客户端动作记录(ir.actions.client 模型的一条记录):

<record id="my_client_action" model="ir.actions.client">
    <field name="name">Some Name</field>
    <field name="tag">my-custom-action</field>
</record>

使用控制面板

默认情况下,客户端动作不显示控制面板。要实现这一点,需要完成几个步骤。

  • 将 hasControlPanel 设为 true。在组件代码中:

    var MyClientAction = AbstractAction.extend({
        hasControlPanel: true,
        loadControlPanel: true, // default: false
        ...
    });
    

    Warning

    当 loadControlPanel 设为 true 时,客户端动作会自动获取搜索视图或控制面板视图的内容。在这种情况下,应按如下方式指定模型名称:

    init: function (parent, action, options) {
        ...
        this.controlPanelParams.modelName = 'model.name';
        ...
    }
    
  • 每当我们需要在控制面板中更新内容时,就调用 updateControlPanel 方法。例如:

    var SomeClientAction = Widget.extend({
        hasControlPanel: true,
        ...
        start: function () {
            this._renderButtons();
            this._update_control_panel();
            ...
        },
        do_show: function () {
             ...
             this._update_control_panel();
        },
        _renderButtons: function () {
            this.$buttons = $(QWeb.render('SomeTemplate.Buttons'));
            this.$buttons.on('click', ...);
        },
        _update_control_panel: function () {
            this.updateControlPanel({
                cp_content: {
                   $buttons: this.$buttons,
                },
            });
        }
    

updateControlPanel 是自定义控制面板内容的主要方法。更多信息请参阅 control_panel_renderer.js 文件。