当前位置: 首页 > article >正文

133.鸿蒙基础01

鸿蒙基础

    • 1.自定义构建函数
      • 1. 构建函数-[@Builder ](/Builder )
      • 2. 构建函数-传参传递(单向)
      • 3. 构建函数-传递参数(双向)
      • 4. 构建函数-传递参数练习
      • 5. 构建函数-[@BuilderParam ](/BuilderParam ) 传递UI
    • 2.组件状态共享
      • 1. 状态共享-父子单向
      • 2. 状态共享-父子双向
      • 3. 状态共享-后代组件
      • 4. 状态共享-状态监听器
      • 5. 综合案例 - 相册图片选取
        • 1-页面布局,准备一个选择图片的按钮并展示
        • 2-准备弹层,点击时展示弹层
        • 3-添加点击事件,设置选中状态
        • 4-点击确定同步给页面
        • 5.关闭弹层
      • 6. @Observed与[@ObjectLink ](/ObjectLink )
      • 7. Next新增修饰符-Require-Track
    • 3.应用状态
      • 1. UIAbility内状态-LocalStorage
      • 2. 应用状态-AppStorage
    • 概述
      • 3. 状态持久化-PersistentStorage
    • 限制条件
      • 4. 状态持久化-preferences首选项
      • 5. 设备状态-Environment(了解)
    • 4.网络管理(需要模拟器)
      • 1. 应用权限
      • 2. HTTP请求(需要模拟器)
    • request接口开发步骤
    • 5.今日案例-美团外卖
      • 1. 目录结构-入口页面
      • 2. 页面结构-底部组件
      • 3. 顶部结构-MTTop(复制粘贴)
      • 4. 页面结构-商品菜单和商品列表
      • 5. 页面结构-购物车
      • 6. 业务逻辑-渲染商品菜单和列表
      • 7. 业务逻辑-封装新增加菜和减菜组件
      • 8. 业务逻辑-加入购物车
      • 9.加菜和减菜按钮加入购物车
      • 10.清空购物车
      • 11.底部内容汇总
  • 美团案例完整代码

1.自定义构建函数

1. 构建函数-@Builder

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

只要使用Builder修饰符修饰的内容,都可以做成对应的UI描述

image.png

@Entry
@Component
struct BuilderCase {
  @State
  list: string[] = ["A", "B","C", "D", "E", "F"]

  @Builder
  getItemBuilder (itemName: string) {
    Row() {
      Text(`${itemName}. 选项`)
    }
    .height(60)
    .backgroundColor("#ffe0dede")
    .borderRadius(8)
    .width("100%")
    .padding({
      left: 20,
      right: 20
    })
  }

  build() {
    Column({ space: 10 }) {
      ForEach(this.list, (item: string) => {
        this.getItemBuilder(item)
      })
    }
    .padding(20)
  }
}
  • 用法- 使用@Builder修饰符修饰

image.png


@Entry
@Component
struct BuilderCase02 {
  build() {
    Row() {
      Column() {
        Row() {
          Row() {
            Text("异常时间")
            Text("2023-12-12")
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .padding({
            left: 15,
            right: 15
          })
          .borderRadius(8)
          .height(40)
          .backgroundColor(Color.White)
        }.padding({
          left: 10,
          right: 10
        })

      }
      .width('100%')
    }
    .height('100%')
    .backgroundColor('#ccc')
  }
}

:::info
假设你有N个这样的单个元素,但是重复的去写会浪费大量的代码,丧失代码的可读性,此时我们就可以使用
builder构建函数
:::

  1. 全局定义- @Builder function name () {}
@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
  })

}
  • 在组件中使用
  Column({ space: 10 }) {
        getCellContent("异常时间", "2023-12-12")
        getCellContent("异常位置", "回龙观")
        getCellContent("异常类型", "漏油")
      }
      .width('100%')

Next里面最大的变化就是全局的自定义Builder函数可以被引用,也就是你的一些公共的builder函数可以抽提出来,像使用函数那样来复用一些样式

image.png

2. 构建函数-传参传递(单向)

:::success
传的参数是按值的话,那个builder不具备响应式特征
传的参数是复杂数据, 而且复杂数据类型中的参数有响应式修饰符修饰,那么具备响应式特征
:::
image.png

@Entry
@Component
struct BuilderTransCase {
  @State
  area: string = "望京"
  @Builder
  getCardItem (leftTitle: string, rightValue: string) {
    Row() {
      Text(leftTitle)
      Text(rightValue)
    }
    .justifyContent(FlexAlign.SpaceBetween)
    .width('100%')
    .height(50)
    .borderRadius(8)
    .backgroundColor(Color.White)
    .padding({
      left: 20,
      right: 20
    })
  }
  @Builder
  getCardItemObj (item: ICardItem) {
    Row() {
      Text(item.leftTitle)
      Text(item.rightValue)
    }
    .justifyContent(FlexAlign.SpaceBetween)
    .width('100%')
    .height(50)
    .borderRadius(8)
    .backgroundColor(Color.White)
    .padding({
      left: 20,
      right: 20
    })
  }

  build() {
   Column({ space: 20 }) {
     Text(this.area)
     this.getCardItem("异常位置", this.area)  // 按值传递不具备响应式
     this.getCardItemObj({  leftTitle: '异常位置', rightValue: this.area }) // 按照引用传递可以实现数据更新
     this.getCardItem("异常时间", "2023-12-12")
     this.getCardItem("异常类型", "漏油")
     Button("上报位置")
       .onClick(() => {
         this.area = "厦门"
       })
   }
   .justifyContent(FlexAlign.Center)
    .width('100%')
    .height('100%')
    .padding(20)
    .backgroundColor(Color.Gray)
  }
}
interface ICardItem {
  leftTitle: string
  rightValue: string
}

:::info
自定义构建函数的参数传递有按值传递和按引用传递两种,均需遵守以下规则:

  • 参数的类型必须与参数声明的类型一致,不允许undefined、null和返回undefined、null的表达式。
  • 在自定义构建函数内部,不允许改变参数值。如果需要改变参数值,且同步回调用点,建议使用@Link。
  • @Builder内UI语法遵循UI语法规则。
    :::

我们发现上一个案例,使用了string这种基础数据类型,即使它属于用State修饰的变量,也不会引起UI的变化

  • 按引用传递参数时,传递的参数可为状态变量,且状态变量的改变会引起@Builder方法内的UI刷新。ArkUI提供**$$**作为按引用传递参数的范式。
ABuilder( $$ : 类型 );

:::info

  • 也就是我们需要在builder中传入一个对象, 该对象使用$$(可使用其他字符)的符号来修饰,此时数据具备响应式了
    :::
class CellParams {
  leftTitle: string = ""
  rightValue: string = ""
}
@Builder
function 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
  })

}
  • 传值
this.getCellContent({ leftTitle: '异常位置', rightValue: this.formData.location })
this.getCellContent({ leftTitle: '异常时间', rightValue: this.formData.time })
this.getCellContent({ leftTitle: '异常类型', rightValue: this.formData.type })

image.png

:::info
同样的,全局Builder同样支持这种用法
:::

@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 = ""
}
@Builder
function 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
  })

}

:::info

  • 使用 @Builder 复用逻辑的时候,支持传参可以更灵活的渲染UI
  • 参数可以使用状态数据,不过建议通过对象的方式传入 @Builder
    :::

3. 构建函数-传递参数(双向)

image.png
:::info
之前我们做过这样一个表单,$$不能绑定整个对象,有没有什么解决办法呢?
:::
新建一个的builder -FormBuilder

@Entry
@Component
struct BuilderCase03 {
  @State
  formData: FormData = {
    name: '张三',
    age: '18',
    bank: '中国银行',
    money: '999'
  }
  @Builder
  FormBuilder(formData:FormData) {
    Column({ space: 20 }) {
      TextInput({ placeholder: '请输入姓名',text:formData.name})
      TextInput({ placeholder: '请输入年龄',text:formData.age})
      TextInput({ placeholder: '请输入银行',text:formData.bank })
      TextInput({ placeholder: '请输入银行卡余额',text:formData.money})
    }.width('100%')
  }
  build() {
    Row() {
      Column({space:20}) {
        this.FormBuilder(this.formData)
        Row({space:20}){
          Button('重置')
            .onClick(()=>{
              this.formData = {
                name: '',
                age: '',
                bank: '',
                money: ''
              }
            })
          Button('注册')
        }
      }
      .width('100%')
      .padding(20)
    }
    .height('100%')
  }
}

interface FormData {
  name: string
  age: string
  bank: string
  money: string
}

:::danger
在页面上尝试使用builder,传入需要展示的数据,点击重置时,会发现UI并不能更新!
因为传递参数必须是{ params1:数据 }格式,params1才是响应式的
:::
改造传值,发现此时响应式了

@Entry
@Component
struct BuilderCase03 {
  @State
  formData: FormData = {
    name: '张三',
    age: '18',
    bank: '中国银行',
    money: '999'
  }
  @Builder
  FormBuilder(formData:FormDataInfo) {
    Column({ space: 20 }) {
      TextInput({ placeholder: '请输入姓名',text:formData.data.name})
      TextInput({ placeholder: '请输入年龄',text:formData.data.age})
      TextInput({ placeholder: '请输入银行',text:formData.data.bank })
      TextInput({ placeholder: '请输入银行卡余额',text:formData.data.money})
    }.width('100%')
  }
  build() {
    Row() {
      Column({space:20}) {
        this.FormBuilder({data:this.formData})
        Row({space:20}){
          Button('重置')
            .onClick(()=>{
              this.formData = {
                name: '',
                age: '',
                bank: '',
                money: ''
              }
            })
          Button('注册')
        }
      }
      .width('100%')
      .padding(20)
    }
    .height('100%')
  }
}

interface FormData {
  name: string
  age: string
  bank: string
  money: string
}
interface FormDataInfo{
  data:FormData
}

改造成双向绑定,builder内部改变时也能通知外层
image.png

@Entry
@Component
struct BuilderCase03 {
  @State
  formData: FormData = {
    name: '张三',
    age: '18',
    bank: '中国银行',
    money: '999'
  }
  @Builder
  FormBuilder($$:FormDataInfo) {
    Column({ space: 20 }) {
      TextInput({ placeholder: '请输入姓名',text:$$.data.name})
      TextInput({ placeholder: '请输入年龄',text:$$.data.age})
      TextInput({ placeholder: '请输入银行',text:$$.data.bank })
      TextInput({ placeholder: '请输入银行卡余额',text:$$.data.money})
    }.width('100%')
  }
  build() {
    Row() {
      Column({space:20}) {
        Text(JSON.stringify(this.formData))
        this.FormBuilder({data:this.formData})
        Row({space:20}){
          Button('重置')
            .onClick(()=>{
              this.formData = {
                name: '',
                age: '',
                bank: '',
                money: ''
              }
            })
          Button('注册')
        }
      }
      .width('100%')
      .padding(20)
    }
    .height('100%')
  }
}

interface FormData {
  name: string
  age: string
  bank: string
  money: string
}
interface FormDataInfo{
  data:FormData
}

image.png

4. 构建函数-传递参数练习

image.png

上图中,是tabs组件中的tabbar属性,支持自定义builder,意味着我们可以定制它的样式

  • 准备八个图标放到资源目录下

图片.zip
image.png

  • 新建一个页面, 声明一个interface并建立四个数据的状态
interface TabInterface {
  name: string
  icon: ResourceStr
  selectIcon: ResourceStr
  title: string
}
  • 循环生成对应的TabContent
@Entry
@Component
struct TabBarBuilderCase {
  @State
  list: TabInterface[] = [{
    icon: $r("app.media.ic_public_message"),
    selectIcon: $r('app.media.ic_public_message_filled'),
    name: 'wechat',
    title: '微信',
  }, {
    icon: $r('app.media.ic_public_contacts_group'),
    selectIcon: $r('app.media.ic_public_contacts_group_filled'),
    name: 'connect',
    title: '联系人',
  }, {
    icon: $r('app.media.ic_gallery_discover'),
    selectIcon: $r('app.media.ic_gallery_discover_filled'),
    name: 'discover',
    title: '发现',
  }, {
    icon: $r('app.media.ic_public_contacts'),
    selectIcon: $r('app.media.ic_public_contacts_filled'),
    name: 'my',
    title: '我的',
  }]


  build() {
    Tabs() {
     ForEach(this.list, (item: TabInterface) => {
       TabContent() {
         Text(item.title)
       }
       .tabBar(item.title)
     })
    }
    .barPosition(BarPosition.End)
  }
}
interface TabInterface {
  name: string
  icon: ResourceStr
  selectIcon: ResourceStr
  title: string
}

image.png

此时,如果我们想实现图中对应的效果,就需要使用自定义Builder来做,因为TabContent的tabBar属性支持CustomBuilder类型,CustomBuilder类型就是builder修饰的函数

  • 在当前组件中声明一个builder函数
 @Builder
  CommonTabBar (item: TabInterface) {
    Column () {
      Image(item.icon)
        .width(20)
        .height(20)
      Text(item.title)
        .fontSize(12)
        .fontColor("#1AAD19")
        .margin({
          top: 5
        })
    }
  }

image.png
image.png

  • 定义一个数据来绑定当前tabs的激活索引
  @State
  currentIndex: number = 0

image.png

  • 根据当前激活索引设置不同的颜色的图标
 @Builder
  CommonTabBar (item: TabInterface) {
    Column () {
      Image(item.name === this.list[this.currentIndex].name ? item.selectIcon : item.icon)
        .width(20)
        .height(20)
      Text(item.title)
        .fontSize(12)
        .fontColor(item.name === this.list[this.currentIndex].name ? "#1AAD19": "#2A2929")
        .margin({
          top: 5
        })
    }
  }

image.png

5. 构建函数-@BuilderParam 传递UI

:::success
插槽-Vue-Slot React-RenderProps

  • 把UI结构体的函数(Builder修饰的函数)当成参数传入到组件中,让组件放入固定的位置去渲染

  • 子组件接收传入的函数的修饰符/装饰器叫做BuilderParam
    :::
    :::info

  • Component可以抽提组件

  • Builder可以实现轻量级的UI复用

完善了吗? 其实还不算,比如下面这个例子
:::

  • BuilderParam的基本使用 - 如何实现定制化Header?

image.png
image.png
:::success
使用BuilderParam的步骤

  • 前提:需要出现父子组件的关系
  • 前提:BuilderParam应出现在子组件中
    1. 子组件声明 @BuilderParam getConent: () => void
    1. BuilderParam的参数可以不给初始值,如果给了初始值, 就是没有内容的默认内容
    1. 父组件传入的时候,它需要用builder修饰的函数又或者是 一个箭头函数中包裹着
    1. 调用builder函数的逻辑
      :::
@Entry
@Component
struct BuildParamCase {
  // 声明的一个要传递的内容!
  @Builder
  LeftBuilder() {
    Image($r('sys.media.ohos_ic_compnent_titlebar_back'))
      .width(20)
  }
  @Builder
  CenterBuilder(){
   Row(){
     Text('最新推荐')

     Text('🔥')
   }
   .layoutWeight(1)
    .justifyContent(FlexAlign.Center)
  }
  @Builder
  RightBuilder(){
    Image($r('sys.media.ohos_ic_public_scan'))
      .width(20)
  }

  build() {
    Row() {
      Column() {
        //   Header容器
        MyBuilderParamChild()
      }
      .width('100%')
    }
    .height('100%')
  }
}

@Component
struct MyBuilderParamChild {
  @Builder
  defaultLeftParam(){
    Text('返回')
  }
  @BuilderParam
  leftContent:()=>void = this.defaultLeftParam


  @Builder
  defaultCenterParam(){
    Text('首页')
      .layoutWeight(1)
      .textAlign(TextAlign.Center)
  }
  @BuilderParam
  centerContent:()=>void =  this.defaultCenterParam

  @Builder
  defaultRightParam(){
    Text('确定')
  }
  @BuilderParam
  rightContent:()=>void =  this.defaultRightParam
  build() {
    Row() {
      //   左
      this.leftContent()
      //   中
      this.centerContent()
      //   右
      this.rightContent()
    }.width('100%')
    .backgroundColor(Color.Pink)
    .padding(20)
  }
}
  • builderParam传值
    :::success

  • 当我们使用builderParam的时候,又需要拿到渲染的数据该怎么办?

场景: 当我们有一个列表组件,该组件的列表格式是固定的,但是每个选项的内容由传入的结构决定怎么搞?

  • 列表组件可以渲染数据-但是每一个选项的UI结构由使用者决定

image.png

  • 拷贝图片到assets

图片.zip
:::

  • 封装一个列表的组件,可以渲染传入的数组
@Preview
@Component
// 列表组件
struct HmList {
  @State
  list: object[] = [] // 不知道传入的是什么类型 统一认为是object
  @BuilderParam
  renderItem: (obj: object) => void

  build() {
    // Grid List WaterFlow
    // 渲染数组
    List ({ space: 10 }) {
      ForEach(this.list, (item: object) => {
        ListItem() {
          // 自定义的结构
          if(this.renderItem) {
            this.renderItem(item)
            // 函数中的this始终指向调用者
          }
        }
      })
    }
    .padding(20)
  }
}
export { HmList }

// WaterFlow FlowItem  Grid GirdItem  List ListItem
  • 父组件调用
import { BuilderParamChild } from './components/BuilderParamChild'
@Entry
@Component
struct BuilderParamCase {
  @State
  list: GoodItem[] = [
    {
      "id": 1,
      "goods_name": "班俏BANQIAO超火ins潮卫衣女士2020秋季新款韩版宽松慵懒风薄款外套带帽上衣",
      "goods_img": "assets/1.webp",
      "goods_price": 108,
      "goods_count": 1,
    },
    {
      "id": 2,
      "goods_name": "嘉叶希连帽卫衣女春秋薄款2020新款宽松bf韩版字母印花中长款外套ins潮",
      "goods_img": "assets/2.webp",
      "goods_price": 129,
      "goods_count": 1,
    },
    {
      "id": 3,
      "goods_name": "思蜜怡2020休闲运动套装女春秋季新款时尚大码宽松长袖卫衣两件套",
      "goods_img": "assets/3.webp",
      "goods_price": 198,
      "goods_count": 1,
    },
    {
      "id": 4,
      "goods_name": "思蜜怡卫衣女加绒加厚2020秋冬装新款韩版宽松上衣连帽中长款外套",
      "goods_img": "assets/4.webp",
      "goods_price": 99,
      "goods_count": 1,
    },
    {
      "id": 5,
      "goods_name": "幂凝早秋季卫衣女春秋装韩版宽松中长款假两件上衣薄款ins盐系外套潮",
      "goods_img": "assets/5.webp",
      "goods_price": 156,
      "goods_count": 1,
    },
    {
      "id": 6,
      "goods_name": "ME&CITY女装冬季新款针织抽绳休闲连帽卫衣女",
      "goods_img": "assets/6.webp",
      "goods_price": 142.8,
      "goods_count": 1,
    },
    {
      "id": 7,
      "goods_name": "幂凝假两件女士卫衣秋冬女装2020年新款韩版宽松春秋季薄款ins潮外套",
      "goods_img": "assets/7.webp",
      "goods_price": 219,
      "goods_count": 2,
    },
    {
      "id": 8,
      "goods_name": "依魅人2020休闲运动衣套装女秋季新款秋季韩版宽松卫衣 时尚两件套",
      "goods_img": "assets/8.webp",
      "goods_price": 178,
      "goods_count": 1,
    },
    {
      "id": 9,
      "goods_name": "芷臻(zhizhen)加厚卫衣2020春秋季女长袖韩版宽松短款加绒春秋装连帽开衫外套冬",
      "goods_img": "assets/9.webp",
      "goods_price": 128,
      "goods_count": 1,
    },
    {
      "id": 10,
      "goods_name": "Semir森马卫衣女冬装2019新款可爱甜美大撞色小清新连帽薄绒女士套头衫",
      "goods_img": "assets/10.webp",
      "goods_price": 153,
      "goods_count": 1,
    }
  ]
  @Builder
  renderItem (item: GoodItem) {
    Row({ space: 10 }) {
      Image(item.goods_img)
        .borderRadius(8)
        .width(120)
        .height(200)
      Column() {
        Text(item.goods_name)
          .fontWeight(FontWeight.Bold)
        Text("¥ "+item.goods_price.toString())
          .fontColor(Color.Red)
          .fontWeight(FontWeight.Bold)
      }
      .padding({
        top: 5,
        bottom: 5
      })
      .alignItems(HorizontalAlign.Start)
      .justifyContent(FlexAlign.SpaceBetween)
      .height(200)
      .layoutWeight(1)
    }
    .width('100%')
  }
  build() {
    Row() {
      Column() {
        BuilderParamChild({
          list:this.list,
          builderItem:(item:object)=>{
            this.renderItem(item as GoodItem)
          }
        })
      }
      .width('100%')
    }
    .height('100%')
  }
}
interface GoodItem {
  goods_name: string
  goods_price: number
  goods_img: string
  goods_count: number
  id: number
}

:::success
1.BuildParam可以没有默认值,但是调用的时候最好判断一下
2.BuildParam可以声明参数,调用的时候传递的参数最后回传给父组件传递的Builder
:::

  • 尾随闭包
    :::success
    Column () { } 中大括号就是尾随闭包的写法
    :::
    :::info
    当我们的组件只有一个BuilderParam的时候,此时可以使用尾随闭包的语法 也就是像我们原来使用Column或者Row组件时一样,直接在大括号中传入
    :::

  • 父组件使用尾随闭包传入

神领物流中有很多这样的Panel栏
image.png
image.png
image.png
我们用尾随闭包来封装这样的组件,理解一下BuildParam的使用
在这里插入图片描述

首先封装一个Panel组件

@Component
struct PanelComp {
  @State
  leftText:string = '左侧标题'

  @BuilderParam
  rightContent:()=>void = this.defaultContent
  @Builder
  defaultContent(){
    Row({space:16}){
      Checkbox().select(true).shape(CheckBoxShape.CIRCLE)
      Text('是')
    }
  }

  build() {
    Row(){
      Text(this.leftText)
      this.rightContent()
    }
    .width('100%')
    .padding(20)
    .backgroundColor('#ccc')
    .borderRadius(8)
    .justifyContent(FlexAlign.SpaceBetween)
  }
}

export { PanelComp }

在这里插入图片描述

  • 接下来父组件使用,并分别传递左侧文字和右侧的结构
import { PanelComp } from './components/PanelComp'

@Entry
@Component
struct BuilderParamClosure {
  @State
  isOn:boolean = false
  build() {
    Row() {
      Column() {
        Text(''+this.isOn)
        PanelComp({
          // 数据
          leftText:'低电量模式'
        }){
          // 结构
          Toggle({
            type:ToggleType.Switch,
            isOn:$$this.isOn
          })
        }
      }
      .width('100%')
      .padding(20)
    }
    .height('100%')
  }
}


在这里插入图片描述

:::success
只有一个BuilderParam且不需要传参的时候,可以使用尾随闭包
注意:尾随闭包用空大括号就代表传递空内容,会替代默认内容
:::

2.组件状态共享

State是当前组件的状态, 用State修饰的数据变化会驱动UI的更新(只有第一层)
父传子的时候,子组件定义变量的时候,如果没有任何的修饰符,那么该值只会在第一次渲染时生效

:::info
接下来,我们学习组件状态传递
我们知道 State是当前组件的状态,它的数据变化可以驱动UI,但是子组件接收的数据没办法更新,我们需要
更多的修饰符来帮助我们完成数据的响应式传递
:::

1. 状态共享-父子单向

在这里插入图片描述

比如我们希望实现这样一个效果,粉色区域是一个子组件,父组件有一个值
如何让父子同时可以进行修改,且保持同步呢?

  • 先写页面

在这里插入图片描述

@Entry
@Component
struct ComponentQuestionCase {
  @State money: number = 999999;

  build() {
    Column() {
      Text('father:' + this.money)
      Button('存100块')
      CompQsChild()
    }
    .padding(20)
    .width('100%')
    .height('100%')
  }
}

@Component
struct CompQsChild {
  @State money: number = 0

  build() {
    Column() {
      Text('child:' + this.money)
      Button('花100块')
    }
    .padding(20)
    .backgroundColor(Color.Pink)
  }
}
  • 传递值给子组件,绑定点击事件修改money,此时会发现,父子组件各改各的

image.png

@Entry
@Component
struct PropCase {
  @State
  money: number = 999999

  build() {
    Column() {
      Text('father:' + this.money)
      Button('存100块')
        .onClick(() => {
          this.money += 100
        })
      // ---------
      // 父给子传值,默认只生效一次
      PropChild({
        money:this.money
      })
    }
    .width('100%')
  }
}

@Component
struct PropChild {
  // @State
  // 用于和传入的值保持同步(单向),如果传入的值改变也会引起UI的更新
  // 自身可以进行修改,但是不推荐
  // 因为父组件再次改变会覆盖自己的内容
  @Prop
  money: number = 0

  build() {
    Column() {
      Text('father:' + this.money)
      Button('花100块')
        .onClick(() => {
          this.money -= 100
        })
    }
    .padding(20)
    .backgroundColor(Color.Pink)
  }
}

此时,我们就可以学习一个新的修饰符@Prop,被@Prop修饰过的数据可以自动监听传递的值,同步保持更新,修改子组件的money修饰符为@Prop,此时就能实现父组件改变,子组件同步更新
:::success
@Prop装饰的变量可以和父组件建立单向的同步关系。@Prop装饰的变量是可变的,但是变化不会同步回其父组件。
:::
在这里插入图片描述

@Entry
@Component
struct ComponentQuestionCase {
  @State money: number = 999999;

  build() {
    Column() {
      Text('father:' + this.money)
      Button('存100块')
        .onClick(()=>{
          this.money+=100
        })
      CompQsChild({money:this.money})
    }
    .padding(20)
    .width('100%')
    .height('100%')
  }
}

@Component
struct CompQsChild {
  @Prop money: number = 0

  build() {
    Column() {
      Text('child:' + this.money)
      Button('花100块')
        .onClick(()=>{
          this.money-=100
        })
    }
    .padding(20)
    .backgroundColor(Color.Pink)
  }
}

:::info
Prop 支持类型和State修饰符基本一致,并且Prop可以给初始值,也可以不给
注意:子组件仍然可以改自己,更新UI,但不会通知父组件(单向),父组件改变后会覆盖子组件自己的值
在这里插入图片描述

:::

2. 状态共享-父子双向

  • Prop修饰符- 父组件数据更新-让子组件更新- 子组件更新-父组件不为所动
    :::info
    Prop是单向的,而Link修饰符则是双向的数据传递,只要使用Link修饰了传递过来的数据,这个时候就是双向同步了
    注意点:
    Link修饰符不允许给初始值
    :::

  • 将刚刚的案例改造成双向的

在这里插入图片描述

子组件中被@Link装饰的变量与其父组件中对应的数据源建立双向数据绑定。

@Entry
@Component
struct ComponentQuestionCase {
  @State
  money: number = 999999;

  build() {
    Column() {
      Text('father:' + this.money)
      Button('存100块')
        .onClick(()=>{
          this.money+=100
        })
      CompQsChild({money:this.money})
    }
    .padding(20)
    .width('100%')
    .height('100%')
  }
}

@Component
struct CompQsChild {
  // 各玩各的
  // @State money: number = 0
  // 听爸爸的话
  // @Prop money: number
  // 团结一心
  @Link money: number

  build() {
    Column() {
      Text('child:' + this.money)
      Button('花100块')
        .onClick(()=>{
          this.money-=100
        })
    }
    .padding(20)
    .backgroundColor(Color.Pink)
  }
}

:::danger
Link修饰符的要求- 你的父组件传值时传的必须是Link或者State修饰的数据
:::
下面这段代码的问题出现在哪里?

@Entry
@Component
struct ComponentQuestionCase {
  @State
  dataInfo: MoneyInfo = {
    money: 99999,
    bank: '中国银行'
  }

  build() {
    Column() {
      Text('father:' + this.dataInfo.money)
      Button('存100块')
        .onClick(() => {
          this.dataInfo.money += 100
        })
      CompQsChild({ dataInfo: this.dataInfo })
    }
    .padding(20)
    .width('100%')
    .height('100%')
  }
}

@Component
struct CompQsChild {
  // 各玩各的
  // @State money: number = 0
  // 听爸爸的话
  // @Prop money: number
  // 团结一心
  @Link dataInfo: MoneyInfo

  build() {
    Column() {
      Text('child:' + this.dataInfo.money)
      Button('花100块')
        .onClick(() => {
          this.dataInfo.money -= 100
        })
      ChildChild({ money: this.dataInfo.money })
    }
    .padding(20)
    .backgroundColor(Color.Pink)
  }
}

@Component
struct ChildChild {
  // 各玩各的
  // @State money: number = 0
  // 听爸爸的话
  // @Prop money: number
  // 团结一心
  @Link money: number

  // @Link dataInfo: MoneyInfo

  build() {
    Column() {
      Text('ChildChild:' + this.money)
      Button('花100块')
        .onClick(() => {
          this.money -= 100
        })
    }
    .padding(20)
    .backgroundColor(Color.Red)
  }
}

interface MoneyInfo {
  money: number
  bank: string
}

3. 状态共享-后代组件

:::info
如果我们的组件层级特别多,ArkTS支持跨组件传递状态数据来实现双向同步@Provide和 @Consume
这特别像Vue中的依赖注入
:::

  • 改造刚刚的案例,不再层层传递,仍然可以实现效果
@Entry
@Component
struct ComponentQuestionCase1 {
  @Provide
  dataInfo: MoneyInfo1 = {
    money: 99999,
    bank: '中国银行'
  }

  build() {
    Column() {
      Text('father:' + this.dataInfo.money)
      Button('存100块')
        .onClick(() => {
          this.dataInfo.money += 100
        })
      CompQsChild1()
    }
    .padding(20)
    .width('100%')
    .height('100%')
  }
}

@Component
struct CompQsChild1 {
  // 各玩各的
  // @State money: number = 0
  // 听爸爸的话
  // @Prop money: number
  // 团结一心
  @Consume
  dataInfo: MoneyInfo1

  build() {
    Column() {
      Text('child:' + this.dataInfo.money)
      Button('花100块')
        .onClick(() => {
          this.dataInfo.money -= 100
        })
      ChildChild1()
    }
    .padding(20)
    .backgroundColor(Color.Pink)
  }
}

@Component
struct ChildChild1 {
  // 各玩各的
  // @State money: number = 0
  // 听爸爸的话
  // @Prop money: number
  // 团结一心
  @Consume
  dataInfo: MoneyInfo1

  // @Link dataInfo: MoneyInfo

  build() {
    Column() {
      Text('ChildChild:' + this.dataInfo.money)
      Button('花100块')
        .onClick(() => {
          this.dataInfo.money -= 100
        })
    }
    .padding(20)
    .backgroundColor(Color.Red)
  }
}

interface MoneyInfo1 {
  money: number
  bank: string
}

:::info
注意: 在不指定Provide名称的情况下,你需要使用相同的名字来定义和接收数据
:::
如果组件已有该命名变量,可以起别名进行提供/接收
:::info
1.提供起别名
@Provide(‘newName’) 重起一个别名叫newName,后代就只能接收newName
:::
在这里插入图片描述

:::info
2.接收起别名
@Consume(‘ProvideName’)
newName:类型
提供的时候没有起别名,接收的时候重起一个别名叫newName
:::

:::info
3.同理,提供的时候起了别名,接收的时候也需要起别名该怎么做呢?
:::

:::danger
注意:@Consume代表数据是接收的,不能有默认值
不要想太多,ArkTS所有内容都不支持深层数据更新 UI渲染
:::

  • 后代传值-案例
    :::success
    黑马云音乐-播放状态传递
    在这里插入图片描述

:::

image.png
:::info
各个页面共享同一个播放状态,而且可以互相控制,如果传递来传递去会非常的麻烦,但是他们都是Tabs组件内的,我们在index页面提供一个状态,在各个组件接收即可
:::

借用之前的TabbarCase进行改造
在这里插入图片描述

  • 创建两个子组件,一个是播放控制的子组件,一个是背景播放的子组件

背景播放组件
在这里插入图片描述

@Component
struct BackPlayComp {
  @Consume
  isPlay:boolean
  build() {
    Row(){
      Row({space:20}){
        Image($r('app.media.b')).width(40)
        Text('耍猴的 - 二手月季')
      }
      Image(this.isPlay?$r('sys.media.ohos_ic_public_pause'):$r('sys.media.ohos_ic_public_play'))
        .width(20)
        .aspectRatio(1)
        .onClick(()=>{
          this.isPlay=!this.isPlay
        })
    }
    .width('100%')
    .padding({left:20,right:20,top:6,bottom:6})
    .backgroundColor(Color.Grey)
    .justifyContent(FlexAlign.SpaceBetween)
  }
}
export {BackPlayComp}

播放控制组件
在这里插入图片描述

@Component
struct PlayControlComp {
  @Consume
  isPlay:boolean
  build() {
    Row({space:20}){
      Image($r('sys.media.ohos_ic_public_play_last'))
        .width(20)
        .aspectRatio(1)

      Image(this.isPlay?$r('sys.media.ohos_ic_public_pause'):$r('sys.media.ohos_ic_public_play'))
        .width(20)
        .aspectRatio(1)
        .onClick(()=>{
          this.isPlay=!this.isPlay
        })

      Image($r('sys.media.ohos_ic_public_play_next'))
        .width(20)
        .aspectRatio(1)
    }
    .width('100%')
    .padding(20)
    .backgroundColor(Color.Pink)
    .justifyContent(FlexAlign.Center)
  }
}
export {PlayControlComp}

首页引用

import { BackPlayComp } from './components/ConnectComp'
import { PlayControlComp } from './components/WechatComp'

@Entry
@Component
struct TabBarCase {
  @State
  list: TabInterface[] = [
    {
      icon: $r("app.media.ic_public_message"),
      selectIcon: $r('app.media.ic_public_message_filled'),
      name: 'wechat',
      title: '微信',
    },
    {
      icon: $r('app.media.ic_public_contacts_group'),
      selectIcon: $r('app.media.ic_public_contacts_group_filled'),
      name: 'connect',
      title: '联系人',
    }, {
    icon: $r('app.media.ic_gallery_discover'),
    selectIcon: $r('app.media.ic_gallery_discover_filled'),
    name: 'discover',
    title: '发现',
  }, {
    icon: $r('app.media.ic_public_contacts'),
    selectIcon: $r('app.media.ic_public_contacts_filled'),
    name: 'my',
    title: '我的',
  }]
  // 组件内的
  @State
  currenIndex: number = 0

  @Provide
  isPlay:boolean = false
  @Builder
  tabBarItem(item: TabInterface) {
    Column({ space: 6 }) {
      Image(item.name === this.list[this.currenIndex].name ? item.selectIcon : item.icon)
        .width(20)
      Text(item.title)
        .fontSize(12)
        .fontColor(item.name === this.list[this.currenIndex].name ? '#1caa20' : '#000')
    }
  }

  build() {
    Row() {
      Stack({alignContent:Alignment.Bottom}) {
        Tabs({ index: $$this.currenIndex }) {
          ForEach(this.list, (item: TabInterface) => {
            TabContent() {
              //   切换展示的内容放这里
              // Text(item.title)
              if (item.name === 'wechat') {
                PlayControlComp()
              } else if (item.name === 'connect') {
                PlayControlComp()
              }

            }.tabBar(this.tabBarItem(item))
          })
        }.barPosition(BarPosition.End)
        BackPlayComp()
          .translate({
            y:-60
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

interface TabInterface {
  name: string
  icon: ResourceStr
  selectIcon: ResourceStr
  title: string
}

:::info
此时,各个页面共享了播放状态,只要任意地方进行改变,都能保持同步
:::

4. 状态共享-状态监听器

如果开发者需要关注某个状态变量的值是否改变,可以使用 @Watch 为状态变量设置回调函数。
Watch(“回调函数名”)中的回调必须在组件中声明,该函数接收一个参数,参数为修改的属性名
注意:Watch修饰符要写在 State Prop Link Provide的修饰符下面,否则会有问题

  • 在第一次初始化的时候,@Watch装饰的方法不会被调用

前面我们做了一个‘抖音’文字抖动效果,如果希望播放的时候希望文字抖动,暂停的时候文字暂停,如下
在这里插入图片描述

改造我们的播放控制组件,添加层叠的文字,并将写死的x,y方向的值设置为变量

@Component
struct PlayControlComp {
  @State
  shakenX:number = 0
  @State
  shakenY:number = 0
  @Consume
  isPlay:boolean
  
  build() {
    Column(){
      Stack(){
        Text('抖音')
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .fontColor('#ff2d83b3')
          .translate({
            x:this.shakenX,
            y:this.shakenY
          })
          .zIndex(1)
        Text('抖音')
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .fontColor('#ffe31fa9')
          .translate({
            x:this.shakenY,
            y:this.shakenX
          })
          .zIndex(2)

        Text('抖音')
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .fontColor('#ff030000')
          .translate({
            x:0,
            y:0
          })
          .zIndex(3)
      }
      Row({space:20}){
        Image($r('sys.media.ohos_ic_public_play_last'))
          .width(20)
          .aspectRatio(1)

        Image(this.isPlay?$r('sys.media.ohos_ic_public_pause'):$r('sys.media.ohos_ic_public_play'))
          .width(20)
          .aspectRatio(1)
          .onClick(()=>{
            this.isPlay=!this.isPlay
          })

        Image($r('sys.media.ohos_ic_public_play_next'))
          .width(20)
          .aspectRatio(1)
      }
      .width('100%')
      .padding(20)
      .backgroundColor(Color.Pink)
      .justifyContent(FlexAlign.Center)
    }

  }
}
export {PlayControlComp}

在这里插入图片描述

:::info
此时我们就可以用@Watch需要观察isPlay的属性了,只要isPlay变了就开始抖动文字
:::

  @Consume
  @Watch('update') //watch写在要监听的数据下方
  isPlay:boolean
  //监听的数据改变时会触发这个函数
  update(){
    if(this.isPlay){
      this.timer = setInterval(()=>{
        this.shakenX = 2 - Math.random()*4
        this.shakenY = 2 - Math.random()*4
      },100)
    }else{
      clearInterval(this.timer)
      this.shakenX = 0
      this.shakenY = 0
    }
  }
  • 完整代码
@Component
struct PlayControlComp {
  @State
  shakenX:number = 0
  @State
  shakenY:number = 0
  timer:number = -1
  @Consume
  @Watch('update')
  isPlay:boolean
  update(){
    if(this.isPlay){
      this.timer = setInterval(()=>{
        this.shakenX = 2 - Math.random()*4
        this.shakenY = 2 - Math.random()*4
      },100)
    }else{
      clearInterval(this.timer)
      this.shakenX = 0
      this.shakenY = 0
    }
  }
  build() {
    Column(){
      Stack(){
        Text('抖音')
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .fontColor('#ff2d83b3')
          .translate({
            x:this.shakenX,
            y:this.shakenY
          })
          .zIndex(1)
        Text('抖音')
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .fontColor('#ffe31fa9')
          .translate({
            x:this.shakenY,
            y:this.shakenX
          })
          .zIndex(2)

        Text('抖音')
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .fontColor('#ff030000')
          .translate({
            x:0,
            y:0
          })
          .zIndex(3)
      }
      Row({space:20}){
        Image($r('sys.media.ohos_ic_public_play_last'))
          .width(20)
          .aspectRatio(1)

        Image(this.isPlay?$r('sys.media.ohos_ic_public_pause'):$r('sys.media.ohos_ic_public_play'))
          .width(20)
          .aspectRatio(1)
          .onClick(()=>{
            this.isPlay=!this.isPlay
          })

        Image($r('sys.media.ohos_ic_public_play_next'))
          .width(20)
          .aspectRatio(1)
      }
      .width('100%')
      .padding(20)
      .backgroundColor(Color.Pink)
      .justifyContent(FlexAlign.Center)
    }

  }
}
export {PlayControlComp}

:::info
简单点说@Watch可以用于主动检测数据变化,需要绑定一个函数,当数据变化时会触发这个函数
:::

5. 综合案例 - 相册图片选取

基于我们已经学习过的单向、双向、后台、状态监听,我们来做一个综合案例,感受一下有了新的修饰符加成,再进行复杂的案例传值时,是否还想之前的知乎一样绕人
在这里插入图片描述

:::info
分析:
1.准备一个用于选择图片的按钮,点击展示弹层
2.准备弹层,渲染所有图片
3.图片添加点击事件,点击时检测选中数量后添加选中状态
4.点击确定,将选中图片同步给页面并关闭弹层
5.取消时,关闭弹层
:::

1-页面布局,准备一个选择图片的按钮并展示

在这里插入图片描述

  • 选择图片Builder
@Builder
export function SelectImageIcon() {
  Row() {
    Image($r('sys.media.ohos_ic_public_add'))
      .width('100%')
      .height('100%')
      .fillColor(Color.Gray)
  }
  .width('100%')
  .height('100%')
  .padding(20)
  .backgroundColor('#f5f7f8')
  .border({
    width: 1,
    color: Color.Gray,
    style: BorderStyle.Dashed
  })
}
  • 页面布局,使用Builder
import { SelectImageIcon } from './builders/SelectBuilder'
@Entry
@Component
struct ImageSelectCase {
  build() {
    Grid() {
      GridItem() {
        SelectImageIcon()
      }.aspectRatio(1)
    }
    .padding(20)
    .width('100%')
    .height('100%')
    .rowsGap(10)
    .columnsGap(10)
    .columnsTemplate('1fr 1fr 1fr')
  }
}

2-准备弹层,点击时展示弹层

:::info
弹层的使用分为3步
1.声明弹层
在这里插入图片描述

2.注册弹层
在这里插入图片描述

3.使用弹层
在这里插入图片描述

:::

  • 弹层组件
// 1.声明一个弹层
@CustomDialog
struct MyDialog {
  controller:CustomDialogController
  build() {
    Column() {
      Text('默认内容')
    }
    .width('100%')
    .padding(20)
    .backgroundColor('#fff')
  }
}

export { MyDialog }
  • 使用弹层
import { SelectImageIcon } from './builders/SelectBuilder'
import { MyDialog } from './components/CustomDialog'
@Entry
@Component
struct ImageSelectCase {
  // 2.注册弹层
  myDialogController:CustomDialogController = new CustomDialogController({
    builder:MyDialog()
  })
  build() {
    Grid() {
      GridItem() {
        SelectImageIcon()
      }.aspectRatio(1)
      .onClick(()=>{
          // 3.使用弹层
          this.myDialogController.open()
      })
    }
    .padding(20)
    .width('100%')
    .height('100%')
    .rowsGap(10)
    .columnsGap(10)
    .columnsTemplate('1fr 1fr 1fr')
  }
}

在这里插入图片描述

:::info
理想很丰满,显示很骨感,不论如何使用弹层,下方都会有一个空白边
这种下半屏或者全屏的展示不适合用CustomDialog,这里只做学习即可
我们看到的效果,更适合用通用属性bindSheet,半模态转场
在这里插入图片描述

需要传入三个参数:
第一个,是否显示模态框
第二个,模态框自定义构建函数
第三个(非必传),模态框的配置项
所以,我们进行改造
:::

import { SelectImageIcon } from './builders/SelectBuilder'
import { MyDialog } from './components/CustomDialog'
import { SelectImage } from './components/SelectImage'

@Entry
@Component
struct ImageSelectCase {
  // 2.注册弹层
  // myDialogController:CustomDialogController = new CustomDialogController({
  //   builder:MyDialog(),
  //   customStyle:true
  // })
  // 下方有留白,取消不了,换一种方案
  @State
  showDialog: boolean = false
  @State
  imageList: ResourceStr[] = [
    "assets/1.webp",
    "assets/2.webp",
    "assets/3.webp",
    "assets/4.webp",
    "assets/5.webp",
    "assets/6.webp",
    "assets/7.webp",
    "assets/8.webp",
    "assets/9.webp",
    "assets/10.webp"
  ]
  @State
  selectList: ResourceStr[] = []
  @State
  selectedList: ResourceStr[] = []

  @Builder
  ImageListBuilder() {
    // 大坑:最外层必须得是容器组件
    Column(){
      SelectImage({imageList:this.imageList})
    }
  }

  build() {
    Grid() {
      GridItem() {
        SelectImageIcon()
      }.aspectRatio(1)
      .onClick(() => {
        // 3.使用弹层
        // this.myDialogController.open()
        this.showDialog = true
      })
    }
    .padding(20)
    .width('100%')
    .height('100%')
    .rowsGap(10)
    .columnsGap(10)
    .columnsTemplate('1fr 1fr 1fr')
    .bindSheet($$this.showDialog, this.ImageListBuilder(), { showClose: false, height: '60%' })
  }
}

:::info
犹豫bindSheet需要一个builder,所以我们声明了一个builder
但是又考虑到了复用,如果其他地方也要选取图片怎么办?我们把内部又抽离成了一个组件
注意:builder内部根级必须是内置组件
:::

@Component
struct SelectImage {
  @Prop
  imageList:ResourceStr[] = []
  build() {
    Column() {
      Row() {
        Text('取消')
        Text('已选中 0/9 张')
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
        Text('确定')
      }.width('100%').padding(20)
      Grid() {
        ForEach(this.imageList, (item: ResourceStr) => {
          GridItem() {
              Image(item)
          }.aspectRatio(1)
        })
      }
      .padding(20)
      .layoutWeight(1)
      .rowsGap(10)
      .columnsGap(10)
      .columnsTemplate('1fr 1fr 1fr')
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#f5f7f8')
  }
}
export { SelectImage }

在这里插入图片描述

3-添加点击事件,设置选中状态
  • 对图片进行改造,统一添加点击事件,并声明一个选中的列表用来收集选中的图片
@Component
struct SelectImage {
  @Prop
  imageList: ResourceStr[] = []
  @State
  selectList: ResourceStr[] = []

  build() {
    Column() {
      Row() {
        Text('取消')
        Text(`已选中${this.selectList.length}/9 张`)
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
        Text('确定')
      }.width('100%').padding(20)
      Grid() {
        ForEach(this.imageList, (item: ResourceStr) => {
          GridItem() {
            Stack({ alignContent: Alignment.BottomEnd }) {
              Image(item)
              if (this.selectList.includes(item)) {
                Image($r('sys.media.ohos_ic_public_select_all'))
                  .width(30)
                  .aspectRatio(1)
                  .fillColor('#ff397204')
                  .margin(4)
              }
            }
          }
          .aspectRatio(1)
          .onClick(() => {
            this.selectList.push(item)
          })
        })
      }
      .padding(20)
      .layoutWeight(1)
      .rowsGap(10)
      .columnsGap(10)
      .columnsTemplate('1fr 1fr 1fr')
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#f5f7f8')
  }
}

export { SelectImage }

:::info
选是能选了,但是选的多了,没有加限制,而且不一定每次都是选多张,所以把能选几张控制一下
包括选中的,要可以取消才行
:::
image.png
在这里插入图片描述

4-点击确定同步给页面

这个就类似于知乎的点赞了,子组件声明一个可以接收父组件传递过来改数据的方法,点确定的时候调用即可
但是,我们学习那么多的修饰符了,就没必要这么麻烦了,既然想子改父,完全可以父传子,用Link接收直接改
父传
在这里插入图片描述

子改
在这里插入图片描述

GIF 2024-5-15 15-25-55.gif
:::info
到这效果基本就完成了,最后一个关闭弹层,你能想到怎么做了吗?
:::

5.关闭弹层

在这里插入图片描述

:::info
再添加一个预览图片的需求,添加后的图片可以点击预览查看,该如何实现呢?
:::
绑定添加事件,用弹层展示图片

  • 自定义弹层
// 1.声明一个弹层
@CustomDialog
struct MyDialog {
  controller:CustomDialogController
  @Prop
  selectedList:ResourceStr[] = []
  @State
  selectIndex:number = 0
  build() {
    Column() {
      Swiper(){
        ForEach(this.selectedList,(item:ResourceStr)=>{
          Image(item)
            .width('100%')
        })
      }.index($$this.selectIndex)
      Text(`${this.selectIndex+1}/${this.selectedList.length}`)
        .fontColor('#fff')
        .margin(20)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#000')
    .justifyContent(FlexAlign.Center)
    .onClick(()=>{
      this.controller.close()
    })
  }
}

export { MyDialog }


  • 使用弹层
import { SelectImageIcon } from './builders/SelectBuilder'
import { MyDialog } from './components/CustomDialog'
import { SelectImage } from './components/SelectImage'

@Entry
@Component
struct ImageSelectCase {
  @State
  selectedList: ResourceStr[] = []
  // 2.注册弹层
  myDialogController:CustomDialogController = new CustomDialogController({
    builder:MyDialog({
      // 传递的属性必须先声明
      selectedList:this.selectedList
    }),
    customStyle:true
  })
  // 下方有留白,取消不了,换一种方案
  @State
  showDialog: boolean = false
  @State
  imageList: ResourceStr[] = [
    "assets/1.webp",
    "assets/2.webp",
    "assets/3.webp",
    "assets/4.webp",
    "assets/5.webp",
    "assets/6.webp",
    "assets/7.webp",
    "assets/8.webp",
    "assets/9.webp",
    "assets/10.webp"
  ]


  @Builder
  ImageListBuilder() {
    // 大坑:最外层必须得是容器组件
    Column(){
      SelectImage({
        imageList:this.imageList,
        selectedList:this.selectedList,
        showDialog:this.showDialog
      })
    }
  }

  build() {
    Grid() {
      ForEach(this.selectedList,(item:ResourceStr)=>{
        GridItem() {
          Image(item)
        }.aspectRatio(1)
        .onClick(()=>{
          this.myDialogController.open()
        })
      })
      GridItem() {
        SelectImageIcon()
      }.aspectRatio(1)
      .onClick(() => {
        // 3.使用弹层
        // this.myDialogController.open()
        this.showDialog = true
      })
    }
    .padding(20)
    .width('100%')
    .height('100%')
    .rowsGap(10)
    .columnsGap(10)
    .columnsTemplate('1fr 1fr 1fr')
    .bindSheet($$this.showDialog, this.ImageListBuilder(), { showClose: false, height: '60%' })
  }
}

6. @Observed与@ObjectLink

:::info
之前讲解Link的时候,我们说了一个要求,就是只有@State或者@Link修饰的数据才能用,
如果是一个数组内有多个对象,将对象传递给子组件的时候就没有办法使用Link了
ArtTS支持 Observed和@ObjectLink来实现这个需求
:::
例如美团点菜,菜品肯定是一个数组,如果我们将每个菜品封装成组件
当对菜品进行修改的时候,就没法再用Link同步了
image.png

使用步骤:

  • 使用 @Observed 修饰这个类
  • 初始化数据:数据确保是通过 @Observed 修饰的类new出来的
  • 通过 @ObjectLink 修饰传递的数据,可以直接修改被关联对象来更新UI

模拟一个点菜的案例来演示用法

在这里插入图片描述

@Entry
@Component
struct ObservedObjectLinkCase {
  @State
  goodsList:GoodsTypeModel[] = [
    new GoodsTypeModel({
      name:'瓜子',
      price:3,
      count:0
    }),
    new GoodsTypeModel({
      name:'花生',
      price:3,
      count:0
    }),
    new GoodsTypeModel({
      name:'矿泉水',
      price:3,
      count:0
    })
  ]
  build() {
    Column(){
      ForEach(this.goodsList,(item:GoodsTypeModel)=>{
        // 2.确保传递的对象是new过observed修饰的
        GoodItemLink({
          goodItem:item
        })
      })
    }
  }
}

@Component
struct GoodItemLink {
  // 3.用ObjectLink修饰
  @ObjectLink
  goodItem:GoodsTypeModel
  build() {
    Row({space:20}){
      Text(this.goodItem.name)
      Text('¥'+this.goodItem.price)
      Image($r('sys.media.ohos_ic_public_remove_filled'))
        .width(20)
        .aspectRatio(1)
        .onClick(()=>{
          this.goodItem.count--
        })
      Text(this.goodItem.count.toString())
      Image($r('sys.media.ohos_ic_public_add_norm_filled'))
        .width(20)
        .aspectRatio(1)
        .onClick(()=>{
          this.goodItem.count++
        })
    }.width('100%').padding(20)
  }
}

interface GoodsType {
  name:string
  price:number
  count:number
}
// 1.使用observed修饰一个类
@Observed
export class GoodsTypeModel implements GoodsType {
  name: string = ''
  price: number = 0
  count: number = 0

  constructor(model: GoodsType) {
    this.name = model.name
    this.price = model.price
    this.count = model.count
  }
}

:::success
改造-知乎案例
点赞- 需求是当前数据的点赞量+1或者-1, 之前实际实现是: 把一条数据 给到父组件-替换了父组件的整行的数据, 并且造成了案例中头像的闪烁-因为这个组件数据被销毁然后被创建
理想效果: 其他一切都不动,只动数量的部分-也就是UI视图的局部更新- 需要使用Observed和ObjectLink
:::

@Observed
export class ReplyItemModel implements ReplyItem {
  avatar: ResourceStr = ''
  author: string = ''
  id: number = 0
  content: string = ''
  time: string = ''
  area: string = ''
  likeNum: number = 0
  likeFlag: boolean | null = null

  constructor(model: ReplyItem) {
    this.avatar = model.avatar
    this.author = model.author
    this.id = model.id
    this.content = model.content
    this.time = model.time
    this.area = model.area
    this.likeNum = model.likeNum
    this.likeFlag = model.likeFlag
  }
}

  • 给知乎的评论组件增加一个ObjectLink修饰符
 // 接收渲染的选项
  @ObjectLink
  item: ReplyItemModel
  • 评论子组件实现点赞的方法
// 更新逻辑
  changeLike () {
    if(this.item.likeFlag) {
      // 点过赞
      this.item.likeNum--
    }
    else {
      // 没有点过赞
      this.item.likeNum++
    }
    this.item.likeFlag = !this.item.likeFlag // 取反
  }
  • 父组件传值优化
 ForEach(this.commentList, (item: ReplyItemModel) => {
         ListItem() {
           HmCommentItem({
             item: item
           })
         }
       })

:::info
细节:此时,我们的头像不再闪动,说明数据已经不需要去更新整条数据来让父组件完成UI的更新,而是子组件内部局部的更新
:::

:::info
注意点:

  • ObjectLink只能修饰被Observed修饰的class类型
  • Observed修饰的class的数据如果是复杂数据类型,需要采用赋值的方式才可以具备响应式特性-因为它只能监听到第一层
  • 如果出现复杂类型嵌套,只需要Observed我们需要的class即可
  • ObjectLink修饰符不能用在Entry修饰的组件中
    :::

:::info
此知识点不太好理解,同学们一定一定多敲几遍!!!!!
:::

7. Next新增修饰符-Require-Track

:::success
Require修饰符
4.0的编辑器中- 如果子组件定义了Prop,那么父组件必须得传,不传则报错
Next版本中,如果你想让父组件必须传递一个属性给你的Prop,作为强制性的约束条件,可以使用Require修饰符
:::

:::success
Require修饰符只能作用在两个修饰符前面Prop BuilderParam
:::

@Entry
@Component
struct RequireCase {
  @State
  message: string = 'Hello World';

  @Builder
  parentContent(){
    Text('builderParam')
  }
  build() {
    Row() {
      Column() {
        RequireChild({
          message: this.message
        }){
          this.parentContent()
        }
      }
      .width('100%')
    }
    .height('100%')
  }
}


@Component
struct RequireChild {
  // 1.Prop
  @Require
  @Prop
  message: string
  // 2.BuilderParam
  @Require
  @BuilderParam
  defaultContent: () => void
  build() {
    Column() {
      Text(this.message)
      this.defaultContent()
    }
  }
}

:::success
Track修饰符- 只针对对象中的某个属性的更新起作用,其余没修饰的属性不能进行UI展示
:::

在这里插入图片描述

该修饰符不存在新的视觉效果,属于性能优化级的,改造知乎点赞,对数据添加@Track查看效果

export interface ReplyItem {
  avatar: ResourceStr // 头像
  author: string   // 作者
  id: number  // 评论的id
  content: string // 评论内容
  time: string // 发表时间
  area: string // 地区
  likeNum: number // 点赞数量
  likeFlag: boolean | null // 当前用户是否点过赞
}
@Observed
export class ReplyItemModel implements ReplyItem {
  @Track
  avatar: ResourceStr = ''
  @Track
  author: string = ''
  @Track
  id: number = 0
  @Track
  content: string = ''
  @Track
  time: string = ''
  @Track
  area: string = ''
  @Track
  likeNum: number = 0
  @Track
  likeFlag: boolean | null = null

  constructor(model: ReplyItem) {
    this.avatar = model.avatar
    this.author = model.author
    this.id = model.id
    this.content = model.content
    this.time = model.time
    this.area = model.area
    this.likeNum = model.likeNum
    this.likeFlag = model.likeFlag
  }
}

:::success
Track的作用只更新对象中的某些字段, Track修饰符用来作用在class中的某些字段,只有被标记的字段才会更新,并且没有被Track标记的字段不能被使用
场景:
假如只想根据对象中某个字段来更新或者渲染视图 就可以使用Track
:::

3.应用状态

:::success
State 组件内状态
Prop 父组件传入
Link 父组件传入
Provide 跨级组件传入
Consume 跨级组件接收
ObjectLink 父组件传入局部更新状态
:::

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

  • LocalStorage-UIAbility状态(内存- 注意:和前端的区分开,它非持久化,非全应用)
  • AppStorage- 应用内状态-多UIAbility共享-(内存-非持久化-退出应用同样消失)
  • PersistentStorage-全局持久化状态(写入磁盘-持久化状态-退出应用 数据同样存在)
  • 首选项- 写入磁盘
  • 关系型数据库 - 写入磁盘
  • 端云数据库
  • 接口调用-云端数据(服务器数据)
    :::

1. UIAbility内状态-LocalStorage

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

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

  • 单向 @LocalStorageProp('user') 组件内可变

  • 双向 @LocalStorageLink('user') 全局均可变
    :::
    案例-修改用户信息

  • 创建一个LocalStorage,用于各个页面间共享数据
    :::info
    步骤:
    1.准备一个含有类型声明的对象作为共享数据
    2.将数据传入new LocalStorage(),得到可以共享的对象
    3.导入共享对象,在需要使用的页面导入该对象,并传入@Entry
    4.声明一个变量,用@LocalStorageProp或@LocalStorageLink修饰进行接收
    5.使用声明的变量进行渲染
    :::

  • LocalStorage的声明与导出

// self是要共享的数据
const   self: Record<string, ResourceStr> = {
  'age': '18',
  'nickName': '一介码农',
  'gender': '男',
  'avtar': $r('app.media.b')
}
// localUserInfo是共享的数据
export const localUserInfo = new LocalStorage(self)

image.png
页面结构直接复制粘贴即可

@Entry
@Component
struct LocalStorageCase01 {
  build() {
    Column() {
      Row() {
        Image($r('sys.media.ohos_ic_back'))
          .width(20)
          .aspectRatio(1)
        Text('个人信息1')
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
        Text('确定')
      }.width('100%')
      .padding(20)
      .alignItems(VerticalAlign.Center)

      Row() {
        Text('头像:')
        Image('')
          .width(40)
      }.width('100%')
      .padding(20)
      .justifyContent(FlexAlign.SpaceBetween)

      Row() {
        Text('昵称:')
        TextInput({ text: '' })
          .textAlign(TextAlign.End)
          .layoutWeight(1)
          .backgroundColor('#fff')
          .padding({
            right: 0
          })
      }.width('100%')
      .padding(20)
      .justifyContent(FlexAlign.SpaceBetween)

      Row() {
        Text('性别:')
        TextInput({ text: '' })
          .textAlign(TextAlign.End)
          .layoutWeight(1)
          .backgroundColor('#fff')
          .padding({
            right: 0
          })
      }.width('100%')
      .padding(20)
      .justifyContent(FlexAlign.SpaceBetween)

      Row() {
        Text('年龄:')
        TextInput({ text: '' })
          .textAlign(TextAlign.End)
          .layoutWeight(1)
          .backgroundColor('#fff')
          .padding({
            right: 0
          })
      }.width('100%')
      .padding(20)
      .justifyContent(FlexAlign.SpaceBetween)
    }
    .width('100%')
    .height('100%')
  }
}
  • 页面引用并传递共享的数据进行使用
// 1.引入可以共享的数据
import { localUserInfo } from './LocalStorageModel'
import { router } from '@kit.ArkUI'

// 2.传递给页面
@Entry(localUserInfo)
@Component
struct LocalStorageCase02 {
  // 3.使用localUserInfo

  @LocalStorageLink('avtar')
  avtar: ResourceStr = ''
  @LocalStorageLink('nickName')
  nickName: ResourceStr = ''
  @LocalStorageLink('gender')
  gender: ResourceStr = ''
  @LocalStorageLink('age')
  age: ResourceStr = ''

  build() {
    Column() {
      Row() {
        Image($r('sys.media.ohos_ic_back'))
          .width(20)
          .aspectRatio(1)
          .onClick(()=>{
            router.back()
          })
        Text('个人信息2')
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
        Text('确定')
      }.width('100%')
      .padding(20)
      .alignItems(VerticalAlign.Center)

      Row() {
        Text('头像:')
        Image(this.avtar)
          .width(40)
      }.width('100%')
      .padding(20)
      .justifyContent(FlexAlign.SpaceBetween)

      Row() {
        Text('昵称:')
        TextInput({ text: $$this.nickName })
          .textAlign(TextAlign.End)
          .layoutWeight(1)
          .backgroundColor('#fff')
          .padding({
            right: 0
          })
      }.width('100%')
      .padding(20)
      .justifyContent(FlexAlign.SpaceBetween)

      Row() {
        Text('性别:')
        TextInput({ text: $$this.gender})
          .textAlign(TextAlign.End)
          .layoutWeight(1)
          .backgroundColor('#fff')
          .padding({
            right: 0
          })
      }.width('100%')
      .padding(20)
      .justifyContent(FlexAlign.SpaceBetween)

      Row() {
        Text('年龄:')
        TextInput({ text: $$this.age })
          .textAlign(TextAlign.End)
          .layoutWeight(1)
          .backgroundColor('#fff')
          .padding({
            right: 0
          })
      }.width('100%')
      .padding(20)
      .justifyContent(FlexAlign.SpaceBetween)
    }
    .width('100%')
    .height('100%')
  }
}
  • 新建一个页面,将共享的数据同时作用到两个页面,router.pushUrl可以跳转页面
//跳转
Text('修改')
          .onClick(()=>{
            router.pushUrl({
              url:'pages/08/LocalStorageDemo/LocalStorageCase01'
            })
          })
//返回
 Image($r('sys.media.ohos_ic_back'))
          .width(20)
          .aspectRatio(1)
          .onClick(()=>{
            router.back()
          })
  • 使用LocalStorageLink实现双向绑定
  @LocalStorageLink('nickName')
  nickName:string = ''

:::info

  • 将LocalStorage实例从UIAbility共享到一个或多个视图,参考 官方示例
  • 使用场景:

服务卡片-只能通过LocalStorage进行接收参数
:::

import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';

export default class EntryAbility extends UIAbility {
  // self是要共享的数据
  self: Record<string, ResourceStr> = {
    'age': '19',
    'nickName': '一介码农',
    'gender': '男',
    'avtar': $r('app.media.b')
  }
  // localUserInfo是共享的数据
  localUserInfo:LocalStorage = new LocalStorage(this.self)
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
  }

  onDestroy(): void {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy');
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/08/LocalStorage/LocalStorage02',this.localUserInfo );
  }

  onWindowStageDestroy(): void {
    // Main window is destroyed, release UI related resources
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
  }

  onForeground(): void {
    // Ability has brought to foreground
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');
  }

  onBackground(): void {
    // Ability has back to background
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground');
  }
}

2. 应用状态-AppStorage

LocalStorage是针对UIAbility的状态共享- 一个UIAbility有个页面
一个应用可能有若干个UIAbility

:::success

概述

AppStorage是在应用启动的时候会被创建的单例。它的目的是为了提供应用状态数据的中心存储,这些状态数据在应用级别都是可访问的。AppStorage将在应用运行过程保留其属性。属性通过唯一的键字符串值访问。
AppStorage可以和UI组件同步,且可以在应用业务逻辑中被访问。
AppStorage支持应用的主线程内多个UIAbility实例间的状态共享。
AppStorage中的属性可以被双向同步,数据可以是存在于本地或远程设备上,并具有不同的功能,比如数据持久化(详见PersistentStorage)。这些数据是通过业务逻辑中实现,与UI解耦,如果希望这些数据在UI中使用,需要用到@StorageProp和@StorageLink。
:::
:::info
AppStorage 是应用全局的UI状态存储,是和应用的进程绑定的,由UI框架在应用程序启动时创建,为应用程序UI状态属性提供中央存储。-注意它也是内存数据,不会写入磁盘
第一种用法-使用UI修饰符

  • **如果是初始化使用 ****AppStorage.setOrCreate(key,value)**
  • 单向 **@StorageProp('user')** 组件内可变
  • 双向 **@StorageLink('user')** 全局均可变

第二种用法 使用API方法

  • **AppStorage.get<ValueType>(key)**** 获取数据**

  • **AppStorage.set<ValueType>(key,value)**** 覆盖数据**
    :::
    :::success
    AppStorage.setOrCreate(“”, T) // 创建或者设置某个字段的属性
    AppStorage.get(“”) // 获取的全局状态类型
    如果遇到获取数据的类型为空,可以用if判断,也可以用非空断言来解决
    StorageLink . - 直接修改-自动同步到全局状态
    StorageProp- 可以改,只会在当前组件生效,只是改的全局状态的副本,不会对全局状态产生影响
    :::
    准备两个页面,A页面登录获取用户信息,B页面展示修改

  • A页面登录模版,用于存入AppStorage

在这里插入图片描述

@Entry
@Component
struct AppStorageCase01 {
  @State
  username: string = ""
  @State
  password: string = ""
  build() {
    Row() {
      Column({ space: 20 }) {
        TextInput({ placeholder: '请输入用户名', text: $$this.username })
        TextInput({ placeholder: '请输入密码', text: $$this.password })
          .type(InputType.Password)
        Button("登录")
          .width('100%')
      }
      .padding(20)
      .width('100%')
    }
    .height('100%')
  }
}

  • B页面登录模版,用于展示AppStorage

在这里插入图片描述

@Entry
@Component
struct AppStorageCase02 {
  build() {
    Column() {
      Row({ space: 20 }) {
        Image($r('app.media.b'))
          .width(60)
          .aspectRatio(1)
          .borderRadius(30)
        Column({ space: 10 }) {
          Text('姓名:老潘')
          Text(`年龄:18岁`)
        }
      }
      .alignItems(VerticalAlign.Center)
      .padding(20)
      .width('100%')

      Button("退出")
    }
    .width('100%')
    .height('100%')
  }
}
  • A页面点击登录
import { router } from '@kit.ArkUI'

@Entry
@Component
struct AppStorageCase01 {
  @State
  username: string = ""
  @State
  password: string = ""
  login(){
    const userInfo:Record<string,string> = {
      'name':'一介码农',
      'age':'99',
    }
    AppStorage.setOrCreate<Record<string,string>>('userInfo',userInfo)
    router.pushUrl({
      url:'pages/08/AppStorageDemo/AppStorageCase1'
    })
  }
  build() {
    Row() {
      Column({ space: 20 }) {
        TextInput({ placeholder: '请输入用户名', text: $$this.username })
        TextInput({ placeholder: '请输入密码', text: $$this.password })
          .type(InputType.Password)
        Button("登录")
          .width('100%')
          .onClick(()=>{
            this.login()
          })
      }
      .padding(20)
      .width('100%')
    }
    .height('100%')
  }
}

  • B页面展示登录信息
@Entry
@Component
struct AppStorageCase02 {
  // 用法1
  // @StorageProp('userInfo')
  // userInfo:Record<string,string> = {}
  // 用法2
  @State
  userInfo:Record<string,string> = {}
  aboutToAppear(): void {
    const userInfo = AppStorage.get<Record<string,string>>('userInfo')
    this.userInfo = userInfo!
  }
  build() {
    Column() {
      Row({ space: 20 }) {
        Image($r('app.media.b'))
          .width(60)
          .aspectRatio(1)
          .borderRadius(30)
        Column({ space: 10 }) {
          Text(`姓名:${this.userInfo.name}`)
          Text(`年龄:${this.userInfo.age}`)
        }
      }
      .alignItems(VerticalAlign.Center)
      .padding(20)
      .width('100%')

      Button("退出")
        .onClick(()=>{
          AppStorage.set('userInfo',null)
          router.back()
        })
    }
    .width('100%')
    .height('100%')
  }
}

新建一个Ability,打开新的UIAbility查看状态

 let want:Want = {
      'deviceId': '', // deviceId为空表示本设备
      'bundleName': 'com.example.harmonyos_next_base',
      'abilityName': 'EntryAbility1',
    };
    (getContext() as common.UIAbilityContext).startAbility(want)

3. 状态持久化-PersistentStorage

:::info
前面讲的所有状态均为内存状态,也就是应用退出便消失,所以如果我们想持久化的保留一些数据,应该使用
PersistentStorage
注意:
UI和业务逻辑不直接访问 PersistentStorage 中的属性,所有属性访问都是对 AppStorage 的访问,AppStorage 中的更改会自动同步到 PersistentStorage

也就是,我们和之前访问AppStorage是一样的,只不过需要提前使用PersistentStorage来声明
:::

PersistentStorage 将选定的 AppStorage 属性保留在设备磁盘上。

:::warning

  • 支持:number, string, boolean, enum 等简单类型;
  • 如果:要支持对象类型,可以转换成json字符串
  • 持久化变量最好是小于2kb的数据,如果开发者需要存储大量的数据,建议使用数据库api。

用法:
PersistentStorage.PersistProp(‘属性名’, 值)

注意: 如果用了持久化, 那么AppStorage读取出来的对象实际上是PersistentStorage存储的json字符串
如果没用持久化 。那么读取出来的对象就是AppStorage对象
:::

将刚刚的token直接持久化存储

PersistentStorage.PersistProp("user", '123') // 初始化磁盘,给一个读取不到时加载的默认值

:::info
只要初始化了数据,我们以后使用AppStorage就可以读取和设置,它会自动同步到我们的磁盘上
目前不支持复杂对象的持久化,如果你需要存储,你需要把它序列化成功字符串

  • 测试:需要在真机或模拟器调试
    :::
    在这里插入图片描述

大家可以在上一个例子之前添加 PersistentStorage.PersistProp(‘属性名’, 值)
然后直接使用AppStorage进行set就可以了,设置完成之后,使用模拟器先把任务销毁,然后再查看数据是否显示

:::success

限制条件

PersistentStorage允许的类型和值有:

  • number, string, boolean, enum 等简单类型。
  • 可以被JSON.stringify()和JSON.parse()重构的对象。例如Date, Map, Set等内置类型则不支持,以及对象的属性方法不支持持久化。

PersistentStorage不允许的类型和值有:

  • 不支持嵌套对象(对象数组,对象的属性是对象等)。因为目前框架无法检测AppStorage中嵌套对象(包括数组)值的变化,所以无法写回到PersistentStorage中。
  • 不支持undefined 和 null 。

持久化数据是一个相对缓慢的操作,应用程序应避免以下情况:

  • 持久化大型数据集。
  • 持久化经常变化的变量。

PersistentStorage的持久化变量最好是小于2kb的数据,不要大量的数据持久化,因为PersistentStorage写入磁盘的操作是同步的,大量的数据本地化读写会同步在UI线程中执行,影响UI渲染性能。如果开发者需要存储大量的数据,建议使用数据库api。
PersistentStorage只能在UI页面内使用,否则将无法持久化数据。
:::

4. 状态持久化-preferences首选项

:::success
此时此刻,需要做一件事, 有token跳转到主页,没有token跳转到登录
:::
:::success
首选项

  • 每一个key的value的长度最大为8kb
  • 创建首选项-仓库的概念- 应用可以有N个仓库,一个仓库中可以有N个key
    :::
import { Context } from '@kit.AbilityKit'
import { preferences } from '@kit.ArkData'
// 两种方式引入的是同一个东西
// import preferences from '@ohos.data.preferences'

export class PreferencesClass {
  // static代表的是静态,可以直接通过类访问
  // store名称
  static defaultStore: string = 'DEFAULT_STORE'
  static firstStore: string = 'FIRST_STORE'
  // 字段名称,一个字段配2个方法,读取和写入
  static tokenKey:string = 'TOKEN_KEY'
  //   仓库中存储字段
  static setToken(content:Context,token:string,storeName:string=PreferencesClass.defaultStore){
    const store = preferences.getPreferencesSync(content,{name:storeName})
    store.putSync(PreferencesClass.tokenKey,token)
    store.flush()
  }
  //   读取仓库中字段
  static getToken(content:Context,storeName:string=PreferencesClass.defaultStore){
    const store = preferences.getPreferencesSync(content,{name:storeName})
    return store.getSync(PreferencesClass.tokenKey,'')
  }
}
  • 在ability中判断

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

5. 设备状态-Environment(了解)

:::info
开发者如果需要应用程序运行的设备的环境参数,以此来作出不同的场景判断,比如多语言,暗黑模式等,需要用到Environment设备环境查询。
:::
在这里插入图片描述

  • 1.将设备的色彩模式存入AppStorage,默认值为Color.LIGHT
Environment.EnvProp('colorMode', Color.LIGHT);
  • 2.可以使用@StorageProp进行查询,从而实现不同UI
@StorageProp('colorMode') 
lang : bgColor = Color.White';

image.png

  • 该环境变量只能查询后写入AppStorage,可以在AppStorage中进行修改,改目前使用场景比较鸡肋,作为面试知识点储备即可
// 使用Environment.EnvProp将设备运行languageCode存入AppStorage中;
Environment.EnvProp('colorMode', 'en');
// 从AppStorage获取单向绑定的languageCode的变量
const lang: SubscribedAbstractProperty<string> = AppStorage.Prop('colorMode');

if (lang.get() ===  Color.LIGHT) {
  console.info('亮色');
} else {
  console.info('暗色');
}

4.网络管理(需要模拟器)

1. 应用权限

ATM (AccessTokenManager) 是HarmonyOS上基于AccessToken构建的统一的应用权限管理能力
应用权限保护的对象可以分为数据和功能:

  • 数据包含了个人数据(如照片、通讯录、日历、位置等)、设备数据(如设备标识、相机、麦克风等)、应用数据。
  • 功能则包括了设备功能(如打电话、发短信、联网等)、应用功能(如弹出悬浮框、创建快捷方式等)等。

根据授权方式的不同,权限类型可分为system_grant(系统授权)和user_grant(用户授权)。

  • 配置文件权限声明
  • 向用户申请授权

例如:访问网络需要联网权限
system_grant(系统授权)配置后直接生效
image.png

{
  "module" : {
    // ...
    "requestPermissions":[
      {
        "name" : "ohos.permission.INTERNET"
      }
    ]
  }
}

例如:获取地址位置权限
user_grant(用户授权)向用户申请
在这里插入图片描述

1.首先在module.json5中配置权限申请地址位置权限

{
  "module" : {
    // ...
    "requestPermissions":[
      {
        "name" : "ohos.permission.INTERNET"
      }{
        "name": "ohos.permission.APPROXIMATELY_LOCATION",
        "reason": "$string:permission_location",
        "usedScene": {"abilities": ["EntryAbility"]}
      }
    ]
  }
}

2.在ability中申请用户授权
在这里插入图片描述

通过abilityAccessCtrl创建管理器进行申请权限

 async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): Promise<void> {
    const manager = abilityAccessCtrl.createAtManager() // 创建程序控制管理器
    await manager.requestPermissionsFromUser(this.context,
      [
        "ohos.permission.APPROXIMATELY_LOCATION"
      ])
  }

开启权限后可以获取经纬度坐标
在这里插入图片描述

import { geoLocationManager } from '@kit.LocationKit';

@Entry
@Component
struct HuaweiMapDemo {
  @State
  result:geoLocationManager.Location  = {} as geoLocationManager.Location
  build() {
    Column() {
      Button('获取经纬度')
        .onClick(async ()=>{
          this.result = await geoLocationManager.getCurrentLocation()
        })
      Text('经度:'+this.result.latitude)
      Text('纬度:'+this.result.longitude)
    }.height('100%')
  }
}

2. HTTP请求(需要模拟器)

:::success

request接口开发步骤

  1. 从@ohos.net.http.d.ts中导入http命名空间
  2. 调用createHttp()方法,创建一个HttpRequest对象
  3. 调用该对象的on()方法,订阅http响应头事件,此接口会比request请求先返回。可以根据业务需要订阅此消息。
  4. 调用该对象的request()方法,传入http请求的url地址和可选参数,发起网络请求
  5. 按照实际业务需要,解析返回结果。
  6. 调用该对象的off()方法,取消订阅http响应头事件。
  7. 当该请求使用完毕时,调用destroy()方法主动销毁。
    :::
// 引入包名
import http from '@ohos.net.http';
import { BusinessError } from '@ohos.base';

// 每一个httpRequest对应一个HTTP请求任务,不可复用
let httpRequest = http.createHttp();
// 用于订阅HTTP响应头,此接口会比request请求先返回。可以根据业务需要订阅此消息
// 从API 8开始,使用on('headersReceive', Callback)替代on('headerReceive', AsyncCallback)。 8+
httpRequest.on('headersReceive', (header) => {
  console.info('header: ' + JSON.stringify(header));
});
httpRequest.request(
  // 填写HTTP请求的URL地址,可以带参数也可以不带参数。URL地址需要开发者自定义。请求的参数可以在extraData中指定
  "EXAMPLE_URL",
  {
    method: http.RequestMethod.POST, // 可选,默认为http.RequestMethod.GET
    // 开发者根据自身业务需要添加header字段
    header: [{
      'Content-Type': 'application/json'
    }],
    // 当使用POST请求时此字段用于传递内容
    extraData: "data to send",
    expectDataType: http.HttpDataType.STRING, // 可选,指定返回数据的类型
    usingCache: true, // 可选,默认为true
    priority: 1, // 可选,默认为1
    connectTimeout: 60000, // 可选,默认为60000ms
    readTimeout: 60000, // 可选,默认为60000ms
    usingProtocol: http.HttpProtocol.HTTP1_1, // 可选,协议类型默认值由系统自动指定
    usingProxy: false, // 可选,默认不使用网络代理,自API 10开始支持该属性
    caPath:'/path/to/cacert.pem', // 可选,默认使用系统预制证书,自API 10开始支持该属性
    clientCert: { // 可选,默认不使用客户端证书,自API 11开始支持该属性
      certPath: '/path/to/client.pem', // 默认不使用客户端证书,自API 11开始支持该属性
      keyPath: '/path/to/client.key', // 若证书包含Key信息,传入空字符串,自API 11开始支持该属性
      certType: http.CertType.PEM, // 可选,默认使用PEM,自API 11开始支持该属性
      keyPassword: "passwordToKey" // 可选,输入key文件的密码,自API 11开始支持该属性
    },
    multiFormDataList: [ // 可选,仅当Header中,'content-Type'为'multipart/form-data'时生效,自API 11开始支持该属性
      {
        name: "Part1", // 数据名,自API 11开始支持该属性
        contentType: 'text/plain', // 数据类型,自API 11开始支持该属性
        data: 'Example data', // 可选,数据内容,自API 11开始支持该属性
        remoteFileName: 'example.txt' // 可选,自API 11开始支持该属性
      }, {
        name: "Part2", // 数据名,自API 11开始支持该属性
        contentType: 'text/plain', // 数据类型,自API 11开始支持该属性
        // data/app/el2/100/base/com.example.myapplication/haps/entry/files/fileName.txt
        filePath: `${getContext(this).filesDir}/fileName.txt`, // 可选,传入文件路径,自API 11开始支持该属性
        remoteFileName: 'fileName.txt' // 可选,自API 11开始支持该属性
      }
    ]
  }, (err: BusinessError, data: http.HttpResponse) => {
    if (!err) {
      // data.result为HTTP响应内容,可根据业务需要进行解析
      console.info('Result:' + JSON.stringify(data.result));
      console.info('code:' + JSON.stringify(data.responseCode));
      // data.header为HTTP响应头,可根据业务需要进行解析
      console.info('header:' + JSON.stringify(data.header));
      console.info('cookies:' + JSON.stringify(data.cookies)); // 8+
      // 当该请求使用完毕时,调用destroy方法主动销毁
      httpRequest.destroy();
    } else {
      console.error('error:' + JSON.stringify(err));
      // 取消订阅HTTP响应头事件
      httpRequest.off('headersReceive');
      // 当该请求使用完毕时,调用destroy方法主动销毁
      httpRequest.destroy();
    }
  }
);

美团外卖接口地址: https://zhousg.atomgit.net/harmonyos-next/takeaway.json

2)使用 @ohos.net.http 模块发请求

import http from '@ohos.net.http'

@Entry
@Component
struct HttpCase {
  aboutToAppear() {
    this.getMeiTuanData()
  }

  async getMeiTuanData() {
    try {
      const req = http.createHttp()
      const res = await  req.request("https://zhousg.atomgit.net/harmonyos-next/takeaway.json")
      AlertDialog.show({
        message: res.result as string
      })
    } catch (e) {

    }

  }

  build() {
    Row() {
      Column() {

      }
      .width('100%')
    }
    .height('100%')
  }
}

在这里插入图片描述

:::success
使用第三方包 axios
:::
:::success
openharmony中心仓地址
:::

  • 安装axios
$  ohpm install @ohos/axios
  • 发起请求
import axios, { AxiosResponse } from '@ohos/axios'
import { promptAction } from '@kit.ArkUI';

@Entry
@Component
struct HttpCase {
  @State message: string = 'Hello World';

  async getData() {
    const result = await axios.get<object, AxiosResponse<object,null>>("https://zhousg.atomgit.net/harmonyos-next/takeaway.json")
    promptAction.showToast({ message: JSON.stringify(result) })
  }

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
        Button("测试请求")
          .onClick(() => {
            this.getData()
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}
interface Data {
  name: string
}

image.png

5.今日案例-美团外卖

:::success
准备基础色值
在一个标准项目中,应该会有几套标准的配色,此时可以使用resources/base/element/color.json来帮我们统一管理,使用时使用$r(“app.color.xxx”)来取值即可
:::

  • 将color赋值到resources/base/element/color.json中
{
  "color": [
    {
      "name": "start_window_background",
      "value": "#FFFFFF"
    },
    {
      "name": "white",
      "value": "#FFFFFF"
    },
    {
      "name": "black",
      "value": "#000000"
    },
    {
      "name": "bottom_back",
      "value": "#222426"
    },
    {
      "name": "main_color",
      "value": "#f8c74e"
    },
    {
      "name": "select_border_color",
      "value": "#fa0"
    },
    {
      "name": "un_select_color",
      "value": "#666"
    },
    {
      "name": "search_back_color",
      "value": "#eee"
    },
    {
      "name": "search_font_color",
      "value": "#999"
    },
    {
      "name": "food_item_second_color",
      "value": "#333"
    },
    {
      "name": "food_item_label_color",
      "value": "#fff5e2"
    },
    {
      "name": "top_border_color",
      "value": "#e4e4e4"
    },
    {
      "name": "left_back_color",
      "value": "#f5f5f5"
    },{
      "name": "font_main_color",
      "value": "#ff8000"
    }
  ]
}

!在这里插入图片描述
在这里插入图片描述

1. 目录结构-入口页面

:::success
新建如下目录结构
pages
-MeiTuan
-api
-components
-models
-utils
-MTIndex.ets(Page)

:::

  • 在MTIndex.ets中设置基础布局
@Entry
  @Component
  struct MTIndex {
    build() {
      Column() {

      }
      .width('100%')
        .height("100%")
        .backgroundColor($r("app.color.white"))
    }
  }

在这里插入图片描述

  • 新建MTTop-MTMain-MTBottom三个组件-在components目录下
@Component
struct MTMain {
  build() {
    Text("MTMain")
  }
}
export default MTMain
@Component
struct MTTop {
  build() {
    Text("MTTop")
  }
}
export default MTTop
@Component
struct MTBottom {
  build() {
    Text("MTBottom")
  }
}
export default MTBottom
  • 在MTIndex.ets中放入
import MTBottom from './components/MTBottom'
import MTMain from './components/MTMain'
import MTTop from './components/MTTop'

@Entry
@Component
struct MTIndex {
  build() {
    Column() {
      Stack({ alignContent: Alignment.Bottom }) {
        Column() {
          MTTop()
          MTMain()
        }
        .height("100%")
        MTBottom()
      }.layoutWeight(1)
    }
    .width('100%')
    .height("100%")
    .backgroundColor($r("app.color.white"))
  }
}

在这里插入图片描述

2. 页面结构-底部组件

在这里插入图片描述

:::success
将图片资源 图片.zip放入到资源目录下 resources/media
:::
在这里插入图片描述

@Preview
@Component
struct MTBottom {
  build() {
    Row () {
      Row() {
        // 小哥的显示
        Badge({
          value: '0',
          position: BadgePosition.Right,
          style: {
            badgeSize: 18
          }
        }){
           Image($r("app.media.ic_public_cart"))
             .width(47)
             .height(69)
             .position({
               y: -20
             })
        }
        .margin({
          left: 25,
          right: 10
        })
        // 显示费用
        Column() {
          Text(){
            // span imageSpan
            Span("¥")
              .fontSize(12)
            Span("0.00")
              .fontSize(24)
          }
            .fontColor($r("app.color.white"))
          Text("预估另需配送费¥5元")
            .fontColor($r("app.color.search_font_color"))
            .fontSize(14)
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)

        Text("去结算")
          .height(50)
          .width(100)
          .backgroundColor($r("app.color.main_color"))
          .textAlign(TextAlign.Center)
          .borderRadius({
            topRight: 25,
            bottomRight: 25
          })
      }
      .height(50)
      .backgroundColor($r("app.color.bottom_back"))
      .width('100%')
      .borderRadius(25)
    }
    .width('100%')
    .padding({
      left: 20,
      right: 20,
      bottom: 20
    })
  }
}
export default MTBottom

3. 顶部结构-MTTop(复制粘贴)

在这里插入图片描述

@Component
struct MTTop {
  @Builder
  NavItem(active: boolean, title: string, subTitle?: string) {
    Column() {
      Text() {
        Span(title)
        if (subTitle) {
          Span(' ' + subTitle)
            .fontSize(10)
            .fontColor(active ? $r("app.color.black") : $r("app.color.un_select_color"))
        }
      }.layoutWeight(1)
      .fontColor(active ? $r("app.color.black") : $r("app.color.un_select_color"))
      .fontWeight(active ? FontWeight.Bold : FontWeight.Normal)

      Text()
        .height(1)
        .width(20)
        .margin({ left: 6 })
        .backgroundColor(active ? $r("app.color.select_border_color") : 'transparent')
    }
    .width(73)
    .alignItems(HorizontalAlign.Start)
    .padding({ top: 3 })
  }
  build() {
    Row() {
      this.NavItem(true, '点菜')
      this.NavItem(false, '评价', '1796')
      this.NavItem(false, '商家')
      Row() {
        Image($r('app.media.ic_public_search'))
          .width(14)
          .aspectRatio(1)
          .fillColor($r("app.color.search_font_color"))
        Text('请输入菜品名称')
          .fontSize(12)
          .fontColor($r("app.color.search_back_color"))
      }
      .backgroundColor($r("app.color.search_back_color"))
      .height(25)
      .borderRadius(13)
      .padding({ left: 5, right: 5 })
      .layoutWeight(1)

    }
    .padding({ left: 15, right: 15 })
    .height(40)
    .border({ width: { bottom: 0.5 }, color: $r("app.color.top_border_color") })
  }
}

export default MTTop

4. 页面结构-商品菜单和商品列表

在这里插入图片描述

  • 抽提MTFoodItem组件(粘贴)
@Preview
@Component
struct MTFoodItem {
  build() {
    Row() {
      Image('https://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/meituan/1.jpg')
        .width(90)
        .aspectRatio(1)
      Column({ space: 5 }) {
        Text('小份酸汤莜面鱼鱼+肉夹馍套餐')
          .textOverflow({
            overflow: TextOverflow.Ellipsis,
          })
          .maxLines(2)
          .fontWeight(600)
        Text('酸汤莜面鱼鱼,主料:酸汤、莜面 肉夹馍,主料:白皮饼、猪肉')
          .textOverflow({
            overflow: TextOverflow.Ellipsis,
          })
          .maxLines(1)
          .fontSize(12)
          .fontColor($r("app.color.food_item_second_color"))
        Text('点评网友推荐')
          .fontSize(10)
          .backgroundColor($r("app.color.food_item_label_color"))
          .fontColor($r("app.color.font_main_color"))
          .padding({ top: 2, bottom: 2, right: 5, left: 5 })
          .borderRadius(2)
        Text() {
          Span('月销售40')
          Span(' ')
          Span('好评度100%')
        }
        .fontSize(12)
        .fontColor($r("app.color.black"))

        Row() {
          Text() {
            Span('¥ ')
              .fontColor($r("app.color.font_main_color"))
              .fontSize(10)
            Span('34.23')
              .fontColor($r("app.color.font_main_color"))
              .fontWeight(FontWeight.Bold)
          }
        }
        .justifyContent(FlexAlign.SpaceBetween)
        .width('100%')
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)
      .padding({ left: 10, right: 10 })
    }
    .padding(10)
    .alignItems(VerticalAlign.Top)
  }
}
export default MTFoodItem
  • 在MTMain中使用
import MTFoodItem from './MTFoodItem'

@Component
struct MTMain {
  list: string[] = ['一人套餐', '特色烧烤', '杂粮主食']
  @State
  activeIndex: number = 0
  build() {
    Row() {
      Column() {
        ForEach(this.list, (item: string, index: number) => {
          Text(item)
            .height(50)
            .width('100%')
            .textAlign(TextAlign.Center)
            .fontSize(14)
            .backgroundColor(this.activeIndex === index ? $r("app.color.white") : $r("app.color.left_back_color"))
            .onClick(() => {
              this.activeIndex = index
            })
        })
      }
      .width(90)

      //   右侧内容
      List() {
        ForEach([1,2,3,4,5,6,7,8,9], () => {
          ListItem() {
            MTFoodItem()
          }
        })
      }.layoutWeight(1)
      .backgroundColor('#fff')
      .padding({
        bottom: 80
      })
    }
    .layoutWeight(1)
    .alignItems(VerticalAlign.Top)
    .width('100%')
  }
}
export default MTMain

image.png

5. 页面结构-购物车

在这里插入图片描述

  • 新建MTCart组件
import MTCartItem from './MTCartItem'

@Component
struct MTCart {
  build() {
    Column() {
      Column() {
        Row() {
          Text('购物车')
            .fontSize(12)
            .fontWeight(600)
          Text('清空购物车')
            .fontSize(12)
            .fontColor($r("app.color.search_font_color"))
        }
        .width('100%')
        .height(40)
        .justifyContent(FlexAlign.SpaceBetween)
        .border({ width: { bottom: 0.5 }, color: $r("app.color.left_back_color") })
        .margin({ bottom: 10 })
        .padding({ left: 15, right: 15 })

        List({ space: 30 }) {
          ForEach([1,2,3,4], () => {
            ListItem() {
              MTCartItem()
            }
          })
        }
        .divider({
          strokeWidth: 0.5,
          color: $r("app.color.left_back_color")
        })
        .padding({ left: 15, right: 15, bottom: 100 })
      }
      .backgroundColor($r("app.color.white"))
      .borderRadius({
        topLeft: 16,
        topRight: 16
      })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.End)
    .backgroundColor('rgba(0,0,0,0.5)')
  }
}
export default MTCart
  • 新建MTCartItem组件(粘贴)
@Component
struct MTCartItem {
  build() {
    Row() {
      Image('https://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/meituan/4.jpeg')
        .width(60)
        .aspectRatio(1)
        .borderRadius(8)
      Column({ space: 5 }) {
        Text('小份酸汤莜面鱼鱼+肉夹馍套餐')
          .fontSize(14)
          .textOverflow({
            overflow: TextOverflow.Ellipsis
          })
          .maxLines(2)
        Row() {
          Text() {
            Span('¥ ')
              .fontColor($r("app.color.font_main_color"))
              .fontSize(10)
            Span('34.23')
              .fontColor($r("app.color.font_main_color"))
              .fontWeight(FontWeight.Bold)
          }
        }
        .justifyContent(FlexAlign.SpaceBetween)
        .width('100%')
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)
      .padding({ left: 10, right: 10 })
    }
    .alignItems(VerticalAlign.Top)
  }
}
export default MTCartItem
  • 在MTIndex.ets中声明管控显示购物车变量
@Provide showCart: boolean = false
  • 在MTIndex.ets中控制显示
import MTBottom from './components/MTBottom'
import MTCart from './components/MTCart'
import MTMain from './components/MTMain'
import MTTop from './components/MTTop'

@Entry
@Component
struct MTIndex {
  @Provide showCart: boolean = false
  build() {
    Column() {
      Stack({ alignContent: Alignment.Bottom }) {
        Column() {
          MTTop()
          MTMain()
        }
        .height("100%")
        if(this.showCart) {
          MTCart()
        }
        MTBottom()

      }.layoutWeight(1)

    }
    .width('100%')
    .height("100%")
    .backgroundColor($r("app.color.white"))
  }
}

:::success
这里MTCart要放在MTBottom前面 利用层级的先后关系实现底部内容挡在购物车前面的效果
:::

  • 点击购物车图标显示隐藏购物车-MTBottom.ets
@Component
struct MTBottom {
  @Consume
  showCart: boolean
  build() {
    Row() {
      Row() {
        Badge({
          value: '0',
          position: BadgePosition.Right,
          style: { badgeSize: 18 }
        }) {
          Image($r("app.media.ic_public_cart"))
            .width(47)
            .height(69)
            .position({ y: -19 })
        }
        .width(50)
        .height(50)
        .margin({ left: 25, right: 10 })
        .onClick(() => {
           this.showCart = !this.showCart
        })

        Column() {
          Text() {
            Span('¥')
              .fontColor('#fff')
              .fontSize(12)
            Span('0.00')
              .fontColor('#fff')
              .fontSize(24)
          }

          Text('预估另需配送费 ¥5')
            .fontSize(12)
            .fontColor('#999')
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Start)

        Text('去结算')
          .backgroundColor($r("app.color.main_color"))
          .alignSelf(ItemAlign.Stretch)
          .padding(15)
          .borderRadius({
            topRight: 25,
            bottomRight: 25
          })
      }
      .height(50)
      .width('100%')
      .backgroundColor($r("app.color.bottom_back"))
      .borderRadius(25)
    }
    .width('100%')
    .padding({ left: 20, right: 20, bottom: 20 })
  }
}
export default MTBottom

在这里插入图片描述

  • 返回键关闭购物车

组件生命周期有一个方法叫onBackPress,可以在Index监听这个方法进行关闭

onBackPress(): boolean | void {
    this.showCart = false
  }

6. 业务逻辑-渲染商品菜单和列表

  • 准备结构返回的数据模型(粘贴)
export class FoodItem {
  id: number = 0
  name: string = ""
  like_ratio_desc: string = ""
  food_tag_list: string[] = []
  price: number = 0
  picture: string = ""
  description: string = ""
  tag: string = ""
  month_saled: number = 0
  count: number = 0
}


export class Category {
  tag: string = ""
  name: string =""
  foods: FoodItem[] = []
}
  • api/index.ets 使用 http 发送请求,获取数据
import { http } from '@kit.NetworkKit'
export class FoodItem {
  id: number = 0
  name: string = ""
  like_ratio_desc: string = ""
  food_tag_list: string[] = []
  price: number = 0
  picture: string = ""
  description: string = ""
  tag: string = ""
  month_saled: number = 0
  count: number = 0
}
export class Category {
  tag: string = ""
  name: string =""
  foods: FoodItem[] = []
}
export const getData =async () => {
  const req = http.createHttp()
  const res = await req.request('https://zhousg.atomgit.net/harmonyos-next/takeaway.json')
  return JSON.parse(res.result as string) as Category[]
}
  • 在MTMain.ets中获取数据
@State
list: Category[] = []
  async aboutToAppear(){
     this.list = await getAllData()
  }
  • MTMain循环内容渲染
import { getAllData } from '../api'
import { Category, FoodItem } from '../models'
import MTFoodItem from './MTFoodItem'

@Component
struct MTMain {
  @State
  activeIndex: number = 0
  @State
  list: Category[] = []
  async aboutToAppear(){
     this.list = await getAllData()
  }
  build() {
    Row() {
      Column() {
        ForEach(this.list, (item: Category, index: number) => {
          Text(item.name)
            .height(50)
            .width('100%')
            .textAlign(TextAlign.Center)
            .fontSize(14)
            .backgroundColor(this.activeIndex === index ? $r("app.color.white") : $r("app.color.left_back_color"))
            .onClick(() => {
              this.activeIndex = index
            })
        })
      }
      .width(90)

      //   右侧内容
      List() {
        ForEach(this.list[this.activeIndex]?.foods || [], (item: FoodItem) => {
          ListItem() {
            MTFoodItem({ item })
          }
        })
      }.layoutWeight(1)
      .backgroundColor($r("app.color.white"))
      .padding({
        bottom: 80
      })
    }
    .layoutWeight(1)
    .alignItems(VerticalAlign.Top)
    .width('100%')
  }
}
export default MTMain
  • MTFoodItem组件使用属性接收数据
import { FoodItem } from '../models'

@Preview
@Component
struct MTFoodItem {
  item: FoodItem = new FoodItem()

  build() {
    Row() {
      Image(this.item.picture)
        .width(90)
        .aspectRatio(1)
      Column({ space: 5 }) {
        Text(this.item.name)
          .textOverflow({
            overflow: TextOverflow.Ellipsis,
          })
          .maxLines(2)
          .fontWeight(600)
        Text(this.item.description)
          .textOverflow({
            overflow: TextOverflow.Ellipsis,
          })
          .maxLines(1)
          .fontSize(12)
          .fontColor($r("app.color.food_item_second_color"))
        ForEach(this.item.food_tag_list, (tag: string) => {
          Text(tag)
            .fontSize(10)
            .backgroundColor($r("app.color.food_item_label_color"))
            .fontColor($r("app.color.font_main_color"))
            .padding({ top: 2, bottom: 2, right: 5, left: 5 })
            .borderRadius(2)
        })

        Text() {
          Span('月销售' + this.item.month_saled)
          Span(' ')
          Span(this.item.like_ratio_desc)
        }
        .fontSize(12)
        .fontColor($r("app.color.black"))

        Row() {
          Text() {
            Span('¥ ')
              .fontColor($r("app.color.font_main_color"))
              .fontSize(10)
            Span(this.item.price?.toString())
              .fontColor($r("app.color.font_main_color"))
              .fontWeight(FontWeight.Bold)
          }
        }
        .justifyContent(FlexAlign.SpaceBetween)
        .width('100%')
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)
      .padding({ left: 10, right: 10 })
    }
    .padding(10)
    .alignItems(VerticalAlign.Top)
  }
}

export default MTFoodItem

在这里插入图片描述

7. 业务逻辑-封装新增加菜和减菜组件

在这里插入图片描述

  • 准备组件的静态结构(粘贴)
@Preview
@Component
struct MTAddCut {
  build() {
    Row({ space: 8 }) {
      Row() {
        Image($r('app.media.ic_screenshot_line'))
          .width(10)
          .aspectRatio(1)
      }.width(16)
      .aspectRatio(1)
      .justifyContent(FlexAlign.Center)
      .backgroundColor($r("app.color.white"))
      .borderRadius(4)
      .border({ width: 0.5 , color: $r("app.color.main_color")})
      Text('0').fontSize(14)
      Row() {
        Image($r('app.media.ic_public_add_filled'))
          .width(10)
          .aspectRatio(1)

      }.width(16)
      .aspectRatio(1)
      .justifyContent(FlexAlign.Center)
      .backgroundColor($r("app.color.main_color"))
      .borderRadius(4)
    }
  }
}
export default MTAddCut
  • 放置在MTFoodItem中

在这里插入图片描述

8. 业务逻辑-加入购物车

:::info
设计购物车模型
我们需要持久化的数据,使用 PersistentStorage.persistProp(CART_KEY, [])
:::

  • 购物车数据更新
import { FoodItem } from '../api'
PersistentStorage.persistProp('cart_list', [])
export class CartStore {
  static addCutCart(item: FoodItem, flag: boolean = true) {
    const list = AppStorage.get<FoodItem[]>('cart_list')!
    const index = list.findIndex(listItem => listItem.id === item.id)
    if (flag) {
      if (index < 0) {
        item.count = 1
        //   新增
        list.unshift(item)
      } else {
        list[index].count++
        // 让第一层发生变化
        list.splice(index, 1,list[index])
      }
    } else {
      list[index].count--
      // 如果减到0就删掉
      if (list[index].count === 0){
        list.splice(index, 1)
      }else{
        // 让第一层发生变化
        list.splice(index, 1,list[index])
      }
    }
    AppStorage.setOrCreate('cart_list',list)
  }
}

:::success
切记:改第二层UI是不会响应式更新的,所以一定是数组自身,或者数组的第一层要变化才行!
:::

  • 现在我们有了加菜-减菜的方法-也可以调用加入菜品
  • 购物车视图更新
    :::info
    在MTCart中使用StorageLink直接取出购物车数据进行双向绑定
    :::
import { FoodItem } from '../api'
import MTAddCut from './MTAddCut'

@Component
struct MTCartItem {
  item:FoodItem = new FoodItem()
  build() {
    Row() {
      Image(this.item.picture)
        .width(60)
        .aspectRatio(1)
        .borderRadius(8)
      Column({ space: 5 }) {
        Text(this.item.name)
          .fontSize(14)
          .textOverflow({
            overflow: TextOverflow.Ellipsis
          })
          .maxLines(2)
        Row() {
          Text() {
            Span('¥ ')
              .fontColor($r("app.color.font_main_color"))
              .fontSize(10)
            Span(this.item.price.toString())
              .fontColor($r("app.color.font_main_color"))
              .fontWeight(FontWeight.Bold)
          }
          MTAddCut({
            food:this.item
          })
        }
        .justifyContent(FlexAlign.SpaceBetween)
        .width('100%')
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)
      .padding({ left: 10, right: 10 })
    }
    .alignItems(VerticalAlign.Top)
  }
}
export default MTCartItem
  • MTCartItem中使用item
import { FoodItem } from '../api'
import MTAddCut from './MTAddCut'

@Component
struct MTCartItem {
  item:FoodItem = new FoodItem()
  build() {
    Row() {
      Image(this.item.picture)
        .width(60)
        .aspectRatio(1)
        .borderRadius(8)
      Column({ space: 5 }) {
        Text(this.item.name)
          .fontSize(14)
          .textOverflow({
            overflow: TextOverflow.Ellipsis
          })
          .maxLines(2)
        Row() {
          Text() {
            Span('¥ ')
              .fontColor($r("app.color.font_main_color"))
              .fontSize(10)
            Span(this.item.price.toString())
              .fontColor($r("app.color.font_main_color"))
              .fontWeight(FontWeight.Bold)
          }
          MTAddCut({
            food:this.item
          })
        }
        .justifyContent(FlexAlign.SpaceBetween)
        .width('100%')
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)
      .padding({ left: 10, right: 10 })
    }
    .alignItems(VerticalAlign.Top)
  }
}
export default MTCartItem

9.加菜和减菜按钮加入购物车

:::info

  1. 使用AppStorage接收所有购物车数据

  2. 根据数量显示减菜按钮和数量元素
    :::

import { FoodItem } from '../api'
import { CarCalcClass } from '../utils/CartCalcClass'

@Preview
@Component
struct MTAddCut {
  @StorageLink('cart_list')
  cartList: FoodItem[] = []
  food: FoodItem = new FoodItem()

  getCount(): number {
    const index = this.cartList.findIndex(listItem => listItem.id === this.food.id)
    return index < 0 ? 0 : this.cartList[index].count
  }

  build() {
    Row({ space: 8 }) {
      Row() {
        Image($r('app.media.ic_screenshot_line'))
          .width(10)
          .aspectRatio(1)
      }
      .width(16)
      .aspectRatio(1)
      .justifyContent(FlexAlign.Center)
      .backgroundColor($r("app.color.white"))
      .borderRadius(4)
      .border({ width: 0.5, color: $r("app.color.main_color") })
      .visibility(this.getCount()>0?Visibility.Visible:Visibility.Hidden)
      .onClick(() => {
        CartStore.addCutCart(this.food, false)
      })

      Text(this.getCount().toString()).fontSize(14)
      Row() {
        Image($r('app.media.ic_public_add_filled'))
          .width(10)
          .aspectRatio(1)

      }
      .width(16)
      .aspectRatio(1)
      .justifyContent(FlexAlign.Center)
      .backgroundColor($r("app.color.main_color"))
      .borderRadius(4)
      .onClick(() => {
        CartStore.addCutCart(this.food)
      })
    }
  }
}

export default MTAddCut
  • 给AddCutCart传入Item
 MTAddCut({ item: this.item })

:::success
在MTCartItem中同样需要放置AddCutCart
:::

  MTAddCut({ item: this.item })

:::success
解决在购物车中添加图片卡的问题
:::

ForEach(this.cartList, (item: FoodItem) => {
            ListItem() {
              MTCartItem({ item  })
            }
          }, (item: FoodItem) => item.id.toString())

10.清空购物车

 Text('清空购物车')
            .fontSize(12)
            .fontColor('#999')
            .onClick(() => {
              CartStore.clearCart()
            })
  • 清空方法
 static clearCarts () {
    AppStorage.set<FoodItem[]>("cart_list", [])
  }

11.底部内容汇总

image.png

import { FoodItem } from '../api'

@Component
struct MTBottom {
  @Consume
  showCart: boolean
  @StorageLink('cart_list')
  cartList: FoodItem[] = []
  getAllCount () {
    return this.cartList.reduce((preValue, item) => preValue + item.count, 0).toString()
  }
  getAllPrice () {
    return this.cartList.reduce((preValue, item) => preValue + item.count * item.price, 0).toFixed(2)
  }
  build() {
    Row() {
      Row() {
        Badge({
          value: '0',
          position: BadgePosition.Right,
          style: {
            badgeSize: 18
          }
        }) {
          Image($r('app.media.ic_public_cart'))
            .width(48)
            .height(70)
            .position({
              y: -20,
            })
        }
        .margin({
          left:25,
          right:10
        })
        .onClick(() => {
          this.showCart = !this.showCart
        })
        Column() {
          Text(){
            // span imageSpan
            Span("¥")
              .fontSize(12)
            Span("0.00")
              .fontSize(24)
          }
          .fontColor($r("app.color.white"))
          Text("预估另需配送费¥5元")
            .fontColor($r("app.color.search_font_color"))
            .fontSize(14)
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)

        Text("去结算")
          .height(50)
          .width(100)
          .backgroundColor($r("app.color.main_color"))
          .textAlign(TextAlign.Center)
          .borderRadius({
            topRight: 25,
            bottomRight: 25
          })
      }
      .height(50)
      .width('100%')
      .backgroundColor($r('app.color.bottom_back'))
      .borderRadius(25)
    }
    .width('100%')
    .padding(20)
  }
}

export default MTBottom

美团案例完整代码

  • MTIndex.ets
import { Category, getData } from './models'
import MTBottom from './components/MTBottom'
import MTCart from './components/MTCart'
import MTMain from './components/MTMain'
import MTTop from './components/MTTop'
import { promptAction } from '@kit.ArkUI'

@Entry
@Component
struct MTIndex {
  @Provide showCart: boolean = false
  @State
  list: Category[] = []

  onBackPress(): boolean | void {
    this.showCart = false
  }

  async aboutToAppear() {
    this.list = await getData()

  }

  build() {
    Stack({ alignContent: Alignment.Bottom }) {
      Column() {
        MTTop()
        MTMain({
          list: this.list
        })
      }
      .height('100%')
      .width('100%')

      if (this.showCart) {
        MTCart()
      }
      MTBottom()
    }
    .width('100%')
    .height('100%')
  }
}
  • components/MTTop.ets
@Component
struct MTTop {
  @Builder
  NavItem(active: boolean, title: string, subTitle?: string) {
    Column() {
      Text() {
        Span(title)
        if (subTitle) {
          Span(' ' + subTitle)
            .fontSize(10)
            .fontColor(active ? $r("app.color.black") : $r("app.color.un_select_color"))
        }
      }.layoutWeight(1)
      .fontColor(active ? $r("app.color.black") : $r("app.color.un_select_color"))
      .fontWeight(active ? FontWeight.Bold : FontWeight.Normal)

      Text()
        .height(1)
        .width(20)
        .margin({ left: 6 })
        .backgroundColor(active ? $r("app.color.select_border_color") : 'transparent')
    }
    .width(73)
    .alignItems(HorizontalAlign.Start)
    .padding({ top: 3 })
  }
  build() {
    Row() {
      this.NavItem(true, '点菜')
      this.NavItem(false, '评价', '1796')
      this.NavItem(false, '商家')
      Row() {
        Image($r('app.media.ic_public_search'))
          .width(14)
          .aspectRatio(1)
          .fillColor($r("app.color.search_font_color"))
        Text('请输入菜品名称')
          .fontSize(12)
          .fontColor($r("app.color.search_back_color"))
      }
      .backgroundColor($r("app.color.search_back_color"))
      .height(25)
      .borderRadius(13)
      .padding({ left: 5, right: 5 })
      .layoutWeight(1)

    }
    .padding({ left: 15, right: 15 })
    .height(40)
    .border({ width: { bottom: 0.5 }, color: $r("app.color.top_border_color") })
  }
}

export default MTTop
  • components/MTMain.ets
import { Category, FoodItem } from '../models'
import MTFoodItem from './MTFoodItem'

@Component
struct MTMain {
  @Link
  list:Category[]

  @State
  activeIndex:number = 0
  build() {
    Row() {
      Column() {
        ForEach(this.list, (item: Category,index:number) => {
          Text(item.name)
            .width('100%')
            .fontSize(14)
            .textAlign(TextAlign.Center)
            .height(50)
            .backgroundColor(this.activeIndex===index?$r("app.color.white") : $r("app.color.left_back_color"))
            .onClick(() => {
              this.activeIndex = index
            })
        })
      }
      .width(90)
      .height('100%')
      .backgroundColor($r("app.color.left_back_color"))

      //   右侧内容
      List() {
        if(this.list.length>0){
          ForEach(this.list[this.activeIndex].foods, (food:FoodItem) => {
            ListItem() {
              MTFoodItem({
                food:food
              })
            }
          })
        }else{
          ListItem(){
            Text('暂无商品~')
              .width('100%')
              .padding(20)
              .textAlign(TextAlign.Center)
              .fontColor($r('app.color.left_back_color'))
          }
        }
      }
      .layoutWeight(1)
      .backgroundColor('#fff')
      .padding({
        bottom: 80
      })
    }
    .width('100%')
    .layoutWeight(1)
    .alignItems(VerticalAlign.Top)
  }
}

export default MTMain

  • components/MTBottom.ets
import { FoodItem } from '../models'

@Component
struct MTBottom {
  @Consume
  showCart: boolean
  @StorageLink('cart_list')
  cartList: FoodItem[] = []
  getAllCount () {
    return this.cartList.reduce((preValue, item) => preValue + item.count, 0).toString()
  }
  getAllPrice () {
    return this.cartList.reduce((preValue, item) => preValue + item.count * item.price, 0).toFixed(2)
  }
  build() {
    Row() {
      Row() {
        Badge({
          value: '0',
          position: BadgePosition.Right,
          style: {
            badgeSize: 18
          }
        }) {
          Image($r('app.media.ic_public_cart'))
            .width(48)
            .height(70)
            .position({
              y: -20,
            })
        }
        .margin({
          left:25,
          right:10
        })
        .onClick(() => {
          this.showCart = !this.showCart
        })
        Column() {
          Text(){
            // span imageSpan
            Span("¥")
              .fontSize(12)
            Span("0.00")
              .fontSize(24)
          }
          .fontColor($r("app.color.white"))
          Text("预估另需配送费¥5元")
            .fontColor($r("app.color.search_font_color"))
            .fontSize(14)
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)

        Text("去结算")
          .height(50)
          .width(100)
          .backgroundColor($r("app.color.main_color"))
          .textAlign(TextAlign.Center)
          .borderRadius({
            topRight: 25,
            bottomRight: 25
          })
      }
      .height(50)
      .width('100%')
      .backgroundColor($r('app.color.bottom_back'))
      .borderRadius(25)
    }
    .width('100%')
    .padding(20)
  }
}

export default MTBottom

  • components/MTCart.ets
import { FoodItem } from '../models'
import MTCartItem from './MTCartItem'
@Component
struct MTCart {
  @Consume
  showCart:boolean
  @StorageLink('cart_list')
  cartList:FoodItem[] = []
  build() {
    Column() {
      Blank()
        .backgroundColor('rgba(0,0,0,0.5)')
        .onClick(()=>{
          this.showCart = false
        })
      Column() {
        Row() {
          Text('购物车')
            .fontSize(12)
            .fontWeight(600)
          Text('清空购物车')
            .fontSize(12)
            .fontColor($r("app.color.search_font_color"))
        }
        .width('100%')
        .height(40)
        .justifyContent(FlexAlign.SpaceBetween)
        .border({ width: { bottom: 0.5 }, color: $r("app.color.left_back_color") })
        .margin({ bottom: 10 })
        .padding({ left: 15, right: 15 })

        List({ space: 30 }) {
          ForEach(this.cartList, (item:FoodItem) => {
            ListItem() {
              MTCartItem({
                item:item
              })
            }
          },(item:FoodItem)=>item.id.toString())
        }
        .divider({
          strokeWidth: 0.5,
          color: $r("app.color.left_back_color")
        })
        .padding({ left: 15, right: 15, bottom: 100 })
      }
      .backgroundColor($r("app.color.white"))
      .borderRadius({
        topLeft: 16,
        topRight: 16
      })
    }
    .height('100%')
    .width('100%')
  }
}
export default MTCart
  • components/MTFoodItem.ets
import { FoodItem } from '../models'
import MTAddCut from './MTAddCut'

@Preview
@Component
struct MTFoodItem {
  food:FoodItem = new FoodItem()
  build() {
    Row() {
      Image(this.food.picture)
        .width(90)
        .aspectRatio(1)
      Column({ space: 5 }) {
        Text(this.food.name)
          .textOverflow({
            overflow: TextOverflow.Ellipsis,
          })
          .maxLines(2)
          .fontWeight(600)
        Text(this.food.description)
          .textOverflow({
            overflow: TextOverflow.Ellipsis,
          })
          .maxLines(1)
          .fontSize(12)
          .fontColor($r("app.color.food_item_second_color"))
        Text(this.food.tag)
          .fontSize(10)
          .backgroundColor($r("app.color.food_item_label_color"))
          .fontColor($r("app.color.font_main_color"))
          .padding({ top: 2, bottom: 2, right: 5, left: 5 })
          .borderRadius(2)
        Text() {
          Span('月销售'+this.food.month_saled)
          Span(' ')
          Span(`好评度${this.food.like_ratio_desc}%`)
        }
        .fontSize(12)
        .fontColor($r("app.color.black"))

        Row() {
          Text() {
            Span('¥ ')
              .fontColor($r("app.color.font_main_color"))
              .fontSize(10)
            Span(this.food.price.toString())
              .fontColor($r("app.color.font_main_color"))
              .fontWeight(FontWeight.Bold)
          }
          MTAddCut({
            food:this.food
          })
        }
        .justifyContent(FlexAlign.SpaceBetween)
        .width('100%')
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)
      .padding({ left: 10, right: 10 })
    }
    .padding(10)
    .alignItems(VerticalAlign.Top)
  }
}
export default MTFoodItem
  • components/MTCartItem.ets
import { FoodItem } from '../models'
import MTAddCut from './MTAddCut'

@Component
struct MTCartItem {
  item:FoodItem = new FoodItem()
  build() {
    Row() {
      Image(this.item.picture)
        .width(60)
        .aspectRatio(1)
        .borderRadius(8)
      Column({ space: 5 }) {
        Text(this.item.name)
          .fontSize(14)
          .textOverflow({
            overflow: TextOverflow.Ellipsis
          })
          .maxLines(2)
        Row() {
          Text() {
            Span('¥ ')
              .fontColor($r("app.color.font_main_color"))
              .fontSize(10)
            Span(this.item.price.toString())
              .fontColor($r("app.color.font_main_color"))
              .fontWeight(FontWeight.Bold)
          }
          MTAddCut({
            food:this.item
          })
        }
        .justifyContent(FlexAlign.SpaceBetween)
        .width('100%')
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)
      .padding({ left: 10, right: 10 })
    }
    .alignItems(VerticalAlign.Top)
  }
}
export default MTCartItem
  • components/MTAddCut.ets
import { FoodItem } from '../models'
import { CartStore } from '../utils/CartCalcClass'

@Preview
@Component
struct MTAddCut {
  @StorageLink('cart_list')
  cartList: FoodItem[] = []
  food: FoodItem = new FoodItem()

  getCount(): number {
    const index = this.cartList.findIndex(listItem => listItem.id === this.food.id)
    return index < 0 ? 0 : this.cartList[index].count
  }

  build() {
    Row({ space: 8 }) {
      Row() {
        Image($r('app.media.ic_screenshot_line'))
          .width(10)
          .aspectRatio(1)
      }
      .width(16)
      .aspectRatio(1)
      .justifyContent(FlexAlign.Center)
      .backgroundColor($r("app.color.white"))
      .borderRadius(4)
      .border({ width: 0.5, color: $r("app.color.main_color") })
      .visibility(this.getCount()>0?Visibility.Visible:Visibility.Hidden)
      .onClick(() => {
        CartStore.addCutCart(this.food, false)
      })

      Text(this.getCount().toString()).fontSize(14)
      Row() {
        Image($r('app.media.ic_public_add_filled'))
          .width(10)
          .aspectRatio(1)

      }
      .width(16)
      .aspectRatio(1)
      .justifyContent(FlexAlign.Center)
      .backgroundColor($r("app.color.main_color"))
      .borderRadius(4)
      .onClick(() => {
        CartStore.addCutCart(this.food)
      })
    }
  }
}

export default MTAddCut
  • api/index.ets
import { http } from '@kit.NetworkKit'
import { Category } from '../models'

export const  getAllData = async () => {
  const req = http.createHttp()
  const res = await  req.request(" https://zhousg.atomgit.net/harmonyos-next/takeaway.json")
  return JSON.parse(res.result as string) as Category[]
}
  • models/index.ets
export class FoodItem {
  id: number = 0
  name: string = ""
  like_ratio_desc: string = ""
  food_tag_list: string[] = []
  price: number = 0
  picture: string = ""
  description: string = ""
  tag: string = ""
  month_saled: number = 0
  count: number = 0
}
export class Category {
  tag: string = ""
  name: string =""
  foods: FoodItem[] = []
}
  • utils/index.ets
import { FoodItem } from '../models'
PersistentStorage.persistProp('cart_list', [])
export class CartStore {
  static addCutCart(item: FoodItem, flag: boolean = true) {
    const list = AppStorage.get<FoodItem[]>('cart_list')!
    const index = list.findIndex(listItem => listItem.id === item.id)
    if (flag) {
      if (index < 0) {
        item.count = 1
        //   新增
        list.unshift(item)
      } else {
        list[index].count++
        list.splice(index, 1,list[index])
      }
    } else {
      list[index].count--
      // 如果减到0就删掉
      if (list[index].count === 0){
        list.splice(index, 1)
      }else{
        list.splice(index, 1,list[index])
      }
    }
    AppStorage.setOrCreate('cart_list',list)
  }
  static clearCarts () {
    AppStorage.set<FoodItem[]>("cart_list", [])
  }
}

http://www.kler.cn/a/387731.html

相关文章:

  • DNS面临的4大类共计11小类安全风险及防御措施
  • 【算法】——二分查找合集
  • win32 / WTL 开发多线程应用,子线程传递大对象给UI线程(主窗口)的方法
  • 鸿蒙next版开发:相机开发-元数据(ArkTS)
  • HarmonyOS Next星河版笔记--界面开发(4)
  • hadoop大数据平台
  • 在IntelliJ IDEA中创建带子模块的SpringBoot工程
  • Spring Boot 与 Vue 共筑高校网上订餐卓越平台
  • js实现各种经典排序算法
  • TG机器人链游开发项目:迈向去中心化游戏新时代
  • ffmpeg视频滤镜:缓入缓出-fade
  • Web安全之SQL注入---基础
  • 大厂社招3年-力扣热点高频刷题记录(已更新100+道热点题)
  • 【算法】排序算法
  • 使用layui过程中的问题
  • STM32各模块
  • 21. 评估架构
  • 快速上手Cellranger
  • 股票投资学习路线图
  • 西南科技大学竞赛与实践——实验一Paillier算法及其实现
  • Spring-Day8
  • Gradle命令编译Android Studio工程项目并签名
  • OJ算法练习(双指针篇)
  • django+postgresql
  • 题目:Wangzyy的卡牌游戏
  • 前端入门一之CSS知识详解