1. 自定义组件基础

news2025/2/24 7:40:16

相关资源:

  1. 📎day10 图片素材.zip

1. 自定义组件基础

概念:在ArkUI中由框架直接提供的称为系统组件 -> Column,Button等,由开发者定义的称为自定义组件

作用:自定义组件可以对 UI和业务逻辑进行封装,从而复用组件

自定义组件语法:

  • struct:自定义组件基于struct实现,struct + 自定义组件名 + {...}的组合构成自定义组件,不能有继承关系。对于struct的实例化,可以省略new。
  • @Component:@Component装饰器仅能装饰struct关键字声明的数据结构。
  • build()函数:build()函数用于定义自定义组件的声明式UI描述,自定义组件必须定义build()函数。
  • @Entry:@Entry装饰的自定义组件将作为UI页面的入口。在单个UI页面中,最多可以使用@Entry装饰一个自定义组件。
  • @Preview:如果想要单独预览组件,可以使用@Preview 进行装饰

注意:自定义组件必须导出后,才能在其他组件中导入使用

export struct SonCom {}

1.1. 成员函数/变量

自定义组件除了必须要实现build()函数外,还可以定义其他的成员函数,以及成员变量

注意:

  1. 成员函数、变量均为私有
  2. 可以在父组件调用子组件时向成员变量传递数据
// HelloComponent.ets
@Component
export struct HelloComponent {
  // 成员变量
  info: string = '感觉自己闷闷哒'
  // 成员变量也可以是函数
  sayHello = ()=>{}
  // 状态变量
  @State message: string = 'Hello, World!';

  // 成员函数
  sayHi() {
    console.log('你好呀')
  }

  build() {
    // HelloComponent自定义组件组合系统组件Row和Text
    Column() {
      Text(this.message)
      Text(this.info)
      Button('修改数据')
        .onClick(() => {
          this.info = '(*  ̄3)(ε ̄ *)'
          this.message = 'Hello,ArkTS'
          this.sayHi()
          this.sayHello()
        })

    }
  }
}
// 页面的.ets
import { HelloComponent } from './components/HelloComponent'

@Entry
@Component
struct CustomComponentDemo {
  build() {
    Column() {
      // 使用组件内部定义的初始值
      HelloComponent()
      // 使用传入的值,覆盖子组件的默认值
      HelloComponent({ info: '你好', message: 'ArkTS' })
      // 函数也可以传入
      HelloComponent({ sayHello:()=>{ console.log('传入的逻辑') } })
    }
  }
}

@Component
struct MyCom {
  // 定义函数用来接收父传入的函数
  sayHell:(p:string)=>void = ()=>{}
  
  build() {
    Column() {
     Button('点我向父组件传值')
      .onClick(()=>{
        // 调用父组件的函数
        this.sayHell('我是子组件传给父组件的数据')
      })
  }
}


@Entry
@Component
struct Index {
  build() {
    Column() {
    
      // 函数也可以传入
      MyCom({ 
        // 接收子组件传入的数据
        sayHell:(msg:string)=>{ console.log('接收子组件传入的数据:'+msg) }
      })
    }
  }
}

1.2. 通用样式事件

自定义组件可以通过点语法的形式设置通用样式,通用事件

子组件()
  .width(100)
  .height(100)
  .backgroundColor(Color.Orange)
  .onClick(() => {
      console.log('外部添加的点击事件')
    })

试一试:

  1. 添加自定义组件,随意设置内容
  2. 使用自定义组件,通过点语法设置通用样式
@Component
struct MyComponent2 {
  build() {
    Button(`Hello World`)
  }
}

@Entry
@Component
struct MyComponent {
  build() {
    Row() {
      MyComponent2()
        .width(200)
        .height(300)
        .backgroundColor(Color.Red)
        .onClick(() => {
            console.log('外部添加的点击事件')
          })
    }
  }
}

说明

ArkUI给自定义组件设置样式时,相当于给MyComponent2套了一个不可见的容器组件,而这些样式是设置在容器组件上的,而非直接设置给MyComponent2的Button组件。通过渲染结果我们可以很清楚的看到,背景颜色红色并没有直接生效在Button上,而是生效在Button所处的开发者不可见的容器组件上。

1.3. 案例-卡片组件

日常开发中,经常会在圆角的容器中展示不同的内容,我们一般称之为卡片,

@Component
struct PanelComp {
  title: string = ''
  more: string = ''
  clickHandler: () => void = () => {
    console.log('默认的逻辑')
  }

  build() {
    Column() {
      Row() {
        Text(this.title)
          .layoutWeight(1)
          .fontWeight(600)
        Row() {
          Text(this.more)
            .fontSize(14)
            .fontColor('#666666')
            .onClick(() => {
              this.clickHandler()
            })
          Image($r('app.media.ic_public_arrow_right'))
            .width(16)
            .fillColor('#666666')
        }
      }
      .padding(10)

      Row() {
        Text('默认内容')
      }
      .height(100)
    }
    .borderRadius(12)
    .backgroundColor('#fff')
    .width('100%')
  }
}

@Entry
@Component
struct Index {
  build() {
    Column({ space: 15 }) {
      PanelComp({
        title: '评价(2000+)', more: '好评率98%', clickHandler() {
          console.log('传入的逻辑')
        }
      })
      Row({ space: 15 }) {
        PanelComp({ title: '推荐', more: '查看全部' })
          .layoutWeight(1)

        PanelComp({ title: '体验', more: '4 条测评' })
          .layoutWeight(1)
      }

    }
    .height('100%')
    .padding(15)
    .backgroundColor('#f5f5f5')
  }
}

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

@BuilderParam 该装饰器用于声明任意UI描述的一个元素,类似slot占位符。

链接

简而言之:就是自定义组件允许外部传递 UI

// SonCom 的实现略

@Entry
@Component
struct Index {
  build() {
    Column({ space: 15 }) {
      SonCom() {
        // 直接传递进来(尾随闭包)
        Button('传入的结构')
          .onClick(() => {
            AlertDialog.show({ message: '点了 Button' })
          })
      }
    }
  }
}

2.1. 单个@BuilderParam参数

首先来看看单个的情况

使用尾随闭包的方式传入:

  • 组件内有且仅有一个使用 @BuilderParam 装饰的属性,即可使用尾随闭包
  • 内容直接在 {} 传入即可

注意:

  • 此场景下自定义组件不支持使用通用属性。
@Component
struct SonCom {
  // 1.设置默认 的 Builder,避免外部不传入
  @Builder
  defaultBuilder() {
    Text('默认的内容')
  }
  
  // 2.定义 BuilderParam 接受外部传入的 ui,并设置默认值
  @BuilderParam ContentBuilder: () => void = this.defaultBuilder



  build() {
    Column() {
      // 3. 使用 @BuilderParam 装饰的成员变量
      this.ContentBuilder()
    }
    .width(300)
    .height(200)
    .border({ width: .5 })
  }
}


// 使用自定义组件时,就可以使用如下方式传递 UI
// 不传递时会使用默认值
SonCom(){
  // 传入的 UI
}

试一试:

  1. 添加自定义组件:
    1. 定义默认的 Builder
    2. 添加BuilderParam,添加类型,并设置默认的 Builder
    3. 组件内部使用 BuilderParam
  1. 外部使用自定义组件,分别测试传递,不传递 UI 的情况

/*
 * @BuilderParam 作用:可以在子组件中提供一个变量(尾随闭包),以便接收父组件传入的【UI结构】
 * 基本使用:
 * 1. 定义尾随闭包
 * 2. 在组件中使用这个尾随闭包
 * 3. 在父组件中调用这个组件传入UI结构
 *
 * 注意点:单个写法特点:如果一个组件中,只有一个尾随闭包,那么这个尾随闭包可以不写变量名,直接使用即可
 * */

@Component
export struct MyBuilder {
  // 1. 定义一个变量,用来接收父组件传递过来的UI结构
  // 默认值
  @Builder defaultBuilder(){
    Text('默认的结构')
  }
  @BuilderParam defaultUI:() => void = this.defaultBuilder

  build() {
    Column(){
      // 2. 使用这个变量
      this.defaultUI()
    }
  }
}


import { MyBuilder } from '../views/MyBuilder'

@Entry
@Component
struct Index {
  build() {
    Column() {
      MyBuilder() {
      // 3. 调用子组件,并向子组件中传入自己定义的UI结构
        Button('按钮')
          .backgroundColor(Color.Red)
          .onClick(()=>{
            AlertDialog.show({message:'OK'})
          })
      }
    }
    .height('100%')
    .width('100%')
    .backgroundColor(Color.Pink)

  }
}

2.2. 多个@BuilderParam 参数

子组件有多个BuilderParam,必须通过参数的方式来传入

核心步骤:

  1. 自定义组件-定义:
    1. 添加多个 @BuilderParam ,并定义默认值
  1. 自定义组件-使用
    1. 通过参数的形式传入多个 Builder,比如

SonCom({ titleBuilder: this.fTitleBuilder, contentBuilder: this.fContentBuilder })

@Component
struct SonCom {
  // 由外部传入 UI
  @BuilderParam titleBuilder: () => void = this.titleDefaultBuilder
  @BuilderParam contentBuilder: () => void = this.contentDefaultBuilder

  // 设置默认 的 Builder,避免外部不传入
  @Builder
  titleDefaultBuilder() {
    Text('默认标题')
  }

  @Builder
  contentDefaultBuilder() {
    Text('默认内容')
  }

  build() {
    Column() {
      Row() {
        this.titleBuilder()
      }
      .layoutWeight(1)

      Divider()
      Row() {
        this.contentBuilder()
      }
      .layoutWeight(1)

    }
    .width(300)
    .height(200)
    .border({ width: .5 })
  }
}

@Entry
@Component
struct Index {
  
  @Builder
  fTitleBuilder() {
    Text('传入的标题')
      .fontSize(20)
      .fontWeight(600)
      .fontColor(Color.White)
      .backgroundColor(Color.Blue)
      .padding(10)
  }

  @Builder
  fContentBuilder() {
    Text('传入的标题')
      .fontSize(20)
      .fontWeight(600)
      .fontColor(Color.White)
      .backgroundColor(Color.Blue)
      .padding(10)
  }

  build() {
    Column({ space: 15 }) {
      // 指定名字传入UI结构
      SonCom({ titleBuilder: this.fTitleBuilder, contentBuilder: this.fContentBuilder })
    }
  }
}

2.3. 总结

// 定义子组件
@Component
export struct MyBuilder{ 
  @Builder defaultBuilder(){
    Text('标题默认的结构')
  }

  @BuilderParam defaultUI:() => void = this.defaultBuilder

  build() {
    Column(){
      this.defaultUI()
    }
  }
}

// 父组件调用子组件
  MyBuilder(){
    Button('按钮')
  }

// 定义子组件
@Component
export struct MyBuilder{ 
  @Builder defaultBuilder(){
    Text('标题默认的结构')
  }
  
  @Builder contentBuilder(){
    Text('内容默认的结构')
  }

  @BuilderParam defaultUI:() => void = this.defaultBuilder
  @BuilderParam contentUI:() => void = this.contentBuilder

  build() {
    Column(){
      this.defaultUI()
      this.contentUI()
    }
  }
}

// 父组件调用子组件
  @Builder defaultBuilder(){
    Button('标题0')
  }

  @Builder contentBuilder(){
    Text('内容')
  }

  MyBuilder({
    defaultUI:this.defaultBuilder,
    contentUI:this.contentBuilder
  })

2.4. 案例-卡片组件优化

使用刚刚学习的知识,让外部可以传递Builder到卡片组件内部

需求:

  1. 调整 卡片自定义组件,支持传入 UI PanelComp(){ // 此处传入 }

思路:

  1. 直接大括号(尾随闭包)传入只需要设置一个BuilderParam即可:

参考代码:

import { PanelComp } from '../views/MyPanel'

@Entry
@Component
struct Index {
  build() {
    Column() {
      PanelComp({ leftTitle:'评价(2000+)',
        rightTitle:'好评率98%' ,
        clickHandler:(id:number)=>{ AlertDialog.show({message:'组件1的回调内容'+id}) }
      })
      {
        Column(){
          Text('组件1的内容')
          Button('按钮')
        }
      }

      PanelComp({ leftTitle:'体验',
        rightTitle:'4条评测',
        clickHandler:(id:number)=>{ AlertDialog.show({message:'组件2的回调内容'+id}) }
      })
      {
        Row(){
          Text('组件1的内容')
          Button('按钮')
        }
      }
    }
    .height('100%')
    .width('100%')
    .backgroundColor(Color.Pink)

  }
}
/*
 * 在组件中编写单个@BuilderParams写法步骤:
 * 1. 定义尾随闭包
 * 2. 准备一个默认的自定义构建函数
 * 3. 使用它
 * */

@Preview
@Component
export struct PanelComp {
  leftTitle: string = '默认标题'
  rightTitle: string = '默认更多'
  clickHandler: (id:number) => void = () => {
  }

  @Builder defaultBuilder(){
    Text('默认的内容')
  }

  @BuilderParam defaultUI:() => void = this.defaultBuilder

  build() {
    Column() {
      Row() {
        Text(this.leftTitle)
          .layoutWeight(1)
          .fontWeight(600)
        Row() {
          Text(this.rightTitle)
            .fontSize(14)
            .fontColor('#666666')

          Image($r('app.media.ic_public_arrow_right'))
            .width(16)
            .fillColor('#666666')

        }
        .onClick(() => {
          // AlertDialog.show({ message: '子组件点击了' })
          this.clickHandler(100)
        })
      }
      .padding(10)

      Row() {
        // 接收父组件传递过来的UI结构
        // Text('默认内容')
        this.defaultUI()
      }
      .height(100)
    }
    .borderRadius(12)
    .backgroundColor('#fff')
    .width('100%')
  }
}

3. 页面路由

页面路由指的是在应用程序中实现不同页面之间的跳转,以及数据传递。

我们先明确自定义组件和页面的关系:

  • 自定义组件:@Component 装饰的UI单元,
  • 页面:即应用的UI页面。可以由一个或者多个自定义组件组成。
    • @Entry装饰的自定义组件为页面的入口组件,即页面的根节点,一个页面有且仅能有一个@Entry

通过 Router 模块就可以实现这个功能. import { router } from '@kit.ArkUI'

步骤:

  1. 创建页面 -> 页面与组件不同的地方是有且只有一个入口组件( @Entry修饰),并且在src/main/resources/base/profile/main_pages.json有配置好了页面路径
    1. 口诀:一入口,一配置

  1. router控制页面跳转

  1. 带参数跳转并获取

3.1. 页面栈

页面栈是用来存储程序运行时页面的一种数据结构,遵循先进后出的原则

页面栈的最大容量为32个页面

3.1.1. pushUrl的情况

先来看看 pushUrl的情况

  1. 默认打开首页 → 首页入栈
  2. pushUrl 去详情页 → 详情页入栈
  3. back 返回上一页 → 详情页出栈
  4. 此时页面栈中应该只有一个页面

整一个过程中,都可以 router.getLength 进行查看

3.1.2. replaceUrl 的情况

再来看看replaceUrl的情况

  1. 默认打开首页 → 首页入栈
  2. replaceUrl 去详情页 → 详情页替换首页,首页销毁
  3. back 无法返回 → 没有上一页

跳转到登录页面时可以使用 replaceUrl,因为无需在页面栈中保存其他页面的页面栈信息了。

3.1.3. 页面栈相关 api

为了让咱们更好的获取页面栈的信息,router 模块也提供了对应的 api 以供使用

// 获取页面栈长度
router.getLength()

// 获取页面状态 
let page = router.getState();
console.log('current index = ' + page.index);
console.log('current name = ' + page.name);
console.log('current path = ' + page.path);

// 清空页面栈
router.clear()

3.2. 路由模式

路由提供了两种不同的跳转模式:

  1. standard(标准实例模式)
  2. Single(单实例模式)

不同模式的决定了页面是否会创建多个实例:

  1. Standard:无论之前是否添加过,一直添加到页面栈【默认】
    1. 场景:适用于每次跳转都呈现全新内容或状态的场景,避免数据展示紊乱(数据变化
  1. Single:如果之前加过页面,会使用之前添加的页面【需要添加参数手动修改】
    1. 场景:适用于那些需要保留页面状态或避免重复创建相同页面的场景(数据不变化

路由模式语法:

3.3. 总结

路由跳转的方式有pushUrl和replaceUrl

pushUrl的跳转要考虑两种模式:标准模式,单例模式

  1. pushUrl({},模式为标准模式(默认的模式)) -> 页面栈中的表现形式

  1. pushUrl({},模式为单例模式) -> 页面栈中的表现形式

  1. replaceUrl() -> 跳转以后的前一个页面自动销毁了

4. 页面和自定义组件的生命周期

组件和页面在创建、显示、销毁的这一整个过程中,会自动执行 一系列的【生命周期钩子】,其实就是一系列的【函数】,让开发者有机会在特定的阶段运行自己的代码

页面生命周期,即被@Entry装饰的组件生命周期,提供以下生命周期接口:

  • onPageShow:页面每次显示时触发一次,包括路由过程、应用进入前台等场景,如果不能触发aboutToAppear函数的时候,网络请求数据代码写在onPageShow中
  • onPageHide:页面每次隐藏时触发一次,包括路由过程、应用进入后台等场景。
  • onBackPress:当用户点击返回按钮时触发。

组件生命周期,即一般用@Component装饰的自定义组件的生命周期,提供以下生命周期接口:

  • aboutToAppear:组件即将出现时回调该接口,具体时机为在创建自定义组件的新实例后,在执行其build()函数之前执行,通常在这里发送网络请求获取数据
  • onDidBuild:组件build()函数执行完成之后回调该接口,开发者可以在这个阶段进行埋点数据上报等不影响实际UI的功能。不建议在onDidBuild函数中更改状态变量、这可能会导致不稳定的UI表现。
  • aboutToDisappear:aboutToDisappear函数在自定义组件析构销毁之前执行。不允许在aboutToDisappear函数中改变状态变量

注意:

  1. @Entry修饰的页面入口组件有:aboutToAppear、onDidBuild、aboutToDisappear、onPageShow、onPageHide、onBackPress
  2. @component修饰的组件有:aboutToAppear、onDidBuild、aboutToDisappear
// 只有被@Entry装饰的组件才可以调用页面的生命周期
  onPageShow() {
    console.info('onPageShow');
  }

  // 只有被@Entry装饰的组件才可以调用页面的生命周期
  onPageHide() {
    console.info('onPageHide');
  }

  // 只有被@Entry装饰的组件才可以调用页面的生命周期
  onBackPress() {
    console.info('onBackPress');
    return true; // 返回true表示页面自己处理返回逻辑,不进行页面路由;返回false表示使用默认的路由返回逻辑,不设置返回值按照false处理
  }

  // 组件生命周期
  aboutToAppear() {
    console.info('aboutToAppear');
  }

  // 组件生命周期
  onDidBuild() {
    console.info('onDidBuild');
  }

  // 组件生命周期
  aboutToDisappear() {
    console.info('aboutToDisappear');
  }

5. 状态管理补充

5.1. 装饰器总览

ArkUI提供了多种装饰器,通过使用这些装饰器,状态变量不仅可以观察在组件内的改变,还可以在不同组件层级间传递,比如父子组件、跨组件层级,也可以观察全局范围内的变化。

根据状态变量的影响范围,将所有的装饰器可以大致分为:

  • 管理组件拥有状态的装饰器:组件级别的状态管理,可以观察组件内变化,和不同组件层级的变化,但需要唯一观察同一个组件树上,即同一个页面内。
  • 管理应用拥有状态的装饰器:应用级别的状态管理,可以观察不同页面,甚至不同UIAbility的状态变化,是应用内全局的状态管理。

咱们来看一张完整的装饰器说明图,咱们后续的学习就围绕着这张图来展开

  1. 管理组件状态:小框中
  2. 管理应用状态:大框中

5.2. @State 组件内状态-补充

@State 装饰器是管理组件内部状态的

@State装饰的变量,或称为状态变量

但是,并不是状态变量的所有更改都会引起UI的刷新,只有可以被框架观察到的修改才会引起UI刷新。

观察变化注意点:

  • 当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。
  • 当装饰的数据类型为Object或数组时
    • 可以观察到自身的赋值的变化
    • 可以观察到对象属性赋值的变化,即Object.keys(observedObject)返回的所有属性
      • 注意:嵌套属性的赋值观察不到
    • 可以观察到数组本身的赋值和添加、删除、更新数组项的变化
      • 注意:数组项中属性的赋值观察不到


interface iPerson {
  name: string
  dog: iDog
}

interface iDog {
  name: string
}

@Entry
@Component
struct Index {
  @State person: iPerson = {
    name: '张三',
    dog: {
      name: '旺财'
    }
  }

  build() {
    Column() {

      Text(JSON.stringify(this.person))

      Button('修改数据')
        .onClick(() => {
        // ✔️1. 如果修改的是整个对象 -> 能观察到变化通知UI更新
        //   this.person = {
        //     name: '李四',
        //     dog: {
        //       name: '旺财1'
        //     }
        //   }

        //  ✔️ 2. 如果修改的是对象的一层属性 -> 能观察到变化,通知UI更新
        //   this.person.name = '李四'
        //   this.person.dog = { name:'萨摩耶' }


        //  ❌ 3. 如果修改的是对象的二层属性 -> 不能观察到变化,UI不会更新
          this.person.dog.name = '萨摩耶'
        })
    }
    .height('100%')
    .width('100%')
  }
}


interface iPerson {
  name:string
}

@Entry
@Component
struct Index {
  @State list:iPerson[] = [
    {name:'张三'}
  ]
  build() {
    Column() {
      Text(JSON.stringify(this.list))
        .onClick(()=>{
            // 修改数组项
          this.list[0] = {name:'李四'}//✔️
          this.list[0].name = '李四'//❌
        })
    }
    .height('100%')
    .width('100%')
    .backgroundColor(Color.Pink)

  }
}

5.3. 状态共享@Prop -父子单向传递

@Prop 装饰的变量可以和父组件建立单向的同步关系

@Prop 装饰的变量是可变的,但是变化不会同步回其父组件

注意:

  1. 修改父组件数据,会同步更新子组件
  2. 修改子组件@Prop 修饰的数据,子组件 UI 更新,更新后的数据不会同步给父组件
  3. 通过回调函数的方式修改父组件的数据,然后触发@Prop数据的更新

随堂演示代码

/* @Prop作用:可以将父文件中的状态变量传递给子组件,让他们形成一个单向数据传递关系
单向     父组件--->子组件    子组件  ---❌--->父组件

语法步骤:
1. 在子组件使用 @Prop定义一个变量
2. 在父组件中调用子组件,并传递参数(父组件中的状态变量)


总结:
1. @Prop的应用场景:父组件要将一个变量传递给子组件,并且在父组件中改变这个变量的值,能让子组件也跟着改变
但是当子组件改了这个变量的值,父组件不会改变

注意点:
1. 父组件传递给子组件的变量,只能是状态状态
2. 只有当父组件中的状态变量值发生改变,才能让子组件更新,但是如果子组件本身修改了这个变量的值也会刷新
3. 如果在子组件中改变的值要通知父组件更新,使用回调函数(父亲给我取的名字,和我自己改的名字同步了)

 *
 * */
import { ChildCom } from './ChildCom'


@Entry
@Component
struct Index {
  @State sonName:string = '张三'

  build() {
    Column({space:40}) {
      ChildCom({ name:this.sonName,updateName:(childName:string)=>{
         this.sonName = childName
      } })

      Divider().backgroundColor(Color.Red).height(2)

      Button('修改儿子的名字').onClick(()=>{
        this.sonName = '张四'
      })
    }
    .height('100%')
    .width('100%')
  }
}

@Component
export struct ChildCom {
  // 定义了一个名字
  @Prop name: string

  updateName:(myname:string)=>void = ()=>{}

  build() {
    Row() {
      Text(this.name)
        .fontSize(50)

      Button('改自己的名字')
        .onClick(()=>{
          // this.name = '张思思'
          this.updateName('张思思')
        })
    }
    .height(50)
    .width('100%')

  }
}

@Component
struct SonCom {
  @Prop info: string
  changeInfo = (newInfo: string) => {
  }

  build() {
    Button('info:' + this.info)
      .onClick(() => {
        this.changeInfo('改啦')
      })
  }
}

@Entry
@Component
struct FatherCom {
  @State info: string = '么么哒'

  build() {
    Column() {
      Text(this.info)
      SonCom({
        info: this.info,
        changeInfo: (newInfo: string) => {
          this.info = newInfo
        }
      })
    }
    .padding(20)
    .backgroundColor(Color.Orange)
  }
}
interface User {
  name: string
  age: number
}

@Entry
@Component
struct Index {
  @State
  userInfo: User = {
    name: 'jack',
    age: 18
  }

  build() {
    Column({ space: 20 }) {
      Text('父组件')
        .fontSize(30)
      Text('用户名:' + this.userInfo.name)
        .white()
        .onClick(() => {
          this.userInfo.name = 'rose'
        })
      Text('年龄:' + this.userInfo.age)
        .white()
        .onClick(() => {
          this.userInfo.age++
        })

      Child({
        user: this.userInfo,
        userChange: (newUser: User) => {
          this.userInfo.name = newUser.name
          this.userInfo.age = newUser.age
        }
      })
    }
    .width('100%')
    .height('100%')
    .alignItems(HorizontalAlign.Center)
    .justifyContent(FlexAlign.Center)
    .backgroundColor(Color.Pink)
  }
}

@Component
struct Child {
  @Prop
  user: User
  userChange: (newUser: User) => void = (newUser: User) => {
  }

  build() {
    Text('子组件:' + JSON.stringify(this.user))
      .padding(10)
      .backgroundColor('#0094ff')
      .fontColor(Color.White)
      .onClick(() => {
        this.userChange({
          name: '路飞',
          age: 26
        })
      })
  }
}

@Extend(Text)
function white() {
  .fontSize(20)
  .fontColor(Color.White)
}

6. 案例-知乎评论

6.1. 静态结构+数据准备

interface ReplyItem {
  id: number
  avatar: ResourceStr
  author: string
  content: string
  time: string
  area: string
  likeNum: number
  likeFlag: boolean
}

@Entry
@Component
struct ZhiHu {
  @State commentList: ReplyItem[] = [
      {
        id: 1,
        avatar: 'https://picx.zhimg.com/027729d02bdf060e24973c3726fea9da_l.jpg?source=06d4cd63',
        author: '偏执狂-妄想家',
        content: '更何况还分到一个摩洛哥[惊喜]',
        time: '11-30',
        area: '海南',
        likeNum: 34,
        likeFlag: false
      },
      {
        id: 2,
        avatar: 'https://pic1.zhimg.com/v2-5a3f5190369ae59c12bee33abfe0c5cc_xl.jpg?source=32738c0c',
        author: 'William',
        content: '当年希腊可是把1:0发挥到极致了',
        time: '11-29',
        area: '北京',
        likeNum: 58,
        likeFlag: true
      },
      {
        id: 3,
        avatar: 'https://picx.zhimg.com/v2-e6f4605c16e4378572a96dad7eaaf2b0_l.jpg?source=06d4cd63',
        author: 'Andy Garcia',
        content: '欧洲杯其实16队球队打正赛已经差不多,24队打正赛意味着正赛阶段在小组赛一样有弱队。',
        time: '11-28',
        area: '上海',
        likeNum: 10,
        likeFlag: false
      },
      {
        id: 4,
        avatar: 'https://picx.zhimg.com/v2-53e7cf84228e26f419d924c2bf8d5d70_l.jpg?source=06d4cd63',
        author: '正宗好鱼头',
        content: '确实眼红啊,亚洲就没这种球队,让中国队刷',
        time: '11-27',
        area: '香港',
        likeNum: 139,
        likeFlag: true
      },
      {
        id: 5,
        avatar: 'https://pic1.zhimg.com/v2-eeddfaae049df2a407ff37540894c8ce_l.jpg?source=06d4cd63',
        author: '柱子哥',
        content: '我是支持扩大的,亚洲杯欧洲杯扩到32队,世界杯扩到64队才是好的,世界上有超过200支队伍,欧洲区55支队伍,亚洲区47支队伍,即使如此也就六成出现率',
        time: '11-27',
        area: '旧金山',
        likeNum: 29,
        likeFlag: false
      },
      {
        id: 6,
        avatar: 'https://picx.zhimg.com/v2-fab3da929232ae911e92bf8137d11f3a_l.jpg?source=06d4cd63',
        author: '飞轩逸',
        content: '禁止欧洲杯扩军之前,应该先禁止世界杯扩军,或者至少把亚洲名额一半给欧洲。',
        time: '11-26',
        area: '里约',
        likeNum: 100,
        likeFlag: false
      }
    ]
  @State rootComment: ReplyItem = {
      id: 1,
      avatar: $r('app.media.avatar'),
      author: '周杰伦',
      content: '意大利拌面应该使用42号钢筋混凝土再加上量子力学缠绕最后通过不畏浮云遮望眼',
      time: '11-30',
      area: '海南',
      likeNum: 98,
      likeFlag: true
    }

  build() {
    Stack({ alignContent: Alignment.Bottom }) {
      Column() {
        Scroll() {
          Column() {
            // 顶部组件
            HmNavBar()
            // 顶部评论
            CommentItem()
            // 分割线
            Divider()
              .strokeWidth(6)
              .color("#f4f5f6")
            // 回复数
            ReplyCount()

            // 回复评论列表
            ForEach(Array.from({ length: 10 }), () => {
              CommentItem()
            })
          }
          .width('100%')
          .backgroundColor(Color.White)
        }
        .padding({
          bottom: 60
        })
        .edgeEffect(EdgeEffect.Spring)
        .scrollBar(BarState.Off)
      }
      .height('100%')

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

@Component
struct HmNavBar {
  build() {
    Row() {
      Row() {
        Image($r('app.media.ic_public_arrow_left'))
          .width(20)
          .height(20)
      }
      .borderRadius(20)
      .backgroundColor('#f6f6f6')
      .justifyContent(FlexAlign.Center)
      .width(30)
      .aspectRatio(1)
      .margin({
        left: 15
      })

      Text("评论回复")
        .layoutWeight(1)
        .textAlign(TextAlign.Center)
        .padding({
          right: 35
        })
    }
    .width('100%')
    .height(50)
    .border({
      width: {
        bottom: 1
      },
      color: '#f6f6f6',
    })
  }
}

@Component
struct CommentItem {
  build() {
    Row() {
      Image($r('app.media.avatar'))
        .width(32)
        .height(32)
        .borderRadius(16)
      Column({ space: 10 }) {
        Text('作者')
          .fontWeight(600)
        Text('内容')
          .lineHeight(20)
          .fontSize(14)
          .fontColor("#565656")

        Row() {
          Text(`11-30 . IP属地 北京`)
            .fontColor("#c3c4c5")
            .fontSize(12)
          Row() {
            Image($r('app.media.like'))
              .width(14)
              .aspectRatio(1)
              .fillColor("#c3c4c5") //  "#c3c4c5" 或 red

            Text('10')
              .fontSize(12)
              .margin({
                left: 5
              })
          }
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)

      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      .padding({
        left: 15,
        right: 5
      })
    }
    .justifyContent(FlexAlign.Start)
    .alignItems(VerticalAlign.Top)
    .width('100%')
    .padding(15)
  }
}

@Component
struct ReplyCount {
  build() {
    Text() {
      Span('回复')
      Span(`${27}`)
    }
    .padding(15)
    .fontWeight(700)
    .alignSelf(ItemAlign.Start)
  }
}

@Component
struct ReplyInput {
  build() {
    Row() {
      TextInput({ placeholder: '回复' })
        .layoutWeight(1)
        .backgroundColor("#f4f5f6")
        .height(40)
      Text('发布')
        .fontColor("#6ecff0")
        .fontSize(14)
        .margin({
          left: 10
        })

    }
    .padding(10)
    .backgroundColor(Color.White)
    .border({
      width: { top: 1 },
      color: "#f4f5f6"
    })
  }
}

6.2. 评论数据渲染

需求:

  1. 顶部评论组件 渲染评论数据
  2. 评论列表组件 渲染评论列表

核心步骤:

  1. 评论组件:
    1. 定义@Prop 接收评论数据
  1. 父组件:
    1. 评论数据传递给子组件
  1. 评论组件
    1. 评论组件,接收数据并使用
interface ReplyItem {
  id: number
  avatar: ResourceStr
  author: string
  content: string
  time: string
  area: string
  likeNum: number
  likeFlag: boolean
}

class ReplyData {
  static getCommentList(): ReplyItem[] {
    return [
      {
        id: 1,
        avatar: 'https://picx.zhimg.com/027729d02bdf060e24973c3726fea9da_l.jpg?source=06d4cd63',
        author: '偏执狂-妄想家',
        content: '更何况还分到一个摩洛哥[惊喜]',
        time: '11-30',
        area: '海南',
        likeNum: 34,
        likeFlag: false
      },
      {
        id: 2,
        avatar: 'https://pic1.zhimg.com/v2-5a3f5190369ae59c12bee33abfe0c5cc_xl.jpg?source=32738c0c',
        author: 'William',
        content: '当年希腊可是把1:0发挥到极致了',
        time: '11-29',
        area: '北京',
        likeNum: 58,
        likeFlag: true
      },
      {
        id: 3,
        avatar: 'https://picx.zhimg.com/v2-e6f4605c16e4378572a96dad7eaaf2b0_l.jpg?source=06d4cd63',
        author: 'Andy Garcia',
        content: '欧洲杯其实16队球队打正赛已经差不多,24队打正赛意味着正赛阶段在小组赛一样有弱队。',
        time: '11-28',
        area: '上海',
        likeNum: 10,
        likeFlag: false
      },
      {
        id: 4,
        avatar: 'https://picx.zhimg.com/v2-53e7cf84228e26f419d924c2bf8d5d70_l.jpg?source=06d4cd63',
        author: '正宗好鱼头',
        content: '确实眼红啊,亚洲就没这种球队,让中国队刷',
        time: '11-27',
        area: '香港',
        likeNum: 139,
        likeFlag: true
      },
      {
        id: 5,
        avatar: 'https://pic1.zhimg.com/v2-eeddfaae049df2a407ff37540894c8ce_l.jpg?source=06d4cd63',
        author: '柱子哥',
        content: '我是支持扩大的,亚洲杯欧洲杯扩到32队,世界杯扩到64队才是好的,世界上有超过200支队伍,欧洲区55支队伍,亚洲区47支队伍,即使如此也就六成出现率',
        time: '11-27',
        area: '旧金山',
        likeNum: 29,
        likeFlag: false
      },
      {
        id: 6,
        avatar: 'https://picx.zhimg.com/v2-fab3da929232ae911e92bf8137d11f3a_l.jpg?source=06d4cd63',
        author: '飞轩逸',
        content: '禁止欧洲杯扩军之前,应该先禁止世界杯扩军,或者至少把亚洲名额一半给欧洲。',
        time: '11-26',
        area: '里约',
        likeNum: 100,
        likeFlag: false
      }
    ]
  }

  static getRootComment(): ReplyItem {
    return {
      id: 1,
      avatar: $r('app.media.avatar'),
      author: '周杰伦',
      content: '意大利拌面应该使用42号钢筋混凝土再加上量子力学缠绕最后通过不畏浮云遮望眼',
      time: '11-30',
      area: '海南',
      likeNum: 98,
      likeFlag: true
    }
  }
}

@Entry
@Component
struct ZhiHu {
  @State commentList: ReplyItem[] = ReplyData.getCommentList()
  @State rootComment: ReplyItem = ReplyData.getRootComment()

  build() {
    Stack({ alignContent: Alignment.Bottom }) {
      Column() {
        Scroll() {
          Column() {
            // 顶部组件
            HmNavBar()
            // 顶部评论
            CommentItem({
              item: this.rootComment
            })
            // 分割线
            Divider()
              .strokeWidth(6)
              .color("#f4f5f6")
            // 回复数
            ReplyCount()

            // 回复评论列表
            ForEach(this.commentList, (item: ReplyItem) => {
              CommentItem({ item: item })
            })
          }
          .width('100%')
          .backgroundColor(Color.White)
        }
        .padding({
          bottom: 60
        })
        .edgeEffect(EdgeEffect.Spring)
        .scrollBar(BarState.Off)
      }
      .height('100%')

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

@Component
struct HmNavBar {
  build() {
    Row() {
      Row() {
        Image($r('app.media.ic_public_arrow_left'))
          .width(20)
          .height(20)
      }
      .borderRadius(20)
      .backgroundColor('#f6f6f6')
      .justifyContent(FlexAlign.Center)
      .width(30)
      .aspectRatio(1)
      .margin({
        left: 15
      })

      Text("评论回复")
        .layoutWeight(1)
        .textAlign(TextAlign.Center)
        .padding({
          right: 35
        })
    }
    .width('100%')
    .height(50)
    .border({
      width: {
        bottom: 1
      },
      color: '#f6f6f6',
    })
  }
}

@Component
struct CommentItem {
  @Prop item: ReplyItem

  build() {
    Row() {
      Image(this.item.avatar)
        .width(32)
        .height(32)
        .borderRadius(16)
      Column({ space: 10 }) {
        Text(this.item.author)
          .fontWeight(600)
        Text(this.item.content)
          .lineHeight(20)
          .fontSize(14)
          .fontColor("#565656")

        Row() {
          Text(`${this.item.time} . IP属地 ${this.item.area}`)
            .fontColor("#c3c4c5")
            .fontSize(12)
          Row() {
            Image($r('app.media.like'))
              .width(14)
              .aspectRatio(1)
              .fillColor("#c3c4c5") //  "#c3c4c5" 或 red

            Text(this.item.likeNum.toString())
              .fontSize(12)
              .margin({
                left: 5
              })
          }
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)

      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      .padding({
        left: 15,
        right: 5
      })
    }
    .justifyContent(FlexAlign.Start)
    .alignItems(VerticalAlign.Top)
    .width('100%')
    .padding(15)
  }
}

@Component
struct ReplyCount {
  build() {
    Text() {
      Span('回复')
      Span(`${27}`)
    }
    .padding(15)
    .fontWeight(700)
    .alignSelf(ItemAlign.Start)
  }
}

@Component
struct ReplyInput {
  build() {
    Row() {
      TextInput({ placeholder: '回复' })
        .layoutWeight(1)
        .backgroundColor("#f4f5f6")
        .height(40)
      Text('发布')
        .fontColor("#6ecff0")
        .fontSize(14)
        .margin({
          left: 10
        })

    }
    .padding(10)
    .backgroundColor(Color.White)
    .border({
      width: { top: 1 },
      color: "#f4f5f6"
    })
  }
}

6.3. 评论点赞-顶部评论

完成顶部评论

需求:

  1. 点击顶部的❤,切换点赞状态

核心步骤:

  1. 子组件:
    1. 定义changeLike函数,在点赞时调用
    2. 数据使用@Prop修饰,父组件状态更新会触发子组件更新
  1. 父组件:
    1. 传递changeLike 给子组件,内部实现点赞顶部评论逻辑
interface ReplyItem {
  id: number
  avatar: ResourceStr
  author: string
  content: string
  time: string
  area: string
  likeNum: number
  likeFlag: boolean
}

class ReplyData {
  static getCommentList(): ReplyItem[] {
    return [
      {
        id: 1,
        avatar: 'https://picx.zhimg.com/027729d02bdf060e24973c3726fea9da_l.jpg?source=06d4cd63',
        author: '偏执狂-妄想家',
        content: '更何况还分到一个摩洛哥[惊喜]',
        time: '11-30',
        area: '海南',
        likeNum: 34,
        likeFlag: false
      },
      {
        id: 2,
        avatar: 'https://pic1.zhimg.com/v2-5a3f5190369ae59c12bee33abfe0c5cc_xl.jpg?source=32738c0c',
        author: 'William',
        content: '当年希腊可是把1:0发挥到极致了',
        time: '11-29',
        area: '北京',
        likeNum: 58,
        likeFlag: true
      },
      {
        id: 3,
        avatar: 'https://picx.zhimg.com/v2-e6f4605c16e4378572a96dad7eaaf2b0_l.jpg?source=06d4cd63',
        author: 'Andy Garcia',
        content: '欧洲杯其实16队球队打正赛已经差不多,24队打正赛意味着正赛阶段在小组赛一样有弱队。',
        time: '11-28',
        area: '上海',
        likeNum: 10,
        likeFlag: false
      },
      {
        id: 4,
        avatar: 'https://picx.zhimg.com/v2-53e7cf84228e26f419d924c2bf8d5d70_l.jpg?source=06d4cd63',
        author: '正宗好鱼头',
        content: '确实眼红啊,亚洲就没这种球队,让中国队刷',
        time: '11-27',
        area: '香港',
        likeNum: 139,
        likeFlag: true
      },
      {
        id: 5,
        avatar: 'https://pic1.zhimg.com/v2-eeddfaae049df2a407ff37540894c8ce_l.jpg?source=06d4cd63',
        author: '柱子哥',
        content: '我是支持扩大的,亚洲杯欧洲杯扩到32队,世界杯扩到64队才是好的,世界上有超过200支队伍,欧洲区55支队伍,亚洲区47支队伍,即使如此也就六成出现率',
        time: '11-27',
        area: '旧金山',
        likeNum: 29,
        likeFlag: false
      },
      {
        id: 6,
        avatar: 'https://picx.zhimg.com/v2-fab3da929232ae911e92bf8137d11f3a_l.jpg?source=06d4cd63',
        author: '飞轩逸',
        content: '禁止欧洲杯扩军之前,应该先禁止世界杯扩军,或者至少把亚洲名额一半给欧洲。',
        time: '11-26',
        area: '里约',
        likeNum: 100,
        likeFlag: false
      }
    ]
  }

  static getRootComment(): ReplyItem {
    return {
      id: 1,
      avatar: $r('app.media.avatar'),
      author: '周杰伦',
      content: '意大利拌面应该使用42号钢筋混凝土再加上量子力学缠绕最后通过不畏浮云遮望眼',
      time: '11-30',
      area: '海南',
      likeNum: 98,
      likeFlag: true
    }
  }
}

@Entry
@Component
struct ZhiHu {
  @State commentList: ReplyItem[] = ReplyData.getCommentList()
  @State rootComment: ReplyItem = ReplyData.getRootComment()

  build() {
    Stack({ alignContent: Alignment.Bottom }) {
      Column() {
        Scroll() {
          Column() {
            // 顶部组件
            HmNavBar()
            // 顶部评论
            CommentItem({
              item: this.rootComment,
              changeLike: () => {
                this.rootComment.likeFlag = !this.rootComment.likeFlag
                if (this.rootComment.likeFlag == true) {
                  // 累加
                  this.rootComment.likeNum++
                } else {
                  // 递减
                  this.rootComment.likeNum--
                }
              }
            })
            // 分割线
            Divider()
              .strokeWidth(6)
              .color("#f4f5f6")
            // 回复数
            ReplyCount()

            // 回复评论列表
            ForEach(this.commentList, (item: ReplyItem, index: number) => {
              CommentItem({
                item: item,
       
              })
            })
          }
          .width('100%')
          .backgroundColor(Color.White)
        }
        .padding({
          bottom: 60
        })
        .edgeEffect(EdgeEffect.Spring)
        .scrollBar(BarState.Off)
      }
      .height('100%')

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

@Component
struct HmNavBar {
  build() {
    Row() {
      Row() {
        Image($r('app.media.ic_public_arrow_left'))
          .width(20)
          .height(20)
      }
      .borderRadius(20)
      .backgroundColor('#f6f6f6')
      .justifyContent(FlexAlign.Center)
      .width(30)
      .aspectRatio(1)
      .margin({
        left: 15
      })

      Text("评论回复")
        .layoutWeight(1)
        .textAlign(TextAlign.Center)
        .padding({
          right: 35
        })
    }
    .width('100%')
    .height(50)
    .border({
      width: {
        bottom: 1
      },
      color: '#f6f6f6',
    })
  }
}

@Component
struct CommentItem {
  @Prop item: ReplyItem
  changeLike = () => {

  }

  build() {
    Row() {
      Image(this.item.avatar)
        .width(32)
        .height(32)
        .borderRadius(16)
      Column({ space: 10 }) {
        Text(this.item.author)
          .fontWeight(600)
        Text(this.item.content)
          .lineHeight(20)
          .fontSize(14)
          .fontColor("#565656")

        Row() {
          Text(`${this.item.time} . IP属地 ${this.item.area}`)
            .fontColor("#c3c4c5")
            .fontSize(12)
          Row() {
            Image($r('app.media.like'))
              .width(14)
              .aspectRatio(1)
              .fillColor(this.item.likeFlag ? Color.Red : "#c3c4c5")//  "#c3c4c5" 或 red
              .onClick(() => {
                this.changeLike()
              })

            Text(this.item.likeNum.toString())
              .fontSize(12)
              .margin({
                left: 5
              })
          }
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)

      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      .padding({
        left: 15,
        right: 5
      })
    }
    .justifyContent(FlexAlign.Start)
    .alignItems(VerticalAlign.Top)
    .width('100%')
    .padding(15)
  }
}

@Component
struct ReplyCount {
  build() {
    Text() {
      Span('回复')
      Span(`${27}`)
    }
    .padding(15)
    .fontWeight(700)
    .alignSelf(ItemAlign.Start)
  }
}

@Component
struct ReplyInput {
  build() {
    Row() {
      TextInput({ placeholder: '回复' })
        .layoutWeight(1)
        .backgroundColor("#f4f5f6")
        .height(40)
      Text('发布')
        .fontColor("#6ecff0")
        .fontSize(14)
        .margin({
          left: 10
        })

    }
    .padding(10)
    .backgroundColor(Color.White)
    .border({
      width: { top: 1 },
      color: "#f4f5f6"
    })
  }
}

6.4. 评论点赞-列表

完成 列表的点赞效果

核心步骤:

  1. 在父组件:
    1. 列表区域实现changeLike函数,实现点赞逻辑
    2. 页面的更新,通过数组的 splice 方法来实现
interface ReplyItem {
  id: number
  avatar: ResourceStr
  author: string
  content: string
  time: string
  area: string
  likeNum: number
  likeFlag: boolean
}

class ReplyData {
  static getCommentList(): ReplyItem[] {
    return [
      {
        id: 1,
        avatar: 'https://picx.zhimg.com/027729d02bdf060e24973c3726fea9da_l.jpg?source=06d4cd63',
        author: '偏执狂-妄想家',
        content: '更何况还分到一个摩洛哥[惊喜]',
        time: '11-30',
        area: '海南',
        likeNum: 34,
        likeFlag: false
      },
      {
        id: 2,
        avatar: 'https://pic1.zhimg.com/v2-5a3f5190369ae59c12bee33abfe0c5cc_xl.jpg?source=32738c0c',
        author: 'William',
        content: '当年希腊可是把1:0发挥到极致了',
        time: '11-29',
        area: '北京',
        likeNum: 58,
        likeFlag: true
      },
      {
        id: 3,
        avatar: 'https://picx.zhimg.com/v2-e6f4605c16e4378572a96dad7eaaf2b0_l.jpg?source=06d4cd63',
        author: 'Andy Garcia',
        content: '欧洲杯其实16队球队打正赛已经差不多,24队打正赛意味着正赛阶段在小组赛一样有弱队。',
        time: '11-28',
        area: '上海',
        likeNum: 10,
        likeFlag: false
      },
      {
        id: 4,
        avatar: 'https://picx.zhimg.com/v2-53e7cf84228e26f419d924c2bf8d5d70_l.jpg?source=06d4cd63',
        author: '正宗好鱼头',
        content: '确实眼红啊,亚洲就没这种球队,让中国队刷',
        time: '11-27',
        area: '香港',
        likeNum: 139,
        likeFlag: true
      },
      {
        id: 5,
        avatar: 'https://pic1.zhimg.com/v2-eeddfaae049df2a407ff37540894c8ce_l.jpg?source=06d4cd63',
        author: '柱子哥',
        content: '我是支持扩大的,亚洲杯欧洲杯扩到32队,世界杯扩到64队才是好的,世界上有超过200支队伍,欧洲区55支队伍,亚洲区47支队伍,即使如此也就六成出现率',
        time: '11-27',
        area: '旧金山',
        likeNum: 29,
        likeFlag: false
      },
      {
        id: 6,
        avatar: 'https://picx.zhimg.com/v2-fab3da929232ae911e92bf8137d11f3a_l.jpg?source=06d4cd63',
        author: '飞轩逸',
        content: '禁止欧洲杯扩军之前,应该先禁止世界杯扩军,或者至少把亚洲名额一半给欧洲。',
        time: '11-26',
        area: '里约',
        likeNum: 100,
        likeFlag: false
      }
    ]
  }

  static getRootComment(): ReplyItem {
    return {
      id: 1,
      avatar: $r('app.media.avatar'),
      author: '周杰伦',
      content: '意大利拌面应该使用42号钢筋混凝土再加上量子力学缠绕最后通过不畏浮云遮望眼',
      time: '11-30',
      area: '海南',
      likeNum: 98,
      likeFlag: true
    }
  }
}

@Entry
@Component
struct ZhiHu {
  @State commentList: ReplyItem[] = ReplyData.getCommentList()
  @State rootComment: ReplyItem = ReplyData.getRootComment()

  build() {
    Stack({ alignContent: Alignment.Bottom }) {
      Column() {
        Scroll() {
          Column() {
            // 顶部组件
            HmNavBar()
            // 顶部评论
            CommentItem({
              item: this.rootComment,
              changeLike: () => {
                this.rootComment.likeFlag = !this.rootComment.likeFlag
                if (this.rootComment.likeFlag == true) {
                  // 累加
                  this.rootComment.likeNum++
                } else {
                  // 递减
                  this.rootComment.likeNum--
                }
              }
            })
            // 分割线
            Divider()
              .strokeWidth(6)
              .color("#f4f5f6")
            // 回复数
            ReplyCount()

            // 回复评论列表
            ForEach(this.commentList, (item: ReplyItem, index: number) => {
              CommentItem({
                item: item,
                changeLike: () => {
                  item.likeFlag = !item.likeFlag
                  if (item.likeFlag == true) {
                    // 累加
                    item.likeNum++
                  } else {
                    // 递减
                    item.likeNum--
                  }
                  this.commentList.splice(index, 1, item)
                }
              })
            })
          }
          .width('100%')
          .backgroundColor(Color.White)
        }
        .padding({
          bottom: 60
        })
        .edgeEffect(EdgeEffect.Spring)
        .scrollBar(BarState.Off)
      }
      .height('100%')

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

@Component
struct HmNavBar {
  build() {
    Row() {
      Row() {
        Image($r('app.media.ic_public_arrow_left'))
          .width(20)
          .height(20)
      }
      .borderRadius(20)
      .backgroundColor('#f6f6f6')
      .justifyContent(FlexAlign.Center)
      .width(30)
      .aspectRatio(1)
      .margin({
        left: 15
      })

      Text("评论回复")
        .layoutWeight(1)
        .textAlign(TextAlign.Center)
        .padding({
          right: 35
        })
    }
    .width('100%')
    .height(50)
    .border({
      width: {
        bottom: 1
      },
      color: '#f6f6f6',
    })
  }
}

@Component
struct CommentItem {
  @Prop item: ReplyItem
  changeLike = () => {

  }

  build() {
    Row() {
      Image(this.item.avatar)
        .width(32)
        .height(32)
        .borderRadius(16)
      Column({ space: 10 }) {
        Text(this.item.author)
          .fontWeight(600)
        Text(this.item.content)
          .lineHeight(20)
          .fontSize(14)
          .fontColor("#565656")

        Row() {
          Text(`${this.item.time} . IP属地 ${this.item.area}`)
            .fontColor("#c3c4c5")
            .fontSize(12)
          Row() {
            Image($r('app.media.like'))
              .width(14)
              .aspectRatio(1)
              .fillColor(this.item.likeFlag ? Color.Red : "#c3c4c5")//  "#c3c4c5" 或 red
              .onClick(() => {
                this.changeLike()
              })

            Text(this.item.likeNum.toString())
              .fontSize(12)
              .margin({
                left: 5
              })
          }
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)

      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      .padding({
        left: 15,
        right: 5
      })
    }
    .justifyContent(FlexAlign.Start)
    .alignItems(VerticalAlign.Top)
    .width('100%')
    .padding(15)
  }
}

@Component
struct ReplyCount {
  build() {
    Text() {
      Span('回复')
      Span(`${27}`)
    }
    .padding(15)
    .fontWeight(700)
    .alignSelf(ItemAlign.Start)
  }
}

@Component
struct ReplyInput {
  build() {
    Row() {
      TextInput({ placeholder: '回复' })
        .layoutWeight(1)
        .backgroundColor("#f4f5f6")
        .height(40)
      Text('发布')
        .fontColor("#6ecff0")
        .fontSize(14)
        .margin({
          left: 10
        })

    }
    .padding(10)
    .backgroundColor(Color.White)
    .border({
      width: { top: 1 },
      color: "#f4f5f6"
    })
  }
}

6.5. 发布评论

最后来完成发布评论

需求:

  1. 发布评论
  2. 评论个数同步更新

核心步骤:

  1. 发布评论
    1. 收集输入框内容
    2. 点击发布传递给父组件
    3. 父组件加入数据
  1. 评论个数同步更新
    1. 将 length 传入子组件
    2. 子组件内部通过 Prop 接收(响应更新)
interface ReplyItem {
  id: number
  avatar: ResourceStr
  author: string
  content: string
  time: string
  area: string
  likeNum: number
  likeFlag: boolean
}

class ReplyData {
  static getCommentList(): ReplyItem[] {
    return [
      {
        id: 1,
        avatar: 'https://picx.zhimg.com/027729d02bdf060e24973c3726fea9da_l.jpg?source=06d4cd63',
        author: '偏执狂-妄想家',
        content: '更何况还分到一个摩洛哥[惊喜]',
        time: '11-30',
        area: '海南',
        likeNum: 34,
        likeFlag: false
      },
      {
        id: 2,
        avatar: 'https://pic1.zhimg.com/v2-5a3f5190369ae59c12bee33abfe0c5cc_xl.jpg?source=32738c0c',
        author: 'William',
        content: '当年希腊可是把1:0发挥到极致了',
        time: '11-29',
        area: '北京',
        likeNum: 58,
        likeFlag: true
      },
      {
        id: 3,
        avatar: 'https://picx.zhimg.com/v2-e6f4605c16e4378572a96dad7eaaf2b0_l.jpg?source=06d4cd63',
        author: 'Andy Garcia',
        content: '欧洲杯其实16队球队打正赛已经差不多,24队打正赛意味着正赛阶段在小组赛一样有弱队。',
        time: '11-28',
        area: '上海',
        likeNum: 10,
        likeFlag: false
      },
      {
        id: 4,
        avatar: 'https://picx.zhimg.com/v2-53e7cf84228e26f419d924c2bf8d5d70_l.jpg?source=06d4cd63',
        author: '正宗好鱼头',
        content: '确实眼红啊,亚洲就没这种球队,让中国队刷',
        time: '11-27',
        area: '香港',
        likeNum: 139,
        likeFlag: true
      },
      {
        id: 5,
        avatar: 'https://pic1.zhimg.com/v2-eeddfaae049df2a407ff37540894c8ce_l.jpg?source=06d4cd63',
        author: '柱子哥',
        content: '我是支持扩大的,亚洲杯欧洲杯扩到32队,世界杯扩到64队才是好的,世界上有超过200支队伍,欧洲区55支队伍,亚洲区47支队伍,即使如此也就六成出现率',
        time: '11-27',
        area: '旧金山',
        likeNum: 29,
        likeFlag: false
      },
      {
        id: 6,
        avatar: 'https://picx.zhimg.com/v2-fab3da929232ae911e92bf8137d11f3a_l.jpg?source=06d4cd63',
        author: '飞轩逸',
        content: '禁止欧洲杯扩军之前,应该先禁止世界杯扩军,或者至少把亚洲名额一半给欧洲。',
        time: '11-26',
        area: '里约',
        likeNum: 100,
        likeFlag: false
      }
    ]
  }

  static getRootComment(): ReplyItem {
    return {
      id: 1,
      avatar: $r('app.media.avatar'),
      author: '周杰伦',
      content: '意大利拌面应该使用42号钢筋混凝土再加上量子力学缠绕最后通过不畏浮云遮望眼',
      time: '11-30',
      area: '海南',
      likeNum: 98,
      likeFlag: true
    }
  }
}

@Entry
@Component
struct ZhiHu {
  @State commentList: ReplyItem[] = ReplyData.getCommentList()
  @State rootComment: ReplyItem = ReplyData.getRootComment()

  build() {
    Stack({ alignContent: Alignment.Bottom }) {
      Column() {
        Scroll() {
          Column() {
            // 顶部组件
            HmNavBar()
            // 顶部评论
            CommentItem({
              item: this.rootComment,
              changeLike: () => {
                this.rootComment.likeFlag = !this.rootComment.likeFlag
                if (this.rootComment.likeFlag == true) {
                  // 累加
                  this.rootComment.likeNum++
                } else {
                  // 递减
                  this.rootComment.likeNum--
                }
              }
            })
            // 分割线
            Divider()
              .strokeWidth(6)
              .color("#f4f5f6")
            // 回复数
            ReplyCount({ count: this.commentList.length })

            // 回复评论列表
            ForEach(this.commentList, (item: ReplyItem, index: number) => {
              CommentItem({
                item: item,
                changeLike: () => {
                  item.likeFlag = !item.likeFlag
                  if (item.likeFlag == true) {
                    // 累加
                    item.likeNum++
                  } else {
                    // 递减
                    item.likeNum--
                  }
                  this.commentList.splice(index, 1, item)
                }
              })
            })
          }
          .width('100%')
          .backgroundColor(Color.White)
        }
        .padding({
          bottom: 60
        })
        .edgeEffect(EdgeEffect.Spring)
        .scrollBar(BarState.Off)
      }
      .height('100%')

      ReplyInput({
        addReply: (inputValue: string) => {
          this.commentList.unshift({
            id: Date.now(),
            avatar: $r('app.media.avatar'),
            author: '小狗钱钱',
            content: inputValue,
            time: `${new Date().getMonth() + 1}-${new Date().getDate()}`,
            area: '浙江',
            likeNum: 0,
            likeFlag: false
          })
        }
      })
    }
    .height('100%')
  }
}

@Component
struct HmNavBar {
  build() {
    Row() {
      Row() {
        Image($r('app.media.ic_public_arrow_left'))
          .width(20)
          .height(20)
      }
      .borderRadius(20)
      .backgroundColor('#f6f6f6')
      .justifyContent(FlexAlign.Center)
      .width(30)
      .aspectRatio(1)
      .margin({
        left: 15
      })

      Text("评论回复")
        .layoutWeight(1)
        .textAlign(TextAlign.Center)
        .padding({
          right: 35
        })
    }
    .width('100%')
    .height(50)
    .border({
      width: {
        bottom: 1
      },
      color: '#f6f6f6',
    })
  }
}

@Component
struct CommentItem {
  @Prop item: ReplyItem
  changeLike = () => {

  }

  build() {
    Row() {
      Image(this.item.avatar)
        .width(32)
        .height(32)
        .borderRadius(16)
      Column({ space: 10 }) {
        Text(this.item.author)
          .fontWeight(600)
        Text(this.item.content)
          .lineHeight(20)
          .fontSize(14)
          .fontColor("#565656")

        Row() {
          Text(`${this.item.time} . IP属地 ${this.item.area}`)
            .fontColor("#c3c4c5")
            .fontSize(12)
          Row() {
            Image($r('app.media.like'))
              .width(14)
              .aspectRatio(1)
              .fillColor(this.item.likeFlag ? Color.Red : "#c3c4c5")//  "#c3c4c5" 或 red
              .onClick(() => {
                this.changeLike()
              })

            Text(this.item.likeNum.toString())
              .fontSize(12)
              .margin({
                left: 5
              })
          }
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)

      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      .padding({
        left: 15,
        right: 5
      })
    }
    .justifyContent(FlexAlign.Start)
    .alignItems(VerticalAlign.Top)
    .width('100%')
    .padding(15)
  }
}

@Component
struct ReplyCount {
  @Prop
  count: number

  build() {
    Text() {
      Span('回复')
      Span(`${this.count}`)
    }
    .padding(15)
    .fontWeight(700)
    .alignSelf(ItemAlign.Start)
  }
}

@Component
struct ReplyInput {
  @State inputValue: string = ''
  addReply = (inputStr: string) => {
  }

  build() {
    Row() {
      TextInput({ placeholder: '回复', text: $$this.inputValue })
        .layoutWeight(1)
        .backgroundColor("#f4f5f6")
        .height(40)
      Text('发布')
        .fontColor("#6ecff0")
        .fontSize(14)
        .margin({
          left: 10
        })
        .onClick(() => {
          this.addReply(this.inputValue)
        })

    }
    .padding(10)
    .backgroundColor(Color.White)
    .border({
      width: { top: 1 },
      color: "#f4f5f6"
    })
  }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2304278.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL MHA 部署全攻略:从零搭建高可用数据库架构

文章目录 1.MHA介绍2.MHA组件介绍3.集群规划4.服务器初始化5.MySQL集群部署5.1 安装MySQL集群5.2 配置一主两从5.3 测试MySQL主从5.4 赋予MHA用户连接权限 6.安装MHA环境6.1 安装MHA Node6.2 安装MHA Manager 7.配置MHA环境8.MySQL MHA高可用集群测试8.1 通过VIP连接MySQL8.2模…

Spring Boot3+Vue2极速整合:10分钟搭建DeepSeek AI对话系统

前言 在生成式AI技术蓬勃发展的今天,大语言模型已成为企业智能化转型和个人效率提升的核心驱动力。作为国产大模型的优秀代表,DeepSeek凭借其卓越的中文语义理解能力和开发者友好的API生态,正在成为构建本土化AI应用的首选平台。 本文将以S…

浅谈 Redis 主从复制原理(二)

大家好,我是此林。 【浅谈 Redis 主从集群原理(一) 】 上一篇文章中,说到了 Redis 主从复制的全量同步和增量同步,repl_baklog 复制缓冲区,以及 slave 挂掉之后数据同步的措施。 下面介绍的上一篇遗留问…

elf_loader:一个使用Rust编写的ELF加载器

本文介绍一个使用Rust实现的ELF加载器。 下面是elf_loader的仓库链接: github: https://github.com/weizhiao/elf_loaderhttps://github.com/weizhiao/elf_loader crates.io: https://crates.io/crates/elf_loaderhttps://crates.io/cra…

连接Sql Server时报错无法通过使用安全套接字层加密与 SQL Server 建立安全连接

文章目录 一. 前言二. 解决方案 方案1方案2 三. 总结 一. 前言 在《数据库原理》这门课的实验上,需要使用SQL Server,然后使用jdbc连接sql server突然报错为:SQLServerException: “Encrypt”属性设置为“true”且 “trustServerCertific…

Qt常用控件之日历QCalendarWidget

日历QCalendarWidget QCalendarWidget 是一个日历控件。 QCalendarWidget属性 属性说明selectDate当前选中日期。minimumDate最小日期。maximumDate最大日期。firstDayOfWeek设置每周的第一天是周几(影响日历的第一列是周几)。gridVisible是否显示日历…

超级详细Spring AI运用Ollama大模型

大模型工具Ollama 官网:https://ollama.com/ Ollama是一个用于部署和运行各种开源大模型的工具; 它能够帮助用户快速在本地运行各种大模型,极大地简化了大模型在本地运行的过程。用户通过执行几条命令就能在本地运行开源大模型,如Lama 2等; 综上&#x…

0083.基于springboot+uni-app的社区车位租赁系统小程序+论文

一、系统说明 基于springbootuni-app的社区车位租赁系统小程序,系统功能齐全, 代码简洁易懂,适合小白学编程。 现如今,信息种类变得越来越多,信息的容量也变得越来越大,这就是信息时代的标志。近些年,计算机科学发展…

计算机视觉行业洞察--影像行业系列第一期

计算机视觉行业产业链的上下游构成相对清晰,从基础技术研发到具体应用场景的多个环节相对成熟。 以下是我结合VisionChina经历和行业龙头企业对计算机视觉行业产业链上下游的拆解总结。 上下游总结 上游产业链分为软硬件两类,视觉的硬件主要指芯片、…

【深度学习量化交易15】基于miniQMT的量化交易回测系统已基本构建完成!AI炒股的框架初步实现

我是Mr.看海,我在尝试用信号处理的知识积累和思考方式做量化交易,应用深度学习和AI实现股票自动交易,目的是实现财务自由~ 目前我正在开发基于miniQMT的量化交易系统——看海量化交易系统。 AI怎么炒股?就是通过量化交易。 近期D…

使用大语言模型(Deepseek)构建一个基于 SQL 数据的问答系统

GitHub代码仓库 架构 从高层次来看,这些系统的步骤如下: 将问题转换为SQL查询:模型将用户输入转换为SQL查询。 执行SQL查询:执行查询。 回答问题:模型根据查询结果响应用户输入。 样本数据 下载样本数据&#xf…

JAVA最新版本详细安装教程(附安装包)

目录 文章自述 一、JAVA下载 二、JAVA安装 1.首先在D盘创建【java/jdk-23】文件夹 2.把下载的压缩包移动到【jdk-23】文件夹内,右键点击【解压到当前文件夹】 3.如图解压会有【jdk-23.0.1】文件 4.右键桌面此电脑,点击【属性】 5.下滑滚动条&…

网络安全之探险

🍅 点击文末小卡片 ,免费获取网络安全全套资料,资料在手,涨薪更快 因为工作相关性,看着第三方公司出具的网络安全和shentou测试报告就想更深入研究一下,于是乎开始探索网络安全方面的知识,度娘、…

基础dp——动态规划

目录 一、什么是动态规划? 二、动态规划的使用步骤 1.状态表示 2.状态转移方程 3.初始化 4.填表顺序 5.返回值 三、试题讲解 1.最小花费爬楼梯 2.下降路径最小和 3.解码方法 一、什么是动态规划? 动态规划(Dynamic Programming&…

(四)趣学设计模式 之 原型模式!

目录 一、 啥是原型模式?二、 为什么要用原型模式?三、 原型模式怎么实现?四、 原型模式的应用场景五、 原型模式的优点和缺点六、 总结 🌟我的其他文章也讲解的比较有趣😁,如果喜欢博主的讲解方式&#xf…

会话对象 Cookie 四、Cookie的路径

1.Cookie的path属性 Cookie还有一个path属性,可以通过Cookie#setPath(String)方法来设置。你可以使用HttpWatch查看响应中的Set-Cookie中是否存在路径。下面是通过Chrome查看Cookie信息。 也就是说,就算你不设置Cookie的path,Cookie也是有路…

hugging face---transformers包

一、前言 不同于计算机视觉的百花齐放,不同网络适用不同情况,NLP则由Transformer一统天下。transformer是2017年提出的一种基于自注意力机制的神经网络架构,transformers库是hugging face社区创造的一个py库,通过该库可以实现统一…

将 Vue 项目打包后部署到 Spring Boot 项目中的全面指南

将 Vue 项目打包后部署到 Spring Boot 项目中的全面指南 在现代 Web 开发中,前后端分离架构已经成为主流。然而,在某些场景下,我们可能需要将前端项目(如 Vue)与后端项目(如 Spring Boot)集成部…

GPIO外设

一、GPIO简介 GPIO,general-purpos IO port,通用输入输出引脚,所有的GPIO引脚都有基本的输入输出功能。 最基本的输出功能:STM32控制引脚输出高、低电平,实现开关控制;最基本的输入功能:检测外部输入电平&…

C++——priority_queue模拟实现

目录 前言 一、优先级队列介绍 二、优先级队列实现 向上调整 向下调整 三、仿函数 总结 前言 上一篇文章我们讲了stack和queue,这两个容器是容器适配器,本质上是一种复用,那本篇文章要讲的优先级队列也是一个容器适配器,我们…