HarmonyOS7 电商首页怎么做才像样?轮播、金刚区和瀑布流一次跑通

发布时间:2026/7/1 18:17:35
HarmonyOS7 电商首页怎么做才像样?轮播、金刚区和瀑布流一次跑通 文章目录前言首页整体布局Swiper 轮播图金刚区Grid 宫格布局商品瀑布流WaterFlow LazyForEach下拉刷新 上拉加载更多实际开发中的几个坑前言上一篇把工程骨架搭好了这篇直接进首页。首页是用户打开 App 看到的第一个页面信息密度很高搜索栏、轮播图、分类入口、商品流全得塞进一个屏幕里还得滑动流畅。我拆成了四个区块来做从上到下搜索栏 → 轮播 → 金刚区 → 商品瀑布流。外面套一个 Scroll整体可滚动。首页整体布局先把大框架搭起来。首页用 Scroll 包裹一个 ColumnColumn 里按顺序放各个区块// entry/src/main/ets/pages/HomePage.etsimport{SwiperBanner}from../components/SwiperBannerimport{CategoryGrid}from../components/CategoryGridimport{ProductWaterFlow}from../components/ProductWaterFlowimport{SearchBar}from../components/SearchBarComponentexportstruct HomePage{StateisRefreshing:booleanfalseStateisLoadingMore:booleanfalseStateproductList:ProductItem[][]StatecurrentPage:number1StatehasMore:booleantruescroller:ScrollernewScroller()build(){Column(){// 顶部搜索栏固定在顶部不跟着滚SearchBar().width(100%)// 可滚动区域Scroll(this.scroller){Column(){// 轮播图SwiperBanner().width(100%).height(180).margin({top:8})// 金刚区分类导航CategoryGrid().width(100%).margin({top:12})// 商品瀑布流ProductWaterFlow({products:this.productList,onLoadMore:()this.loadMore()}).width(100%).margin({top:12})}.width(100%)}.layoutWeight(1).scrollBar(BarState.Off).edgeEffect(EdgeEffect.Spring).onScrollEdge((side:Edge){if(sideEdge.Top){this.onRefresh()}if(sideEdge.Bottom){this.loadMore()}})}.width(100%).height(100%).backgroundColor(#F5F5F5).onAppear((){this.loadProducts()})}privateasyncloadProducts(){// 首页商品数据加载this.currentPage1// TODO: 调用 ProductRepository.getHomeProductsthis.productListgenerateMockProducts(20)}privateasyncloadMore(){if(this.isLoadingMore||!this.hasMore)returnthis.isLoadingMoretruethis.currentPageconstmoregenerateMockProducts(20)if(more.length0){this.hasMorefalse}else{this.productList[...this.productList,...more]}this.isLoadingMorefalse}privateasynconRefresh(){if(this.isRefreshing)returnthis.isRefreshingtrueawaitthis.loadProducts()this.hasMoretruethis.isRefreshingfalse}}interfaceProductItem{id:stringname:stringprice:numberoriginalPrice:numberimageUrl:stringsales:number}搜索栏是固定在顶部的不跟 Scroll 一起滚动。这个决策很重要——用户滑到中间想搜索的时候不需要滚回顶部。Swiper 轮播图轮播图用 Swiper 组件ArkUI 内置的开箱即用。我加了一些定制自动播放、圆点指示器、圆角卡片样式。// entry/src/main/ets/components/SwiperBanner.etsComponentexportstruct SwiperBanner{StatebannerList:BannerItem[][{id:1,imageUrl:$rawfile(banner_1.png),link:},{id:2,imageUrl:$rawfile(banner_2.png),link:},{id:3,imageUrl:$rawfile(banner_3.png),link:},{id:4,imageUrl:$rawfile(banner_4.png),link:},]StatecurrentIndex:number0build(){Stack({alignContent:Alignment.Bottom}){Swiper(){ForEach(this.bannerList,(item:BannerItem){Image(item.imageUrl).width(100%).height(180).objectFit(ImageFit.Cover).borderRadius(12).onClick((){// 点击跳转对应活动页})},(item:BannerItem)item.id)}.indicator(false).loop(true).autoPlay(true).interval(3000).duration(500).onChange((index:number){this.currentIndexindex}).width(100%).height(180)// 自定义指示器Row({space:6}){ForEach(this.bannerList,(item:BannerItem,index:number){Circle().width(this.currentIndexindex?16:6).height(6).fill(this.currentIndexindex?#FF6B35:#FFFFFF80).animation({duration:200})},(item:BannerItem,index:number)item.id)}.margin({bottom:12})}.padding({left:12,right:12})}}interfaceBannerItem{id:stringimageUrl:Resource link:string}我关掉了默认 indicator自己画了一个。当前页的指示器拉长成椭圆形其余是小圆点视觉上比默认的好看不少。animation加了 200ms 过渡切换时指示器有伸缩动画细节感就出来了。金刚区Grid 宫格布局金刚区就是首页中间那一排分类入口图标电商 App 的标配。我用 Grid 做了一个两行五列的布局// entry/src/main/ets/components/CategoryGrid.etsComponentexportstruct CategoryGrid{privatecategories:GridItem[][{id:1,name:新鲜水果,icon:$rawfile(ic_fruit.png)},{id:2,name:时令蔬菜,icon:$rawfile(ic_vegetable.png)},{id:3,name:肉禽蛋,icon:$rawfile(ic_meat.png)},{id:4,name:海鲜水产,icon:$rawfile(ic_seafood.png)},{id:5,name:乳品烘焙,icon:$rawfile(ic_dairy.png)},{id:6,name:休闲零食,icon:$rawfile(ic_snack.png)},{id:7,name:酒水饮料,icon:$rawfile(ic_drink.png)},{id:8,name:粮油调味,icon:$rawfile(ic_grain.png)},{id:9,name:速食冻品,icon:$rawfile(ic_frozen.png)},{id:10,name:全部分类,icon:$rawfile(ic_all.png)},]build(){Grid(){ForEach(this.categories,(item:GridItem){GridItem(){Column({space:6}){Image(item.icon).width(44).height(44).objectFit(ImageFit.Contain)Text(item.name).fontSize(12).fontColor(#333333).maxLines(1)}.width(100%).height(100%).justifyContent(FlexAlign.Center).onClick((){// 跳转到对应分类页})}},(item:GridItem)item.id)}.columnsTemplate(1fr 1fr 1fr 1fr 1fr).rowsTemplate(1fr 1fr).rowsGap(12).columnsGap(0).height(170).padding({left:12,right:12}).backgroundColor(Color.White).borderRadius(12).margin({left:12,right:12})}}interfaceGridItem{id:stringname:stringicon:Resource}10 个图标分两行排列用columnsTemplate控制 5 列等宽。高度固定 170vp给图标和文字留够空间。这里有个小细节背景是白色圆角卡片外面是灰色页面底色形成视觉层次感。金刚区和轮播图都用了 12vp 圆角保持统一。商品瀑布流WaterFlow LazyForEach这是首页最复杂的部分。商品列表用瀑布流展示——两列不等高的卡片高度由商品图片和信息决定。ArkUI 的WaterFlow组件天然支持这个布局。数据量大的时候不能全量渲染得用LazyForEach做懒加载。这里需要一个IDataSource实现// entry/src/main/ets/model/ProductDataSource.etsimport{LazyForEach}fromkit.ArkUIexportclassProductDataSourceimplementsIDataSource{privateproducts:ProductItem[][]privatelisteners:DataChangeListener[][]publictotalCount():number{returnthis.products.length}publicgetData(index:number):ProductItem{returnthis.products[index]}publicgetDataIndex(item:ProductItem):number{returnthis.products.findIndex(pp.iditem.id)}publicregisterDataChangeListener(listener:DataChangeListener):void{this.listeners.push(listener)}publicunregisterDataChangeListener(listener:DataChangeListener):void{constindexthis.listeners.indexOf(listener)if(index0){this.listeners.splice(index,1)}}publicappendData(items:ProductItem[]):void{conststartthis.products.lengththis.products.push(...items)this.listeners.forEach(listener{listener.onDataAdd(this.products.length-items.length,this.products.length-1)})}publicreloadData(items:ProductItem[]):void{this.productsitemsthis.listeners.forEach(listener{listener.onDataReload()})}}有了 DataSourceWaterFlow 商品卡片写起来就清爽了// entry/src/main/ets/components/ProductWaterFlow.etsimport{ProductDataSource}from../model/ProductDataSourceComponentexportstruct ProductWaterFlow{StatedataSource:ProductDataSourcenewProductDataSource()Propproducts:ProductItem[][]onLoadMore?:()voidaboutToAppear(){this.dataSource.reloadData(this.products)}build(){WaterFlow({scroller:newScroller()}){LazyForEach(this.dataSource,(item:ProductItem){FlowItem(){this.ProductCard(item)}},(item:ProductItem)item.id)}.columnsTemplate(1fr 1fr).columnsGap(8).rowsGap(8).padding({left:12,right:12}).height(600)// 给一个足够大的高度让 Scroll 接管滚动.onReachEnd((){this.onLoadMore?.()})}BuilderProductCard(item:ProductItem){Column(){// 商品图片高度随机制造瀑布流效果Image(item.imageUrl).width(100%).height(item.id.charCodeAt(0)%20?180:150).objectFit(ImageFit.Cover).borderRadius({topLeft:8,topRight:8})Column({space:4}){Text(item.name).fontSize(14).fontColor(#333333).maxLines(2).textOverflow({overflow:TextOverflow.Ellipsis}).width(100%)Row(){Text(¥${item.price}).fontSize(18).fontWeight(FontWeight.Bold).fontColor(#FF4D4F)if(item.originalPriceitem.price){Text(¥${item.originalPrice}).fontSize(12).fontColor(#999999).decoration({type:TextDecorationType.LineThrough}).margin({left:6})}Blank()Text(已售${item.sales}).fontSize(11).fontColor(#BBBBBB)}.width(100%)}.padding(10)}.backgroundColor(Color.White).borderRadius(8)}}WaterFlow 的columnsTemplate(1fr 1fr)指定两列等宽columnsGap和rowsGap控制间距。每张卡片的高度不同图片高度随机自然就形成了瀑布流的错落效果。下拉刷新 上拉加载更多刷新和加载更多我在首页的 Scroll 上统一处理的。onScrollEdge监听滚动到边缘的事件Edge.Top触发下拉刷新Edge.Bottom触发加载更多。有个体验上的小问题加载更多的时候如果数据还没回来用户继续滑会重复触发。所以我用了isLoadingMore和hasMore两个标志位做防抖在loadMore方法里一开始就判断privateasyncloadMore(){if(this.isLoadingMore||!this.hasMore)return// ...}hasMore在返回数据为空时设为 false之后就再也不会触发加载了。刷新时把它重置回 true又能继续翻页。实际项目中我还加了一个底部加载提示在 ProductWaterFlow 下面放一个 Row 显示「加载中…」或「没有更多了」这里为了篇幅就不展开了。实际开发中的几个坑WaterFlow 放在 Scroll 里高度问题WaterFlow 本身不会自动撑开需要给一个足够大的固定高度或者用constraintSize限制。我一开始没给高度整个瀑布流就塌了。LazyForEach 的 key 必须唯一用item.id做 key千万别用 index。用 index 的话追加数据后前面的 key 不变但位置变了LazyForEach 会重新渲染所有卡片滑动位置也会跳。图片加载闪烁网络图片首次加载会有短暂空白可以加一个灰色占位背景在 Image 组件上用.backgroundColor(#F0F0F0)就行。首页这个页面信息密度大组件也多建议先把布局跑通用 mock 数据把样式调好最后再接真实接口。下一篇我们搞搜索模块输入联想、搜索历史、搜索结果页一个都不能少。