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