社区页面的第一版是静态展示——五条帖子、三个按钮、点了没反应。这一版给社区加上了完整的动态交互点赞有动画、评论能输入、关注会变色、顶部能发帖、点击用户名能跳转到用户主页。从看起来像真的到用起来像真的这是原型走向产品的关键一步。完整效果数据模型的扩展第一版Post接口只有5个字段这一版扩展到了10个interface Post { user: string; avatar: string; content: string; likes: number; time: string liked: boolean; followed: boolean; comment: string; showComment: boolean comments: string[] }新增的5个字段全部服务于交互字段类型作用likedboolean当前用户是否已点赞followedboolean当前用户是否已关注此人commentstring评论输入框的当前内容showCommentboolean是否展开评论输入框commentsstring[]该帖子的评论列表初始数据中营养师阿杰的liked: true默认已点赞游泳健将的followed: true默认已关注。这样页面加载后就能看到两种状态的视觉差异——已点赞的❤️红色和未点赞的灰色已关注的灰色按钮和未关注的红色按钮。发帖功能顶部输入区Row() { Text().fontSize(28).width(40).height(40).borderRadius(20) .backgroundColor(#F5F5F7).textAlign(TextAlign.Center) TextInput({ placeholder: 分享你的运动瞬间..., text: this.newPost }) .layoutWeight(1).height(40).fontSize(14).backgroundColor(#F5F5F7) .borderRadius(20).padding({ left: 14 }).margin({ left: 10 }) .onChange((v: string) { this.newPost v }) }.width(100%).padding({ left: 16, right: 16, top: 4, bottom: 10 })输入区在导航栏和帖子列表之间。左边是固定的用户头像emoji右边是TextInput。placeholder是灰色提示文字分享你的运动瞬间…用户开始输入后自动消失。onChange把输入内容同步到State newPost变量。发布按钮Text(发布).fontSize(14).fontColor(A).fontWeight(FontWeight.Bold) .onClick(() { if (this.newPost.trim().length 0) { const copy: Post[] [{ user: 我, avatar: , content: this.newPost, likes: 0, time: 刚刚, liked: false, followed: false, comment: , showComment: false, comments: [] }] for (let c: number 0; c this.posts.length; c) { copy.push(this.posts[c]) } this.posts copy this.newPost } })发布按钮在导航栏右侧红色文字。点击时做了三件事检查输入不为空trim后长度大于0创建新帖子对象插入新数组的头部清空输入框新帖子的用户名写死为我头像写死为时间写死为刚刚。这三样应该从登录状态获取——但原型阶段够用。关键操作是this.posts copy——没有用push或unshift而是创建新数组再赋值给State变量。这是ArkUI状态管理的核心要求State检测的是变量引用的变化不是对象内部的变化。直接this.posts.unshift(newPost)虽然修改了数组内容但this.posts仍然指向同一个数组引用框架不会触发重新渲染。点赞交互private toggleLike(idx: number): void { if (this.posts[idx].liked) { this.posts[idx].likes-- } else { this.posts[idx].likes } this.posts[idx].liked !this.posts[idx].liked this.posts this.posts.slice() }点赞逻辑如果已点赞就减1未点赞就加1然后切换liked状态。最后用this.posts.slice()创建新数组——和发帖时的copy数组效果一样都是让State检测到引用变化。点赞按钮的动画Row() { Text(p.liked ? ❤️ : ).fontSize(14) .scale({ x: p.liked ? 1.2 : 1, y: p.liked ? 1.2 : 1 }) .animation({ duration: 200, curve: Curve.EaseOut }) Text( p.likes.toString()).fontSize(12).fontColor(p.liked ? A : T3) }.margin({ right: 24 }).onClick(() { this.toggleLike(idx) })点赞时❤️图标会从1倍放大到1.2倍200毫秒完成。.animation()修饰符让scale变化产生过渡效果——没有这个修饰符图标会瞬间从1倍跳到1.2倍没有动画感。Curve.EaseOut是缓动曲线先快后慢。点击的瞬间图标迅速放大然后慢慢减速到1.2倍。这种曲线符合人的直觉——动作开始时有力结束时柔和。点赞数字的颜色也会变化已点赞时数字变红色A未点赞时是灰色T3。颜色变化不需要animation——它是瞬间切换的和图标的动画形成对比。关注按钮的双态切换Text(p.followed ? 已关注 : 关注).fontSize(11) .fontWeight(FontWeight.Medium) .fontColor(p.followed ? T2 : A) .padding({ left: 10, right: 10, top: 4, bottom: 4 }) .backgroundColor(p.followed ? #F5F5F7 : #FFF0F0) .borderRadius(10) .onClick(() { this.toggleFollow(idx) })关注按钮有两种状态状态文字文字颜色背景色未关注 关注A红色#FFF0F0浅红已关注已关注T2灰色#F5F5F7浅灰未关注时红色醒目吸引点击已关注后变灰色低调减少视觉干扰。这种主操作→已操作的颜色变化是社交产品的标准设计。toggleFollow的实现和toggleLike一样——修改字段、创建新数组private toggleFollow(idx: number): void { this.posts[idx].followed !this.posts[idx].followed this.posts this.posts.slice() }评论系统展开/收起评论输入框private toggleComment(idx: number): void { this.posts[idx].showComment !this.posts[idx].showComment this.posts this.posts.slice() }点击评论按钮切换showComment的值。为true时显示输入框为false时隐藏。评论输入框if (p.showComment) { Row() { TextInput({ placeholder: 写评论..., text: p.comment }) .layoutWeight(1).height(36).fontSize(13).backgroundColor(#F8F7FC) .borderRadius(18).padding({ left: 14 }) .onChange((v: string) { p.comment v }) Text(发送).fontSize(12).fontWeight(FontWeight.Bold).fontColor(A) .padding({ left: 10, right: 10, top: 6, bottom: 6 }) .backgroundColor(#FFF0F0).borderRadius(12).margin({ left: 8 }) .onClick(() { this.sendComment(idx) }) }.width(100%).padding({ top: 6, bottom: 4 }) .backgroundColor(#FCFCFE).borderRadius(12) }输入框在帖子卡片底部展开和帖子内容在同一张白色卡片内。TextInput绑定p.comment用户输入时同步更新。发送按钮在右边红色文字浅红背景。发送评论private sendComment(idx: number): void { if (this.posts[idx].comment.trim().length 0) return this.posts[idx].comments.push(this.posts[idx].comment) this.posts[idx].showComment false this.posts[idx].comment const copy: Post[] [] for (let c: number 0; c this.posts.length; c) { copy.push(this.posts[c]) } this.posts copy }四个步骤空内容检查→推入评论数组→收起输入框→清空内容→创建新数组触发渲染。最后的for循环创建新数组——和toggleLike的slice()效果一样但写法更啰嗦。两种方式都对slice()更简洁。已有评论的展示if (p.comments.length 0) { Column() { ForEach(p.comments, (c: string) { Row() { Text().fontSize(11).margin({ right: 6 }) Text(c).fontSize(12).fontColor(T2).layoutWeight(1) }.width(100%).padding({ top: 4, bottom: 4 }) }) }.width(100%).padding({ top: 4, bottom: 8, left: 4, right: 4 }) .backgroundColor(#FCFCFE).borderRadius(10).margin({ bottom: 10 }) }已有评论在一个浅灰背景#FCFCFE的圆角区域内每条评论用图标文字展示。评论文字12px灰色T2比帖子正文14px黑色层级更低——视觉上区分帖子内容和评论内容。初始数据中健身达人小王有2条评论瑜伽教练Lily有1条营养师阿杰有2条。跑步爱好者和游泳健将没有评论——评论区域不显示。用户主页跳转社区页面的跳转入口Column() { Text(p.user).fontSize(14).fontWeight(FontWeight.Bold).fontColor(T1) Text(p.time).fontSize(11).fontColor(T3) }.alignItems(HorizontalAlign.Start).margin({ left: 10 }).layoutWeight(1) .onClick(() { router.pushUrl({ url: pages/UserProfile, params: { name: p.user, avatar: p.avatar } as Recordstring, Object }) })点击用户名或头像区域跳转到UserProfile页面传递两个参数用户名和头像emoji。router.pushUrl会把参数序列化目标页面通过router.getParams()接收。参数传递用as Recordstring, Object类型断言——这是ArkUI路由参数的标准写法。如果不加断言TypeScript会报类型错误。UserProfile页面的参数接收State name: string 用户 State avatar: string State followed: boolean false aboutToAppear(): void { const p router.getParams() as Recordstring, Object if (p) { if (p[name]) this.name p[name] as string if (p[avatar]) this.avatar p[avatar] as string } }aboutToAppear在组件创建时执行从路由参数中读取name和avatar。如果参数不存在用户直接打开UserProfile页面使用默认值用户和。这种有参数用参数没参数用默认值的模式在整个App中反复出现——WorkoutPage的planId、ProgressPage的返回逻辑都是同样的思路。防御性编程不假设调用方一定传了正确的参数。用户主页的结构用户主页分四部分导航栏返回按钮 用户名 关注按钮。关注按钮和社区页面的逻辑一样——点击切换关注状态。但这里有个问题UserProfile的followed是独立的State和CommunityPage的posts[idx].followed没有同步。在社区页面关注某人后跳转到用户主页主页的关注按钮仍然是未关注状态。头像和标签Text(this.avatar).fontSize(64).width(100).height(100).borderRadius(50) .backgroundColor(#F5F5F7).textAlign(TextAlign.Center) Text(this.name).fontSize(22).fontWeight(FontWeight.Bold).fontColor(T1).margin({ top: 16 }) Text(运动达人 · 连续打卡 15 天).fontSize(13).fontColor(T3).margin({ top: 4 })头像从社区帖子的44×44放大到100×100——用户主页是个人空间头像应该更醒目。标签运动达人 · 连续打卡 15 天是硬编码的实际项目中应该从用户数据获取。统计数据行Row({ space: 0 }) { this.Stat(156, 动态) Row() {}.width(1).height(30).backgroundColor(#F0EEF4) this.Stat(2.3k, 粉丝) Row() {}.width(1).height(30).backgroundColor(#F0EEF4) this.Stat(89, 关注) }.width(100%).padding(16).backgroundColor(#FFFFFF).borderRadius(16)三个统计数字156动态、2.3k粉丝、89关注。数字之间用1px宽的灰色竖线分隔——不是用Divider组件而是用一个空的Row设置宽度1px和背景色。这种假分割线在ArkUI中很常见——Divider组件的样式控制不够灵活手动造一个更可控。StatBuilder把数字和标签组合成一个ColumnlayoutWeight(1)让三个统计项均分宽度。最近动态Text(最近动态).fontSize(16).fontWeight(FontWeight.Bold).fontColor(T1) Column({ space: 10 }) { Text(今天完成了5组波比跳燃脂效果太棒了坚持就是胜利 ) .fontSize(14).fontColor(T1).lineHeight(22).width(100%) .padding(14).backgroundColor(#FFFFFF).borderRadius(14) Text(晨跑打卡第15天配速又提升了 ‍♂️) .fontSize(14).fontColor(T1).lineHeight(22).width(100%) .padding(14).backgroundColor(#FFFFFF).borderRadius(14) Text(分享一组居家核心训练动作 ) .fontSize(14).fontColor(T1).lineHeight(22).width(100%) .padding(14).backgroundColor(#FFFFFF).borderRadius(14) }最近动态是三条硬编码的帖子——没有ForEach直接写了三个Text。这是因为用户主页的帖子列表和社区页面的帖子列表是独立的数据源。社区页面显示所有人的帖子用户主页只显示当前用户的帖子。原型阶段用硬编码够用真实项目需要从后端获取。状态管理的关键模式整个社区交互依赖一个核心模式修改字段→创建新数组→赋值给State变量。// ❌ 错误方式直接修改数组 this.posts[idx].liked true // 引用没变UI不更新 // ✅ 正确方式创建新数组 this.posts[idx].liked true this.posts this.posts.slice() // 引用变了UI更新ArkUI的State检测的是变量引用的变化不是对象内部属性的变化。直接修改this.posts[idx].liked不会触发重新渲染——必须让this.posts指向一个新对象。这是和其他框架的一个重要区别。Vue用Object.defineProperty或Proxy检测属性变化React用setState的不可变更新。ArkUI选择了最简单的方式只检测引用变化。优点是实现简单、性能可预测缺点是开发者必须手动创建新数组容易忘记导致bug。在社区页面中这个模式出现了5次发帖、点赞、关注、展开评论、发送评论。每次都是同样的套路——修改字段、创建新数组。如果以后交互更多删除帖子、置顶、举报这个模式会重复更多次。到时候应该抽成一个通用的更新函数。踩坑记录ForEach和State的配合。ForEach渲染帖子列表时如果直接修改this.posts[idx]的属性而不创建新数组UI不会更新。点赞、关注、评论三个交互各踩了一次这个坑。最终的解决方案都是this.posts this.posts.slice()。评论输入框的双向绑定。TextInput({ text: p.comment })和.onChange((v: string) { p.comment v })配合实现双向绑定。但p.comment是Post对象的属性直接修改它不会触发State更新。发送评论时必须手动创建新数组才能让评论列表刷新。路由参数的类型断言。router.getParams() as Recordstring, Object——返回值必须断言为Record才能访问具体字段。如果不加asTypeScript会报类型错误。但运行时如果参数不存在router.getParams()返回undefined需要判空处理。UserProfile的关注状态独立。UserProfile页面有自己的State followed和CommunityPage的posts[idx].followed是独立的。在社区页面关注某人后跳转到用户主页主页的关注按钮仍然是未关注状态——因为两个页面的状态没有同步。真实项目应该用全局状态管理AppStorage或自定义状态服务来同步关注状态。发布逻辑的代码位置。发布按钮的onClick逻辑包含数组操作——如果在导航栏的Row里放太多逻辑代码可读性会下降。更好的做法是把发布逻辑抽成一个方法onClick里只调用方法名。当前代码为了减少方法数量把逻辑内联了但代价是build方法变长。从静态展示到动态交互社区页面的变化不只是多了几个onClick。它验证了ArkUI状态管理的核心机制——State的引用检测、ForEach的渲染触发、路由参数的传递。这些机制在首页、训练页、进度页都在用但社区页面把它们集中到了一个场景里一个页面里同时有数组更新发帖、对象属性更新点赞、条件渲染评论框、页面间传参用户主页。如果说其他页面是单个知识点的练习社区页面就是综合考试。