本博客包含多个文档和书籍的翻译,但有能力者推荐阅读英文原版

Odoo 12开发者指南第四章 创建Odoo插件模块

Odoo Alan 6个月前 (05-13) 3336次浏览 1个评论

全书完整目录请见:Odoo 12开发者指南(Cookbook)第三版

本章中,我们将讲解如下内容:

  • 创建和安装一个新的插件模块
  • 完成插件模块的声明
  • 组织插件模块文件结构
  • 添加模型
  • 添加菜单项和视图
  • 添加访问权限
  • 使用脚手架命令来创建模块

技术准备

本章中,我们将预设你已安装了Odoo并且按照第一章 安装Odoo开发环境进行的操作。你还应熟悉第二章 管理Odoo服务器实例中所描述的查找和安装附加的插件模块。本章中所使用的代码可以从如下GitHub仓库中进行下载:https://github.com/alanhou/odoo12-cookbook/tree/master/Chapter04/my_library。

代码实时操作的视频请见:http://t.cn/E9PzDbm

引言

现在我们已经有了开发环境并且知道如何管理Odoo实例和数据库,可以学习如何创建Odoo插件模块了。

这里我们的主要目标是理解一个插件模块的结构是什么样的以及对其进行补充的典型增量工作流。本章中各节所讨论的各种组件会在后续章节中进行扩充讲解。

Odoo的插件模块是什么?

除框架代码以外,Odoo的所有基础代码都以模块的形式组合在一起。这些模块可以随时从数据库中安装或卸载。这些模块有两大目的。要么你可以添加新应用/业务逻辑,要么你可以修改已有应用。简言之,Odoo中的一切都始于模块也终于模块。

Odoo由不同规模的公司所使用,每个公司都有不同的业务流和要求。处理这一问题,Odoo将应用的功能拆分到了不同的模块中。这些模块可按需在数据库中进行加载。基本上,用户可以在任何时间点启用/禁用这些功能。因此,相同的软件可以按不同的要求进行调整。查看下面Odoo模块的截屏;该列中第一个模块是主应用,其它的模块为该应用添加功能而设计。让模块列表按应用分类进行分组,进入Apps菜单并对Category应用分组:

Odoo 12开发者指南第四章 创建Odoo插件模块

如果你计划在Odoo中开发新应用,应为不同功能设置边界。这有助于将你的应用切分为不同的插件模块。既然你已经知道了Odoo中插件模块的用途,我们可以开始构建自己的插件模块了。

创建和安装一个新的插件模块

这一节中,我们将新建一个模块,让其在我们的Odoo实例中可用并安装它。

准备工作

我们需要准备好一个Odoo实例来开始我们的开发。

如果我按照第一章 安装Odoo开发环境从源码轻松安装Odoo一节进行操作的话,Odoo应该在~/odoo-dev/odoo下。为方便讲解,我们假定Odoo安装在该路径下,但是你可以使用其它你自己偏好的路径。

我们还需要一个位置来安装自己的Odoo模块。就本节而言,我们将使用odoo的同级目录:~/odoo-dev/local-addon。

如何操作…

本章的示例中,我们将创建一个小型的插件模块来管理图书馆中的一系列图书。

如下步骤将创建并安装一个新的插件模块:

  1. 进入到工作目录即你要操作并放置新建的自定义模块的插件目录中:
  2. 为新模块选择一个技术名称并使用该名称作为模块名创建目录。本例中,我们将使用my_library:

    ℹ️模块的技术名称必须是有效的Python标识符,需以字母开头,仅包含字母、数字和下划线。建议在模块名称中只使用小写字母。

  3. 通过添加__init__.py文件来让Python模块可导入:
  4. 为Odoo添加一个最小化的模块声明来将其作为一个插件模块。创建__manifest__.py 文件并添加如下行:
  5. 启动你的Odoo实例,将你的插件目录添加到插件路径中:

    ℹ️如果在该Odoo命令中添加了 –save 选项,插件路径会被保存到配置文件中。下次你启动服务时,如果没有提供插件路径选项的话,就会使用它。

  6. 让这个新模块在Odoo中可用;使用管理员登录Odoo,启动开发者模式,然后在Apps顶级菜单中选择Update Apps List。现在Odoo就识别到了我们的模块了。
  7. 选择顶部的Apps菜单,在右上方的搜索栏中,删除默认的应用过滤器并搜索my_library,点击它的Install按钮,就完成了安装。

运行原理…

Odoo模块是一个包含代码文件及其它资源的目录。所使用的目录名为模块的技术名称。模块声明中的 name 键对应其标题。

__manifest__.py文件是模块的声明。它包含一个带有模块元数据的Python字典,有分类、版本、它所依赖的模块以及一系列它将要加载的数据文件。在这一节中,我们使用了最简化的声明文件,但在真实的模块中,我们将使用其它的重要争名。这将在下一节完成插件模块的声明是进行讨论。

模块目录必须是Python可导入的,因此即便目录为空也要添加一个__init__.py 文件。要载入一个模块,Odoo模块将会导入它。这会导致__init__.py文件中的代码被执行,它作为运行该模块Python代码的一个入口。因此,它通常会包含加载模块Python文件及子模块的一些重要声明语句。

已识别模块可通过命令行使用–init或-i选项直接安装。这一模块列表在提供插件路径新建数据库时获取到的模块进行初始化设置。可通过Update Module List菜单更新已有数据库。

完成插件模块的声明

声明文件对于Odoo模块非常重要。它包含插件模块重要的元数据并声明了应加载的数据文件。

准备工作

我们应当有一个包含__manifest__.py声明文件的模块来进行操作。你可能要按前一节的步骤来提供一个可以操作的模块。

如何操作…

我们为这个插件模块添加一个声明文件和一个图标:

  1. 以最相关的键名创建一个声明文件,编辑模块的__manifest__.py文件至内容如下:
  2. 为该模块添加一个图标,需选择一个PNG图像来供使用并将其拷贝至static/description/icon.png

运行原理…

声明文件中的内容是一个常规的Python字典,包含键和值。我们的示例声明中包含了大部分相关的键名:

  • name:这是该模块的标题。
  • summary:这是一个单行描述的副标题。
  • description:这是一个以普通文本或重构文本(RST)格式编写的长描述。通常放在三个引号中,Python中使用三个引号来界定多行文本。RST的快速入门指南请见http://docutils.sourceforge.net/docs/user/rst/quickstart.html。
  • author:是一个作者姓名的字符串。如果有多个作者的话,一般使用逗号来进行分隔,但注意它仍应是一个字符串,而非Python列表。
  • website:这个 URL 可供人们访问来了解模块或作者的更多信息。
  • category:这用于在网络上组织模块。标准的分类名称列表请见https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml。但也可以定义这些名称以外的分类名称。
  • version:这是该模块的版本号。可由Odoo应用商店用于检测已安装模块的新版本。如果版本号没有以Odoo目标版本号(如12.0)开始,会进行自动添加。但是,如果你显式的声明Odoo目标版本号信息量会充足,比如用12.0.1.0.0或12.0.1.0来替代1.0.0或1.0。
  • depends:这是该模块所直接依赖的模块技术名称列表。如果你的模块不依赖于任何其它插件模块,那么应至少添加一个base模块。别忘记包含这个模块所引用的XML ID、视图或模块的定义模块。那样会确保它们以正确的顺序进行加载,避免难以调试的错误。
  • data:这是在模块安装或升级时需加载数据文件的相对路径列表。这些路径相对于模块的根目录。通常,这些是XML和CSV文件,但也可以使用YAML格式的数据文件。这些内容会在第七章 模块数据中深入讨论。
  • demo:这是加载演示数据的文件的相对路径列表。仅在创建数据库时启用了Demo Data标记时都会进行加载。

模块图标使用的图像是位于static/description/icon.png的一个PNG文件。

小贴士:Odoo的大版本一般会有较大的变化,因此如不进行转化或迁移操作的话,一个大版本中构建的模块不大可能与下一个版本进行兼容。因此,在安装模块前确定Odoo的目标版本号就非常的重要了。

扩展内容

也可以使用一个单独的描述文件来替代模块声明中的长描述。自8.0版起,可通过后缀名为 .txt、.rst或.md(Markdown)的README文件来进行替换。此外,可在模块中包含一个description/index.html文件。

这个HTML描述会覆盖掉声明文件中所定义的描述。

还有一些常用的其它键名:

  • licence:默认值为LGPL-3。这一标识符用于模块对外使用的证书。其它可用的证书有AGPL-3、Odoo自有证书v1.0(多用于付费应用)以及其它OSI核准的证书。
  • application:如果为True,模块作为应用列出。通常这用于一个功能区的中央模块。
  • auto_install:若为True,表示这是一个胶水模块,在它所有的依赖模块安装后会被自动安装。
  • installable:若为True(默认值),表示该模块可以进行安装。
  • external_dependencies:有些Odoo模块内部使用了Python/bin库。如果你的模块使用了这些库,需要在这里进行添加。如果列出的模块在你的主机上没有安装的话则会停止该模块的安装。
  • {pre_init, post_init, uninstall}_hook:这是在安装/卸载时调用的Python函数钩子。更多详细示例,请见第九章 高级服务端开发技巧

组织插件模块文件结构

一个插件模块包含代码及其它资源文件,如XML文件和图像。大多数这些文件,我们都可以自由选择其在插件目录中的存放位置。

但是,Odoo使用了一些模块结构的惯例,建议遵循这一惯例。

准备工作

我们应当有一个插件模块目录,其中仅包含__init__.py和__manifest__.py文件。本节中,我们假定目录为local-addons/my_library。

如何操作…

执行如下步骤来为该插件模块创建基本结构:

  1. 为代码文件创建目录:
  2. 编辑模块的顶级 __init__.py文件,这样子目录中的代码会被加载到:

    这会给我们一个包含最常用目录的入手结构,类似下面这样:

运行原理…

为大家普及一下背景,Odoo插件模块可以有三种类型文件:

  • Python代码由 __init__.py加载,通过该文件导入.py文件及代码子目录。子目录中包含的Python代码,再由其内部的__init__.py导入。
  • 模块声明文件__manifest__.py中data和demo键名所声明供加载的数据文件通常是用户界面、fixture数据和演示数据中会使用到的XML和CSV文件。还可使用YAML文件,可以包含一些模块加载时运行的过程指令,例如,通过程序生成或更新记录而非在XML文件中加入数据。
  • 网页资源,如JavaScript代码和库、CSS、SASS和QWeb/HTML模板。这些文件用于在 UI 元素中构建UI组成部分及管理用户动作。它们通过继承主模板的XML文件来进行声明,用于为网页客户端或网站页面添加这些资源。

插件文件以如下目录进行组织:

  • models/包含后端代码文件,用于创建模型及其业务逻辑。推荐每个模型一个文件并使用与模型相同的名称,例如 library.book模型的对应文件为library_book.py。这些在第五章 应用模型中会进行深入讲解。
  • views/包含用于用户界面的XML文件,包含动作、表单、列表等等。类似于模型,建议每个模型一个文件。网站模块的文件名通常以_template后缀进行结尾。后端视图在第十章 后端视图中讲解,网站视图在第十五章 CMS网站开发中进行讲解。
  • data/包含模块初始数据的其它数据文件。数据文件在第七章 模块数据中进行讲解。
  • demo/包含带演示数据的数据文件,对于测试、培训或模块评测都非常有用。
  • Odoo会在i18n/中查找.pot及.po翻译文件。更多详情参见第十二章 国际化。这些文件无需在声明文件中提及。
  • security/包含定义访问控制列表的数据文件,通常是一个ir.model.access.csv文件,也可以是一个XML文件,用于定义权限组及行级权限的记录规则。参见第十一章 权限安全来获取更多内容。
  • controllers/包含网站控制器的代码文件,用于为模块提供各种功能。网页控制器在第十四章 网页服务端开发中进行讲解。
  • static/用于放置所有的网页资源。和其它目录不同,该目录名不只是一种惯例。这一目录中的文件无需用户登录即可对外提供访问。该目录多包含JavaScript、样式表、图像等文件。它们无需在模块声明文件中进行提及,但需要在网页模板中引用。这会在第十五章 CMS网站开发中进行讨论。

小贴士:在向模块添加新文件时,不要忘记在__manifest__.py(数据文件)或__init__.py(代码文件)文件中进行声明,否则会忽略这些文件而不进行加载。

添加模型

数据结构中定义的模型会用于我们的业务应用。这一节向读者展示如何为模块添加基本模型。

在我们的示例中,希望对图书馆的书籍进行管理。那么,我们需要创建一个代表书籍的模型。每本书包含书名及一名或多名作者。

准备工作

我们需要有一个模块来进行操作。如果你按照本章第一节创建和安装一个新的插件模块操作的话,则会有一个名为my_library的空模块。我们将使用它来做进一步讲解。

如何操作…

要添加新模型,我们需要添加一个Python文件来描述它,然后升级插件模块(如未安装则执行安装)。以下使用的路径为我们的插件模块目录中的相对路径(例如,~/odoo-dev/local-addons/my_library/):

  1. 为模块添加一个 Python 文件models/library_book.py,代码如下:
  2. 在模块中添加一个Python初始化文件models/__init__.py来加载代码,内容如下:
  3. 编辑模块的Python初始化文件来在模块中加载models/目录:
  4. 从命令行或用户界面中的Apps菜单中升级该Odoo模块。如果你在升级模块时仔细查看服务日志的话,会看到如下行:

然后,就可以在我们的Odoo实例中使用这一新的library.book模型了。有两种方式来查看我们的模型是否在数据库中进行了添加。

第一种方式是你可以在用户界面中进行查看。激活开发者工具,然后打开菜单Settings>Technical>Database Structure>Models。然后在那里搜索library.book模型。

第二种方式是查看PostgreSQL数据库中的表数据。你可以在数据库中搜索library_book数据表。在下面的代码示例中,我们使用了test-12.0作为数据库。但是你可以修改为你自己的数据库名:

运行原理…

第1步中我们在模块中创建一个了一个Python文件。

Odoo框架有着其自己的ORM框架。这一ORM对PostgreSQL数据库进行了抽象。通过继承Odoo Python类Model,我们可以创建自己的模型(数据表)。在定义新模型时,也将其加入到了中央模型仓库中。这让其它模块在之后对其进行修改变得更为容易。

模型有一些以下划线为前缀的通用属性。最重要的一个是_name,它提供了一个唯一内部标识符来在Odoo实例中进行使用。ORM会根据这个属性来生成数据表。本节中,我们使用了_name = ‘library.book’。基于这一属性,该ORM框架会创建一个名为library_book的新数据表。注意ORM在创建新表的名称时会通过将_name属性中的. 替换为 _ 。

模型字段以类型属性的方式进行定义。我们首先以Char类型定义了name字段。模型中有这一字段会非常方便,因为默认在其它模型中引用该模型时,它会用作记录描述。

我们还使用了一个关联字段的示例author_ids。这在图书和其作者之前建立了一个多对多的关联。一本书可以有多个作者,而每个作者也可以编写多本书。

有关模型有很多可以讨论的地方,我们将在第五章 应用模型中进行深度讲解。

接下来,我们必须让我们的模块可以识别到这一新的Python文件。这通过__init__.py文件来实现。因为将代码放在了models/子目录内,我们需要前述的__init__.py 文件来导入该目录,其中又应包含另一个__init__.py文件,在其中导入那里的每一个代码文件(本例中只有一个文件)。

对Odoo模型的修改通过升级该模型来启用。Odoo服务会处理由模型类到数据库结构变化的转换。

虽然此处没有提供示例,业务逻辑也可以在这些Python文件中进行添加,或通过对模型类添加新的方法,或继承已有方法,如create() 或 write()。这在第六章 基本服务端部署中进行讨论。

添加菜单项和视图

在有了我们的数据结构所需的模型之后,我们希望用户可以通过用户界面与它们进行交互。本节基于上一节的图书模型进行创建,添加一个菜单项来显示一个包含列表和表单视图的用户界面。

准备工作

需要有实现library.book模型的插件模块,这在上一节中已经提供到。使用的路径为相对插件模块所处位置的相对路径(如~/odoo-dev/local-addons/my_library/)。

如何操作…

要添加一个视图,我们将添加一个XML文件,其中包含对于模块的定义。因其是一个新模型,我们还应添加一个菜单选项来让用户可以访问它。

注意下述步骤的顺序也是很重要的,因为其它的一些会引用到在之前步骤中定义的ID:

  1. 创建一个XML文件views/library_book.xml来添加描述用户界面的数据记录:
  2. 在插件模块声明文件__manifest__.py中添加新的数据文件,通过views/library_book.xml来进行添加:
  3. 在library_book.xml文件中添加打开视图的动作:
  4. 在library_book.xml文件中添加菜单项,让其对用户可见:
  5. 在library_book.xml文件中添加一个自定义表单视图:
  6. 在library_book.xml文件中添加自定义树状(列表)视图:
  7. 在library_book.xml文件中添加自定义搜索选项:

从版本号12开始, admin 用户必须要获取相应的访问权限才能在用户界面中访问我们的模型。在没有赋予相应的访问权限时,Odoo是不会显示你的菜单和视图的在下一节中,我们将为我们的模型添加访问权限,在那之后你才可以通过admin用户来查看这些菜单和视图。

以超级用户访问 Odoo

通过将admin转换为超级用户superuser,你就可以不受访问权限的限制,因此无需授予访问权限即可访问所有菜单和视图。要将admin用户转换为superuser,先激活开发者模式。然后在开发者工具选项中,点击Become super user选项。

以下截图供您参考:

Odoo 12开发者指南第四章 创建Odoo插件模块

在成为超级用户之后,你的菜单会显示一个条状背景,如下图所示:

Odoo 12开发者指南第四章 创建Odoo插件模块

这时如果你试着升级模块,则会看到一个新的菜单选项(可能会需要刷新浏览器)。点击Books菜单会打开图书模型的列表视图,如下图所示:

Odoo 12开发者指南第四章 创建Odoo插件模块

运行原理…

在底层中,用户界面由存储在特定模型中的记录所定义。前两步中创建了一个空的XML文件来定义待加载的记录,然后将它们添加到模型的数据文件列表中来供安装。

数据文件可以放在该模型目录中的任意位置,但按照习惯用户界面的数据文件在views/子目录中定义。通常,这些文件的名称是基于模型的名称的。本例中,我们为library.book模型创建用户界面,因此我们创建了views/library_book.xml文件。

下一步是定义在客户端主区域用户界面中显示的窗口动作。该动作有一个由 res_model,定义的目标模型,name属性用于在用户打开动作时向用户所显示的标题。这些都是基本属性。窗口动作还支持其它属性,让我们对视图渲染方式拥有更多的控制,比如显示什么视图,为可用的记录添加过滤器或设置默认值。这些会在第十章 后端视图中进行讨论。

通常,数据记录使用<record>标签定义,在本例中我们为ir.actions.act_window模型创建了一条记录。这会创建窗口动作。

类似地,菜单项存储在ir.ui.menu模型中,我们可以使用<record>标签来进行创建。但是在Odoo中有一个名为<menuitem>的快捷标签,因此我们在本例中进行了使用。

以下是菜单项的主要属性:

  • name:这是要显示的菜单项文本。
  • action:这是要执行的动作的标识符。我们使用前一步中所创建的窗口动作ID。
  • sequence:这用于设置同级菜单项的显示顺序。
  • parent:这是父级菜单项的标识符。本例中的菜单项没有父级,即它会显示在顶级菜单中。

至此,我们还没有在模型中定义任何视图。但是,如果这时你升级了该模型,Odoo会自动地实时为你创建视图。可是,我们一定会想要控制视图的展示内容,因此,在接下来的两步中创建了一个表单视图和一个树状视图。

这两个视图通过 ir.ui.view 模型上的一条记录进行定义。我们所使用的属性如下:

  • name:这是标识视图的标题。在Odoo的源码中,你会发现这里重复使用了XML ID,但是你完全可以添加一个更易于阅读的名称作为标题。

    小贴士:如果省略了name字段,Odoo会使用模型名称及视图类型来生成一个。对于新模型的标准视图这完全没有问题。在继承视图时建议使用一个更具说明性的名称,因为这会让你在Odoo用户界面上查找具体视图时更为方便。

  • model:这是目标模型的内部标识符,和_name属性中的所定义的名称一致。
  • arch:这是视图结构,实际定义结构的地方。这里不同类型的视图会有不同。

表单视图在顶级<form>元素中定义,它的画布是一个两列网格。在表单内,<group>元素用于在垂直方向上编排字段。两个组会生成包含<field>元素所定义字段的两个列。字段根据数据类型使用其默认组件,但可以使用widget属性来指定所要使用的组件。

树状视图要简单些,它们以包含用于各列中显示的<field>元素的顶级<tree>元素定义。

最后,我们添加了搜索视图来在右上角的搜索框中扩展搜索选项。在顶级的<search>标签中,可以包含<field>和<filter>元素。字段元素是在搜索视图中可用于搜索的其它字段。过滤器元素为可通过点击激活的预置过滤条件。这些话题会在第十章 后端视图中进行讨论。

添加访问权限

在添加新的数据模型时,你需要定义谁可以创建、读取、更新和删除记录。在创建一个全新的应用时,这可能还包含新的用户组的定义。因此,如果用户没有访问权限的话,Odoo则不会该显示菜单和视图。在上一节中,我们将admin用户转换为超级用户来访问这个菜单。在学完本节后,你就可以直接以admin用户来访问我们图书模块的菜单和视图了。

这一节使用上一节中的定义的图书模型,会定义一个用户安全组来控制谁能够访问或修改书籍记录。

准备工作

我们会需要上一节中实现了library.book模型的插件模块,因为在这一节我们会为其添加权限规则。使用的是插件模块所在位置(例如~/odoo-dev/local-addons/my_library/)的相对路径。

如何操作…

我们要在本节中添加的安全规则如下:

  • 所有人均可阅读图书记录
  • 名为Librarians的新用户组拥有创建、阅读、更新和删除书籍记录的权限

要进行实现,我们需要执行如下步骤:

  1. 创建一个名为security/groups.xml的文件并添加如下内容:
  2. 添加一个名为security/ir.model.access.csv的文件并加入如下内容:
  3. 在__manifest__.py中添加这两个数据文件:

在实例中更新插件后即可使用新定义的权限规则了。

运行原理…

我们提供了两个新数据文件并添加到了插件模块的声明文件中,这样安装或升级该模块时会在数据库中加载它们:

  • security/groups.xml通过创建一条res.groups记录来定义一个新权限组。我们通过admin用户的引用ID base.user_admin为其授予了Librarians的权限,这样admin用户将拥有library.book模型的权限。
  • ir.model.access.csv通过组来关联模型的权限。第一行group_id:id列为空,表示该规则适用于所有人。最后一行授予了我们刚刚创建的组的成员所有权限:

小贴士:声明文件中data版块内文件的顺序非常重要,创建权限组的文件必须要在列出安全权限的文件之前加载,因为安全权限的定义依赖于组的存在。因为视图可具体到权限组,我们推荐将组的定义文件放在该列表中会更为保险。

其它内容

使用脚手架命令来创建模块

在新建 Odoo 模块时,需要设置一些范本代码。为帮助大家快速新建模块,Odoo提供了scaffold(脚手架)命令。

本节向你展示如何使用scaffold命令新建一个模块,它会创建一个目录和文件结构来供使用。

准备工作

我们将在一个自定义模块目录中新建一个插件模块,因此需要已安装Odoo并且给自定模块一个目录。假定Odoo安装的位置为~/odoo-dev/odoo,我们的自定义模块会放置在~/odoo-dev/local-addons目录中。

如何操作…

我们将使用scaffold命令来创建样例代码。按照给定的步骤来使用scaffold命令新建一个模块:

  1. 更改工作目录到想要放置模块的地方。它可以是你所选择的任意目录,但需要在一个插件路径中才可进行使用。按照我们在前面小节中所选择的目录,应该是这样的:
  2. 为这个新模块选择一个技术名称,并使用scaffold命令来创建它。本例中,我们选择的是my_module:
  3. 编辑默认声明文件__manifest__.py并修改相应的值。你一定会想要修改name键中的模块标题。

以下是生成的插件模块的结构:

现在你应编辑各个生成的文件来适应你对新模块的需求。

运行原理…

scaffold命令创建基于一个模板的新模块结构。

默认,这个新模块在当前工作目录中进行创建,但我们也可以指定一个目录来创建该模块,将其作为一个额外的参数进行传递。

参考如下示例:

此处使用了一个默认模板,但也可以为网站主题编写的主题模板。可以使用 -t选项来选择一个指定的模板。我们也可以使用包含模板的一个目录来作为路径。

这表示我们可以通过scaffold命令来使用我们自己的模板。内置的主题可作为一个向导,可以在./odoo/cli/templates这个Odoo子目录中找到。我们可以用类似下面的命令来使用我们自己的模板:

喜欢 (13)
[]
分享 (0)
发表我的评论
取消评论

表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
(1)个小伙伴在吐槽
  1. 签到成功!签到时间:2019-06-26 11:06:33,每日打卡,生活更精彩哦~
    wind2019-06-26 14:33 回复