HarmonyOS 应用 · 校园宿舍拼单首页深度解析:一段沉浸式渐变与数据可视化的 ArkUI 之旅 HarmonyOS 应用 · 校园宿舍拼单首页深度解析一段沉浸式渐变与数据可视化的 ArkUI 之旅本文以 HarmonyOS 6.0API 23 ArkTS 声明式 UI 为技术底座逐行拆解「校园宿舍拼单」应用首屏HomeTab的设计理念、布局结构、状态管理与交互细节。所有截图均来自本人电脑正在运行的 HarmonyOS 模拟器真机画面未做任何美化与裁剪修饰。一、项目缘起为什么是宿舍拼单高校宿舍是天然的高密度熟人小生态——四人间或六人间里室友们的生活半径高度重合从早八的豆浆到深夜的泡面从换季的洗衣液到周末的炸鸡外卖几乎每一笔消费都能找到拼的契机。单独点一杯奶茶要 12 元的配送费三个人拼一单只要 8 元囤一箱 12 瓶可乐单买 36 元宿舍团下来只要 18 元。这些看似微不足道的差价乘以一个学期、一栋宿舍楼就是一笔可观的宿舍小金库。正是基于这种真实而高频的需求「校园宿舍拼单」应用应运而生。整个工程以 ArkTS 为开发语言、ArkUI 声明式范式为 UI 框架、Stage 模型为应用骨架目标是打造一款轻量但体验完整的拼单工具。它由底部四个 Tab 组成首页推荐、拼单发起、订单进行中/历史、我的个人中心。其中首页是用户停留时间最长、信息密度最高的门面也是整个项目里视觉效果与交互细节打磨得最用心的部分。下文围绕entry/src/main/ets/pages/HomeTab.ets这一个文件从架构、主题、布局、组件、状态、交互六大维度进行一次完整而细致的拆解。项目源码开源https://gitee.com/codenestFlow/HarmonyOSHub二、整体架构首页在工程中的位置整个应用入口是Index.ets。它是一个Entry装饰的Component通过 ArkUI 的Tabs容器承载四个TabContent分别对应HomeTab、Func1Tab、Func2Tab、ProfileTab。底部 Tab 栏由自定义的Builder bar(title, icon, idx)渲染配合State current: number与onChange实现切换。Entry Component struct Index { State current: number 0; StorageProp(safeBottom) safeBottom: number 0; build() { Tabs({ barPosition: BarPosition.End, index: this.current }) { TabContent() { HomeTab() }.tabBar(this.bar(首页, , 0)) TabContent() { Func1Tab() }.tabBar(this.bar(拼单, , 1)) TabContent() { Func2Tab() }.tabBar(this.bar(订单, , 2)) TabContent() { ProfileTab() }.tabBar(this.bar(我的, , 3)) } .barBackgroundColor(#FFFFFF) .barHeight(60 this.safeBottom) ... } }可以看到HomeTab是四个 Tab 的第一个idx 0且它的图标是小屋 emoji文案为首页。Tab 栏整体高度为60 this.safeBottom正是因为 Tab 处于屏幕最底部需要让出 HarmonyOS 导航条的高度否则导航条会盖住 Tab 图标造成视觉割裂。这一点也是首页乃至整个 App 之所以能看起来像一款成熟产品的关键基础——安全区适配。HomeTab自身的结构非常清晰最外层是一个Column上半部分是固定不滚动的Header()标题区下半部分是占满剩余高度的Scroll内部是一个Column({ space: 14 })依次堆叠 6 个Builder渲染的区块HeroCard→QuickActionRow→WeekChart→ChipRow→SectionTitle(热门拼单)→GroupList→RankCard。build() { Column() { this.Header() Scroll() { Column({ space: 14 }) { this.HeroCard() this.QuickActionRow() this.WeekChart() this.ChipRow() this.SectionTitle(热门拼单) this.GroupList() this.RankCard() } .width(100%) .padding({ left: D.pad, right: D.pad, top: 14, bottom: D.pad this.safeBottom 20 }) } .layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top) } .width(100%).height(100%).backgroundColor(C.bg) }这里有几个非常专业的细节整体背景色为C.bg浅蓝灰#F2F5FB既不会与白色卡片产生强烈对比又能让卡片边界自然浮现出来是一种偏克制的舒适感配色。Scroll不显示滚动条scrollBar(BarState.Off)符合现代 App 的极简趋势。底部 padding 加上safeBottom——这是为了防止最后一个区块RankCard 品类排行的内容被底部 Tab 栏遮挡是沉浸式布局的标准做法。下面这张图是 HarmonyOS 模拟器里首页首屏的真实运行效果可以看到从上到下依次是状态栏、Header宿舍拼单、HeroCard渐变统计卡 搜索框、QuickActionRow4 个圆形快捷入口、WeekChart本周拼单趋势条形图、ChipRow分类筛选、热门拼单标题 第一张 GroupCard 露出、底部 Tab 栏。整张图信息密度高却不拥挤关键在于每个区块之间留了 14vp 的纵向间距Column({ space: 14 })视觉节奏感极好。三、沉浸式全屏与安全区适配在 HarmonyOS 上做沉浸式看起来是 UI 工作本质却是应用启动时就要完成的窗口配置。这件事由entry/src/main/ets/entryability/EntryAbility.ets完成async onWindowStageCreate(windowStage: window.WindowStage): Promisevoid { windowStage.loadContent(pages/Index, (err) { ... try { const win windowStage.getMainWindowSync(); win.setWindowLayoutFullScreen(true); win.setWindowSystemBarProperties({ statusBarContentColor: #1C2333, navigationBarContentColor: #1C2333 }); const top win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM); const bottom win.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR); AppStorage.setOrCreate(safeTop, px2vp(top.topRect.height)); AppStorage.setOrCreate(safeBottom, px2vp(bottom.bottomRect.height)); } catch (e) { ... } }); }这段代码做了三件事全屏布局setWindowLayoutFullScreen(true)让窗口内容延伸到状态栏与导航条区域渐变色 HeroCard 才能从屏幕最顶部开始绘制。状态栏图标颜色setWindowSystemBarProperties把状态栏文字与导航条图标的颜色都设为#1C2333与Theme.ets里的C.text完全一致。动态避让区域getWindowAvoidArea分别拿到系统顶部状态栏与底部导航条的高度px2vp()换算后存入AppStorage的safeTop与safeBottom。接下来的事情交给 ArkUI 的响应式系统。HomeTab通过两个装饰器订阅这两个值StorageProp(safeTop) safeTop: number 0; StorageProp(safeBottom) safeBottom: number 0;StorageProp从AppStorage单例读取数据并在数据变化时自动触发 UI 刷新。这意味着折叠屏展开、刘海屏切换、旋转屏幕时UI 都能毫秒级重新对齐——开发者只需要声明我要读哪个 keyArkUI 负责key 变了就重新渲染。在Header与外层Column中safeTop 10作为顶部 padding 使用让标题文字永远刚好落在状态栏下方 10vp 处。这种看不见但能感受到的细节是判断一款 App 是否专业的试金石。四、全局主题系统颜色与尺寸的中央仓库ArkTS 严格模式不允许any、不允许类型断言因此所有可复用的视觉常量必须显式声明。entry/src/main/ets/common/Theme.ets用两个 class 把颜色和尺寸集中起来export class C { static readonly bg: string #F2F5FB; static readonly card: string #FFFFFF; static readonly cardSoft: string #F4F6FB; static readonly stroke: string #EAEEF6; static readonly text: string #1C2333; static readonly textSub: string #5A6483; static readonly textDim: string #9AA3BC; static readonly primary: string #FF6B35; static readonly primarySoft: string #FFEDE5; static readonly accent: string #FFB020; static readonly warn: string #FF9F1C; static readonly danger: string #FF5A6E; static readonly ok: string #2BB673; } export class D { static readonly rLg: number 20; static readonly rMd: number 14; static readonly rSm: number 10; static readonly pad: number 16; }C是Color的缩写底色bg是淡蓝灰#F2F5FB卡片白card软卡片cardSoft用于头像底色文字层级分text、textSub、textDim三档。主色调是热情的橙红primary #FF6B35搭配金黄色accent #FFB020做渐变契合美食拼单的烟火气。danger用于价格强调ok用于已满员等成功状态。D是Design的缩写3 档圆角20/14/10和统一内边距 16。整个项目所有圆角 20vp 的大卡片、“圆角 10vp 的小按钮”、内边距 16vp 的容器都从这里取数。这种设计 token思路和前端领域的 Tailwind Config 异曲同工但它是用 ArkTS 原生 class 实现的——没有运行时代价没有打包体积类型系统还会帮你杜绝拼写错误。五、Header标题与副标题的克制美学Builder Header() { Column() { Text(宿舍拼单) .fontSize(22).fontWeight(FontWeight.Bold).fontColor(C.text) .width(100%) .padding({ top: this.safeTop 10, left: D.pad, right: D.pad, bottom: 6 }) Text(今天想拼点什么) .fontSize(13).fontColor(C.textDim) .width(100%) .padding({ left: D.pad, right: D.pad, bottom: 12 }) } .width(100%).backgroundColor(C.card) }Header只有两行文字主标题宿舍拼单副标题今天想拼点什么——一句口语化的问句迅速建立情感连接把用户从我打开了一个 App的陌生感里拉出来。主标题 22 号加粗主文色副标题 13 号暗淡色。背景是纯白C.card在视觉上把标题区与下方渐变 HeroCard 区做一个轻量切分。整个 Header 的纵向高度由safeTop 10 (22 字高) 6 (13 字高) 12自解释计算不依赖任何 magic number对后续做多端适配非常友好。六、HeroCard渐变背景与数据冲击力HeroCard 是首页的门面担当。它用一张从橙红 (C.primary) 到金黄 (C.accent) 的 135° 线性渐变打底承载两个核心元素上方的 3 栏统计数字下方的搜索框。Builder HeroCard() { Column({ space: 14 }) { Row() { Column({ space: 4 }) { Text(本周拼单).fontSize(12).fontColor(#FFFFFF).opacity(0.85) Text(128).fontSize(28).fontWeight(FontWeight.Bold).fontColor(#FFFFFF) } .alignItems(HorizontalAlign.Start).layoutWeight(1) Column({ space: 4 }) { Text(累计省).fontSize(12).fontColor(#FFFFFF).opacity(0.85) Text(¥386).fontSize(28).fontWeight(FontWeight.Bold).fontColor(#FFFFFF) } .alignItems(HorizontalAlign.Start).layoutWeight(1) Column({ space: 4 }) { Text(参与人).fontSize(12).fontColor(#FFFFFF).opacity(0.85) Text(24).fontSize(28).fontWeight(FontWeight.Bold).fontColor(#FFFFFF) } .alignItems(HorizontalAlign.Start).layoutWeight(1) } .width(100%) Row({ space: 8 }) { Text().fontSize(16) TextInput({ placeholder: 搜索拼单商品、店铺..., text: this.searchText }) .onChange((val: string) { this.searchText val; }) .backgroundColor(#33FFFFFF).borderRadius(D.rSm) .height(38).layoutWeight(1) .placeholderColor(#FFFFFFCC).placeholderFont({ size: 13 }) .fontColor(#FFFFFF) } .width(100%) .padding({ left: 12, right: 12, top: 8, bottom: 8 }) .backgroundColor(#22FFFFFF).borderRadius(D.rMd) } .width(100%) .padding(18) .borderRadius(D.rLg) .linearGradient({ angle: 135, colors: [[C.primary, 0.0], [C.accent, 1.0]] }) }3 栏统计本周拼单 128 单 / 累计省 ¥386 / 参与人 24 人。每栏都是一个Column({ space: 4 })由小标签 大数字组成。3 个Column的layoutWeight(1)让它们等分父容器的水平空间形成稳定的三足鼎立。文字颜色全部用纯白#FFFFFF小标签加opacity(0.85)让层级退后 1 档——这是大厂 Banner 常用的明度对比而非灰度对比手法既避免了死板的灰白又保持了渐变背景的活力。搜索框用TextInput实现placeholder 是搜索拼单商品、店铺…。它和左侧的放大镜 emoji 共用一个Row整体再被一个半透明白色Row容器背景色#22FFFFFF包裹形成凹陷在卡片里的视觉。#22FFFFFF是带 22% 不透明度的白色相当于 macOS 上的 frosted glass——既能隐约看到底下橙黄渐变又能让前景文字清晰可读。State searchText: string 维护搜索词onChange时双向同步是 ArkTS 的受控输入标准写法。圆角与渐变外层borderRadius(D.rLg)是 20vp配合padding(18)让卡片有适度的呼吸感。.linearGradient({ angle: 135, colors: [[C.primary, 0.0], [C.accent, 1.0]] })是 ArkUI 的线性渐变语法angle: 135表示从左上到右下的对角线方向颜色从橙红平滑过渡到金黄是生活类 App 的经典配色。七、QuickActionRow4 个圆形入口的网格紧跟在 HeroCard 下方的是 4 个圆形快捷入口对应奶茶、外卖、日用、福利四个最高频的场景Builder QuickActionRow() { Row() { ForEach(this.quickActions, (qa: QuickAction) { Column({ space: 8 }) { Row() { Text(qa.emoji).fontSize(24) } .width(46).height(46) .backgroundColor(qa.bgColor).borderRadius(14) .justifyContent(FlexAlign.Center) Text(qa.label).fontSize(11).fontColor(C.textSub) } .layoutWeight(1) .onClick(() { promptAction.showToast({ message: qa.label }); }) }, (qa: QuickAction) qa.label) } .width(100%) .padding({ top: 16, bottom: 4 }) }数据结构由接口QuickAction定义emojilabelbgColor。4 个图标的背景分别是#FFEDE5淡橙红、#FFF3E0淡金黄、#E8F5E9淡草绿、#F3E5F5淡紫粉——典型的莫兰迪色风格让 emoji 头像更突出又不喧宾夺主。每个圆形图标 46×46vp圆角 14vp居中显示 emoji下方再放一个 11 字号的小标签。ForEach(this.quickActions, (qa) {...}, (qa) qa.label)是 ArkUI 的列表渲染。第三参数 keyGenerator 是推荐做法——它告诉框架如何唯一标识每一项有助于提升列表渲染性能和组件复用准确性让后续局部刷新只重绘变化的单元格。onClick里调用promptAction.showToast弹轻提示是可点击但暂未实装的标准占位写法。layoutWeight(1)让 4 个入口等分宽度无论 6.1 寸小屏还是 10 寸平板都不会挤成一团。八、WeekChart声明式条形图零依赖实现数据可视化听起来要拉图表库但在 ArkUI 声明式范式下纯组件拼装就能做出足够精美的条形图。WeekChart渲染一周 7 天的拼单数量点击柱子还能切换选中态Builder WeekChart() { Column({ space: 12 }) { Row() { Text(本周拼单趋势).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text) Blank() Text(查看 ).fontSize(12).fontColor(C.textDim) } .width(100%) Row({ space: 0 }) { ForEach(this.barData, (b: BarData, idx: number) { Column({ space: 6 }) { Stack({ alignContent: Alignment.Bottom }) { Column() .width(18) .height(this.barData[idx].value * 1.2) .borderRadius(6) .linearGradient({ angle: 180, colors: this.selectedBar idx ? [[C.primary, 0.0], [C.accent, 1.0]] : [[C.primarySoft, 0.0], [C.primary, 1.0]] }) .opacity(this.selectedBar idx ? 1.0 : 0.6) } .width(28).height(120) Text(b.day).fontSize(11) .fontColor(this.selectedBar idx ? C.primary : C.textDim) .fontWeight(this.selectedBar idx ? FontWeight.Bold : FontWeight.Normal) } .layoutWeight(1) .onClick(() { this.selectedBar idx; }) }, (b: BarData) b.day) } .width(100%) } .width(100%) .padding(16) .backgroundColor(C.card) .borderRadius(D.rLg) .border({ width: 1, color: C.stroke }) }整张图的关键技巧只有一个——用Stack({ alignContent: Alignment.Bottom })把柱子钉在底部。Stack是 ArkUI 的层叠容器。把一根Column柱子放在 Stack 里并通过alignContent: Alignment.Bottom让它向底部对齐再用height(b.value * 1.2)动态控制柱高——这样不管数据是 45 还是 100柱子都从坐标原点长出来而不是从顶部往下垂下去。每根柱子是 18vp 宽、圆角 6vp 的Column外层包裹一个 28×120vp 的Stack占据固定位置。视觉上每根柱子之间留出约 10vp 的间隙呈现独立的柱子而非连成一片。选中态用State selectedBar: number 5默认选中周六数组下标 5。当某根柱子被点击时this.selectedBar idx立即触发重渲染——选中柱的渐变从primarySoft - primary浅到深升级为primary - accent橙红到金黄不透明度从 0.6 变为 1.0柱下的星期标签颜色从textDim变为主色primary字重从 Normal 变为 Bold。这种渐变色 不透明度 文字颜色 字重四重变化的组合让选中态既醒目又不突兀。7 天数据依次为 60/80/45/90/70/100/55。乘以 1.2 之后最高柱为 120vp刚好填满 Stack 高度。右下角的查看 “没有 onClick纯展示——这是用户研究的经典手法看到 “” 用户的潜意识会想点一下会怎样”于是点击率显著高于详情等纯文字按钮。九、ChipRow横向滚动的分类筛选拼单可能涉及饮料、零食、外卖、日用等多个品类首页需要一个轻量的筛选器。ChipRow用一个横向Scroll包住一排TextChip 实现Builder ChipRow() { Scroll() { Row({ space: 10 }) { ForEach(this.cats, (cat: string, idx: number) { Text(cat) .fontSize(13) .fontColor(this.activeCat idx ? #FFFFFF : C.textSub) .padding({ left: 14, right: 14, top: 7, bottom: 7 }) .backgroundColor(this.activeCat idx ? C.primary : C.card) .borderRadius(16) .onClick(() { this.activeCat idx; promptAction.showToast({ message: 筛选: cat }); }) }, (cat: string) cat) } } .scrollable(ScrollDirection.Horizontal) .scrollBar(BarState.Off).width(100%) }5 个分类全部、饮料、零食、外卖、日用实际数据多时可继续追加横向滚动会自动支持。State activeCat: number 0默认选中全部。选中态用主色背景 白字未选中用白色卡片背景 次文色——这种填充 vs 描边的对比比单纯换文字颜色更有按下了的物理感。Scroll的scrollable(ScrollDirection.Horizontal)确保只能横向滚动scrollBar(BarState.Off)隐藏滚动条——配合 16vp 圆角整行 Chip 看起来像一颗颗糖果活泼而不轻浮。onClick里调用promptAction.showToast弹筛选: xxx是给用户即时反馈的小技巧。十、热门拼单列表GroupList GroupCard 的设计哲学如果说 HeroCard 是首页的封面那 GroupList 才是首页的内容。它承担着把用户从逛引导到买的核心转化职责。Builder GroupList() { Column({ space: 12 }) { ForEach(this.groups, (g: GroupItem) { this.GroupCard(g) }, (g: GroupItem) g.id.toString()) } .width(100%) }groups是一个长度为 5 的数组每一项都是GroupItem接口的实例奶茶拼单 第二杯半价一点点¥12、炸鸡全家桶肯德基¥39、可乐 12 瓶装校园超市¥18、麻辣烫拼单张亮麻辣烫¥22、蛋挞一盒 6 只烘焙坊¥15。每条数据包含 emoji、名称、店铺、价格、已拼人数、需要人数、剩余提示、评分、热度、标签共 10 个字段几乎涵盖了拼单场景下决策所需的全部信息。GroupCard是整个首页代码量最大的单个BuilderBuilder GroupCard(g: GroupItem) { Column({ space: 10 }) { Row({ space: 12 }) { Stack({ alignContent: Alignment.TopEnd }) { Row() { Text(g.emoji).fontSize(40) } .width(68).height(68) .backgroundColor(C.cardSoft) .borderRadius(D.rMd) .justifyContent(FlexAlign.Center) Text(g.tag) .fontSize(9).fontColor(#FFFFFF) .padding({ left: 5, right: 5, top: 2, bottom: 2 }) .backgroundColor(C.danger).borderRadius(4) .margin({ top: 0, right: 0 }) } .width(68).height(68) Column({ space: 5 }) { Text(g.name).fontSize(15).fontWeight(FontWeight.Medium).fontColor(C.text) .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }) Row({ space: 8 }) { Text(⭐ g.rating).fontSize(11).fontColor(C.warn) Text( g.hot).fontSize(11).fontColor(C.textDim) Text(g.shop).fontSize(11).fontColor(C.textDim) } Row({ space: 6 }) { Text(¥ g.price).fontSize(16).fontWeight(FontWeight.Bold).fontColor(C.danger) Text(· g.left).fontSize(11).fontColor(C.warn) } } .alignItems(HorizontalAlign.Start) .layoutWeight(1) Button(立即拼) .fontSize(13).fontColor(#FFFFFF).backgroundColor(C.primary) .borderRadius(D.rSm).height(34) .onClick(() { promptAction.showToast({ message: 加入: g.name }); }) } .width(100%) Stack({ alignContent: Alignment.Start }) { Column().width(100%).height(6).backgroundColor(C.cardSoft).borderRadius(3) Column() .width(g.joined / g.need * 100 %) .height(6) .borderRadius(3) .linearGradient({ angle: 0, colors: [[C.primary, 0.0], [C.accent, 1.0]] }) } .width(100%) Row() { Text(已拼 g.joined / g.need 人) .fontSize(11).fontColor(C.textSub) Blank() if (g.joined g.need) { Text(已满员).fontSize(11).fontColor(C.ok).fontWeight(FontWeight.Medium) } else { Text(差 (g.need - g.joined) 人成团).fontSize(11).fontColor(C.primary) } } .width(100%) } .width(100%) .padding(14) .backgroundColor(C.card) .borderRadius(D.rLg) .border({ width: 1, color: C.stroke }) }整个 GroupCard 可以拆成上中下三个区域来理解上层核心信息区由Row({ space: 12 })横向排列三个元素——左侧的 emoji 头像、中间的拼单详情、右侧的立即拼按钮。emoji 头像用一个 68×68vp 的圆角矩形Row作为容器里面居中放一个 40 字号的 emoji。再用Stack({ alignContent: Alignment.TopEnd })在右上角叠加一个标签比如热销“爆品”“划算”“热门”“限特”。Stack 的TopEnd对齐让标签自然出现在右上角无需手动算坐标。这种头像 角标的设计在电商 App 中非常常见但在这里通过纯声明式语法一气呵成。拼单详情纵向Column({ space: 5 })装 3 行文字。第一行是拼单名称maxLines(1)textOverflow: Ellipsis保证单行省略避免卡片高度参差不齐第二行是评分 ⭐、热度 、店铺名用 emoji 代替图标省资源又国际化第三行是价格¥XXdanger 红色加粗强调价格刺激转化 状态文字剩2单/已满。立即拼按钮Button高度 34vp圆角 10vp主色背景白字。onClick弹 toast 占位。在小尺寸屏幕上按钮宽度由文字自适应整个卡片剩余空间全部交给中间的详情列——这是典型的次要按钮处理方式它不能太大抢戏但又必须足够显眼让用户能想点就点。中层拼单进度条核心技巧是双层ColumnStack模拟进度条。Stack({ alignContent: Alignment.Start }) { Column().width(100%).height(6).backgroundColor(C.cardSoft).borderRadius(3) Column() .width(g.joined / g.need * 100 %) .height(6) .borderRadius(3) .linearGradient({ angle: 0, colors: [[C.primary, 0.0], [C.accent, 1.0]] }) } .width(100%)底层是一个浅色C.cardSoft的轨道Column宽度 100%上层是一个渐变橙红→金黄的进度Column宽度根据g.joined / g.need计算得到比如已拼 3 人、需要 5 人进度就是 60%。Stack默认按子组件顺序层叠配合alignContent: Alignment.Start让上层进度条从最左侧开始。这种百分比宽度 渐变是实现精美进度条最轻量的方法不需要任何 Canvas 与第三方库。下层人数与成团提示左边已拼 X/Y 人用次文色右边根据是否满员动态切换文案——满员显示绿色已满员否则显示主色差 N 人成团。if (g.joined g.need)这种条件渲染让 UI 自动适配业务状态开发者无需手动维护成团/未成团两份模板。下图是首页中部——热门拼单列表的真机截图可以看到 4 张并排的 GroupCard每张卡片都包含了 emoji 头像 角标、详情三行、立即拼按钮、进度条、人数与成团提示可以看到第三张卡片可乐 12 瓶装校园超市只剩 2 单进度条只填了 1/3第一张奶茶拼单 第二杯半价剩 2 单进度条填了 3/5而第二张炸鸡全家桶肯德基已经满员进度条完全填满且文字变成绿色已满员——这正是条件渲染 动态宽度的视觉效果。十一、RankCard品类排行简洁有力的收尾首页最末端的RankCard是一个小而美的数据排行榜告诉用户今天大家都在拼什么。Builder RankCard() { Column({ space: 14 }) { Row() { Text(品类排行).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text) Blank() Text(更新于 10:00).fontSize(11).fontColor(C.textDim) } .width(100%) ForEach(this.rankItems, (r: RankItem, idx: number) { Row({ space: 12 }) { Text((idx 1).toString()) .fontSize(16).fontWeight(FontWeight.Bold) .fontColor(idx 2 ? C.primary : C.textDim) .width(24) Text(r.emoji).fontSize(20) Text(r.name).fontSize(14).fontColor(C.text).layoutWeight(1) Stack({ alignContent: Alignment.Start }) { Column().width(100%).height(6).backgroundColor(C.cardSoft).borderRadius(3) Column() .width(r.count / r.total * 100 %) .height(6) .borderRadius(3) .backgroundColor(C.primary) } .width(80) Text(r.count 单).fontSize(12).fontColor(C.primary).fontWeight(FontWeight.Medium) } .width(100%) }, (r: RankItem) r.id.toString()) } .width(100%) .padding(16) .backgroundColor(C.card) .borderRadius(D.rLg) .border({ width: 1, color: C.stroke }) }数据结构只有 3 项奶茶 128 单、炸鸡 96 单、麻辣烫 72 单。视觉布局是 5 列等比名次24vp 固定宽 emoji20 字号 名称layoutWeight(1)自动撑满 80vp 宽的小进度条 单数。前 2 名的名次数字用主色 primary第 3 名用暗淡色 textDim——这是一个低成本但效果显著的金牌银牌铜牌视觉区分。进度条宽度由r.count / r.total * 100 %计算。r.total 200是参考总量让 3 个品类的进度条长度有合理的比例差异。下图是首页底部的真机截图可以看到蛋挞一盒 6 只限特标签的 GroupCard以及下方的品类排行卡片仔细看右下角会发现奶茶 的进度条最长128 单炸鸡 居中96 单麻辣烫 最短72 单三根柱子的视觉差异恰好对应它们的销量排名。十二、状态管理与响应式ArkTS 的单向数据流整个HomeTab用到了 3 个State状态变量State activeCat: number 0; State selectedBar: number 5; State searchText: string ;activeCat当前选中的分类下标。点击 Chip 时this.activeCat idx触发 ChipRow 重渲染。selectedBar当前选中的柱状图下标。点击柱子时切换配合 WeekChart 的多重视觉变化。searchText搜索框受控输入。onChange实时同步保证与 HeroCard 内TextInput一致。外加 2 个从AppStorage订阅的全局值StorageProp(safeTop) safeTop: number 0; StorageProp(safeBottom) safeBottom: number 0;其中 3 个State变量是组件自身的状态构成组件内部的事实来源而 2 个StorageProp是只读单向同步AppStorage → 组件其事实来源是AppStorage组件只能读取不能修改。UI 永远只是状态的映射状态变化由用户事件触发整个数据流是单向的用户操作 → 状态更新 → 框架自动重渲染。相比传统 Android 的命令式 UI手动findViewByIdsetTextsetVisibility单向数据流有 3 个明显优势无中间态错误控件不可能 setText 后视图不刷新可预测性给定相同状态UI 一定渲染出相同结果天然支持多端同一份代码在手机、平板、二合一设备上都能正确渲染。十三、ArkTS 严格模式下的工程化细节整个HomeTab完全符合项目 README 里描述的代码规范所有数据结构GroupItem、QuickAction、BarData、RankItem都用interface显式声明没有any/object编译器能在 IDE 阶段就发现字段拼写错误ForEach第三参数 keyGenerator 是推荐做法有助于提升列表渲染性能和组件复用准确性全文没有as any、as number、as string这种逃生舱口每一个 UI 区块都是独立的Builder让build()函数保持简洁——一眼就能看清首页由哪几部分组成。最值得称道的是主题统一所有颜色、圆角、间距都从Theme.ets取数。如果产品经理某天说主色想从橙红换成森绿只需要改C.primary一个值整个首页的所有 HeroCard、按钮、进度条、Chip、Tag 会瞬间切换——这是设计系统在 ArkTS 工程中的最直接落地。十四、可演进方向虽然首页已经相当精致但要走向产品级还有几条自然的演进路径把private groups接入真实接口实现数据真实化源码中groups是 private 普通成员变量非State若要实现数据变化自动驱动 UI 刷新需将其改为State用 ArkUI 原生Refresh组件包住外层Scroll实现下拉刷新让 HeroCard 三个数字与真实用户行为打通让 Banner 数据活起来WeekChart 拓展为日 / 周 / 月维度切换让 ChipRow 的activeCat真正过滤 GroupList 而不只是弹 toastsearchText接入商品搜索实现边输入边联想最后给 GroupCard 加一个进入页面的飞入动画让用户的视觉节奏更舒服。这些改造大多不需要重写 UI只需要在现有状态 → 渲染的链路里增加数据和触发点。十五、写在最后回到开篇的三段式真机截图——从顶部 Header 到底部 Tab从渐变 HeroCard 到条形图 WeekChart从 ChipRow 到 GroupList 再到 RankCard整张首页用约 380 行 ArkTS 代码构建出了一个视觉舒适、信息密度合理、交互细节到位的高质量首屏。它没有炫技没有冗余的装饰每一行代码都在解决一个具体的视觉或交互问题。这正是 HarmonyOS ArkUI 声明式范式的精髓——用最少的代码描述最丰富的 UI。当你熟悉了Component/Builder/State/StorageProp/ForEach这五个核心概念后写一个完整的页面会比想象中轻松得多而像 Theme token、沉浸式全屏、安全区适配这些看不见的工程一旦做好就能让整个 App 的体验瞬间拉满。如果你也对 HarmonyOS 应用开发感兴趣不妨从这种小而美的单页面项目开始尝试。当你把首页打磨到这种程度整个 App 的门面也就立住了。