HarmonyOS7 电商首页怎么做才像样?轮播、金刚区和瀑布流一次跑通
2026/7/1 18:17:04 网站建设 项目流程

文章目录

    • 前言
    • 首页整体布局
    • Swiper 轮播图
    • 金刚区:Grid 宫格布局
    • 商品瀑布流:WaterFlow + LazyForEach
    • 下拉刷新 + 上拉加载更多
    • 实际开发中的几个坑

前言

上一篇把工程骨架搭好了,这篇直接进首页。首页是用户打开 App 看到的第一个页面,信息密度很高:搜索栏、轮播图、分类入口、商品流,全得塞进一个屏幕里,还得滑动流畅。

我拆成了四个区块来做,从上到下:搜索栏 → 轮播 → 金刚区 → 商品瀑布流。外面套一个 Scroll,整体可滚动。

首页整体布局

先把大框架搭起来。首页用 Scroll 包裹一个 Column,Column 里按顺序放各个区块:

// entry/src/main/ets/pages/HomePage.etsimport{SwiperBanner}from'../components/SwiperBanner'import{CategoryGrid}from'../components/CategoryGrid'import{ProductWaterFlow}from'../components/ProductWaterFlow'import{SearchBar}from'../components/SearchBar'@Componentexportstruct HomePage{@StateisRefreshing:boolean=false@StateisLoadingMore:boolean=false@StateproductList:ProductItem[]=[]@StatecurrentPage:number=1@StatehasMore:boolean=truescroller:Scroller=newScroller()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(side===Edge.Top){this.onRefresh()}if(side===Edge.Bottom){this.loadMore()}})}.width('100%').height('100%').backgroundColor('#F5F5F5').onAppear(()=>{this.loadProducts()})}privateasyncloadProducts(){// 首页商品数据加载this.currentPage=1// TODO: 调用 ProductRepository.getHomeProductsthis.productList=generateMockProducts(20)}privateasyncloadMore(){if(this.isLoadingMore||!this.hasMore)returnthis.isLoadingMore=truethis.currentPage++constmore=generateMockProducts(20)if(more.length===0){this.hasMore=false}else{this.productList=[...this.productList,...more]}this.isLoadingMore=false}privateasynconRefresh(){if(this.isRefreshing)returnthis.isRefreshing=trueawaitthis.loadProducts()this.hasMore=truethis.isRefreshing=false}}interfaceProductItem{id:stringname:stringprice:numberoriginalPrice:numberimageUrl:stringsales:number}

搜索栏是固定在顶部的,不跟 Scroll 一起滚动。这个决策很重要——用户滑到中间想搜索的时候,不需要滚回顶部。

Swiper 轮播图

轮播图用 Swiper 组件,ArkUI 内置的,开箱即用。我加了一些定制:自动播放、圆点指示器、圆角卡片样式。

// entry/src/main/ets/components/SwiperBanner.ets@Componentexportstruct 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:number=0build(){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.currentIndex=index}).width('100%').height(180)// 自定义指示器Row({space:6}){ForEach(this.bannerList,(item:BannerItem,index:number)=>{Circle().width(this.currentIndex===index?16:6).height(6).fill(this.currentIndex===index?'#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.ets@Componentexportstruct 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}from'@kit.ArkUI'exportclassProductDataSourceimplementsIDataSource{privateproducts:ProductItem[]=[]privatelisteners:DataChangeListener[]=[]publictotalCount():number{returnthis.products.length}publicgetData(index:number):ProductItem{returnthis.products[index]}publicgetDataIndex(item:ProductItem):number{returnthis.products.findIndex(p=>p.id===item.id)}publicregisterDataChangeListener(listener:DataChangeListener):void{this.listeners.push(listener)}publicunregisterDataChangeListener(listener:DataChangeListener):void{constindex=this.listeners.indexOf(listener)if(index>=0){this.listeners.splice(index,1)}}publicappendData(items:ProductItem[]):void{conststart=this.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.products=itemsthis.listeners.forEach(listener=>{listener.onDataReload()})}}

有了 DataSource,WaterFlow 商品卡片写起来就清爽了:

// entry/src/main/ets/components/ProductWaterFlow.etsimport{ProductDataSource}from'../model/ProductDataSource'@Componentexportstruct ProductWaterFlow{@StatedataSource:ProductDataSource=newProductDataSource()@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)%2===0?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.originalPrice>item.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')指定两列等宽,columnsGaprowsGap控制间距。每张卡片的高度不同(图片高度随机),自然就形成了瀑布流的错落效果。

下拉刷新 + 上拉加载更多

刷新和加载更多我在首页的 Scroll 上统一处理的。onScrollEdge监听滚动到边缘的事件,Edge.Top触发下拉刷新,Edge.Bottom触发加载更多。

有个体验上的小问题:加载更多的时候如果数据还没回来,用户继续滑会重复触发。所以我用了isLoadingMorehasMore两个标志位做防抖,在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 数据把样式调好,最后再接真实接口。下一篇我们搞搜索模块,输入联想、搜索历史、搜索结果页,一个都不能少。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询