文章目录
- 前言
- 一、原生表单实现方式
- 二、问题发现与分析
- 1.项目中遇到问题
- 2.问题具体分析
- 三、具体解决方法
- 第一步:把Span变成输入框
- 第二步:改写_renderFieldWidget
前言
Odoo作为快速搭建系统的框架,我们在利用它便捷高效功能的同时,有没有觉得有些页面不太好看呢?
今天我们一起来聊聊如何让odoo原生的form表单更美观更符合用户体验~
一、原生表单实现方式
Odoo为了极致的简约,字段的定义直接通过xml,然后渲染到页面上展示。
如果需要调整整体布局,将字段分成两列三列展示,可以使用group标签进行分组,如果想做成分页样式可以使用page标签。
虽然Odoo提供的整体样式,能够满足常见的业务场景,但是odoo对字段底层样式却没有那么灵活的配置选项;样式改变只能改变简单的必填与否,如果需求对字段进行比较复杂的操作或者指令,就必须通过widget来实现。
Odoo给我们也提供了很多原生的widget,比如实现多对多用widget=“many2many_tags”,枚举把下拉框变成单选框用widget=“radio”,时间格式只保留年月日用widget="date"等等
二、问题发现与分析
1.项目中遇到问题
问题起源于我们细致入微的产品经理,拿到我们做的表单之后,就说了一句话:丑,那一片空白是什么?多行之间是错行的?
2.问题具体分析
我们知道,Odoo原生页面就是字段为空值显示空白,不够美观,而且空白样式和输入样式的高度不同,就会使得group分组之后,每一行不是在一条水平线上。
在编辑模式下,可编辑字段是输入框,不可编辑字段仍然是空白,或者一段数据,对比一下,视觉上冲击力更强。
调试模式下,输入框是input标签,而只读字段的被渲染成span,稍微了解一点前端知识的同学都知道,span本身在页面上是看不到的,只有span中有文字才会显示出文字。
为了解决这种问题,让表格更好看,让整个页面看起来更赏心悦目。我们想办法对页面进行改造。根据以上发现,既然字段的属性不能解决这个问题,那就通过widget来实现,原生方法不可能考虑到所有使用场景,根据实际场景还是要自定义。
三、具体解决方法
第一步:把Span变成输入框
明确目标是想要把显示的span变成显示输入框,那就去找到加载页面的时候,渲染字段的方法。
对于不同的数据类型,加载的时候执行的模块和方法不同,那就要根据数据类型分批改造方法,以字符串类型为例,在保持原有数据功能不变的情况下,对其方法进行拓展。
在渲染字段的时候进行判断,如果是只读属性,就把本来要渲染的数据,放到输入框里,同时定义好输入框的宽度,和原生输入框保持一致,避免长短不一的情况
同样的我们可以对多对一关系、浮点类型、日期类型等进行改造,显示输入框。
以上改造的主要逻辑都是一样的,只不过不同数据类型封装的数组格式不同,需要从中取出展示在页面上的值。
除此之外,关于多对多类型有另外的方法,因为多对多类型本来就带有输入框,只是没有显示出来,而且页面上的值是通过关联表查出的自己改写显示起来比较复杂。需要把它原本的输入框显示出来即可。显示的样式和颜色从别的输入框抓取。
改写完成,使用的时候给字段绑上特定的widget就可以啦。
看下效果:
是不是舒服多了,编辑功能保留原有的就行,只是把只读的输入框和编辑的默认框高度宽度保持一致就好啦!
第二步:改写_renderFieldWidget
到这里,进度条已经走完80%了。
这时候你会发现,如果该字段有值时会显示输入框,但是为空时,还是一片空白,好像并没有完成成功……
这是因为字段加载空值的时候不会通过 _render方法;
我们需要改写 _renderFieldWidget 方法,在刚开始加载的时候,对所有情况进行统一,无论什么情况都执行我们改写的代码。
return FormRenderer.extend({
_renderFieldWidget: function (node, record, options) {
if (!this.renderInvisible && node.attrs.modifiers.invisible === true) {
return $();
}
options = options || {};
var fieldName = node.attrs.name;
// Register the node-associated modifiers
var mode = options.mode || this.mode;
var modifiers = this._registerModifiers(node, record, null, options);
// Initialize and register the widget
// Readonly status is known as the modifiers have just been registered
var Widget = record.fieldsInfo[this.viewType][fieldName].Widget;
const legacy = !(Widget.prototype instanceof owl.Component);
const widgetOptions = {
mode: modifiers.readonly ? 'readonly' : mode,
viewType: this.viewType,
};
let widget;
if (legacy) {
widget = new Widget(this, fieldName, record, widgetOptions);
} else {
widget = new FieldWrapper(this, Widget, {
fieldName,
record,
options: widgetOptions,
});
}
// Register the widget so that it can easily be found again
if (this.allFieldWidgets[record.id] === undefined) {
this.allFieldWidgets[record.id] = [];
}
this.allFieldWidgets[record.id].push(widget);
widget.__node = node;
// Prepare widget rendering and save the related promise
var $el = $('<div>');
let def;
if (legacy) {
def = widget._widgetRenderAndInsert(function () {
});
} else {
def = widget.mount(document.createDocumentFragment());
}
this.defs.push(def);
// Update the modifiers registration by associating the widget and by
// giving the modifiers options now (as the potential callback is
// associated to new widget)
var self = this;
def.then(function () {
// when the caller of renderFieldWidget uses something like
// this.renderFieldWidget(...).addClass(...), the class is added on
// the temporary div and not on the actual element that will be
// rendered. As we do not return a promise and some callers cannot
// wait for this.defs, we copy those classnames to the final element.
widget.$el.addClass($el.attr('class'));
$el.replaceWith(widget.$el);
self._registerModifiers(node, record, widget, {
callback: function (element, modifiers, record) {
element.$el.toggleClass('o_field_empty', !!(
record.data.id &&
(modifiers.readonly || mode === 'readonly') &&
false //我们需要改写的地方
));
},
keepBaseMode: !!options.keepBaseMode,
mode: mode,
});
self._postProcessField(widget, node);
});
return $el;
},
});
修改的位置源码是:element.widget.isEmpty(),我们改成false,字段数据为空的时候不生效。
在注册widget的时候引入这个文件就可以啦
再去页面看看,是不是好看多了~
最后,给页面上所有字段都绑定widget就大功告成了!
本次分享就到这里啦~~~
最后感谢小伙伴丁涛的技术支持和指导~
如有疑问,欢迎联系我们~