个人中心的六个菜单入口中身体数据和我的成就是两个纯展示型页面。它们没有复杂的交互逻辑但在布局方式上有明显差异——身体数据是纵向列表成就页面是网格。这两个页面恰好代表了 ArkTS 里两种最常用的排列模式值得拆开看看各自的实现细节。完整效果BodyDataPage列表卡片的数据展示页面结构身体数据页面的视觉结构很简单顶部导航栏中间六个指标卡片纵向排列每个卡片占据一行。从截图看每张卡片左侧是指标名称小字灰色和数值大字彩色右侧是一个灰色的右箭头暗示可以点击进入编辑。整体布局用Scroll Column实现Scroll(){Column(){this.DataCard(体重,68.5,kg,A)this.DataCard(身高,175,cm,#5DADE2)this.DataCard(BMI,22.4,正常,#00B894)this.DataCard(体脂率,18.2,%,#FF9F43)this.DataCard(肌肉量,52.3,kg,#8A5DFA)this.DataCard(基础代谢,1680,千卡,#E8734A)Blank().height(20)}.width(100%).padding({left:16,right:16})}Scroll保证了内容超出屏幕时可以上下滑动Column里的六个DataCard依次纵向排列。最后的Blank().height(20)是底部留白防止最后一张卡片紧贴屏幕边缘。DataCard Builder 的拆解BuilderDataCard(label:string,value:string,unit:string,color:string){Row(){Column(){Text(label).fontSize(13).fontColor(T2)Row(){Text(value).fontSize(32).fontWeight(FontWeight.Bold).fontColor(color)Text( unit).fontSize(14).fontColor(T3)}.margin({top:4})}.alignItems(HorizontalAlign.Start).layoutWeight(1)SymbolGlyph($r(sys.symbol.chevron_right)).fontSize(14).fontColor([#DDD])}.width(100%).padding(18).backgroundColor(#FFFFFF).borderRadius(16).margin({bottom:10})}逐层分析这个 Builder 的布局嵌套最外层 Row横向排列layoutWeight(1)让左侧内容区占据所有剩余空间右侧的箭头图标被挤到最右边。中间 Column纵向排列标签和数值alignItems(HorizontalAlign.Start)让文字左对齐。数值行 Row把数值和单位放在同一行。数值用fontSize(32)的大号加粗字体单位用fontSize(14)的小号灰色字体。这个大小对比制造了数值为主、单位为辅的视觉层次。右侧箭头SymbolGlyph($r(sys.symbol.chevron_right))是系统自带的右箭头符号。用$r()引用系统符号资源而不是硬编码路径保证了在不同设备和系统版本上都能正确渲染。颜色编码的设计意图六个指标各有自己的颜色指标颜色色值体重红色#FF4757身高蓝色#5DADE2BMI绿色#00B894体脂率橙色#FF9F43肌肉量紫色#8A5DFA基础代谢珊瑚色#E8734A这不是随意配色。红色代表核心指标体重是最重要的身体数据蓝色代表基础属性身高不会变绿色代表健康状态BMI 正常是好事橙色代表需要关注体脂率偏高的话需要警惕紫色和珊瑚色分别对应肌肉和代谢和 App 的主色调体系保持一致。这种一个指标一个颜色的做法在数据展示页面里很常见。好处是用户扫一眼就能通过颜色定位到自己关心的指标不需要逐行阅读文字。坏处是如果指标太多超过 7-8 个颜色区分度会下降。目前 6 个指标正好在可区分的范围内。编辑按钮的占位右上角的红色编辑文字目前没有绑定任何点击事件。它是一个 UI 占位——告诉用户这里以后可以编辑身体数据但实际功能还没实现。这种先放占位后填逻辑的做法在 App 开发中很普遍。好处是用户能提前感知到页面的完整功能规划坏处是如果占位时间太长用户点了没反应会觉得 App 有 bug。在原型阶段可以接受上线前必须处理。AchievementPage网格布局的成就展示页面结构成就页面的视觉和身体数据完全不同——它是两列网格每个徽章占一个格子。从截图看8 个徽章排列成 4 行 2 列已获得的徽章有彩色标签未获得的被压暗。整体布局用Scroll Column Grid ForEach实现Scroll(){Column(){Grid(){ForEach(this.badges,(b:Badge){GridItem(){// 徽章内容}})}.columnsTemplate(1fr 1fr).columnsGap(10).rowsGap(10).width(100%).padding({left:16,right:16})Blank().height(20)}.width(100%)}Grid是 ArkTS 的网格布局组件columnsTemplate(1fr 1fr)定义了两列等宽布局。1fr是 fraction分数的缩写两个1fr表示把可用空间平分成两份。columnsGap(10)设置列间距为 10vprowsGap(10)设置行间距为 10vp。和身体数据页的Column纵向排列不同Grid天然支持多行多列不需要嵌套Row。这是选择Grid而不是Row Column组合的核心原因。Badge 接口与数据定义interfaceBadge{icon:stringname:stringdesc:stringearned:booleancolor:string}用interface定义徽章的数据结构。在 ArkTS 里interface和type都能用来定义对象类型但interface在 IDE 里的类型提示更完整——鼠标悬停时能看到每个字段的名称和类型而type有时只显示object。Statebadges:Badge[][{icon:,name:连续7天,desc:连续运动7天,earned:true,color:#FF4757},{icon:,name:百次训练,desc:完成100次训练,earned:true,color:#FF9F43},{icon:,name:马拉松,desc:累计跑步42公里,earned:true,color:#5DADE2},{icon:⚡,name:燃脂达人,desc:累计消耗10000千卡,earned:true,color:#FECA57},{icon:,name:30天挑战,desc:连续运动30天,earned:false,color:#8A5DFA},{icon:,name:年度冠军,desc:年度运动时长第一,earned:false,color:#E8734A},{icon:,name:力量之王,desc:完成500次力量训练,earned:false,color:#00B894},{icon:,name:社区之星,desc:获得1000个赞,earned:true,color:#6C5CE7},]8 个徽章其中 5 个已获得earned: true3 个未获得。color字段用于已获得徽章的标签背景色每个徽章有自己的主题色。GridItem 内部的条件渲染这是成就页面最核心的实现细节——根据earned布尔值渲染完全不同的 UIGridItem(){Column(){Text(b.icon).fontSize(36).margin({bottom:8}).opacity(b.earned?1:0.25)Text(b.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(b.earned?T1:T3)Text(b.desc).fontSize(10).fontColor(T3).margin({top:2})if(b.earned){Text(✓ 已获得).fontSize(9).fontColor(b.color).fontWeight(FontWeight.Bold).padding({left:8,right:8,top:2,bottom:2}).backgroundColor(b.color15).borderRadius(6).margin({top:6})}else{Row(){}.width(30).height(2).borderRadius(1).backgroundColor(#E0DCE8).margin({top:8})}}}三个关键的视觉差异图标透明度已获得的徽章图标opacity(1)正常显示未获得的opacity(0.25)几乎隐形。这让用户一眼就能区分哪些成就已经解锁。名称颜色已获得的用深色T1#1E1B2E未获得的用灰色T3#BBBBBB。颜色对比进一步强化了已完成 vs 未完成的区分。底部标签已获得的显示带颜色的✓ 已获得标签未获得的只放一个小灰色横条。这个横条的作用是占位——如果两个格子一个有标签一个没有底部会对不齐视觉上会显得参差不齐。加一个等高的横条保持了网格的整齐。已获得数量的动态计算privateearnedCount():number{letc:number0for(leti:number0;ithis.badges.length;i){if(this.badges[i].earned)c}returnc}右上角显示的5/8就是用这个方法计算的。遍历badges数组统计earned为true的数量。没有用filter().length的写法原因是 ArkTS 里Array.filter()返回的类型推断有时候不够精确IDE 会报类型警告。手动for循环虽然啰嗦一点但类型安全不会有问题。整卡片的透明度控制.width(100%).padding(14).backgroundColor(#FFFFFF).borderRadius(16).opacity(b.earned?1:0.6)除了图标和文字的单独透明度控制整个 GridItem 的容器也有透明度变化——未获得的卡片整体opacity(0.6)。这种双重透明度整体 局部制造了一种未解锁的视觉效果比单纯灰掉文字更自然。两个页面的布局对比身体数据页和成就页面在布局策略上的差异本质上是数据维度决定的维度身体数据页成就页面数据结构扁平列表每项独立网格矩阵每项等权信息密度一行一项信息量大一格一项信息量小排列方式纵向 Column二维 Grid交互暗示右箭头→可编辑无箭头→纯展示滚动方向纵向 Scroll纵向 Scroll内容可能横向扩展身体数据用列表是因为每个指标有标签 数值 单位三层信息需要水平空间来展开。成就用网格是因为每个徽章只有图标 名称两层信息正方形格子刚好够放。选错布局方式会导致视觉混乱。如果把身体数据塞进网格每格里挤三个字段会显得拥挤如果把成就排成列表一行一个徽章会浪费大量横向空间页面会变得很长。页面导航的统一性两个页面的导航栏结构完全一致Row(){Row(){SymbolGlyph($r(sys.symbol.chevron_left)).fontSize(20).fontColor([T1])}.width(34).height(34).borderRadius(17).backgroundColor(rgba(0,0,0,0.03)).justifyContent(FlexAlign.Center).onClick((){router.back()})Text(页面标题).fontSize(20).fontWeight(FontWeight.Bold).fontColor(T1).margin({left:10}).layoutWeight(1)// 右侧内容}34×34 的圆形返回按钮、rgba(0,0,0,0.03)的浅灰背景、20px 的系统左箭头。这个返回按钮的样式从运动记录页、训练页、饮食记录页一路沿用下来已经成为整个 App 的标准导航模式。唯一差异是右侧区域身体数据页放了红色编辑文字成就页面放了5/8计数。这种左侧固定、右侧灵活的设计让导航栏在保持一致性的同时能根据页面需求展示不同的辅助信息。数据目前是 Mock 的两个页面的数据目前都是写死在State里的 mock 数据。身体数据的六个指标值、成就的八个徽章都是硬编码的初始值。如果以后要接真实数据改造路径很清晰身体数据页从本地存储或服务器获取用户的身体指标替换State的初始值成就页面根据用户的运动数据计算哪些成就已解锁动态更新earned字段但在当前原型阶段mock 数据足够用来验证 UI 布局和视觉效果。先把界面做对再把数据接通——这是大多数 App 开发的实际节奏。