【鸿蒙ArkUI实战开发】基于tabs实现页面布局

100 篇文章 3 订阅
100 篇文章 1 订阅

场景描述

在页面布局过程中,Tabs可以将产品包含的所有内容进行清晰分类,一目了然地呈现应用的内容范围,方便概览与跳转

场景一:tab嵌套list的吸顶效果

场景二:tabbar样式自定义:

1、tabs切换、监听

2、样式自定义

3、tabbar尾端文字渐变

场景三:tabContent切换动画

方案描述

场景一:tab嵌套list的吸顶效果

方案一

实现思路

1、最外层为tabs组件,首页tabContent主要用的stack组件嵌套了scroll组件+导航输入框组件,其中scroll组件嵌套了tabs组件,tabs里面嵌套list组件。

2、外层的滚动组件scroll主要通过onScroll,onScrollEdge以及onScrollFrameBegin回调判断页面是否在顶部,中间还是底部。

3、里层list组件也是通过onReachStart,onReachEnd,onScrollFrameBegin回调来判断list列表是否在顶部,中间还是底部,使用scrollBy滑动指定距离。如Scroll嵌套List滚动时,List组件的edgeEffect属性需设置为EdgeEffect.None。

核心代码

// scroll部分主要逻辑

enum ScrollPosition{

  start,

  center,

  end

}

@Entry

@Component

struct NestedScroll {

  @State listPosition: number = ScrollPosition.start; // 0代表滚动到List顶部,1代表中间值,2代表滚动到List底部。

  @State scrollPosition: number = ScrollPosition.start; // 0代表滚动到页面顶部,1代表中间值,2代表滚动到页面底部。


  ...

  build() {

    Column() {

      Tabs({ barPosition: BarPosition.End, index: this.currentIndex, controller: this.TabsController }) {

        TabContent() {

          Stack({ alignContent: Alignment.Top }) {

            Scroll(this.scrollerForScroll) {

              Column() {

                Column(){

                }

                .width("100%")

                .height("40%")

                .backgroundColor(Color.Pink)



                // tabbar

                Row({ space: 7 }) {

                  Scroll() {

                    ...

                  }

                }

                //tabs

                Tabs({ barPosition: BarPosition.Start, controller: this.subsController }) {

                  TabContent() {

                    List({ space: 10, scroller: this.scrollerForList }) {

                      ...

                    }

                    .onReachStart(() => {

                      this.listPosition = ScrollPosition.start

                    })

                    .onReachEnd(() => {

                      this.listPosition = ScrollPosition.end

                    })

                    .onScrollFrameBegin((offset: number, state: ScrollState) => {

                      console.info('chenoffset::'+offset)

                      // 滑动到列表中间时

                      if (!((this.listPosition == ScrollPosition.start && offset < 0)

                        || (this.listPosition == ScrollPosition.end && offset > 0))) {

                        this.listPosition = ScrollPosition.center

                      }

                      // 如果页面已滚动到底部 且 列表不在顶部或列表有正向偏移量

                      if (this.scrollPosition == ScrollPosition.end

                        && (this.listPosition != ScrollPosition.start || offset > 0)) {

                        console.info('chenoffsetscrollBy::'+offset)

                        return { offsetRemain: offset };

                      } else {

                        // scrollBy滑动指定距离

                        console.info('chenoffsetscrollBy滑动指定距离::'+offset)

                        this.scrollerForScroll.scrollBy(0, offset)

                        return { offsetRemain: 0 };

                      }

                    })

                  }.tabBar('关注')

                  ...

                }

              }

              .width("100%")

              .height("92%")

              .backgroundColor('#F1F3F5')

            }

          }

          .scrollBar(BarState.Off)

          .width("100%")

          .height("100%")

          // 滚动事件回调, 返回竖直方向偏移量,单位vp

          .onScroll((xOffset: number, yOffset: number) => {

            this.currentYOffset = this.scrollerForScroll.currentOffset().yOffset;

            console.info('this.currentYOffset'+this.currentYOffset)

            // 非(页面在顶部或页面在底部),则页面在中间

            if (!((this.scrollPosition == ScrollPosition.start && yOffset < 0)

              || (this.scrollPosition == ScrollPosition.end && yOffset > 0))) {

              this.scrollPosition = ScrollPosition.center

            }

          })

          // 当组件滚动到边缘时触发

          .onScrollEdge((side: Edge) => {

            if (side == Edge.Top) {

              // 页面在顶部

              this.scrollPosition = ScrollPosition.start

            } else if (side == Edge.Bottom) {

              // 页面在底部

              this.scrollPosition = ScrollPosition.end

            }

          })

          .onScrollFrameBegin(offset => {

            if (this.scrollPosition == ScrollPosition.end) {

              return { offsetRemain: 0 };

            } else {

              return { offsetRemain: offset };

            }

          })

          // 顶部导航输入框

          Row() {

            TextInput({ text: '', placeholder: 'input your word...', controller: this.controller }).fontSize(24)

          }

          .justifyContent(FlexAlign.Center)

          .backgroundColor('#00ffffff')

          .width('100%')

          .height('8%')

        }

      }.tabBar(this.tabBuilder(0, '首页'))

      ...

    }

    ...

  }.width('100%')

}

}

方案二:

通过原生属性nestedScroll,结合calc计算高度实现上述效果

核心代码

Tabs({ barPosition: BarPosition.Start, controller: this.subsController }) {

  TabContent() {

    List({ space: 10, scroller: this.scrollerForList }) {

      ...

    }

    .nestedScroll({

      scrollForward: NestedScrollMode.PARENT_FIRST,

      scrollBackward: NestedScrollMode.SELF_FIRST

    })

场景二:tabbar样式自定义

方案

由于tabs本身是有组件进行封装的,如果需要自定义样式,可以使用swiper自定义实现,Swiper在能力演进上会比Tabs能力强,比如使用swiper自定义的tabs组件可以实现数据懒加载功能

通过swiper实现tabs以下功能点:

1.下划线跟手动画:通过swiper的onGestureSwipe在页面跟手滑动过程中的回调,返回index以及extraInfo动画相关信息来判断当前index、页签距离左边margin,以及当前页签的宽度信息等,再利用动画开始以及动画结束回调结合animateTo实现下划线的动效。

2.tabbar 选中文字颜色变化:判断是否为currentIndex设置为不一样的文字颜色。

3.tabbar 选中页签位置居中:用scroll+row自定义页签栏,通过scroll实现页签停留位置居中效果。

4.使用图像效果blendMode,将当前控件的内容与下方画布已有内容进行混合,给自定义tabbar的组件row设置.blendMode,给row的父组件设置linearGradient以及blendMode来实现文字尾端渐变效果。

关于blendMode枚举说明,s表示源像素,d表示目标像素,sa表示原像素透明度,da表示目标像素透明度,r表示混合后像素,ra表示混合后像素透明度。

BlendMode.SRC_IN:r = s * da,只显示源像素中与目标像素重叠的部分。

BlendMode.SRC_OVER:r = s * (1 - da),只显示源像素中与目标像素不重叠的部分。

BlendApplyType.OFFSCREEN:将此组件和子组件内容绘制到离屏画布上,然后整体进行混合

核心代码

第一步:通过scroll组件+row组件实现自定义可滑动的tabbar

Row(){

  Column() {

    Scroll(this.scroller) {

      Row() {

        ForEach(this.arr, (item: string, index: number) => {

          Column() {

            Text(item)

              .fontSize(16)

              .borderRadius(5)

                //字体颜色粗细变化

              .fontColor(this.indicatorIndex === index ? Color.Red : Color.Black)

              .fontWeight(this.indicatorIndex === index ? FontWeight.Bold : FontWeight.Normal)

              .margin({ left: this.initialTabMargin, right: this.initialTabMargin })

              .id(index.toString())

              .onAreaChange((oldValue: Area, newValue: Area) => {

                if (this.indicatorIndex === index && (this.indicatorMarginLeft === 0 || this.indicatorWidth === 0)) {

                  if (newValue.globalPosition.x != undefined) {

                    let positionX = Number.parseFloat(newValue.globalPosition.x.toString());

                    this.indicatorMarginLeft = Number.isNaN(positionX) ? 0 : positionX;

                  }

                  let width = Number.parseFloat(newValue.width.toString());

                  this.indicatorWidth = Number.isNaN(width) ? 0 : width;

                }

              })

              .onClick(() => {

                this.indicatorIndex = index;

                this.underlineScrollAuto(this.animationDuration, index);

                this.scrollIntoView(index);

                // swiper进行联动

                this.swiperIndex = index;

              })

          }

          .width(this.textLength[index] * 28)

        }, (item: string) => item)

      }

      .height(32)

    }

    .width('100%')

    .scrollable(ScrollDirection.Horizontal)

    .scrollBar(BarState.Off)

    .edgeEffect(EdgeEffect.Spring)

    .onScroll((xOffset: number, yOffset: number) => {

      console.info(xOffset + ' ' + yOffset)

      this.indicatorMarginLeft -= xOffset;

    })

    .onScrollStop(() => {

      console.info('Scroll Stop')

      this.underlineScrollAuto(0, this.indicatorIndex);

    })

    //下划线

    Column()

      .width(this.indicatorWidth)

      .height(2)

      .borderRadius(2)

      .backgroundColor(Color.Red)

      .alignSelf(ItemAlign.Start)

      .margin({ left: this.indicatorMarginLeft, top: 5 })

  }

  .width('92%')

  .margin({ top: 15, bottom: 10})

  Text('更多')

    .width(36)

    .height(50)

    .backgroundColor(Color.Pink)

    .fontSize(16)

    .borderRadius(5)

}

第二步:通过swiper组件来写tabContent对应的区域,主要用swiper的属性index(this.swiperIndex)来联动上面的自定义tabbar,swiper里面可以使用LazyForEach来实现数据懒加载功能

Swiper(this.swiperController) {

  LazyForEach(this.data, (item: number) => {

    Column() {

      Text(item.toString())

      ...

    }

    .onAreaChange((oldValue: Area, newValue: Area) => {

      let width = Number.parseFloat(newValue.width.toString());

      this.swiperWidth = Number.isNaN(width) ? 0 : width;

    })

  }, (item: string) => item)

}

.onChange((index: number)=>{

  this.swiperIndex = index;

})

.cachedCount(2)

.index(this.swiperIndex)

.indicator(false)

.curve(this.animationCurve)

.loop(false)

第三步:

1、通过swiper的onGestureSwipe,实现跟手过程中是左滑还是右滑,计算当前以及下一个目标页面的索引值,当前距离左边的距离,以及当前tabbar的宽度

2、通过用componentUtils.getRectangleById,获取指定id的组件大小、位置、平移缩放旋转及仿射矩阵属性信息,得到当前距离左边的距离以及对应tabbar的宽度,用onAnimationStart在切换动画开始触发的时候,下划线跟踪页面一起滑动,同时宽度渐变

3、当滑动结束时通过onAnimationEnd以及自定义tabbar的scrollTo等回调实现tabbar在滚动结束之后再中间位置

.onAnimationStart((index: number, targetIndex: number, event: SwiperAnimationEvent) => {

  // 切换动画开始时触发该回调。下划线跟着页面一起滑动,同时宽度渐变。

  this.indicatorIndex = targetIndex;

  this.underlineScrollAuto(this.animationDuration, targetIndex);

})

  .onAnimationEnd((index: number, event: SwiperAnimationEvent) => {

    // 切换动画结束时触发该回调。下划线动画停止。

    let currentIndicatorInfo = this.getCurrentIndicatorInfo(index, event);

    this.startAnimateTo(0, currentIndicatorInfo.left, currentIndicatorInfo.width);

    this.scrollIntoView(index);

  })

  .onGestureSwipe((index: number, event: SwiperAnimationEvent) => {

    // 在页面跟手滑动过程中,逐帧触发该回调。

    let currentIndicatorInfo = this.getCurrentIndicatorInfo(index, event);

    this.indicatorIndex = currentIndicatorInfo.index;//当前页签index

    this.indicatorMarginLeft = currentIndicatorInfo.left;//当前页签距离左边margin

    this.indicatorWidth = currentIndicatorInfo.width;//当前页签宽度

  })

// 获取屏幕宽度,单位vp

private getDisplayWidth(): number {

  return this.displayInfo != null ? px2vp(this.displayInfo.width) : 0;

}

// 获取组件大小、位置、平移缩放旋转及仿射矩阵属性信息。

private getTextInfo(index: number): Record<string, number> {

  let modePosition :componentUtils.ComponentInfo = componentUtils.getRectangleById(index.toString());

  try {

  return { 'left': px2vp(modePosition.windowOffset.x), 'width': px2vp(modePosition.size.width) }

} catch (error) {

  return { 'left': 0, 'width': 0 }

}

}

// 当前下划线动画

private getCurrentIndicatorInfo(index: number, event: SwiperAnimationEvent): Record<string, number> {

  let nextIndex = index;

  // 滑动范围限制,Swiper不可循环,Scroll保持不可循环

  if (index > 0 && event.currentOffset > 0) {

  nextIndex--; // 左滑

} else if (index < this.data.totalCount() - 1 && event.currentOffset < 0) {

  nextIndex++; // 右滑

}

this.nextIndicatorIndex = nextIndex;

// 获取当前tabbar的属性信息

let indexInfo = this.getTextInfo(index);

// 获取目标tabbar的属性信息

let nextIndexInfo = this.getTextInfo(nextIndex);

// 滑动页面超过一半时页面切换

this.swipeRatio = Math.abs(event.currentOffset / this.swiperWidth);

let currentIndex = this.swipeRatio > 0.5 ? nextIndex : index; // 页面滑动超过一半,tabBar切换到下一页。

let currentLeft = indexInfo.left + (nextIndexInfo.left - indexInfo.left) * this.swipeRatio;

let currentWidth = indexInfo.width + (nextIndexInfo.width - indexInfo.width) * this.swipeRatio;

this.indicatorIndex = currentIndex;

return { 'index': currentIndex, 'left': currentLeft, 'width': currentWidth };

}



private scrollIntoView(currentIndex: number): void {

  const indexInfo = this.getTextInfo(currentIndex);

  let tabPositionLeft = indexInfo.left;

  let tabWidth = indexInfo.width;

  // 获取屏幕宽度,单位vp

  const screenWidth = this.getDisplayWidth();

  const currentOffsetX: number = this.scroller.currentOffset().xOffset;//当前滚动的偏移量

  this.scroller.scrollTo({

                         // 将tabbar可滑动时候定位在正中间

                           xOffset: currentOffsetX + tabPositionLeft - screenWidth / 2 + tabWidth / 2,

                           yOffset: 0,

                           animation: {

                             duration: this.animationDuration,

                             curve: this.animationCurve, // 动画曲线

                           }

                         });

  this.underlineScrollAuto(this.animationDuration, currentIndex);

}

private startAnimateTo(duration: number, marginLeft: number, width: number): void {

  animateTo({

  duration: duration, // 动画时长

  curve: this.animationCurve, // 动画曲线

  onFinish: () => {

    console.info('play end')

  }

}, () => {

  this.indicatorMarginLeft = marginLeft;

  this.indicatorWidth = width;

})

}

// 下划线动画

private underlineScrollAuto(duration: number, index: number): void {

  let indexInfo = this.getTextInfo(index);

  this.startAnimateTo(duration, indexInfo.left, indexInfo.width);

}

第四步:使用图像效果blendMode以及颜色渐变linearGradient实现文字尾端有渐变的效果

Scroll(this.scroller) {

  Row() {

    ForEach(this.arr, (item: string, index: number) => {

      ...

    }, (item: string) => item)

  }

  .blendMode(BlendMode.SRC_IN, BlendApplyType.OFFSCREEN)

  .backgroundColor(Color.Transparent)

  .height(32)

}

// 设置tabbar文字尾端显隐

.linearGradient({

  angle: 90,

  colors: [['rgba(0, 0, 0, 0)', 0], ['rgba(0, 0, 0, 1)', 0], ['rgba(0, 0, 0, 1)', 0.9], ['rgba(0, 0, 0, 0)', 1]]

})

.blendMode(BlendMode.SRC_OVER, BlendApplyType.OFFSCREEN)

场景三:tabContent切换动画

方案

通过customContentTransition实现了自定义Tabs页面的切换动画,index0-1,2-3是缩放,其他页面切换时显隐from:动画开始时,当前页面的index值。to:动画开始时,目标页面的index值。使用customContentTransition注意事项:1、当使用自定义切换动画时,Tabs组件自带的默认切换动画会被禁用,同时,页面也无法跟手滑动。2、当设置为undefined时,表示不使用自定义切换动画,仍然使用组件自带的默认切换动画。3、当前自定义切换动画不支持打断。4、目前自定义切换动画只支持两种场景触发:点击页签和调用TabsController.changeIndex()接口。

核心代码

// 自定义tabContent切换效果

// customContentTransition 控制是否为undefined

@State useCustomAnimation: boolean = true

// tabContent对应内容区域缩放值

@State tabContent0Scale: number = 1.0

@State tabContent1Scale: number = 1.0

@State tabContent2Scale: number = 1.0

@State tabContent3Scale: number = 1.0

// tabContent对应内容区域显隐值

@State tabContent0Opacity: number = 1.0

@State tabContent1Opacity: number = 1.0

@State tabContent2Opacity: number = 1.0

@State tabContent3Opacity: number = 1.0

private firstTimeout: number = 1000

private secondTimeout: number = 1000

private first2secondDuration: number = 2000

private second2thirdDuration: number = 2000

private first2thirdDuration: number = 2000

// - from:动画开始时,当前页面的index值。

// - to:动画开始时,目标页面的index值。

private baseCustomAnimation: (from: number, to: number) => TabContentAnimatedTransition = (from: number, to: number) => {

  if ((from === 0 && to === 1) || (from === 1 && to === 0)|| (from === 2 && to === 3)||(from ===3 && to === 2)) {

    // 缩放动画

    let firstCustomTransition = {

      timeout: this.firstTimeout,

      transition: (proxy: TabContentTransitionProxy) => {

        if (proxy.from === 0 && proxy.to === 1) {

          this.tabContent0Scale = 1.0

          this.tabContent1Scale = 0.5

        } else {

          this.tabContent0Scale = 0.5

          this.tabContent1Scale = 1.0

        }

        if (proxy.from === 2 && proxy.to === 3) {

          this.tabContent2Scale = 1.0

          this.tabContent3Scale = 0.5

          this.tabContent3Opacity = 1.0

        } else {

          this.tabContent2Scale = 0.5

          this.tabContent3Scale = 1.0

          this.tabContent2Opacity = 1.0 //透明度

        }

        animateTo({

          duration: this.first2secondDuration,

          onFinish: () => {

            proxy.finishTransition()

          }

        }, () => {

          if (proxy.from === 0 && proxy.to === 1) {

            this.tabContent0Scale = 0.5

            this.tabContent1Scale = 1.0

          } else {

            this.tabContent0Scale = 1.0

            this.tabContent1Scale = 0.5

          }

          if (proxy.from === 2 && proxy.to === 3) {

            this.tabContent2Scale = 0.5

            this.tabContent3Scale = 1.0

            this.tabContent2Opacity = 1.0 //透明度

          } else {

            this.tabContent2Scale = 1.0

            this.tabContent3Scale = 0.5

          }

        })

      }

    } as TabContentAnimatedTransition;

    return firstCustomTransition;

  } else {

    // 透明度动画

    let secondCustomTransition = {

      timeout: this.secondTimeout,

      transition: (proxy: TabContentTransitionProxy) => {

        if ((proxy.from === 1 && proxy.to === 2) || (proxy.from === 2 && proxy.to === 1)) {

          if (proxy.from === 1 && proxy.to === 2) {

            this.tabContent1Opacity = 1.0

            this.tabContent2Opacity = 0.5

          } else {

            this.tabContent1Opacity = 0.5

            this.tabContent2Opacity = 1.0

            this.tabContent1Scale = 1.0

          }

          animateTo({

            duration: this.second2thirdDuration,

            onFinish: () => {

              proxy.finishTransition()

            }

          }, () => {

            if (proxy.from === 1 && proxy.to === 2) {

              this.tabContent1Opacity = 0.5

              this.tabContent2Opacity = 1.0

              this.tabContent2Scale = 1.0

            } else {

              this.tabContent1Opacity = 1.0

              this.tabContent2Opacity = 0.5

            }

          })

        } else if ((proxy.from === 0 && proxy.to === 2) || (proxy.from === 2 && proxy.to === 0) || (proxy.from === 0 && proxy.to === 3) || (proxy.from === 3 && proxy.to === 0) ) {

          if (proxy.from === 0 && proxy.to === 2) {

            this.tabContent0Opacity = 1.0

            this.tabContent2Opacity = 0.5

          } else {

            this.tabContent0Opacity = 0.5

            this.tabContent2Opacity = 1.0

          }

          if (proxy.from === 0 && proxy.to === 3) {

            this.tabContent0Opacity = 1.0

            this.tabContent3Opacity = 0.5

          } else {

            this.tabContent0Opacity = 0.5

            this.tabContent3Opacity = 1.0

          }

          animateTo({

            duration: this.first2thirdDuration,

            onFinish: () => {

              proxy.finishTransition()

            }

          }, () => {

            if (proxy.from === 0 && proxy.to === 2) {

              this.tabContent0Opacity = 0.5

              this.tabContent2Opacity = 1.0

            } else {

              this.tabContent0Opacity = 1.0

              this.tabContent2Opacity = 0.5

            }

            if (proxy.from === 0 && proxy.to === 3) {

              this.tabContent0Opacity = 0.5

              this.tabContent3Opacity = 1.0

            } else {

              this.tabContent0Opacity = 1.0

              this.tabContent3Opacity = 0.5

            }

          })

        }

      }

    } as TabContentAnimatedTransition;

    return secondCustomTransition;

  }

}

鸿蒙全栈开发全新学习指南

有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以要有一份实用的鸿蒙(HarmonyOS NEXT)学习路线与学习文档用来跟着学习是非常有必要的。

针对一些列因素,整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开发技术的学习路线,包含了鸿蒙开发必掌握的核心知识要点,内容有(ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、WebGL、元服务、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、OpenHarmony驱动开发、系统定制移植等等)鸿蒙(HarmonyOS NEXT)技术知识点。

本路线共分为四个阶段

第一阶段:鸿蒙初中级开发必备技能

在这里插入图片描述

第二阶段:鸿蒙南北双向高工技能基础:gitee.com/MNxiaona/733GH

在这里插入图片描述

第三阶段:应用开发中高级就业技术

第四阶段:全网首发-工业级南向设备开发就业技术:gitee.com/MNxiaona/733GH

在这里插入图片描述

《鸿蒙 (Harmony OS)开发学习手册》(共计892页)

如何快速入门?

1.基本概念
2.构建第一个ArkTS应用
3.……

开发基础知识:gitee.com/MNxiaona/733GH

1.应用基础知识
2.配置文件
3.应用数据管理
4.应用安全管理
5.应用隐私保护
6.三方应用调用管控机制
7.资源分类与访问
8.学习ArkTS语言
9.……

在这里插入图片描述

基于ArkTS 开发

1.Ability开发
2.UI开发
3.公共事件与通知
4.窗口管理
5.媒体
6.安全
7.网络与链接
8.电话服务
9.数据管理
10.后台任务(Background Task)管理
11.设备管理
12.设备使用信息统计
13.DFX
14.国际化开发
15.折叠屏系列
16.……

在这里插入图片描述

鸿蒙开发面试真题(含参考答案):gitee.com/MNxiaona/733GH

在这里插入图片描述

鸿蒙入门教学视频:

美团APP实战开发教学:gitee.com/MNxiaona/733GH

写在最后

  • 如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
  • 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  • 关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识。
  • 想要获取更多完整鸿蒙最新学习资源,请移步前往小编:gitee.com/MNxiaona/733GH

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值