odoo原生form表单改造成输入框

引子:

odoo作为快速搭建网站的框架,我们在利用它便捷高效功能的同时,有没有觉得这个页面,不太好看呢?

今天我们一起来聊聊如果让odoo原生的form表单更美观更符合用户体验~

 

Odoo原生实现方式

odoo为了极致的简约,字段的定义直接通过xml,然后渲染到页面上展示。

如果需要调整整体布局,将字段分成两列三列展示,可以使用group标签进行分组,如果想做成分页样式可以使用page标签······

虽然odoo提供的整体样式,能够满足常见的业务场景,但是odoo对字段底层样式却没有那么灵活的配置选项;

样式改变只能改变简单的必填与否,如果需求对字段进行比较复杂的操作或者指令,就必须通过widget来实现。

Odoo给我们也提供了很多原生的widget,比如实现多对多用widget="many2many_tags",枚举把下拉框变成单选框用widget="radio",时间格式只保留年月日用widget="date"等等

 

项目中遇到问题: 

问题起源于我们细致入微的产品经理,拿到我们做的表单之后,就说了一句话:丑,那一片空白是什么?

我们知道,odoo原生页面就是字段为空值显示空白,不够美观,而且空白样式和输入样式的高度不同,就会使得group分组之后,每一行不是在一条水平线上。

在编辑模式下,可编辑字段是输入框,不可编辑字段仍然是空白,或者一段数据,对比一下,视觉上冲击力更强。

调试模式下,输入框是input标签,而只读字段的被渲染成span,稍微了解一点前端知识的同学都知道,span本身在页面上是看不到的,只有span中有文字才会显示出文字。

就像下面这样:

 

看起来很空很丑,而且如果展示的字段比较多的话,只读字段高度会变化,横不能成行。

 

解决方法:

为了解决这种问题,让表格更好看,让整个页面看起来更赏心悦目。我们想办法对页面进行改造。

根据以上发现,既然字段的属性不能解决这个问题,那就通过widget来实现,原生方法不可能考虑到所有使用场景,根据实际场景还是要自定义。

明确目标是想要把显示的span变成显示输入框,那就去找到加载页面的时候,渲染字段的方法。

对于不同的数据类型,加载的时候执行的模块和方法不同,那就要根据数据类型分批改造方法,

以字符串类型为例,在保持原有数据功能不变的情况下,对其方法进行拓展

    var inputChar = FieldChar.extend({
        _render: function () {
            let self = this;
            this._super.apply(this, arguments);
            if (self.mode == 'readonly') {
                this.$el.text('');
                this.$el.css({
                    'width': '100%'
                });
                $('<input disabled style="width: 100%"/>').appendTo(this.$el).val(self.value || '')
            }
        }
    });

在渲染字段的时候进行判断,如果是只读属性,就把本来要渲染的数据,放到输入框里,同时定义好输入框的宽度,和原生输入框保持一致,避免长短不一的情况

同样的我们可以对多对一关系、浮点类型、日期类型等进行改造,显示输入框

var inputFloat = FieldFloat.extend({
        _render: function () {
            let self = this;
            this._super.apply(this, arguments);
            if (self.mode == 'readonly') {
                this.$el.text('');
                this.$el.css({
                    'width': '100%'
                });
                let value = self.value.toFixed(6).split('.')
                value[0] = value[0].replace(new RegExp('(\\d)(?=(\\d{3})+$)', 'ig'), "$1,")
                $('<input disabled style="width: 100%"/>').appendTo(this.$el).val(value.join('.') || '')
            }
        }
    });
var inputDate = FieldDate.extend({
        _render: function () {
            let self = this;
            this._super.apply(this, arguments);
            if (self.mode == 'readonly') {
                this.$el.text('');
                this.$el.css({
                    'width': '100%'
                });
                $('<input disabled style="width: 100%"/>').appendTo(this.$el).val(self.value._i || '')
            }
        }
    });
 var FieldSelection = FieldSelection.extend({
        _render: function () {
            let self = this;
            this._super.apply(this, arguments);
            if (self.mode == 'readonly') {
                this.$el.text('');
                this.$el.css({
                    'width': '100%'
                });
                $('<input disabled style="width: 100%"/>').appendTo(this.$el).val(self.value || '')
            }
        }
    });
var inputMany2One = FieldMany2One.extend({
        _render: function () {
            let self = this;
            this._super.apply(this, arguments);
            if (self.mode == 'readonly') {
                this.$el.text('');
                this.$el.css({
                    'width': '100%'
                });
                $('<input disabled style="width: 100%"/>').appendTo(this.$el).val(self.m2o_value || '')
            }
        }
    });

以上改造的主要逻辑都是一样的,只不过不同数据类型封装的数组格式不同,需要从中取出展示在页面上的值

除此之外,关于多对多类型有另外的方法,因为多对多类型本来就带有输入框,只是没有显示出来,而且页面上的值是通过关联表查出的自己改写显示起来比较复杂。

需要把它原本的输入框显示出来即可。显示的样式和颜色从别的输入框抓取。

var inputMany2Many = FieldMany2ManyTags.extend({
        _render: function () {
            let self = this;
            this._super.apply(this, arguments);
            if (self.mode == 'readonly') {
                self.$el.css({
                    'border': '1px solid #ccc',
                    'border-color': 'rgba(118, 118, 118, 0.3)',
                    'min-height': '25px',
                    'display': 'flex',
                    'background-color': '#F8F8F8',
                    'align-items': 'center'
                })
            }
        }
    });

改写完成,使用的时候给字段绑上特定的widget就可以啦

看下效果,

是不是舒服多了,编辑功能保留原有的就行,只是把只读的输入框和编辑的默认框高度宽度保持一致就好啦!

到这里,进度条已经走完80%了,

这时候你会发现,如果该字段有值时会显示输入框,但是为空时,还是一片空白,好像并没有完成成功qaq·······

这是因为字段加载空值的时候不会通过 _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就大功告成了,去页面看看,是不是好看多了~

 

 

------------------已在公司公众号推送-------------------

 

posted @ 2023-02-17 14:44  木_糖  阅读(175)  评论(0编辑  收藏  举报