鸿蒙-2. 知乎评论

作为一个移动端开发者,不仅要学习安卓,iOS,Flutter,RN,Vue以及鸿蒙,当下鸿蒙只有部分大厂开发,作为一般中小厂开发者项目实战机会少,当然自己也可以写写demo,保持学习新技术热情。

阶段案例-知乎回复#

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}

评论功能实现可以从不同角度来看,以下是几种常见的实现思路: ### 评论业务设计层面 对于从零开始设计类似乎的评论系统,开发者可根据自身需求,从基础功能出发进行设计。例如,有开发者因在百度上难以找到合适的评论业务方案,且认为乎的评论设计不错,从而决定自己设计评论系统。这意味着要考虑评论的存储、展示、交互等多方面。在存储方面,需要设计合理的数据库表结构来存储评论内容、评论者信息、评论时间、点赞数等;在展示方面,要考虑不同评论层级(如主评论、子评论)的布局;在交互方面,要实现评论的发布、删除、点赞等功能。 ### 数据获取与展示层面 可利用爬虫爬取评论,并结合flask框架做简单的可视化。具体做法是将乎的某个评论内容爬取下来,取出里面的关键字,并按照点赞数排序,形成一个表单,点击查看可以看到原来的内容。这涉及到网络请求、数据解析、数据存储和前端展示等技术。例如,使用Python的`requests`库进行网络请求,使用`BeautifulSoup`库进行数据解析,使用`flask`框架搭建Web服务器进行数据展示。 ```python import requests from bs4 import BeautifulSoup from flask import Flask, render_template_string app = Flask(__name__) def get_zhihu_comments(url): response = requests.get(url) soup = BeautifulSoup(response.text, 'html.parser') # 这里需要根据页面的实际结构来提取评论内容 comments = [] # 示例代码,实际需要根据具体HTML结构修改 comment_elements = soup.find_all('div', class_='comment') for comment in comment_elements: content = comment.find('p').text like_count = int(comment.find('span', class_='like-count').text) comments.append({'content': content, 'like_count': like_count}) comments.sort(key=lambda x: x['like_count'], reverse=True) return comments @app.route('/') def index(): url = 'https://example.com/zhihu-comments' # 替换为实际的评论页面URL comments = get_zhihu_comments(url) html = """ <html> <head> <title>评论可视化</title> </head> <body> <h1>评论可视化</h1> <table> <tr> <th>评论内容</th> <th>点赞数</th> </tr> {% for comment in comments %} <tr> <td><a href="#" onclick="showComment('{{ comment.content }}')">{{ comment.content[:20] }}...</a></td> <td>{{ comment.like_count }}</td> </tr> {% endfor %} </table> <script> function showComment(content) { alert(content); } </script> </body> </html> """ return render_template_string(html, comments=comments) if __name__ == '__main__': app.run(debug=True) ``` ### 前端交互层面 在前端实现类似乎的收起评论效果。可以通过监听滚动条的变化,根据元素在页面上的位置和屏幕可视高度来控制收起评论按钮的显示和隐藏。例如,使用Vue.js框架,在`data`中定义一个变量来控制按钮的显示状态,在`mounted`钩子中监听滚动条事件,在`methods`中编写处理滚动事件的函数,在`beforeDestroy`钩子中移除滚动事件监听。 ```vue <template> <div> <div ref="element"> <!-- 评论内容 --> </div> <button v-if="commentButShow">收起评论</button> </div> </template> <script> export default { data() { return { // 收起评论按钮 commentButShow: false }; }, mounted() { // 监听滚动条的变化 window.addEventListener('scroll', this.handleScroll, true); }, methods: { // 收起评论按钮出现和消失 handleScroll(e) { // 监听滚动条的变化 let t = document.documentElement.scrollTop || document.body.scrollTop; // 获取元素在页面上的top值和高度 let comtheightTop = this.$refs.element.getBoundingClientRect().top; let comtheight = this.$refs.element.offsetHeight; // 兼容性获取屏幕高度 let winHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight; // 屏幕可视高度+元素距离顶部的距离 let height01 = comtheightTop + winHeight; // 屏幕可视高度+元素距离顶部的距离+元素高度 let allHeight = height01 + comtheight; if (comtheightTop < 0) { if (comtheight - Math.abs(comtheightTop) > winHeight) { this.commentButShow = true; } else { this.commentButShow = false; } } else { this.commentButShow = false; } } }, beforeDestroy() { window.removeEventListener("scroll", this.handleScroll); } }; </script> ``` ### 数据模型层面 在开发如鸿蒙NEXT等项目时,需要定义评论相关的实体类。例如,定义一个`ContentModel`类来表示评论内容,包含评论的ID、头像、作者、内容、图片、时间、地区等属性,并通过构造函数进行初始化。 ```typescript export default class ContentModel { id: number = 0; avatar: string = ''; author: string = ''; content: string = ''; images: string | Resource = ''; time: string = ''; area: string = ''; constructor(id: number, avatar: string, author: string, content: string, images: string | Resource, time: string, area: string) { this.id = id; this.avatar = avatar; this.author = author; this.content = content; this.time = time; this.area = area; this.images = images; } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值