【鸿蒙开发基础学习】创建列表(List)

创建列表(List)

一、概述

  1. 布局与约束:列表是一种复杂的容器,当列表项达到一定数量,内容超过屏幕大小时,可以自动提供滚动功能。它适合用于呈现同类数据类型或数据类型集,例如图片和文本。在列表中显示数据集合是许多应用程序中的常见要求(如通讯录、音乐列表、购物清单等)。
  2. 使用列表的优势:使用列表可以轻松高效地显示结构化、可滚动的信息。通过在List组件中按垂直或者水平方向线性排列子组件ListItemGroupListItem,为列表中的行或列提供单个视图,或使用循环渲染迭代一组行或列,或混合任意数量的单个视图和ForEach结构,构建一个列表。List组件支持使用条件渲染、循环渲染、懒加载等渲染控制方式生成子组件。

二、布局与约束

  1. 列表作为容器的布局特点
    • 列表作为一种容器,会自动按其滚动方向排列子组件,向列表中添加组件或从列表中移除组件会重新排列子组件。
    • 在垂直列表中,List按垂直方向自动排列ListItemGroupListItemListItemGroup用于列表数据的分组展示,其子组件也是ListItemListItem表示单个列表项,可以包含单个子组件。
      List、ListItemGroup和ListItem组件关系
    • List的子组件必须是ListItemGroupListItemListItemListItemGroup必须配合List来使用。
  2. 布局能力
    • List除了提供垂直和水平布局能力、超出屏幕时可以滚动的自适应延伸能力之外,还提供了自适应交叉轴方向上排列个数的布局能力。
    • 利用垂直布局能力可以构建单列或者多列垂直滚动列表。
      ![垂直滚动列表(左:单列;右:多列)](https://img-blog.csdnimg.cn/img_convert/c798f55039e05f390d5b7cb1157cbe54.png
    • 利用水平布局能力可以构建单行或多行水平滚动列表。
      ![水平滚动列表(左:单行;右:多行](https://img-blog.csdnimg.cn/img_convert/6856c102763c97664c70faab7db385c0.png
    • 如果布局每列等宽,且不需要跨行跨列布局,相比GridWaterFlow,则更推荐使用List
  3. 约束
    • 列表的主轴方向是指子组件列的排列方向,也是列表的滚动方向。垂直于主轴的轴称为交叉轴,其方向与主轴方向相互垂直。
    • 如果List组件主轴或交叉轴方向设置了尺寸,则其对应方向上的尺寸为设置值。
    • 如果List组件主轴方向没有设置尺寸,当List子组件主轴方向总尺寸小于List的父组件尺寸时,List主轴方向尺寸自动适应子组件的总尺寸。
    • 如果子组件主轴方向总尺寸超过List父组件尺寸时,List主轴方向尺寸适应List的父组件尺寸。
    • List组件交叉轴方向在没有设置尺寸时,其尺寸默认自适应父组件尺寸。

三、开发布局

  1. 设置主轴方向
    • List组件主轴默认是垂直方向,即默认情况下不需要手动设置List方向,就可以构建一个垂直滚动列表。
    • 若是水平滚动列表场景,将ListlistDirection属性设置为Axis.Horizontal即可实现。listDirection默认为Axis.Vertical,即主轴默认是垂直方向。
   List() { 
     //... 
   } 
  .listDirection(Axis.Horizontal) 
  1. 设置交叉轴布局
    • List组件的交叉轴布局可以通过lanesalignListItem属性进行设置,lanes属性用于确定交叉轴排列的列表项数量,alignListItem用于设置子组件在交叉轴方向的对齐方式。
    • List组件的lanes属性通常用于在不同尺寸的设备自适应构建不同行数或列数的列表,即一次开发、多端部署的场景,例如歌单列表。lanes属性的取值类型是"number | LengthConstrain",即整数或者LengthConstrain类型。以垂直列表为例,如果将lanes属性设为 2,表示构建的是一个两列的垂直列表。lanes的默认值为 1,即默认情况下,垂直列表的列数是 1。
   List() { 
     //... 
   } 
  .lanes(2) 
  • 当其取值为LengthConstrain类型时,表示会根据LengthConstrainList组件的尺寸自适应决定行或列数。
   @Entry 
   @Component 
   struct EgLanes { 
     @State egLanes: LengthConstrain = { minLength: 200, maxLength: 300 }
     build() { 
       List() { 
         //... 
       } 
      .lanes(this.egLanes) 
     } 
   } 
  • 同样以垂直列表为例,当alignListItem属性设置为ListItemAlign.Center表示列表项在水平方向上居中对齐。alignListItem的默认值是ListItemAlign.Start,即列表项在列表交叉轴方向上默认按首部对齐。
   List() { 
     //... 
   } 
  .alignListItem(ListItemAlign.Center) 
  1. 在列表中显示数据
    • 列表视图垂直或水平显示项目集合,在行或列超出屏幕时提供滚动功能,使其适合显示大型数据集合。
    • 在最简单的列表形式中,List静态地创建其列表项ListItem的内容。
   @Entry 
   @Component 
   struct CityList { 
     build() { 
       List() { 
         ListItem() { 
           Text('北京').fontSize(24) 
         } 

         ListItem() { 
           Text('杭州').fontSize(24) 
         } 

         ListItem() { 
           Text('上海').fontSize(24) 
         } 
       } 
      .backgroundColor('#FFF1F3F5') 
      .alignListItem(ListItemAlign.Center) 
     } 
   } 
  • 由于在ListItem中只能有一个根节点组件,不支持以平铺形式使用多个组件。因此,若列表项是由多个组件元素组成的,则需要将这多个元素组合到一个容器组件内或组成一个自定义组件。
   List() { 
     ListItem() { 
       Row() { 
         Image($r('app.media.iconE')) 
          .width(40) 
          .height(40) 
          .margin(10) 

         Text('小明') 
          .fontSize(20) 
       } 
     } 

     ListItem() { 
       Row() { 
         Image($r('app.media.iconF')) 
          .width(40) 
          .height(40) 
          .margin(10) 

         Text('小红') 
          .fontSize(20) 
       } 
     } 
   } 
```javascript
   - 迭代列表内容:通常,应用通过数据集合动态地创建列表。使用循环渲染可从数据源中迭代获取数据,并在每次迭代过程中创建相应的组件,降低代码复杂度。ArkTS 通过`ForEach`提供了组件的循环渲染能力。
```javascript
   import { util } from '@kit.ArkTS' 

   class Contact { 
     key: string = util.generateRandomUUID(true); 
     name: string; 
     icon: Resource; 

     constructor(name: string, icon: Resource) { 
       this.name = name; 
       this.icon = icon; 
     } 
   } 

   @Entry 
   @Component 
   struct SimpleContacts { 
     private contacts: Array<object> = [ 
       new Contact('小明', $r("app.media.iconA")), 
       new Contact('小红', $r("app.media.iconB")), 
     ] 

     build() { 
       List() { 
         ForEach(this.contacts, (item: Contact) => { 
           ListItem() { 
             Row() { 
               Image(item.icon) 
                .width(40) 
                .height(40) 
                .margin(10) 
               Text(item.name).fontSize(20) 
             } 
            .width('100%') 
            .justifyContent(FlexAlign.Start) 
           } 
         }, (item: Contact) => JSON.stringify(item)) 
       } 
      .width('100%') 
     } 
   } 
  • List组件中,ForEach除了可以用来循环渲染ListItem,也可以用来循环渲染ListItemGroupListItemGroup的循环渲染详细使用请参见支持分组列表。

四、自定义列表样式

  1. 设置内容间距
    • 在初始化列表时,如需在列表项之间添加间距,可以使用space参数。例如,在每个列表项之间沿主轴方向添加 10vp 的间距:
   List({ space: 10 }) { 
     //... 
   } 
  1. 添加分隔线
    • 分隔线用来将界面元素隔开,使单个元素更加容易识别。
    • List提供了divider属性用于给列表项之间添加分隔线。在设置divider属性时,可以通过strokeWidthcolor属性设置分隔线的粗细和颜色。startMarginendMargin属性分别用于设置分隔线距离列表侧边起始端的距离和距离列表侧边结束端的距离。
   class DividerTmp { 
     strokeWidth: Length = 1 
     startMargin: Length = 60 
     endMargin: Length = 10 
     color: ResourceColor = '#ffe9f0f0' 

     constructor(strokeWidth: Length, startMargin: Length, endMargin: Length, color: ResourceColor) {
       this.strokeWidth = strokeWidth 
       this.startMargin = startMargin 
       this.endMargin = endMargin 
       this.color = color 
     } 
   } 
   @Entry 
   @Component 
   struct EgDivider { 
     @State egDivider: DividerTmp = new DividerTmp(1, 60, 10, '#ffe9f0f0')
     build() { 
       List() { 
         //... 
       } 
      .divider(this.egDivider) 
     } 
   } 
  • 说明:分隔线的宽度会使ListItem之间存在一定间隔,当List设置的内容间距小于分隔线宽度时,ListItem之间的间隔会使用分隔线的宽度。当List存在多列时,分割线的startMarginendMargin作用于每一列上。List组件的分隔线画在两个ListItem之间,第一个ListItem上方和最后一个ListItem下方不会绘制分隔线。
  1. 添加滚动条
    • 当列表项高度(宽度)超出屏幕高度(宽度)时,列表可以沿垂直(水平)方向滚动。在页面内容很多时,若用户需快速定位,可拖拽滚动条。
    • 在使用List组件时,可通过scrollBar属性控制列表滚动条的显示。scrollBar的取值类型为BarState,当取值为BarState.Auto表示按需显示滚动条。此时,当触摸到滚动条区域时显示控件,可上下拖拽滚动条快速浏览内容,拖拽时会变粗。若不进行任何操作,2 秒后滚动条自动消失。
    • scrollBar属性 API version 9 及以下版本默认值为BarState.Off,从 API version 10 版本开始默认值为BarState.Auto
  List() { 
    //... 
  } 
 .scrollBar(BarState.Auto) 
  1. 支持分组列表
    • 在列表中支持数据的分组展示,可以使列表显示结构清晰,查找方便,从而提高使用效率。分组列表在实际应用中十分常见。
    • List组件中使用ListItemGroup对项目进行分组,可以构建二维列表。
    • List组件中可以直接使用一个或者多个ListItemGroup组件,ListItemGroup的宽度默认充满List组件。在初始化ListItemGroup时,可通过header参数设置列表分组的头部组件。
   @Entry 
   @Component 
   struct ContactsList { 
     
     @Builder itemHead(text: string) { 
       // 列表分组的头部组件,对应联系人分组 A、B 等位置的组件
       Text(text) 
        .fontSize(20) 
        .backgroundColor('#fff1f3f5') 
        .width('100%') 
        .padding(5) 
     } 

     build() { 
       List() { 
         ListItemGroup({ header: this.itemHead('A') }) {
           // 循环渲染分组 A 的 ListItem 
         } 

         ListItemGroup({ header: this.itemHead('B') }) {
           // 循环渲染分组 B 的 ListItem 
         } 
       } 
     } 
   } 
  • 如果多个ListItemGroup结构类似,可以将多个分组的数据组成数组,然后使用ForEach对多个分组进行循环渲染。例如在联系人列表中,将每个分组的联系人数据contacts(可参考迭代列表内容章节)和对应分组的标题title数据进行组合,定义为数组contactsGroups。然后在ForEach中对contactsGroups进行循环渲染,即可实现多个分组的联系人列表。可参考添加粘性标题章节示例代码。
  1. 添加粘性标题
    • 粘性标题是一种常见的标题模式,常用于定位字母列表的头部元素。
    • List组件的sticky属性配合ListItemGroup组件使用,用于设置ListItemGroup中的头部组件是否呈现吸顶效果或者尾部组件是否呈现吸底效果。
    • 通过给List组件设置sticky属性为StickyStyle.Header,即可实现列表的粘性标题效果。如果需要支持吸底效果,可以通过footer参数初始化ListItemGroup的底部组件,并将sticky属性设置为StickyStyle.Footer
  import { util } from '@kit.ArkTS' 
  class Contact { 
    key: string = util.generateRandomUUID(true); 
    name: string; 
    icon: Resource; 

    constructor(name: string, icon: Resource) { 
      this.name = name; 
      this.icon = icon; 
    } 
  } 
  class ContactsGroup { 
    title: string = '' 
    contacts: Array<object> | null = null 
    key: string = "" 
  } 
  export let contactsGroups: object[] = [ 
    { 
      title: 'A', 
      contacts: [ 
        new Contact('艾佳', $r('app.media.iconA')), 
        new Contact('安安', $r('app.media.iconB')), 
        new Contact('Angela', $r('app.media.iconC')),
      ], 
      key: util.generateRandomUUID(true) 
    } as ContactsGroup, 
    { 
      title: 'B', 
      contacts: [ 
        new Contact('白叶', $r('app.media.iconD')), 
        new Contact('伯明', $r('app.media.iconE')), 
      ], 
      key: util.generateRandomUUID(true) 
    } as ContactsGroup, 
    //... 
  ] 
  @Entry 
  @Component 
  struct ContactsList { 
    // 定义分组联系人数据集合 contactsGroups 数组 
    @Builder itemHead(text: string) { 
      // 列表分组的头部组件,对应联系人分组 A、B 等位置的组件
      Text(text) 
       .fontSize(20) 
       .backgroundColor('#fff1f3f5') 
       .width('100%') 
       .padding(5) 
    } 
    build() { 
      List() { 
        // 循环渲染 ListItemGroup,contactsGroups 为多个分组联系人 contacts 和标题 title 的数据集合
        ForEach(contactsGroups, (itemGroup: ContactsGroup) => {
          ListItemGroup({ header: this.itemHead(itemGroup.title) }) {
            // 循环渲染 ListItem 
            if (itemGroup.contacts) { 
              ForEach(itemGroup.contacts, (item: Contact) => {
                ListItem() { 
                  //... 
                } 
              }, (item: Contact) => JSON.stringify(item))
            } 
          } 
        }, (itemGroup: ContactsGroup) => JSON.stringify(itemGroup))
      }.sticky(StickyStyle.Header)  // 设置吸顶,实现粘性标题效果
    } 
  } 
  1. 控制滚动位置
    • 控制滚动位置在实际应用中十分常见,例如当新闻页列表项数量庞大,用户滚动列表到一定位置时,希望快速滚动到列表底部或返回列表顶部。此时,可以通过控制滚动位置来实现列表的快速定位。
    • List组件初始化时,可以通过scroller参数绑定一个Scroller对象,进行列表的滚动控制。
   private listScroller: Scroller = new Scroller(); 
   Stack({ alignContent: Alignment.Bottom }) { 
     // 将 listScroller 用于初始化 List 组件的 scroller 参数,完成 listScroller 与列表的绑定。
     List({ space: 20, scroller: this.listScroller }) { 
       //... 
     } 

     Button() { 
       //... 
     } 
    .onClick(() => { 
       // 点击按钮时,指定跳转位置,返回列表顶部 
       this.listScroller.scrollToIndex(0) 
     }) 
   } 
  1. 响应滚动位置
    • 许多应用需要监听列表的滚动位置变化并作出响应。例如,在联系人列表滚动时,如果跨越了不同字母开头的分组,则侧边字母索引栏也需要更新到对应的字母位置。
    • 此场景可以通过监听List组件的onScrollIndex事件来实现,右侧索引栏需要使用字母表索引组件AlphabetIndexer
    • 在列表滚动时,根据列表此时所在的索引值位置firstIndex,重新计算字母索引栏对应字母的位置selectedIndex。由于AlphabetIndexer组件通过selected属性设置了选中项索引值,当selectedIndex变化时会触发AlphabetIndexer组件重新渲染,从而显示为选中对应字母的状态。
   const alphabets = ['#', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K',
     'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
   @Entry 
   @Component 
   struct ContactsList { 
     @State selectedIndex: number = 0; 
     private listScroller: Scroller = new Scroller(); 

     build() { 
       Stack({ alignContent: Alignment.End }) { 
         List({ scroller: this.listScroller }) {} 
       .onScrollIndex((firstIndex: number) => { 
          // 根据列表滚动到的索引值,重新计算对应联系人索引栏的位置 this.selectedIndex
        }) 

        // 字母表索引组件 
        AlphabetIndexer({ arrayValue: alphabets, selected: 0 })
         .selected(this.selectedIndex) 
       } 
     } 
   } 
  • 说明:计算索引值时,ListItemGroup作为一个整体占一个索引值,不计算ListItemGroup内部ListItem的索引值。
  1. 响应列表项侧滑
    • 侧滑菜单在许多应用中都很常见。例如,通讯类应用通常会给消息列表提供侧滑删除功能,即用户可以通过向左侧滑列表的某一项,再点击删除按钮删除消息。
    • ListItemswipeAction属性可用于实现列表项的左右滑动功能。swipeAction属性方法初始化时有必填参数SwipeActionOptions,其中,start参数表示设置列表项右滑时起始端滑出的组件,end参数表示设置列表项左滑时尾端滑出的组件。
    • 在消息列表中,end参数表示设置ListItem左滑时尾端划出自定义组件,即删除按钮。在初始化end方法时,将滑动列表项的索引传入删除按钮组件,当用户点击删除按钮时,可以根据索引值来删除列表项对应的数据,从而实现侧滑删除功能。
    • 实现尾端滑出组件的构建。
   @Builder itemEnd(index: number) { 
     // 构建尾端滑出组件 
     Button({ type: ButtonType.Circle }) { 
       Image($r('app.media.ic_public_delete_filled'))
        .width(20) 
        .height(20) 
     } 
   .onClick(() => { 
       // this.messages 为列表数据源,可根据实际场景构造。点击后从数据源删除指定数据项。
       this.messages.splice(index, 1); 
     }) 
   } 
  • 绑定swipeAction属性到可左滑的ListItem上。
   // 构建 List 时,通过 ForEach 基于数据源 this.messages 循环渲染 ListItem。
   ListItem() { 
     //... 
   } 
 .swipeAction({ 
    end: { 
      // index 为该 ListItem 在 List 中的索引值。 
      builder: () => { this.itemEnd(index) }, 
    } 
  }) // 设置侧滑属性. 
  1. 给列表项添加标记
    • 添加标记是一种无干扰性且直观的方法,用于显示通知或将注意力集中到应用内的某个区域。例如,当消息列表接收到新消息时,通常对应的联系人头像的右上方会出现标记,提示有若干条未读消息。
    • ListItem中使用Badge组件可实现给列表项添加标记功能。Badge是可以附加在单个组件上用于信息标记的容器组件。
    • 在消息列表中,若希望在联系人头像右上角添加标记,可在实现消息列表项ListItem的联系人头像时,将头像Image组件作为Badge的子组件。
    • Badge组件中,countposition参数用于设置需要展示的消息数量和提示点显示位置,还可以通过style参数灵活设置标记的样式。
   ListItem() { 
     Badge({ 
       count: 1, 
       position: BadgePosition.RightTop, 
       style: { badgeSize: 16, badgeColor: '#FA2A2D' }
     }) { 
       // Image 组件实现消息联系人头像 
       //... 
     } 
   } 
  1. 下拉刷新与上拉加载
  • 页面的下拉刷新与上拉加载功能在移动应用中十分常见,例如,新闻页面的内容刷新和加载。这两种操作的原理都是通过响应用户的触摸事件,在顶部或者底部显示一个刷新或加载视图,完成后再将此视图隐藏。
  • 以下拉刷新为例,其实现主要分成三步:
    • 监听手指按下事件,记录其初始位置的值。
    • 监听手指按压移动事件,记录并计算当前移动的位置与初始值的差值,大于 0 表示向下移动,同时设置一个允许移动的最大值。
    • 监听手指抬起事件,若此时移动达到最大值,则触发数据加载并显示刷新视图,加载完成后将此视图隐藏。
  • 下拉刷新与上拉加载的具体实现可参考新闻数据加载。

五、编辑列表

  1. 新增列表项
    • 当用户点击添加按钮时,提供用户新增列表项内容选择或填写的交互界面,用户点击确定后,列表中新增对应的项目。
    • 定义列表项数据结构,以待办事项管理为例,首先定义待办数据结构。
   //ToDo.ets 
   import { util } from '@kit.ArkTS' 

   export class ToDo { 
     key: string = util.generateRandomUUID(true);
     name: string; 

     constructor(name: string) { 
       this.name = name; 
     } 
   } 
  • 构建列表整体布局和列表项。
   //ToDoListItem.ets 
   import { ToDo } from './ToDo'; 
   @Component 
   export struct ToDoListItem { 
     @Link isEditMode: boolean 
     @Link selectedItems: ToDo[] 
     private toDoItem: ToDo = new ToDo(""); 

     build() { 
      Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
        //... 
      } 
    .width('100%') 
    .height(80) 
     //.padding() 根据具体使用场景设置 
    .borderRadius(24) 
     //.linearGradient() 根据具体使用场景设置 
    .gesture( 
       GestureGroup(GestureMode.Exclusive, 
       LongPressGesture() 
        .onAction(() => { 
           //... 
         }) 
       ) 
     ) 
    } 
   } 
  • 初始化待办列表数据和可选事项,最后,构建列表布局和列表项。
   //ToDoList.ets 
   import { ToDo } from './ToDo'; 
   import { ToDoListItem } from './ToDoListItem';

   @Entry 
   @Component 
   struct ToDoList { 
     @State toDoData: ToDo[] = [] 
     @Watch('onEditModeChange') @State isEditMode: boolean = false
     @State selectedItems: ToDo[] = [] 
     private availableThings: string[] = ['读书', '运动', '旅游', '听音乐', '看电影', '唱歌']

     onEditModeChange() { 
       if (!this.isEditMode) { 
         this.selectedItems = [] 
       } 
    } 

     build() { 
       Column() { 
         Row() { 
           if (this.isEditMode) { 
             Text('X') 
              .fontSize(20) 
              .onClick(() => { 
                 this.isEditMode = false; 
               }) 
              .margin({ left: 20, right: 20 }) 
           } else { 
             Text('待办') 
              .fontSize(36) 
              .margin({ left: 40 }) 
             Blank() 
             Text('+') //提供新增列表项入口,即给新增按钮添加点击事件
              .onClick(() => { 
                 TextPickerDialog.show({ 
                   range: this.availableThings, 
                   onAccept: (value: TextPickerResult) => {
                     let arr = Array.isArray(value.index)? value.index : [value.index];
                     for (let i = 0; i < arr.length; i++) { 
                       this.toDoData.push(new ToDo(this.availableThings[arr[i]])); // 新增列表项数据 toDoData(可选事项)
                     } 
                   }, 
                 }) 
               }) 
           } 
           List({ space: 10 }) { 
             ForEach(this.toDoData, (toDoItem: ToDo) => {
               ListItem() { 
                 // 将 toDoData 的每个数据放入到以 model 的形式放进 ListItem 里
                 ToDoListItem({ 
                   isEditMode: this.isEditMode, 
                   toDoItem: toDoItem, 
                   selectedItems: this.selectedItems })
               } 
             }, (toDoItem: ToDo) => toDoItem.key.toString())
           } 
         } 
       } 
     } 
   } 
  1. 删除列表项
    • 当用户长按列表项进入删除模式时,提供用户删除列表项选择的交互界面,用户勾选完成后点击删除按钮,列表中删除对应的项目。
    • 列表的删除功能一般进入编辑模式后才可使用,所以需要提供编辑模式的入口。
    • 以待办列表为例,通过监听列表项的长按事件,当用户长按列表项时,进入编辑模式。
   // 结构参考 
   export class ToDo { 
     key: string = util.generateRandomUUID(true);
     name: string; 
     toDoData: ToDo[] = []; 

     constructor(name: string) { 
       this.name = name; 
     } 
   } 
   // 实现参考 
   Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
     //... 
   } 
 .gesture( 
    GestureGroup(GestureMode.Exclusive, 
    LongPressGesture() 
     .onAction(() => { 
        //... 
      }) 
    ) 
  ) 
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

铸剑先生100

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值