【HarmonyOS NEXT星河版开发学习】综合测试案例-各平台评论部分

 

目录

 前言

功能展示

整体页面布局

最新和最热 

写评论 

点赞功能 

界面构建

 初始数据的准备

 列表项部分的渲染

底部区域 

index部分 

 知识点概述

List组件

List组件简介

ListItem组件详解

ListItemGroup组件介绍

ForEach循环渲染

列表分割线设置

列表排列方向设置

索引值计算规则

@Prop装饰器 

@Prop注解的基本作用

@Prop注解的限制条件

@Prop注解的使用规则

@Prop注解的具体使用场景

全套代码

BottomCom部分

InfoCom部分

InfoItem部分

CommentDate部分

Index部分


个人主页→VON

收录专栏→鸿蒙开发小型案例总结​​​​​

基础语法部分会发布于github 和 gitee上面(暂未发布)

由于一些个人的特殊原因,基础部分的代码迟迟未进行上传,我会尽快进行整理发布后会第一时间进行通知,希望大家多多谅解

 前言

鸿蒙基础部分到这里也要和大家说再见了,此案例所用到的知识点众多构建过程较为复杂。希望读者们能够认真观看。我也会尽力帮助大家梳理代码以及各各部分的逻辑及其思路。

此案例可以应用于多个地方,比如博客,短视频app等都可以进行应用

功能展示

整体页面布局

整体分为三大部分,分别为头部、中间、底部。认真观察不难发现头部区域采用了Row,中间区域采用了List,底部区域采用了两个Row。整体布局为层叠布局。 

最新和最热 

 

最新和最热点击时显示的效果不同,并且列表所展示的也不同。点击最新时评论的内容会根据时间进行排序,最热时会根据点赞数进行排序。

写评论 

 自己可以在写评论处发布评论,列表会根据评论进行渲染。

点赞功能 

 

界面构建

由于代码的长度限制,过于基础的部分不在进行逐一讲解,希望大家谅解

 最新和最热是两个按钮组件,Extend装饰器进行修改,当未被选中时是一种状态,被选中时是另外一种状态。这两种状态的样式我是根据最左侧的颜色来进行适当调整的,大家也可以效仿。

 初始数据的准备

 初始数据是自己进行构建的一些数据,这些数据被打包在CommentDate中,其中点赞数和等级是随机的,其他部分都是自定义的。因为这是一个离线的静态页面,并没有实现交互功能所以目前只能这样来代替。

 列表项部分的渲染

渲染部分用到了CommentDate中的数据来进行逐一渲染的,因为实现交互的是子组件,但是要修改的部分是父组件的一些内容,所以对于可变的数据用到了Prop装饰器进行的装饰。 

底部区域 

底部区域实现了双向绑定,当输入评论并且单击回车键后数据会进行增加。并且会排在首位,也就是下标为0的位置增添了一个全新的数据。 

index部分 

 

一些函数的定义及其功能的实现全都在index页面来进行实现的,所以这一界面要尽量保持简介,每一部分都可以进行抽取单独进行ui界面的构建,通过import来进行导入即可。 

 知识点概述

List组件

鸿蒙开发中的List组件是一个功能强大且常用的UI组件,用于呈现连续的数据列表。

List组件简介

  • 基础定义:List组件是一种容器组件,用于展示一系列相同宽度的列表项,适合连续、多行呈现同类数据,例如图片和文本。
  • 参数配置:List组件接受三个主要参数:space(子组件主轴方向的间隔)、initialIndex(初次加载时视口起始位置显示的item索引)、scroller(可滚动组件的控制器)。

ListItem组件详解

  • 基本概念:ListItem是具体的列表项组件,必须配合List使用,用于展示每个具体的数据项。
  • 参数设置:主要参数包括selectable(是否可被鼠标框选)和swipeAction(设置划出组件的属性)。

ListItemGroup组件介绍

  • 功能描述:用于展示列表项分组,宽度默认充满List组件,也必须配合List使用。
  • 主要参数:header(头部组件)、footer(尾部组件)和space(列表项间距)。

ForEach循环渲染

  • 应用场景:当列表由多个相似或重复的列表项组成时,为减少代码冗余,可以使用ForEach进行循环渲染。
  • 工作原理:ForEach接口基于数组类型数据进行循环渲染,需与容器组件配合使用,如List。

列表分割线设置

  • 功能解释:通过divider属性设置列表项之间的分割线样式,提升视觉上的区分度和美观性。
  • 参数说明:主要参数包括strokeWidth(分割线的线宽)、color(分割线的颜色)、startMargin和endMargin(分割线距离列表侧边起始端和结束端的距离)。

列表排列方向设置

  • 垂直排列:List默认采用垂直排列方式,即列表项按垂直方向线性排列。
  • 水平排列:通过设置listDirection属性为Axis.Horizontal,可以实现列表项的水平排列。

索引值计算规则

  • 规则概述:索引值用来确定列表项在列表中的具体位置,初次加载时默认从0开始。
  • 细节掌握:初始索引可以通过initialIndex参数手动设置,但需确保索引值不超过列表项总数。

@Prop装饰器 

@Prop是一个方便的注解,用于在鸿蒙应用开发中实现组件之间的数据传递

在现代软件开发中,组件化和数据传递是提高开发效率和代码可维护性的关键环节。特别是在鸿蒙这类分布式操作系统中,对组件间数据传递机制的优化尤为重要。下面将详细探讨@Prop注解的作用、限制条件、使用规则以及具体的使用场景。

@Prop注解的基本作用

  • 单向数据同步:@Prop注解主要用于实现组件间的单向数据同步。这意味着,当父组件的状态发生改变时,这些改变会通过@Prop注解传递给子组件,但子组件对这些属性的修改不会影响到父组件。
  • 支持的数据类型:@Prop能够处理各种基本数据类型,包括字符串、数字、布尔值和枚举类型。这保证了其在不同场景下的灵活性和适用性。

@Prop注解的限制条件

  • 复杂类型的深拷贝:当@Prop涉及到复杂数据类型时(如对象或数组),会进行深拷贝操作。这一过程中,除了基本类型(如字符串、数字)、Map、Set、Date和Array外,其他类型可能会丢失。
  • 使用场景限制:@Prop不能在@Entry装饰的自定义组件中使用。这限定了@Prop的使用范围,通常仅适用于页面级组件。

@Prop注解的使用规则

  • 参数配置:@Prop不需要特定参数,其同步类型为单向同步。
  • 类型要求严格:使用@Prop时必须明确指定被装饰变量的具体类型,不允许使用any类型,也不允许使用undefined和null作为默认值。

@Prop注解的具体使用场景

  • 简单数据类型同步:例如,父组件中的@State状态可以通过@Prop注解传递给子组件。如果父组件的状态发生更新,子组件的@Prop也会相应更新。但如果子组件尝试修改这些属性,更改不会反映到父组件中。
  • 数组项同步:当父组件中的@State数组项更新时,子组件中对应的@Prop也会同步更新。例如,父组件可以包含一个数字数组,每个元素用来初始化子组件中的一个@Prop。同样,子组件对@Prop的修改不会反映到父组件中。
  • 类对象属性同步:父组件中的@State类对象可以用来初始化子组件的@Prop。任何父组件中对象属性的更新都会同步到子组件,但子组件对@Prop的修改依旧不会反向同步到父组件。

全套代码

BottomCom部分

@Component
struct BottomCom {
  @State txt:string=''
  onSubmitComment=(content:string)=>{}
  build() {
    Row(){
      Row(){
        Image($r('app.media.edit'))
          .width(20)
          .margin({left:10})
        TextInput({
          placeholder:'写评论...',
          // 双向绑定
          text:$$this.txt
        })
          .backgroundColor(Color.Transparent)
          .fontSize(18)
          // 回车
          .onSubmit(()=>{
            this.onSubmitComment(this.txt)
          })
      }
      .height(40)
      .backgroundColor('#f5f6f5')
      .borderRadius(20)
      .margin({left:15,right:20,top:10,bottom:10})
      .layoutWeight(1)

      Image($r('app.media.love_stare'))
        .width(25)
      Image($r("app.media.like_stare"))
        .width(25)
        .margin({left:15,right:10})

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

export default BottomCom

InfoCom部分

import BottomCom from './BottomCom'
@Extend(Button)
function ButStyle(click:boolean){
  .fontSize(12)
  .border({width:1,color:click?'#fff':'#ffbeb7b7'})
  .width(46)
  .height(32)
  .padding({left:5,right:5})
  .fontColor(click ? '#80555858' :'#ff1f1e1e')
  .backgroundColor(click ? '#fff' : '#1ae0e0e0')
}
@Component
struct InfoCom {
  @State click:boolean=true
  onSort=(type:number)=>{

  }
  build() {
    Row(){
      Text('全部评论')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
      Row(){
        Button('最新')
          .ButStyle(!this.click)
          .onClick(()=>{
            this.click=true
            this.onSort(0)
          })
        Button('最热')
          .ButStyle(this.click)
          .onClick(()=>{
            this.click=false
            this.onSort(1)
          })
      }
    }
    .justifyContent(FlexAlign.SpaceBetween)
    .padding({left:20,right:20})
    .width('100%')
    .height(60)
  }
}
export default InfoCom

InfoItem部分

import { CommentDate } from '../model/CommentDate'

@Component
struct InfoItem {
  @Prop itemObj:CommentDate
  @Prop index:number
  onLikeClick=(index:number)=>{

  }
  build() {
    // 列表项组件
    Column(){
      Row(){
        // 头像
        Image(this.itemObj.avatar)
          .width(30)
          .borderRadius(15)
          .margin({top:10,right:5})
        // 昵称
        Text(this.itemObj.name)
          .fontColor('#808d8585')
          .fontSize(14)
          .margin({top:10,left:5})
        // 等级
        Image(this.itemObj.levelIcon)
          .width(20)
          .margin({left:10,top:10})
      }
      // 评论内容
      Text(this.itemObj.commentext)
        .fontSize(13)
        .fontWeight(700)
        .margin({top:10,left:40})
      Row(){
        // 时间
        Text(this.itemObj.timeString)
          .fontSize(10)
          .fontColor(Color.Gray)
        // 点赞
        Row(){
          Image(this.itemObj.islike ? $r('app.media.like_end') : $r('app.media.like_stare'))
            .width(12)
          Text(this.itemObj.likenum.toString())
            .fontSize(10)
            .fontColor(this.itemObj.islike ? Color.Red : Color.Gray)
        }
        .onClick(()=>{
          this.onLikeClick(this.index)
        })
      }
      .width('100%')
      .padding({left:40,right:20,top:15})
      .justifyContent(FlexAlign.SpaceBetween)
    }
    .alignItems(HorizontalAlign.Start)
    .padding({left:15,top:10})
  }
}

export default InfoItem

CommentDate部分

// 准备评论数据类
export class CommentDate{
  avatar:ResourceStr;// 头像
  name:string;// 昵称
  level:number;// 用户等级
  likenum:number;// 点赞数量
  commentext:string;// 评论内容
  islike:boolean;// 是否喜欢
  levelIcon:Resource;// level等级
  timeString:string;// 发布时间
  time:number // 时间戳

  constructor(avatar: ResourceStr, name: string, level: number, likenum: number, commentext: string,
              islike: boolean,time:number) {
    this.avatar=avatar
    this.name=name
    this.level=level
    this.likenum=likenum
    this.commentext=commentext
    this.islike=islike
    this.time=time
    this.levelIcon=this.convertLevel(this.level)
    this.timeString=this.convertTime(time)
  }

  //时间转换函数
  convertTime(time:number){
    const currentTimestamp = new Date().getTime();
    // 转换为秒
    const timeDifference = (currentTimestamp-time)/1000;
    console.log(timeDifference.toString())
    if(timeDifference<0 || timeDifference==0){
      return '刚刚';
    }else if(timeDifference<60){
      return `${Math.floor(timeDifference)}秒前`;
    }else if(timeDifference<3600){
      return `${Math.floor(timeDifference/60)}分钟前`;
    }else if(timeDifference<86400){
      return `${Math.floor(timeDifference/3600)}小时前`;
    }else if(timeDifference<604800){
      return `${Math.floor(timeDifference/86400)}天前`;
    }else if(timeDifference<2592000){
      return `${Math.floor(timeDifference/604800)}周前`;
    }else if(timeDifference<31536000){
      return `${Math.floor(timeDifference/2592000)}个月前`;
    }else{
      return `${Math.floor(timeDifference/31536000)}年前`;
    }
  }

  // 等级图片获取
  convertLevel(Level:number){
    const iconList=[
      $r('app.media.lv1'),
      $r('app.media.lv2'),
      $r('app.media.lv3'),
      $r('app.media.lv4'),
      $r('app.media.lv5'),
      $r('app.media.lv6')
    ]
    return iconList[Level]
  }
}

// 封装一个方法,创建假数据
export const createListRange=():CommentDate[]=>{
  let result:CommentDate[]=new Array()
  result=[
    new CommentDate($r('app.media.tx_01'),'JohnYan',Math.floor(Math.random()*6),Math.floor(Math.random()*100),'要是那天,我抓住你就好了',false,1705850201128),
    new CommentDate($r('app.media.tx_03'),'cv工程师',Math.floor(Math.random()*6),Math.floor(Math.random()*100),'故事不长,也不难讲,相识一场,爱而不得',false,1643800201128),
    new CommentDate($r('app.media.tx_04'),'风行水上',Math.floor(Math.random()*6),Math.floor(Math.random()*100),'后来啊,书没有读好,喜欢的人也没有在一起',false,1715850201128),
    new CommentDate($r('app.media.tx_05'),'枫以',Math.floor(Math.random()*6),Math.floor(Math.random()*100),'你根本忘不了一个认认真真爱过的人,你以为错过的是一个人,其实你错过的是一整个人生',false,1680850201128),
    new CommentDate($r('app.media.tx_06'),'幼稚园里的幼稚鬼',Math.floor(Math.random()*6),Math.floor(Math.random()*100),'有些伤痛即使已经痊愈,也会留下难以愈合的伤疤',false,1705850201128),
    new CommentDate($r('app.media.tx_07'),'浮临子',Math.floor(Math.random()*6),Math.floor(Math.random()*100),'不是所有的梦想都会成真,不是所有的伤痛都能愈合,但我们要有勇气继续前行',false,1625050201128),
    new CommentDate($r('app.media.tx_08'),'╭⌒浅浅笑',Math.floor(Math.random()*6),Math.floor(Math.random()*100),'孤独并不可怕,可怕的是渴望有人陪伴而得不到',false,1720850201128),
    new CommentDate($r('app.media.tx_09'),'枕头说它不想醒',Math.floor(Math.random()*6),Math.floor(Math.random()*100),'可惜爱不是写诗 我只能欲言又止',false,1745050201128)
  ]
  return result
}

Index部分

import InfoCom from '../components/InfoCom'
import BottomCom from '../components/BottomCom'
import InfoItem from '../components/InfoItem'
import {CommentDate,createListRange} from '../model/CommentDate'

@Entry
@Component
struct Index {
  // 处理点赞时的方法
  handlelike(index:number){
    // 要有唯一标识
    // AlertDialog.show({
    //   message:index.toString()
    // })
    // 父组件的方法,如果抽取出来,如果直接传递给子组件会有this指向问题,this通常直接指向调用者
    // 需要用箭头函数包一层,保证this还是指向父组件

    // 根据index进行判断
    let itemData=this.commentList[index]
    if(itemData.islike){
      itemData.likenum-=1
    }else{
      itemData.likenum+=1
    }
    itemData.islike= !itemData.islike

    // 对于复杂类型:状态对象,状态数组,只会对第一层数据进行监视变化
    this.commentList.splice(index,1,itemData)
  }
  // 处理提交
  handleSubmit(content:string){
    // 将数据添加到数组最前面
    const newItem:CommentDate=new CommentDate(
      $r('app.media.tx_01'),'我',2,0,content,false,new Date().getTime()
    )
    this.commentList=[newItem,...this.commentList]
  }
  // 处理排序
  handleSort(type:number){
    if(type==0){
      this.commentList.sort((a,b)=>{
        return b.time-a.time
      })
    }else if(type==1){
      this.commentList.sort((a,b)=>{
        return b.likenum-a.likenum
      })
    }

  }
  // 初始化数据
  @State commentList:CommentDate[]=createListRange()
  // 生命周期函数,会自动执行
  aboutToAppear(): void {
    this.handleSort(0)
  }
  build() {
    Column(){
      //头部
      InfoCom({
        onSort:(type:number)=>{
          this.handleSort(type)
        }
      })
      //中间
      List(){
        ForEach(this.commentList,(item:CommentDate,index:number)=>{
          ListItem(){
            // 列表项组件
            InfoItem({
              index:index,
              itemObj:item,
              onLikeClick:(index:number)=>{
                // 此处的this就是父组件
                this.handlelike(index)
              }
            })
          }
        })

      }
      .width('100%')
      .layoutWeight(1)
      //底部
      BottomCom({
        onSubmitComment:(content:string)=>{
          this.handleSubmit(content)
        }
      })
    }
    .width('100%')
    .height('100%')
  }
}

  • 21
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值