系列文章 :鸿蒙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装饰器,实现父组件Index的currentTab与子组件BottomTabBar之间的双向同步。当用户点击 Tab 时,currentTab自动更新,页面内容随之切换。 -
@Builder 页面构建器 :使用
@Builder装饰器定义各个 Tab 对应的页面内容,代码结构清晰,每页独立维护。 -
animation 动画:图标和文字切换时带有 200ms 的过渡动画,避免生硬的瞬间切换。
-
顶部阴影 :通过
shadow的offsetY: -2向上方投射阴影,让 TabBar 与内容区域有明确的视觉分界。
八、实现5:下拉刷新与上拉加载更多
8.1 UI 效果描述
首页商品列表支持两种加载交互:
-
下拉刷新:下拉到顶部后松手,触发数据刷新,列表重置为第一页。
-
上拉加载更多:滚动到底部时自动加载下一页数据,追加到列表末尾。
8.2 完整代码
现在补全首页 HomePage Builder 的实现,将所有组件组装在一起:
// 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. 图片优化
-
使用
ImageKnife或Glide等图片加载库实现三级缓存 -
服务端返回缩略图用于列表,点击后加载高清大图
-
为不同屏幕密度提供合适的图片资源
4. 性能优化
-
商品列表使用
LazyForEach实现懒加载,减少内存占用 -
图片使用
cachedCount属性预加载可视区域外的图片 -
避免在
build方法中创建复杂对象,将计算逻辑前置到aboutToAppear或数据层
5. 无障碍适配
为关键组件添加 accessibilityText 和 accessibilityDescription,让视障用户也能通过 TalkBack 使用你的应用。
10.3 系列文章导航
-
本文为 鸿蒙NEXT开发实战系列 第14篇
-
下一篇预告:ArkUI 动画进阶 -- 手势驱动的交互动效实战
如果你在实现过程中遇到问题,欢迎在评论区留言讨论。完整的项目源码已同步到 GitHub 仓库,可以直接 clone 运行。
写在最后:电商首页看似简单,实则是一个综合性极强的 UI 实战项目。掌握本文中的所有组件用法和布局技巧后,你不仅能够独立完成鸿蒙应用的首页开发,更能在面对其他复杂页面时举一反三。技术的提升从来不是一蹴而就的,把每一个案例做精做透,才是成长的捷径。