系列文章:鸿蒙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 // 底部自定义TabBar3.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 关键代码解析
属性/方法 | 作用 |
|---|---|
| 开启自动播放,无需手动触发定时器 |
| 设置自动播放间隔为 3000 毫秒 |
| 隐藏 Swiper 内置指示器,改用自定义圆点 |
| 回调当前页索引,用于同步指示器状态 |
| 自定义指示器,选中态宽度展开、颜色变化 |
要点:自定义指示器比内置指示器灵活得多,可以自由控制样式、颜色和动画效果。通过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装饰器,实现父组件Index的currentTab与子组件BottomTabBar之间的双向同步。当用户点击 Tab 时,currentTab自动更新,页面内容随之切换。@Builder 页面构建器:使用
@Builder装饰器定义各个 Tab 对应的页面内容,代码结构清晰,每页独立维护。animation 动画:图标和文字切换时带有 200ms 的过渡动画,避免生硬的瞬间切换。
顶部阴影:通过
shadow的offsetY: -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 关键代码解析
机制 | 说明 |
|---|---|
| List 滚动到底部时触发,用于上拉加载更多 |
| 原生下拉刷新容器, |
| 防止重复触发加载请求,确保上一次请求完成后再发起新请求 |
| 标识是否还有更多数据,到底后显示"已经到底了"提示 |
展开运算符合并数组 |
|
性能提示:当商品列表数据量较大时,建议使用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 核心组件汇总清单
文件路径 | 组件名 | 职责 |
|---|---|---|
|
| 轮播图 + 自定义指示器 |
|
| 横向滚动分类导航 |
|
| 单个商品卡片展示 |
|
| 双列商品网格容器 |
|
| 自定义底部导航栏 |
| 接口定义 | Product / BannerItem / CategoryItem |
| 数据层 | Mock 数据生成 |
十、总结与扩展建议
10.1 本篇知识点回顾
通过这个电商首页实战,我们系统性地练习了 ArkUI 中最核心的布局和交互能力:
组件/能力 | 核心知识点 |
|---|---|
| 自动播放、循环、自定义指示器、onChange 回调 |
| 横向滚动列表、弹性边缘效果 |
| 网格布局、columnsTemplate、间距控制 |
| 纵向滚动列表、onReachEnd 事件 |
| 原生下拉刷新、refreshing 双向绑定 |
| 父子组件数据双向同步 |
| 声明式 UI 片段复用 |
| 卡片视觉层次和圆角 |
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. 图片优化
使用
ImageKnife或Glide等图片加载库实现三级缓存服务端返回缩略图用于列表,点击后加载高清大图
为不同屏幕密度提供合适的图片资源
4. 性能优化
商品列表使用
LazyForEach实现懒加载,减少内存占用图片使用
cachedCount属性预加载可视区域外的图片避免在
build方法中创建复杂对象,将计算逻辑前置到aboutToAppear或数据层
5. 无障碍适配
为关键组件添加accessibilityText和accessibilityDescription,让视障用户也能通过 TalkBack 使用你的应用。
10.3 系列文章导航
本文为鸿蒙NEXT开发实战系列第14篇
下一篇预告:ArkUI 动画进阶 -- 手势驱动的交互动效实战
如果你在实现过程中遇到问题,欢迎在评论区留言讨论。完整的项目源码已同步到 GitHub 仓库,可以直接 clone 运行。
写在最后:电商首页看似简单,实则是一个综合性极强的 UI 实战项目。掌握本文中的所有组件用法和布局技巧后,你不仅能够独立完成鸿蒙应用的首页开发,更能在面对其他复杂页面时举一反三。技术的提升从来不是一蹴而就的,把每一个案例做精做透,才是成长的捷径。