文章目录
- 前言
- Article 模型里混合了内容和互动状态
- 分类筛选不改 articles 本身
- 点赞要同时改 isLiked 和 likes
- 收藏只改布尔值,不影响计数
- 精选卡片是固定取第三篇文章
- 这个页面的价值在于状态口径清楚
- 完整代码
- 最后总结
前言
内容流页面看起来只是文章卡片堆起来,但只要加上分类、点赞、收藏,状态关系就开始变复杂。分类改变的是列表视图,点赞和收藏改变的是文章本身;这两类状态如果混在一起写,很容易出现“筛选后点赞丢失”或者“回到全部列表状态不一致”的问题。
这个示例的实现比较适合拿来练内容列表的基本功:articles是主数据源,selectedCategory只负责过滤视图,点赞和收藏都回写到对应文章对象。它没有做搜索、详情跳转和分页加载,所以重点可以放在列表状态怎么保持一致上。
Article 模型里混合了内容和互动状态
Article_nyyr既保存文章展示信息,也保存用户对文章的互动状态。
interfaceArticle_nyyr{id:numbertitle:stringauthor:stringavatar:stringreadTime:numbercategory:stringlikes:numbercomments:numberisLiked:booleanisBookmarked:booleansummary:stringpublishDate:string}title、author、summary、publishDate决定卡片内容;category用来做筛选;likes、comments、isLiked、isBookmarked决定底部互动区怎么显示。
这个模型在示例里很直观,但真实项目里可能会把互动状态拆出去。原因是文章内容通常来自推荐流接口,而点赞、收藏状态可能来自用户关系接口,两者更新频率并不一样。教学案例放在一起,能更容易看清 UI 联动。
分类筛选不改 articles 本身
selectedCategory保存当前选中的分类,filteredgetter 根据它返回要展示的文章列表。
@StateselectedCategory:string='全部'@Statecategories:string[]=['全部','技术','设计','AI','生活']getfiltered():Article_nyyr[]{if(this.selectedCategory==='全部')returnthis.articlesreturnthis.articles.filter(a=>a.category===this.selectedCategory)}这里最重要的是:筛选只改变视图,不改变articles。点“设计”时,列表只显示设计类文章;切回“全部”时,原来的文章数组还在,点赞和收藏状态也不会丢。
顶部分类标签的选中态也来自selectedCategory。
Text(cat).fontColor(this.selectedCategory===cat?'#FFF':'#666').backgroundColor(this.selectedCategory===cat?'#007AFF':'#F0F0F0').onClick(()=>this.selectedCategory=cat)这让一个状态同时控制筛选结果和标签样式。写内容流时,这种单一状态源会比到处传布尔值稳很多。
点赞要同时改 isLiked 和 likes
toggleLike(id)不是只切换红心图标,它还会同步增减点赞数。
toggleLike(id:number){this.articles=this.articles.map(a=>{if(a.id===id){a.likes=a.isLiked?a.likes-1:a.likes+1;a.isLiked=!a.isLiked}returna})}这里的顺序也值得看:先根据旧的isLiked判断点赞数该加还是减,再翻转isLiked。如果先翻转再算数量,很容易把加减方向写反。
这段代码仍然是本地乐观更新,没有接口失败回滚。真实产品里,点赞通常会先让 UI 立即变,再请求服务端;如果请求失败,要么回滚,要么做重试提示。示例里没有这层网络状态,所以教程里不要把它讲成完整点赞系统。
收藏只改布尔值,不影响计数
收藏逻辑比点赞简单,只有isBookmarked一个字段。
toggleBookmark(id:number){this.articles=this.articles.map(a=>{if(a.id===id){a.isBookmarked=!a.isBookmarked}returna})}列表底部根据它切换收藏图标。
Text(article.isBookmarked?'🔖':'📑').onClick(()=>this.toggleBookmark(article.id))它不会影响文章排序,也不会把文章加入单独的收藏列表。当前实现只是“在当前文章流里标记收藏状态”。如果后续要做收藏页,需要额外从articles里筛isBookmarked,或者维护独立收藏数据。
精选卡片是固定取第三篇文章
顶部“今日精选”不是根据筛选结果取第一篇,也不是根据点赞数计算出来,而是直接展示this.articles[2]。
Text(this.articles[2].title)Text(this.articles[2].summary)Text(this.articles[2].author)这点很容易被旧稿讲偏。它现在只是一个固定精选位,分类切换不会影响它,点赞收藏也没有入口作用在这张精选卡片上。如果要做真正的精选模块,至少应该有isFeatured字段,或者用推荐接口返回专门的精选文章。
右上角搜索图标也是纯展示,没有绑定搜索输入或点击事件。当前页面可交互的部分只有分类、点赞和收藏。
这个页面的价值在于状态口径清楚
这页最值得借的不是卡片阴影或渐变,而是状态口径:主数据只有articles,分类是视图条件,点赞和收藏都更新主数据。这样即使在筛选后的列表里操作某篇文章,回到全部列表时状态仍然一致。
继续扩展时,我会先加文章详情跳转,再补搜索和分页。分页时要特别注意点赞后的状态合并:新数据回来以后,不能把用户刚刚点过的本地状态冲掉。
完整代码
下面保留案例完整代码,方便你直接对照学习、复制和重构。
import{router}from'@kit.ArkUI'interfaceArticle_nyyr{id:numbertitle:stringauthor:stringavatar:stringreadTime:numbercategory:stringlikes:numbercomments:numberisLiked:booleanisBookmarked:booleansummary:stringpublishDate:string}@Entry@Componentstruct FeaturedArticleFeed{@Statearticles:Article_nyyr[]=[{id:1,title:'HarmonyOS 5.0 新特性深度解析',author:'技术达人',avatar:'👨💻',readTime:8,category:'技术',likes:234,comments:18,isLiked:false,isBookmarked:false,summary:'深入探讨鸿蒙5.0在应用开发、分布式能力和性能优化方面的重大更新...',publishDate:'2小时前'},{id:2,title:'从零开始学产品设计:UI/UX 入门指南',author:'设计师小王',avatar:'🎨',readTime:12,category:'设计',likes:567,comments:42,isLiked:true,isBookmarked:true,summary:'产品设计不仅仅是视觉美观,更需要深入理解用户心理和业务逻辑...',publishDate:'5小时前'},{id:3,title:'2026年AI工具推荐:提升工作效率50%',author:'AI探索者',avatar:'🤖',readTime:6,category:'AI',likes:1203,comments:89,isLiked:false,isBookmarked:false,summary:'精选10款AI工具,覆盖写作、编程、设计、数据分析等多个场景...',publishDate:'昨天'},{id:4,title:'极简主义生活方式:断舍离的实践哲学',author:'生活美学',avatar:'🌿',readTime:5,category:'生活',likes:788,comments:56,isLiked:true,isBookmarked:false,summary:'在信息爆炸的时代,学会取舍反而是一种生产力...',publishDate:'2天前'},]@StateselectedCategory:string='全部'@Statecategories:string[]=['全部','技术','设计','AI','生活']getfiltered():Article_nyyr[]{if(this.selectedCategory==='全部')returnthis.articlesreturnthis.articles.filter(a=>a.category===this.selectedCategory)}toggleLike(id:number){this.articles=this.articles.map(a=>{if(a.id===id){a.likes=a.isLiked?a.likes-1:a.likes+1;a.isLiked=!a.isLiked}returna})}toggleBookmark(id:number){this.articles=this.articles.map(a=>{if(a.id===id){a.isBookmarked=!a.isBookmarked}returna})}build(){Column(){Row(){Image($r('app.media.ic_back')).width(24).height(24).onClick(()=>router.back())Text('发现精选').fontSize(18).fontWeight(FontWeight.Bold).layoutWeight(1).textAlign(TextAlign.Center)Text('🔍').fontSize(20)}.width('100%').padding({left:16,right:16,top:12,bottom:12})// CategoriesScroll(){Row(){ForEach(this.categories,(cat:string)=>{Text(cat).fontSize(13).fontColor(this.selectedCategory===cat?'#FFF':'#666').backgroundColor(this.selectedCategory===cat?'#007AFF':'#F0F0F0').borderRadius(16).padding({left:16,right:16,top:6,bottom:6}).margin({right:8}).onClick(()=>this.selectedCategory=cat)})}.padding({left:16,right:16})}.scrollable(ScrollDirection.Horizontal).width('100%').margin({bottom:12})// FeaturedColumn(){Text('🔥 今日精选').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#FF4757').margin({bottom:8})Text(this.articles[2].title).fontSize(17).fontWeight(FontWeight.Bold).fontColor('#FFF').margin({bottom:4})Text(this.articles[2].summary).fontSize(12).fontColor('#FFFFFFCC').lineHeight(18).margin({bottom:8})Row(){Text(this.articles[2].author).fontSize(12).fontColor('#FFFFFFAA')Text(\` · \${this.articles[2].readTime}分钟阅读\`).fontSize(12).fontColor('#FFFFFFAA')}}.width('calc(100% - 32vp)').backgroundColor('#007AFF').borderRadius(16).padding(20).linearGradient({colors:[['#667EEA',0.0],['#764BA2',1.0]],angle:135}).alignItems(HorizontalAlign.Start).margin({bottom:16})// Article_nyyr listList({space:12}){ForEach(this.filtered,(article:Article_nyyr)=>{ListItem(){Column(){Row(){Text(article.avatar).fontSize(32).margin({right:12})Column(){Text(article.author).fontSize(13).fontColor('#555').fontWeight(FontWeight.Medium)Text(article.publishDate).fontSize(11).fontColor('#AAA').margin({top:2})}.layoutWeight(1).alignItems(HorizontalAlign.Start)Text(article.category).fontSize(11).fontColor('#007AFF').backgroundColor('#E8F0FE').borderRadius(8).padding({left:8,right:8,top:3,bottom:3})}.width('100%').margin({bottom:8})Text(article.title).fontSize(16).fontWeight(FontWeight.Bold).fontColor('#333').margin({bottom:6})Text(article.summary).fontSize(13).fontColor('#777').lineHeight(20).margin({bottom:10}).maxLines(2).textOverflow({overflow:TextOverflow.Ellipsis})Row(){Text(\`⏱ \${article.readTime}min\`).fontSize(12).fontColor('#AAA')Blank()Text(article.isLiked?'❤️':'🤍').fontSize(16).margin({right:4}).onClick(()=>this.toggleLike(article.id))Text(\`\${article.likes}\`).fontSize(12).fontColor('#888').margin({right:12})Text('💬').fontSize(16).margin({right:4})Text(\`\${article.comments}\`).fontSize(12).fontColor('#888').margin({right:12})Text(article.isBookmarked?'🔖':'📑').fontSize(16).onClick(()=>this.toggleBookmark(article.id))}.width('100%')}.width('100%').backgroundColor('#FFF').borderRadius(14).padding(16).shadow({radius:4,color:'#0000001A',offsetY:2})}})}.layoutWeight(1).padding({left:16,right:16})}.width('100%').height('100%').backgroundColor('#F5F5F5')}}最后总结
精选文章流页面 这个案例并不靠复杂技巧取胜,它更像一个结构扎实、逻辑完整的业务页面样板。把这篇文章里的状态设计、函数组织和页面分层吃透之后,你再去写同类型功能,速度会明显快很多。