Monorail tutorial

1 Reusing UI portions (ViewComponents)

一些ui部分在一些页面经常被复用。如果这些内容决大多数是静态的内容,我们可以使用ViewComponentViewComponent类和Controller类的功能类似。都可以使用views,可以传送数据去view。同样还支持inner sections和paraments。

 

Creating a ViewComponent

ViewComponent类
继承自ViewComponent抽象类。有3个方法可以重载 Initialize:用来初始化view component的状态,通常用来核查提供的参数。
Render:(selects the view or uses another approach to render the component content)选择一个view或者使用 SupportsSection:invoked by the view engine to check if the component supports the section supplied on the view

 

using  Castle.MonoRail.Framework;

public   class  HeaderComponent : ViewComponent
{
}

 

上面的ViewComponent因为客户什么都没定义所以返回1个default行为。这个default行为呈现出和这个Component相关的view,这个view是 views/components/headercomponent/default.vm.

像Controller一样你可以选择不同的view

 

 

using  Castle.MonoRail.Framework;

public   class  HeaderComponent : ViewComponent

{
    
public override void Render()
        
{
            RenderView(
"otherview");
        }

}

 


上面的代码会选择views/components/headercomponent/otherview.vm.

Using a ViewComponent

ViewComponent和Controller没有关系,只和被Controller选定的views有关

 

#component(HeaderComponent)
#blockcomponent(NewsComponent)
< ul >
#foreach($new in $news)
 
< li > $news.Date $news.Title </ li >
#end
</ ul >
#end

 

Using parameters

你可以提供一些参数给ViewComponent.使用ComponentParams可以在ViewComponent使用参数}

 

using  Castle.MonoRail.Framework;
public   class  TableComponent : ViewComponent
{
    
private ICollection elements;
    
private object border;
    
private string style;
    
private object cellpadding;
    
private object cellspacing;
 
    
public override void Initialize()
      
{
          elements 
= (ICollection) ComponentParams["elements"];
          border 
= ComponentParams["border"];
          style 
= (String) ComponentParams["style"];
          cellpadding 
= ComponentParams["cellpadding"];
          cellspacing 
= ComponentParams["cellspacing"];

        
base.Initialize();
      }

      

 

 

 

#blockcomponent(TableComponent with "elements=$items" "border=0" "style=border: 1px solid black;" "cellpadding=0" "cellspacing=2")



#end

 

 Block and nested sections

 

#blockcomponent(RepeatComponent)
This is the inner content
$counter
#end

 

 

 

using  Castle.MonoRail.Framework;

public   class  RepeatComponent : ViewComponent
{
    
public override void Render()
      
{
        
for(int i=0; i < 5; i++)
          
{
              PropertyBag[
"counter"= i;
              Context.RenderBody();
          }

      }

}


 

 

 

#blockcomponent(TableComponent with "elements=$items")
#colheaders
< tr >
    
< th > &nbsp; </ th >
    
< th > Element </ th >
</ tr >
#end

#item
< tr >
    
< td > $index </ td >
    
< td > $item </ td >
</ tr >
#end
 

#altitem
< tr >
    
< td  align ="center" > $index </ td >
    
< td > $item </ td >
</ tr >
#end

#end

 

 

 

 

using  Castle.MonoRail.Framework;

public   class  TableComponent : ViewComponent
{
    
private ICollection elements;
    
private object border;
    
private string style;
    
private object cellpadding;
    
private object cellspacing;

    
public override void Initialize()
      
{
          elements 
= (ICollection) ComponentParams["elements"];

          border 
= ComponentParams["border"];
          style 
= (String) ComponentParams["style"];

          cellpadding 
= ComponentParams["cellpadding"];

          cellspacing 
= ComponentParams["cellspacing"];

       
base.Initialize();
      }


    
public override void Render()
      
{
          RenderText(

              String.Format(
"<table border=\"{0}\" style=\"{1}\" cellpadding=\"{2}\" cellspacing=\"{3}\">"

                            border, style, cellpadding, cellspacing));

        
if (Context.HasSection("colheaders"))
          
{
              Context.RenderSection(
"colheaders");
          }


        
if (elements != null)
          
{
            
int index = 0;
            
foreach(object item in elements)
              
{
                  PropertyBag[
"index"= ++index;
                  PropertyBag[
"item"= item;

                
if (Context.HasSection("altitem"&& index % 2 != 0)
                  
{
                      Context.RenderSection(
"altitem");
                  }

                
else
                  
{
                      Context.RenderSection(
"item");
                  }

              }

          }


          RenderText(
"</table>");
      }


    
public override bool SupportsSection(string name)
      
{
        
return name == "colheaders" || name == "item" || name == "altitem";
      }

}


 

 更多关于 SmartDispatcherController

 Source

默认的binder使用Params收集参数,你也可以自定义这些:

 

using  Castle.MonoRail.Framework;
 
public   class  ProductController : SmartDispatcherController
{
    
public void Create([DataBind("product", From=ParamStore.Form)] Product product)
       
{
           
       }

}

 


Defining accessible properties

DataBindAttribute经常作用于Domain Model Class,你可能不想所有的属性都可绑定。你可以使用Allow 和Exclude

用逗号分隔属性

 

public   class  AccountController : SmartDispatcherController 
{
    
public void CreateAccount([DataBind("account", Allow="Name,Email,Password")] Account account)
       

           
       }

}

 

上面的例子表示你只想 Name,Email,Password属性绑定值,其它的都被忽视

Exclude属性和Allow属性刚好相反。

 

 Binding Errors

 Binding errors可能出现,如验证日期。使用简单的binding ,异常会被抛出,当使用了DataBindAttribute,异常不会被抛出

 可以使用GetDataBindErrors方法进入错误信息

 

 

public   class  AccountController : SmartDispatcherController 
{
    
public void CreateAccount([DataBind("account")] Account account)
      

          ErrorList errors 
= GetDataBindErrors(account);

          
      }

}


 

ErrorList继承自ICollection,所以你可以枚举错误信息。你也可以检查一个属性的转换。

 

public   class  AccountController : SmartDispatcherController 
{
    
public void CreateAccount([DataBind("account")] Account account)
      

          ErrorList errors 
= GetDataBindErrors(account);

        
if (errors.Contains("DateOfBirth"))
          
{
              Flash[
"error"= errors["DateOfBirth"].ToString(); // Or Exception

              RedirectToAction(
"New", Params);
          }

          
      }

}

 

BindObject and BindObjectInstance

You do not need to always use parameters to have an object bound. The methods BindObject and BindObjectInstance, exposed by the SmartDispatcherController, allow you to have the same functionality. The benefit is that not under every case you want to perform the bindings. For example: 

 

public   class  AccountController : SmartDispatcherController 
{
    
public void CreateAccount(bool acceptedConditions)
      

        
if (acceptedConditions)
          
{
              Account account 
= (Account) BindObject(ParamStore.Form, typeof(Account), "account");

              
          }


          

      }

}


 

 

SmartDispatcherController

 SmartDispatcherController继承自Controller,支持参数绑定。他允许你绑定来自form的参数到action的arguments。另外还支持重载。

 monorail可以绑定简单的值还可以绑定复杂的对象。

 Simple binding

考虑下面的html表单

 

< form  action ="/User/Search.rails"  method ="post" >  
         Name: 
< input  type ="text"  name ="name"   />  
         Email: 
< input  type ="text"  name ="email"   />  
         Country: 
    
< select  name ="country" >  
        
< option  value ="44" > England </ option >
        
< option  value ="55" > Brazil </ option >
    
</ select >
    
< input  type ="submit"  value ="Search"   />
</ form >  

 

 标准的在Cntroller获得窗体元素值的,方法如下

Params:has query string, form and environment entries

Form:Has only form entries (method post)

Query:Has only query string entries

eg:

 

using  Castle.MonoRail.Framework;

public   class  UserController : Controller
{
    
public void Search()
         
{
             String name 
= Form["name"];
             String email 
= Form["email"];
             String country 
= Form["country"];

        
// Perform search 
         }

}


 

 

现在如果你使用SmartDispatcherController你可以用更简洁的代码代替上面的

 

using  Castle.MonoRail.Framework;
public   class  UserController : SmartDispatcherController
{
    
public void Search(string name, string email, string country)
         
{
        
// Perform search 
         }

}

 

SmartdispathcherController履行一些约定(更多在下面)。如果值没被提供,参数会假设是个默认值。另外,如果提供了值,但是无法转换成相应的类型,那么就会throw exception,这个action就不会执行了

Array support

 Controller支持Array。你可以使用2种方法使得窗体元素工作。

 第1种方法是重复这个元素的name

eg:

 

< form  action ="SaveValues.rails"  method ="post" >  
    
< input  type ="text"  name ="name"  value ="1"   />  
    
< input  type ="text"  name ="name"  value ="2"   />  
    
< input  type ="text"  name ="name"  value ="3"   />  
    
< input  type ="text"  name ="name"  value ="4"   />  
    
< input  type ="text"  name ="name"  value ="5"   />  
</ form >     

 

 

第2种方法是使用indexed value notation。index value 对monorail来说是没什么意义的,但是必须唯一。

< form action= "SaveValues.rails"  method= "post"
    < input type= "text"  name= "name[0]"  value= "1"  /
    < input type= "text"  name= "name[1]"  value= "2"  /
    < input type= "text"  name= "name[2]"  value= "3"  /
    < input type= "text"  name= "name[3]"  value= "4"  /
    < input type= "text"  name= "name[4]"  value= "5"  /
< /form>    

 

eg:

using Castle.MonoRail.Framework;
 
public  class UserController : SmartDispatcherController
{
     public  void SaveValues( string[] name)
         {
             ...
         }
}

 

Using the DataBindAttribute

比起一般的值来,我相信你更想绑定对象,使用DataBindAttribute这一切将变得可能

First of all you must use a prefix which is required to avoid name clashing. It is as giving the form elements a name space. The form below uses product as a prefix:

简单的值,聚合对象,数组都被支持。

值得注意的是,只用form元素使用了正确的name约定,binder才会工作。

首先你需要你一个前缀,这样就能避免name发生冲突。这相当于给了窗体元素一个命名空间。

下面的窗体使用 product作为前缀。

eg:

< form method= "post"  action= "create.rails">
< input type= "text"  name= "product.id"  />
< input type= "text"  name= "product.name"  />
< input type= "checkbox"  name= "product.inStock"  id= " value=" true " />
< /form>

On the controller action you must specify the prefix as the argument to the DataBindAttribute:

 

在Controller Action 这1端 你必须把上面使用的product前缀最为DataBind的参数


using Castle.MonoRail.Framework;
 
public  class ProductController : SmartDispatcherController
{
     public  void Create([DataBind( "product")] Product prod)
        {
        }
}
 
public  class Product
{
     private  int id;
     private  String name;
     private  bool inStock;         
     public  int Id
        {
             get {  return id; }
             set { id =  value; }
        }
 
     public  string Name
        {
             get {  return name; }
             set { name =  value; }
        }
 
     public  bool InStock
        {
             get {  return inStock; }
             set { inStock =  value; }
        }
}

 

Arrays

1.窗体元素必须使用indexed notation(像之前所说的)

< form method= "post"  action= "create.rails">
< input type= "text"  name= "product[0].id"  />
< input type= "text"  name= "product[0].name"  />
< input type= "checkbox"  name= "product[0].inStock"  id= " value=" true " />
 
< input type= "text"  name= "product[1].id"  />
< input type= "text"  name= "product[1].name"  />
< input type= "checkbox"  name= "product[1].inStock"  id= " value=" true " />
< /form>

2.Controller必须声明参数是Products数组类型

using Castle.MonoRail.Framework;
 
public  class ProductController : SmartDispatcherController
{
     public  void Create([DataBind( "product")] Product[] prods)
      {
      }
}
 
 
 
public  class Product
{
     private Category[] categories;
 
     // others fields omitted
    
     public Category[] Categories
      {
           get {  return categories; }
           set { categories =  value; }
      }
 
     // others properties omitted
}
 
 
public  class Category
{
     private  String name;
 
     public  string Name
      {
           get {  return name; }
           set { name =  value; }
      }
}
< form method= "post"  action= "create.rails">
< input type= "text"  name= "product.id"  />
< input type= "text"  name= "product.name"  />
< input type= "checkbox"  name= "product.inStock"  id= " value=" true " />
 
< input type= "checkbox"  name= "product.categories[0].name"  value= "Kitchen"  /
< input type= "checkbox"  name= "product.categories[1].name"  value= "Bedroom"  /
< input type= "checkbox"  name= "product.categories[2].name"  value= "Living-room"  /
< /form>

Layouts

Layout 允许你使用template ,这样你可以在每个View使用一些公共的html

Layout是标准的View,大那时他们需要放在名字为layouts的活页夹下,layouts活页夹必须放在views活页夹下。

你可以使用Layout Attribute把Controller和Layout关联起来。

eg:

using Castle.MonoRail.Framework;
 
[Layout( "application")]
public  class CustomerController : Controller
{
     public  void Index()
       {
       }
}

 

In some scenarios you might want to turn off the layout processing. To do so use CancelLayout method. There are other cases where you want to render a specific view and turn off layout at the same time. The RenderView and RenderSharedView have overloads to allow you to do that.

某些情况下你不想呈现layout。你可以使用CancelLayout取消呈现layout。

使用下面的方法也可以取消呈现layout

RenderView(String name, bool skipLayout)
RenderView(String controller, String name, bool skipLayout)
RenderSharedView(String name, bool skipLayout)

using Castle.MonoRail.Framework;
 
[Layout( "application")]
public  class CustomerController : Controller
{
     public  void Index()
       {
           RenderView( "welcome"true);
       }
}

 


< html>
 
Welcome
 
$childContent
 
Footer
 
< /html>

Rescues

rescue是一个特殊的view,只有在发生异常的时候才呈现出来。rescue的视图文件必须在views活页夹下的rescues活页夹下。

rescue可以和一个controller或者1个action联系起来工作。你可以为一个特定的exception写1个rescue.
如果1个action发生了exception,MonoRail会匹配和这个exception最接近的定义了的rescue,

using Castle.MonoRail.Framework;
 
[Rescue( "dberror"typeof(System.Data.SqlException))]
public  class ProductController : Controller
{
    [Rescue( "commonerror")]
     public  void Index()
      {
         throw  new System.Data.SqlException( "fake error");
      }
 
    [Rescue( "dumbprogrammer"typeof(DivideByZeroException))]
     public  void List()
      {
         int val = 0;
         int x = 10 / val;
      }
 
     public  void Search()
      {
      }

 

上面的rescue定义了下面的规则

1.这个Controller的任何action    throws SqlException,view/rescues/dberror 将被呈现

2.如果Index Action发生任意exception(包括SqlException),view/rescues/commonerror将呈现,他会覆盖Controller级别的Rescue

3.如果List Action throws    DivideByZeroException view/rescues/dumbprogrammer 将呈现

Flash

Flash是一种在Requests之间保留暂时值的方法。当你执行一些操作然后redirect的时候十分有用。

在redirect目标页面你可以check flash得到一些状态玛,或者错误信息等

using Castle.MonoRail.Framework;
 
public  class AdminController : Controller
{
     public  void PasswordManagement()
       {
       }
 
     public  void ChangePassword()
       {
            String passwd = Params[ "password"];
    
         if (passwd.Length < 6)
           {
               Flash[ "error"] =  "Password too weak, operation aborted";
           }
         else
           {
             // Change password
           }
    
           RedirectToAction( "PasswordManagement");
       }
}

 

上面的代码可能不太清楚。下面让我们看看到底发生了什么

1。 用户进入PasswordManagement Action

2 一个包含修改密码的     post去     ChangePassword action 的form的页面出现

3。执行ChangePassword action的时候     添加一些实体到Flash里面

4。用户又被send去PasswordManagement action

5。PasswordManagement View检查 Flash,显示一些有意义的信息

Working with Views

活页夹结构约定

必须有一个活页夹。如果Controller和area联系,那么这个必须反映在View活页夹结构上选择一个View呈现

当action被调用的时候mr会预选择和这个action同名的view呈现。

eg:


using Castle.MonoRail.Framework;
 
public  class CustomerController : Controller
{
     public  void Index()
          {
          }
}

 

当index action被调用,views\customer\index View会被预选择

如果我们不想views\customer\index view被呈现,我们可以使用RenderView方法,选择不同的view

using Castle.MonoRail.Framework;
 
public  class CustomerController : Controller
{
     public  void Index()
         {
             RenderView( "welcome");
         }
}

 

上面的代码选择了一个 views\customer\welcome view

 

Note:

当RenderView被调用的时候View并不执行,只是选择了。只有在action方法返回的时候view才执行

传参数给View

You would probably want to supply data to the view so it can generate dynamic content. This should be done using the PropertyBag. For example:

可能你想提供一些数据给view这样你就能生成动态内容。这应该使用PropertyBag。

eg:

using Castle.MonoRail.Framework;
public  class TestController : Controller
{
     public  void ShowTime()
         {
             PropertyBag[ "now"] =  DateTime.Now;
         }
}

 

PropertyBag 是一个词典。每种View引擎使用一个信道使得数据在view里面可得。下面的例子使用引擎

NVelocity

eg:

< html>
Hello, the time now is $now
< /html>

 

共享Views

 

一些Views可能被某些Controllers共享。这种情况我们使用RenderShareView

using Castle.MonoRail.Framework;
public  class CustomerController : Controller
{
     public  void Index()     
        {         
           RenderSharedView( "common/welcome");
        }
}

 

上面的代码选择views\common\welcome view

取消View

尽管听起来很奇怪,在某些情况下我们不想view呈现

我们可以使用CancelView 方法

using Castle.MonoRail.Framework;
public  class CustomerController : Controller
{
     public  void Index()
        {
            CancelView();
        }
}

 

Filters

Filters在action的前后执行。对于安全来说这个很有用(提交数据之前进行验证),还可以减少重复代码

创建一个Filter

Filter必须实现IFilter接口,这样filter才能和你的Controller结合起来

using Castle.MonoRail.Framework;
 
public  class AuthenticationFilter : IFilter
{
     public  bool Perform(ExecuteEnum exec, IRailsEngineContext context, Controller controller)
          {
         if (context.Session.Contains( "user"))
              {
             return  true;
              }
         else
              {
                  context.Response.Redirect( "account""login");
              }
 
         return  false;
          }
}

 

 

ExecuteEnum属性可以设置Filter在什么时候被执行。

ExecuteEnum fields Description

BeforeAction 在action之前执行.

AfterAction 在action之后执行.

AfterRendering 在rendering后执行.

Always 每一步都执行.

只用FilterAttribute可以把filter和Controller联系起来

using Castle.MonoRail.Framework;
 
[FilterAttribute(ExecuteEnum.BeforeAction,  typeof(AuthenticationFilter))]
public  class AdminController : Controller
{
     public  void Index()
          {
          }
}

 

 

Ordering

 

一个Controller可以使用多个Filter,而且可以使用ExecutionOrder属性对Filter执行的先后进行排序。

值越低,优先级越高。

eg:

using Castle.MonoRail.Framework;
 
[FilterAttribute(ExecuteEnum.BeforeAction,  typeof(AuthenticationFilter), ExecutionOrder=0)]
[FilterAttribute(ExecuteEnum.BeforeAction,  typeof(LocalizationFilter), ExecutionOrder=1)]
public  class AdminController : Controller
{
     public  void Index()
         {
         }

 

上面的代码,AuthenticationFilter 在LocalizationFilter之前执行

Skipping filters

有些情况下我们不想1个action或多个action执行一个或多个filter。

那么我们可以使用SkipFilter在这些case上

eg:

using Castle.MonoRail.Framework;
 
[FilterAttribute(ExecuteEnum.BeforeAction,  typeof(AuthenticationFilter), ExecutionOrder=0)]
[FilterAttribute(ExecuteEnum.BeforeAction,  typeof(LocalizationFilter), ExecutionOrder=1)]
public  class AdminController : Controller
{
        [SkipFilter]
     public  void Index()
        {
        }
 
        [SkipFilter( typeof(LocalizationFilter))]
     public  void Create()
        {
        }
 
     public  void Update()
        {
        }
}

在上面的代码中

Index action不会执行任何Filter

Create action不会执行LocalizationFilter

Update action 执行所有的Filter

给Filter附参数

参数类

using Castle.MonoRail.Framework;
[AttributeUsage(AttributeTargets.Class, AllowMultiple= false, Inherited= true), Serializable]
public  class MyCoolFilterAttribute : FilterAttribute
{
     private  readonly  string fileName;
 
     public MyCoolFilterAttribute( String fileName) :  base(ExecuteEnum.BeforeAction,  typeof(CoolFilterImpl))
       {
         this.fileName = fileName;
       }
    
     public  string FileName
       {
            get {  return fileName; }
       }
}

 

Filter类

using Castle.MonoRail.Framework;
 
public  class CoolFilterImpl : IFilter, IFilterAttributeAware
{
     private MyCoolFilterAttribute attribute;
 
     // Implementation of IFilterAttributeAware
     public FilterAttribute Filter 
       { 
            set { attribute = (MyCoolFilterAttribute)  value; } 
       }
    
     // Implementation of IFilter
     public  bool Perform(ExecuteEnum exec, IRailsEngineContext context, Controller controller)
       {
         // Now you can access the parameters:
            String fileName = attribute.FileName; 
        
         // Work
        
         // Allow the process to go on
         return  true;
       }
}

 

Controller类

using Castle.MonoRail.Framework;
[MyCoolFilterAttribute( "customer_messages.txt")]
public  class CustomerController : Controller
{
     public  void Index()
       {
       }

 

Controller 名字的公约
官方建议使用Name+Controller后缀最为Controller类的名字。这样一个Controller被注册的时候,MonoRail会去掉后缀Comtroller使用Name。Controller名字在url进入的时候被使用。
例如ProductController的名字是Product.

我们还可以自己定义名字而不是用之前的约定,这样我们就要使用到了ControllerDetails属性
eg:

using Castle.MonoRail.Framework;

[ControllerDetails( "cust")]
public  class Customer : Controller
{

}

在这我们使用cust作为进入名字

Areas
monorail支持使用概念areas,这是个逻辑Controller群组。这样可以定义一些Controller属于一个群组。
默认的areas为空
为了定义Areas我们还要使用ControllerDetails属性
eg:

using Castle.MonoRail.Framework;

[ControllerDetails(Area= "admin")]
public  class UsersController : Controller
{
     public  void Index()
        {
        }
}

这个Controller可以通过下面的url进入
/admin/users/index.rails

上面的Areas是一级的      我们还可以定义多级的
eg:

using Castle.MonoRail.Framework;

[ControllerDetails(Area= "admin/users")]
public  class PasswordMngController : Controller
{
     public  void Index()
        {
        }
}

我们使用下面的url进入这个Controller
/admin/users/passwordmng/index.rails


Default Action
当程序找不到匹配的方法的时候,会去调用用DefaultAction属性定义的方法。
这样网叶设计人员就可以添加一个View而不需要程序员去添加一个新的方法。
使用DefaultAction属性给Controller提供一个默认action。
DefaultAction只可以定义在class级别

有下面2种情况
1.       没有制定DefaultAction的名字,那么默认的方法就是public void DefaultAction()

[DefaultAction]
public  class HomeController : Controller
{
     public  void Index()
        {
        }

     public  void DefaultAction()
        {
         string template =  "notfound";

         if (HasTemplate( "home/" + Action))
            {
                template = Action;
            }

            RenderView(template);
        }
}


2 .      指定了DefaultAction的名字      那么调用的方法就是你指定的方法

[DefaultAction( "foo")]
public  class HomeController : Controller
{
     public  void Index()
        {
        }

     public  void foo()
        {
         string template =  "notfound";

         if (HasTemplate( "home/" + Action))
            {
                template = Action;
            }

            RenderView(template);
        }
}


Redirecting
Controller提供了大量的Redirect

Name

Description

RedirectToAction(string action)

定向到同一个Controller的别的action

RedirectToAction(String action, params String[] queryStringParameters)

Redirects to another action in the same controller specifying query string entries.

RedirectToAction(String action, IDictionary parameters)

Redirects to another action in the same controller specifying query string entries.

Redirect(String url)

Redirects to the specified URL.

Redirect(String url, IDictionary parameters)

Redirects to the specified URL specifying query string entries.

Redirect(String controller, String action)

Redirects to another controller and action.

Redirect(String area, String controller, String action)

Redirects to another controller and action (within an area).

转载于:https://www.cnblogs.com/ccsonline/archive/2007/06/12/779953.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值