第十四章、CMS网站开发**

Odoo有一个功能齐全的内容管理系统(CMS)。通过拖放功能,你的最终用户可以在几分钟内设计一个页面,但是在Odoo CMS中开发一个新功能或构建块就不是那么简单了。在本章中,您将探索Odoo的前台开发。您将学习如何创建网页。您还将学习如何创建用户可以在页面上拖放的构建块。进阶内容,如Urchin跟踪模块(UTMs),搜索引擎优化(SEO),多网站,GeoIP,和网站地图也涵盖在这一章。简而言之,您将了解开发交互式网站所需的所有内容。

重要信息

所有的Odoo CMS功能都是通过website和web_editor模块实现的。如果您想了解CMS在内部是如何工作的,请查看这两个模块。你可以在这里找到代码在行动视频:http://bit.ly/2UH0eMM。

本章将涵盖如下内容:

  1. 管理静态资源
  2. 为网站添加CSS及JavaScript
  3. 创建或修改QWeb模板
  4. 配置动态路由
  5. 为用户提供静态代码片段
  6. 为用户提供动态代码片段
  7. 获取网站用户输入的数据
  8. 管理SEO配置项
  9. 管理站点地图
  10. 获取访客的国家信息
  11. 跟踪营销活动
  12. 管理多网站
  13. 重定向老的URL
  14. 发布网站

管理静态资源

现代网站包含了大量的JavaScript和CSS文件。当页面加载到浏览器中时,这些静态文件向服务器发出单独的请求。请求次数越多,网站速度越慢。为了避免这个问题,大多数网站通过组合多个文件来提供静态资产。市场上有一些工具可以管理这类东西,但是Odoo有自己的实现来管理静态资产。

什么是资源包以及有哪些资源?

在Odoo中,静态资产管理并不像在其他应用中那么简单。Odoo有很多不同的应用程序和代码库。不同的Odoo应用程序有不同的用途和ui。这些应用程序不共享公共代码,所以在某些情况下,我们想加载一些资产,但我们不想对所有情况都这样做。在页面上加载不必要的静态资产不是一个好做法。为了避免在所有应用程序中加载额外的资源,Odoo使用了资源包的概念。资产包的工作是将所有JavaScript和CSS组合在一个文件中,并通过最小化它来减少其大小。Odoo代码库中有资产包,不同的代码库也有不同的资产包。

以下是Odoo中使用的不同资产包:

  • web.assets_common: 这个资产包包括所有应用程序通用的所有基本实用程序,如jQuery、Underscore.js、Font Awesome等。该资产包用于前端(网站)、后端、销售点、报告等。这个公共资产在Odoo几乎无处不在。它还包含用于Odoo模块系统的boot.js文件。
  • web.assets_backend: 这个资产包用于Odoo(企业资源规划(ERP)部分)的后端。它包含与web客户机、视图、字段小部件、操作管理器等相关的所有代码。
  • web.assets_frontend|website.assets_frontend: 这个资产包用于Odoo的前端(网站部分)。它包含了所有相关的代码到网站端应用程序,如电子商务、博客、在线事件、论坛、实时聊天等。注意,这个资产包不包含与网站编辑或拖放特性(网站构建器)相关的代码。这背后的原因是,我们不想加载编辑器资产的公共使用的网站。
  • website.assets_editor|web_editor.summernote: 这个资产包包含与网站编辑片段选项和拖放功能(网站构建器)相关的代码。只有当用户拥有编辑权限时,才会在网站上加载它。它也被用于群发邮件的设计者。
  • web.report_assets_common: QWeb报告只是从HTML生成的PDF文件。该资产被加载到报告布局中。

重要信息

还有一些其他用于特定应用的资产包:point_ of_sale.assets, survey.survey_assets, mass_mailing. layout, and website_slides.slide_embed_assets

Odoo通过AssetBundle类来管理它的静态资产

,它位于/odoo/addons/base/models/assetsbundle.py。现在,AssetBundle不仅可以组合多个文件;它还有更多的功能。以下是它提供的特性列表:

  • 它结合了多个JavaScript和CSS文件。
  • 它通过从文件内容中删除注释、额外空格和回车来减少JavaScript和CSS文件。删除这些额外的数据将减少静态资产的大小,并提高页面加载速度。
  • 它内置了对CSS预处理器的支持,比如SCSS和LESS。这意味着您可以添加SCSS和更少的文件,它们将被自动编译并添加到包中。

自定义资源

正如我们所看到的,Odoo针对不同的代码库有不同的资产。要获得正确的结果,您需要选择正确的资产包,将定制的JavaScript和CSS文件放入其中。例如,如果你正在设计一个网站,你需要把你的文件加载到web.assets_frontend。虽然这种情况很少见,但有时您需要创建一个全新的资产包。您可以创建自己的资产包,我们将在下一节中进行描述。

步骤

  1. 创建QWeb模板并添加JavaScript、CSS或SCSS文件,如下所示:
<template id="my_custom_assets" name="My Custom Assets">
<link rel="stylesheet" type="text/scss" href="/my_module/static/src/scss/my_scss.scss"/>
<link rel="stylesheet" type="text/css" href="/my_module/static/src/scss/my_css.css"/>
<script type="text/JavaScript" src="/my_module/static/src/js/widgets/my_ JavaScript.js"/>
</template>
  1. 使用t-call-assets在QWeb模板中,你想加载这个包,如下:
<template id="some_page">
...
<head>
<t t-call-assets="my_module.my_custom_assets" t-js="false"/>
<t t-call-assets="my_module.my_custom_assets" t-css="false"/>
</head>
...

原理

步骤1,我们使用my_custom_assets外部ID创建了新的QWeb模板。在这个模板中,您需要列出所有的CSS、SCSS和JavaScript文件。首先,Odoo会将SCSS文件编译成CSS,然后将所有CSS和JavaScript文件合并成一个单独的CSS和JavaScript文件。

步骤2,我们已经在模板中加载了CSS和JavaScript资源。t-css和t-js属性只用于加载样式表或脚本。

重要信息

在大多数网站开发中,您需要将JavaScript和CSS文件添加到现有的资产包中。添加新的资产包是非常罕见的。只有当你想开发没有Odoo CMS功能的页面/应用程序时才需要它。在下一个菜谱中,您将学习如何将自定义CSS/JavaScript添加到现有的资产包中。

更多

在Odoo中调试JavaScript非常困难,因为AssetBundle会将多个JavaScript文件合并到一个文件中,并将其最小化。通过使用资产启用developer模式,您可以跳过资产绑定,页面将单独加载静态资产,这样您就可以轻松调试。

组合资产生成一次并存储在ir中。附件的模型。在那之后,它们从附件中被送达。如果你想重新生成资产,你可以通过调试选项,如下图所示:

小贴士

正如你所知,odoo只会产生一次资产。这是一个头痛问题,因为它需要频繁重启服务器。为了解决这个问题,您可以在命令行中使用dev=xml,这将直接加载资产,因此不需要重新启动服务器。

为网站添加CSS及JavaScript

在本节,我们将介绍如何向网站添加CSS和JavaScript。

准备

我们使用第三章,创建odoo模块,中的my_library模块。你可以从 [GitHub](https://github. com/PacktPublishing/Odoo-14-Development-Cookbook-Fourth- Edition/tree/master/Chapter14/00_initial_module/my_library) 下载。我们将添加CSS、SCSS和JavaScript文件,这些文件将修改网站。因为我们正在修改网站,我们将需要添加网站作为依赖。像这样修改清单文件:

...
'depends': ['base', 'website'],
...

步骤

  1. 添加一个名为views/templates.xml的文件,并添加一个空视图覆盖,如以下(不要忘记在__manifest__.py中列出文件):
<odoo>
<template id="assets_frontend" inherit_id="web.assets_frontend">
<xpath expr="." position="inside">
<!-- points 2 & 3 go here /-->
</xpath>
</template>
</odoo>
  1. 添加CSS和SCSS文件的引用,如下所示:
<link href="/my_library/static/src/css/my_library.css" rel="stylesheet" type="text/css"/>
<link href="/my_library/static/src/scss/my_library.scss" rel="stylesheet" type="text/scss"/>
  1. 添加一个引用到你的JavaScript文件,如下所示:
<script src="/my_library/static/src/js/my_library.js" type="text/javascript" />
  1. 在静态/src/ CSS /my_library中添加一些CSS代码。css,如下:
body main {
background: #b9ced8;
}
  1. 在静态/src/ SCSS /my_library中添加一些SCSS代码。scss,如下所示:
$my-bg-color: #1C2529;
$my-text-color: #D3F4FF; nav.navbar {
background-color: $my-bg-color !important;
.navbar-nav .nav-link span {
color: darken($my-text-color, 15);
font-weight: 600;
}
}
footer.o_footer {
background-color: $my-bg-color !important;
color: $my-text-color;
}
  1. 在static/src/js/my_library.js中添加一些JavaScript代码,如下所示:
odoo.define('my_library', function (require) {
var core = require('web.core');
alert(core._t('Hello world'));
return {
// if you created functionality to export, add ithere
}
});

更新你的模块后,你应该看到Odoo网站在菜单、正文和页脚有自定义颜色,并且在每个页面加载时都有一个有点烦人的Hello World弹出窗口,如下图所示:

原理

odoo的CMS依赖于名为QWeb的XML模板引擎,我们将在下一节中详细介绍。资源包通过QWeb模板引入。在步骤1、2、3中,我们扩展了web.assets_frontend文件加载样式及js文件。我们选择web.assets_frontend是因为每一个网页都会加载这些文件。

步骤4,我们添加了CSS文件,用于设置网站的背景颜色。

小贴士

对于CSS/SCSS文件,有时顺序很重要。因此,如果您需要覆盖在另一个附加组件中定义的样式,则必须确保您的文件在您想要修改的原始文件之后加载。这可以通过调整视图的优先级字段或直接继承附加组件的视图来实现,该视图将引用注入到CSS文件中。详细信息,请参阅第9章“后端视图”中的“更改现有视图-视图继承配方”。

步骤5,我们添加了SCSS文件。odoo支持SCSS的预处理程序,将自动将SCSS编译为CSS文件。在我们的例子中,我们设置了几个变量及使用了darken的函数(可将$my-text-color变暗15%)。SCSS预处理器还有很多其他功能;如果你想了解更多关于SCSS的信息,请参考http://sass-lang.com/。

步骤6,我们添加了js文件,用于在页面加载完后弹框。为了避免JavaScript的排序问题,Odoo使用了一种非常类似于RequireJS的机制。在我们的JavaScript文件中,我们调用了odoo.define(),它需要两个参数:您想要定义的名称空间和包含实际实现的函数。如果您正在开发一个广泛使用JavaScript的复杂产品,那么您可以将代码划分为逻辑上不同的部分,并在不同的函数中定义它们。这将非常有用,因为您可以通过require导入函数来重用它们。此外,要定义模块的命名空间,请添加附加组件的名称,将其作为前缀,并用点分隔,以避免将来的命名冲突。如web模块下的,web.core和web.data。

对于第二个参数,definition函数只接收一个参数require,这个函数可以用来获取对其他模块中定义的JavaScript名称空间的引用。在所有与Odoo的交互中使用这个,并且永远不要依赖全局Odoo对象。

然后,您自己的函数可以返回一个对象,该对象指向您希望为其他附加组件提供的引用,或者如果没有此类引用,则不指向任何引用。如果你已经从你的函数返回了一些引用,你可以在另一个函数中使用它们,如下面的例子所示:

odoo.define('my_module', function (require) {
var test = {
key1: 'value1',
key2: 'value2'
};
var square = function (number) {
return 2 * 2;
};
return {
test: test,
square: square
}
});
// In another file
odoo.define('another_module', function (require) {
var my_module = require('my_module');
console.log(my_module.test.key1);
console.log('square of 5 is', my_module.square(5));
});

更多

为了提高性能,Odoo只在前端加载最少的JavaScript。一旦页面被完全加载,资源中的所有其他JavaScript将被惰性加载,并且最小的可用资源拥有web.assets_frontend_minimal_js ID。

创建或修改QWeb模板

我们将在第四章“应用模型”中开发的my_library附加组件中添加网站功能。我们感兴趣的是允许用户浏览图书馆,如果他们以适当的权限登录,允许他们从网站界面编辑图书详细信息。

准备

本节,我们将使用来自https://github.com/ PacktPublishing/oodoo-14-developing-cookbook-fourth-edition /tree/master/Chapter14/00_initial_module/my_library目录的my_library,该目录来自本书的GitHub存储库。

步骤

  1. 在controllers/main.py中添加展示图书列表的控制器,如下所示:
from odoo import http
from odoo.http import request
class Main(http.Controller):
@http.route('/books', type='http', auth="user",
website=True)
def library_books(self):
return request.render('my_library.books', {
'books': request.env['library.book'].search([]),
})
  1. 在views/templates.xml中添加最小模板(确保您已经在清单中添加了views/templates.xml文件):
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="books">
<t t-call="website.layout">
<!-- Add page elements here -->
</t>
</template>
</odoo>
  1. 在website.layout,添加可拖拽元素(class="oe_structure"),如下:
<div class="oe_structure">
<section class="pt32 pb32 bg-secondary oe_custom_bg">
<div class="container text-center">
<h1> Editable text and supports drag and drop.</h1>
</div>
</section>
</div>
  1. 在website.layout中添加代码块,以展示图书的信息,如下:
<div class="container">
<t t-foreach="books" t-as="book">
<div t-attf-class="card mt-3 #{'bg-info' if book_ odd else ''}">
<div class="card-body" id="card_body">
<h3 t-field="book.name"/>
<t t-if="book.date_release">
<div t-field="book.date_release" class="text-muted"/>
</t>
<b class="mt8"> Authors </b>
<ul>
<li t-foreach="book.author_ids" t-as="author">
<span t-esc="author.name" />
</li>
</ul>
</div>
</div>
</t>
</div>
  1. 在website.layout中添加不可编辑的元素:
<section class="container mt16" contenteditable="False"> This is a non-editable text after the list of books. </section>

在浏览器中打开http://your-server-url:8069/books,您将能够看到图书列表和作者。通过这段代码,用户可以看到图书及其详细信息的列表。如果有适当的权限,用户还可以更改图书细节和其他文本。

原理

步骤1,我们有一个路由处理器接收用户自定义参数。这些参数将从处理器传递给QWeb模板。

步骤2、3、4、5,我们创造了一个名为Books的模板,用于生成用于展示图书的HTML的代码。代码由t元素包裹,并通过t-call属性调用website.layout模板,odoo将渲染website.layout模板,并将我们生成的HTML代码插入其中。website.layout包含必要的文件,比如Bootstrap、JQery、Font Awesome等。这些文件用于设计web页面。website.layout还包含了默认的头部、尾部、代码块及页面编辑功能。这样,我们得到一个完整的Odoo网页与菜单,页脚,页面编辑功能,而不必重复代码在所有页面。

步骤3、4、5,我们再website.layout中添加了HTML代码及QWeb模板的属性。HTML将展示图书的列表。一些常用的QWeb属性及他们用法如下:

循环

要处理记录集或可迭代数据类型,你需要一个机构循环遍历列表,t-foreach,单个元素通过t元素实现。如下:

<t t-foreach="[1, 2, 3, 4, 5]" t-as="num">
<p><t t-esc="num"/></p>
</t>

渲染后结果如下:

<p>1</p>
<p>2</p>
<p>3</p>
<p>4</p>
<p>5</p>

你可以在任意元素中使用t-foreach及t-as属性。这时,在迭代器中,这个元素及其内容将会重复渲染。如下将生成上面一样的HTML代码:

<p t-foreach="[1, 2, 3, 4, 5]" t-as="num">
<t t-esc="num"/>
</p>

在t-foreach循环中,还有几个额外的变量,变量名将根据t-as设置的value组合而来。如前面的t-as=book的例子,book-odd变量将在迭代次数为奇数时为True,偶数时为False。在本例中,我们使用这个方法来为我们的卡片设置交替的背景颜色。

以下是其他可用的变量:

  • book_index: 将返回当前迭代的序号(0开始)
  • book_first、book_last: 如果迭代号第一个或者最后一个时为True
  • book_value: 如果book是一个字段,那么将返回他的值,本案例中将返回字典所有的键值。
  • book_size: 返回列表的大小。
  • book_even、book_odd: 偶数、基数为True
  • book_parity: 在迭代时,偶数索引包含偶数值,奇数索引包含奇数值。

重要小贴士

这里的示例基于我们的场景。在本例中,您需要用t-as属性的给定值替换book。

动态属性

QWeb模板可以动态设置属性值。这可以通过以下三种方式实现。

第一种方法是通过t-att-$attr_name。在模板呈现时,创建了一个属性$attr_name;它的值可以是任何有效的Python表达式。这是通过当前上下文计算的,结果设置为属性的值,如下所示:

<div t-att-total="10 + 5 + 5"/>

渲染后为:

<div total="20"></div>

第二种方法是通过t-attf-$attr_name。这与前面的选项类似。唯一的区别是只有字符串之间的{{..}}和#{…}会被计算。主要用于计算类,如下例所示:

<t t-foreach="['info', 'danger', 'warning']" t-as="color">
<div t-attf-class="alert alert-#{color}">
Simple bootstrap alert
</div>
</t>

渲染后为:

<div class="alert alert-info">
Simple bootstrap alert
</div>
<div class="alert alert-danger">
Simple bootstrap alert
</div>
<div class="alert alert-warning">
Simple bootstrap alert
</div>

第三种方法是通过t-att=mapping属性。该选项在将呈现字典数据的模板转换为属性和值之后接受字典。看看下面的例子:

 <div t-att="{'id': 'my_el_id', 'class': 'alert alert- danger'}"/>

渲染后如下:

<div id="my_el_id" class="alert alert-danger"/>

Fields

h3和div标签使用t-field属性。t-field的值必须是长度为1的数据集。这可以在页面以编辑模式打开的时候可编辑。当用户保存后修改的值可更新到数据库。当然,当前用户必须具备访问权限才可以哦。通过t-options属性,你可以将一个字典传递给字段渲染器,包括想要使用的widget。目前,后端还没有大量的小部件集合,所以这里的选择有点有限。例如,你想展示一个图片,可如下:

<span t-field="author.image_small" t-options="{'widget': 'image'}"/>

t-field有一些限制。它仅作用于数据集且不能用于元素。你需要使用诸如或

的HTML元素。t-esc与t-field类似,但它并不局限于数据集,并且可用于各种类型的字段,但是它不可编辑。 另一个不同点是,t-field是会根据用户语言调整展示值的。而t-esc展示的数据库中的原始值。例如,对于英语用户,通过t-field展示datetime字段的值时,将展示12/15/2018 17:12:13格式。而使用t-esc的时候,将展示2018-12-15 17:12:13(若当地时区与UTC时区不同,则时间也会不同哦)

Conditionals 条件

注意,显示出版日期的部分由t元素包装,t-if属性设置。此属性计算规则符合python的代码逻辑,元素只有在判断条件为true的时候才进行渲染。如下的例子,只有设置了出版日期的时候显示div。然而,在复杂的逻辑下,还需要用到t-elif和t-else,如下:

<div t-if="state == 'new'">
Text will be added of state is new.
</div>
<div t-elif="state == 'progress'">
Text will be added of state is progress.
</div>
<div t-else="">
Text will be added for all other stages.
</div>

设置变量

QWeb模板还能够在模板本身中定义变量。定义模板之后,可以在后续模板中使用该变量。你可以这样设置变量:

<t t-set="my_var" t-value="5 + 1"/>
<t t-esc="my_var"/>

子模板

如果您正在开发一个大型应用程序,管理大型模板可能会很困难。QWeb模板支持子模板,因此您可以将大型模板划分为较小的子模板,并且可以在多个模板中重用它们。对于子模板,你可以使用t-call属性,就像下面这个例子:

<template id="first_template">
<div> Test Template </div>
</template>
<template id="second_template">
<t t-call="first_template"/>
</template>

Inline editing 内联编辑

用户可以在编辑模式下直接修改记录内容。通过t-field加载的数据默认是可编辑的。

如果你想配置可编辑、可拖拽的元素,那么可将元素的class配置为oe_structure。在我们的例子中,我们在顶层模板添加了该类。

如果你想禁用网站某个区域的编辑功能,可设置contenteditable=False属性。步骤5中,我们在

设置了该属性。

小贴士

To make the page multi-website-compatible, when you edit a page/view through the website editor, Odoo will create a separate copy of the page for that website. This means that subsequent code updates will never make it to the edited website page. In order to also get the ease of use of inline editing and the possibility of updating your HTML code in subsequent releases, create one view that contains the semantic HTML elements and a second one that injects editable elements. Then, only the latter view will be copied, and you can still have updates for the parent view.

对于这里使用的其他CSS类,请参考Bootstrap的文档。

在步骤1中,我们已经声明了渲染模板的路由。如果您注意到,我们在route()中使用了website=True参数,它将在模板中传递一些额外的上下文,如菜单、用户语言、公司等等。这将在网站上使用。布局,以呈现菜单和页脚。参数website=True还允许在网站中支持多语言。它还以更好的方式显示异常。

在函数末尾,我们返回了渲染的模板。

更多

我们可以通过inherit_id继承已有的模板,并通过xpath定位修改的位置实现对现有模板的调整。例如,我们想在Authors标签旁展示作者的数量,可以通过如下方式实现:

<template id="books_ids_inh" inherit_id="my_library.books">
<xpath expr="//div[@id='card_body']/b" position="replace">
<b class="mt8"> Authors (<t t-esc="len(book.author_ ids)"/>) </b>
</xpath>
</template>

QWeb模板其实是qweb类型的普通视图。template标签是带有特定属性record元素的缩写。后台其实创建了一个ir.ui.view模型qweb类型的新纪录。通过tempalte标签的name及inherit_id属性,可以设置记录的inherit_id字段。

在下一节中,我们将学习如何管理动态路由。

参考

关于QWeb模板的参考如下:

  • 总的来说,Odoo广泛使用Bootstrap,您应该使用它来轻松地获得自适应设计。
  • 有关视图继承的详细信息,请参阅第9章后端视图。
  • 更深入理解控制器,可参考第十三章的"配置url及添加访问控制"章节。
  • 关于更新已有路由的内容,可参考第十三章"调整已有路由"章节。

配置动态路由

在网站开发项目中,我们经常需要创建动态的路由。比如,在电商中,每一个商品都有详细的页面且URL不同。在本节中,我们将展示每本书的详细内容。

准备

我们会使用之前的my_library模块。为了使每本书页面更吸引人,我们将添加一些字段。如下:

class LibraryBook(models.Model):
_name = 'library.book'
name = fields.Char('Title', required=True)
date_release = fields.Date('Release Date')
author_ids = fields.Many2many('res.partner', string='Authors')
image = fields.Binary(attachment=True)
html_description = fields.Html()

步骤

  1. 在main.py添加新路由
@http.route('/books/<model("library.book"):book>', type='http', auth="user", website=True)
def library_book_detail(self, book):
return request.render( 'my_library.book_detail', {'book': book, })
  1. 添加模板:
<template id="book_detail" name="Books Details">
<t t-call="website.layout">
<div class="container">
<div class="row mt16">
<div class="col-5">
<span t-field="book.image" t-options="'widget': 'image','class':'mx-auto d-block img-thumbnail'">
</div>
<div class="offset-1 col-6">
<h1 t-field="book.name" />
<t t-if="book.date_release">
<div t-field="book.date_release" class="text-muted" />
</t>
<b class="mt8">Authors</b>
<ul>
<li t-foreach="book.author_ids" t-as="author">
<span t-esc="author.name">
</li>
</ul>
</div>
</div>
</div>
<div t-field="book.html_description"/>
</t>
</template>
  1. 添加按钮,导航到图书的详细页面:
<div t-attf-class="card mt24 #{'bg-light' if book_odd else ''}">
<div class="card-body">
<h3 t-field="book.name" />
<t t-if="book.date_release">
<div t-field="book.date_release" class="text-muted">
</t>
<b class="mt8">Authors</b>
<ul>
<li t-foreach="book.author_ids" t-as="author">
<span t-esc="author.name"/>
</li>
</ul>
<a t-attf-href="/books/#{book.id}" class="btn btn-primary btn-sm">
<i class="fa fa-book"/>Book Detail
</a>
</div>
</div>

原理

步骤1,我们创建了动态路由。其中<model("library.book"):book>,如/books/1。odoo将自动将ID为1的library.book赋值给book。

步骤2,我们新建了一个展示图书详细页面的QWeb模板。其中html_description字段是html类型的值。odoo将自动添加可拖拽的代码到html类型的值。

步骤3,添加了到每本书的链接。

小贴士

模型路由还支持域过滤。例如,如果要基于某个条件限制某些书籍,可以按如下方式将域传递到路由:

/books/<model("library.book", "[(name','!=', 'Book 1')]"):team>/submit

这将限制名为"Book 1"的图书。

更多

Odoo使用werkzeug来处理HTTP请求。Odoo在werkzeug周围添加了一个薄薄的包装,以方便处理路由。上面的例子中<model("library.book"):book>。这是Odoo自己的实现,但是它也支持werkzeug路由的所有特性。因此,您可以这样使用路由:

  • /page/int:page 接受整数值。
  • /page/<any(about, help):page_name>:接受选择值
  • /pages/ 接受字符串。
  • /pages/<category>/<int:page>: 接受多个参数

    更多详细内容可参考 http://

    werkzeug.pocoo.org/docs/0.14/routing/.

为用户提供静态代码片段

odoo网站编辑器提供了几种编辑功能区的方式,可拖拽可编辑。本节将介绍如何构建自己的功能区。这些功能区称为代码段。有几种类型的代码段,通常可分为静态和动态。静态代码段是固定的,除非用户主动修改。动态区域是依赖于数据库数据变化的。本节我们将介绍如何创建静态代码段。

准备

步骤

代码段其实是将被注入到添加模块区域的QWeb视图。我们将创建一个展示图书的image和图书的title。你可以在页面上拖放功能块,可以编辑图片及标题。

  1. 添加新文件views/snippets.xml
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!-- Step 2 and 3 comes here -->
</odoo>
  1. 添加QWeb视图如下:
<template id="snippet_book_cover" name="Book Cover">
<section class="pt-3 pb-3">
<div class="container">
<div class="row align-items-center">
<div class="col-lg-6 pt16 pb16">
<h1>Odoo 12 Development Cookbook</h1>
<p>
Learn with Odoo development
quicky with examples
</p>
<a class="btn btn-primary" href="#"> Book Details
</a>
</div>
<div class="col-lg-6 pt16 pb16">
<img src="/my_library/static/src/img/cover.jpeg" class="mx-auto img-thumbnail w-50 img img-fluid shadow"/>
</div>
</div>
</div>
</section>
</template>
  1. 将代码段添加到website.snippets
<template id="book_snippets_options" inherit_id="website. snippets">
<xpath expr="//div[@id='snippet_structure']/ div[hasclass('o_panel_body')]" position="inside">
<t t-snippet="my_library.snippet_book_cover" t-thumbnail="/my_library/static/src/img/s_book_thumb.png"/>
</xpath>
</template>
  1. 添加封面及缩略图/my_library/ static/src/img。

原理

静态代码段其实就是HTML代码的区块。步骤1,我们创建了QWeb的模板。在HTML中,我们使用了Bootstrap的架构。静态代码段可通过拖拽的形式加载到页面上。一般而言,在代码段中使用section元素及Bootstrap类将会非常方便,因为odoo为我们提供了开箱即用的页面、背景、尺寸的编辑功能。

步骤2,我们在代码列表中注册我们的代码段。可通过继承website.snippets实现。在网站的编辑器中,将被分为不同的区域。在我们的例子中,我们可通过xpath注册代码段。为了展示我们的代码段,可通过标签加t-snippet属性实现。t-snippet属性值是XML ID值。我们还可以使用t-thumbnail用于展示代码区的缩略图。

小贴士

website.snippets模板包含了所有的默认代码段,你可以在/addons/website/views/snippets/snippets.xml详细了解。

当你使用合适的Bootstrap架构时,odoo将自动添加一些默认的选项。比如,在我们的例子中,你可以设置背景色,背景图,宽度,高度等。在/addons/website/views/snippets/snippets.xml中可以看到全部的选项。下一节,我们将了解如何添加我们自己的可选项。

步骤3,我们已经在结构块下面列出了我们的代码片段。更新模块后,就可以拖放代码段了。在步骤4中,我们刚刚为代码段缩略图添加了一个图像。

更多

在这种情况下,不需要额外的JavaScript。Odoo的编辑器提供了很多开箱即用的选项和控件,它们对于静态代码段来说已经足够了。您将在 website/views/snippets.xml中找到所有现有的代码段和选项。

Snippet选项还支持data exclude、data drop near和data-drop-in属性,这些属性决定了将代码段从代码段栏中拖出时可以将其放置在何处。这些也是jQuery选择器,在这个方法的第3步中,我们没有使用它们,因为我们允许将代码片段放在内容可以到达的任何地方。

为用户提供动态代码片段

本节,我们将学习如何创建动态代码片段。

准备

步骤

  1. 在views/snippets.xml添加QWeb模板
<template id="snippet_book_dynamic" name="Latest Books">
<section class="book_list">
<div class="container">
<h2>Latest books</h2>
<table class="table book_snippet table-striped" data-number-of-books="5">
<tr>
<th>Name</th>
<th>Release date</th>
</tr>
</table>
</div>
</section>
</template>
  1. 注册代码片段并添加选项改变代码行为:
<template id="book_snippets_options" inherit_id="website.snippets">
<!-- register snippet -->
<xpath expr="//div[@id='snippet_structure']/div[hasclass('o_panel_body')]" position="inside">
<t t-snippet="my_library.snippet_book_dynamic" t-thumbnail="/my_library/static/src/img/s_ list.png"/>
</xpath>
<xpath expr="//div[@id='snippet_options']" position="inside">
<!—Add step 3 here -->
</xpath>
</template>
  1. 在图书片添加选项:
<div data-selector=".book_snippet">
<we-select string="Table Style">
<we-button data-select-class="table-striped">
Striped
</we-button>
<we-button data-select-class="table-dark">
Dark
</we-button>
<we-button data-select-class="table-bordered">
Bordered
</we-button>
</we-select>
<we-button-group string="No of Books" data-attribute-name="numberOfBooks">
<we-button data-select-data-attribute="5">
5
</we-button>
<we-button data-select-data-attribute="10">
10
</we-button>
<we-button data-select-data-attribute="15">
15
</we-button>
</we-button-group>
</div>
  1. 添加/static/src/snippets.js文件
odoo.define('book.dynamic.snippet', function (require) {
'use strict';
var publicWidget = require('web.public.widget');
// Add step 5 here
});
  1. 添加public小部件渲染book代码片段:
publicWidget.registry.books = publicWidget.Widget.extend({
selector: '.book_snippet',
disabledInEditableMode: false,
start: function () {
var self = this;
var rows = this.$el[0].dataset.numberOfBooks || '5';
this.$el.find('td').parents('tr').remove();
this._rpc({
model: 'library.book',
method: 'search_read',
domain: [],
fields: ['name', 'date_release'],
orderBy: [{
name: 'date_release',
asc: false
}],
limit: parseInt(rows)
}).then(function (data) {
_.each(data, function (book) {
self.$el.append(
$('<tr />').append(
$('<td />').text(book.name),
$('<td />').text(book.date_release)
));
});
});
},
});
  1. 添加js文件
<template id="assets_frontend" inherit_id="website.assets_frontend">
<xpath expr="." position="inside">
<script src="/my_library/static/src/js/ snippets.js" type="text/javascript" />
</xpath>
</template>

更新模块,我们新增了名为Latest books的代码段,提供了一个可选择展示最新添加几本书的选项。

原理

步骤1,我们添加了QWeb模板,包含了table的基础架构,并动态生成图书的行。

步骤2,我们注册了动态代码段,我们添加了改变代码行为的自定义的选项。我们添加的第一个选项是选择Table样式。第二个选项是图书的数量。我们使用<we-select>和<we-button-group>标签。这些标签提供了不同的GUI展示。<we-select>标签将展示一个下拉选项,<we-button-group>将作为按钮组供用户选择。还有几个其他的GUI选项,<we-checkbox>和<we-colorpicker>。你可以在 /addons/website/views/snippets/snippets.xml 查看更多GUI选项。

如果仔细观察这些选项,就会发现选项按钮有data-select-class和data-select-data-attribute属性。这将让Odoo知道当用户选择一个选项时要更改哪个属性。data-select- class将在用户选择该选项时设置元素的class属性,而data-select-data-attribute将设置元素的自定义属性和值。注意,它将使用data-attribute-name的值来设置属性。

现在,我们已经添加了代码片段和代码片段选项。如果此时拖放代码片段,则只会看到表头和代码片段选项。更改snippet选项将更改表样式,但还没有图书数据。为此,我们需要编写一些JavaScript代码来获取数据并将其显示在表中。在步骤3中,我们已经添加了JavaScript代码,用于在表中呈现图书数据。要将JavaScript对象映射到HTML元素,Odoo使用PublicWidget。现在,可以通过require('web.public.widget')模块获得PublicWidget。使用PublicWidget的关键属性是选择器属性。在selector属性中,您需要使用元素的CSS选择器,Odoo将自动将元素与PublicWidget绑定。您可以访问$el属性中的相关元素。除了_rpc之外,其余的代码都是基本的JavaScript和jQuery。_rpc方法用于发出网络请求并获取图书数据。我们将在第15章“Web客户端开发”的服务器配方的RPC调用中学习更多关于_rpc方法的知识。

更多

如果您想创建自己的代码片段选项,可以在代码片段选项上使用t-js选项。之后,您需要在JavaScript代码中定义自己的选项。详细内容可参见 addons/website/static/src/js/editor/snippets.options.js

获取网站用户输入的数据

在网站开发模式下,我们经常需要获取用户输入。本节,我们将为用户创建一个针对图书反馈问题的html 表格。

准备

本节,我们使用my_library模块,我们需要一个新的模型存储问题信息。

1. 在library.book模型中添加字段及book.issues模型,如下:

class LibraryBook(models.Model):
_name = 'library.book'
name = fields.Char('Title', required=True)
date_release = fields.Date('Release Date')
author_ids = fields.Many2many('res.partner', string='Authors')
image = fields.Binary(attachment=True)
html_description = fields.Html()
book_issue_id = fields.One2many('book.issue', 'book_id')
class LibraryBookIssues(models.Model):
_name = 'book.issue'
book_id = fields.Many2one('library.book', required=True)
submitted_by = fields.Many2one('res.users')
isuue_description = fields.Text()

2. 在图书form视图中添加book_issues_id字段:

<group string="Book Issues">
<field name="book_issue_id" nolabel="1">
<tree>
<field name="create_date"/> <field name="submitted_by"/>
<field name="isuue_description"/>
</tree>
</field>
</group>

3. 添加book.issue的访问记录

acl_book_issues,library.book_issue,model_book_issue,group_librarian,1,1,1,1

步骤

1. 在main.py添加路由

@http.route("/books/submit_issues", type="http", auth="user", website=True)
def books_issues(self, **post):
if post.get("book_id"):
book_id = int(post.get("book_id"))
issue_description = post.get("issue_description")
request.env["book.issue"].sudo().create(
{
"book_id": book_id,
"issue_description": issue_description,
"submitted_by": request.env.user.id,
}
)
return request.redirect("/books/submit_ issues?submitted=1")
return request.render(
"my_library.books_issue_form",
{
"books": request.env["library.book"].search([]),
"submitted": post.get("submitted", False),
},
)

2. 添HTML form:

<template id="books_issue_form" name="Book Issues Form">
<t t-call="website.layout">
<div class="container mt32">
<!-- add the page elements here(step 3 and 4)-->
</div>
</t>
</template>

3. 为页面添加条件头,如下所示:

<t t-if="submitted">
<h3 class="alert alert-success mt16 mb16">
<i class="fa fa-thumbs-up"/>
Book submitted successfully
</h3>
<h1> Report the another book issue </h1>
</t>
<t t-else="">
<h1> Report the book issue </h1>
</t>

4. 添加<form>

<div class="row mt16">
<div class="col-6">
<form method="post">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="form-group">
<label>Select Book</label>
<select class="form-control" name="book_id">
<t t-foreach="books" t-as="book">
<option t-att-value="book.id">
<t t-esc="book.name"/>
</option>
</t>
</select>
</div>
<div class="form-group">
<label>Issue Description</label>
<textarea name="issue_description" class="form-control" placeholder="e.g. pages are missing"/>
</div>
<button type="submit" class="btn btn-primary">
Submit
</button>
</form>
</div>
</div>

原理

步骤1,我们创建了一个提交图书问题的路径。函数中的post参数将接受URL中的所有查询参数。您还将在post参数中获得提交的表单数据。在我们的示例中,我们使用了相同的控制器来显示页面并提交问题。如果我们在post中找到数据,我们将创建一个问题记录,然后用提交的查询参数将用户重定向到问题页面,这样用户就可以看到确认问题已经提交,因此如果他/她想提交另一个问题,就可以提交另一个问题。

小贴士

我们使用sudo()创建图书发行记录,因为普通用户(访问者)没有创建新的图书发行记录的访问权限。尽管如果用户从web页面提交了一个问题,则有必要创建图书问题记录。这是sudo()用法的一个实际示例。

步骤2,我们已经为issue页面创建了模板。在步骤3中,我们已经添加了条件头文件。提交问题后,将显示success头。

步骤4,我们添加了<form>,其中包含三个字段:csrf_token、图书选择和问题描述。最后两个字段用于从网站用户获取输入。然而,csrf_token被用来避免跨站请求伪造(CSRF)攻击。如果你不在表单中使用它,用户就不能提交表单。当您提交表单时,您将在步骤1的books_issues()方法中获得提交的数据作为**post参数。

小贴士

禁用csrf,可设置csrf=False

更多

我们可以为form单独指定post地址

<form action="/my_url" method="post">

并新增路由

@http.route('/my_url', type='http', method='POST', auth='user', website=True)

管理SEO配置项

管理站点地图

获取访客的国家信息

跟踪营销活动

管理多网站

odoo支持同一个odoo实例运行多个网站并展示不同的内容。

准备

步骤

  1. 在library.book模型中添加继承website.multi.mixin
class LibraryBook(models.Model):
_name = 'library.book'
_inherit = ['website.seo.metadata', 'website.multi.mixin']
  1. 在图书的form视图下新增website_id
<group>
<field name="author_ids" widget="many2many_tags"/>
<field name="website_id"/>
</group>
  1. 管理/books路由
@http.route('/books', type='http', auth="user", website=True)
def library_books(self, **post):
domain = ['|', ('restrict_country_ids', '=', False), ('restrict_country_ids', 'not in', [country_id])]
domain += request.website.website_domain()
return request.render( 'my_library.books', {
'books': request.env['library.book']. search(domain),
})
  1. 导入werkzeug并调整图书的细节,并限制另一个网站的访问
import werkzeug
...
@http.route('/books/<model("library.book"):book>', type='http', auth='user', website=True, sitemap=sitemap_books)
def library_book_detail(self, book, **post):
if not book.can_access_from_current_website():
raise werkzeug.exceptions.NotFound()
return request.render('my_library.book_detail',{'book':book, 'main_object': book})
···

更新模块。为不同的图书设置不同的网站。现在,打开/books,可以看到图书的列表。然后修改网站,再次检查图书列表。如下:

原理

步骤1,我们引入了website.multi.mixin类,可用于管理网站。mixin类将添加website_id字段,可用于当前记录用于哪个网站。

步骤2,添加视图。

步骤3,我们修改了用于查找书籍列表的域。request.website.website_domain()将返回筛选出非网站书籍的域。

小贴士

请注意,有些记录没有设置任何网站id。这些记录将在所有网站上显示。这意味着,如果某本书上没有“网站id”字段,则该书将显示在所有网站上。

然后,我们在web搜索中添加了域,如下所示:

  • 步骤4,我们限制了图书访问。如果这本书不适合当前的网站,那么我们将提出一个找不到的错误。can_access_from_current_website()方法将返回值True(如果书籍记录用于当前活动的网站),如果书籍记录用于其他网站,则返回值False。
  • 我们在路由控制器中添加了**post。这是因为如果没有配置**post,/books和/books/model:library.book:book将无法接受参数。他们也会产生一个错误,而切换网站从网站切换器,所以我们添加了它。通常,在每个控制器中添加**post是一种好的做法,这样它们就可以处理查询参数。

重定向老的URL

当我们迁移网站的时候,需要将老的URL重定向到新的URL。好的重定向,可以让SEO依旧指向新的URL。本节,我们将介绍重定向相关知识。

准备

步骤

在我们老的网站,/library将展示图书列表。而my_library模块的/books也是展示图书列表。因此我们可以将/library指向/books。

  1. 激活开发者模式
  2. 打开 Website|Configuration|Redirects。
  3. 点击 新建 。
  4. 输入新旧URL。
  5. 选择Action的值301 Moved permanently。
  6. 保存记录。

原理

页面重定向很简单;它只是HTTP协议的一部分。在我们的示例中,我们将/库移到了/图书。我们使用了301移动永久重定向进行重定向。以下是Odoo中提供的所有重定向选项:

  • 404NotFound: 如果要为页提供404notfound响应,则使用此选项。注意,对于这样的请求,Odoo将显示默认的404页面。
  • 301 Moved temporarily: 此选项将旧URL永久重定向到新URL。这种类型的重定向将把搜索引擎优化排名移动到一个新的页面。
  • 302 Moved temporarily: 此选项将旧URL临时重定向到新URL。当您需要在有限的时间内重定向URL时,请使用此选项。这种类型的重定向不会将SEO排名移动到新页面。
  • 308 Redirect/Rewrite: 一个有趣的选择-有了这个,你将能够改变/重写现有的Odoo网址到新的。在这个方法中,这将允许我们将旧的/库URL重写为新的/图书URL。因此,我们不需要使用/library的301永久移动规则重定向旧的URL。

重定向规则窗体上还有几个字段。其中之一是Active字段,如果您想不时启用/禁用规则,可以使用该字段。第二个重要领域是网站。当您使用多网站功能并且希望将重定向规则仅限于一个网站时,将使用“网站”字段。但是,默认情况下,该规则将应用于所有网站。

发布网站

在业务流中,有时需要允许或撤消对公共用户的页面访问。其中一个例子是电子商务产品,您需要根据可用性发布或取消发布产品。在本节中,我们将看到如何为公共用户发布和取消发布图书记录。

准备

提醒

请将路由中的auth='user'调整为auth='public'

步骤

  1. 在Library.book模型中添加引用website.published.mixin
class LibraryBook(models.Model):
_name = 'library.book'
_description = 'Library Book'
_inherit = ['website.seo.metadata','website. published.mixin']
  1. 添加新文件my_library/security/rules.xml,并添加新纪录如下:
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="books_rule_portal_public" model="ir. rule">
<field name="name">
Portal/Public user: read published books
</field>
<field name="model_id" ref="my_library.model_library_book"/>
<field name="groups" eval="[(4, ref('base.group_portal')),(4, ref('base.group_public'))]"/>
<field name="domain_force"> [('website_published','=', True)]
</field>
<field name="perm_read" eval="True"/>
</record>
</odoo>
  1. 更新模块

要publish/unpublish 图书,可以使用图书详细信息页面的前一屏幕截图中显示的切换。

原理

Odoo提供了一个现成的mixin来处理记录的发布管理。它为你做了大部分工作。你只需要添加website.published.mixin你的模特。在步骤1中,我们添加了网站.published.mixin我们的图书模型。这将添加发布和取消发布图书所需的所有字段和方法。一旦您将这个mixin添加到books模型中,您将能够看到在book detail页面上切换状态的按钮,如上图所示。

小贴士

我们正在从book details路由发送一个book record作为主对象。否则,您将无法在“书本详细信息”页上看到“发布/取消发布”按钮。

添加mixin将在图书的详细信息页面上显示publish/unpublish按钮,但不会限制公共用户访问它。为此,我们需要添加一个记录规则。在步骤2中,我们添加了一个记录规则来限制对未出版书籍的访问。如果您想了解有关记录规则的更多信息,请参阅第10章“安全访问”。

更多

publish mixin将启用网站上的“发布/取消发布”按钮。但是如果您想在后端表单视图上显示重定向按钮,publishmixin也可以提供一种方法。以下步骤显示如何将重定向按钮添加到书本的窗体视图:

  1. 在library.book计算书籍URL的模型:
@api.depends('name')
def _compute_website_url(self):
for book in self:
book.website_url = '/books/%s' % (slug(book))
  1. 添加重定向按钮
<sheet>
<div class="oe_button_box" name="button_box">
<field name="is_published" widget="website_redirect_button"/>
</div>

添加按钮后,您将能够在书本的窗体视图中看到该按钮,单击它,您将被重定向到书本的详细信息页面。

【odoo14】第十四章、CMS网站开发的更多相关文章

  1. 【odoo14】第十三章、网站开发(对外服务)

    本章我们将介绍一些关于odoo web服务方面的基础知识.进阶的内容,将在第十四章介绍. odoo中的web请求是由python的werkzeug库驱动的.odoo为了操作方便,对werkzeug进行 ...

  2. 第十四章 web前端开发小白学爬虫

    老猿从事IT开发快三十年了,接触互联网也很久了,但自己没有做过web前端开发,只知道与前端开发相关的一些基本概念,如B/S架构.html标签.js脚本.css样式.xml解析.cookies.http ...

  3. 第十四章 Odoo 12开发之部署和维护生产实例

    本文中将学习将 Odoo 服务器作为生产环境的基本准备.安装和维护服务器是一个复杂的话题,应该由专业人员完成.本文中所学习的不足以保证普通用户创建应对包含敏感数据和服务的健壮.安全环境. 本文旨在介绍 ...

  4. 【odoo14】第十五章、网站客户端开发

    odoo的web客户端.后台是员工经常使用的地方.在第九章中,我们了解了如何使用后台提供的各种可能性.本章,我们将了解如何扩展这种可能性.其中web模块包含了我们在使用odoo中的各种交互行为. 本章 ...

  5. HTML第十四章总结 HTML forms

    第十四章主要讲了 html forms,通过 forms,我们可以得到 customers' feedback,使得网页能够 interactive,本章的内容分为三个部分: forms 的 elem ...

  6. “全栈2019”Java多线程第三十四章:超时自动唤醒被等待的线程

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

  7. “全栈2019”Java多线程第二十四章:等待唤醒机制详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

  8. “全栈2019”Java多线程第十四章:线程与堆栈详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

  9. “全栈2019”Java异常第十四章:将异常输出到文本文件中

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java异 ...

随机推荐

  1. 僵尸进程 & 孤儿进程

    参考博文 基本概念 僵尸进程:是所有进程都会进入的一种进程状态,子进程退出,而父进程并没有调用 wait() 或 waitpid() 获取子进程的状态信息,那么子进程的 PID 和 进程描述符 等资源 ...

  2. Redis性能指标监控

    监控指标 •性能指标:Performance•内存指标: Memory•基本活动指标:Basic activity•持久性指标: Persistence•错误指标:Error 性能指标:Perform ...

  3. IOS键盘收起后,页面底部留白处理

    环境:vue+vant 的H5页面 场景:输入框输入信息时,如登录.注册等表单信息 问题:键盘收回后页面底部留白,导致dialog组件按钮位移,视觉上,其中的按钮无法正常工作 解决方案:监听失去焦点时 ...

  4. Bootstrap页头

    页头组件能够为 h1 标签增加适当的空间,并且与页面的其他部分形成一定的分隔.它支持 h1 标签内内嵌 small 元素的默认效果,还支持大部分其他组件(需要增加一些额外的样式). <div c ...

  5. github & markdown & image layout

    github & markdown & image layout css & right https://github.com/sindresorhus/log-symbols ...

  6. css text gradient color, css fonts gradient color

    css text gradient color, css fonts gradient color css 字体渐变色 demo https://codepen.io/xgqfrms/pen/OJya ...

  7. infinite auto load more & infinite scroll & load more

    infinite auto load more & infinite scroll & load more https://codepen.io/xgqfrms/pen/NZVvGM ...

  8. Baccarat如何点燃DEFI市场?

    目前DeFi是成为了各大生态的"兵家必争之地",与此同时DeFi的高收益也成为吸引散户入局的一个利器.而虽然流动性挖矿板块近期的温度有所下降,但是这其中不乏还是有很多收益颇丰的De ...

  9. CSS布局,div居中,文字居中

    .main { width: 100%; margin: 0 auto; .banner { img { width: 100%; } } .article { margin-bottom: 100p ...

  10. c#(winform)获取本地打印机

    引用 using System.Drawing.Printing; //代码 PrintDocument prtdoc = new PrintDocument(); string strDefault ...