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

1. 自定义组件基础

相关资源:

  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和TextColumn() {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,避免外部不传入@BuilderdefaultBuilder() {Text('默认的内容')}// 2.定义 BuilderParam 接受外部传入的 ui,并设置默认值@BuilderParam ContentBuilder: () => void = this.defaultBuilderbuild() {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.defaultBuilderbuild() {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,避免外部不传入@BuildertitleDefaultBuilder() {Text('默认标题')}@BuildercontentDefaultBuilder() {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 {@BuilderfTitleBuilder() {Text('传入的标题').fontSize(20).fontWeight(600).fontColor(Color.White).backgroundColor(Color.Blue).padding(10)}@BuilderfContentBuilder() {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.defaultBuilderbuild() {Column(){this.defaultUI()}}
}// 父组件调用子组件MyBuilder(){Button('按钮')}

// 定义子组件
@Component
export struct MyBuilder{ @Builder defaultBuilder(){Text('标题默认的结构')}@Builder contentBuilder(){Text('内容默认的结构')}@BuilderParam defaultUI:() => void = this.defaultBuilder@BuilderParam contentUI:() => void = this.contentBuilderbuild() {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.defaultBuilderbuild() {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: stringdog: 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: stringupdateName:(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: stringchangeInfo = (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: stringage: number
}@Entry
@Component
struct Index {@StateuserInfo: 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.namethis.userInfo.age = newUser.age}})}.width('100%').height('100%').alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center).backgroundColor(Color.Pink)}
}@Component
struct Child {@Propuser: UseruserChange: (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: numberavatar: ResourceStrauthor: stringcontent: stringtime: stringarea: stringlikeNum: numberlikeFlag: 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" 或 redText('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: numberavatar: ResourceStrauthor: stringcontent: stringtime: stringarea: stringlikeNum: numberlikeFlag: 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: ReplyItembuild() {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" 或 redText(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: numberavatar: ResourceStrauthor: stringcontent: stringtime: stringarea: stringlikeNum: numberlikeFlag: 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.likeFlagif (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: ReplyItemchangeLike = () => {}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: numberavatar: ResourceStrauthor: stringcontent: stringtime: stringarea: stringlikeNum: numberlikeFlag: 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.likeFlagif (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.likeFlagif (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: ReplyItemchangeLike = () => {}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: numberavatar: ResourceStrauthor: stringcontent: stringtime: stringarea: stringlikeNum: numberlikeFlag: 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.likeFlagif (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.likeFlagif (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: ReplyItemchangeLike = () => {}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 {@Propcount: numberbuild() {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"})}
}

相关文章:

1. 自定义组件基础

相关资源: 📎day10 图片素材.zip 1. 自定义组件基础 概念:在ArkUI中由框架直接提供的称为系统组件 -> Column,Button等,由开发者定义的称为自定义组件 作用:自定义组件可以对 UI和业务逻辑进行封装&…...

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是否显示日历…...

Java——抽象类

在Java中,抽象类(Abstract Class) 是一种特殊的类,用于定义部分实现的类结构,同时允许子类提供具体的实现。抽象类通常用于定义通用的行为或属性,而将具体的实现细节留给子类。 1. 抽象类的定义 语法&…...

Linux CentOS 上 Ollama 的安装与部署:从入门到实践

Linux CentOS 上 Ollama 的安装与部署:从入门到实践 随着人工智能技术的快速发展,大语言模型(LLMs)的应用场景越来越广泛。Ollama 作为一个轻量级的 AI 模型管理工具,为开发者提供了便捷的模型部署和管理解决方案。本文将详细介绍如何在 Linux CentOS 系统上安装和部署 O…...

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

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

Django项目设计一个简单CRUD

在Django中实现一个学生类(Student)的CRUD(创建、读取、更新、删除)操作需要几个步骤,包括定义模型、创建视图、设置URL、以及配置模板。以下是一个基本的实现步骤: 1. 创建Django项目和应用 首先&#x…...

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

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

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

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

网页请求腾讯云环境的云函数

背景&#xff1a;uniapp&#xff0c;做一个管理后台 需求&#xff1a;在PC端网页请求云环境的云函数 npm npm install cloudbase/js-sdk -S 在APP中&#xff0c;封装匿名登陆&#xff0c;因为未登录时无法请求云函数 app.vue <script>import Vue from vueimport cl…...

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

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

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

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

QT闲记-工具栏

工具栏通常用来放置常用的操作按钮,如QPushButton,QAction等。可以放置在顶部,底部,左侧,右侧,并且支持拖曳,浮动。 1、创建工具栏 通常通过QMainWindow 提供的addToolBar()来创建,它跟菜单栏一样,如果需要工具栏,一般情况下,我们设置这个类的基类为QMainWindow。 …...

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

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

网络安全之探险

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

基础dp——动态规划

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

异常处理在 Promptic 中怎么实现?

在 Promptic 中&#xff0c;异常处理主要通过与 tenacity 库结合来实现&#xff0c;用于处理诸如 API 限流、临时故障等问题。以下是实现异常处理的具体方法和步骤&#xff1a; 1. 使用 tenacity 实现重试机制 tenacity 是一个强大的 Python 库&#xff0c;用于实现重试逻辑&…...

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

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

会话对象 Cookie 四、Cookie的路径

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

A. Jagged Swaps

time limit per test 1 second memory limit per test 256 megabytes You are given a permutation†† aa of size nn. You can do the following operation Select an index ii from 22 to n−1n−1 such that ai−1<aiai−1<ai and ai>ai1ai>ai1. Swap aia…...

hugging face---transformers包

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

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

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

ipad连接电脑断断续续,不断弹窗的解决办法

因为ipad air 屏幕摔坏&#xff0c;换了一个内外屏&#xff0c;想用爱思检验一下屏幕真伪&#xff0c; 连接电脑时&#xff0c;断断续续&#xff0c;连上几秒钟然后就断开&#xff0c;然后又连上 然后又断开&#xff0c;不断地弹出信任的弹窗。 刚开始以为是数据线问题&#x…...

GPIO外设

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

C++——priority_queue模拟实现

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

计算机网络基础:DOS命令、批处理脚本常见命令

目录 1. DOS 基础命令 1. echo 、 > 编写文件 2. type 读取文件 3. copy con 整段编写 4. attrib 命令 5. 快速生成空文件 6. 修改关联性 7. 关机shutdown 8. 复制文件copy、移动文件move 9. 重命名ren 2. 批处理 2.1 简单显示 2.2 死循环 2.3 定时关机小程序 …...