相关资源:
- 📎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()函数外,还可以定义其他的成员函数,以及成员变量
注意:
- 成员函数、变量均为私有
- 可以在父组件调用子组件时向成员变量传递数据
// 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('外部添加的点击事件')
})
试一试:
- 添加自定义组件,随意设置内容
- 使用自定义组件,通过点语法设置通用样式
@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
}
试一试:
- 添加自定义组件:
-
- 定义默认的 Builder
- 添加BuilderParam,添加类型,并设置默认的 Builder
- 组件内部使用 BuilderParam
- 外部使用自定义组件,分别测试传递,不传递 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,必须通过参数的方式来传入
核心步骤:
- 自定义组件-定义:
-
- 添加多个 @BuilderParam ,并定义默认值
- 自定义组件-使用
-
- 通过参数的形式传入多个 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到卡片组件内部
需求:
- 调整 卡片自定义组件,支持传入 UI PanelComp(){ // 此处传入 }
思路:
- 直接大括号(尾随闭包)传入只需要设置一个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'
步骤:
- 创建页面 -> 页面与组件不同的地方是有且只有一个入口组件( @Entry修饰),并且在
src/main/resources/base/profile/main_pages.json
有配置好了页面路径
-
- 口诀:一入口,一配置
- router控制页面跳转
- 带参数跳转并获取
3.1. 页面栈
页面栈是用来存储程序运行时页面的一种数据结构,遵循先进后出的原则
页面栈的最大容量为32个页面
3.1.1. pushUrl的情况
先来看看 pushUrl的情况
- 默认打开首页 → 首页入栈
- pushUrl 去详情页 → 详情页入栈
- back 返回上一页 → 详情页出栈
- 此时页面栈中应该只有一个页面
整一个过程中,都可以 router.getLength 进行查看
3.1.2. replaceUrl 的情况
再来看看replaceUrl的情况
- 默认打开首页 → 首页入栈
- replaceUrl 去详情页 → 详情页替换首页,首页销毁
- 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. 路由模式
路由提供了两种不同的跳转模式:
- standard(标准实例模式)
- Single(单实例模式)
不同模式的决定了页面是否会创建多个实例:
- Standard:无论之前是否添加过,一直添加到页面栈【默认】
-
- 场景:适用于每次跳转都呈现全新内容或状态的场景,避免数据展示紊乱(数据变化)
- Single:如果之前加过页面,会使用之前添加的页面【需要添加参数手动修改】
-
- 场景:适用于那些需要保留页面状态或避免重复创建相同页面的场景(数据不变化)
路由模式语法:
3.3. 总结
路由跳转的方式有pushUrl和replaceUrl
pushUrl的跳转要考虑两种模式:标准模式,单例模式
- pushUrl({},模式为标准模式(默认的模式)) -> 页面栈中的表现形式
- pushUrl({},模式为单例模式) -> 页面栈中的表现形式
- replaceUrl() -> 跳转以后的前一个页面自动销毁了
4. 页面和自定义组件的生命周期
组件和页面在创建、显示、销毁的这一整个过程中,会自动执行 一系列的【生命周期钩子】,其实就是一系列的【函数】,让开发者有机会在特定的阶段运行自己的代码
页面生命周期,即被@Entry装饰的组件生命周期,提供以下生命周期接口:
- onPageShow:页面每次显示时触发一次,包括路由过程、应用进入前台等场景,如果不能触发aboutToAppear函数的时候,网络请求数据代码写在onPageShow中
- onPageHide:页面每次隐藏时触发一次,包括路由过程、应用进入后台等场景。
- onBackPress:当用户点击返回按钮时触发。
组件生命周期,即一般用@Component装饰的自定义组件的生命周期,提供以下生命周期接口:
- aboutToAppear:组件即将出现时回调该接口,具体时机为在创建自定义组件的新实例后,在执行其build()函数之前执行,通常在这里发送网络请求获取数据
- onDidBuild:组件build()函数执行完成之后回调该接口,开发者可以在这个阶段进行埋点数据上报等不影响实际UI的功能。不建议在onDidBuild函数中更改状态变量、这可能会导致不稳定的UI表现。
- aboutToDisappear:aboutToDisappear函数在自定义组件析构销毁之前执行。不允许在aboutToDisappear函数中改变状态变量
注意:
- @Entry修饰的页面入口组件有:aboutToAppear、onDidBuild、aboutToDisappear、onPageShow、onPageHide、onBackPress
- @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的状态变化,是应用内全局的状态管理。
咱们来看一张完整的装饰器说明图,咱们后续的学习就围绕着这张图来展开
- 管理组件状态:小框中
- 管理应用状态:大框中
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 装饰的变量是可变的,但是变化不会同步回其父组件
注意:
- 修改父组件数据,会同步更新子组件
- 修改子组件@Prop 修饰的数据,子组件 UI 更新,更新后的数据不会同步给父组件
- 通过回调函数的方式修改父组件的数据,然后触发@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. 评论数据渲染
需求:
- 顶部评论组件 渲染评论数据
- 评论列表组件 渲染评论列表
核心步骤:
- 评论组件:
-
- 定义@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
})
// 分割线
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. 评论点赞-顶部评论
完成顶部评论
需求:
- 点击顶部的❤,切换点赞状态
核心步骤:
- 子组件:
-
- 定义changeLike函数,在点赞时调用
- 数据使用@Prop修饰,父组件状态更新会触发子组件更新
- 父组件:
-
- 传递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. 评论点赞-列表
完成 列表的点赞效果
核心步骤:
- 在父组件:
-
- 列表区域实现changeLike函数,实现点赞逻辑
- 页面的更新,通过数组的 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. 发布评论
最后来完成发布评论
需求:
- 发布评论
- 评论个数同步更新
核心步骤:
- 发布评论
-
- 收集输入框内容
- 点击发布传递给父组件
- 父组件加入数据
- 评论个数同步更新
-
- 将 length 传入子组件
- 子组件内部通过 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"
})
}
}