鸿蒙-2. 知乎评论

阶段案例-知乎回复

1. 底部输入区域

  • 抽离 Nav Comment 组件
  • 使用 Stack 组件底部输入框固定在下方
  • 加上 Scroll 将来页面内容可以滚动
1@Entry
2@Component
3struct Index {
4  build() {
5    Stack({ alignContent: Alignment.Bottom }) {
6      Column() {
7        Scroll() {
8          Column() {
9            // 导航
10            NavComp()
11            // 评论
12            CommentComp()
13            // 分割线
14            Divider()
15              .strokeWidth(8)
16              .color('#f5f5f5')
17            // 回复列表
18            
19          }
20          .padding({ bottom: 50 })
21        }
22      }
23      .width('100%')
24      .height('100%')
25
26      Row({ space: 15 }){
27        TextInput({ placeholder: '回复~' })
28          .layoutWeight(1)
29        Text('发布')
30          .fontColor('#069')
31      }
32      .padding({ left: 15, right: 15 })
33      .width('100%')
34      .height(50)
35      .backgroundColor('#fff')
36      .border({ width: { top: 0.5 }, color: '#e4e4e4' })
37    }
38
39  }
40}
41
42// 导航
43@Component
44struct NavComp {
45  build() {
46    Row() {
47      Row() {
48        Image($r('app.media.ic_public_arrow_left'))
49          .width(16)
50          .aspectRatio(1)
51        // svg 图标可以使用填充颜色
52        // .fillColor('red')
53      }
54      .width(24)
55      .aspectRatio(1)
56      .backgroundColor('#f5f5f5')
57      .borderRadius(12)
58      .justifyContent(FlexAlign.Center)
59      .margin({ left: 16 })
60
61      Text('评论回复')
62        .layoutWeight(1)
63        .textAlign(TextAlign.Center)
64        .padding({ right: 40 })
65    }
66    .height(40)
67    .border({ width: { bottom: 0.5 }, color: '#e4e4e4' })
68  }
69}
70
71// 评论
72@Component
73struct CommentComp {
74  build() {
75    Row() {
76      Image($r('app.media.avatar'))
77        .width(32)
78        .aspectRatio(1)
79        .borderRadius(16)
80      Column({ space: 5 }) {
81        Text('周杰伦')
82          .width('100%')
83          .fontWeight(FontWeight.Bold)
84          .fontSize(15)
85        Text('大理石能雕刻出肌肉和皮肤的质感,那个年代的工匠好牛啊')
86          .width('100%')
87        Row() {
88          Text('10-21 · IP属地北京')
89            .fontSize(12)
90            .fontColor('#c3c4c5')
91          Row({ space: 4 }) {
92            Image($r('app.media.ic_public_heart'))
93              .width(14)
94              .aspectRatio(1)
95              .fillColor('#c3c4c5')
96            Text('100')
97              .fontSize(12)
98              .fontColor('#c3c4c5')
99          }
100        }
101        .width('100%')
102        .justifyContent(FlexAlign.SpaceBetween)
103      }
104      .layoutWeight(1)
105      .padding({ left: 10 })
106    }
107    .padding(15)
108    .alignItems(VerticalAlign.Top)
109  }
110}

2. 静态回复列表

  • 参考评论组件,使用 ForEach 循环相同的回复容器
1Column() {
2  Text('回复 100')
3    .width('100%')
4    .fontWeight(600)
5  ForEach([1, 2, 3, 4, 5, 6, 7], () => {
6    Row() {
7      Image($r('app.media.avatar'))
8        .width(32)
9        .aspectRatio(1)
10        .borderRadius(16)
11      Column({ space: 5 }) {
12        Text('周杰伦')
13          .width('100%')
14          .fontWeight(FontWeight.Bold)
15          .fontSize(15)
16        Text('大理石能雕刻出肌肉和皮肤的质感,那个年代的工匠好牛啊')
17          .width('100%')
18        Row() {
19          Text('10-21 · IP属地北京')
20            .fontSize(12)
21            .fontColor('#c3c4c5')
22          Row({ space: 4 }) {
23            Image($r('app.media.ic_public_heart'))
24              .width(14)
25              .aspectRatio(1)
26              .fillColor('#c3c4c5')
27            Text('100')
28              .fontSize(12)
29              .fontColor('#c3c4c5')
30          }
31        }
32        .width('100%')
33        .justifyContent(FlexAlign.SpaceBetween)
34      }
35      .layoutWeight(1)
36      .padding({ left: 10 })
37    }
38    .padding({ top: 15, bottom: 15 })
39    .alignItems(VerticalAlign.Top)
40  })
41
42}
43.padding(15)

3. 实现渲染

  • 使用 class 定义好回复数据模型 ReplyItem
  • 初始化一些模拟数据
  • 完成页面渲染
models/index.ets
1export class ReplyItem {
2  id: number
3  avatar: string
4  author: string
5  content: string
6  time: string
7  area: string
8  likeNum: number
9  likeFlag?: boolean
10}
11
12export const replyList: ReplyItem[] = [
13  {
14    id: 1,
15    avatar: 'https://picx.zhimg.com/027729d02bdf060e24973c3726fea9da_l.jpg?source=06d4cd63',
16    author: '偏执狂-妄想家',
17    content: '更何况还分到一个摩洛哥[惊喜]',
18    time: '11-30',
19    area: '海南',
20    likeNum: 34
21  },
22  {
23    id: 2,
24    avatar: 'https://pic1.zhimg.com/v2-5a3f5190369ae59c12bee33abfe0c5cc_xl.jpg?source=32738c0c',
25    author: 'William',
26    content: '当年希腊可是把1:0发挥到极致了',
27    time: '11-29',
28    area: '北京',
29    likeNum: 58
30  },
31  {
32    id: 3,
33    avatar: 'https://picx.zhimg.com/v2-e6f4605c16e4378572a96dad7eaaf2b0_l.jpg?source=06d4cd63',
34    author: 'Andy Garcia',
35    content: '欧洲杯其实16队球队打正赛已经差不多,24队打正赛意味着正赛阶段在小组赛一样有弱队。',
36    time: '11-28',
37    area: '上海',
38    likeNum: 10
39  },
40  {
41    id: 4,
42    avatar: 'https://picx.zhimg.com/v2-53e7cf84228e26f419d924c2bf8d5d70_l.jpg?source=06d4cd63',
43    author: '正宗好鱼头',
44    content: '确实眼红啊,亚洲就没这种球队,让中国队刷',
45    time: '11-27',
46    area: '香港',
47    likeNum: 139
48  },
49  {
50    id: 5,
51    avatar: 'https://pic1.zhimg.com/v2-eeddfaae049df2a407ff37540894c8ce_l.jpg?source=06d4cd63',
52    author: '柱子哥',
53    content: '我是支持扩大的,亚洲杯欧洲杯扩到32队,世界杯扩到64队才是好的,世界上有超过200支队伍,欧洲区55支队伍,亚洲区47支队伍,即使如此也就六成出现率',
54    time: '11-27',
55    area: '旧金山',
56    likeNum: 29
57  },
58  {
59    id: 6,
60    avatar: 'https://picx.zhimg.com/v2-fab3da929232ae911e92bf8137d11f3a_l.jpg?source=06d4cd63',
61    author: '飞轩逸',
62    content: '禁止欧洲杯扩军之前,应该先禁止世界杯扩军,或者至少把亚洲名额一半给欧洲。',
63    time: '11-26',
64    area: '里约',
65    likeNum: 100
66  }
67]
pages/Index.ets
1import { ReplyItem, replyList } from '../models'
2@Entry
3@Component
4struct Index {
5
6  @State
7  replyList: ReplyItem[] = replyList
8
9  build() {
10    Stack({ alignContent: Alignment.Bottom }) {
11      Column() {
12        Scroll() {
13          Column() {
14            // 导航
15            NavComp()
16            // 评论
17            CommentComp()
18            // 分割线
19            Divider()
20              .strokeWidth(8)
21              .color('#f5f5f5')
22            // 回复列表
23            Column() {
24              Text('回复 100')
25                .width('100%')
26                .fontWeight(600)
27              ForEach(
28                this.replyList, 
29                (item: ReplyItem) => {
30                  Row() {
31                    Image(item.avatar)
32                      .width(32)
33                      .aspectRatio(1)
34                      .borderRadius(16)
35                    Column({ space: 5 }) {
36                      Text(item.author)
37                        .width('100%')
38                        .fontWeight(FontWeight.Bold)
39                        .fontSize(15)
40                      Text(item.content)
41                        .width('100%')
42                      Row() {
43                        Text(`${item.time} · IP属地${item.area}`)
44                          .fontSize(12)
45                          .fontColor('#c3c4c5')
46                        Row({ space: 4 }) {
47                          Image($r('app.media.ic_public_heart'))
48                            .width(14)
49                            .aspectRatio(1)
50                            .fillColor('#c3c4c5')
51                          Text(item.likeNum.toString())
52                            .fontSize(12)
53                            .fontColor('#c3c4c5')
54                        }
55                      }
56                      .width('100%')
57                      .justifyContent(FlexAlign.SpaceBetween)
58                    }
59                    .layoutWeight(1)
60                    .padding({ left: 10 })
61                  }
62                  .padding({ top: 15, bottom: 15 })
63                  .alignItems(VerticalAlign.Top)
64                },
65                // key 有默认你规则
66                // key 为了元素复用
67                // 如果没有写,会自动生成一个key,index_ + JSON.stringify(item),不建议不写
68                // (item: ReplyItem) => item.id.toString() 写一个ID做唯一标识,需要key也更新才能更新对应UI
69                // item => id + likeNum + likeFlag 把需要更新的字段合在一起当做key
70                // ({ id, likeNum, likeFlag }) => JSON.stringify({ id, likeNum, likeFlag })
71
72                // 学习了 @Observed @ObjectLink 这样也可以更新~
73                // (item: ReplyItem) => item.id.toString()
74              )
75
76            }
77            .padding(15)
78          }
79          .padding({ bottom: 50 })
80        }
81      }
82      .width('100%')
83      .height('100%')
84
85      Row({ space: 15 }){
86        TextInput({ placeholder: '回复~' })
87          .layoutWeight(1)
88        Text('发布')
89          .fontColor('#069')
90      }
91      .padding({ left: 15, right: 15 })
92      .width('100%')
93      .height(50)
94      .backgroundColor('#fff')
95      .border({ width: { top: 0.5 }, color: '#e4e4e4' })
96    }
97
98  }
99}

4. 实现点赞

  • 注册点赞区域点击事件
  • 通过索引复制的方式完成数据的更新和UI的更新
1onLike(item: ReplyItem) {
2  const reply = { ...item }
3  if (reply.likeFlag) {
4    reply.likeNum--
5    reply.likeFlag = false
6    promptAction.showToast({ message: '取消点赞' })
7  } else {
8    reply.likeNum++
9    reply.likeFlag = true
10    promptAction.showToast({ message: '点赞成功' })
11  }
12  const index = this.replyList.findIndex(rep => rep.id === reply.id)
13  this.replyList[index] = reply
14}
1Row({ space: 4 }) {
2  Image($r('app.media.heart'))
3    .width(14)
4    .height(14)
5    .fillColor(item.likeFlag ? '#ff6600' : '#c3c4c5')
6    .margin({ right: 4 })
7  Text(item.likeNum.toString())
8    .fontSize(14)
9    .fontColor(item.likeFlag ? '#ff6600' : '#c3c4c5')
10}
11.onClick(() => {
12  this.onLike(item)
13})

6. 进行回复

  • 收集输入框数据
  • 发布评论内容,和情况输入内容
  • 需要扩展头像类型兼容 Resource 类型
models/Index.ets
1export class ReplyItem {
2  id: number
3+  avatar: string | Resource
4  author: string
5  content: string
6  time: string
7  area: string
8  likeNum: number
9  likeFlag?: boolean
10}
pages/Index.ets
1onReply () {
2  const reply: ReplyItem = {
3    id: Math.random(),
4    content: this.content,
5    author: 'Zhousg',
6    avatar: $r('app.media.avatar'),
7    time: '12-01',
8    likeNum: 0,
9    area: '北京'
10  }
11  this.replyList.unshift(reply)
12  this.content = ''
13  promptAction.showToast({ message: '回复成功' })
14}
pages/Index.ets
1Row({ spcae: 15 }) {
2  TextInput({ placeholder: '回复~', text: this.content })
3    .placeholderColor('#c3c4c5')
4    .layoutWeight(1)
5    .onChange((value) => {
6      this.content = value
7    })
8  Text('发布')
9    .fontSize(14)
10    .fontColor('#09f')
11    .onClick(()=>{
12      this.onReply()
13    })
14}