【odoo14】【好书学习】第十五章、网站客户端开发
odoo的web客户端、后台是员工经常使用的地方。在第九章中,我们了解了如何使用后台提供的各种可能性。本章,我们将了解如何扩展这种可能性。其中web模块包含了我们在使用odoo中的各种交互行为。
本章将依赖于web模块。odoo有两个不同的版本(社区版、企业版)。社区版包含web模块,而企业版是对web的扩展模块web_enterprise模块。
企业版提供了定制的手机端自适应、可搜索的菜单及模块化设计。
重要提醒
与其他Odoo版本相比,odoo14对于后端web客户端来说有点独特。它包含两种管理odoo后台GUI的框架。第一个是传统基于小部件的框架,第二个是基于Odoo Web Library(OWL)的框架。OWL是odoo14的最新UI框架。两者都使用QWeb模板,但是在语法及运行原理方面有一些明显的调整。
尽管odoo14有新的框架OWL,但是odoo并没有广泛的使用。大多数网页客户端依旧使用老的框架。本章,我们将了解如何通过小部件的框架调整网页客户端。下一章节,我们将介绍OWL。
本章我们将创建一个用于获取用户输入的小部件。我们还将从头创建一个新视图。读完本章后,你将能够在Odoo后端创建自己的UI元素。
小贴士
odoo的用户交互依赖于JavaScript。本章中,我们假设你已经具备JavaScript、JQuery、Underscore.js和SCSS的基础知识。
本章主要内容如下:
- 创建自定义控件
- 使用客户端侧的QWeb模板
- 通过RPC调用后端python方法
- 创建新的视图
- 调试用客户端侧的代码
- 通过引导提升交互感
- 手机端js
创建自定义控件
正如您在第9章后端视图中看到的,我们可以使用小部件以不同的格式显示特定的数据。例如,我们使用widget='image'以图像的形式显示一个二进制字段。为了演示如何创建自己的小部件,我们将编写一个小部件,它允许用户选择一个整数字段,但我们将以不同的方式显示它。代替输入框,我们将显示一个颜色选择器,以便我们可以选择一个颜色号。在这里,每个数字将被映射到其相关的颜色。
准备
在本教程中,我们将使用my_library模块和基本字段和视图。
步骤
我们将添加一个JavaScript文件,其中包含小部件的逻辑,并添加一个SCSS文件来执行一些样式化操作。然后,我们将向books表单添加一个整数字段,以使用我们的新小部件。执行以下步骤添加一个新的字段小部件:
- 添加一个static/src/js/field_widget.js文件。关于这里使用的语法,请参考《CMS网站开发》第14章中扩展CSS和JavaScript的内容:
odoo.define('my_field_widget', function (require) {
"use strict";
var AbstractField = require('web.AbstractField');
var fieldRegistry = require('web.field_registry');
...
})
- 创建widget:
var colorField = AbstractField.extend({
- 设置CSS类、根元素以及支持的字段类型:
className: 'o_int_colorpicker',
tagName: 'span',
supportedFieldTypes: ['integer'],
- 配置js事件
events: {
'click .o_color_pill': 'clickPill',
},
- 重载构造函数
init: function(){
this.totalColors = 10;
this._super.apply(this, arguments);
}
- 重载DOM元素的_renderEdit和_renderReadonly函数
_renderEdit: function(){
this.$el.empty();
for(var i=0;i<this.totalColors;i++)
{
var className = "o_color_pill o_color_" + i;
if(this.value===i){
className += ' active';
}
this.$el.append($('<span>', {
'class': className,
'data-val': i,
}));
}
},
_renderReadonly: function(){
var className = "o_color_pill active readonly o_color_" + this.value;
this.$el.append($('<span>', {
'class': className,
}));
},
- 添加处理函数
clickPill: function(ev){
var $target = $(ev.currentTarget);
var data = $target.data();
this._setValue(data.val.toString());
}
}); // close AbstractField
- 注册widget:
fieldRegistry.add('int_color', colorField);
- 对其他组件可见:
return {
colorField: colorField,
};
}; // close 'my_field_widget' Namespace
- 添加SCSS,static/src/scss/field_widget.scss:
.o_int_colorpicker {
.o_color_pill {
display: inline-block;
height: 25px;
width: 25px;
margin: 4px;
border-radius: 25px;
position: relative;
@for $size from 1 through length($o-colors) {
&.o_color_#{$size - 1} {
background-color: nth($o-colors, $size);
&:not(.readonly):hover {
transform: scale(1.2);
transition: 0.3s;
cursor: pointer;
}
&.active:after {
content: "\f00c";
display: inline-block;
font: normal 14px/1 FontAwesome;
font-size: inherit;
color: #fff;
position: absolute;
padding: 4px;
font-size: 16px;
}
}
}
}
}
- 注册js及scss文件
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<template id="assets_end" inherit_id="web.assets_ backend">
<xpath expr="." position="inside">
<script src="/my_library /static/src/js/field_widget.js" type="text/javascript" />
<link href="/my_library/static/src/scss/field_widget.scss" rel="stylesheet" type="text/scss" />
</xpath>
</template>
</odoo>
- 添加color字段
color = fields.Integer()
- 在form视图添加color字段
<group>
<field name="date_release"/>
<field name="color" widget="int_color"/>
</group>
更新后如下图所示:
原理
让我们来了解下widget的生命周期:
- init(): 这是widget的构造函数。是widget初始化时最先被调用的函数。
- willStart(): 当widget初始化之后调用,被添加到DOM时调用。可用于异步初始化widget数据。它还应该返回一个延迟对象,可以简单地从super()调用获得该对象。我们将在后续菜谱中使用此方法。
- start(): 在widget渲染完成但尚未添加到DOM时调用。它对于后期渲染工作非常有用,并且应该返回一个延迟的对象。你可以在this.$el中访问一个已渲染的元素。
- destory(): 当widget被摧毁时调用。一般用于基础的清理工作,比如事件解绑。
重要信息
widget的基础类是Widget(web.Widget)。如果你想进一步了解该类,可在/addons/web/static/src/js/core/widget.js中查看。
步骤1,我们引入了AbstractField和fieldRegistry。
步骤2,我们创建AbstractField的扩展类colorField。通过该类,colorField将获得AbstractField的所有属性及方法。
步骤3,我们添加了三个属性: className用于定义根元素的类;tagName是根元素的标签;supportedFieldTypes代表当前widget可作用于哪些类型的field字段。在我们的例子中,我们创建了可用于整型字段的widget。
步骤4,我们映射了widget支持的事件。通过key是"事件的名称 CSS选择器",两者之间是空格。value是函数名。所以,当事件被触发的时候,函数将自动执行。本节,当用户点击了颜色圆点,将会在color字段设置所对应的整数值。
步骤5,我们重写了init方法,并设置了this.totalColors的值。通过该变量,决定展示的颜色圆点的个数。
步骤6,我们添加了_renderEdit和_renderReadonly函数。_renderEdit在编辑模式下调用,_renderReadonly在只读模式下调用。在编辑模式下,我们添加了代表不同颜色的标签。通过点击标签,我们可设置该字段的值,并将添加到this.$el中。$el是widget的根元素,将被添加到form视图。在只读模式下,我们仅展示当前字段代表的颜色。当前,我们通过硬编码的方式添加了颜色圆点,下一章,我们将通过JavaScript QWeb模板渲染小圆点。注意,我们再编辑模式下使用了在init()函数中设置的tottalColors属性值。
步骤7,我们添加了clickPill函数管理颜色圆点的点击事件。为了设置字段值,我们使用了_setValue方法。这个方法是AbstractField中的方法。当我们设置了字段值,odoo将渲染widget并再次调用_renderEdit方法。
步骤8,在定义完widget后我们需将其注册到web.field_registry。注意,所有视图的widget都会通过web.field_registry查找widget。所以如果你创建一个在list视图下展示字段的widget,也同样需要将其注册到web.field_registry中。
最后,我们将widget暴露出来,以便其他的模块也可以扩展或者继承。
更多
web.mixins命名空间定义了一组非常有用的类mixin。本章中我们已经使用过mixin了。AbstractField继承自Widget类,Widget继承自两个mixin。第一个是EventDispatcherMixin,可提供触发事件及捕获事件的接口。第二个是ServicesMixin,提供了RPC和动作所需的函数。
重要小贴士
当我们重载函数的时候,我们需要了解原始函数的返回值。一个常见的BUG是忘记返回父函数所需的对象,而引起报错。
Widgets可用于数据验证。通过isValid函数实现我们这方面的需求。
使用客户端侧的QWeb模板
正如以编程方式在JavaScript中创建HTML代码是一个坏习惯一样,您应该在客户端JavaScript代码中只创建最少数量的DOM元素。幸运的是,客户端也有模板引擎可用,更幸运的是,客户端模板引擎具有与服务器端模板相同的语法。
准备
我们将把DOM元素的创建移动到QWeb,使其更加模块化。
步骤
我们需要将QWeb定义添加到清单中,并更改JavaScript代码,以便我们可以使用它。请按照以下步骤启动:
- 导入web.core并提取qweb引用,如下代码所示:
odoo.define('my_field_widget', function (require) {
"use strict";
var AbstractField = require('web.AbstractField');
var fieldRegistry = require('web.field_registry');
var core = require('web.core');
var qweb = core.qweb;
...
- 修改从widget继承的_renderEdit方法,渲染元素
_renderEdit: function () {
this.$el.empty();
var pills = qweb.render('FieldColorPills', {
widget: this
});
this.$el.append(pills);
},
- 添加static/src/xml/qweb_template.xml:
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="FieldColorPills">
<t t-foreach="widget.totalColors" t-as='pill_no'>
<span t-attf-class="o_color_pill o_ color_#{pill_no} #{widget.value === pill_no and 'active' or ''}" t-att-data-val="pill_no"/>
</t>
</t>
</templates>
- 注册QWeb模板
"qweb": [ 'static/src/xml/qweb_template.xml',
],
原理
在CMS网站开发的第14章中,已经有了关于QWeb创建或修改模板基础的全面讨论,我们将在这里重点讨论它的不同之处。首先,在这我们处理的是JavaScript QWeb模板的实现,相对的是服务器侧python的实现。这就意味着,你不能访问数据集及上下文,你只可以访问传递给qweb.render函数的参数。
在我们的例子中,我们将当前对象赋值给widget。这就意味着你可以操作widget的JavaScript实现,并让模板访问相关属性及函数。由于我们可以访问小部件上可用的所有属性,我们可以通过检查totalColors属性来检查模板中的值。
由于客户端QWeb与QWeb视图无关,因此有一种不同的机制使web客户端知道这些模板,通过QWeb键将它们添加到相对于加载项根目录的文件名列表中的加载项清单中。
小贴士
如果不想在清单中列出QWeb模板,可以使用代码段上的xmlDependencies键来延迟加载模板。对于xmlDependencies,QWeb模板仅在小部件初始化时加载。
更多
在这里使用QWeb的原因是可扩展性,这是客户端和服务器端QWeb的第二大区别。在客户端,不能使用XPath表达式;需要使用jQuery选择器和操作。例如,如果我们想从另一个模块向小部件添加用户图标,我们将使用以下代码在每个小部件中添加一个图标:
<t t-extend="FieldColorPills">
<t t-jquery="span" t-operation="prepend">
<i class="fa fa-user" />
</t>
</t>
如果此处我们使用t-name属性,那么我们将使用原始模板的副本,而不动原始模板。t-operation的可能值还有append, before, after, inner及replace。正如其名,可将t元素中的内容追加目标元素内元素的最后、目标元素的前或后、替换目标元素的内容、替换目标元素及其内容。还有t-operation='attributes',可设置目标元素的属性值。
另一个不同之处是,客户端QWeb中的名称不是以模块名称命名的,因此您必须为模板选择名称,这些模板可能是安装的所有附加组件中唯一的,这就是为什么开发人员倾向于选择相当长的名称。
参考
如果您想了解有关QWeb模板的更多信息,请参阅以下几点:
- 与Odoo的其他部分相比,客户端QWeb引擎的错误消息和处理不太方便。一个小错误可能并不会影响程序的运行,因此这也就加大了初学者发现问题的难度。
- 幸运的是,odoo提供了一些客户端QWeb模板的调试模型。我们将在 “调试你的客户端代码”一节中学习。
通过RPC调用后端python方法
我们的widget需要从服务器查询数据。本节,我们将在颜色圆点上显示一个tooltip提醒。当我们鼠标悬停在小圆点上时,将展示那个颜色相关图书的数量。我们将通过RPC调用,获取特定颜色图书的数量。
准备
步骤
- 添加willStart函数并设置colorGroupData的值:
willStart: function(){
var self = this;
this.colorGroupData = {};
var colorDataPromise = this._rpc({
model: this.model,
method: 'read_group',
domain: [],
fields: ['color'],
groupBy: ['color'],
}).then(function(result){
_.each(result, function(r){
self.colorGroupData[r.color] = r.color_count;
});
});
return Promise.all([this._super.apply(this, arguments), colorDataPromise]);
},
- 更新_renderEdit函数,并设置tooltip:
_renderEdit: function(){
this.$el.empty();
var pills = qweb.render('FieldColorPills', {widget: this});
this.$el.append(pills);
this.$el.find('[data-toggle="tooltip"]').tooltip();
},
- 更新FieldColorPills模板并添加tooltip数据:
<t t-name="FieldColorPills">
<t t-foreach="widget.totalColors" t-as='pill_no'>
<span t-attf-class="o_color_pill o_color_#{pill_ no} #{widget.value === pill_no and 'active' or ''}" t-att-data-val="pill_no" data-toggle="tooltip" data-placement="top" t-attf-title="This color is used in #{widget.colorGroupData[pill_no] or 0 } books." />
</t>
</t>
更新模块,效果如下:
原理
willStart函数在渲染之前调用,并返回一个Promise对象,带对象需在渲染开始前生成。
我们依赖于ServiceMixin类的_rpc函数进行数据调用。该方法可实现调用模型的公开函数,如search、read、write等,在我们的例子中,我们使用了read_group函数。
步骤1,我们通过_rpc调用了read_group函数。我们以color分组获取每组颜色的数量。我们将color_count和color序号映射到colorGroupData中供QWeb模板使用。In the last line of the function, we resolved willStart with super and our RPC call using $.when. Because of this, rendering only occurs after the values are fetched and after any asynchronous action super that was busy earlier, has finished, too.
步骤2,初始化tooltip。
步骤3,我们通过colorGroupData设置tooltip的值。
小贴士
你可以在widget的任何地方调用_rpc函数。注意,这是一个异步调用函数。您需要正确地管理延迟对象,以获得所需的结果。
更多
The AbstractField class comes with a couple of interesting properties, one of which we just used. In our example, we used the this.model property, which holds the name of the current model (for example, library.book). Another property is this.field, which contains roughly the output of the model's fields_get() function for the field the widget is displaying. This will give all the information related to the current field. For example, for x2x fields, the fields_get() function gives you information about the co-model or the domain. You can also use this to query the field's string, size, or whatever other property you can set on the field during model definition.
Another helpful property is nodeOptions, which contains data passed via the options attribute in the
本文来自博客园,作者:老韩头的开发日常,转载请注明原文链接:https://www.cnblogs.com/xushuotec/p/14520539.html