odoo官方文档第十二章 Javascript Reference

Javascript Reference

本文档介绍了Odoo Javascript框架。 这个框架在代码行方面不是一个大型应用程序,但它非常通用,因为它基本上是一个将声明性接口描述转换为实时应用程序的机器,能够与数据库中的每个模型和记录进行交互。 甚至可以使用Web客户端来修改Web客户端的界面。

Odoo中所有文档字符串的html版本可在以下位置获得:
Javascript API

Overview(概述)

Javascript框架旨在处理三个主要用例:

主要是有账户的用户登录 ,无账户数据浏览 , 销售的接口

  • Web客户端(the web client):这是私有Web应用程序,可以在其中查看和编辑业务数据。 这是一个单页面应用程序(页面永远不会重新加载,只有在需要时才从服务器获取新数据)
  • 网站(the website):这是Odoo的公共部分。 它允许身份不明的用户作为客户端浏览某些内容,购物或执行许多操作。 这是一个经典的网站:各种路由控制器和一些javascript使其工作。
  • 销售点(the point of sale):这是销售点的界面。 它是一个专门的单页面应用程序。
    一些javascript代码对于这三个用例是通用的,并且捆绑在一起(参见下面的资产部分)。 本文档主要关注Web客户端设计。

Web client

Single Page Application(单独页面应用)

简而言之,WebClient的WebClient实例是整个用户界面的根组件。 它的责任是协调所有各种子组件,并提供服务,如rpcs,本地存储等。

在运行时,Web客户端是单页面应用程序。 每次用户执行操作时,都不需要从服务器请求完整页面。 相反,它只会请求它所需的内容,然后替换/更新视图。 此外,它管理URL:它与Web客户端状态保持同步。

这意味着当用户正在使用Odoo时,Web客户端类(和操作管理器)实际上会创建并销毁许多子组件。 状态是高度动态的,每个小部件都可以随时销毁。

Overview of web client JS code(Web客户端JS代码概述)

在这里,我们在web / static / src / js插件中快速概述了Web客户端代码。 请注意,故意不是详尽无遗的。 我们只覆盖最重要的文件/文件夹。

  • boot.js:这是定义模块系统的文件。 它需要先加载。
  • core /:这是一个较低级别构建块的集合。 值得注意的是,它包含类系统,小部件系统,并发实用程序以及许多其他类/函数。
  • chrome /:在这个文件夹中,我们有大多数大小部件组成了大部分用户界面。
  • chrome / abstract_web_client.js和chrome / web_client.js:这些文件一起定义了WebClient小部件,它是Web客户端的根小部件。
  • chrome / action_manager.js:这是将动作转换为小部件的代码(例如看板或表单视图)
  • chrome / search_X.js所有这些文件定义了搜索视图(它不是Web客户端视图中的视图,仅从服务器的角度来看)
  • fields:所有主视图字段小部件都在此处定义
  • views:这是视图所在的位置

Assets Management

在Odoo中管理资产并不像在其他一些应用程序中那样简单。 其中一个原因是我们有各种情况需要一些但不是所有资产。 例如,Web客户端,销售点,网站甚至移动应用程序的需求是不同的。 此外,一些资产可能很大,但很少需要。 在这种情况下,我们有时希望它们被懒惰地加载。

主要思想是我们在xml中定义一组bundle。 捆绑包在此定义为文件集合(javascript,css,less)。 在Odoo中,最重要的包在addons / web / views / webclient_templates.xml文件中定义。 它看起来像这样:

<template id="web.assets_common" name="Common Assets (used in backend interface and website)">
    <link rel="stylesheet" type="text/css" href="/web/static/lib/jquery.ui/jquery-ui.css"/>
    ...
    <script type="text/javascript" src="/web/static/src/js/boot.js"></script>
    ...
</template>

然后可以使用t-call-assets指令将包中的文件插入到模板中:

<t t-call-assets="web.assets_common" t-js="false"/>
<t t-call-assets="web.assets_common" t-css="false"/>

以下是服务器使用这些指令呈现模板时发生的情况:

  • 捆绑包中描述的所有较少的文件都被编译成css文件。 名为file.less的文件将在名为file.less.css的文件中编译。

  • if we are in debug=assets mode,
    将t-js属性设置为false的t-call-assets指令将替换为指向css文件的样式表标记列表
    将t-css属性设置为false的t-call-assets指令将替换为指向js文件的脚本标记列表

  • if we are not in debug=assets mode,
    css文件将被连接和缩小,然后分成不超过4096条规则的文件(以克服IE9的旧限制)。 然后,我们根据需要生成尽可能多的样式表标签
    将js文件连接并缩小,然后生成脚本标记
    请注意,资产文件是缓存的,因此理论上,浏览器只应加载一次。

Main bundles

启动Odoo服务器时,它会检查捆绑包中每个文件的时间戳,如有必要,将创建/重新创建相应的捆绑包。

以下是大多数开发人员需要了解的一些重要捆绑包:

web.assets_common:此捆绑包包含Web客户端,网站以及销售点通用的大多数资产。 这应该包含odoo框架的低级构建块。 请注意,它包含boot.js文件,该文件定义了odoo模块系统。
web.assets_backend:此捆绑包含特定于Web客户端的代码(特别是Web客户端/操作管理器/视图)
web.assets_frontend:这个包是所有特定于公共网站的:电子商务,论坛,博客,活动管理,......

Adding files in an asset bundle(添加资产包中的文件)

将位于addons / web中的文件添加到包中的正确方法很简单:只需将文件webclient_templates.xml中的脚本或样式表标记添加到包中即可。 但是当我们在不同的插件中工作时,我们需要从该插件添加一个文件。 在这种情况下,应该分三步完成:

  1. 在views /文件夹中添加assets.xml文件
  2. 在清单文件的'data'键中添加字符串'views / assets.xml'
  3. 创建所需包的继承视图,并使用xpath表达式添加文件。 例如,
<template id="assets_backend" name="helpdesk assets" inherit_id="web.assets_backend">
    <xpath expr="//script[last()]" position="after">
        <link rel="stylesheet" href="/helpdesk/static/src/less/helpdesk.less"/>
        <script type="text/javascript" src="/helpdesk/static/src/js/helpdesk_dashboard.js"></script>
    </xpath>
</template>

请注意,当用户加载odoo Web客户端时,捆绑包中的文件都会立即加载。 这意味着每次都通过网络传输文件(浏览器缓存处于活动状态时除外)。 在某些情况下,延迟加载某些资产可能更好。 例如,如果窗口小部件需要大型库,并且该窗口小部件不是体验的核心部分,那么在实际创建窗口小部件时仅加载库可能是个好主意。 widget类实际上内置了对此用例的支持。 (参见QWeb模板引擎部分

What to do if a file is not loaded/updated(如果未加载/更新文件该怎么办)

文件可能无法正确加载的原因有很多。 以下是您可以尝试解决此问题的一些事项:

  • 服务器启动后,它不知道资产文件是否已被修改。因此,您只需重新启动服务器即可重新生成资产。
  • 检查控制台(在开发工具中,通常用F12打开)以确保没有明显的错误
    尝试在文件开头添加一个console.log(在任何模块定义之前),这样你就可以看到文件是否已经加载
  • 在用户界面中,在调试模式下(INSERT LINK HERE TO DEBUG MODE),有一个选项可强制服务器更新其资产文件。
    使用debug = assets模式。这实际上会绕过资产包(注意它实际上并没有解决问题。服务器仍然使用过时的捆绑包)
  • 最后,对于开发人员来说,最方便的方法是使用--dev = all选项启动服务器。这将激活文件观察程序选项,这将在必要时自动使资产无效。请注意,如果操作系统是Windows,它不能很好地工作。
  • 记得刷新你的页面!
  • 或者可能要保存您的代码文件......

重新创建资产文件后,您需要刷新页面,重新加载正确的文件(如果不起作用,可以缓存文件)。

Javascript Module System

一旦我们能够将javascript文件加载到浏览器中,我们需要确保它们以正确的顺序加载。 为了做到这一点,Odoo定义了一个小模块系统(位于addons / web / static / src / js / boot.js文件中,需要先加载)。

受AMD启发的Odoo模块系统通过在全局odoo对象上定义函数define来工作。 然后我们通过调用该函数来定义每个javascript模块。 在Odoo框架中,模块是一段将尽快执行的代码。 它有一个名称,可能还有一些依赖项。 加载其依赖项后,也会加载一个模块。 然后,模块的值是定义模块的函数的返回值。

例如,它可能如下所示:

// in file a.js
odoo.define('module.A', function (require) {
    "use strict";

    var A = ...;

    return A;
});

// in file b.js
odoo.define('module.B', function (require) {
    "use strict";

    var A = require('module.A');

    var B = ...; // something that involves A

    return B;
});

定义模块的另一种方法是在第二个参数中明确给出依赖项列表。

odoo.define('module.Something', ['module.A', 'module.B'], function (require) {
    "use strict";

    var A = require('module.A');
    var B = require('module.B');

    // some code
});

如果某些依赖项缺失/未就绪,则不会加载该模块。 几秒钟后控制台会出现警告。

请注意,不支持循环依赖项。 这是有道理的,但这意味着需要小心。

Defining a module

odoo.define方法有三个参数:

  • moduleName:javascript模块的名称。它应该是一个独特的字符串。惯例是使用odoo插件的名称,然后是特定的描述。例如,'web.Widget'描述了在web插件中定义的模块,该模块导出Widget类(因为第一个字母是大写的)
    如果名称不唯一,则会抛出异常并在控制台中显示。

  • 依赖项(dependencies):第二个参数是可选的。如果给定,它应该是一个字符串列表,每个字符串对应一个javascript模块。这描述了在执行模块之前需要加载的依赖项。如果这里没有明确给出依赖关系,那么模块系统将通过调用toString从函数中提取它们,然后使用regexp查找所有require语句。
  • 最后,最后一个参数是一个定义模块的函数。它的返回值是模块的值,可以传递给需要它的其他模块。请注意,异步模块有一个小例外,请参阅下一节。

如果发生错误,将在控制台中记录(在调试模式下):

  • Missing dependencies:这些模块不会出现在页面中。 JavaScript文件可能不在页面中或模块名称错误
  • Failed modules:检测到javascript错误
  • Rejected modules:该模块返回被拒绝的延迟。 它(及其相关模块)未加载。
  • Rejected linked modules:依赖被拒绝模块的模块
  • Non loaded modules:依赖于丢失或失败模块的模块

Asynchronous modules

模块在准备好之前需要执行一些工作。 例如,它可以执行rpc来加载一些数据。 在这种情况下,模块可以简单地返回延迟(promise)。 在这种情况下,模块系统将在注册模块之前等待延迟完成。

odoo.define('module.Something', ['web.ajax'], function (require) {
    "use strict";

    var ajax = require('web.ajax');

    return ajax.rpc(...).then(function (result) {
        // some code here
        return something;
    });
});

Best practices

  • 记住模块名称的约定:addon name后缀为模块名称。
    在模块顶部声明所有依赖项。 此外,它们应按模块名称的字母顺序排序。
  • 这使您更容易理解您的模块。
  • 最后声明所有导出的值
  • 尽量避免从一个模块中导出太多东西。 通常最好只在一个(小/小)模块中导出一个东西。
  • 异步模块可用于简化某些用例。 例如,web.dom_ready模块返回一个deferred,当dom实际就绪时将解析。 因此,需要DOM的另一个模块可能只是在某处有一个require('web.dom_ready')语句,而代码只会在DOM准备就绪时执行。
  • 尽量避免在一个文件中定义多个模块。 它在短期内可能很方便,但实际上这很难维护。

Class Syestem

Odoo是在ECMAScript 6课程开始之前开发的。 在Ecmascript 5中,定义类的标准方法是定义一个函数并在其原型对象上添加方法。 这很好,但是当我们想要使用继承,mixins时,它有点复杂。

出于这些原因,Odoo决定使用自己的类系统,灵感来自John Resig。 基类位于web.Class中,位于文件class.js中。

Creating a subclass

让我们讨论如何创建类。 主要机制是使用extend方法(这或多或少相当于ES6类中的extend)。

var Class = require('web.Class');

var Animal = Class.extend({
    init: function () {
        this.x = 0;
        this.hunger = 0;
    },
    move: function () {
        this.x = this.x + 1;
        this.hunger = this.hunger + 1;
    },
    eat: function () {
        this.hunger = 0;
    },
});

在此示例中,_init_函数是构造函数。 它将在创建实例时调用。 使用new关键字完成实例。

Inheritance

能够继承现有类很方便。 这可以通过在超类上使用extend方法来完成。 调用方法时,框架将秘密重新绑定一个特殊方法:_super到当前调用的方法。 这允许我们在需要调用父方法时使用this._super。

var Animal = require('web.Animal');

var Dog = Animal.extend({
    move: function () {
        this.bark();
        this._super.apply(this, arguments);
    },
    bark: function () {
        console.log('woof');
    },
});

var dog = new Dog();
dog.move()

Mixins

odoo类系统不支持多重继承,但是对于那些我们需要共享某些行为的情况,我们有一个mixin系统:extend方法实际上可以接受任意数量的参数,并将所有这些参数组合在新类中。

var Animal = require('web.Animal');
var DanceMixin = {
    dance: function () {
        console.log('dancing...');
    },
};

var Hamster = Animal.extend(DanceMixin, {
    sleep: function () {
        console.log('sleeping');
    },
});

在这个例子中,Hamster类是Animal的子类,但它也混合了DanceMixin。

Patching an existing class

这并不常见,但我们有时需要修改另一个类。 目标是有一个机制来改变一个类和所有未来/现在的实例。 这是通过使用include方法完成的:

var Hamster = require('web.Hamster');

Hamster.include({
    sleep: function () {
        this._super.apply(this, arguments);
        console.log('zzzz');
    },
});

Widgets

Widget类实际上是用户界面的重要构建块。 几乎用户界面中的所有内容都在窗口小部件的控制之下。 Widget类在widget.js中的模块web.Widget中定义。

简而言之,Widget类提供的功能包括:

  • 小部件之间的父/子关系(PropertiesMixin)
  • 具有安全功能的广泛生命周期管理(例如
    在破坏父母的过程中自动销毁儿童小部件)
  • 使用qweb自动渲染
  • 各种实用功能,以帮助与外部环境进行交互。

以下是基本计数器小部件的示例:

var Widget = require('web.Widget');

var Counter = Widget.extend({
    template: 'some.template',
    events: {
        'click button': '_onClick',
    },
    init: function (parent, value) {
        this._super(parent);
        this.count = value;
    },
    _onClick: function () {
        this.count++;
        this.$('.val').text(this.count);
    },
});

对于此示例,假设模板some.template(并且已正确加载:模板位于文件中,该模板在模块清单中的qweb键中正确定义)由下式给出:

<div t-name="some.template">
    <span class="val"><t t-esc="widget.count"/></span>
    <button>Increment</button>
</div>

此示例窗口小部件可以按以下方式使用:

// Create the instance
var counter = new Counter(this, 4);
// Render and insert into DOM
counter.appendTo(".some-div");

此示例说明了Widget类的一些功能,包括事件系统,模板系统,具有初始父参数的构造函数。

Widget Lifecycle

与许多组件系统一样,widget类具有明确定义的生命周期。 通常的生命周期如下:调用init,然后启动,然后进行渲染,然后启动并最终销毁。

Widget.init(parent)

这是构造函数。 init方法应该初始化小部件的基本状态。 它是同步的,可以被覆盖以从小部件的创建者/父级中获取更多参数

Arguments
parent (Widget())
-- 新窗口小部件的父窗口,用于处理自动销毁和事件传播。 对于没有父项的窗口小部件,可以为null

Widget.willStart()

在创建窗口小部件时以及在附加到DOM的过程中,此方法将由框架调用一次。 willStart方法是一个应该返回延迟的钩子。 在继续渲染步骤之前,JS框架将等待延迟完成。 请注意,此时,窗口小部件没有DOM根元素。 willStart钩子对于执行某些异步工作(例如从服务器获取数据)非常有用

[Rendering]()

此步骤由框架自动完成。 会发生什么是框架检查是否在窗口小部件上定义了模板键。 如果是这种情况,那么它将使用绑定到渲染上下文中的窗口小部件的窗口小部件键来呈现该模板(请参阅上面的示例:我们在QWeb模板中使用widget.count来读取窗口小部件中的值)。 如果没有定义模板,我们读取tagName键并创建相应的DOM元素。 渲染完成后,我们将结果设置为窗口小部件的$ el属性。 在此之后,我们会自动绑定events和custom_events键中的所有事件。

Widget.start()

渲染完成后,框架将自动调用start方法。 这对于执行一些专门的后期渲染工作很有用。 例如,设置库。

必须返回延迟以指示其工作何时完成。

Returns: deferred object

Widget.destroy()

这始终是小部件生命中的最后一步。 当一个小部件被销毁时,我们基本上执行所有必要的清理操作:从组件树中删除小部件,解除所有事件的绑定,......

当窗口小部件的父窗体被销毁时自动调用,如果窗口小部件没有父窗口,或者如果它被删除但其父窗口仍然存在,则必须显式调用。

请注意,不一定要调用willStart和start方法。 可以创建一个小部件(将调用init方法),然后销毁(destroy方法),而不必将其附加到DOM。 如果是这种情况,则甚至不会调用willStart和start。

Widget API

Widget.tagName

如果窗口小部件未定义模板,则使用 默认为div,将用作标记名称以创建DOM元素以设置为窗口小部件的DOM根。 可以使用以下属性进一步自定义此生成的DOM根:

Widget.id

用于在生成的DOM根上生成id属性。 请注意,这很少需要,如果窗口小部件可以多次使用,可能不是一个好主意。

Widget.className

用于在生成的DOM根上生成类属性。 请注意,它实际上可以包含多个css类:'some-class other-class'

Widget.attributes

将属性名称(对象文字)映射到属性值。 这些k:v对中的每一个将被设置为生成的DOM根上的DOM属性。

Widget.el

原始DOM元素设置为窗口小部件的根(仅在启动生命周期方法之后可用)

Widget.template

应设置为QWeb模板的名称。 如果设置,模板将在窗口小部件初始化之后但在启动之前呈现。 模板生成的根元素将设置为窗口小部件的DOM根。

xmlDependencies

在呈现窗口小部件之前需要加载的xml文件的路径列表。 这不会导致加载任何已加载的东西。

events

事件是事件选择器(事件名称和由空格分隔的可选CSS选择器)到回调的映射。 回调可以是窗口小部件方法或函数对象的名称。 在任何一种情况下,this都将设置为小部件:

events: {
    'click p.oe_some_class a': 'some_method',
    'change input': function (e) {
        e.stopPropagation();
    }
},

选择器用于jQuery的事件委托,只有与选择器匹配的DOM根的后代才会触发回调。 如果省略了选择器(仅指定了事件名称),则将直接在窗口小部件的DOM根上设置事件。

注意:不鼓励使用内联函数,将来有时可能会删除它。

custom_events

这几乎与events属性相同,但键是任意字符串。 它们表示由某些子窗口小部件触发的业务事件。 当一个事件被触发时,它将“冒泡”小部件树(有关更多详细信息,请参阅有关组件通信的部分)。

Widget.isDestroyed()

Returns: true如果小部件正在被销毁或被销毁,否则为假

Widget.$(selector)

将指定为参数的CSS选择器应用于窗口小部件的DOM根目录:

this.$(selector);

在功能上与:

this.$el.find(selector);

Arguments:
selector (String)
-- CSS selector
Returns: jQuery object

这个帮助方法类似于Backbone.View.$

Widget.setElement(element)

将窗口小部件的DOM根重新设置为提供的元素,还处理重新设置DOM根的各种别名以及取消设置和重新设置委派事件。

Arguments:
element (Element)
-- a DOM element or jQuery object to set as the widget's DOM root 要设置为窗口小部件DOM根的DOM元素或jQuery对象

Inserting a widget in the DOM

Widget.appendTo(element)

呈现窗口小部件并将其作为目标的最后一个子项插入,使用.appendTo()

Widget.prependTo(element)

呈现窗口小部件并将其作为目标的第一个子项插入,使用.prependTo()

Widget.insertAfter(element)

渲染窗口小部件并将其作为目标的前一个兄弟插入,使用.insertAfter()

Widget.insertBefore(element)

渲染窗口小部件并将其作为目标的以下兄弟插入,使用.insertBefore()

所有这些方法都接受相应的jQuery方法接受的任何内容(CSS选择器,DOM节点或jQuery对象)。 他们都返回延期,并负责三项任务:

  • rendering the widget's root element via(渲染窗口小部件的根元素通过)
    renderElement()

  • inserting the widget's root element in the DOM using whichever jQuery(使用任何jQuery将小部件的根元素插入DOM) method they match
  • 启动窗口小部件,并返回启动它的结果

Widget Guidelines

  • Identifiers (id attribute) should be avoided. In generic applications(应避免使用标识符(id属性)。 在通用应用程序中)
    和模块,id限制了组件的可重用性,并且往往使代码更脆弱。 大多数情况下,它们可以替换为任何内容,类或保持对DOM节点或jQuery元素的引用。

如果id是绝对必要的(因为第三方库需要一个),则应使用_.uniqueId()部分生成id,例如:

this.id = _.uniqueId('my-widget-');
  • 避免使用可预测/常见的CSS类名。 诸如“内容”或“导航”之类的类名称可能与所需的含义/语义相匹配,但很可能其他开发人员具有相同的需求,从而产生命名冲突和意外行为。 通用类名称应以例如前缀为例。 它们所属组件的名称(创建“非正式”命名空间,就像在C或Objective-C中一样)。

  • 应避免使用全局选择器。 由于组件可能在单个页面中多次使用(Odoo中的示例是仪表板),因此查询应限制在给定组件的范围内。 未过滤的选择(例如$(selector)document.querySelectorAll(selector)通常会导致意外或不正确的行为。 Odoo Web的Widget()具有提供其DOM根($ el)的属性,以及直接选择节点的快捷方式($())。

  • 更一般地说,永远不要假设您的组件拥有或控制超出其个人$ el的任何内容(因此,避免使用对父窗口小部件的引用)

  • 除非绝对琐碎,否则Html模板/渲染应该使用QWeb。

  • 所有交互式组件(向屏幕显示信息或拦截DOM事件的组件)必须从Widget()继承并正确实现和使用其API和生命周期。

QWeb Template Engine

Web客户端使用QWeb模板引擎来呈现窗口小部件(除非它们覆盖renderElement方法以执行其他操作)。 Qweb JS模板引擎基于XML,并且主要与python实现兼容。

现在,让我们解释一下如何加载模板。 每当Web客户端启动时,都会对/ web / webclient / qweb路由建立一个rpc。 然后,服务器将返回每个已安装模块的数据文件中定义的所有模板的列表。 每个模块清单中的qweb条目中都列出了正确的文件。

在启动第一个小部件之前,Web客户端将等待加载该模板列表。

这种机制可以很好地满足我们的需求,但有时候,我们想要延迟加载模板。 例如,假设我们有一个很少使用的小部件。 在这种情况下,我们可能不希望在主文件中加载其模板,以使Web客户端稍微轻一些。 在这种情况下,我们可以使用Widget的xmlDependencies键:

var Widget = require('web.Widget');

var Counter = Widget.extend({
    template: 'some.template',
    xmlDependencies: ['/myaddon/path/to/my/file.xml'],

    ...

});

有了这个,Counter小部件将在其willStart方法中加载xmlDependencies文件,因此在执行渲染时模板将准备就绪。

Event system

目前Odoo支持两种事件系统:一个允许添加监听器和触发事件的简单系统,以及一个更完整的系统,它也会使事件“冒泡”。

这两个事件系统都在事件mixins.js中的EventDispatcherMixin中实现。 这个mixin包含在Widget类中。

Base Event system

这个事件系统在历史上是第一个。 它实现了一个简单的总线模式。 我们有4种主要方法:

  • on:用于在事件上注册侦听器。
  • off:用于删除事件监听器。
  • once:用于注册只调用一次的侦听器。
  • trigger(触发器):触发事件。 这将导致每个侦听器被调用。

以下是有关如何使用此事件系统的示例:

var Widget = require('web.Widget');
var Counter = require('myModule.Counter');

var MyWidget = Widget.extend({
    start: function () {
        this.counter = new Counter(this);
        this.counter.on('valuechange', this, this._onValueChange);
        var def = this.counter.appendTo(this.$el);
        return $.when(def, this._super.apply(this, arguments);
    },
    _onValueChange: function (val) {
        // do something with val
    },
});

// in Counter widget, we need to call the trigger method:

... this.trigger('valuechange', someValue);

转载于:https://www.cnblogs.com/myt2000/p/10997945.html

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值