当前位置:   article > 正文

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

【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部分

  1. @Component
  2. struct BottomCom {
  3. @State txt:string=''
  4. onSubmitComment=(content:string)=>{}
  5. build() {
  6. Row(){
  7. Row(){
  8. Image($r('app.media.edit'))
  9. .width(20)
  10. .margin({left:10})
  11. TextInput({
  12. placeholder:'写评论...',
  13. // 双向绑定
  14. text:$$this.txt
  15. })
  16. .backgroundColor(Color.Transparent)
  17. .fontSize(18)
  18. // 回车
  19. .onSubmit(()=>{
  20. this.onSubmitComment(this.txt)
  21. })
  22. }
  23. .height(40)
  24. .backgroundColor('#f5f6f5')
  25. .borderRadius(20)
  26. .margin({left:15,right:20,top:10,bottom:10})
  27. .layoutWeight(1)
  28. Image($r('app.media.love_stare'))
  29. .width(25)
  30. Image($r("app.media.like_stare"))
  31. .width(25)
  32. .margin({left:15,right:10})
  33. }
  34. .width('100%')
  35. .height(60)
  36. }
  37. }
  38. export default BottomCom

InfoCom部分

  1. import BottomCom from './BottomCom'
  2. @Extend(Button)
  3. function ButStyle(click:boolean){
  4. .fontSize(12)
  5. .border({width:1,color:click?'#fff':'#ffbeb7b7'})
  6. .width(46)
  7. .height(32)
  8. .padding({left:5,right:5})
  9. .fontColor(click ? '#80555858' :'#ff1f1e1e')
  10. .backgroundColor(click ? '#fff' : '#1ae0e0e0')
  11. }
  12. @Component
  13. struct InfoCom {
  14. @State click:boolean=true
  15. onSort=(type:number)=>{
  16. }
  17. build() {
  18. Row(){
  19. Text('全部评论')
  20. .fontSize(20)
  21. .fontWeight(FontWeight.Bold)
  22. Row(){
  23. Button('最新')
  24. .ButStyle(!this.click)
  25. .onClick(()=>{
  26. this.click=true
  27. this.onSort(0)
  28. })
  29. Button('最热')
  30. .ButStyle(this.click)
  31. .onClick(()=>{
  32. this.click=false
  33. this.onSort(1)
  34. })
  35. }
  36. }
  37. .justifyContent(FlexAlign.SpaceBetween)
  38. .padding({left:20,right:20})
  39. .width('100%')
  40. .height(60)
  41. }
  42. }
  43. export default InfoCom

InfoItem部分

  1. import { CommentDate } from '../model/CommentDate'
  2. @Component
  3. struct InfoItem {
  4. @Prop itemObj:CommentDate
  5. @Prop index:number
  6. onLikeClick=(index:number)=>{
  7. }
  8. build() {
  9. // 列表项组件
  10. Column(){
  11. Row(){
  12. // 头像
  13. Image(this.itemObj.avatar)
  14. .width(30)
  15. .borderRadius(15)
  16. .margin({top:10,right:5})
  17. // 昵称
  18. Text(this.itemObj.name)
  19. .fontColor('#808d8585')
  20. .fontSize(14)
  21. .margin({top:10,left:5})
  22. // 等级
  23. Image(this.itemObj.levelIcon)
  24. .width(20)
  25. .margin({left:10,top:10})
  26. }
  27. // 评论内容
  28. Text(this.itemObj.commentext)
  29. .fontSize(13)
  30. .fontWeight(700)
  31. .margin({top:10,left:40})
  32. Row(){
  33. // 时间
  34. Text(this.itemObj.timeString)
  35. .fontSize(10)
  36. .fontColor(Color.Gray)
  37. // 点赞
  38. Row(){
  39. Image(this.itemObj.islike ? $r('app.media.like_end') : $r('app.media.like_stare'))
  40. .width(12)
  41. Text(this.itemObj.likenum.toString())
  42. .fontSize(10)
  43. .fontColor(this.itemObj.islike ? Color.Red : Color.Gray)
  44. }
  45. .onClick(()=>{
  46. this.onLikeClick(this.index)
  47. })
  48. }
  49. .width('100%')
  50. .padding({left:40,right:20,top:15})
  51. .justifyContent(FlexAlign.SpaceBetween)
  52. }
  53. .alignItems(HorizontalAlign.Start)
  54. .padding({left:15,top:10})
  55. }
  56. }
  57. export default InfoItem

CommentDate部分

  1. // 准备评论数据类
  2. export class CommentDate{
  3. avatar:ResourceStr;// 头像
  4. name:string;// 昵称
  5. level:number;// 用户等级
  6. likenum:number;// 点赞数量
  7. commentext:string;// 评论内容
  8. islike:boolean;// 是否喜欢
  9. levelIcon:Resource;// level等级
  10. timeString:string;// 发布时间
  11. time:number // 时间戳
  12. constructor(avatar: ResourceStr, name: string, level: number, likenum: number, commentext: string,
  13. islike: boolean,time:number) {
  14. this.avatar=avatar
  15. this.name=name
  16. this.level=level
  17. this.likenum=likenum
  18. this.commentext=commentext
  19. this.islike=islike
  20. this.time=time
  21. this.levelIcon=this.convertLevel(this.level)
  22. this.timeString=this.convertTime(time)
  23. }
  24. //时间转换函数
  25. convertTime(time:number){
  26. const currentTimestamp = new Date().getTime();
  27. // 转换为秒
  28. const timeDifference = (currentTimestamp-time)/1000;
  29. console.log(timeDifference.toString())
  30. if(timeDifference<0 || timeDifference==0){
  31. return '刚刚';
  32. }else if(timeDifference<60){
  33. return `${Math.floor(timeDifference)}秒前`;
  34. }else if(timeDifference<3600){
  35. return `${Math.floor(timeDifference/60)}分钟前`;
  36. }else if(timeDifference<86400){
  37. return `${Math.floor(timeDifference/3600)}小时前`;
  38. }else if(timeDifference<604800){
  39. return `${Math.floor(timeDifference/86400)}天前`;
  40. }else if(timeDifference<2592000){
  41. return `${Math.floor(timeDifference/604800)}周前`;
  42. }else if(timeDifference<31536000){
  43. return `${Math.floor(timeDifference/2592000)}个月前`;
  44. }else{
  45. return `${Math.floor(timeDifference/31536000)}年前`;
  46. }
  47. }
  48. // 等级图片获取
  49. convertLevel(Level:number){
  50. const iconList=[
  51. $r('app.media.lv1'),
  52. $r('app.media.lv2'),
  53. $r('app.media.lv3'),
  54. $r('app.media.lv4'),
  55. $r('app.media.lv5'),
  56. $r('app.media.lv6')
  57. ]
  58. return iconList[Level]
  59. }
  60. }
  61. // 封装一个方法,创建假数据
  62. export const createListRange=():CommentDate[]=>{
  63. let result:CommentDate[]=new Array()
  64. result=[
  65. new CommentDate($r('app.media.tx_01'),'JohnYan',Math.floor(Math.random()*6),Math.floor(Math.random()*100),'要是那天,我抓住你就好了',false,1705850201128),
  66. new CommentDate($r('app.media.tx_03'),'cv工程师',Math.floor(Math.random()*6),Math.floor(Math.random()*100),'故事不长,也不难讲,相识一场,爱而不得',false,1643800201128),
  67. new CommentDate($r('app.media.tx_04'),'风行水上',Math.floor(Math.random()*6),Math.floor(Math.random()*100),'后来啊,书没有读好,喜欢的人也没有在一起',false,1715850201128),
  68. new CommentDate($r('app.media.tx_05'),'枫以',Math.floor(Math.random()*6),Math.floor(Math.random()*100),'你根本忘不了一个认认真真爱过的人,你以为错过的是一个人,其实你错过的是一整个人生',false,1680850201128),
  69. new CommentDate($r('app.media.tx_06'),'幼稚园里的幼稚鬼',Math.floor(Math.random()*6),Math.floor(Math.random()*100),'有些伤痛即使已经痊愈,也会留下难以愈合的伤疤',false,1705850201128),
  70. new CommentDate($r('app.media.tx_07'),'浮临子',Math.floor(Math.random()*6),Math.floor(Math.random()*100),'不是所有的梦想都会成真,不是所有的伤痛都能愈合,但我们要有勇气继续前行',false,1625050201128),
  71. new CommentDate($r('app.media.tx_08'),'╭⌒浅浅笑',Math.floor(Math.random()*6),Math.floor(Math.random()*100),'孤独并不可怕,可怕的是渴望有人陪伴而得不到',false,1720850201128),
  72. new CommentDate($r('app.media.tx_09'),'枕头说它不想醒',Math.floor(Math.random()*6),Math.floor(Math.random()*100),'可惜爱不是写诗 我只能欲言又止',false,1745050201128)
  73. ]
  74. return result
  75. }

Index部分

  1. import InfoCom from '../components/InfoCom'
  2. import BottomCom from '../components/BottomCom'
  3. import InfoItem from '../components/InfoItem'
  4. import {CommentDate,createListRange} from '../model/CommentDate'
  5. @Entry
  6. @Component
  7. struct Index {
  8. // 处理点赞时的方法
  9. handlelike(index:number){
  10. // 要有唯一标识
  11. // AlertDialog.show({
  12. // message:index.toString()
  13. // })
  14. // 父组件的方法,如果抽取出来,如果直接传递给子组件会有this指向问题,this通常直接指向调用者
  15. // 需要用箭头函数包一层,保证this还是指向父组件
  16. // 根据index进行判断
  17. let itemData=this.commentList[index]
  18. if(itemData.islike){
  19. itemData.likenum-=1
  20. }else{
  21. itemData.likenum+=1
  22. }
  23. itemData.islike= !itemData.islike
  24. // 对于复杂类型:状态对象,状态数组,只会对第一层数据进行监视变化
  25. this.commentList.splice(index,1,itemData)
  26. }
  27. // 处理提交
  28. handleSubmit(content:string){
  29. // 将数据添加到数组最前面
  30. const newItem:CommentDate=new CommentDate(
  31. $r('app.media.tx_01'),'我',2,0,content,false,new Date().getTime()
  32. )
  33. this.commentList=[newItem,...this.commentList]
  34. }
  35. // 处理排序
  36. handleSort(type:number){
  37. if(type==0){
  38. this.commentList.sort((a,b)=>{
  39. return b.time-a.time
  40. })
  41. }else if(type==1){
  42. this.commentList.sort((a,b)=>{
  43. return b.likenum-a.likenum
  44. })
  45. }
  46. }
  47. // 初始化数据
  48. @State commentList:CommentDate[]=createListRange()
  49. // 生命周期函数,会自动执行
  50. aboutToAppear(): void {
  51. this.handleSort(0)
  52. }
  53. build() {
  54. Column(){
  55. //头部
  56. InfoCom({
  57. onSort:(type:number)=>{
  58. this.handleSort(type)
  59. }
  60. })
  61. //中间
  62. List(){
  63. ForEach(this.commentList,(item:CommentDate,index:number)=>{
  64. ListItem(){
  65. // 列表项组件
  66. InfoItem({
  67. index:index,
  68. itemObj:item,
  69. onLikeClick:(index:number)=>{
  70. // 此处的this就是父组件
  71. this.handlelike(index)
  72. }
  73. })
  74. }
  75. })
  76. }
  77. .width('100%')
  78. .layoutWeight(1)
  79. //底部
  80. BottomCom({
  81. onSubmitComment:(content:string)=>{
  82. this.handleSubmit(content)
  83. }
  84. })
  85. }
  86. .width('100%')
  87. .height('100%')
  88. }
  89. }

声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号