当前位置: 首页 > news >正文

ArkUI电商首页完整实战

系列文章:鸿蒙NEXT开发实战系列 -- 第14篇适合人群:有ArkUI基础的开发者开发环境:DevEco Studio 5.0.5+ | HarmonyOS NEXT (API 14)阅读时长:约30分钟


一、引言:为什么用电商首页练手

电商首页是前端/移动端开发中最经典的综合实战场景。一个看似简单的电商首页,实际上涵盖了绝大部分常见的 UI 交互模式:

  • 轮播图:自动播放、手势滑动、指示器联动

  • 分类导航:横向滚动、图标+文字组合布局

  • 商品列表:瀑布流/网格布局、图片懒加载、价格标签

  • 底部导航栏:多 Tab 切换、图标选中态、页面路由

  • 下拉刷新与上拉加载:列表性能优化、分页数据加载

如果你能独立实现一个完整的电商首页,说明你已经掌握了 ArkUI 开发中 80% 的核心能力。本文将带你从零搭建一个功能完整、代码可复用的鸿蒙电商首页,每个组件都配有详细解析,确保你能真正理解原理并应用到自己的项目中。


二、最终效果预览

完成本实战后,你将得到如下效果的页面:

+------------------------------------------+ | [搜索栏] [消息图标] | +------------------------------------------+ | +--------------------------------------+| | | Swiper 轮播广告区域 || | | (自动轮播 + 底部圆点指示器) || | +--------------------------------------+| +------------------------------------------+ | [分类1] [分类2] [分类3] [分类4] [分类5] > | | [图标] [图标] [图标] [图标] [图标] | +------------------------------------------+ | 热销推荐 | | +-------------+ +-------------+ | | | 商品图片 | | 商品图片 | | | | 商品标题 | | 商品标题 | | | | ¥99.00 | | ¥199.00 | | | +-------------+ +-------------+ | | +-------------+ +-------------+ | | | ... | | ... | | | +-------------+ +-------------+ | +------------------------------------------+ | [首页] [分类] [购物车] [我的] | +------------------------------------------+

整体采用经典的电商布局:顶部搜索栏 + 轮播图 + 分类导航 + 双列商品网格 + 底部 TabBar,并支持下拉刷新和上拉加载更多。


三、项目架构设计

3.1 页面结构拆分

我们将页面拆分为以下独立组件,每个组件职责单一、可复用:

pages/ └── Index.ets // 主页面,负责组装各组件 components/ ├── SearchBar.ets // 顶部搜索栏 ├── BannerSwiper.ets // 轮播图组件 ├── CategoryNav.ets // 分类导航组件 ├── ProductGrid.ets // 商品瀑布流网格 ├── ProductCard.ets // 单个商品卡片 └── BottomTabBar.ets // 底部自定义TabBar

3.2 数据模型定义

在开始编写组件之前,先定义好核心数据模型:

// models/Product.ets /** 商品数据模型 */ export interface Product { id: number; title: string; // 商品标题 price: number; // 价格 originalPrice: number; // 原价 image: string; // 商品图片地址 sales: number; // 销量 } /** 轮播图数据模型 */ export interface BannerItem { id: number; image: string; title: string; } /** 分类导航数据模型 */ export interface CategoryItem { id: number; name: string; icon: string; // 图标资源路径或symbol名称 }

3.3 Mock 数据准备

为了让项目可以独立运行,我们准备一组 Mock 数据:

// data/MockData.ets import { BannerItem, CategoryItem, Product } from '../models/Product'; /** 轮播图数据 */ export const bannerList: BannerItem[] = [ { id: 1, image: $r('app.media.banner1'), title: '618年中大促' }, { id: 2, image: $r('app.media.banner2'), title: '新品首发' }, { id: 3, image: $r('app.media.banner3'), title: '品牌特卖' }, ]; /** 分类导航数据 */ export const categoryList: CategoryItem[] = [ { id: 1, name: '手机', icon: 'phone' }, { id: 2, name: '电脑', icon: 'monitor' }, { id: 3, name: '服饰', icon: 'shirt' }, { id: 4, name: '美妆', icon: 'palette' }, { id: 5, name: '家居', icon: 'home' }, { id: 6, name: '食品', icon: 'coffee' }, { id: 7, name: '运动', icon: 'run' }, { id: 8, name: '图书', icon: 'book' }, ]; /** 生成商品Mock数据 */ export function generateProducts(page: number, pageSize: number): Product[] { const products: Product[] = []; const startId = (page - 1) * pageSize + 1; for (let i = 0; i < pageSize; i++) { const id = startId + i; products.push({ id, title: `鸿蒙精选好物第${id}款 超值特惠不容错过`, price: Math.round(Math.random() * 500 + 50), originalPrice: Math.round(Math.random() * 800 + 200), image: $r('app.media.product_sample'), sales: Math.round(Math.random() * 10000), }); } return products; }

四、实现1:Swiper 轮播图组件

4.1 UI 效果描述

轮播图占据页面顶部核心区域,支持自动循环播放(3秒间隔),底部有圆点指示器跟随当前轮播页切换,用户也可以手动左右滑动。

4.2 完整代码

// components/BannerSwiper.ets @Component export struct BannerSwiper { @Link bannerList: BannerItem[]; @State currentIndex: number = 0; build() { Column() { Swiper() { ForEach(this.bannerList, (item: BannerItem) => { Image(item.image) .width('100%') .height(180) .borderRadius(12) .objectFit(ImageFit.Cover) }) } .autoPlay(true) // 自动播放 .interval(3000) // 播放间隔3秒 .loop(true) // 循环播放 .indicator(false) // 隐藏默认指示器,使用自定义指示器 .duration(500) // 切换动画时长 .curve(Curve.EaseInOut) // 切换动画曲线 .width('100%') .height(180) .margin({ top: 12 }) .padding({ left: 16, right: 16 }) .onChange((index: number) => { this.currentIndex = index; }) // 自定义圆点指示器 Row() { ForEach(this.bannerList, (_: BannerItem, index: number) => { Circle() .width(this.currentIndex === index ? 16 : 8) .height(8) .fill(this.currentIndex === index ? '#FF6B35' : '#CCCCCC') .borderRadius(4) .animation({ duration: 300, curve: Curve.EaseInOut, }) .margin({ left: 3, right: 3 }) }) } .justifyContent(FlexAlign.Center) .width('100%') .margin({ top: 8 }) } } }

4.3 关键代码解析

属性/方法

作用

autoPlay(true)

开启自动播放,无需手动触发定时器

interval(3000)

设置自动播放间隔为 3000 毫秒

indicator(false)

隐藏 Swiper 内置指示器,改用自定义圆点

onChange

回调当前页索引,用于同步指示器状态

Circle+ 动画

自定义指示器,选中态宽度展开、颜色变化

要点:自定义指示器比内置指示器灵活得多,可以自由控制样式、颜色和动画效果。通过animation属性让宽度变化带有过渡动效,提升视觉体验。


五、实现2:商品分类导航

5.1 UI 效果描述

横向可滚动的图标列表,每行显示 4-5 个分类,支持超出屏幕后左右滑动。每个分类由圆形图标和文字标签组成,点击后有按压反馈效果。

5.2 完整代码

// components/CategoryNav.ets @Component export struct CategoryNav { @Link categoryList: CategoryItem[]; build() { Column() { Text('全部分类') .fontSize(18) .fontWeight(FontWeight.Bold) .width('100%') .padding({ left: 16, top: 12, bottom: 8 }) Scroll() { Row() { ForEach(this.categoryList, (item: CategoryItem) => { Column() { // 图标容器 Stack() { Circle() .width(48) .height(48) .fill('#FFF0EB') Text(this.getIconSymbol(item.icon)) .fontSize(24) .fontColor('#FF6B35') } Text(item.name) .fontSize(12) .fontColor('#333333') .margin({ top: 6 }) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) } .width(72) .alignItems(HorizontalAlign.Center) .padding({ top: 8, bottom: 8 }) .borderRadius(12) .onClick(() => { console.info(`点击分类: ${item.name}, id: ${item.id}`); }) }) } .padding({ left: 16, right: 16 }) } .scrollable(ScrollDirection.Horizontal) // 横向滚动 .scrollBar(BarState.Off) // 隐藏滚动条 .edgeEffect(EdgeEffect.Spring) // 弹性边缘效果 } .width('100%') .backgroundColor(Color.White) .borderRadius(12) .margin({ left: 16, right: 16, top: 12 }) .padding({ bottom: 12 }) } /** 根据icon名称返回对应的symbol或文字占位 */ private getIconSymbol(iconName: string): string { // 实际项目中建议使用 SymbolGlyph 或 Image 组件 const iconMap: Record<string, string> = { 'phone': '\uf10b', 'monitor': '\uf108', 'shirt': '\uf553', 'palette': '\uf53f', 'home': '\uf015', 'coffee': '\uf0f4', 'run': '\uf70c', 'book': '\uf02d', }; return iconMap[iconName] ?? '\uf05a'; } }

5.3 关键代码解析

  • Scroll + Row 组合Scroll组件的scrollable(ScrollDirection.Horizontal)让内容横向滚动,内部用Row水平排列子项。这是 ArkUI 中实现横向滚动列表的标准模式。

  • scrollBar(BarState.Off):隐藏滚动条,保持界面整洁。

  • edgeEffect(EdgeEffect.Spring):滚动到边缘时有弹性回弹效果,符合移动端操作习惯。

  • 按压交互:在实际项目中,建议在外层Column上添加.stateStyles实现按压态颜色变化,增强触感反馈。


六、实现3:Grid 商品瀑布流

6.1 UI 效果描述

商品区域采用双列网格布局(类似淘宝/京东),每个商品卡片包含:商品图片(带圆角)、标题(最多两行,超出省略)、销量标签、原价(划线价)和现价。整体支持滚动和懒加载。

6.2 完整代码

// components/ProductCard.ets @Component export struct ProductCard { product: Product = {} as Product; build() { Column() { // 商品图片 Image(this.product.image) .width('100%') .aspectRatio(1) // 1:1 正方形 .objectFit(ImageFit.Cover) .borderRadius({ topLeft: 12, topRight: 12 }) // 信息区域 Column() { // 商品标题 Text(this.product.title) .fontSize(14) .fontColor('#333333') .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) .lineHeight(20) // 销量 Text(`已售 ${this.formatSales(this.product.sales)}`) .fontSize(11) .fontColor('#999999') .margin({ top: 4 }) // 价格区域 Row() { Text('¥') .fontSize(12) .fontColor('#FF4D4F') .fontWeight(FontWeight.Bold) Text(this.product.price.toFixed(2)) .fontSize(18) .fontColor('#FF4D4F') .fontWeight(FontWeight.Bold) Text(`¥${this.product.originalPrice.toFixed(2)}`) .fontSize(11) .fontColor('#BBBBBB') .decoration({ type: TextDecorationType.LineThrough }) .margin({ left: 6 }) } .alignItems(VerticalAlign.Bottom) .margin({ top: 8 }) } .padding({ left: 8, right: 8, top: 6, bottom: 10 }) .alignItems(HorizontalAlign.Start) } .width('100%') .backgroundColor(Color.White) .borderRadius(12) .shadow({ radius: 8, color: 'rgba(0,0,0,0.06)', offsetX: 0, offsetY: 2, }) } /** 格式化销量数字 */ private formatSales(sales: number): string { if (sales >= 10000) { return (sales / 10000).toFixed(1) + '万'; } return sales.toString(); } }
// components/ProductGrid.ets @Component export struct ProductGrid { @Link productList: Product[]; build() { Column() { // 区域标题 Row() { Text('热销推荐') .fontSize(18) .fontWeight(FontWeight.Bold) .fontColor('#333333') Blank() Text('查看更多 >') .fontSize(13) .fontColor('#999999') } .width('100%') .padding({ left: 16, right: 16, top: 16, bottom: 8 }) // 双列网格 Grid() { ForEach(this.productList, (product: Product) => { GridItem() { ProductCard({ product: product }) } }) } .columnsTemplate('1fr 1fr') // 两列等宽 .columnsGap(8) // 列间距 .rowsGap(8) // 行间距 .width('100%') .padding({ left: 8, right: 8, bottom: 8 }) .layoutWeight(1) // 占据剩余空间 } .width('100%') } }

6.3 关键代码解析

  • columnsTemplate('1fr 1fr'):这是 Grid 实现双列布局的关键,1fr 1fr表示两列等分可用空间。如果要三列则写'1fr 1fr 1fr'

  • columnsGap / rowsGap:控制网格的列间距和行间距,让卡片之间留有呼吸空间。

  • ProductCard 独立组件:将单个商品卡片抽为独立组件,方便在其他页面(搜索结果、收藏列表等)复用。

  • aspectRatio(1):让商品图片保持 1:1 的正方形比例,这是电商图片的标准比例。

  • shadow:通过shadow属性给卡片添加微弱的阴影,营造"浮起来"的视觉层次感。

  • 销量格式化:超过 1 万的销量显示为 "1.2万",更符合中文阅读习惯。


七、实现4:自定义底部 TabBar

7.1 UI 效果描述

底部包含 4 个 Tab:首页、分类、购物车、我的。选中态图标变色、文字加粗变色,未选中态为灰色。支持点击切换,同时配合页面路由实现真正的页面切换。

7.2 完整代码

// components/BottomTabBar.ets export interface TabItem { title: string; icon: Resource; // 未选中图标 selectedIcon: Resource; // 选中图标 index: number; } @Component export struct BottomTabBar { @Link selectedIndex: number; tabItems: TabItem[] = []; build() { Row() { ForEach(this.tabItems, (item: TabItem) => { Column() { Image(this.selectedIndex === item.index ? item.selectedIcon : item.icon) .width(24) .height(24) .objectFit(ImageFit.Contain) .animation({ duration: 200, curve: Curve.EaseInOut }) Text(item.title) .fontSize(10) .fontColor(this.selectedIndex === item.index ? '#FF6B35' : '#999999') .fontWeight(this.selectedIndex === item.index ? FontWeight.Bold : FontWeight.Normal) .margin({ top: 2 }) .animation({ duration: 200, curve: Curve.EaseInOut }) } .layoutWeight(1) .justifyContent(FlexAlign.Center) .height('100%') .onClick(() => { if (this.selectedIndex !== item.index) { this.selectedIndex = item.index; } }) }) } .width('100%') .height(56) .backgroundColor(Color.White) .border({ width: { top: 0.5 }, color: '#E5E5E5', }) .padding({ bottom: 8 }) .shadow({ radius: 8, color: 'rgba(0,0,0,0.08)', offsetX: 0, offsetY: -2, }) } }

7.3 主页面集成 TabBar

// pages/Index.ets import { bannerList, categoryList, generateProducts } from '../data/MockData'; import { BannerSwiper } from '../components/BannerSwiper'; import { CategoryNav } from '../components/CategoryNav'; import { ProductGrid } from '../components/ProductGrid'; import { BottomTabBar, TabItem } from '../components/BottomTabBar'; import { Product } from '../models/Product'; @Entry @Component struct Index { @State currentTab: number = 0; @State products: Product[] = generateProducts(1, 10); @State isRefreshing: boolean = false; @State currentPage: number = 1; private tabItems: TabItem[] = [ { title: '首页', icon: $r('app.media.ic_home'), selectedIcon: $r('app.media.ic_home_active'), index: 0 }, { title: '分类', icon: $r('app.media.ic_category'), selectedIcon: $r('app.media.ic_category_active'), index: 1 }, { title: '购物车', icon: $r('app.media.ic_cart'), selectedIcon: $r('app.media.ic_cart_active'), index: 2 }, { title: '我的', icon: $r('app.media.ic_profile'), selectedIcon: $r('app.media.ic_profile_active'), index: 3 }, ]; build() { Column() { // ---- 首页内容区域 ---- if (this.currentTab === 0) { this.HomePage() } else if (this.currentTab === 1) { this.CategoryPage() } else if (this.currentTab === 2) { this.CartPage() } else { this.ProfilePage() } // ---- 底部 TabBar ---- BottomTabBar({ selectedIndex: $currentTab, tabItems: this.tabItems, }) } .width('100%') .height('100%') .backgroundColor('#F5F5F5') } @Builder HomePage() { // 首页内容在下一节实现,包含下拉刷新和上拉加载 } @Builder CategoryPage() { Column() { Text('分类页面') .fontSize(24) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) } @Builder CartPage() { Column() { Text('购物车页面') .fontSize(24) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) } @Builder ProfilePage() { Column() { Text('我的页面') .fontSize(24) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) } }

7.4 关键代码解析

  • @Link 双向绑定selectedIndex使用@Link装饰器,实现父组件IndexcurrentTab与子组件BottomTabBar之间的双向同步。当用户点击 Tab 时,currentTab自动更新,页面内容随之切换。

  • @Builder 页面构建器:使用@Builder装饰器定义各个 Tab 对应的页面内容,代码结构清晰,每页独立维护。

  • animation 动画:图标和文字切换时带有 200ms 的过渡动画,避免生硬的瞬间切换。

  • 顶部阴影:通过shadowoffsetY: -2向上方投射阴影,让 TabBar 与内容区域有明确的视觉分界。


八、实现5:下拉刷新与上拉加载更多

8.1 UI 效果描述

首页商品列表支持两种加载交互:

  • 下拉刷新:下拉到顶部后松手,触发数据刷新,列表重置为第一页。

  • 上拉加载更多:滚动到底部时自动加载下一页数据,追加到列表末尾。

8.2 完整代码

现在补全首页HomePageBuilder 的实现,将所有组件组装在一起:

// pages/Index.ets -- HomePage 部分完善 @Entry @Component struct Index { @State currentTab: number = 0; @State bannerData: BannerItem[] = bannerList; @State categoryData: CategoryItem[] = categoryList; @State products: Product[] = generateProducts(1, 10); @State currentPage: number = 1; @State isLoadingMore: boolean = false; @State hasMore: boolean = true; private scroller: Scroller = new Scroller(); @Builder HomePage() { List({ scroller: this.scroller }) { // 轮播图区域 ListItem() { BannerSwiper({ bannerList: $bannerData }) } // 分类导航区域 ListItem() { CategoryNav({ categoryList: $categoryData }) } // 商品网格区域 ListItem() { ProductGrid({ productList: $products }) .padding({ top: 12 }) } // 加载状态提示 ListItem() { Row() { if (this.isLoadingMore) { LoadingProgress() .width(20) .height(20) .color('#FF6B35') Text('加载中...') .fontSize(13) .fontColor('#999999') .margin({ left: 6 }) } else if (!this.hasMore) { Text('-- 已经到底了 --') .fontSize(13) .fontColor('#CCCCCC') } } .width('100%') .height(50) .justifyContent(FlexAlign.Center) } } .width('100%') .layoutWeight(1) .scrollBar(BarState.Off) .edgeEffect(EdgeEffect.Spring) .onReachEnd(() => { // 上拉加载更多 if (!this.isLoadingMore && this.hasMore) { this.loadMore(); } }) .onScrollStop(() => { console.info('列表停止滚动'); }) } /** 模拟下拉刷新 */ private onRefresh(): void { this.currentPage = 1; this.hasMore = true; // 模拟网络请求延迟 setTimeout(() => { this.products = generateProducts(1, 10); this.isLoadingMore = false; }, 1000); } /** 模拟上拉加载更多 */ private loadMore(): void { this.isLoadingMore = true; this.currentPage++; // 模拟网络请求延迟 setTimeout(() => { const newProducts = generateProducts(this.currentPage, 10); if (this.currentPage > 5) { // 模拟没有更多数据 this.hasMore = false; } else { this.products = [...this.products, ...newProducts]; } this.isLoadingMore = false; }, 1000); } }

8.3 下拉刷新 -- Refresh 组件方式

HarmonyOS NEXT 提供了原生的Refresh组件来实现下拉刷新,用法如下:

@Builder HomePage() { Refresh({ refreshing: $$this.isRefreshing, // 双向绑定刷新状态 offset: 60, // 下拉触发偏移量 friction: 65, // 摩擦系数 }) { List({ scroller: this.scroller }) { // ... 上述 List 内容保持不变 ... } .width('100%') .layoutWeight(1) .scrollBar(BarState.Off) .onReachEnd(() => { if (!this.isLoadingMore && this.hasMore) { this.loadMore(); } }) } .onRefreshing(() => { // 下拉刷新触发时的回调 this.onRefresh(); }) .width('100%') .height('100%') }

8.4 关键代码解析

机制

说明

onReachEnd()

List 滚动到底部时触发,用于上拉加载更多

Refresh组件

原生下拉刷新容器,refreshing双向绑定控制加载动画

isLoadingMore状态锁

防止重复触发加载请求,确保上一次请求完成后再发起新请求

hasMore标记

标识是否还有更多数据,到底后显示"已经到底了"提示

展开运算符合并数组

[...this.products, ...newProducts]将新数据追加到现有列表末尾

性能提示:当商品列表数据量较大时,建议使用LazyForEach替代ForEach,实现按需渲染,避免一次性创建过多组件导致内存压力。

使用LazyForEach的改写方式:

// 需要实现 IDataSource 接口 class ProductDataSource implements IDataSource { private products: Product[] = []; totalCount(): number { return this.products.length; } getData(index: number): Product { return this.products[index]; } registerDataChangeListener(listener: DataChangeListener): void {} unregisterDataChangeListener(listener: DataChangeListener): void {} pushData(newProducts: Product[]): void { this.products.push(...newProducts); } resetData(newProducts: Product[]): void { this.products = newProducts; } }

九、完整源码汇总

9.1 Index.ets 主页面

// pages/Index.ets import { BannerItem, CategoryItem, Product } from '../models/Product'; import { bannerList, categoryList, generateProducts } from '../data/MockData'; import { BannerSwiper } from '../components/BannerSwiper'; import { CategoryNav } from '../components/CategoryNav'; import { ProductGrid } from '../components/ProductGrid'; import { BottomTabBar, TabItem } from '../components/BottomTabBar'; @Entry @Component struct Index { @State currentTab: number = 0; @State bannerData: BannerItem[] = bannerList; @State categoryData: CategoryItem[] = categoryList; @State products: Product[] = []; @State currentPage: number = 1; @State isRefreshing: boolean = false; @State isLoadingMore: boolean = false; @State hasMore: boolean = true; private scroller: Scroller = new Scroller(); private tabItems: TabItem[] = [ { title: '首页', icon: $r('app.media.ic_home'), selectedIcon: $r('app.media.ic_home_active'), index: 0 }, { title: '分类', icon: $r('app.media.ic_category'), selectedIcon: $r('app.media.ic_category_active'), index: 1 }, { title: '购物车', icon: $r('app.media.ic_cart'), selectedIcon: $r('app.media.ic_cart_active'), index: 2 }, { title: '我的', icon: $r('app.media.ic_profile'), selectedIcon: $r('app.media.ic_profile_active'), index: 3 }, ]; aboutToAppear(): void { this.products = generateProducts(1, 10); } build() { Column() { // 顶部搜索栏 this.SearchBarBuilder() // 主内容区域 if (this.currentTab === 0) { this.HomePage() } else if (this.currentTab === 1) { this.PlaceholderPage('分类') } else if (this.currentTab === 2) { this.PlaceholderPage('购物车') } else { this.PlaceholderPage('我的') } // 底部TabBar BottomTabBar({ selectedIndex: $currentTab, tabItems: this.tabItems }) } .width('100%') .height('100%') .backgroundColor('#F5F5F5') } /** 顶部搜索栏 */ @Builder SearchBarBuilder() { Row() { Row() { Text('\uf002') // 搜索图标 .fontSize(14) .fontColor('#999999') .margin({ right: 8 }) Text('搜索商品、品牌') .fontSize(14) .fontColor('#CCCCCC') } .height(36) .borderRadius(18) .backgroundColor('#F0F0F0') .padding({ left: 16, right: 16 }) .layoutWeight(1) .margin({ right: 12 }) // 消息图标 Text('\uf0f3') // 铃铛图标 .fontSize(20) .fontColor('#333333') } .width('100%') .height(52) .padding({ left: 16, right: 16 }) .backgroundColor(Color.White) .alignItems(VerticalAlign.Center) } /** 首页内容 */ @Builder HomePage() { Refresh({ refreshing: $$this.isRefreshing, offset: 60, friction: 65, }) { List({ scroller: this.scroller }) { ListItem() { BannerSwiper({ bannerList: $bannerData }) } ListItem() { CategoryNav({ categoryList: $categoryData }) } ListItem() { ProductGrid({ productList: $products }) .padding({ top: 12 }) } ListItem() { this.LoadingFooter() } } .width('100%') .layoutWeight(1) .scrollBar(BarState.Off) .edgeEffect(EdgeEffect.Spring) .onReachEnd(() => { if (!this.isLoadingMore && this.hasMore) { this.loadMore(); } }) } .onRefreshing(() => { this.onRefresh(); }) .width('100%') .height('100%') } /** 列表底部加载状态 */ @Builder LoadingFooter() { Row() { if (this.isLoadingMore) { LoadingProgress().width(20).height(20).color('#FF6B35') Text('加载中...').fontSize(13).fontColor('#999999').margin({ left: 6 }) } else if (!this.hasMore) { Text('-- 已经到底了 --').fontSize(13).fontColor('#CCCCCC') } } .width('100%') .height(50) .justifyContent(FlexAlign.Center) } /** 占位页面 */ @Builder PlaceholderPage(title: string) { Column() { Text(`${title}页面`) .fontSize(24) .fontColor('#999999') } .width('100%') .layoutWeight(1) .justifyContent(FlexAlign.Center) } /** 下拉刷新 */ private onRefresh(): void { this.currentPage = 1; this.hasMore = true; setTimeout(() => { this.products = generateProducts(1, 10); this.isRefreshing = false; }, 1000); } /** 上拉加载更多 */ private loadMore(): void { this.isLoadingMore = true; this.currentPage++; setTimeout(() => { if (this.currentPage > 5) { this.hasMore = false; } else { const newProducts = generateProducts(this.currentPage, 10); this.products = [...this.products, ...newProducts]; } this.isLoadingMore = false; }, 1500); } }

9.2 核心组件汇总清单

文件路径

组件名

职责

components/BannerSwiper.ets

BannerSwiper

轮播图 + 自定义指示器

components/CategoryNav.ets

CategoryNav

横向滚动分类导航

components/ProductCard.ets

ProductCard

单个商品卡片展示

components/ProductGrid.ets

ProductGrid

双列商品网格容器

components/BottomTabBar.ets

BottomTabBar

自定义底部导航栏

models/Product.ets

接口定义

Product / BannerItem / CategoryItem

data/MockData.ets

数据层

Mock 数据生成


十、总结与扩展建议

10.1 本篇知识点回顾

通过这个电商首页实战,我们系统性地练习了 ArkUI 中最核心的布局和交互能力:

组件/能力

核心知识点

Swiper

自动播放、循环、自定义指示器、onChange 回调

Scroll+Row

横向滚动列表、弹性边缘效果

Grid+GridItem

网格布局、columnsTemplate、间距控制

List+ListItem

纵向滚动列表、onReachEnd 事件

Refresh

原生下拉刷新、refreshing 双向绑定

@Link/@State

父子组件数据双向同步

@Builder

声明式 UI 片段复用

shadow/borderRadius

卡片视觉层次和圆角

10.2 生产环境扩展建议

如果你准备将此代码用于实际项目,以下几点建议供参考:

1. 网络请求层

将 Mock 数据替换为真实的网络请求,推荐封装统一的网络工具类:

import { http } from '@kit.NetworkKit'; async function fetchProducts(page: number): Promise<Product[]> { const response = await http.createHttp().request( `https://api.example.com/products?page=${page}`, { method: http.RequestMethod.GET } ); return JSON.parse(response.result as string).data; }

2. 状态管理

当项目规模增大,建议引入状态管理方案。对于中小型项目,@Observed+@ObjectLink足够;大型项目可以考虑 AppStorage 或第三方状态管理库。

3. 图片优化

  • 使用ImageKnifeGlide等图片加载库实现三级缓存

  • 服务端返回缩略图用于列表,点击后加载高清大图

  • 为不同屏幕密度提供合适的图片资源

4. 性能优化

  • 商品列表使用LazyForEach实现懒加载,减少内存占用

  • 图片使用cachedCount属性预加载可视区域外的图片

  • 避免在build方法中创建复杂对象,将计算逻辑前置到aboutToAppear或数据层

5. 无障碍适配

为关键组件添加accessibilityTextaccessibilityDescription,让视障用户也能通过 TalkBack 使用你的应用。


10.3 系列文章导航

  • 本文为鸿蒙NEXT开发实战系列第14篇

  • 下一篇预告:ArkUI 动画进阶 -- 手势驱动的交互动效实战

如果你在实现过程中遇到问题,欢迎在评论区留言讨论。完整的项目源码已同步到 GitHub 仓库,可以直接 clone 运行。


写在最后:电商首页看似简单,实则是一个综合性极强的 UI 实战项目。掌握本文中的所有组件用法和布局技巧后,你不仅能够独立完成鸿蒙应用的首页开发,更能在面对其他复杂页面时举一反三。技术的提升从来不是一蹴而就的,把每一个案例做精做透,才是成长的捷径。

http://www.jsqmd.com/news/786837/

相关文章:

  • CANN/ATVOSS块调度运行接口
  • 人与人的四种差别
  • 5分钟学会:无需越狱导出iOS微信聊天记录的终极方案
  • Hyprland高效截图工具链:集成hyprshot、swappy与pngquant的一键工作流
  • ARM GICv3虚拟化架构与ICH_LR寄存器解析
  • 从零搭建轻量级夜间构建系统:基于Docker与Cron的自动化实践
  • AI应用测试工程2026:如何系统化测试你的LLM应用
  • 基于Vue 3与Vite的快速后台管理框架:fast-soy-admin深度解析
  • 在Taotoken控制台中清晰追踪项目成本与各模型消耗明细
  • BLDC电机控制原理与PID优化实践
  • DeepSeek API调用延迟怎么优化?首字生成时间怎么降低?
  • 边缘部署LLM的混合精度量化技术与优化实践
  • NCM文件格式逆向解析与音频转换技术实现
  • Llama-Chinese项目实战:从中文增量预训练到指令微调部署全解析
  • MCP3551 Delta-Sigma ADC原理与高精度设计实战
  • Atom编辑器终极中文汉化指南:告别英文界面,提升编程效率
  • 抖音视频下载终极指南:3分钟掌握批量无水印下载技巧
  • 工业神经系统:11 老手血泪Tips + 新手避坑清单
  • 系统级自动化测试框架设计:从核心原理到工程实践
  • 32位FMC+SDRAM支持+串行PSRAM:STM32H7A3IIT6的大内存设计
  • Next.js SEO优化实战:使用nextjs-seo-optimizer提升搜索引擎排名
  • Godot双网格瓦片地图系统:实现复杂2D游戏地图的职责分离与高效管理
  • AI模型管理利器:OpenClaw Venice模型切换器原理与实战
  • ImagenTY:基于DashScope API的AI图像生成技能,专为中文渲染与Agent集成设计
  • CCaaS架构:解耦并发控制的分布式数据库创新设计
  • 容器化定时任务管理:基于Docker与Cron的轻量级解决方案
  • Prisma与GraphQL Relay游标分页集成实战指南
  • HKUDS开源NanoBot
  • ARM CoreSight调试架构与寄存器配置实战
  • 对比自行维护多个API密钥,使用Taotoken统一管理带来的效率提升