关于鸿蒙的笔记整理

提示:有使用过 vue 或 react 的小伙伴更容易理解

知识点强调: ArkTS所有内容都不支持深层数据更新 UI渲染


一、关于样式

1 . 默认单位 vp

答: vp 是 virtual pixel 的缩写,根据设备像素密度转化为屏幕物理像素,px 直接表示设备的像素,因为我们设备的分辨率密度不同,最好是使用 vp

适配: 可以使用伸缩布局layoutWeight,flex布局,网格系统,栅格系统布局,

2 . 写公共样式

在开发过程中会出现大量代码在进行重复样式设置,@Styles 和 Extend 可以帮我们进行样式复用

1. @styles 方式

  • 只支持通用属性 和 通用事件,且不支持箭头函数语法
  • 在组件内(局部)无需加 function , 在组件外(全局) 定义时要加function
@Styles function textStyle () {
  .width(100)
  .height(50)
  .backgroundColor(Color.Pink)
  .borderRadius(25)
  .onClick(() => {
    promptAction.showToast({
       message: "测试"
    })
  })
}

2. Extend 方式

  • 使用 @Extend 装饰器修饰的函数只能是 全局
  • 且参数可以是一个函数,实现复用事件且可处理不同逻辑
  • 函数可以进行 传参,如果参数是状态变量,状态更新后会刷新UI
// 全局  原生组件                     参数
//  ↓     ↓                          ↓ 
@Extend(Text) function textInputAll (callback?: () => void) {
  .width(100)
  .height(50)
  .backgroundColor(Color.Pink)
  .borderRadius(25)
  .textAlign(TextAlign.Center)
  .fontColor(Color.White)
  .onClick(() => {
    callback && callback()
  })
}

二 、 加载图片

  1. 使用本地图片
// 可以新建一个文件夹,里面放本地图片(ets下)
Image('/assets/a.png')
  1. 使用 resource 下的 media 图片
// resource/media  (a 是文件名,扩展名省略)
Image($r('/app.media.a'))
  1. 使用 resource 下的 rawfile 图片
// resource/rawfile
Image($rawfile('a.png'))
  1. 使用网络图片(必须申请网络权限)
// resource/rawfile
Image("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fsafe-img.xhscdn.com%2Fbw1%2F2bf1b169-d217-44c3-a5b3-dd00813bc20d%3FimageView2%2F2%2Fw%2F1080%2Fformat%2Fjpg&refer=http%3A%2F%2Fsafe-img.xhscdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1704614176&t=e15a2fd5193aeeb24fc95b5dbe395907")
"requestPermissions": [{
  "name":"ohos.permission.INTERNET"
}],

三 、 自定义构建函数 @Builder

如果你不想在直接抽象组件, ArkUI 还提供了一种更轻量的UI元素复用机制 @Builder,可以将重复使用的 UI 元素抽象成一个方法,在 build 方法里调用。称之为自定义构建函数

  1. 使用@Builder 定义一个函数(全局加function)
  2. 在组件里使用这个函数
// 定义 Builder
@Builder
function getCellContent(leftTitle: string, rightValue: string) {
  Row() {
    Row() {
      Text(leftTitle)
      Text(rightValue)
    }
    .width('100%')
    .justifyContent(FlexAlign.SpaceBetween)
    .padding({
      left: 15,
      right: 15
    })
    .borderRadius(8)
    .height(40)
    .backgroundColor(Color.White)
  }.padding({
    left: 10,
    right: 10
  })

}
class CardClass {
  time: string = ""
  location: string = ""
  type: string = ""
}
@State formData: CardClass = {
    time: "2023-12-12",
    location: '回龙观',
    type: '漏油'
  }
// 在组件里使用
Column({ space: 10 }) {
        getCellContent("异常时间", this.formData.time)
        getCellContent("异常位置", this.formData.location)
        getCellContent("异常类型", this.formData.type)
        
        Button("修改数据").onClick(() => {
          this.formData.location = "望京"
        })
      }
      .width('100%')

全局自定义函数的问题

  1. 全局的自定义构建函数可以被整个应用获取(下一代可用-当前4.0暂不支持),不允许使用this和bind方法。

  2. 不可被其他文件引用

  3. 当我点击按钮时数据即使是响应式的,当数据发生改变,该函数不会自动渲染

    • 因为我们刚刚传过去的是一个string类型, string 类型是一个基础类型,按值传递,不具备响应式更新的特点 解决方案:改为按引用传递
// 完整代码
@Entry
@Component
struct BuilderCase {
  @State formData: CardClass = {
    time: "2023-12-12",
    location: '回龙观',
    type: '漏油'
  }
  @Builder
  getCellContent($$: CellParams) {
    Row() {
      Row() {
        Text($$.leftTitle)
        Text($$.rightValue)
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)
      .padding({
        left: 15,
        right: 15
      })
      .borderRadius(8)
      .height(40)
      .backgroundColor(Color.White)
    }.padding({
      left: 10,
      right: 10
    })

  }
  build() {
    Row() {
      Column() {
        Column({ space: 10 }) {
          this.getCellContent({ leftTitle: '异常时间', rightValue: this.formData.time })
          this.getCellContent({ leftTitle: '异常位置', rightValue: this.formData.location })
          this.getCellContent({ leftTitle: '异常类型', rightValue: this.formData.type })
        }
        .width('100%')
        Button("修改数据").onClick(() => {
          this.formData.location = "望京"
        })
      }
      .width('100%')
    }
    .height('100%')
    .backgroundColor('#ccc')
  }
}

class CardClass {
  time: string = ""
  location: string = ""
  type: string = ""
}
class CellParams {
  leftTitle: string = ""
  rightValue: string = ""
}

四、构建函数-@BuilderParam 传递UI

Vue里面有个叫做slot插槽的东西,就是可以传入自定义的结构,整体复用父组件的外观
ArkTS提供了一个叫做BuilderParam的修饰符,你可以在组件中定义这样一个函数属性,在使用组件时直接传入

  1. 使用@BuilderParam 声明一个组件,子组件要在想要显示插槽的地方来调用传入的方法
  2. 在父组件里调用并传入,父组件传递是一个函数,这个函数也要使用 @Builder 修饰
// 使用BuilderParam 声明组件
@Component
struct  HMCard {
  @BuilderParam
  content: () => void
  build() {
    Column () {
      Text("卡片组件")
      Divider()
      Text("传入内容")
      if(this.content) {
        this.content()  // 子组件要在想要显示插槽的地方来调用传入的方法
      }
    }
  }
}
@Entry
@Component
struct BuilderParamCase {
// 声明渲染的函数组件
  @Builder
  getContent () {
    Row() {
      Text("插槽内容")
        .fontColor(Color.Red)
    }
  }
  build() {
    Row() {
      Column() {
        HMCard({ content: this.getContent })  // 调用组件并传入要渲染的函数
      }
      .width('100%')
    }
    .height('100%')
  }
}

五 、 父子组件传值

  • 父传子
// 父组件的子组件上,传递一个对象(HmCommentItem)
   HmCommentItem({ item:item})
// 子组件上进行接收
  item: Partial<ReplyItem> = {} // 默认是public 
  1. item 默认是用 public 修饰的,第一次渲染数据是准确的,当父组件的值发生改变时,子组件是不会改变的
  2. 如果想让子组件跟着发生改变,看下面的组件共享
  • 子传父(目的是修改父组件的值)
// 父组件的子组件上,传递一个方法(HmCommentItem)
 HmCommentItem({ item:item,changeLike:(item)=>{
                this.changeLike(item)
              }})
// 子组件上进行接收
changeLike: (params: ReplyItem) => void = () => {} // 接受一个无返回值的方法,默认是空函数

六、组件状态 (state 组件内)

  • 状态共享 (父子单向)
// 子组件上进行接收
 @Prop item: Partial<ReplyItem> 
  1. Prop只能修饰string number boolean类型的数据- (Next全部都支持了各种类型),Prop 只会在当前子组件生效,不会传到父组件
  2. 在子组件里是不能直接修改父组件的值

  • 状态共享 (父子双向)
  1. 在上面我们用了 Prop 可以在父组件里修改数据,子组件也会同步数据,
  2. 但是prop 的作用在当前的组件,
  3. @Link , 可以实现父子同步
// 子组件上进行接收
 @Link item: Partial<ReplyItem> 

ps : Link修饰的数据必须得是最外层的 State数据,也就是说不能是数组对象的某一项



  • 状态共享 (后代组件)
  1. 如果我们的组件层级特别多,ArkTS支持跨组件传递状态数据来实现双向同步@Provide和 @Consume
  2. 这特别像Vue中的依赖注入
假设我们有三层组件,Index-Child-Grand,
Index的数据不想经过Child而直接给到Grand可以使用该修饰器



@Entry
@Component
struct ProvideCase02 {
 @Provide count: number = 0
  build() {
    Row() {
      Column({ space: 15 }) {
        Text(this.count.toString())
          .fontSize(50)
        Button("顶级组件+1")
          .onClick(() => {
            this.count++
          })
        Divider()
        Child()
      }
      .width('100%')
    }
    .height('100%')
  }
}

@Component
struct Child {
  build() {
    Column() {
      Text("子组件")
        .fontSize(40)
      Divider()
      Grand()
    }
  }
}

@Component
struct Grand {
  @Consume count: number
  build() {
    Column() {
      Text("孙组件")
        .fontSize(30)
      Text(this.count.toString())
    }
  }  
}

注意:这是双向的修改数据



  • 状态共享 (状态监听器)
  1. 如果开发者需要关注某个状态变量的值是否改变,可以使用 @Watch 为状态变量设置回调函数。
  2. Watch(“回调函数名”)中的回调必须在组件中声明,该函数接收一个参数,参数为修改的属性名
  3. Watch修饰符要写在 State Prop Link Provide的修饰符下面,否则会有问题
    ● 在第一次初始化的时候,@Watch装饰的方法不会被调用
 @Provide('aa')
 @Watch('updateCount')
 count: number = 0
  updateCount(keyName: string) {
    promptAction.showToast({
      message: this.count.toString()
    })
    console.log(keyName,this.count.toString())
}



  • 状态共享 (@Observed 、@ObjectLink)

之前讲解Link的时候,我们遇到了一个问题,就是循环生成的item没办法用item传递给子组件的Link,也就是封装的组件没办法做双向更新同步,那么ArtTS支持 Observed和@ObjectLink来实现这个需求

使用步骤:
● 类 class 数据需要定义通过构造函数,使用 @Observed 修饰这个类
● 初始化数据:需要通过初始化构造函数的方式添加
● 通过 @ObjectLink 关联对象,可以直接修改被关联对象来更新UI

@Entry
@Component
struct ObjectLinkCase {
  @State message: string = 'Hello World'
  // 定义数据时,使用new创建对象,之前是字面量创建
  // 原因:
  @State
  list: FoodObjectClass[] = [new FoodObjectClass({
    order_id: 1,
    food_name: '鱼香肉丝',
    food_price: 18.8,
    food_count: 1
  }) ,new FoodObjectClass({
    order_id: 2,
    food_name: '粗溜丸子',
    food_price: 26,
    food_count: 2
  }) , new FoodObjectClass({
    order_id: 3,
    food_name: '杂粮煎饼',
    food_price: 12,
    food_count: 1
  }) ]
  build() {
    Row() {
      Column({ space: 20 }) {
         ForEach(this.list, (item: FoodObjectClass) => {
           FoodItem({ item: item })
         })

        BottomCart({ myList: $list  })
      }
      .width('100%')
    }
    .height('100%')
  }
}
@Extend(Text)
function TextStyle () {
  .layoutWeight(1).textAlign(TextAlign.Center).fontSize(20)
}

@Extend(Text)
function AddCutStyle () {
  .width(40)
  .height(40)
  .borderRadius(20)
  .backgroundColor(Color.Grey)
  .textAlign(TextAlign.Center)
  .fontSize(20)
}

@Component
struct FoodItem {
  // 步骤二 : Observed必须和ObjectLink才有UI更新的效果
  @ObjectLink
  item: FoodObjectClass
  build() {
    Row() {
      Text(this.item.food_name).TextStyle()
      Text(this.item.food_price.toFixed(2)).TextStyle()
      Row() {
          Text("-").AddCutStyle()
            .onClick(() => {
              this.item.food_count--
            })
            .visibility(this.item.food_count > 0 ? Visibility.Visible : Visibility.Hidden)
          Text(this.item.food_count.toString()).TextStyle()
            .visibility(this.item.food_count > 0 ? Visibility.Visible : Visibility.Hidden)

        Text("+").AddCutStyle()
          .onClick(() => {
             this.item.food_count++
          })
      }.layoutWeight(1)
    }
    .width('100%')
    .height(40)
  }
}


// 底部组件
@Component
struct BottomCart {
  @Link
  myList: FoodObjectClass[]
  build() {
     Button("更改菜品的数量")
       .onClick(() => {
         this.myList = this.myList.map(item => {
           item.food_count++
           return item
         })
       })
  }
}
// 初始化数据 : 定义了一个接口
interface IFoodInfo {
  order_id: number
  food_name: string
  food_price: number
  food_count: number
}

// 步骤一 : 食品类 
// implements : 使用接口
@Observed
class FoodObjectClass implements  IFoodInfo  {
  order_id: number = 0
  food_name:  string = ""
  food_price: number = 0
  food_count: number = 0
  constructor(obj: IFoodInfo) {
    this.order_id = obj.order_id
    this.food_name = obj.food_name
    this.food_price = obj.food_price
    this.food_count = obj.food_count
  }
}

注意:

  1. interface声明类型不需要给初始值,class声明类型必须给初始值(next 版本要求)

  2. 使用了Observed这个装饰器来修饰class,那么只要我们改动class的属性,它就会驱动UI的更新(只是第一层,多层怎么办,往下看)

  3. 只有Observed修饰的class才可以被 ObjectLink使用,并且Entry修饰的组件不允许使用ObjectLink

  4. ObjectLink只能修饰被Observed修饰的class类型

  5. Observed修饰的class的数据如果是复杂数据类型,需要采用赋值的方式才可以具备响应式特性-因为它监听的是该属性的set和get


总结: ( State组件内状态)

  1. Prop 子组件修饰符 -4.0 boolean/number/string- 单向数据流
  2. Link 子组件修饰符-双向数据流,所有类型都支持- 必须通过$前缀-(循环数据就没有办法传入)
  3. Provide和Consume 双向数据流-所有结构均支持
  4. Watch 可以监听State Link Prop ObjectLink的数据变化
  5. Observed和ObjectLink

七、更新深层的数据

我们都知道 ArkTS 所有内容都不支持深层数据更新 UI渲染 , next 版本取消了 解构赋值我们怎么办呢?

  1. 对于对象类型的数据,我们多定义了一个接口 ( 看 flag1)
  2. 修改值时,我们传入 class 里 (看flag2)
@Entry
@Component
struct MulitiStateCase {

   @State
   user: IUserProfileModel  = new IUserProfileModel({
     username: '老高',
     age: 34,
     sex: "男",
     address: new IAddressModel({
       province: '河北',
       city: '衡水',
       area: '深州'
     })
   })

  build() {
    Row() {
      Column() {
        // UI更新只能监听到一层
        Row() {
          Text(this.user.username).fontSize(40)
          Text(this.user.age.toString()).fontSize(40)
          Text(this.user.address.province).fontSize(40)
          Text(this.user.address.city).fontSize(40)
          Text(this.user.address.area).fontSize(40)
        }
        .width('100%')
        .height(50)
        Button("更新名字和年龄")
          .onClick(() => {
            // this.user.username = "老高坏坏的"
            // this.user.age = 25
            this.user.address.city = "廊坊"
            this.user.address = new IAddressModel(this.user.address)
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}
// flag1
interface IAddress {
  province: string
  city: string
  area: string
}

interface  IUserProfile {
  username: string
  age: number
  sex: '男' | '女'
  address: IAddress
}
export class IAddressModel implements IAddress {
  province: string = ''
  city: string = ''
  area: string = ''

  constructor(model: IAddress) {
    this.province = model.province
    this.city = model.city
    this.area = model.area
  }
}
export class IUserProfileModel implements IUserProfile {
  username: string = ''
  age: number = 0
  sex: '男' | '女' = '男'
  address: IAddress = new IAddressModel({} as IAddress)

  constructor(model: IUserProfile) {
    this.username = model.username
    this.age = model.age
    this.sex = model.sex
    this.address = model.address
  }
}

八 、应用状态

ArtTS提供了好几种状态用来帮助我们管理我们的全局数据

  • State组件内状态
  • LocalStorage - UIAbility状态(ps1)
  • AppStorage - 应用内状态-多UIAbility共享-(内存-非持久化-退出应用同样消失)
  • PersistenStroage-全局持久化状态(写入磁盘-持久化状态-退出应用 数据同样存在)
  1. ps1:前端localStorage 是写入磁盘的,所以是持久化;鸿蒙的LocalStorage 是写入内存的,当应用关闭了,数据清除了

LocalStorage:

  • localStorage 是页面级的UI状态存储,一个应用可能有若干个UIAbility
  • 通过 @Entry 装饰器接收的参数可以在页面内共享同一个 LocalStorage 实例。 LocalStorage 也可以在 UIAbility 内,页面间共享状态。
    用法

创建 LocalStorage 实例:const storage = new LocalStorage({ key: value })

  • 单向 @LocalStorageProp(‘user’) 组件内可变
  • 双向 @LocalStorageLink(‘user’) 全局均可变
import router from '@ohos.router'

// 步骤一: 定义数据
export class UserInfoClass {
  name: string = ""
  age: number = 0
}
let user: Record<string, UserInfoClass> = { "user": {
  name: '老高',
  age: 34
}};
// 步骤二:存数据
let storage: LocalStorage = new LocalStorage(user);
// ps: entry 这里需要先接受
@Entry(storage)
@Component
struct LocalStorageCase {
  @State message: string = 'Hello World'
  // 步骤三: 取数据 (  @LocalStorageProp 数据流是单向的,说明不能修改)
  @LocalStorageProp("user")
  myUser: UserInfoClass = {}
  build() {
    Row() {
      Column({ space: 15 }) {
      // 步骤四 : 当做变量就可以直接使用了
        Text("姓名:" + this.myUser.name)
        Text("年龄:" + this.myUser.age)
        Button("跳转到另一个页面")
          .onClick(() => {
            router.pushUrl({
              url: 'pages/LocalStorageCase2'
            })
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

AppStorage :

  • AppStorage 是应用全局的UI状态存储,是和应用的进程绑定的
  • 由UI框架在应用程序启动时创建,为应用程序UI状态属性提供中央存储。注意它也是内存数据,不会写入磁盘

用法:

  1. 使用UI修饰符
    ● 如果是初始化使用 AppStorage.SetOrCreate(key,value)
    ● 单向 @StorageProp(‘user’) 组件内可变
    ● 双向 @StorageLink(‘user’) 全局均可变
  2. 使用API方法
    ● AppStorage.Get(key) 获取数据
    ● AppStorage.Set(key,value) 覆盖数据
    ● const link: SubscribedAbstractProperty = AppStorage.Link(key) 覆盖数据
    ○ link.set(value) 修改
    ○ link.get() 获取
import router from '@ohos.router'
import promptAction from '@ohos.promptAction'
@Entry
@Component
struct AppStorageCase02 {
  @StorageProp("user_token")
  token: string = ''

  @StorageLink("user_token")
  linkToken: string = ''
  onPageShow() {
    promptAction.showToast({
      message:  AppStorage.Get<string>("user_token") || '无token'
    })
  }
  build() {
    Row() {
      Column() {
        Text(this.token)
       Button("登录")
         .onClick(() => {
           AppStorage.SetOrCreate<string>("user_token", "123456")
           router.pushUrl({
             url: 'pages/AppStorageCaseTrans'
           })
         })
        Button("修改token")
          .onClick(() => {
            // this.linkToken = '678910'
            const link =   AppStorage.Link("user_token") as SubscribedAbstractProperty<string>
            link.set("abcde")
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

PersistentStorage:

前面讲的所有状态均为内存状态,也就是应用退出便消失,所以如果我们想持久化的保留一些数据,应该使用 PersistentStorage

注意:

UI和业务逻辑不直接访问 PersistentStorage 中的属性,所有属性访问都是对 AppStorage 的访问,AppStorage 中的更改会自动同步到 PersistentStorage。也就是,我们和之前访问AppStorage是一样的,只不过需要提前使用PersistentStorage来声明

  • PersistentStorage 将选定的 AppStorage 属性保留在设备磁盘上。
  • 支持:number, string, boolean, enum 等简单类型;
  • 如果:要支持对象类型,可以转换成json字符串
  • 持久化变量最好是小于2kb的数据,如果开发者需要存储大量的数据,建议使用数据库api。
PersistentStorage.PersistProp("user_token", '') // 初始化磁盘

只要初始化了数据,我们以后使用AppStorage就可以读取和设置,它会自动同步到我们的磁盘上
目前不支持复杂对象的持久化,如果你需要存储,你需要把它序列化成功字符串
● 测试:需要在真机或模拟器调试
大家可以在上一个例子之前添加 PersistentStorage.PersistProp(‘属性名’, 值)
然后直接使用AppStorage进行Set就可以了,设置完成之后,使用模拟器先把任务销毁,然后再查看数据是否显示

  • 20
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
华为鸿蒙HarmonyOS开发整理资料汇总,共38份。 1学前必读:HarmonyOS学习资源主题分享 2学前必读:OpenHarmony-联盟生态资料合集 3-1.HarmonyOS概述:技术特性 3-2.HarmonyOS概述:开发工具与平台 3-3.HarmonyOS概述:系统安全 3-4.HarmonyOS概述:系统定义 3-5.HarmonyOS概述:下载与安装软件 3-6.HarmonyOS概述:应用开发基础知识 3-7.HarmonyOS概述:最全HarmonyOS文档和社区资源使用技巧 4-1.生态案例:【开发者说】重塑经典,如何在HarmonyOS手机上还原贪吃蛇游戏 4-2.生态案例:HarmonyOLabo涂鸦鸿蒙亲子版 4-3.生态案例:HarmonyOS分镜头APP案例 4-4.生态案例:HarmonyOS时光序历史学习案例 4-5.生态案例:HarmonyOS先行者说 宝宝巴士携手HarmonyOS共同打造儿童教育交互新体验 4-6.生态案例:HarmonyOS智能农场物联网连接实践 4-7.生态案例:分布式开发样例,带你玩转多设备 4-8.生态案例:华为分布式日历应用开发实践 5-1.【Codelab】HarmonyOS基于图像模块实现图库图片的四种常见操作 5-2.【CodeLab】手把手教你创建第一个手机“Hello World” 5-3.【Codelab】如此简单!一文带你学会15个HarmonyOS JS组件 5-4.【Codelab】懒人“看”书新法—鸿蒙语音播报,到底如何实现? 5-5.【Codelab】基于AI通用文字识别的图像搜索,这波操作亮了 5-6.【Codelab】开发样例概览 6-1.技术解读之HarmonyOS轻量JS开发框架与W3C标准差异分析 6-2.技术解读之HarmonyOS驱动加载过程分析 6-3.技术解读之HarmonyOS组件库使用实践 6-4.技术解读之华为架构师解读:HarmonyOS低时延高可靠消息传输原理 6-5.技术解读之解密HarmonyOS UI框架 6-6.技术解读之如何从OS框架层面实现应用服务功能解耦 7-1.常见问题之HarmonyOS元服务的设计与开发解析 7-2.常见问题之Java开发 7-3.常见问题之JS开发 7-4.常见问题之模拟器登录 7-5.常见问题之模拟器运行 7-6.常见问题之如何使用JsJava开发HarmonyOS UI 7-7.常见问题之应用配置 7-8.常见问题之预览器运行 8【视频合集】入门到进阶视频学习资料合集30+

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值