Mixoo

Hi, 好久不见


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

  • 搜索

Odoo与微信Oauth免登录模块

发表于 2021-02-23 | 分类于 odoo

微信Oauth登录

首先明确一点,这里说的微信Oauth登录指的是微信开放平台注册的应用,不是微信公众号的应用。因此,我们需要先到微信开放平台注册一个应用,因为我们要在odoo搭建的网站中使用,因此这里选择网站应用:

注册完账号之后,我们会得到应用的ID和密钥。

配置微信Oauth登录

拿到应用ID和密钥之后,我们就可以配置我们的微信登录模块了:

然后我们打开开发者模式,在菜单设置-用户-Oauth服务商中,启用微信开放平台:

测试

然后我们回到登录页面,可以看到出现了一个微信登录的按钮:

点击登录后,将跳转到扫码页面,用户扫描完成后确认登录将登录到自己的门户页面。

Odoo 阿里云 OSS对象存储模块

发表于 2021-01-20 | 分类于 odoo

我们知道odoo的附件默认是存储在磁盘上,这对我们单机服务器来说并没有什么不妥,但是如果我们使用的是应用集群,那么一个显而易见的问题就来了,我们应该如何保证每台服务器上的静态文件保持一致?

要保证多台服务器的体验一致,那么我们势必就要把这些静态文件统一到某个地方进行集中管理,通常我们的方案是使用文件服务器,也是现在互联网企业的最常见的解决方案之一。但我们今天要讲的不是试用文件服务器,而是现在各大云服务器厂商提供的对象存储功能。至于什么是对象存储,这里就不过多的讲了,请同学们自己去查找相关资料。

我们今天以阿里云OSS对象存储为例,来看一下应该如何解决上述的问题。

注册阿里云OSS

我们首先要去阿里云注册一个OSS账号,拿到应用的Access Key和Access Secret,这是我们管理OSS文件的凭证。然后我们选择一个地区,创建一个bucket,拿到endpoint和bucket name等参数,然后我们把这些参数填到阿里云模块的配置中:

附件

最近,我写的<<odoo开发指北>>一书中上线了关于附件和binary字段的详细介绍,有兴趣的同学可以关注我的公众号进行阅读。

我们先要处理附件的上传,最近

odoo POS 集成微信支付和支付宝支付

发表于 2021-01-12 | 分类于 odoo

最近有小伙伴有这方面的需求,市场上又没有十分靠谱的现成的解决方案,于是花了一天的时间把微信和支付宝的POS集成模块做了出来。这里记录一下开发的大概流程和使用介绍。

支付宝

我们之前是对接过支付宝在线支付的,而且因为支付宝有沙箱环境,因此,对接的比较流畅。我们先去支付宝开发中心注册一个开发者账号,然后可以拿到支付宝的沙箱环境账号和密钥。这里需要一部安卓手机,因为支付宝的沙箱钱包只有安卓版没有苹果版本。

沙箱应用中可以找到 APPID,设置密钥 和商家密钥等关键参数。

根据我们之前对接支付宝在线支付的经验,我们需要的参数有一下几个:

  • App Seller Id: 支付宝商家ID
  • Alipay App Id: 支付宝应用ID
  • Mechant Private Key: 商户私钥
  • Alipay Public Key: 支付宝RSA公钥
  • Sign Type: 加密方式

其中支付宝RSA公钥可以通过支付宝提供的生成工具生成,也可以直接自己生成,具体方法参考官方文档,这里不赘述了。

接下来,我们开始对接。首先,我们要搞清楚POS条码支付的流程(区别于用户主动扫码支付,这里称之为条码支付):

  1. 用户出示条码/二维码,实际是出示了自己条码支付的授权码
  2. 商户在通过扫描到自己的系统中之后,系统通过条码支付接口发起扣款请求
  3. 用户在手机端确认金额
  4. 商户系统扣款成功
  5. 用户收到扣款通知

其中第3步,通常为了支付的便捷,小额支付通常会跳过这一步。

因为支付过程中可能会碰到失败的情况,因此需要商户在系统中获取到用户支付中这个状态的时候,进行主动查询确认,一般经过三次尝试如果仍旧支付不成功时,调用撤销接口取消交易。对应到后台,我们需要三个接口:

  1. 支付接口
  2. 查询接口
  3. 撤销接口

支付接口

1
2
3
4
5
6
7
8
9
def barcode_pay(self, order_no, subject, auth_code, amount):
"""条码支付"""
# 条码支付
alipay = self._get_alipay_client()
_logger.debug(f"POS支付宝条码支付:{order_no},{subject},{amount}")
res = alipay.pay.trade_pay(
order_no, 'bar_code', auth_code, subject, total_amount=amount)
_logger.debug(f"POS支付宝支付结果:{res}")
return res

查询接口

1
2
3
4
5
6
def query_payment(self, trade_no):
"""查询支付结果"""
alipay = self._get_alipay_client()
res = alipay.pay.trade_query(trade_no=trade_no)
_logger.debug(f"POS支付宝交易查询结果:{res}")
return res

撤销接口

1
2
3
4
5
6
def cancel_payment(self, trade_no):
"""取消交易"""
alipay = self._get_alipay_client()
res = alipay.pay.trade_cancel(trade_no=trade_no)
_logger.debug(f"POS支付宝交易取消:{trade_no}")
return res

其中,查询和取消接口均可以使用商户订单号或者支付宝流水号,这里我们为了方便起见统一用的时支付宝流水号。

为了在POS系统中新增一个支付方法,我们需要新增一个支付方法,支付宝,并将我们之前讲到的参数放到设置里:

然后我们在门店的设置里新增这个支付方法:

新增之后,我们就可以在POS界面看到新增的支付方法了。

前端的对接相对比较复杂,我们要继承PaymentInterface对象,新生成一个PaymentAlipay 对象,负责处理前端的业务请求。

其中最主要的方法是处理后端传过来的响应并作出合适的反馈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
_alipay_handle_response: function (response) {
var line = this.pos.get_order().selected_paymentline;
var self = this;
if (response.code == '10000') {
self._alipay_success(response.trade_no)
self.pos.gui.close_popup();
}
else if (response.code == '10003') {
line.set_payment_status("pending")
this.pos.gui.show_popup('confirm', {
'title': '支付中',
'body': '等待用户付款',
'confirm': this.check_payment,
'cancel': this.cancel_payment,
'payment_method': line.payment_method,
'trade_no': response.trade_no
});
}
else if (response.code = '40004') {
line.set_payment_status("pending")
this.pos.gui.show_popup('error', {
'title': '支付失败',
"body": response.sub_msg
});
}
},

支付宝在支付以后,通常会返回一下几种状态的结果:

  • 10000: 支付成功
  • 10003: 用户支付中
  • 40004: 支付失败

其中 10003代表等待用户确认支付,一般出现在大额支付或者长期未使用的客户中,此时商户应该等待用户支付完成后,进行查询操作,然后完成后续的业务处理。

微信支付

微信支付的整体流程与支付宝一致,区别在于微信支付的金额单位是分,因此我们需要在处理过程中做一些处理,另外,微信的商户订单号并部支持空格,因此对于odoo默认的”Order 12345”这种订单号也需要做适配,同样地,我们在交易查询和交易撤销的接口中也应该使用微信的交易号而不是商户订单号。

效果展示

支付成功:

支付失败:

需要的同学欢迎到我的淘宝购买哦

odoo 阿里云短信模块

发表于 2020-12-27 | 分类于 odoo

odoo虽然自带短信模块,但是由于不适合国内的国情,因此作用并不是很大,今天给大家带来一款本地化的短信模块——阿里云短信模块。

注册阿里云短信

首先,我们要去阿里云短信注册一个账号,通过实名认证之后,我们会得到两个关键参数:Access Key和Access Secret,这两个参数是我们之后调用短信服务的凭证,请妥善保存。

得到Key和参数之后,我们需要给我们的短信建立一个签名,用来告诉用户这条短信的发送方,短信签名需要通过人工审核以后才可以使用。

添加短信模版

由于国内短信发送愈来愈严格的趋势,我们使用阿里云发送短信时必须要使用短信模版,短信模版是一种固定了大多数文本和少量变量的短信格式。例如:”您的短信验证码是:${code}【腾讯】”,这里我们就只能传入变量code和它的值,并不能输入更多的信息。

根据我们的业务需求,在阿里云短信中添加适量的短信模版,等待人工审核通过之后,我们可以得到一个短信模版的代码。这样当我们发送短信的时候就可以根据代码指定要发送的短信模版。

安装并设置阿里云短信模块

然后,我们安装阿里云短信模块,安装完成后,到设置中,把上面的三个参数填入到设置中。

然后我们在设置-技术-Phone/SMS-阿里云短信模版中将我们要使用的短信模版添加进去:

这样我们就完成了所有的短信设置。

发送测试

设置完之后,我们可以发一条短信测试一下我们的设置是否成功。在设置-技术-Phone/SMS-短信中新建一条短信:

服务提供商这里我们选择阿里云,短信模版这里我们以短信验证码为例,我们将参数写入到短信的body体中,然后点击发送按钮,即可完成发送操作。如果发送出现错误,会在错误信息中显示出具体的出错原因:

拓展

我们知道,各个企业在使用短信服务的场景各不相同,因此不可能将所有的场景涵盖在一个模块中,所幸的是,本模块拓展起来也很简单,开发者只需要在需要使用短信的场景中创建一个短信对象,然后调用send方法发送即可。

1
2
3
4
5
6
7
sms = self.env['sms.sms'].create({
"number":"18511112222",
"service_provider": "aliyun",
"aliyun_template":1,
"body":"{'code':'1234'}"
})
sms.send()

本模块已上架本人的淘宝店铺,欢迎选购哦。

Odoo使用百度NLP自动解析联系人地址

发表于 2020-11-20 | 分类于 odoo

话说,天下苦odoo的联系人地址久矣,不符合国情的情况下,输入还比较费劲。今天给大家带来一个快捷录入地址的模块——百度NLP模块

注册

既然要使用百度的服务,那么首先就得去他的百度云上注册账号。

注册完成后,去创建一个自然语言处理应用,并记录下应用的APPID、KEY和SECRET:

设置

然后,下载安装我们的百度NLP模块以后,在设置-百度NLP中填入你的应用ID,KEY和密钥:

在联系人中使用智能地址解析

然后我们就可以在联系人那里,方便地录入我们地联系人地址了,这里我们使用地址”北京市亦庄京东总部A座 刘强东 18611112222”作为示例:

对于联系人下面的子联系人类型,发票地址等也同样有效。

odoo 树形列表明细预览模块

发表于 2020-11-12 | 分类于 odoo

不知道你们是否碰到过这样的问题,在树形列表视图上想要显示x2many字段中某些字段的值。默认情况下,x2many如果显示在列表视图上,会显示为”x条记录”这个鬼样子,没有有效的信息。虽然,我们可以使用many2many_tags部件将其显示出来,但只能显示一个字段。

今天我们带来一个优雅一点的解决方案,list_preview_widget模块。这个模块内置了一个兼容char字段的小部件list_preview,我们只需要将要预览的部件使用这个小部件就可以预览明细里的字段了。

首先,我们安装好这个模块,然后在要添加预览的对象中新增一个文本字段,这里我们以stock.picking模块为例:

1
2
3
4
5
6
def _get_dispay_text(self):
for picking in self:
picking.display_text = ";".join(
f"型号:{line.product_variant or '无'},数量:{line.product_uom_qty},备注:{line.sale_note or''}" for line in picking.move_ids_without_package)

display_text = fields.Char("明细预览", compute="_get_dispay_text")

我们新增加了一个文本字段display_text,这个字段的值是计算方式获取的,以方便我们灵活定制要显示的内容。

然后,我们在视图中新增这个字段,并标记为list_preview部件:

1
2
3
4
5
6
7
8
9
10
<record id="view_picking_list" model="ir.ui.view">
<field name="name">view picking list</field>
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.vpicktree"/>
<field name="arch" type="xml">
<field name="partner_id" position="after">
<field name="display_text" widget="list_preview"/>
</field>
</field>
</record>

然后我们更新我们的自定义模块,可以看到分拣列表中已经有一个小图标,点击可以看到预览的内容:

有需要的同学欢迎到我的淘宝选购

Ubuntu 18.04 安装 odoo14

发表于 2020-11-06 | 分类于 odoo

odoo14 在 Ubuntu 18.04上安装并没有那么丝滑,首先碰到的错误就是:

1
2
3
4
5
6
The following information may help to resolve the situation:

The following packages have unmet dependencies:
odoo : PreDepends: init-system-helpers (>= 1.54~) but 1.51 is to be installed
Depends: python3-xlwt but it is not installable
E: Unable to correct problems, you have held broken packages.

意思就是找不到这个两个依赖包,既然系统中没有,那么我们手动来安装好了

init-system-helpers

我们使用wget命令获取init-system-helpers。

然后使用如下命令进行安装:

1
dpkg -i init-system-helpers_1.58_all.deb

python3-xlwt

同样的,我们使用wget下载python3-xlwt

然后使用命令安装:

1
dpkg -i python3-xlwt_1.3.0-3_all.deb

解决了这个两个依赖,再次安装odoo14就可以了。

odoo报表格式缺少样式

发表于 2020-10-27 | 分类于 odoo

适用环境 Ubuntu

odoo系统添加了HTTPS证书以后,打印出来的报表,缺失了css样式,后台可以查到的输出信息有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
QSslSocket: cannot resolve CRYPTO_set_id_callback
QSslSocket: cannot resolve CRYPTO_set_locking_callback
QSslSocket: cannot resolve sk_free
QSslSocket: cannot resolve sk_num
QSslSocket: cannot resolve sk_pop_free
QSslSocket: cannot resolve sk_value
QSslSocket: cannot resolve SSL_library_init
QSslSocket: cannot resolve SSL_load_error_strings
QSslSocket: cannot resolve SSLv3_client_method
QSslSocket: cannot resolve SSLv23_client_method
QSslSocket: cannot resolve SSLv3_server_method
QSslSocket: cannot resolve SSLv23_server_method
QSslSocket: cannot resolve X509_STORE_CTX_get_chain
QSslSockeQSslSocket: cannot resolve CRYPTO_num_lockst: cannot resolve OPENSSL_add_all_algorithms_noconf
QSslSocket: cannot resolve OPENSSL_add_all_algorithms_conf
QSslSocket: cannot resolve SSLeay
QSslSocket: cannot call unresolved function CRYPTO_num_locks
QSslSocket: cannot call unresolved function CRYPTO_set_id_callback
QSslSocket: cannot call unresolved function CRYPTO_set_locking_callback
QSslSocket: cannot call unresolved function SSL_library_init
QSslSocket: cannot call unresolved function SSLv23_client_method
QSslSocket: cannot call unresolved function sk_num
QSslSocket: cannot call unresolved function SSLv23_client_method
QSslSocket: cannot call unresolved function SSL_library_init
QSslSocket: cannot call unresolved function SSLv23_client_method
QSslSocket: cannot call unresolved function SSL_library_init
QSslSocket: cannot call unresolved function SSLv23_client_method
QSslSocket: cannot call unresolved function SSL_library_init
QSslSocket: cannot call unresolved function SSLv23_client_method
QSslSocket: cannot call unresolved function SSL_library_init
QSslSocket: cannot call unresolved function SSLv23_client_method
QSslSocket: cannot call unresolved function SSL_library_init
QSslSocket: cannot call unresolved function SSLv23_client_method
QSslSocket: cannot call unresolved function SSL_library_init
QSslSocket: cannot call unresolved function SSLv23_client_method
QSslSocket: cannot call unresolved function SSL_library_init
Exit with code 1 due to network error: UnknownNetworkError
QSslSocket: cannot call unresolved function CRYPTO_num_locks
QSslSocket: cannot call unresolved function CRYPTO_set_id_callback
QSslSocket: cannot call unresolved function CRYPTO_set_locking_callback

貌似是wkhtmltopdf缺少了某种解析的功能,通过google查到相关问题

找到解决方案(Ubuntu 18.04):

1
apt-get install libssl1.0-dev

odoo企业版安装部署说明

发表于 2020-10-23 | 分类于 odoo

企业版的本质

首先,我们需要知道的一点,企业版不是一个完全独立的版本,而是在社区版的基础上附加了一个包含若干模块的addons包组成的企业版。也就是说,任何一个版本的社区版,只要配置好企业版的代码包,就成为了企业版。

企业版具体包含了哪些模块,读者可以到社区版的模块列表中查看哪些需要升级的模块即可,也可以到官方服务器上进行实际版本的体验。

安装

这里我们假设读者已经安装好了社区版,结合前面讲的,安装企业版就变成了一件非常容易的事情。

  1. 首先,我们将企业版代码包解压到任意目录中,例如,我们这里解压到了/opt/enterprise-14.0目录中。
  2. 然后我们编辑配置文件 /etc/odoo/odoo.conf 中的addons_path参数,将企业版的目录填进去:

    1
    addons_path = /opt/odoo14/addons,/opt/enterprise-14.0
  3. 重启odoo服务

这样就完成了企业版的升级。

升级数据库

企业版安装完成以后创建的数据库都是企业版的数据库,那么如何将原来是社区版的数据库升级到企业版呢?

方法也是非常简单:

到应用-模块 中搜索web模块,然后点击升级,等待此模块升级完成以后,你的数据库就会变成企业版啦。

mixoo ❤ odoo

OWL 框架简介

发表于 2020-10-09 | 分类于 odoo

OWL是什么

从odoo 14.0开始,Odoo官方又一次更换了前端技术,推出了新一代的前端框架OWL(Odoo Web Libary)。OWL从odoo独立出来,成为了一个独立的前端框架。其特点是包含一个组件声明系统、基于钩子的响应系统、默认的并发支持和一个存储的前端路由。

为什么是OWL?

为什么odoo没有使用现有的成熟的前端框架比如React和Vue,而是要重新独立开发一套新的框架。odoo官方给出的一个长篇的理由解释。大概有如下几个理由:

  • odoo官方不想将前端技术依赖于外界的某个特定公司的产品,想要完全拥有技术的掌控权。
  • 尽管函数式编程是一种趋势,但是odoo并不想放弃一些务实的特性,比如继承。
  • React和Vue的现有的社区库,不能满足odoo动态编译延迟计算的需求。
  • odoo基于Xml布局,替换React和Vue的话要重写编译模块,费时费力且对开发者不够友好。
  • JIT编译和响应的需要
  • React和Vue的并发特性不满足Odoo前端的需求。

简而言之,就是odoo官方觉得市面上的产品没有可以直接拿来使用的,不如自己开发一套。

一个简单的例子

接下来,我们看一个简单的例子,这个例子将在页面中画一个按钮,单击按钮实现计数器自动加1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const { Component, useState } = owl;
const { xml } = owl.tags;

class Counter extends Component {
static template = xml`
<button t-on-click="state.value++">
Click Me! [<t t-esc="state.value"/>]
</button>`;

state = useState({ value: 0 });
}

class App extends Component {
static template = xml`
<div>
<span>Hello Owl</span>
<Counter />
</div>`;

static components = { Counter };
}

const app = new App();
app.mount(document.body);

首先从owl取Compent、useState和xml组件,然后定义一个Counter组件,使用内置模板,然后定义一个App,加载Counter组件,最后挂载到页面中。

实现的效果如下:

1.jpg

这样我们就完成了一个简单的OWL应用。

基础教程

接下来,我们来写一个学习应用todoApp。我们创建一个文件夹todoApp,里边包含一个index.html,app.css,app.js:

1
2
3
4
5
todoapp/
index.html
app.css
app.js
owl.js

然后编写index.html:

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>OWL Todo App</title>
<link rel="stylesheet" href="app.css" />
<script src="https://github.com/odoo/owl/releases/download/v1.0.11/owl.min.js"></script>
<script src="app.js"></script>
</head>
<body></body>
</html>

app.js:

1
2
3
(function(){
console.log("Hello OWL", owl.__info__.version);
})();

刷新页面,将可以从console中看到owl的版本号。

添加第一个组件

Owl应用是由组件组成的,每个应用只能有一个根组件,我们来定义一个App组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const {Component} = owl;
const {xml} = owl.tags;
const {whenReady} = owl.utils;

class App extends Component {
static template = xml`<div>todo App</div>`;
}

function setup(){
const app = new App();
app.mount(document.body);
}

whenReady(setup);

显示一些任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class App extends Component {
static template = xml/* xml */ `
<div class="task-list">
<t t-foreach="tasks" t-as="task" t-key="task.id">
<div class="task">
<input type="checkbox" t-att-checked="task.isCompleted"/>
<span><t t-esc="task.title"/></span>
</div>
</t>
</div>`;

tasks = [
{
id: 1,
title: "buy milk",
isCompleted: true,
},
{
id: 2,
title: "clean house",
isCompleted: false,
},
];
}

这里用到了qweb的技术,不熟的同学可以去查看相关资料。

添加一下样式

1
2
3
4
5
6
7
8
9
10
11
.task-list {
width: 300px;
margin: 50px auto;
background: aliceblue;
padding: 10px;
}

.task {
font-size: 18px;
color: #111111;
}

这里将完成的任务设置一下透明度,以显示区别:

1
<div class="task" t-att-class="task.isCompleted ? 'done' : ''">

对应的样式:

1
2
3
.task.done {
opacity: 0.7;
}

将任务抽象为子组件

任务应该抽离出来成为一个单独的子组件,拥有闭合的行为和外观。

Task组件表现为一个任务,但是它不能拥有任务的状态,状态仍然为App组件所拥有。因此,Task可以通过prop来获取数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// -------------------------------------------------------------------------
// Task Component
// -------------------------------------------------------------------------
const TASK_TEMPLATE = xml /* xml */`
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
<input type="checkbox" t-att-checked="props.task.isCompleted"/>
<span><t t-esc="props.task.title"/></span>
</div>`;

class Task extends Component {
static template = TASK_TEMPLATE;
static props = ["task"];
}

// -------------------------------------------------------------------------
// App Component
// -------------------------------------------------------------------------
const APP_TEMPLATE = xml /* xml */`
<div class="task-list">
<t t-foreach="tasks" t-as="task" t-key="task.id">
<Task task="task"/>
</t>
</div>`;

class App extends Component {
static template = APP_TEMPLATE;
static components = { Task };

tasks = [
{
id: 1,
title: "buy milk",
isCompleted: true,
},
{
id: 2,
title: "clean house",
isCompleted: false,
},
];
}

// -------------------------------------------------------------------------
// Setup code
// -------------------------------------------------------------------------
function setup() {
owl.config.mode = "dev";
const app = new App();
app.mount(document.body);
}

whenReady(setup);
  • 当我们定义一个子组件时,我们需要将它添加到components中。
  • Task中有一个props属性,它仅用于验证目的。
  • 仅当Owl的状态为dev时,验证机制才会生效。生产记得将dev去掉,否则会影响性能。

添加任务1

目前我们还是硬编码的任务,现在,我们将把任务的添加权限交还给用户:

1
2
3
4
5
6
7
8
<div class="todo-app">
<input placeholder="Enter a new task" t-on-keyup="addTask"/>
<div class="task-list">
<t t-foreach="tasks" t-as="task" t-key="task.id">
<Task task="task"/>
</t>
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
addTask(ev) {
// 13 is keycode for ENTER
if (ev.keyCode === 13) {
const title = ev.target.value.trim();
ev.target.value = "";
console.log('adding task', title);
// todo
}
}

tasks=[]

t-on-keyup绑定了按键事件,当用户按下enter键时,将任务名称输出到控制台中。
然后我们给input添加一个自动的焦点:

1
2
3
4
5
6

inputRef = useRef("add-input");

mounted(){
this.inputRef.el.focus();
}

添加任务2

目前为止,我们仅仅添加了控制台的输出,并没有将任务真正添加到页面中,接下来,我们将把任务真正地插入到列表中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

class App extends Component {
static template = APP_TEMPLATE;
static components = { Task };

nextId = 1;
tasks = [];

addTask(ev) {
// 13 is keycode for ENTER
if (ev.keyCode === 13) {
const title = ev.target.value.trim();
ev.target.value = "";
console.log('adding task', title);

if (title) {
const newTask = {
id: this.nextId++,
title: title,
isCompleted: false
};
this.tasks.push(newTask);
console.log(this.tasks);
}
}
}

inputRef = useRef("add-input");

mounted() {
this.inputRef.el.focus();
}

// tasks = []
}

运行后你会发现,任务并没有如我们期望的那样显示在界面中,但是打开console你会发现,代码如期地运行了。这里的问题在于owl不知道何时应该重新渲染页面,我们可以使用useState这个钩子来使tasks变得更reactive。

1
2
3
4
5
// on top of the file
const { useRef, useState } = owl.hooks;

// replace the task definition in App with the following:
tasks = useState([]);

改变任务状态

虽然我们可以在界面上添加任务并显示出来了,但是我们又发现了新问题:勾选了任务,并没有改变透明度。原因是没有改变任务的isCompleted属性。

这里有个问题,任务是由Task组件显示出来的,但是它并不是控件状态的所有者,我们需要与它的父控件App交互,我们可以在Task中触发事件,并在App中进行监听。

1
<input type="checkbox" t-att-checked="props.task.isCompleted" t-on-click="toggleTask"/>

input控件中添加对单击事件的监听。

1
2
3
toggleTask(ev) {
this.trigger('toggle-task', { id: this.props.task.id });
}

将task中的触发事件继续向上传递,在App中

1
<div class="task-list" t-on-toggle-task='toggleTask'>
1
2
3
4
toggleTask(ev) {
const task = this.tasks.find(t => t.id == ev.detail.id);
task.isCompleted = !task.isCompleted;
}

这样就实现了完成的任务透明度变浅的效果。

添加删除按钮

接下来,我们添加一个删除按钮:

1
2
3
4
5
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
<input type="checkbox" t-att-checked="props.task.isCompleted" t-on-click="toggleTask"/>
<span><t t-esc="props.task.title"/></span>
<span class="delete" t-on-click="deleteTask">🗑</span>
</div>

添加按钮删除事件:

1
2
3
deleteTask() {
this.trigger('delete-task', {id: this.props.task.id});
}

在App中监听delete-task事件:

1
<div class="task-list" t-on-toggle-task="toggleTask" t-on-delete-task="deleteTask">
1
2
3
4
deleteTask(ev) {
const index = this.tasks.findIndex(t => t.id === ev.detail.id);
this.tasks.splice(index, 1);
}

Store

Owl Store是Owl应用状态的管理中心,由React Redux and VueX开发。

使用Store重构的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
const { Component, Store } = owl;
const { xml } = owl.tags;
const { whenReady } = owl.utils;
const { useRef, useDispatch, useStore } = owl.hooks;

//
// Store
//
const actions = {
addTask({ state }, title) {
title = title.trim();
if (title) {
const task = {
id: state.nextId++,
title: title,
isCompleted: false
};
state.tasks.push(task);
}
},

toggleTask({ state }, id) {
const task = state.tasks.find((t) => t.id === id);
task.isCompleted = !task.isCompleted;
},

deleteTask({ state }, id) {
const index = state.tasks.findIndex((t) => t.id === id);
state.tasks.splice(index, 1)
}
};

const initialState = {
nextId: 1,
tasks: [],
};



// -------------------------------------------------------------------------
// Task Component
// -------------------------------------------------------------------------
const TASK_TEMPLATE = xml /* xml */`
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
<input type="checkbox" t-att-checked="props.task.isCompleted"
t-on-click="dispatch('toggleTask', props.task.id)"/>
<span><t t-esc="props.task.title"/></span>
<span class="delete" t-on-click="dispatch('deleteTask', props.task.id)">🗑</span>
</div>`;

class Task extends Component {
static template = TASK_TEMPLATE;
static props = ["task"];
dispatch = useDispatch();
}

// -------------------------------------------------------------------------
// App Component
// -------------------------------------------------------------------------
const APP_TEMPLATE = xml /* xml */`
<div class="todo-app">
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
<div class="task-list">
<t t-foreach="tasks" t-as="task" t-key="task.id">
<Task task="task"/>
</t>
</div>
</div>`;

class App extends Component {
static template = APP_TEMPLATE;
static components = { Task };

inputRef = useRef("add-input");
tasks = useStore((state) => state.tasks);
dispatch = useDispatch();

mounted() {
this.inputRef.el.focus();
}

addTask(ev) {
if (ev.keyCode == 13) {
this.dispatch("addTask", ev.target.value);
ev.target.value = ""
}
}
}

// -------------------------------------------------------------------------
// Setup code
// -------------------------------------------------------------------------
function setup() {
owl.config.mode = "dev";
const store = new Store({ actions, state: initialState });
App.env.store = store;
const app = new App();
app.mount(document.body);
}

whenReady(setup);

本地化存储任务

目前,我们的任务管理应用可以正常地增删,只是如果一旦刷新浏览器,数据就没了。接下来,我们将任务进行本地化存储:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

function makeStore() {
const localState = window.localStorage.getItem("todoApp");
const state = localState ? JSON.parse(localState) : initialState;
const store = new Store({ state, actions });
store.on("update", null, () => {
localStorage.setItem("todoApp", JSON.stringify(store.state));
})
return store;
}

function setup() {
owl.config.mode = "dev";
// const store = new Store({ actions, state: initialState });
// App.env.store = store;
App.env.store = makeStore();
const app = new App();
app.mount(document.body);
}

过滤任务

最后,我们希望给这个这个应用增加一个过滤系统,可以根据任务的状态进行过滤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// on top of file, readd useState:
const { useRef, useDispatch, useState, useStore } = owl.hooks;

// in App:
filter = useState({value: "all"})

get displayedTasks() {
switch (this.filter.value) {
case "active": return this.tasks.filter(t => !t.isCompleted);
case "completed": return this.tasks.filter(t => t.isCompleted);
case "all": return this.tasks;
}
}

setFilter(filter) {
this.filter.value = filter;
}

在页面中显示过滤按钮:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div class="todo-app">
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
<div class="task-list">
<t t-foreach="displayedTasks" t-as="task" t-key="task.id">
<Task task="task"/>
</t>
</div>
<div class="task-panel" t-if="tasks.length">
<div class="task-counter">
<t t-esc="displayedTasks.length"/>
<t t-if="displayedTasks.length lt tasks.length">
/ <t t-esc="tasks.length"/>
</t>
task(s)
</div>
<div>
<span t-foreach="['all', 'active', 'completed']"
t-as="f" t-key="f"
t-att-class="{active: filter.value===f}"
t-on-click="setFilter(f)"
t-esc="f"/>
</div>
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.task-panel {
color: #0088ff;
margin-top: 8px;
font-size: 14px;
display: flex;
}

.task-panel .task-counter {
flex-grow: 1;
}

.task-panel span {
padding: 5px;
cursor: pointer;
}

.task-panel span.active {
font-weight: bold;
}

至此,我们就使用OWL完成了一个简单应用的编写。

12…23
长腿叔叔

长腿叔叔

长腿叔叔的技术博客

229 日志
37 分类
41 标签
GitHub 淘宝
友情链接
  • BUG集散地
© 2021 长腿叔叔
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.2